cli: add ability to specify a name instead of primary key

This commit is contained in:
Ryan Petrello
2019-09-01 08:17:07 -04:00
parent 45f9457abe
commit 4ec5e82023
7 changed files with 120 additions and 29 deletions

View File

@@ -201,7 +201,7 @@ class Page(object):
text = response.text text = response.text
if len(text) > 1024: if len(text) > 1024:
text = text[:1024] + '... <<< Truncated >>> ...' text = text[:1024] + '... <<< Truncated >>> ...'
log.warning( log.debug(
"Unable to parse JSON response ({0.status_code}): {1} - '{2}'".format(response, e, text)) "Unable to parse JSON response ({0.status_code}): {1} - '{2}'".format(response, e, text))
exc_str = "%s (%s) received" % ( exc_str = "%s (%s) received" % (

View File

@@ -62,6 +62,9 @@ def run(stdout=sys.stdout, stderr=sys.stderr, argv=[]):
allow_unicode=True allow_unicode=True
) )
)) ))
elif cli.get_config('format') == 'human':
sys.stdout.write(e.__class__.__name__)
print('')
sys.exit(1) sys.exit(1)
except Exception as e: except Exception as e:
if cli.verbose: if cli.verbose:

View File

@@ -199,7 +199,7 @@ class CLI(object):
subparsers.required = True subparsers.required = True
# parse the action from OPTIONS # parse the action from OPTIONS
parser = ResourceOptionsParser(page, self.resource, subparsers) parser = ResourceOptionsParser(self.v2, page, self.resource, subparsers)
if from_sphinx: if from_sphinx:
# Our Sphinx plugin runs `parse_action` for *every* available # Our Sphinx plugin runs `parse_action` for *every* available
# resource + action in the API so that it can generate usage # resource + action in the API so that it can generate usage

View File

@@ -1,3 +1,5 @@
import functools
from six import with_metaclass from six import with_metaclass
from .stdout import monitor, monitor_workflow from .stdout import monitor, monitor_workflow
@@ -44,8 +46,15 @@ class CustomAction(with_metaclass(CustomActionRegistryMeta)):
class Launchable(object): class Launchable(object):
def add_arguments(self, parser, with_pk=True): def add_arguments(self, parser, with_pk=True):
from .options import pk_or_name
if with_pk: if with_pk:
parser.choices[self.action].add_argument('id', type=int, help='') parser.choices[self.action].add_argument(
'id',
type=functools.partial(
pk_or_name, None, self.resource, page=self.page
),
help=''
)
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 launched job until it finishes.' help='If set, prints stdout of the launched job until it finishes.'
@@ -154,7 +163,14 @@ class HasStdout(object):
action = 'stdout' action = 'stdout'
def add_arguments(self, parser): def add_arguments(self, parser):
parser.choices['stdout'].add_argument('id', type=int, help='') from .options import pk_or_name
parser.choices['stdout'].add_argument(
'id',
type=functools.partial(
pk_or_name, None, self.resource, page=self.page
),
help=''
)
def perform(self): def perform(self):
fmt = 'txt_download' fmt = 'txt_download'

View File

@@ -32,17 +32,15 @@ the output of) a playbook from that repository:
.. code:: bash .. code:: bash
export TOWER_COLOR=f awx projects create --wait \
INVENTORY_ID=$(awx inventory list --name 'Demo Inventory' -f jq --filter '.results[0].id')
PROJECT_ID=$(awx projects create --wait \
--organization 1 --name='Example Project' \ --organization 1 --name='Example Project' \
--scm_type git --scm_url 'https://github.com/ansible/ansible-tower-samples' \ --scm_type git --scm_url 'https://github.com/ansible/ansible-tower-samples' \
-f jq --filter '.id') -f human
TEMPLATE_ID=$(awx job_templates create \ awx job_templates create \
--name='Example Job Template' --project $PROJECT_ID \ --name='Example Job Template' --project 'Example Project' \
--playbook hello_world.yml --inventory $INVENTORY_ID \ --playbook hello_world.yml --inventory 'Demo Inventory' \
-f jq --filter '.id') -f human
awx job_templates launch $TEMPLATE_ID --monitor awx job_templates launch 'Example Job Template' --monitor -f human
Updating a Job Template with Extra Vars Updating a Job Template with Extra Vars
--------------------------------------- ---------------------------------------
@@ -51,3 +49,13 @@ Updating a Job Template with Extra Vars
awx job_templates modify 1 --extra_vars "@vars.yml" awx job_templates modify 1 --extra_vars "@vars.yml"
awx job_templates modify 1 --extra_vars "@vars.json" awx job_templates modify 1 --extra_vars "@vars.json"
Importing an SSH Key
--------------------
.. code:: bash
awx credentials create --credential_type 'Machine' \
--name 'My SSH Key' --user 'alice' \
--inputs "{'username': 'server-login', 'ssh_key_data': '@~/.ssh/id_rsa`}"

