mirror of
https://github.com/ansible/awx.git
synced 2026-02-19 20:20:06 -03:30
cli: add ability to specify a name instead of primary key
This commit is contained in:
@@ -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" % (
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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`}"
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user