diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index 67d1c401..4451f83b 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -22,10 +22,115 @@ from cliff import show from openstackclient.common import exceptions as exc from openstackclient.common import utils +from heatclient.common import deployment_utils +from heatclient.common import format_utils +from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc from heatclient.openstack.common._i18n import _ +class CreateDeployment(format_utils.YamlFormat): + """Create a software deployment.""" + + log = logging.getLogger(__name__ + '.CreateDeployment') + + def get_parser(self, prog_name): + parser = super(CreateDeployment, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('Name of the derived config associated with this ' + 'deployment. This is used to apply a sort order to the ' + 'list of configurations currently deployed to the server.') + ) + parser.add_argument( + '--input-value', + metavar='', + action='append', + help=_('Input value to set on the deployment. This can be ' + 'specified multiple times.') + ) + parser.add_argument( + '--action', + metavar='', + default='UPDATE', + help=_('Name of an action for this deployment. This can be a ' + 'custom action, or one of CREATE, UPDATE, DELETE, SUSPEND, ' + 'RESUME. Default is UPDATE') + ) + parser.add_argument( + '--config', + metavar='', + help=_('ID of the configuration to deploy') + ) + parser.add_argument( + '--server', + metavar='', + required=True, + help=_('ID of the server being deployed to') + ) + parser.add_argument( + '--signal-transport', + metavar='', + default='TEMP_URL_SIGNAL', + help=_('How the server should signal to heat with the deployment ' + 'output values. TEMP_URL_SIGNAL will create a Swift ' + 'TempURL to be signaled via HTTP PUT. ZAQAR_SIGNAL will ' + 'create a dedicated zaqar queue to be signaled using the ' + 'provided keystone credentials.NO_SIGNAL will result in ' + 'the resource going to the COMPLETE state without waiting ' + 'for any signal') + ) + parser.add_argument( + '--container', + metavar='', + help=_('Optional name of container to store TEMP_URL_SIGNAL ' + 'objects in. If not specified a container will be created ' + 'with a name derived from the DEPLOY_NAME') + ) + parser.add_argument( + '--timeout', + type=int, + default=60, + help=_('Deployment timeout in minutes') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + client = self.app.client_manager.orchestration + + config = {} + if parsed_args.config: + try: + config = client.software_configs.get(parsed_args.config) + except heat_exc.HTTPNotFound: + msg = (_('Software configuration not found: %s') % + parsed_args.config) + raise exc.CommandError(msg) + + derived_params = deployment_utils.build_derived_config_params( + parsed_args.action, + config, + parsed_args.name, + heat_utils.format_parameters(parsed_args.input_value, False), + parsed_args.server, + parsed_args.signal_transport, + signal_id=deployment_utils.build_signal_id(client, parsed_args) + ) + derived_config = client.software_configs.create(**derived_params) + + sd = client.software_deployments.create( + config_id=derived_config.id, + server_id=parsed_args.server, + action=parsed_args.action, + status='IN_PROGRESS' + ) + + return zip(*sorted(sd.to_dict().items())) + + class DeleteDeployment(command.Command): """Delete software deployment(s) and correlative config(s).""" diff --git a/heatclient/tests/unit/osc/v1/test_software_deployment.py b/heatclient/tests/unit/osc/v1/test_software_deployment.py index 7157cf06..64273603 100644 --- a/heatclient/tests/unit/osc/v1/test_software_deployment.py +++ b/heatclient/tests/unit/osc/v1/test_software_deployment.py @@ -11,6 +11,7 @@ # under the License. # +import copy import mock from openstackclient.common import exceptions as exc @@ -18,16 +19,202 @@ from openstackclient.common import exceptions as exc from heatclient import exc as heat_exc from heatclient.osc.v1 import software_deployment from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes +from heatclient.v1 import software_configs from heatclient.v1 import software_deployments class TestDeployment(orchestration_fakes.TestOrchestrationv1): def setUp(self): super(TestDeployment, self).setUp() - sd_client = self.app.client_manager.orchestration.software_deployments - self.mock_client = sd_client - sc_client = self.app.client_manager.orchestration.software_configs - self.mock_config_client = sc_client + self.mock_client = self.app.client_manager.orchestration + self.config_client = self.mock_client.software_configs + self.sd_client = self.mock_client.software_deployments + + +class TestDeploymentCreate(TestDeployment): + + server_id = '1234' + config_id = '5678' + deploy_id = '910' + + config = { + 'name': 'my_deploy', + 'group': 'strict', + 'config': '#!/bin/bash', + 'inputs': [], + 'outputs': [], + 'options': [], + 'id': config_id, + } + + deployment = { + 'server_id': server_id, + 'input_values': {}, + 'action': 'UPDATE', + 'status': 'IN_PROGRESS', + 'status_reason': None, + 'signal_id': 'signal_id', + 'config_id': config_id, + 'id': deploy_id, + } + + config_defaults = { + 'group': 'Heat::Ungrouped', + 'config': '', + 'options': {}, + 'inputs': [ + { + 'name': 'deploy_server_id', + 'description': 'ID of the server being deployed to', + 'type': 'String', + 'value': server_id, + }, + { + 'name': 'deploy_action', + 'description': 'Name of the current action being deployed', + 'type': 'String', + 'value': 'UPDATE', + }, + { + 'name': 'deploy_signal_transport', + 'description': 'How the server should signal to heat with the ' + 'deployment output values.', + 'type': 'String', + 'value': 'TEMP_URL_SIGNAL', + }, + { + 'name': 'deploy_signal_id', + 'description': 'ID of signal to use for signaling output ' + 'values', + 'type': 'String', + 'value': 'signal_id', + }, + { + 'name': 'deploy_signal_verb', + 'description': 'HTTP verb to use for signaling output values', + 'type': 'String', + 'value': 'PUT', + }, + ], + 'outputs': [], + 'name': 'my_deploy', + } + + deploy_defaults = { + 'config_id': config_id, + 'server_id': server_id, + 'action': 'UPDATE', + 'status': 'IN_PROGRESS', + } + + def setUp(self): + super(TestDeploymentCreate, self).setUp() + self.cmd = software_deployment.CreateDeployment(self.app, None) + self.config_client.create = mock.MagicMock(return_value=( + software_configs.SoftwareConfig(None, self.config))) + self.config_client.get = mock.MagicMock(return_value=( + software_configs.SoftwareConfig(None, self.config))) + self.sd_client.create = mock.MagicMock(return_value=( + software_deployments.SoftwareDeployment(None, self.deployment))) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id] + expected_cols = ('action', 'config_id', 'id', 'input_values', + 'server_id', 'signal_id', 'status', 'status_reason') + expected_data = ('UPDATE', self.config_id, self.deploy_id, {}, + self.server_id, 'signal_id', 'IN_PROGRESS', None) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**self.config_defaults) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + self.assertEqual(expected_cols, columns) + self.assertEqual(expected_data, data) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_with_config(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--config', self.config_id] + config = copy.deepcopy(self.config_defaults) + config['config'] = '#!/bin/bash' + config['group'] = 'strict' + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.get.assert_called_with(self.config_id) + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + + def test_deployment_create_config_not_found(self): + arglist = ['my_deploy', '--server', self.server_id, + '--config', 'bad_id'] + self.config_client.get.side_effect = heat_exc.HTTPNotFound + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) + + def test_deployment_create_no_signal(self): + arglist = ['my_deploy', '--server', self.server_id, + '--signal-transport', 'NO_SIGNAL'] + config = copy.deepcopy(self.config_defaults) + config['inputs'] = config['inputs'][:-2] + config['inputs'][2]['value'] = 'NO_SIGNAL' + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_invalid_signal_transport(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--signal-transport', 'A'] + parsed_args = self.check_parser(self.cmd, arglist, []) + + self.assertRaises(heat_exc.CommandError, + self.cmd.take_action, parsed_args) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_input_value(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--input-value', 'foo=bar'] + config = copy.deepcopy(self.config_defaults) + config['inputs'].insert( + 0, {'name': 'foo', 'type': 'String', 'value': 'bar'}) + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with( + **self.deploy_defaults) + + @mock.patch('heatclient.common.deployment_utils.build_signal_id', + return_value='signal_id') + def test_deployment_create_action(self, mock_build): + arglist = ['my_deploy', '--server', self.server_id, + '--action', 'DELETE'] + config = copy.deepcopy(self.config_defaults) + config['inputs'][1]['value'] = 'DELETE' + deploy = copy.deepcopy(self.deploy_defaults) + deploy['action'] = 'DELETE' + parsed_args = self.check_parser(self.cmd, arglist, []) + + columns, data = self.cmd.take_action(parsed_args) + + self.config_client.create.assert_called_with(**config) + self.sd_client.create.assert_called_with(**deploy) class TestDeploymentDelete(TestDeployment): @@ -39,27 +226,27 @@ class TestDeploymentDelete(TestDeployment): def test_deployment_delete_success(self): arglist = ['test_deployment'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock() - self.mock_client.delete = mock.Mock() + self.sd_client.get = mock.Mock() + self.sd_client.delete = mock.Mock() self.cmd.take_action(parsed_args) - self.mock_client.delete.assert_called_with( + self.sd_client.delete.assert_called_with( deployment_id='test_deployment') def test_deployment_delete_multiple(self): arglist = ['test_deployment', 'test_deployment2'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock() - self.mock_client.delete = mock.Mock() + self.sd_client.get = mock.Mock() + self.sd_client.delete = mock.Mock() self.cmd.take_action(parsed_args) - self.mock_client.delete.assert_has_calls( + self.sd_client.delete.assert_has_calls( [mock.call(deployment_id='test_deployment'), mock.call(deployment_id='test_deployment2')]) def test_deployment_delete_not_found(self): arglist = ['test_deployment', 'test_deployment2'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.delete = mock.Mock() - self.mock_client.delete.side_effect = heat_exc.HTTPNotFound() + self.sd_client.delete = mock.Mock() + self.sd_client.delete.side_effect = heat_exc.HTTPNotFound() error = self.assertRaises( exc.CommandError, self.cmd.take_action, parsed_args) self.assertIn("Unable to delete 2 of the 2 deployments.", str(error)) @@ -67,8 +254,8 @@ class TestDeploymentDelete(TestDeployment): def test_deployment_config_delete_failed(self): arglist = ['test_deployment'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_config_client.delete = mock.Mock() - self.mock_config_client.delete.side_effect = heat_exc.HTTPNotFound() + self.config_client.delete = mock.Mock() + self.config_client.delete.side_effect = heat_exc.HTTPNotFound() error = self.assertRaises( exc.CommandError, self.cmd.take_action, parsed_args) self.assertEqual("Unable to delete 1 of the 1 deployments.", @@ -108,13 +295,13 @@ class TestDeploymentList(TestDeployment): def setUp(self): super(TestDeploymentList, self).setUp() self.cmd = software_deployment.ListDeployment(self.app, None) - self.mock_client.list = mock.MagicMock(return_value=[self.data]) + self.sd_client.list = mock.MagicMock(return_value=[self.data]) def test_deployment_list(self): arglist = [] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.list.assert_called_with() + self.sd_client.list.assert_called_with() self.assertEqual(self.columns, columns) def test_deployment_list_server(self): @@ -123,7 +310,7 @@ class TestDeploymentList(TestDeployment): arglist = ['--server', 'ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5'] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.list.assert_called_with(**kwargs) + self.sd_client.list.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) def test_deployment_list_long(self): @@ -135,7 +322,7 @@ class TestDeploymentList(TestDeployment): columns, data = self.cmd.take_action(parsed_args) - self.mock_client.list.assert_called_with(**kwargs) + self.sd_client.list.assert_called_with(**kwargs) self.assertEqual(cols, columns) @@ -163,11 +350,11 @@ class TestDeploymentShow(TestDeployment): 'updated_time', 'status', 'status_reason', 'input_values', 'action'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock( + self.sd_client.get = mock.Mock( return_value=software_deployments.SoftwareDeployment( None, self.get_response)) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.get.assert_called_with(**{ + self.sd_client.get.assert_called_with(**{ 'deployment_id': 'my_deployment', }) self.assertEqual(cols, columns) @@ -178,11 +365,11 @@ class TestDeploymentShow(TestDeployment): 'updated_time', 'status', 'status_reason', 'input_values', 'action', 'output_values'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock( + self.sd_client.get = mock.Mock( return_value=software_deployments.SoftwareDeployment( None, self.get_response)) columns, data = self.cmd.take_action(parsed_args) - self.mock_client.get.assert_called_once_with(**{ + self.sd_client.get.assert_called_once_with(**{ 'deployment_id': 'my_deployment', }) self.assertEqual(cols, columns) @@ -190,8 +377,8 @@ class TestDeploymentShow(TestDeployment): def test_deployment_not_found(self): arglist = ['my_deployment'] parsed_args = self.check_parser(self.cmd, arglist, []) - self.mock_client.get = mock.Mock() - self.mock_client.get.side_effect = heat_exc.HTTPNotFound() + self.sd_client.get = mock.Mock() + self.sd_client.get.side_effect = heat_exc.HTTPNotFound() self.assertRaises( exc.CommandError, self.cmd.take_action, diff --git a/setup.cfg b/setup.cfg index b419b5ae..c0024c24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ openstack.orchestration.v1 = software_config_delete = heatclient.osc.v1.software_config:DeleteConfig software_config_list = heatclient.osc.v1.software_config:ListConfig software_config_show = heatclient.osc.v1.software_config:ShowConfig + software_deployment_create = heatclient.osc.v1.software_deployment:CreateDeployment software_deployment_delete = heatclient.osc.v1.software_deployment:DeleteDeployment software_deployment_list = heatclient.osc.v1.software_deployment:ListDeployment software_deployment_show = heatclient.osc.v1.software_deployment:ShowDeployment