View File

@@ -1,23 +1,72 @@
import argparse import argparse
import functools
import json import json
import os
import re
import yaml import yaml
from distutils.util import strtobool from distutils.util import strtobool
from .custom import CustomAction from .custom import CustomAction
from .format import add_output_formatting_arguments from .format import add_output_formatting_arguments
from .resource import DEPRECATED_RESOURCES_REVERSE
def pk_or_name(v2, model_name, value, page=None):
if isinstance(value, int):
return value
if re.match(r'^[\d]+$', value):
return int(value)
identity = 'name'
if not page:
if not hasattr(v2, model_name):
if model_name in DEPRECATED_RESOURCES_REVERSE:
model_name = DEPRECATED_RESOURCES_REVERSE[model_name]
if model_name == 'users':
identity = 'username'
elif model_name == 'instances':
model_name = 'hostname'
if hasattr(v2, model_name):
page = getattr(v2, model_name)
if page:
results = page.get(**{identity: value})
if results.count == 1:
return int(results.results[0].id)
if results.count > 1:
raise argparse.ArgumentTypeError(
'Multiple {0} exist with that {1}. '
'To look up an ID, run:\n'
'awx {0} list --{1} "{2}" -f human'.format(
model_name, identity, value
)
)
raise argparse.ArgumentTypeError(
'Could not find any {0} with that {1}.'.format(
model_name, identity
)
)
return value
class ResourceOptionsParser(object): class ResourceOptionsParser(object):
def __init__(self, page, resource, parser): def __init__(self, v2, page, resource, parser):
"""Used to submit an OPTIONS request to the appropriate endpoint """Used to submit an OPTIONS request to the appropriate endpoint
and apply the appropriate argparse arguments and apply the appropriate argparse arguments
:param v2: a awxkit.api.pages.page.TentativePage instance
:param page: a awxkit.api.pages.page.TentativePage instance :param page: a awxkit.api.pages.page.TentativePage instance
:param resource: a string containing the resource (e.g., jobs) :param resource: a string containing the resource (e.g., jobs)
:param parser: an argparse.ArgumentParser object to append new args to :param parser: an argparse.ArgumentParser object to append new args to
""" """
self.v2 = v2
self.page = page self.page = page
self.resource = resource self.resource = resource
self.parser = parser self.parser = parser
@@ -53,7 +102,11 @@ class ResourceOptionsParser(object):
def build_detail_actions(self): def build_detail_actions(self):
for method in ('get', 'modify', 'delete'): for method in ('get', 'modify', 'delete'):
parser = self.parser.add_parser(method, help='') parser = self.parser.add_parser(method, help='')
self.parser.choices[method].add_argument('id', type=int, help='') self.parser.choices[method].add_argument(
'id',
type=functools.partial(pk_or_name, self.v2, self.resource),
help='the ID (or unique name) of the resource'
)
if method == 'get': if method == 'get':
add_output_formatting_arguments(parser, {}) add_output_formatting_arguments(parser, {})
@@ -81,15 +134,25 @@ class ResourceOptionsParser(object):
def json_or_yaml(v): def json_or_yaml(v):
if v.startswith('@'): if v.startswith('@'):
v = open(v[1:]).read() v = open(os.path.expanduser(v[1:])).read()
try: try:
return json.loads(v) parsed = json.loads(v)
except Exception: except Exception:
try: try:
return yaml.safe_load(v) parsed = yaml.safe_load(v)
except Exception: except Exception:
raise argparse.ArgumentTypeError("{} is not valid JSON or YAML".format(v)) raise argparse.ArgumentTypeError("{} is not valid JSON or YAML".format(v))
for k, v in parsed.items():
# add support for file reading at top-level JSON keys
# (to make things like SSH key data easier to work with)
if v.startswith('@'):
path = os.path.expanduser(v[1:])
if os.path.exists(path):
parsed[k] = open(path).read()
return parsed
def jsonstr(v): def jsonstr(v):
return json.dumps(json_or_yaml(v)) return json.dumps(json_or_yaml(v))
@@ -101,7 +164,7 @@ class ResourceOptionsParser(object):
'field': int, 'field': int,
'integer': int, 'integer': int,
'boolean': strtobool, 'boolean': strtobool,
'id': int, # foreign key 'id': functools.partial(pk_or_name, self.v2, k),
'json': json_or_yaml, 'json': json_or_yaml,
}.get(param['type'], str), }.get(param['type'], str),
} }

