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:
parent
a42e858e96
commit
2c0fecf69c
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
})
|
||||
|
@ -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()))
|
||||
|
21
tripleoclient/workflows/base.py
Normal file
21
tripleoclient/workflows/base.py
Normal 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']
|
89
tripleoclient/workflows/plan_management.py
Normal file
89
tripleoclient/workflows/plan_management.py
Normal 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()))
|
Loading…
Reference in New Issue
Block a user