From c882cda58678e9509cdb398ea8ed46a4d33b8efd Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 29 Oct 2019 16:30:03 -0400 Subject: [PATCH] properly parse CLI arguments for launch endpoints see: https://github.com/ansible/awx/issues/5093 --- awx/api/metadata.py | 9 +++++++- awxkit/awxkit/cli/custom.py | 39 +++++++++++++++++++++++---------- awxkit/awxkit/cli/options.py | 17 ++++++++++++-- awxkit/test/cli/test_options.py | 6 ++--- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index afdcdf57c0..b9d71e12c5 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -158,9 +158,16 @@ class Metadata(metadata.SimpleMetadata): isinstance(field, JSONField) or isinstance(model_field, JSONField) or isinstance(field, DRFJSONField) or - isinstance(getattr(field, 'model_field', None), JSONField) + isinstance(getattr(field, 'model_field', None), JSONField) or + field.field_name == 'credential_passwords' ): field_info['type'] = 'json' + elif ( + isinstance(field, ManyRelatedField) and + field.field_name == 'credentials' + # launch-time credentials + ): + field_info['type'] = 'list_of_ids' elif isinstance(model_field, BooleanField): field_info['type'] = 'boolean' diff --git a/awxkit/awxkit/cli/custom.py b/awxkit/awxkit/cli/custom.py index 45146b493e..807bd005b7 100644 --- a/awxkit/awxkit/cli/custom.py +++ b/awxkit/awxkit/cli/custom.py @@ -41,13 +41,13 @@ class CustomAction(with_metaclass(CustomActionRegistryMeta)): def perform(self): raise NotImplementedError() - def add_arguments(self, parser): + def add_arguments(self, parser, resource_options_parser): pass class Launchable(object): - def add_arguments(self, parser, with_pk=True): + def add_arguments(self, parser, resource_options_parser, with_pk=True): from .options import pk_or_name if with_pk: parser.choices[self.action].add_argument( @@ -70,6 +70,14 @@ class Launchable(object): help='If set, waits until the launched job finishes.' ) + launch_time_options = self.page.connection.options( + self.page.endpoint + '1/{}/'.format(self.action) + ) + if launch_time_options.ok: + launch_time_options = launch_time_options.json()['actions']['POST'] + resource_options_parser.options['LAUNCH'] = launch_time_options + resource_options_parser.build_query_arguments(self.action, 'LAUNCH') + def monitor(self, response, **kwargs): mon = monitor_workflow if response.type == 'workflow_job' else monitor if kwargs.get('monitor') or kwargs.get('wait'): @@ -84,8 +92,13 @@ class Launchable(object): return response def perform(self, **kwargs): - response = self.page.get().related.get(self.action).post() - self.monitor(response, **kwargs) + monitor_kwargs = { + 'monitor': kwargs.pop('monitor', False), + 'wait': kwargs.pop('wait', False), + 'timeout': kwargs.pop('timeout', False), + } + response = self.page.get().related.get(self.action).post(kwargs) + self.monitor(response, **monitor_kwargs) return response @@ -103,7 +116,7 @@ class ProjectCreate(CustomAction): action = 'create' resource = 'projects' - def add_arguments(self, parser): + def add_arguments(self, parser, resource_options_parser): parser.choices[self.action].add_argument( '--monitor', action='store_true', help=('If set, prints stdout of the project update until ' @@ -139,8 +152,10 @@ class AdhocCommandLaunch(Launchable, CustomAction): action = 'create' resource = 'ad_hoc_commands' - def add_arguments(self, parser): - Launchable.add_arguments(self, parser, with_pk=False) + def add_arguments(self, parser, resource_options_parser): + Launchable.add_arguments( + self, parser, resource_options_parser, with_pk=False + ) def perform(self, **kwargs): monitor_kwargs = { @@ -164,7 +179,7 @@ class HasStdout(object): action = 'stdout' - def add_arguments(self, parser): + def add_arguments(self, parser, resource_options_parser): from .options import pk_or_name parser.choices['stdout'].add_argument( 'id', @@ -204,7 +219,7 @@ class AssociationMixin(object): action = 'associate' - def add_arguments(self, parser): + def add_arguments(self, parser, resource_options_parser): from .options import pk_or_name parser.choices[self.action].add_argument( 'id', @@ -361,7 +376,7 @@ class SettingsList(CustomAction): action = 'list' resource = 'settings' - def add_arguments(self, parser): + def add_arguments(self, parser, resource_options_parser): parser.choices['list'].add_argument( '--slug', help='optional setting category/slug', default='all' ) @@ -385,7 +400,7 @@ class RoleMixin(object): ] roles = {} # this is calculated once - def add_arguments(self, parser): + def add_arguments(self, parser, resource_options_parser): from .options import pk_or_name if not RoleMixin.roles: @@ -510,7 +525,7 @@ class SettingsModify(CustomAction): action = 'modify' resource = 'settings' - def add_arguments(self, parser): + def add_arguments(self, parser, resource_options_parser): options = self.page.__class__( self.page.endpoint + 'all/', self.page.connection ).options() diff --git a/awxkit/awxkit/cli/options.py b/awxkit/awxkit/cli/options.py index c303154827..c198809e6f 100644 --- a/awxkit/awxkit/cli/options.py +++ b/awxkit/awxkit/cli/options.py @@ -20,6 +20,13 @@ UNIQUENESS_RULES = { } +def pk_or_name_list(v2, model_name, value, page=None): + return [ + pk_or_name(v2, model_name, v.strip(), page=page) + for v in value.split(',') + ] + + def pk_or_name(v2, model_name, value, page=None): if isinstance(value, int): return value @@ -193,6 +200,7 @@ class ResourceOptionsParser(object): 'boolean': strtobool, 'id': functools.partial(pk_or_name, self.v2, k), 'json': json_or_yaml, + 'list_of_ids': functools.partial(pk_or_name_list, self.v2, k), }.get(param['type'], str), } meta_map = { @@ -200,6 +208,7 @@ class ResourceOptionsParser(object): 'integer': 'INTEGER', 'boolean': 'BOOLEAN', 'id': 'ID', # foreign key + 'list_of_ids': '[ID, ID, ...]', 'json': 'JSON/YAML', } if param.get('choices', []): @@ -209,13 +218,17 @@ class ResourceOptionsParser(object): # explicitly tell us in OPTIONS all the time) if isinstance(kwargs['choices'][0], int): kwargs['type'] = int - kwargs['choices'] = [str(choice) for choice in kwargs['choices']] + else: + kwargs['choices'] = [str(choice) for choice in kwargs['choices']] elif param['type'] in meta_map: kwargs['metavar'] = meta_map[param['type']] if param['type'] == 'id' and not kwargs.get('help'): kwargs['help'] = 'the ID of the associated {}'.format(k) + if param['type'] == 'list_of_ids': + kwargs['help'] = 'a list of comma-delimited {} to associate (IDs or unique names)'.format(k) + if param['type'] == 'json' and method != 'list': help_parts = [] if kwargs.get('help'): @@ -266,4 +279,4 @@ class ResourceOptionsParser(object): continue if action.action not in self.parser.choices: self.parser.add_parser(action.action, help='') - action(self.page).add_arguments(self.parser) + action(self.page).add_arguments(self.parser, self) diff --git a/awxkit/test/cli/test_options.py b/awxkit/test/cli/test_options.py index 3b10ea677b..3eb5e51f47 100644 --- a/awxkit/test/cli/test_options.py +++ b/awxkit/test/cli/test_options.py @@ -128,17 +128,17 @@ class TestOptions(unittest.TestCase): page = OptionsPage.from_json({ 'actions': { 'POST': { - 'limit': {'type': 'integer'} + 'max_hosts': {'type': 'integer'} }, } }) - options = ResourceOptionsParser(None, page, 'job_templates', self.parser) + options = ResourceOptionsParser(None, page, 'organizations', self.parser) options.build_query_arguments('create', 'POST') assert 'create' in self.parser.choices out = StringIO() self.parser.choices['create'].print_help(out) - assert '--limit INTEGER' in out.getvalue() + assert '--max_hosts INTEGER' in out.getvalue() def test_boolean_argument(self): page = OptionsPage.from_json({