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
This commit is contained in:
parent
69c747df3b
commit
05b4f117bb
cratonclient
@ -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)
|
Loading…
x
Reference in New Issue
Block a user