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:
parent
61da7b23b2
commit
d67af77f20
|
@ -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)."""
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue