Skip to content

Commit

Permalink
cli: add ability to specify a name instead of primary key
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanpetrello committed Sep 3, 2019
1 parent 45f9457 commit 4ec5e82
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 29 deletions.
2 changes: 1 addition & 1 deletion awxkit/awxkit/api/pages/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def page_identity(self, response, request_json=None):
text = response.text
if len(text) > 1024:
text = text[:1024] + '... <<< Truncated >>> ...'
log.warning(
log.debug(
"Unable to parse JSON response ({0.status_code}): {1} - '{2}'".format(response, e, text))

exc_str = "%s (%s) received" % (
Expand Down
3 changes: 3 additions & 0 deletions awxkit/awxkit/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def run(stdout=sys.stdout, stderr=sys.stderr, argv=[]):
allow_unicode=True
)
))
elif cli.get_config('format') == 'human':
sys.stdout.write(e.__class__.__name__)
print('')
sys.exit(1)
except Exception as e:
if cli.verbose:
Expand Down
2 changes: 1 addition & 1 deletion awxkit/awxkit/cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def parse_action(self, page, from_sphinx=False):
subparsers.required = True

# parse the action from OPTIONS
parser = ResourceOptionsParser(page, self.resource, subparsers)
parser = ResourceOptionsParser(self.v2, page, self.resource, subparsers)
if from_sphinx:
# Our Sphinx plugin runs `parse_action` for *every* available
# resource + action in the API so that it can generate usage
Expand Down
20 changes: 18 additions & 2 deletions awxkit/awxkit/cli/custom.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import functools

from six import with_metaclass

from .stdout import monitor, monitor_workflow
Expand Down Expand Up @@ -44,8 +46,15 @@ def add_arguments(self, parser):
class Launchable(object):

def add_arguments(self, parser, with_pk=True):
from .options import pk_or_name
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(
'--monitor', action='store_true',
help='If set, prints stdout of the launched job until it finishes.'
Expand Down Expand Up @@ -154,7 +163,14 @@ class HasStdout(object):
action = 'stdout'

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):
fmt = 'txt_download'
Expand Down
26 changes: 17 additions & 9 deletions awxkit/awxkit/cli/docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,15 @@ the output of) a playbook from that repository:

.. code:: bash
export TOWER_COLOR=f
INVENTORY_ID=$(awx inventory list --name 'Demo Inventory' -f jq --filter '.results[0].id')
PROJECT_ID=$(awx projects create --wait \
awx projects create --wait \
--organization 1 --name='Example Project' \
--scm_type git --scm_url 'https://github.com/ansible/ansible-tower-samples' \
-f jq --filter '.id')
TEMPLATE_ID=$(awx job_templates create \
--name='Example Job Template' --project $PROJECT_ID \
--playbook hello_world.yml --inventory $INVENTORY_ID \
-f jq --filter '.id')
awx job_templates launch $TEMPLATE_ID --monitor
-f human
awx job_templates create \
--name='Example Job Template' --project 'Example Project' \
--playbook hello_world.yml --inventory 'Demo Inventory' \
-f human
awx job_templates launch 'Example Job Template' --monitor -f human
Updating a Job Template with Extra Vars
---------------------------------------
Expand All @@ -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.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`}"
75 changes: 69 additions & 6 deletions awxkit/awxkit/cli/options.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,72 @@
import argparse
import functools
import json
import os
import re
import yaml

from distutils.util import strtobool

from .custom import CustomAction
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):

def __init__(self, page, resource, parser):
def __init__(self, v2, page, resource, parser):
"""Used to submit an OPTIONS request to the appropriate endpoint
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 resource: a string containing the resource (e.g., jobs)
:param parser: an argparse.ArgumentParser object to append new args to
"""
self.v2 = v2
self.page = page
self.resource = resource
self.parser = parser
Expand Down Expand Up @@ -53,7 +102,11 @@ def build_list_actions(self):
def build_detail_actions(self):
for method in ('get', 'modify', 'delete'):
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':
add_output_formatting_arguments(parser, {})

Expand Down Expand Up @@ -81,15 +134,25 @@ def build_query_arguments(self, method, http_method):

def json_or_yaml(v):
if v.startswith('@'):
v = open(v[1:]).read()
v = open(os.path.expanduser(v[1:])).read()
try:
return json.loads(v)
parsed = json.loads(v)
except Exception:
try:
return yaml.safe_load(v)
parsed = yaml.safe_load(v)
except Exception:
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):
return json.dumps(json_or_yaml(v))

Expand All @@ -101,7 +164,7 @@ def jsonstr(v):
'field': int,
'integer': int,
'boolean': strtobool,
'id': int, # foreign key
'id': functools.partial(pk_or_name, self.v2, k),
'json': json_or_yaml,
}.get(param['type'], str),
}
Expand Down
21 changes: 11 additions & 10 deletions awxkit/test/cli/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_list(self):
'POST': {},
}
})
ResourceOptionsParser(page, 'users', self.parser)
ResourceOptionsParser(None, page, 'users', self.parser)
assert 'list' in self.parser.choices

def test_list_filtering(self):
Expand All @@ -54,7 +54,7 @@ def test_list_filtering(self):
},
}
})
options = ResourceOptionsParser(page, 'users', self.parser)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('list', 'POST')
assert 'list' in self.parser.choices

Expand All @@ -71,7 +71,7 @@ def test_list_not_filterable(self):
},
}
})
options = ResourceOptionsParser(page, 'users', self.parser)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('list', 'POST')
assert 'list' in self.parser.choices

Expand All @@ -90,7 +90,7 @@ def test_creation_optional_argument(self):
},
}
})
options = ResourceOptionsParser(page, 'users', self.parser)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices

Expand All @@ -110,7 +110,7 @@ def test_creation_required_argument(self):
},
}
})
options = ResourceOptionsParser(page, 'users', self.parser)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices

Expand All @@ -126,7 +126,7 @@ def test_integer_argument(self):
},
}
})
options = ResourceOptionsParser(page, 'job_templates', self.parser)
options = ResourceOptionsParser(None, page, 'job_templates', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices

Expand All @@ -142,7 +142,7 @@ def test_boolean_argument(self):
},
}
})
options = ResourceOptionsParser(page, 'users', self.parser)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices

Expand All @@ -168,7 +168,7 @@ def test_choices(self):
},
}
})
options = ResourceOptionsParser(page, 'users', self.parser)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices

Expand All @@ -181,13 +181,14 @@ def test_actions_with_primary_key(self):
page = OptionsPage.from_json({
'actions': {'GET': {}, 'POST': {}}
})
ResourceOptionsParser(page, 'users', self.parser)
ResourceOptionsParser(None, page, 'users', self.parser)
assert method in self.parser.choices

out = StringIO()
self.parser.choices[method].print_help(out)
assert 'positional arguments:\n id' in out.getvalue()


class TestSettingsOptions(unittest.TestCase):

def setUp(self):
Expand All @@ -203,7 +204,7 @@ def test_list(self):
}
})
page.endpoint = '/settings/all/'
ResourceOptionsParser(page, 'settings', self.parser)
ResourceOptionsParser(None, page, 'settings', self.parser)
assert 'list' in self.parser.choices
assert 'modify' in self.parser.choices

Expand Down

0 comments on commit 4ec5e82

Please sign in to comment.