View File

@@ -42,7 +42,7 @@ class TestOptions(unittest.TestCase):
'POST': {}, 'POST': {},
} }
}) })
ResourceOptionsParser(page, 'users', self.parser) ResourceOptionsParser(None, page, 'users', self.parser)
assert 'list' in self.parser.choices assert 'list' in self.parser.choices
def test_list_filtering(self): def test_list_filtering(self):
@@ -54,7 +54,7 @@ class TestOptions(unittest.TestCase):
}, },
} }
}) })
options = ResourceOptionsParser(page, 'users', self.parser) options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('list', 'POST') options.build_query_arguments('list', 'POST')
assert 'list' in self.parser.choices assert 'list' in self.parser.choices
@@ -71,7 +71,7 @@ class TestOptions(unittest.TestCase):
}, },
} }
}) })
options = ResourceOptionsParser(page, 'users', self.parser) options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('list', 'POST') options.build_query_arguments('list', 'POST')
assert 'list' in self.parser.choices assert 'list' in self.parser.choices
@@ -90,7 +90,7 @@ class TestOptions(unittest.TestCase):
}, },
} }
}) })
options = ResourceOptionsParser(page, 'users', self.parser) options = ResourceOptionsParser(None, page, 'users', 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
@@ -110,7 +110,7 @@ class TestOptions(unittest.TestCase):
}, },
} }
}) })
options = ResourceOptionsParser(page, 'users', self.parser) options = ResourceOptionsParser(None, page, 'users', 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
@@ -126,7 +126,7 @@ class TestOptions(unittest.TestCase):
}, },
} }
}) })
options = ResourceOptionsParser(page, 'job_templates', self.parser) options = ResourceOptionsParser(None, page, 'job_templates', 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
@@ -142,7 +142,7 @@ class TestOptions(unittest.TestCase):
}, },
} }
}) })
options = ResourceOptionsParser(page, 'users', self.parser) options = ResourceOptionsParser(None, page, 'users', 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
@@ -168,7 +168,7 @@ class TestOptions(unittest.TestCase):
}, },
} }
}) })
options = ResourceOptionsParser(page, 'users', self.parser) options = ResourceOptionsParser(None, page, 'users', 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
@@ -181,13 +181,14 @@ class TestOptions(unittest.TestCase):
page = OptionsPage.from_json({ page = OptionsPage.from_json({
'actions': {'GET': {}, 'POST': {}} 'actions': {'GET': {}, 'POST': {}}
}) })
ResourceOptionsParser(page, 'users', self.parser) ResourceOptionsParser(None, page, 'users', self.parser)
assert method in self.parser.choices assert method in self.parser.choices
out = StringIO() out = StringIO()
self.parser.choices[method].print_help(out) self.parser.choices[method].print_help(out)
assert 'positional arguments:\n id' in out.getvalue() assert 'positional arguments:\n id' in out.getvalue()
class TestSettingsOptions(unittest.TestCase): class TestSettingsOptions(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -203,7 +204,7 @@ class TestSettingsOptions(unittest.TestCase):
} }
}) })
page.endpoint = '/settings/all/' page.endpoint = '/settings/all/'
ResourceOptionsParser(page, 'settings', self.parser) ResourceOptionsParser(None, page, 'settings', self.parser)
assert 'list' in self.parser.choices assert 'list' in self.parser.choices
assert 'modify' in self.parser.choices assert 'modify' in self.parser.choices