From 2c0fecf69ce4889b208b60400eccd935ee32a8d6 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Wed, 24 Aug 2016 11:47:30 +0100 Subject: [PATCH] Add 'openstack overcloud plan create' command This uses the new Mistral actions and workflows to create a plan with the tripleo heat templates. Templates can either be provided by the user or the default templates on the undercloud can be used. Closes-Bug: #1616014 Change-Id: I4f82fda01215b9a45669862ef26c69421fdaad59 --- requirements.txt | 2 +- setup.cfg | 1 + tripleoclient/plugin.py | 28 ++++ tripleoclient/tests/v1/test_overcloud_plan.py | 141 ++++++++++++++++++ tripleoclient/v1/overcloud_plan.py | 38 +++++ tripleoclient/workflows/base.py | 21 +++ tripleoclient/workflows/plan_management.py | 89 +++++++++++ 7 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 tripleoclient/workflows/base.py create mode 100644 tripleoclient/workflows/plan_management.py diff --git a/requirements.txt b/requirements.txt index 89239785b..fc4662fe9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,5 +16,5 @@ six>=1.9.0 # MIT os-cloud-config # Apache-2.0 websocket-client>=0.32.0 # LGPLv2+ -# tripleo-common lib is not yet on PyPi +# tripleoclient is tied to tripleo-common and expects to use the latest code. -e git://github.com/openstack/tripleo-common.git#egg=tripleo_common diff --git a/setup.cfg b/setup.cfg index f09b47b22..7139ae91d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,7 @@ openstack.tripleoclient.v1 = overcloud_node_import = tripleoclient.v1.overcloud_node:ImportNode overcloud_node_introspect = tripleoclient.v1.overcloud_node:IntrospectNode overcloud_node_provide = tripleoclient.v1.overcloud_node:ProvideNode + overcloud_plan_create = tripleoclient.v1.overcloud_plan:CreatePlan overcloud_plan_delete = tripleoclient.v1.overcloud_plan:DeletePlan overcloud_plan_list = tripleoclient.v1.overcloud_plan:ListPlans overcloud_profiles_match = tripleoclient.v1.overcloud_profiles:MatchProfiles diff --git a/tripleoclient/plugin.py b/tripleoclient/plugin.py index 02d0306be..035d0b609 100644 --- a/tripleoclient/plugin.py +++ b/tripleoclient/plugin.py @@ -21,6 +21,7 @@ import uuid import websocket from openstackclient.common import utils +from swiftclient import client as swift_client LOG = logging.getLogger(__name__) @@ -143,7 +144,34 @@ class ClientWrapper(object): def __init__(self, instance): self._instance = instance + self._object_store = None def messaging_websocket(self, queue_name='tripleo'): """Returns a websocket for the messaging service""" return WebsocketClient(self._instance, queue_name) + + @property + def object_store(self): + """Returns an object_store service client + + The Swift/Object client returned by python-openstack client isn't an + instance of python-swiftclient, and had far less functionality. + """ + + if self._object_store is not None: + return self._object_store + + endpoint = self._instance.get_endpoint_for_service_type( + "object-store", + region_name=self._instance._region_name, + ) + + token = self._instance.auth.get_token(self._instance.session) + + kwargs = { + 'preauthurl': endpoint, + 'preauthtoken': token + } + + self._object_store = swift_client.Connection(**kwargs) + return self._object_store diff --git a/tripleoclient/tests/v1/test_overcloud_plan.py b/tripleoclient/tests/v1/test_overcloud_plan.py index 7948e1dab..e0be76a1e 100644 --- a/tripleoclient/tests/v1/test_overcloud_plan.py +++ b/tripleoclient/tests/v1/test_overcloud_plan.py @@ -14,6 +14,7 @@ import mock from openstackclient.tests import utils +from tripleoclient import exceptions from tripleoclient.v1 import overcloud_plan @@ -84,3 +85,143 @@ class TestOvercloudDeletePlan(utils.TestCommand): input={'container': 'test-plan1'}), mock.call('tripleo.delete_plan', input={'container': 'test-plan2'})]) + + +class TestOvercloudCreatePlan(utils.TestCommand): + + def setUp(self): + super(TestOvercloudCreatePlan, self).setUp() + + self.cmd = overcloud_plan.CreatePlan(self.app, None) + self.app.client_manager.workflow_engine = mock.Mock() + self.tripleoclient = mock.Mock() + + self.websocket = mock.Mock() + self.websocket.__enter__ = lambda s: self.websocket + self.websocket.__exit__ = lambda s, *exc: None + self.tripleoclient = mock.Mock() + self.tripleoclient.messaging_websocket.return_value = self.websocket + self.app.client_manager.tripleoclient = self.tripleoclient + + self.workflow = self.app.client_manager.workflow_engine + + # Mock UUID4 generation for every test + uuid4_patcher = mock.patch('uuid.uuid4', return_value="UUID4") + self.mock_uuid4 = uuid4_patcher.start() + self.addCleanup(self.mock_uuid4.stop) + + def test_create_default_plan(self): + + # Setup + arglist = ['overcast'] + verifylist = [ + ('name', 'overcast'), + ('templates', None) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.websocket.wait_for_message.return_value = { + "status": "SUCCESS" + } + + # Run + self.cmd.take_action(parsed_args) + + # Verify + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.create_default_deployment_plan', + workflow_input={ + 'container': 'overcast', + 'queue_name': 'UUID4' + }) + + def test_create_default_plan_failed(self): + + # Setup + arglist = ['overcast'] + verifylist = [ + ('name', 'overcast'), + ('templates', None) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.websocket.wait_for_message.return_value = { + "status": "ERROR", "message": "failed" + } + + # Run + self.assertRaises(exceptions.WorkflowServiceError, + self.cmd.take_action, parsed_args) + + # Verify + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.create_default_deployment_plan', + workflow_input={ + 'container': 'overcast', + 'queue_name': 'UUID4' + }) + + @mock.patch("tripleoclient.workflows.plan_management.tarball") + def test_create_custom_plan(self, mock_tarball): + + # Setup + arglist = ['overcast', '--templates', '/fake/path'] + verifylist = [ + ('name', 'overcast'), + ('templates', '/fake/path') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.websocket.wait_for_message.return_value = { + "status": "SUCCESS" + } + mock_result = mock.Mock(output='{"result": null}') + self.workflow.action_executions.create.return_value = mock_result + + # Run + self.cmd.take_action(parsed_args) + + # Verify + self.workflow.action_executions.create.assert_called_once_with( + 'tripleo.create_container', {"container": "overcast"} + ) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.create_deployment_plan', + workflow_input={ + 'container': 'overcast', + 'queue_name': 'UUID4' + }) + + @mock.patch("tripleoclient.workflows.plan_management.tarball") + def test_create_custom_plan_failed(self, mock_tarball): + + # Setup + arglist = ['overcast', '--templates', '/fake/path'] + verifylist = [ + ('name', 'overcast'), + ('templates', '/fake/path') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.websocket.wait_for_message.return_value = { + "status": "ERROR", "message": "failed" + } + mock_result = mock.Mock(output='{"result": null}') + self.workflow.action_executions.create.return_value = mock_result + + # Run + self.assertRaises(exceptions.WorkflowServiceError, + self.cmd.take_action, parsed_args) + + # Verify + self.workflow.action_executions.create.assert_called_once_with( + 'tripleo.create_container', {"container": "overcast"} + ) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.create_deployment_plan', + workflow_input={ + 'container': 'overcast', + 'queue_name': 'UUID4' + }) diff --git a/tripleoclient/v1/overcloud_plan.py b/tripleoclient/v1/overcloud_plan.py index 84b255049..52f56921e 100644 --- a/tripleoclient/v1/overcloud_plan.py +++ b/tripleoclient/v1/overcloud_plan.py @@ -12,11 +12,14 @@ import json import logging +import uuid from cliff import command from cliff import lister from openstackclient.i18n import _ +from tripleoclient.workflows import plan_management + class ListPlans(lister.Lister): """List overcloud deployment plans.""" @@ -74,3 +77,38 @@ class DeletePlan(command.Command): except Exception: self.log.exception( "Error parsing action result %s", execution.output) + + +class CreatePlan(command.Command): + """Create a deployment plan""" + + log = logging.getLogger(__name__ + ".CreatePlan") + + def get_parser(self, prog_name): + parser = super(CreatePlan, self).get_parser(prog_name) + parser.add_argument( + 'name', + help=_('The name of the plan, which is used for the object ' + 'storage container, workflow environment and orchestration ' + 'stack names.')) + parser.add_argument( + '--templates', + help=_('The directory containing the Heat templates to deploy. ' + 'If this isn\'t provided, the templates packaged on the ' + 'Undercloud will be used.'), + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + clients = self.app.client_manager + + name = parsed_args.name + + if parsed_args.templates: + plan_management.create_plan_from_templates( + clients, name, parsed_args.templates) + else: + plan_management.create_default_plan( + clients, container=name, queue_name=str(uuid.uuid4())) diff --git a/tripleoclient/workflows/base.py b/tripleoclient/workflows/base.py new file mode 100644 index 000000000..264223d7b --- /dev/null +++ b/tripleoclient/workflows/base.py @@ -0,0 +1,21 @@ +# 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. +import json + + +def call_action(workflow_client, action, **input_): + """Trigger a Mistral action and parse the JSON response""" + + result = workflow_client.action_executions.create(action, input_) + + # Parse the JSON output. Mistral client should do this for us really. + return json.loads(result.output)['result'] diff --git a/tripleoclient/workflows/plan_management.py b/tripleoclient/workflows/plan_management.py new file mode 100644 index 000000000..059cf703a --- /dev/null +++ b/tripleoclient/workflows/plan_management.py @@ -0,0 +1,89 @@ +# 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. +import tempfile +import uuid + +from tripleo_common.utils import tarball + +from tripleoclient import exceptions +from tripleoclient.workflows import base + + +def _upload_templates(swift_client, container_name, tht_root): + """tarball up a given directory and upload it to Swift to be extracted""" + + with tempfile.NamedTemporaryFile() as tmp_tarball: + tarball.create_tarball(tht_root, tmp_tarball.name) + tarball.tarball_extract_to_swift_container( + swift_client, tmp_tarball.name, container_name) + + +def create_default_plan(clients, **workflow_input): + workflow_client = clients.workflow_engine + tripleoclients = clients.tripleoclient + queue_name = workflow_input['queue_name'] + + execution = workflow_client.executions.create( + 'tripleo.plan_management.v1.create_default_deployment_plan', + workflow_input=workflow_input + ) + + with tripleoclients.messaging_websocket(queue_name) as ws: + payload = ws.wait_for_message(execution.id) + + if payload['status'] == 'SUCCESS': + print ("Default plan created") + else: + raise exceptions.WorkflowServiceError( + 'Exception creating plan: {}'.format(payload['message'])) + + +def create_deployment_plan(clients, **workflow_input): + + workflow_client = clients.workflow_engine + tripleoclients = clients.tripleoclient + queue_name = workflow_input['queue_name'] + + execution = workflow_client.executions.create( + 'tripleo.plan_management.v1.create_deployment_plan', + workflow_input=workflow_input + ) + + with tripleoclients.messaging_websocket(queue_name) as ws: + payload = ws.wait_for_message(execution.id) + + if payload['status'] == 'SUCCESS': + print ("Plan created") + else: + raise exceptions.WorkflowServiceError( + 'Exception creating plan: {}'.format(payload['message'])) + + +def list_deployment_plans(workflow_client, **input_): + return base.call_action(workflow_client, 'tripleo.list_plans', **input_) + + +def create_container(workflow_client, **input_): + return base.call_action(workflow_client, 'tripleo.create_container', + **input_) + + +def create_plan_from_templates(clients, name, tht_root): + workflow_client = clients.workflow_engine + swift_client = clients.tripleoclient.object_store + + print("Creating Swift container to store the plan") + create_container(workflow_client, container=name) + print("Creating plan from template files in: {}".format(tht_root)) + _upload_templates(swift_client, name, tht_root) + create_deployment_plan(clients, container=name, + queue_name=str(uuid.uuid4()))