Thomas Maddox 05b4f117bb CLI and client support for get/set/delete of resource vars
Functionality is supported both	for the	Python client:

host = craton.hosts.get(item_id=8)
host.variables.update(x='foo', y={'a': 47, 'b': True}, z='baz')
host.variables.delete("foo", "bar", "baz")

As well	as for the CLI:

craton host-vars-get 1
craton host-vars-set 3 x=true y=47 z=foo/bar w=3.14159
cat <<EOF | craton host-vars-set 13
{
    "glance_default_store": "not-so-swift",
    "neutron_l2_population": false,
    "make_stuff_up": true,
    "some/namespaced/variable": {"a": 1, "b": 2}
}
EOF
craton --format json host-vars-get 13 | jq -C
craton host-vars-delete 13 make_stuff_up
craton host-vars-set 13 x= y=42   # deletes x

This patch implements the basis for supporting this
in other resources as well, however we only address
hosts here as an initial implementation. We will
fast-follow with support in other resources.

Partial-Bug: 1659110
Change-Id: Id30188937518d7103d6f943cf1d038b039dc30cc
2017-03-06 14:27:35 +00:00

177 lines
5.7 KiB
Python

# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Craton CLI helper classes and functions."""
import functools
import json
import os
import sys
from oslo_utils import encodeutils
from oslo_utils import strutils
from cratonclient import exceptions as exc
def arg(*args, **kwargs):
"""Decorator for CLI args.
Example:
>>> @arg("name", help="Name of the new entity.")
... def entity_create(args):
... pass
"""
def _decorator(func):
"""Decorator definition."""
add_arg(func, *args, **kwargs)
return func
return _decorator
def add_arg(func, *args, **kwargs):
"""Bind CLI arguments to a shell.py `do_foo` function."""
if not hasattr(func, 'arguments'):
func.arguments = []
# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in func.arguments:
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.arguments.insert(0, (args, kwargs))
def field_labels_from(attributes):
"""Generate a list of slightly more human readable field names.
This takes the list of fields/attributes on the object and makes them
easier to read.
:param list attributes:
The attribute names to convert. For example, ``["parent_id"]``.
:returns:
List of field names. For example ``["Parent Id"]``
:rtype:
list
Example:
>>> field_labels_from(["id", "name", "cloud_id"])
["Id", "Name", "Cloud Id"]
"""
return [field.replace('_', ' ').title() for field in attributes]
def handle_shell_exception(function):
"""Generic error handler for shell methods."""
@functools.wraps(function)
def wrapper(cc, args):
prop_map = {
"vars": "variables"
}
try:
function(cc, args)
except exc.ClientException as client_exc:
# NOTE(thomasem): All shell methods follow a similar pattern,
# so we can parse this name to get intended parts for
# messaging what went wrong to the end-user.
# The pattern is "do_<resource>_(<prop>_)<verb>", like
# do_project_show or do_project_vars_get, where <prop> is
# not guaranteed to be there, but will afford support for
# actions on some property of the resource.
parsed = function.__name__.split('_')
resource = parsed[1]
verb = parsed[-1]
prop = parsed[2] if len(parsed) > 3 else None
msg = 'Failed to {}'.format(verb)
if prop:
# NOTE(thomasem): Prop would be something like "vars" in
# "do_project_vars_get".
msg = '{} {}'.format(msg, prop_map.get(prop))
# NOTE(thomasem): Append resource and ClientException details
# to error message.
msg = '{} for {} {} due to "{}: {}"'.format(
msg, resource, args.id, client_exc.__class__,
encodeutils.exception_to_unicode(client_exc)
)
raise exc.CommandError(msg)
return wrapper
def env(*args, **kwargs):
"""Return the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
def convert_arg_value(v):
"""Convert different user inputs to normalized type."""
# NOTE(thomasem): Handle case where one wants to escape this value
# conversion using the format key='"value"'
if v.startswith('"'):
return v.strip('"')
lower_v = v.lower()
if strutils.is_int_like(v):
return int(v)
if strutils.is_valid_boolstr(lower_v):
return strutils.bool_from_string(lower_v)
if lower_v == 'null' or lower_v == 'none':
return None
try:
return float(v)
except ValueError:
pass
return v
def variable_updates(variables):
"""Derive list of expected variables for a resource and set them."""
update_variables = {}
delete_variables = set()
for variable in variables:
k, v = variable.split('=', 1)
if v:
update_variables[k] = convert_arg_value(v)
else:
delete_variables.add(k)
if not sys.stdin.isatty():
if update_variables or delete_variables:
raise exc.CommandError("Cannot use variable settings from both "
"stdin and command line arguments. Please "
"choose one or the other.")
update_variables = json.load(sys.stdin)
return (update_variables, list(delete_variables))
def variable_deletes(variables):
"""Delete a list of variables (by key) from a resource."""
if not sys.stdin.isatty():
if variables:
raise exc.CommandError("Cannot use variable settings from both "
"stdin and command line arguments. Please "
"choose one or the other.")
delete_variables = json.load(sys.stdin)
else:
delete_variables = variables
return delete_variables