Merge "CLI and client support for get/set/delete of resource vars"
This commit is contained in:
		@@ -12,7 +12,15 @@
 | 
			
		||||
# 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):
 | 
			
		||||
@@ -66,6 +74,43 @@ def field_labels_from(attributes):
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
@@ -76,3 +121,56 @@ def env(*args, **kwargs):
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ from oslo_utils import strutils
 | 
			
		||||
class CRUDClient(object):
 | 
			
		||||
    """Class that handles the basic create, read, upload, delete workflow."""
 | 
			
		||||
 | 
			
		||||
    key = None
 | 
			
		||||
    key = ""
 | 
			
		||||
    base_path = None
 | 
			
		||||
    resource_class = None
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +45,7 @@ class CRUDClient(object):
 | 
			
		||||
 | 
			
		||||
            base_path = '/regions'
 | 
			
		||||
 | 
			
		||||
        And it's ``key``, e.g.,
 | 
			
		||||
        And its ``key``, e.g.,
 | 
			
		||||
 | 
			
		||||
        .. code-block:: python
 | 
			
		||||
 | 
			
		||||
@@ -128,16 +128,26 @@ class CRUDClient(object):
 | 
			
		||||
        response = self.session.put(url, json=kwargs)
 | 
			
		||||
        return self.resource_class(self, response.json(), loaded=True)
 | 
			
		||||
 | 
			
		||||
    def delete(self, item_id=None, skip_merge=True, **kwargs):
 | 
			
		||||
    def delete(self, item_id=None, skip_merge=True, json=None, **kwargs):
 | 
			
		||||
        """Delete the item based on the keyword arguments provided."""
 | 
			
		||||
        self.merge_request_arguments(kwargs, skip_merge)
 | 
			
		||||
        kwargs.setdefault(self.key + '_id', item_id)
 | 
			
		||||
        url = self.build_url(path_arguments=kwargs)
 | 
			
		||||
        response = self.session.delete(url, params=kwargs)
 | 
			
		||||
        response = self.session.delete(url, params=kwargs, json=json)
 | 
			
		||||
        if 200 <= response.status_code < 300:
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        """Return string representation of a Variable."""
 | 
			
		||||
        return '%(class)s(%(session)s, %(url)s, %(extra_request_kwargs)s)' % \
 | 
			
		||||
            {
 | 
			
		||||
                "class": self.__class__.__name__,
 | 
			
		||||
                "session": self.session,
 | 
			
		||||
                "url": self.url,
 | 
			
		||||
                "extra_request_kwargs": self.extra_request_kwargs,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE(sigmavirus24): Credit for this Resource object goes to the
 | 
			
		||||
# keystoneclient developers and contributors.
 | 
			
		||||
@@ -149,6 +159,7 @@ class Resource(object):
 | 
			
		||||
 | 
			
		||||
    HUMAN_ID = False
 | 
			
		||||
    NAME_ATTR = 'name'
 | 
			
		||||
    subresource_managers = {}
 | 
			
		||||
 | 
			
		||||
    def __init__(self, manager, info, loaded=False):
 | 
			
		||||
        """Populate and bind to a manager.
 | 
			
		||||
@@ -162,6 +173,15 @@ class Resource(object):
 | 
			
		||||
        self._add_details(info)
 | 
			
		||||
        self._loaded = loaded
 | 
			
		||||
 | 
			
		||||
        session = self.manager.session
 | 
			
		||||
        subresource_base_url = self.manager.build_url(
 | 
			
		||||
            {"{0}_id".format(self.manager.key): self.id}
 | 
			
		||||
        )
 | 
			
		||||
        for attribute, cls in self.subresource_managers.items():
 | 
			
		||||
            setattr(self, attribute,
 | 
			
		||||
                    cls(session, subresource_base_url,
 | 
			
		||||
                        **self.manager.extra_request_kwargs))
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        """Return string representation of resource attributes."""
 | 
			
		||||
        reprkeys = sorted(k
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@
 | 
			
		||||
# License for the specific language governing permissions and limitations
 | 
			
		||||
# under the License.
 | 
			
		||||
"""Base class implementation for formatting plugins."""
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Formatter(object):
 | 
			
		||||
@@ -66,7 +65,8 @@ class Formatter(object):
 | 
			
		||||
            we will not know how to handle it. In that case, we will raise a
 | 
			
		||||
            ValueError.
 | 
			
		||||
        """
 | 
			
		||||
        if isinstance(item_to_format, crud.Resource):
 | 
			
		||||
        to_dict = getattr(item_to_format, 'to_dict', None)
 | 
			
		||||
        if to_dict is not None:
 | 
			
		||||
            self.handle_instance(item_to_format)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,9 @@
 | 
			
		||||
"""Hosts resource and resource shell wrapper."""
 | 
			
		||||
from __future__ import print_function
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from cratonclient.common import cliutils
 | 
			
		||||
from cratonclient import exceptions as exc
 | 
			
		||||
 | 
			
		||||
@@ -279,3 +282,60 @@ def do_host_delete(cc, args):
 | 
			
		||||
    else:
 | 
			
		||||
        print("Host {0} was {1} deleted.".
 | 
			
		||||
              format(args.id, 'successfully' if response else 'not'))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cliutils.arg('id',
 | 
			
		||||
              metavar='<host>',
 | 
			
		||||
              type=int,
 | 
			
		||||
              help='ID or name of the host.')
 | 
			
		||||
@cliutils.handle_shell_exception
 | 
			
		||||
def do_host_vars_get(cc, args):
 | 
			
		||||
    """Get variables for a host."""
 | 
			
		||||
    variables = cc.hosts.get(args.id).variables.get()
 | 
			
		||||
    formatter = args.formatter.configure(dict_property="Variable", wrap=72)
 | 
			
		||||
    formatter.handle(variables)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cliutils.arg('id',
 | 
			
		||||
              metavar='<host>',
 | 
			
		||||
              type=int,
 | 
			
		||||
              help='ID of the host.')
 | 
			
		||||
@cliutils.arg('variables', nargs=argparse.REMAINDER)
 | 
			
		||||
@cliutils.handle_shell_exception
 | 
			
		||||
def do_host_vars_set(cc, args):
 | 
			
		||||
    """Set variables for a host."""
 | 
			
		||||
    host_id = args.id
 | 
			
		||||
    if not args.variables and sys.stdin.isatty():
 | 
			
		||||
        raise exc.CommandError(
 | 
			
		||||
            'Nothing to update... Please specify variables to set in the '
 | 
			
		||||
            'following format: "key=value". You may also specify variables to '
 | 
			
		||||
            'delete by key using the format: "key="'
 | 
			
		||||
        )
 | 
			
		||||
    adds, deletes = cliutils.variable_updates(args.variables)
 | 
			
		||||
    variables = cc.hosts.get(host_id).variables
 | 
			
		||||
    if deletes:
 | 
			
		||||
        variables.delete(*deletes)
 | 
			
		||||
    variables.update(**adds)
 | 
			
		||||
    formatter = args.formatter.configure(wrap=72, dict_property="Variable")
 | 
			
		||||
    formatter.handle(variables.get())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cliutils.arg('id',
 | 
			
		||||
              metavar='<host>',
 | 
			
		||||
              type=int,
 | 
			
		||||
              help='ID of the host.')
 | 
			
		||||
@cliutils.arg('variables', nargs=argparse.REMAINDER)
 | 
			
		||||
@cliutils.handle_shell_exception
 | 
			
		||||
def do_host_vars_delete(cc, args):
 | 
			
		||||
    """Delete variables for a host by key."""
 | 
			
		||||
    host_id = args.id
 | 
			
		||||
    if not args.variables and sys.stdin.isatty():
 | 
			
		||||
        raise exc.CommandError(
 | 
			
		||||
            'Nothing to delete... Please specify variables to delete by '
 | 
			
		||||
            'listing the keys you wish to delete separated by spaces.'
 | 
			
		||||
        )
 | 
			
		||||
    deletes = cliutils.variable_deletes(args.variables)
 | 
			
		||||
    variables = cc.hosts.get(host_id).variables
 | 
			
		||||
    response = variables.delete(*deletes)
 | 
			
		||||
    print("Variables {0} deleted.".
 | 
			
		||||
          format('successfully' if response else 'not'))
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ from cratonclient import exceptions as exc
 | 
			
		||||
from cratonclient.shell.v1 import hosts_shell
 | 
			
		||||
from cratonclient.tests.integration.shell import base
 | 
			
		||||
from cratonclient.v1 import hosts
 | 
			
		||||
from cratonclient.v1 import variables
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestHostsShell(base.ShellTestCase):
 | 
			
		||||
@@ -389,3 +390,101 @@ class TestHostsShell(base.ShellTestCase):
 | 
			
		||||
        test_args = Namespace(id=1, region=1)
 | 
			
		||||
        hosts_shell.do_host_delete(client, test_args)
 | 
			
		||||
        mock_delete.assert_called_once_with(vars(test_args)['id'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestHostsVarsShell(base.ShellTestCase):
 | 
			
		||||
    """Test Host Variable shell calls."""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        """Basic set up for all tests in this suite."""
 | 
			
		||||
        super(TestHostsVarsShell, self).setUp()
 | 
			
		||||
        self.host_url = 'http://127.0.0.1/v1/hosts/1'
 | 
			
		||||
        self.variables_url = '{}/variables'.format(self.host_url)
 | 
			
		||||
        self.test_args = Namespace(id=1, formatter=mock.Mock())
 | 
			
		||||
 | 
			
		||||
        # NOTE(thomasem): Make all calls seem like they come from CLI args
 | 
			
		||||
        self.stdin_patcher = \
 | 
			
		||||
            mock.patch('cratonclient.common.cliutils.sys.stdin')
 | 
			
		||||
        self.patched_stdin = self.stdin_patcher.start()
 | 
			
		||||
        self.patched_stdin.isatty.return_value = True
 | 
			
		||||
 | 
			
		||||
        # NOTE(thomasem): Mock out a session object to assert resulting API
 | 
			
		||||
        # calls
 | 
			
		||||
        self.mock_session = mock.Mock()
 | 
			
		||||
        self.mock_get_response = self.mock_session.get.return_value
 | 
			
		||||
        self.mock_put_response = self.mock_session.put.return_value
 | 
			
		||||
        self.mock_delete_response = self.mock_session.delete.return_value
 | 
			
		||||
        self.mock_delete_response.status_code = 204
 | 
			
		||||
 | 
			
		||||
        # NOTE(thomasem): Mock out a client to assert craton Python API calls
 | 
			
		||||
        self.client = mock.Mock()
 | 
			
		||||
        self.mock_host_resource = self.client.hosts.get.return_value
 | 
			
		||||
        self.mock_host_resource.variables = variables.VariableManager(
 | 
			
		||||
            self.mock_session, self.host_url
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        """Clean up between tests."""
 | 
			
		||||
        super(TestHostsVarsShell, self).tearDown()
 | 
			
		||||
        self.stdin_patcher.stop()
 | 
			
		||||
 | 
			
		||||
    def test_do_host_vars_get_gets_correct_host(self):
 | 
			
		||||
        """Assert the proper host is retrieved when calling get."""
 | 
			
		||||
        self.mock_get_response.json.return_value = \
 | 
			
		||||
            {"variables": {"foo": "bar"}}
 | 
			
		||||
        hosts_shell.do_host_vars_get(self.client, self.test_args)
 | 
			
		||||
        self.client.hosts.get.assert_called_once_with(
 | 
			
		||||
            vars(self.test_args)['id'])
 | 
			
		||||
 | 
			
		||||
    def test_do_host_vars_delete_gets_correct_host(self):
 | 
			
		||||
        """Assert the proper host is retrieved when calling delete."""
 | 
			
		||||
        self.test_args.variables = ['foo', 'bar']
 | 
			
		||||
        hosts_shell.do_host_vars_delete(self.client, self.test_args)
 | 
			
		||||
        self.client.hosts.get.assert_called_once_with(
 | 
			
		||||
            vars(self.test_args)['id'])
 | 
			
		||||
 | 
			
		||||
    def test_do_host_vars_update_gets_correct_host(self):
 | 
			
		||||
        """Assert the proper host is retrieved when calling update."""
 | 
			
		||||
        self.test_args.variables = ['foo=', 'bar=']
 | 
			
		||||
        mock_resp_json = {"variables": {"foo": "bar"}}
 | 
			
		||||
        self.mock_get_response.json.return_value = mock_resp_json
 | 
			
		||||
        self.mock_put_response.json.return_value = mock_resp_json
 | 
			
		||||
 | 
			
		||||
        hosts_shell.do_host_vars_set(self.client, self.test_args)
 | 
			
		||||
        self.client.hosts.get.assert_called_once_with(
 | 
			
		||||
            vars(self.test_args)['id'])
 | 
			
		||||
 | 
			
		||||
    def test_do_host_vars_get_calls_session_get(self):
 | 
			
		||||
        """Assert the proper host is retrieved when calling get."""
 | 
			
		||||
        self.mock_get_response.json.return_value = \
 | 
			
		||||
            {"variables": {"foo": "bar"}}
 | 
			
		||||
        hosts_shell.do_host_vars_get(self.client, self.test_args)
 | 
			
		||||
        self.mock_session.get.assert_called_once_with(self.variables_url)
 | 
			
		||||
 | 
			
		||||
    def test_do_host_vars_delete_calls_session_delete(self):
 | 
			
		||||
        """Verify that do host-vars-delete calls expected session.delete."""
 | 
			
		||||
        self.test_args.variables = ['foo', 'bar']
 | 
			
		||||
        hosts_shell.do_host_vars_delete(self.client, self.test_args)
 | 
			
		||||
        self.mock_session.delete.assert_called_once_with(
 | 
			
		||||
            self.variables_url,
 | 
			
		||||
            json=('foo', 'bar'),
 | 
			
		||||
            params={},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_do_host_vars_update_calls_session_put(self):
 | 
			
		||||
        """Verify that do host-vars-delete calls expected session.delete."""
 | 
			
		||||
        self.test_args.variables = ['foo=baz', 'bar=boo', 'test=']
 | 
			
		||||
        mock_resp_json = {"variables": {"foo": "bar"}}
 | 
			
		||||
        self.mock_get_response.json.return_value = mock_resp_json
 | 
			
		||||
        self.mock_put_response.json.return_value = mock_resp_json
 | 
			
		||||
 | 
			
		||||
        hosts_shell.do_host_vars_set(self.client, self.test_args)
 | 
			
		||||
        self.mock_session.delete.assert_called_once_with(
 | 
			
		||||
            self.variables_url,
 | 
			
		||||
            json=('test',),
 | 
			
		||||
            params={},
 | 
			
		||||
        )
 | 
			
		||||
        self.mock_session.put.assert_called_once_with(
 | 
			
		||||
            self.variables_url,
 | 
			
		||||
            json={'foo': 'baz', 'bar': 'boo'}
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,7 @@ class TestCrudIntegration(base.TestCase):
 | 
			
		||||
        self.session.request.assert_called_once_with(
 | 
			
		||||
            method='DELETE',
 | 
			
		||||
            url='http://example.com/v1/test/1',
 | 
			
		||||
            json=None,
 | 
			
		||||
            params={},
 | 
			
		||||
            endpoint_filter={'service_type': 'fleet_management'},
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								cratonclient/tests/unit/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cratonclient/tests/unit/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
"""Unit tests for cratonclient.common submodules."""
 | 
			
		||||
							
								
								
									
										134
									
								
								cratonclient/tests/unit/common/test_cliutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								cratonclient/tests/unit/common/test_cliutils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
# Copyright 2010-2011 OpenStack Foundation
 | 
			
		||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
 | 
			
		||||
#
 | 
			
		||||
# 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.
 | 
			
		||||
"""Unit tests for the cratonclient.crud module members."""
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
 | 
			
		||||
from cratonclient.common import cliutils
 | 
			
		||||
from cratonclient.tests import base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCLIUtils(base.TestCase):
 | 
			
		||||
    """Test for the CRUDClient class."""
 | 
			
		||||
 | 
			
		||||
    def test_convert_arg_value_bool(self):
 | 
			
		||||
        """Assert bool conversion."""
 | 
			
		||||
        trues = ['true', 'TRUE', 'True', 'trUE']
 | 
			
		||||
        falses = ['false', 'FALSE', 'False', 'falSe']
 | 
			
		||||
 | 
			
		||||
        for true in trues:
 | 
			
		||||
            self.assertTrue(cliutils.convert_arg_value(true))
 | 
			
		||||
 | 
			
		||||
        for false in falses:
 | 
			
		||||
            self.assertFalse(cliutils.convert_arg_value(false))
 | 
			
		||||
 | 
			
		||||
    def test_convert_arg_value_none(self):
 | 
			
		||||
        """Assert None conversion."""
 | 
			
		||||
        nones = ['none', 'null', 'NULL', 'None', 'NONE']
 | 
			
		||||
 | 
			
		||||
        for none in nones:
 | 
			
		||||
            self.assertIsNone(cliutils.convert_arg_value(none))
 | 
			
		||||
 | 
			
		||||
    def test_convert_arg_value_integer(self):
 | 
			
		||||
        """Assert integer conversion."""
 | 
			
		||||
        ints = ['1', '10', '145']
 | 
			
		||||
 | 
			
		||||
        for integer in ints:
 | 
			
		||||
            value = cliutils.convert_arg_value(integer)
 | 
			
		||||
            self.assertTrue(isinstance(value, int))
 | 
			
		||||
 | 
			
		||||
    def test_convert_arg_value_float(self):
 | 
			
		||||
        """Assert float conversion."""
 | 
			
		||||
        floats = ['5.234', '1.000', '1.0001', '224.1234']
 | 
			
		||||
 | 
			
		||||
        for num in floats:
 | 
			
		||||
            value = cliutils.convert_arg_value(num)
 | 
			
		||||
            self.assertTrue(isinstance(value, float))
 | 
			
		||||
 | 
			
		||||
    def test_convert_arg_value_string(self):
 | 
			
		||||
        """Assert string conversion."""
 | 
			
		||||
        strings = ["hello", "path/to/thing", "sp#cial!", "heyy:this:works"]
 | 
			
		||||
 | 
			
		||||
        for string in strings:
 | 
			
		||||
            value = cliutils.convert_arg_value(string)
 | 
			
		||||
            self.assertTrue(isinstance(value, str))
 | 
			
		||||
 | 
			
		||||
    def test_convert_arg_value_escapes(self):
 | 
			
		||||
        """Assert escaped conversion works to afford literal values."""
 | 
			
		||||
        escapes = ['"007"', '"1"', '"1.0"', '"False"', '"True"', '"None"']
 | 
			
		||||
 | 
			
		||||
        for escaped in escapes:
 | 
			
		||||
            value = cliutils.convert_arg_value(escaped)
 | 
			
		||||
            self.assertTrue(isinstance(value, str))
 | 
			
		||||
 | 
			
		||||
    @mock.patch('cratonclient.common.cliutils.sys.stdin')
 | 
			
		||||
    def test_variable_updates_from_args(self, mock_stdin):
 | 
			
		||||
        """Assert cliutils.variable_updates(...) when using arguments."""
 | 
			
		||||
        test_data = ["foo=bar", "test=", "baz=1", "bumbleywump=cucumberpatch"]
 | 
			
		||||
        mock_stdin.isatty.return_value = True
 | 
			
		||||
        expected_updates = {
 | 
			
		||||
            "foo": "bar",
 | 
			
		||||
            "baz": 1,
 | 
			
		||||
            "bumbleywump": "cucumberpatch"
 | 
			
		||||
        }
 | 
			
		||||
        expected_deletes = ["test"]
 | 
			
		||||
 | 
			
		||||
        updates, deletes = cliutils.variable_updates(test_data)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_updates, updates)
 | 
			
		||||
        self.assertEqual(expected_deletes, deletes)
 | 
			
		||||
 | 
			
		||||
    @mock.patch('cratonclient.common.cliutils.sys.stdin')
 | 
			
		||||
    def test_variable_updates_from_stdin(self, mock_stdin):
 | 
			
		||||
        """Assert cliutils.variable_updates(...) when using stdin."""
 | 
			
		||||
        mock_stdin.isatty.return_value = False
 | 
			
		||||
        mock_stdin.read.return_value = \
 | 
			
		||||
            '{"foo": {"bar": "baz"}, "bumbleywump": "cucumberpatch"}'
 | 
			
		||||
        expected_updates = {
 | 
			
		||||
            "foo": {
 | 
			
		||||
                "bar": "baz"
 | 
			
		||||
            },
 | 
			
		||||
            "bumbleywump": "cucumberpatch"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        updates, deletes = cliutils.variable_updates([])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_updates, updates)
 | 
			
		||||
        self.assertEqual([], deletes)
 | 
			
		||||
 | 
			
		||||
    @mock.patch('cratonclient.common.cliutils.sys.stdin')
 | 
			
		||||
    def test_variable_deletes_from_args(self, mock_stdin):
 | 
			
		||||
        """Assert cliutils.variable_deletes(...) when using arguments."""
 | 
			
		||||
        test_data = ["foo", "test", "baz"]
 | 
			
		||||
        mock_stdin.isatty.return_value = True
 | 
			
		||||
        expected_deletes = test_data
 | 
			
		||||
 | 
			
		||||
        deletes = cliutils.variable_deletes(test_data)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_deletes, deletes)
 | 
			
		||||
 | 
			
		||||
    @mock.patch('cratonclient.common.cliutils.sys.stdin')
 | 
			
		||||
    def test_variable_deletes_from_stdin(self, mock_stdin):
 | 
			
		||||
        """Assert cliutils.variable_deletes(...) when using stdin."""
 | 
			
		||||
        mock_stdin.isatty.return_value = False
 | 
			
		||||
        mock_stdin.read.return_value = \
 | 
			
		||||
            '["foo", "test", "baz"]'
 | 
			
		||||
        expected_deletes = ["foo", "test", "baz"]
 | 
			
		||||
 | 
			
		||||
        deletes = cliutils.variable_deletes([])
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(expected_deletes, deletes)
 | 
			
		||||
@@ -46,7 +46,7 @@ class TestBaseFormatter(testbase.TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_handle_detects_resources(self):
 | 
			
		||||
        """Verify we handle instances explicitly."""
 | 
			
		||||
        resource = crud.Resource(mock.Mock(), {})
 | 
			
		||||
        resource = crud.Resource(mock.Mock(), {"id": 1234})
 | 
			
		||||
        method = 'handle_instance'
 | 
			
		||||
        with mock.patch.object(self.formatter, method) as handle_instance:
 | 
			
		||||
            self.formatter.handle(resource)
 | 
			
		||||
 
 | 
			
		||||
@@ -121,6 +121,7 @@ class TestCRUDClient(base.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.session.delete.assert_called_once_with(
 | 
			
		||||
            'http://example.com/v1/test/1',
 | 
			
		||||
            json=None,
 | 
			
		||||
            params={}
 | 
			
		||||
        )
 | 
			
		||||
        self.assertFalse(self.resource_spec.called)
 | 
			
		||||
@@ -134,6 +135,7 @@ class TestCRUDClient(base.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.session.delete.assert_called_once_with(
 | 
			
		||||
            'http://example.com/v1/test/1',
 | 
			
		||||
            json=None,
 | 
			
		||||
            params={}
 | 
			
		||||
        )
 | 
			
		||||
        self.assertFalse(self.resource_spec.called)
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class TestCloud(base.TestCase):
 | 
			
		||||
    def test_is_a_resource_instance(self):
 | 
			
		||||
        """Verify that a Cloud instance is an instance of a Resource."""
 | 
			
		||||
        manager = mock.Mock()
 | 
			
		||||
        self.assertIsInstance(clouds.Cloud(manager, {}),
 | 
			
		||||
        self.assertIsInstance(clouds.Cloud(manager, {"id": 1234}),
 | 
			
		||||
                              crud.Resource)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class TestRegion(base.TestCase):
 | 
			
		||||
    def test_is_a_resource_instance(self):
 | 
			
		||||
        """Verify that a Region instance is an instance of a Resource."""
 | 
			
		||||
        manager = mock.Mock()
 | 
			
		||||
        self.assertIsInstance(regions.Region(manager, {}),
 | 
			
		||||
        self.assertIsInstance(regions.Region(manager, {"id": 1234}),
 | 
			
		||||
                              crud.Resource)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										67
									
								
								cratonclient/tests/unit/v1/test_variables.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								cratonclient/tests/unit/v1/test_variables.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
#   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.
 | 
			
		||||
"""Tests for `cratonclient.v1.clouds` module."""
 | 
			
		||||
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
from cratonclient.tests import base
 | 
			
		||||
from cratonclient.v1 import variables
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestVariables(base.TestCase):
 | 
			
		||||
    """Tests for the Cloud Resource."""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        """Basic test setup."""
 | 
			
		||||
        super(TestVariables, self).setUp()
 | 
			
		||||
        session = mock.Mock()
 | 
			
		||||
        self.variable_mgr = variables.VariableManager(session, '')
 | 
			
		||||
 | 
			
		||||
    def test_is_a_crudclient(self):
 | 
			
		||||
        """Verify our CloudManager is a CRUDClient."""
 | 
			
		||||
        self.assertIsInstance(self.variable_mgr, crud.CRUDClient)
 | 
			
		||||
 | 
			
		||||
    def test_variables_dict(self):
 | 
			
		||||
        """Assert Variables instantiation produces sane variables dict."""
 | 
			
		||||
        test_data = {
 | 
			
		||||
            "foo": "bar",
 | 
			
		||||
            "zoo": {
 | 
			
		||||
                "baz": "boo"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        resource_obj = self.variable_mgr.resource_class(
 | 
			
		||||
            mock.Mock(),
 | 
			
		||||
            {"variables": test_data}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        expected_variables_dict = {
 | 
			
		||||
            "foo": variables.Variable("foo", "bar"),
 | 
			
		||||
            "zoo": variables.Variable("zoo", {"baz": "boo"})
 | 
			
		||||
        }
 | 
			
		||||
        self.assertDictEqual(expected_variables_dict,
 | 
			
		||||
                             resource_obj._variables_dict)
 | 
			
		||||
 | 
			
		||||
    def test_to_dict(self):
 | 
			
		||||
        """Assert Variables.to_dict() produces original variables dict."""
 | 
			
		||||
        test_data = {
 | 
			
		||||
            "foo": "bar",
 | 
			
		||||
            "zoo": {
 | 
			
		||||
                "baz": "boo"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        resource_obj = self.variable_mgr.resource_class(
 | 
			
		||||
            mock.Mock(),
 | 
			
		||||
            {"variables": test_data}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertDictEqual(test_data, resource_obj.to_dict())
 | 
			
		||||
@@ -13,12 +13,15 @@
 | 
			
		||||
# under the License.
 | 
			
		||||
"""Hosts resource and resource manager."""
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
from cratonclient.v1 import variables
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Host(crud.Resource):
 | 
			
		||||
    """Representation of a Host."""
 | 
			
		||||
 | 
			
		||||
    pass
 | 
			
		||||
    subresource_managers = {
 | 
			
		||||
        'variables': variables.VariableManager,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HostManager(crud.CRUDClient):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										116
									
								
								cratonclient/v1/variables.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								cratonclient/v1/variables.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""Variables manager code."""
 | 
			
		||||
from collections import MutableMapping
 | 
			
		||||
from cratonclient import crud
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Variable(object):
 | 
			
		||||
    """Represents a Craton variable key/value pair."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, value):
 | 
			
		||||
        """Instantiate key/value pair."""
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.value = value
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other):
 | 
			
		||||
        """Assess equality of Variable objects."""
 | 
			
		||||
        if isinstance(other, type(self)):
 | 
			
		||||
            return self.name == other.name and self.value == other.value
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        """Return string representation of a Variable."""
 | 
			
		||||
        return '%(class)s(name=%(name)r, value=%(value)r)' % \
 | 
			
		||||
            {
 | 
			
		||||
                "class": self.__class__.__name__,
 | 
			
		||||
                "name": self.name,
 | 
			
		||||
                "value": self.value
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Variables(MutableMapping):
 | 
			
		||||
    """Represents a dictionary of Variables."""
 | 
			
		||||
 | 
			
		||||
    _var_key = 'variables'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, manager, info, loaded=False):
 | 
			
		||||
        """Instantiate a Variables dict.
 | 
			
		||||
 | 
			
		||||
        Converts each value to a Variable representation of the full key/value
 | 
			
		||||
        pair and assigns this mapping to self, as this extends the "dict"
 | 
			
		||||
        type.
 | 
			
		||||
 | 
			
		||||
        This class is intended to look like crud.Resource interface-wise, but
 | 
			
		||||
        it's unfortunately a hack to get around some limitations of
 | 
			
		||||
        crud.Resource, specifically how crud.Resource overlaying an API
 | 
			
		||||
        response onto a Python class can have variable keys conflicting with
 | 
			
		||||
        legitimate attributes of the class itself. Because this is supposed
 | 
			
		||||
        to be a dictionary-like thing, though, we don't wish to use the
 | 
			
		||||
        manager to make API calls when users are treating this like a dict.
 | 
			
		||||
        """
 | 
			
		||||
        self._manager = manager
 | 
			
		||||
        self._loaded = loaded
 | 
			
		||||
        self._variables_dict = {
 | 
			
		||||
            k: Variable(k, v)
 | 
			
		||||
            for (k, v) in info.get(self._var_key, dict()).items()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, key):
 | 
			
		||||
        """Get item from self._variables_dict."""
 | 
			
		||||
        return self._variables_dict[key]
 | 
			
		||||
 | 
			
		||||
    def __setitem__(self, key, value):
 | 
			
		||||
        """Set item in self._variables_dict."""
 | 
			
		||||
        self._variables_dict[key] = value
 | 
			
		||||
 | 
			
		||||
    def __delitem__(self, key):
 | 
			
		||||
        """Delete item from self._variables_dict."""
 | 
			
		||||
        del self._variables_dict[key]
 | 
			
		||||
 | 
			
		||||
    def __len__(self):
 | 
			
		||||
        """Get length of self._variables_dict."""
 | 
			
		||||
        return len(self._variables_dict)
 | 
			
		||||
 | 
			
		||||
    def __iter__(self):
 | 
			
		||||
        """Return iterator of self._variables_dict."""
 | 
			
		||||
        return iter(self._variables_dict)
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        """Return string representation of Variables."""
 | 
			
		||||
        info = ", ".join("%s=%s" % (k, self._variables_dict[k]) for k, v in
 | 
			
		||||
                         self._variables_dict.items())
 | 
			
		||||
        return "<%s %s>" % (self.__class__.__name__, info)
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        """Return this the original variables as a dict."""
 | 
			
		||||
        return {k: v.value for (k, v) in self._variables_dict.items()}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VariableManager(crud.CRUDClient):
 | 
			
		||||
    """A CRUD manager for variables."""
 | 
			
		||||
 | 
			
		||||
    base_path = '/variables'
 | 
			
		||||
    resource_class = Variables
 | 
			
		||||
 | 
			
		||||
    def delete(self, *args, **kwargs):
 | 
			
		||||
        """Wrap crud.CRUDClient's delete to simplify for the variables.
 | 
			
		||||
 | 
			
		||||
        One can pass in a series of keys to delete, and this will pass the
 | 
			
		||||
        correct arguments to the crud.CRUDClient.delete function.
 | 
			
		||||
 | 
			
		||||
        .. code-block:: python
 | 
			
		||||
 | 
			
		||||
            >>> craton.hosts.get(1234).variables.delete('var-a', 'var-b')
 | 
			
		||||
            <Response [204]>
 | 
			
		||||
        """
 | 
			
		||||
        return super(VariableManager, self).delete(json=args, **kwargs)
 | 
			
		||||
		Reference in New Issue
	
	Block a user