diff --git a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_client_factory.py b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_client_factory.py index b2076afcff0..2760d02866c 100644 --- a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_client_factory.py +++ b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_client_factory.py @@ -15,7 +15,7 @@ def batch_client_factory(**_): return get_mgmt_service_client(BatchManagementClient) -def batch_data_service_factory(**kwargs): +def batch_data_service_factory(kwargs): account_name = kwargs.pop('account_name', None) account_key = kwargs.pop('account_key', None) account_endpoint = kwargs.pop('account_endpoint', None) diff --git a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_command_type.py b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_command_type.py index aaaab23bb20..a99b21571f7 100644 --- a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_command_type.py +++ b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_command_type.py @@ -12,6 +12,7 @@ FilesCompleter, DirectoriesCompleter) +from . import _validators as validators from azure.cli.core.commands import ( create_command, command_table, @@ -23,13 +24,10 @@ from azure.cli.core.commands._introspection import ( extract_full_summary_from_signature, extract_args_from_signature) -from ._validators import ( - validate_options, - validate_file_destination, - validate_client_parameters, - validate_required_parameter) +_CLASS_NAME = re.compile(r"<(.*?)>") # Strip model name from class docstring +_UNDERSCORE_CASE = re.compile('(?!^)([A-Z]+)') # Convert from CamelCase to underscore_case FLATTEN = 3 # The level of complex object namespace to flatten. IGNORE_OPTIONS = { # Options parameters that should not be exposed as arguments. 'ocp_date', @@ -59,12 +57,129 @@ } +def _load_model(name): + """Load a model class from the SDK in order to inspect for + parameter names and whether they're required. + :param str name: The model class name to load. + :returns: Model class + """ + if name.startswith('azure.'): + namespace = name.split('.') + else: + namespace = ['azure', 'batch', 'models', name] + model = __import__(namespace[0]) + for level in namespace[1:]: + model = getattr(model, level) + return model + + +def _build_prefix(arg, param, path): + """Recursively build a command line argument prefix from the request + parameter object to avoid name conflicts. + :param str arg: Currenct argument name. + :param str param: Original request parameter name. + :param str path: Request parameter namespace. + """ + prefix_list = path.split('.') + if len(prefix_list) == 1: + return arg + resolved_name = prefix_list[0] + "_" + param + if arg == resolved_name: + return arg + for prefix in prefix_list[1:]: + new_name = prefix + "_" + param + if new_name == arg: + return resolved_name + resolved_name = new_name + return resolved_name + + +def find_param_type(model, param): + """Parse the parameter type from the model docstring. + :param class model: Model class. + :param str param: The name of the parameter. + :returns: str + """ + # Search for the :type param_name: in the docstring + pattern = r":type {}:(.*?)\n(\s*:param |\s*:rtype:|\s*:raises:|\"\"\")".format(param) + param_type = re.search(pattern, model.__doc__, re.DOTALL) + return re.sub(r"\n\s*", "", param_type.group(1).strip()) + + +def find_param_help(model, param): + """Parse the parameter help info from the model docstring. + :param class model: Model class. + :param str param: The name of the parameter. + :returns: str + """ + # Search for :param param_name: in the docstring + pattern = r":param {}:(.*?)\n\s*:type ".format(param) + param_doc = re.search(pattern, model.__doc__, re.DOTALL) + return re.sub(r"\n\s*", "", param_doc.group(1).strip()) + + +def find_return_type(model): + """Parse the parameter help info from the model docstring. + :param class model: Model class. + :param str param: The name of the parameter. + :returns: str + """ + # Search for :rtype: in the docstring + pattern = r":rtype: (.*?)\n(\s*:rtype:|\s*:raises:|\"\"\")" + return_type = re.search(pattern, model.__doc__, re.DOTALL) + if return_type: + return re.sub(r"\n\s*", "", return_type.group(1)) + + +def class_name( type_str): + """Extract class name from type docstring. + :param str type_str: Parameter type docstring. + :returns: class name + """ + return _CLASS_NAME.findall(type_str)[0] + + +def operations_name(class_str): + """Convert the operations class name into Python case. + :param str class_str: The class name. + """ + if class_str.endswith('Operations'): + class_str = class_str[:-10] + return _UNDERSCORE_CASE.sub(r'_\1', class_str).lower() + + +def full_name(arg_details): + """Create a full path to the complex object parameter of a + given argument. + :param dict arg_details: The details of the argument. + :returns: str + """ + return ".".join([arg_details['path'], arg_details['root']]) + + +def group_title(path): + """Create a group title from the argument path. + :param str path: The complex object path of the argument. + :returns: str + """ + group_path = path.split('.') + title = ' : '.join(group_path) + for group in group_path: + title = title.replace(group, " ".join([n.title() for n in group.split('_')]), 1) + return title + + +def arg_name(name): + """Convert snake case argument name to a command line name. + :param str name: The argument parameter name. + :returns: str + """ + return "--" + name.replace('_', '-') + + class BatchArgumentTree(object): """Dependency tree parser for arguments of complex objects""" - _class_name = re.compile(r"<(.*?)>") # Strip model name from class docstring - _underscore_case = re.compile('(?!^)([A-Z]+)') # Convert from CamelCase to underscore_case - def __init__(self, validator): self._arg_tree = {} self._request_param = {} @@ -110,7 +225,7 @@ def _parse(self, namespace, path, required): if child_arg in required_args: continue details = self._arg_tree[child_arg] - if '.'.join([details['path'], details['root']]) in dependencies: + if full_name(details) in dependencies: required_args.append(child_arg) elif details['path'] in dependencies: required_args.extend(self._parse(namespace, details['path'], True)) @@ -176,6 +291,10 @@ def compile_args(self): details['options']['action'] = 'store_true' elif details['type'].startswith('['): details['options']['nargs'] = '+' + elif details['type'] in ['iso-8601', 'rfc-1123']: + details['options']['type'] = validators.datetime_format + elif details['type'] == 'duration': + details['options']['type'] = validators.duration_format yield (name, CliCommandArgument(dest=name, **details['options'])) def existing(self, name): @@ -186,86 +305,6 @@ def existing(self, name): """ return name in self._arg_tree - def class_name(self, type_str): - """Extract class name from type docstring. - :param str type_str: Parameter type docstring. - :returns: class name - """ - return self._class_name.findall(type_str)[0] - - def operations_name(self, class_str): - """Convert the operations class name into Python case. - :param str class_str: The class name. - """ - return self._underscore_case.sub(r'_\1', class_str[:-10]).lower() - - @staticmethod - def full_name(arg_details): - """Create a full path to the complex object parameter of a - given argument. - :param dict arg_details: The details of the argument. - :returns: str - """ - return ".".join([arg_details['path'], arg_details['root']]) - - @staticmethod - def group_title(path): - """Create a group title from the argument path. - :param str path: The complex object path of the argument. - :returns: str - """ - group_path = path.split('.') - group_title = ' : '.join(group_path) - for group in group_path: - group_title = group_title.replace(group, - " ".join([n.title() for n in group.split('_')]), 1) - return group_title - - @staticmethod - def arg_name(name): - """Convert snake case argument name to a command line name. - :param str name: The argument parameter name. - :returns: str - """ - return "--" + name.replace('_', '-') - - @staticmethod - def find_param_type(model, param): - """Parse the parameter type from the model docstring. - :param class model: Model class. - :param str param: The name of the parameter. - :returns: str - """ - # Search for the :type param_name: in the docstring - pattern = r":type {}:(.*?)\n(\s*:param |\s*:rtype:|\s*:raises:|\"\"\")".format(param) - param_type = re.search(pattern, model.__doc__, re.DOTALL) - return re.sub(r"\n\s*", "", param_type.group(1).strip()) - - @staticmethod - def find_param_help(model, param): - """Parse the parameter help info from the model docstring. - :param class model: Model class. - :param str param: The name of the parameter. - :returns: str - """ - # Search for :param param_name: in the docstring - pattern = r":param {}:(.*?)\n\s*:type ".format(param) - param_doc = re.search(pattern, model.__doc__, re.DOTALL) - return re.sub(r"\n\s*", "", param_doc.group(1).strip()) - - @staticmethod - def find_return_type(model): - """Parse the parameter help info from the model docstring. - :param class model: Model class. - :param str param: The name of the parameter. - :returns: str - """ - # Search for :rtype: in the docstring - pattern = r":rtype: (.*?)\n(\s*:rtype:|\s*:raises:|\"\"\")" - return_type = re.search(pattern, model.__doc__, re.DOTALL) - if return_type: - return re.sub(r"\n\s*", "", return_type.group(1)) - def parse_mutually_exclusive(self, namespace, required, params): """Validate whether two or more mutually exclusive arguments or argument groups have been set correctly. @@ -274,7 +313,7 @@ def parse_mutually_exclusive(self, namespace, required, params): request properties. """ argtree = self._arg_tree.items() - ex_arg_names = [a for a, v in argtree if self.full_name(v) in params] + ex_arg_names = [a for a, v in argtree if full_name(v) in params] ex_args = [getattr(namespace, a) for a, v in argtree if a in ex_arg_names] ex_args = [x for x in ex_args if x is not None] ex_group_names = [] @@ -282,7 +321,7 @@ def parse_mutually_exclusive(self, namespace, required, params): for arg_group in params: child_args = self._get_children(arg_group) if child_args: - ex_group_names.append(self.group_title(arg_group)) + ex_group_names.append(group_title(arg_group)) if any([getattr(namespace, arg) for arg in child_args]): ex_groups.append(ex_group_names[-1]) @@ -293,7 +332,7 @@ def parse_mutually_exclusive(self, namespace, required, params): message = ("The follow arguments or argument groups are mutually " "exclusive and cannot be combined: \n") if message: - missing = [self.arg_name(n) for n in ex_arg_names] + ex_group_names + missing = [arg_name(n) for n in ex_arg_names] + ex_group_names message += '\n'.join(missing) raise ValueError(message) @@ -312,10 +351,10 @@ def parse(self, namespace): raise ValueError("Cannot access JSON request file: " + namespace.json_file) except ValueError as err: raise ValueError("Invalid JSON file: {}".format(err)) - for name in self._arg_tree: - if getattr(namespace, name): - raise ValueError("--json-file cannot be combined with " + \ - self.arg_name(name)) + other_values = [arg_name(n) for n in self._arg_tree if getattr(namespace, n)] + if other_values: + message = "--json-file cannot be combined with:\n" + raise ValueError(message + '\n'.join(other_values)) return except AttributeError: pass @@ -329,7 +368,7 @@ def parse(self, namespace): missing_args = [n for n in required_args if not getattr(namespace, n)] if missing_args: message = "The following additional arguments are required:\n" - message += "\n".join([self.arg_name(m) for m in missing_args]) + message += "\n".join([arg_name(m) for m in missing_args]) raise ValueError(message) self.done = True @@ -352,7 +391,7 @@ def __init__(self, module_name, name, operation, factory, transform_result, #pyl # The name of the request options parameter self._options_param = self._format_options_name(operation) # The name of the group for options arguments - self._options_group = self.parser.group_title(self._options_param) + self._options_group = group_title(self._options_param) # Arguments used for request options self._options_attrs = [] # The loaded options model to populate for the request @@ -465,30 +504,14 @@ def _build_options(self, kwargs): continue setattr(kwargs[self._options_param], param, param_value) - @staticmethod - def _load_model(name): - """Load a model class from the SDK in order to inspect for - parameter names and whether they're required. - :param str name: The model class name to load. - :returns: Model class - """ - if name.startswith('azure.'): - namespace = name.split('.') - else: - namespace = ['azure', 'batch', 'models', name] - model = __import__(namespace[0]) - for level in namespace[1:]: - model = getattr(model, level) - return model - def _load_options_model(self, func_obj): """Load the request headers options model to gather arguments. :param func func_obj: The request function. """ - option_type = self.parser.find_param_type(func_obj, self._options_param) - option_type = self.parser.class_name(option_type) - self._options_model = self._load_model(option_type)() - self._options_attrs = list(self._options_model.__dict__.keys()) + option_type = find_param_type(func_obj, self._options_param) + option_type = class_name(option_type) + self._options_model = _load_model(option_type)() + self._options_attrs = list(self._options_model.__dict__.keys()) def _format_options_name(self, operation): """Format the name of the request options parameter from the @@ -498,7 +521,7 @@ def _format_options_name(self, operation): """ operation = operation.split('#')[-1] op_class, op_function = operation.split('.') - op_class = self.parser.operations_name(op_class) + op_class = operations_name(op_class) return "{}_{}_options".format(op_class, op_function) def _should_flatten(self, param): @@ -519,51 +542,30 @@ def _get_attrs(self, model, path): conditions.append(model._validation.get(attr, {}).get('readonly')) #pylint: disable=W0212 conditions.append(model._validation.get(attr, {}).get('constant')) #pylint: disable=W0212 conditions.append('.'.join([path, attr]) in self.ignore) - conditions.append(details['type'][0] in ['[', '{']) # TODO: Add support for lists. + conditions.append(details['type'][0] in ['{']) if not any(conditions): yield attr, details - @staticmethod - def _build_prefix(arg, param, path): - """Recursively build a command line argument prefix from the request - parameter object to avoid name conflicts. - :param str arg: Currenct argument name. - :param str param: Original request parameter name. - :param str path: Request parameter namespace. - """ - prefix_list = path.split('.') - if len(prefix_list) == 1: - return arg - resolved_name = prefix_list[0] + "_" + param - if arg == resolved_name: - return arg - for prefix in prefix_list[1:]: - new_name = prefix + "_" + param - if new_name == arg: - return resolved_name - resolved_name = new_name - return resolved_name - def _process_options(self): """Process the request options parameter to expose as arguments.""" for param in [o for o in self._options_attrs if o not in IGNORE_OPTIONS]: + options = {} + options['required'] = False + options['arg_group'] = self._options_group + if param in ['if_modified_since', 'if_unmodified_since']: + options['type'] = validators.datetime_format if param in FLATTEN_OPTIONS: for f_param, f_docstring in FLATTEN_OPTIONS[param].items(): - yield (f_param, CliCommandArgument(f_param, - options_list=[self.parser.arg_name(f_param)], - required=False, - default=None, - help=f_docstring, - validator=validate_options, - arg_group=self._options_group)) + options['default'] = None + options['help'] = f_docstring + options['options_list'] = [arg_name(f_param)] + options['validator'] = validators.validate_options + yield (f_param, CliCommandArgument(f_param, **options)) else: - docstring = self.parser.find_param_help(self._options_model, param) - yield (param, CliCommandArgument(param, - options_list=[self.parser.arg_name(param)], - required=False, - default=getattr(self._options_model, param), - help=docstring, - arg_group=self._options_group)) + options['default'] = getattr(self._options_model, param) + options['help'] = find_param_help(self._options_model, param) + options['options_list'] = [arg_name(param)] + yield (param, CliCommandArgument(param, **options)) def _resolve_conflict(self, arg, param, path, options, typestr, dependencies, conflicting): #pylint:disable=too-many-arguments """Resolve conflicting command line arguments. @@ -578,18 +580,18 @@ def _resolve_conflict(self, arg, param, path, options, typestr, dependencies, co if self.parser.existing(arg): conflicting.append(arg) existing = self.parser.dequeue_argument(arg) - existing['name'] = self._build_prefix(arg, existing['root'], existing['path']) - existing['options']['options_list'] = [self.parser.arg_name(existing['name'])] + existing['name'] = _build_prefix(arg, existing['root'], existing['path']) + existing['options']['options_list'] = [arg_name(existing['name'])] self.parser.queue_argument(**existing) - new = self._build_prefix(arg, param, path) - options['options_list'] = [self.parser.arg_name(new)] + new = _build_prefix(arg, param, path) + options['options_list'] = [arg_name(new)] self._resolve_conflict(new, param, path, options, typestr, dependencies, conflicting) elif arg in conflicting: - new = self._build_prefix(arg, param, path) + new = _build_prefix(arg, param, path) if new in conflicting: self.parser.queue_argument(arg, path, param, options, typestr, dependencies) else: - options['options_list'] = [self.parser.arg_name(new)] + options['options_list'] = [arg_name(new)] self._resolve_conflict(new, param, path, options, typestr, dependencies, conflicting) else: @@ -607,18 +609,35 @@ def _flatten_object(self, path, param_model, conflict_names=[]): #pylint: disabl for param_attr, details in self._get_attrs(param_model, path): options = {} - options['options_list'] = [self.parser.arg_name(param_attr)] + options['options_list'] = [arg_name(param_attr)] options['required'] = False - options['arg_group'] = self.parser.group_title(path) - options['help'] = self.parser.find_param_help(param_model, param_attr) - options['validator'] = lambda ns: validate_required_parameter(ns, self.parser) + options['arg_group'] = group_title(path) + options['help'] = find_param_help(param_model, param_attr) + options['validator'] = \ + lambda ns: validators.validate_required_parameter(ns, self.parser) options['default'] = None # Extract details from signature if details['type'] in BASIC_TYPES: self._resolve_conflict(param_attr, param_attr, path, options, details['type'], required_attrs, conflict_names) + elif details['type'].startswith('['): + # We only expose a list arg if there's a validator for it + # This will fail for 2D arrays - though Batch doesn't have any yet + inner_type = details['type'][1:-1] + if inner_type in BASIC_TYPES: # TODO + continue + else: + inner_type = operations_name(inner_type) + try: + validator = getattr(validators, inner_type + "_format") + options['type'] = validator + self._resolve_conflict( + param_attr, param_attr, path, options, + details['type'], required_attrs, conflict_names) + except AttributeError as err: + continue else: - attr_model = self._load_model(details['type']) + attr_model = _load_model(details['type']) if not hasattr(attr_model, '_attribute_map'): # Must be an enum self._resolve_conflict(param_attr, param_attr, path, options, details['type'], required_attrs, conflict_names) @@ -627,44 +646,43 @@ def _flatten_object(self, path, param_model, conflict_names=[]): #pylint: disabl def _load_transformed_arguments(self, handler): """Load all the command line arguments from the request parameters. - :param str operation: The operation function reference. :param func handler: The operation function. """ self._load_options_model(handler) for arg in extract_args_from_signature(handler): - arg_type = self.parser.find_param_type(handler, arg[0]) + arg_type = find_param_type(handler, arg[0]) if arg[0] == self._options_param: for option_arg in self._process_options(): yield option_arg elif arg_type.startswith(":class:"): # TODO: could add handling for enums - param_type = self.parser.class_name(arg_type) + param_type = class_name(arg_type) self.parser.set_request_param(arg[0], param_type) - param_model = self._load_model(param_type) + param_model = _load_model(param_type) self._flatten_object(arg[0], param_model) for flattened_arg in self.parser.compile_args(): yield flattened_arg param = 'json_file' - json_class = self.parser.class_name(arg_type).split('.')[-1] + json_class = class_name(arg_type).split('.')[-1] docstring = "A file containing the {} object in JSON format, " \ "if this parameter is specified, all other parameters" \ " are ignored.".format(json_class) yield (param, CliCommandArgument(param, - options_list=[self.parser.arg_name(param)], + options_list=[arg_name(param)], required=False, default=None, completer=FilesCompleter(), help=docstring)) elif arg[0] not in self.ignore: yield arg - if self.parser.find_return_type(handler) == 'Generator': + if find_return_type(handler) == 'Generator': param = 'destination' docstring = "The path to the destination file or directory." yield (param, CliCommandArgument(param, - options_list=[self.parser.arg_name(param)], + options_list=[arg_name(param)], required=True, default=None, completer=DirectoriesCompleter(), - validator=validate_file_destination, + validator=validators.validate_file_destination, help=docstring)) @@ -678,7 +696,7 @@ def cli_data_plane_command(name, operation, client_factory, transform=None, #pyl # add parameters required to create a batch client group_name = 'Batch Account' command.cmd.add_argument('account_name', '--account-name', required=False, default=None, - validator=validate_client_parameters, arg_group=group_name, + validator=validators.validate_client_parameters, arg_group=group_name, help='Batch account name. Environment variable: ' 'AZURE_BATCH_ACCOUNT') command.cmd.add_argument('account_key', '--account-key', required=False, default=None, @@ -702,7 +720,7 @@ def cli_custom_data_plane_command(name, operation, client_factory, transform=Non # add parameters required to create a batch client group_name = 'Batch Account' command.add_argument('account_name', '--account-name', required=False, default=None, - validator=validate_client_parameters, arg_group=group_name, + validator=validators.validate_client_parameters, arg_group=group_name, help='Batch account name. Environment variable: AZURE_BATCH_ACCOUNT') command.add_argument('account_key', '--account-key', required=False, default=None, arg_group=group_name, diff --git a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_params.py b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_params.py index 45abfae0aa8..c0eb20c92b9 100644 --- a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_params.py +++ b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_params.py @@ -14,7 +14,7 @@ get_resource_name_completion_list, enum_choice_list) from ._validators import \ - (application_enabled, datetime_type, storage_account_id) + (application_enabled, datetime_format, storage_account_id) # pylint: disable=line-too-long # ARGUMENT DEFINITIONS @@ -38,8 +38,8 @@ register_cli_argument('batch application {}'.format(command), 'account_name', batch_name_type, options_list=('--name', '-n'), validator=application_enabled) register_cli_argument('batch application package create', 'package_file', help='The path of the application package in zip format', completer=FilesCompleter()) -register_cli_argument('batch pool resize', 'if_modified_since', help='Specify this header to perform the operation only if the resource has been modified since the specified date/time.', type=datetime_type, arg_group='Pre-condition') -register_cli_argument('batch pool resize', 'if_unmodified_since', help='Specify this header to perform the operation only if the resource has not been modified since the specified date/time.', type=datetime_type, arg_group='Pre-condition') +register_cli_argument('batch pool resize', 'if_modified_since', help='Specify this header to perform the operation only if the resource has been modified since the specified date/time.', type=datetime_format, arg_group='Pre-condition') +register_cli_argument('batch pool resize', 'if_unmodified_since', help='Specify this header to perform the operation only if the resource has not been modified since the specified date/time.', type=datetime_format, arg_group='Pre-condition') register_cli_argument('batch pool resize', 'if_match', help='An ETag is specified. Specify this header to perform the operation only if the resource\'s ETag is an exact match as specified', arg_group='Pre-condition') register_cli_argument('batch pool resize', 'if_none_match', help='An ETag is specified. Specify this header to perform the operation only if the resource\'s ETag does not match the specified ETag.', arg_group='Pre-condition') register_cli_argument('batch pool resize', 'pool_id', help='The ID of the pool.') diff --git a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_validators.py b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_validators.py index 110a42ee683..ebe19c5b0e2 100644 --- a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_validators.py +++ b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/_validators.py @@ -21,45 +21,63 @@ # TYPES VALIDATORS -def datetime_type(string): - """ Validates UTC datetime in format '%Y-%m-%d\'T\'%H:%M\'Z\''. """ + +def datetime_format(value): + """Validate the correct format of a datetime string and deserialize.""" try: - date_obj = Deserializer.deserialize_iso(string) + datetime_obj = Deserializer.deserialize_iso(value) except DeserializationError: message = "Argument {} is not a valid ISO-8601 datetime format" - raise ValueError(message.format(string)) + raise ValueError(message.format(value)) else: - return date_obj - - -def validate_datetime(namespace, name, parser): - """Validate the correct format of a datetime string and deserialize.""" - date_str = getattr(namespace, name) - if date_str and isinstance(date_str, str): - try: - date_obj = Deserializer.deserialize_iso(date_str) - except DeserializationError: - message = "Argument {} is not a valid ISO-8601 datetime format" - raise ValueError(message.format(name)) - else: - setattr(namespace, name, date_obj) - validate_required_parameter(namespace, parser) + return datetime_obj -def validate_duration(name, value): +def duration_format(value): """Validate the correct format of a timespan string and deserilize.""" try: - value = Deserializer.deserialize_duration(value) + duration_obj = Deserializer.deserialize_duration(value) except DeserializationError: message = "Argument {} is not in a valid ISO-8601 duration format" - raise ValueError(message.format(name)) + raise ValueError(message.format(value)) + else: + return duration_obj + + +def metadata_item_format(value): + """Validate listed metadata arguments""" + try: + data_name, data_value = value.split('=') + except ValueError: + message = ("Incorrectly formatted metadata. " + "Argmuent values should be in the format a=b c=d") + raise ValueError(message) else: - return value + return {'name': data_name, 'value': data_value} -def validate_metadata(namespace): - if namespace.metadata: - namespace.metadata = dict(x.split('=', 1) for x in namespace.metadata) +def environment_setting_format(value): + """Validate listed enviroment settings arguments""" + try: + env_name, env_value = value.split('=') + except ValueError: + message = ("Incorrectly formatted enviroment settings. " + "Argmuent values should be in the format a=b c=d") + raise ValueError(msg) + else: + return {'name': env_name, 'value': env_value} + + +def application_package_reference_format(value): + """Validate listed application package reference arguments""" + app_reference = value.split('#', 1) + package = {'application_id': app_reference[0]} + try: + package['version'] = app_reference[1] + except IndexError: # No specified version - ignore + pass + return package + # COMMAND NAMESPACE VALIDATORS @@ -102,7 +120,7 @@ def validate_options(namespace): start = namespace.start_range end = namespace.end_range except AttributeError: - pass + return else: namespace.ocp_range = None del namespace.start_range @@ -111,19 +129,6 @@ def validate_options(namespace): start = start if start else 0 end = end if end else "" namespace.ocp_range = "bytes={}-{}".format(start, end) - # TODO: Should we also try RFC-1123? - for date_arg in ['if_modified_since', 'if_unmodified_since']: - try: - date_str = getattr(namespace, date_arg) - if date_str and isinstance(date_str, str): - date_obj = Deserializer.deserialize_iso(date_str) - except AttributeError: - pass - except DeserializationError: - message = "Argument {} is not a valid ISO-8601 datetime format" - raise ValueError(message.format(date_arg)) - else: - setattr(namespace, date_arg, date_obj) def validate_file_destination(namespace): @@ -143,8 +148,8 @@ def validate_file_destination(namespace): try: os.mkdir(file_dir) except EnvironmentError as exp: - raise ValueError("Directory {} does not exist, and cannot be created: {}".\ - format(file_dir, exp)) + message = "Directory {} does not exist, and cannot be created: {}" + raise ValueError(message.format(file_dir, exp)) if os.path.isfile(file_path): raise ValueError("File {} already exists.".format(file_path)) namespace.destination = file_path @@ -183,9 +188,16 @@ def validate_client_parameters(namespace): # CUSTOM REQUEST VALIDATORS -def validate_pool_settings(namespace, parser): +def validate_pool_settings(ns, parser): """Custom parsing to enfore that either PaaS or IaaS instances are configured in the add pool request body. """ groups = ['pool.cloud_service_configuration', 'pool.virtual_machine_configuration'] - parser.parse_mutually_exclusive(namespace, True, groups) + parser.parse_mutually_exclusive(ns, True, groups) + + paas_sizes = ['small', 'medium', 'large', 'extralarge'] + if ns.vm_size and ns.vm_size.lower() in paas_sizes and not ns.os_family: + message = ("The selected VM size in incompatible with Virtual Machine Configuration. " + "Please swap for the IaaS equivalent: Standard_A1 (small), Standard_A2 " + "(medium), Standard_A3 (large), or Standard_A4 (extra large).") + raise ValueError(message) \ No newline at end of file diff --git a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/commands.py b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/commands.py index 02fd3aa1cd4..b1291229d4c 100644 --- a/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/commands.py +++ b/src/command_modules/azure-cli-batch/azure/cli/command_modules/batch/commands.py @@ -17,7 +17,7 @@ # pylint: disable=line-too-long # Mgmt Account Operations -factory = lambda args: batch_client_factory(**args).batch_account +factory = lambda args: batch_client_factory(args).batch_account cli_command(__name__, 'batch account list', custom_path.format('list_accounts'), factory) cli_command(__name__, 'batch account show', mgmt_path.format('batch_account', 'BatchAccountOperations.get'), factory) cli_command(__name__, 'batch account create', custom_path.format('create_account'), factory) @@ -28,38 +28,38 @@ cli_command(__name__, 'batch account keys list', mgmt_path.format('batch_account', 'BatchAccountOperations.get_keys'), factory) cli_command(__name__, 'batch account keys renew', mgmt_path.format('batch_account', 'BatchAccountOperations.regenerate_key'), factory) -factory = lambda args: batch_client_factory(**args).application +factory = lambda args: batch_client_factory(args).application cli_command(__name__, 'batch application list', mgmt_path.format('application', 'ApplicationOperations.list'), factory) cli_command(__name__, 'batch application show', mgmt_path.format('application', 'ApplicationOperations.get'), factory) cli_command(__name__, 'batch application create', mgmt_path.format('application', 'ApplicationOperations.create'), factory) cli_command(__name__, 'batch application set', custom_path.format('update_application'), factory) cli_command(__name__, 'batch application delete', mgmt_path.format('application', 'ApplicationOperations.delete'), factory) -factory = lambda args: batch_client_factory(**args).application_package +factory = lambda args: batch_client_factory(args).application_package cli_command(__name__, 'batch application package create', custom_path.format('create_application_package'), factory) cli_command(__name__, 'batch application package delete', mgmt_path.format('application_package', 'ApplicationPackageOperations.delete'), factory) cli_command(__name__, 'batch application package show', mgmt_path.format('application_package', 'ApplicationPackageOperations.get'), factory) cli_command(__name__, 'batch application package activate', mgmt_path.format('application_package', 'ApplicationPackageOperations.activate'), factory) -factory = lambda args: batch_client_factory(**args).location +factory = lambda args: batch_client_factory(args).location cli_command(__name__, 'batch location quotas show', mgmt_path.format('location', 'LocationOperations.get_quotas'), factory) # Data Plane Commands -factory = lambda args: batch_data_service_factory(**args).application +factory = lambda args: batch_data_service_factory(args).application cli_data_plane_command('batch application summary list', data_path.format('application', 'ApplicationOperations.list'), factory) cli_data_plane_command('batch application summary show', data_path.format('application', 'ApplicationOperations.get'), factory) -factory = lambda args: batch_data_service_factory(**args).account +factory = lambda args: batch_data_service_factory(args).account cli_data_plane_command('batch pool node-agent-skus list', data_path.format('account', 'AccountOperations.list_node_agent_skus'), factory) -factory = lambda args: batch_data_service_factory(**args).certificate +factory = lambda args: batch_data_service_factory(args).certificate cli_custom_data_plane_command('batch certificate create', custom_path.format('create_certificate'), factory) cli_custom_data_plane_command('batch certificate delete', custom_path.format('delete_certificate'), factory) cli_data_plane_command('batch certificate show', data_path.format('certificate', 'CertificateOperations.get'), factory) cli_data_plane_command('batch certificate list', data_path.format('certificate', 'CertificateOperations.list'), factory) -factory = lambda args: batch_data_service_factory(**args).pool +factory = lambda args: batch_data_service_factory(args).pool cli_data_plane_command('batch pool usage-metrics list', data_path.format('pool', 'PoolOperations.list_pool_usage_metrics'), factory) cli_data_plane_command('batch pool all-stats show', data_path.format('pool', 'PoolOperations.get_all_pools_lifetime_statistics'), factory) cli_data_plane_command('batch pool create', data_path.format('pool', 'PoolOperations.add'), factory, validator=validate_pool_settings) @@ -75,7 +75,7 @@ cli_data_plane_command('batch pool os upgrade', data_path.format('pool', 'PoolOperations.upgrade_os'), factory) cli_data_plane_command('batch node delete', data_path.format('pool', 'PoolOperations.remove_nodes'), factory) -factory = lambda args: batch_data_service_factory(**args).job +factory = lambda args: batch_data_service_factory(args).job cli_data_plane_command('batch job all-stats show', data_path.format('job', 'JobOperations.get_all_jobs_lifetime_statistics'), factory) cli_data_plane_command('batch job create', data_path.format('job', 'JobOperations.add'), factory) cli_data_plane_command('batch job delete', data_path.format('job', 'JobOperations.delete'), factory) @@ -88,7 +88,7 @@ cli_data_plane_command('batch job stop', data_path.format('job', 'JobOperations.terminate'), factory) cli_data_plane_command('batch job prep-release-status list', data_path.format('job', 'JobOperations.list_preparation_and_release_task_status'), factory) -factory = lambda args: batch_data_service_factory(**args).job_schedule +factory = lambda args: batch_data_service_factory(args).job_schedule cli_data_plane_command('batch job-schedule create', data_path.format('job_schedule', 'JobScheduleOperations.add'), factory) cli_data_plane_command('batch job-schedule delete', data_path.format('job_schedule', 'JobScheduleOperations.delete'), factory) cli_data_plane_command('batch job-schedule show', data_path.format('job_schedule', 'JobScheduleOperations.get'), factory) @@ -99,7 +99,7 @@ cli_data_plane_command('batch job-schedule stop', data_path.format('job_schedule', 'JobScheduleOperations.terminate'), factory) cli_data_plane_command('batch job-schedule list', data_path.format('job_schedule', 'JobScheduleOperations.list'), factory) -factory = lambda args: batch_data_service_factory(**args).task +factory = lambda args: batch_data_service_factory(args).task cli_custom_data_plane_command('batch task create', custom_path.format('create_task'), factory) cli_data_plane_command('batch task list', data_path.format('task', 'TaskOperations.list'), factory) cli_data_plane_command('batch task delete', data_path.format('task', 'TaskOperations.delete'), factory) @@ -109,13 +109,13 @@ cli_data_plane_command('batch task stop', data_path.format('task', 'TaskOperations.terminate'), factory) cli_data_plane_command('batch task subtask list', data_path.format('task', 'TaskOperations.list_subtasks'), factory) -factory = lambda args: batch_data_service_factory(**args).file +factory = lambda args: batch_data_service_factory(args).file cli_data_plane_command('batch task file delete', data_path.format('file', 'FileOperations.delete_from_task'), factory) cli_data_plane_command('batch task file download', data_path.format('file', 'FileOperations.get_from_task'), factory) cli_data_plane_command('batch task file show', data_path.format('file', 'FileOperations.get_node_file_properties_from_task'), factory) cli_data_plane_command('batch task file list', data_path.format('file', 'FileOperations.list_from_task'), factory) -factory = lambda args: batch_data_service_factory(**args).compute_node +factory = lambda args: batch_data_service_factory(args).compute_node cli_data_plane_command('batch node-user create', data_path.format('compute_node', 'ComputeNodeOperations.add_user'), factory) cli_data_plane_command('batch node-user delete', data_path.format('compute_node', 'ComputeNodeOperations.delete_user'), factory) cli_data_plane_command('batch node-user set', data_path.format('compute_node', 'ComputeNodeOperations.update_user'), factory) @@ -128,7 +128,7 @@ cli_data_plane_command('batch node remote-login-settings show', data_path.format('compute_node', 'ComputeNodeOperations.get_remote_login_settings'), factory) cli_data_plane_command('batch node remote-desktop show', data_path.format('compute_node', 'ComputeNodeOperations.get_remote_desktop'), factory) -factory = lambda args: batch_data_service_factory(**args).file +factory = lambda args: batch_data_service_factory(args).file cli_data_plane_command('batch node file delete', data_path.format('file', 'FileOperations.delete_from_compute_node'), factory) cli_data_plane_command('batch node file download', data_path.format('file', 'FileOperations.get_from_compute_node'), factory) cli_data_plane_command('batch node file show', data_path.format('file', 'FileOperations.get_node_file_properties_from_compute_node'), factory)