properly parse CLI arguments for launch endpoints

see: https://github.com/ansible/awx/issues/5093
This commit is contained in:
Ryan Petrello
2019-10-29 16:30:03 -04:00
parent 8cb7b388dc
commit c882cda586
4 changed files with 53 additions and 18 deletions

View File

@@ -158,9 +158,16 @@ class Metadata(metadata.SimpleMetadata):
isinstance(field, JSONField) or isinstance(field, JSONField) or
isinstance(model_field, JSONField) or isinstance(model_field, JSONField) or
isinstance(field, DRFJSONField) 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' 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): elif isinstance(model_field, BooleanField):
field_info['type'] = 'boolean' field_info['type'] = 'boolean'

View File

@@ -41,13 +41,13 @@ class CustomAction(with_metaclass(CustomActionRegistryMeta)):
def perform(self): def perform(self):
raise NotImplementedError() raise NotImplementedError()
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
pass pass
class Launchable(object): 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 from .options import pk_or_name
if with_pk: if with_pk:
parser.choices[self.action].add_argument( parser.choices[self.action].add_argument(
@@ -70,6 +70,14 @@ class Launchable(object):
help='If set, waits until the launched job finishes.' 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): def monitor(self, response, **kwargs):
mon = monitor_workflow if response.type == 'workflow_job' else monitor mon = monitor_workflow if response.type == 'workflow_job' else monitor
if kwargs.get('monitor') or kwargs.get('wait'): if kwargs.get('monitor') or kwargs.get('wait'):
@@ -84,8 +92,13 @@ class Launchable(object):
return response return response
def perform(self, **kwargs): def perform(self, **kwargs):
response = self.page.get().related.get(self.action).post() monitor_kwargs = {
self.monitor(response, **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 return response
@@ -103,7 +116,7 @@ class ProjectCreate(CustomAction):
action = 'create' action = 'create'
resource = 'projects' resource = 'projects'
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
parser.choices[self.action].add_argument( parser.choices[self.action].add_argument(
'--monitor', action='store_true', '--monitor', action='store_true',
help=('If set, prints stdout of the project update until ' help=('If set, prints stdout of the project update until '
@@ -139,8 +152,10 @@ class AdhocCommandLaunch(Launchable, CustomAction):
action = 'create' action = 'create'
resource = 'ad_hoc_commands' resource = 'ad_hoc_commands'
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
Launchable.add_arguments(self, parser, with_pk=False) Launchable.add_arguments(
self, parser, resource_options_parser, with_pk=False
)
def perform(self, **kwargs): def perform(self, **kwargs):
monitor_kwargs = { monitor_kwargs = {
@@ -164,7 +179,7 @@ class HasStdout(object):
action = 'stdout' action = 'stdout'
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
from .options import pk_or_name from .options import pk_or_name
parser.choices['stdout'].add_argument( parser.choices['stdout'].add_argument(
'id', 'id',
@@ -204,7 +219,7 @@ class AssociationMixin(object):
action = 'associate' action = 'associate'
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
from .options import pk_or_name from .options import pk_or_name
parser.choices[self.action].add_argument( parser.choices[self.action].add_argument(
'id', 'id',
@@ -361,7 +376,7 @@ class SettingsList(CustomAction):
action = 'list' action = 'list'
resource = 'settings' resource = 'settings'
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
parser.choices['list'].add_argument( parser.choices['list'].add_argument(
'--slug', help='optional setting category/slug', default='all' '--slug', help='optional setting category/slug', default='all'
) )
@@ -385,7 +400,7 @@ class RoleMixin(object):
] ]
roles = {} # this is calculated once roles = {} # this is calculated once
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
from .options import pk_or_name from .options import pk_or_name
if not RoleMixin.roles: if not RoleMixin.roles:
@@ -510,7 +525,7 @@ class SettingsModify(CustomAction):
action = 'modify' action = 'modify'
resource = 'settings' resource = 'settings'
def add_arguments(self, parser): def add_arguments(self, parser, resource_options_parser):
options = self.page.__class__( options = self.page.__class__(
self.page.endpoint + 'all/', self.page.connection self.page.endpoint + 'all/', self.page.connection
).options() ).options()

View File

@@ -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): def pk_or_name(v2, model_name, value, page=None):
if isinstance(value, int): if isinstance(value, int):
return value return value
@@ -193,6 +200,7 @@ class ResourceOptionsParser(object):
'boolean': strtobool, 'boolean': strtobool,
'id': functools.partial(pk_or_name, self.v2, k), 'id': functools.partial(pk_or_name, self.v2, k),
'json': json_or_yaml, 'json': json_or_yaml,
'list_of_ids': functools.partial(pk_or_name_list, self.v2, k),
}.get(param['type'], str), }.get(param['type'], str),
} }
meta_map = { meta_map = {
@@ -200,6 +208,7 @@ class ResourceOptionsParser(object):
'integer': 'INTEGER', 'integer': 'INTEGER',
'boolean': 'BOOLEAN', 'boolean': 'BOOLEAN',
'id': 'ID', # foreign key 'id': 'ID', # foreign key
'list_of_ids': '[ID, ID, ...]',
'json': 'JSON/YAML', 'json': 'JSON/YAML',
} }
if param.get('choices', []): if param.get('choices', []):
@@ -209,13 +218,17 @@ class ResourceOptionsParser(object):
# explicitly tell us in OPTIONS all the time) # explicitly tell us in OPTIONS all the time)
if isinstance(kwargs['choices'][0], int): if isinstance(kwargs['choices'][0], int):
kwargs['type'] = 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: elif param['type'] in meta_map:
kwargs['metavar'] = meta_map[param['type']] kwargs['metavar'] = meta_map[param['type']]
if param['type'] == 'id' and not kwargs.get('help'): if param['type'] == 'id' and not kwargs.get('help'):
kwargs['help'] = 'the ID of the associated {}'.format(k) 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': if param['type'] == 'json' and method != 'list':
help_parts = [] help_parts = []
if kwargs.get('help'): if kwargs.get('help'):
@@ -266,4 +279,4 @@ class ResourceOptionsParser(object):
continue continue
if action.action not in self.parser.choices: if action.action not in self.parser.choices:
self.parser.add_parser(action.action, help='') self.parser.add_parser(action.action, help='')
action(self.page).add_arguments(self.parser) action(self.page).add_arguments(self.parser, self)

View File

@@ -128,17 +128,17 @@ class TestOptions(unittest.TestCase):
page = OptionsPage.from_json({ page = OptionsPage.from_json({
'actions': { 'actions': {
'POST': { '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') options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices assert 'create' in self.parser.choices
out = StringIO() out = StringIO()
self.parser.choices['create'].print_help(out) 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): def test_boolean_argument(self):
page = OptionsPage.from_json({ page = OptionsPage.from_json({