OpenstackClient plugin for deployment create

This change implements the 'openstack software deployment create' command.

Blueprint: heat-support-python-openstackclient

Change-Id: I757ca2896e1e4a0ad96a5f34c34205e099715801
This commit is contained in:
Bryan Jones 2016-01-26 21:58:26 +00:00
parent 61da7b23b2
commit d67af77f20
3 changed files with 317 additions and 24 deletions

View File

@ -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='<DEPLOYMENT_NAME>',
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='<KEY=VALUE>',
action='append',
help=_('Input value to set on the deployment. This can be '
'specified multiple times.')
)
parser.add_argument(
'--action',
metavar='<ACTION>',
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='<CONFIG>',
help=_('ID of the configuration to deploy')
)
parser.add_argument(
'--server',
metavar='<SERVER>',
required=True,
help=_('ID of the server being deployed to')
)
parser.add_argument(
'--signal-transport',
metavar='<TRANSPORT>',
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='<CONTAINER_NAME>',
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)."""

View File

@ -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,

View File

@ -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