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
This commit is contained in:
Dougal Matthews 2016-08-24 11:47:30 +01:00
parent a42e858e96
commit 2c0fecf69c
7 changed files with 319 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@ -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'
})

View File

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

View File

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

View File

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