From 0306a41fa0e5bf81b14466b5cd8618b4dd589070 Mon Sep 17 00:00:00 2001 From: Mark Vanderwiel Date: Fri, 11 Dec 2015 13:47:28 -0600 Subject: [PATCH] Add openstack client stack resource signal Add the openstack stack resource signal command. Based from the existing heat commands: heat resource-signal Change-Id: I3b3628d86b71d448feea197f6c92d3d3d19726b5 Blueprint: heat-support-python-openstackclient --- heatclient/osc/v1/resources.py | 76 +++++++++++++++++- .../tests/unit/osc/v1/test_resources.py | 78 ++++++++++++++++++- setup.cfg | 1 + 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/heatclient/osc/v1/resources.py b/heatclient/osc/v1/resources.py index 685629cc..dae8470c 100644 --- a/heatclient/osc/v1/resources.py +++ b/heatclient/osc/v1/resources.py @@ -13,16 +13,20 @@ """Orchestration v1 Stack action implementations""" +from cliff import command import logging import six +from six.moves.urllib import request from cliff import lister from cliff import show from openstackclient.common import exceptions as exc from openstackclient.common import utils from openstackclient.i18n import _ +from oslo_serialization import jsonutils from heatclient.common import format_utils +from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc @@ -161,10 +165,78 @@ def _resource_metadata(heat_client, args): try: metadata = heat_client.resources.metadata(**fields) except heat_exc.HTTPNotFound: - raise exc.CommandError(_('Stack or resource not found: ' - '%(stack)s %(resource)s') % + raise exc.CommandError(_('Stack %(stack)s or resource %(resource)s ' + 'not found.') % {'stack': args.stack, 'resource': args.resource}) + data = list(six.itervalues(metadata)) columns = list(six.iterkeys(metadata)) return columns, data + + +class ResourceSignal(command.Command): + """Signal a resource with optional data.""" + + log = logging.getLogger(__name__ + ".ResourceSignal") + + def get_parser(self, prog_name): + parser = super(ResourceSignal, self).get_parser(prog_name) + parser.add_argument( + 'stack', + metavar='', + help=_('Name or ID of stack the resource belongs to'), + ) + parser.add_argument( + 'resource', + metavar='', + help=_('Name of the resoure to signal'), + ) + parser.add_argument( + '--data', + metavar='', + help=_('JSON Data to send to the signal handler') + ) + parser.add_argument( + '--data-file', + metavar='', + help=_('File containing JSON data to send to the signal handler') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + heat_client = self.app.client_manager.orchestration + return _resource_signal(heat_client, parsed_args) + + +def _resource_signal(heat_client, args): + fields = {'stack_id': args.stack, + 'resource_name': args.resource} + data = args.data + data_file = args.data_file + if data and data_file: + raise exc.CommandError(_('Should only specify one of data or ' + 'data-file')) + + if data_file: + data_url = heat_utils.normalise_file_path_to_url(data_file) + data = request.urlopen(data_url).read() + + if data: + try: + data = jsonutils.loads(data) + except ValueError as ex: + raise exc.CommandError(_('Data should be in JSON format: %s') % ex) + if not isinstance(data, dict): + raise exc.CommandError(_('Data should be a JSON dict')) + + fields['data'] = data + try: + heat_client.resources.signal(**fields) + except heat_exc.HTTPNotFound: + raise exc.CommandError(_('Stack %(stack)s or resource %(resource)s ' + 'not found.') % + {'stack': args.stack, + 'resource': args.resource}) diff --git a/heatclient/tests/unit/osc/v1/test_resources.py b/heatclient/tests/unit/osc/v1/test_resources.py index 498b8b9b..367e1fd3 100644 --- a/heatclient/tests/unit/osc/v1/test_resources.py +++ b/heatclient/tests/unit/osc/v1/test_resources.py @@ -219,4 +219,80 @@ class TestResourceMetadata(TestResource): parsed_args = self.check_parser(self.cmd, arglist, []) self.resource_client.metadata = mock.Mock( side_effect=heat_exc.HTTPNotFound) - self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Stack my_stack or resource my_resource not found.', + str(error)) + + +class TestResourceSignal(TestResource): + + def setUp(self): + super(TestResourceSignal, self).setUp() + self.cmd = resources.ResourceSignal(self.app, None) + self.resource_client.signal = mock.Mock() + + def test_resource_signal(self): + arglist = ['my_stack', 'my_resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.resource_client.signal.assert_called_with(**{ + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) + + def test_resource_signal_error(self): + arglist = ['my_stack', 'my_resource'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.resource_client.signal = mock.Mock( + side_effect=heat_exc.HTTPNotFound) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Stack my_stack or resource my_resource not found.', + str(error)) + + def test_resource_signal_data(self): + arglist = ['my_stack', 'my_resource', + '--data', '{"message":"Content"}'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.resource_client.signal.assert_called_with(**{ + 'data': {u'message': u'Content'}, + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) + + def test_resource_signal_data_not_json(self): + arglist = ['my_stack', 'my_resource', '--data', '{'] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertIn('Data should be in JSON format', str(error)) + + def test_resource_signal_data_and_file_error(self): + arglist = ['my_stack', 'my_resource', + '--data', '{}', '--data-file', 'file'] + parsed_args = self.check_parser(self.cmd, arglist, []) + error = self.assertRaises(exc.CommandError, + self.cmd.take_action, + parsed_args) + self.assertEqual('Should only specify one of data or data-file', + str(error)) + + @mock.patch('six.moves.urllib.request.urlopen') + def test_resource_signal_file(self, urlopen): + data = mock.Mock() + data.read.side_effect = ['{"message":"Content"}'] + urlopen.return_value = data + + arglist = ['my_stack', 'my_resource', '--data-file', 'test_file'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + self.resource_client.signal.assert_called_with(**{ + 'data': {u'message': u'Content'}, + 'stack_id': 'my_stack', + 'resource_name': 'my_resource' + }) diff --git a/setup.cfg b/setup.cfg index d721e5e9..4dc93c92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ openstack.orchestration.v1 = stack_resource_list = heatclient.osc.v1.resources:ResourceList stack_resource_metadata = heatclient.osc.v1.resources:ResourceMetadata stack_resource_show = heatclient.osc.v1.resources:ResourceShow + stack_resource_signal = heatclient.osc.v1.resources:ResourceSignal stack_resume = heatclient.osc.v1.stack:ResumeStack stack_show = heatclient.osc.v1.stack:ShowStack stack_snapshot_list = heatclient.osc.v1.snapshot:ListSnapshot