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
|
os-cloud-config # Apache-2.0
|
||||||
websocket-client>=0.32.0 # LGPLv2+
|
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
|
-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_import = tripleoclient.v1.overcloud_node:ImportNode
|
||||||
overcloud_node_introspect = tripleoclient.v1.overcloud_node:IntrospectNode
|
overcloud_node_introspect = tripleoclient.v1.overcloud_node:IntrospectNode
|
||||||
overcloud_node_provide = tripleoclient.v1.overcloud_node:ProvideNode
|
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_delete = tripleoclient.v1.overcloud_plan:DeletePlan
|
||||||
overcloud_plan_list = tripleoclient.v1.overcloud_plan:ListPlans
|
overcloud_plan_list = tripleoclient.v1.overcloud_plan:ListPlans
|
||||||
overcloud_profiles_match = tripleoclient.v1.overcloud_profiles:MatchProfiles
|
overcloud_profiles_match = tripleoclient.v1.overcloud_profiles:MatchProfiles
|
||||||
|
@ -21,6 +21,7 @@ import uuid
|
|||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
from openstackclient.common import utils
|
from openstackclient.common import utils
|
||||||
|
from swiftclient import client as swift_client
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -143,7 +144,34 @@ class ClientWrapper(object):
|
|||||||
|
|
||||||
def __init__(self, instance):
|
def __init__(self, instance):
|
||||||
self._instance = instance
|
self._instance = instance
|
||||||
|
self._object_store = None
|
||||||
|
|
||||||
def messaging_websocket(self, queue_name='tripleo'):
|
def messaging_websocket(self, queue_name='tripleo'):
|
||||||
"""Returns a websocket for the messaging service"""
|
"""Returns a websocket for the messaging service"""
|
||||||
return WebsocketClient(self._instance, queue_name)
|
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 openstackclient.tests import utils
|
||||||
|
|
||||||
|
from tripleoclient import exceptions
|
||||||
from tripleoclient.v1 import overcloud_plan
|
from tripleoclient.v1 import overcloud_plan
|
||||||
|
|
||||||
|
|
||||||
@ -84,3 +85,143 @@ class TestOvercloudDeletePlan(utils.TestCommand):
|
|||||||
input={'container': 'test-plan1'}),
|
input={'container': 'test-plan1'}),
|
||||||
mock.call('tripleo.delete_plan',
|
mock.call('tripleo.delete_plan',
|
||||||
input={'container': 'test-plan2'})])
|
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 json
|
||||||
import logging
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
from cliff import command
|
from cliff import command
|
||||||
from cliff import lister
|
from cliff import lister
|
||||||
from openstackclient.i18n import _
|
from openstackclient.i18n import _
|
||||||
|
|
||||||
|
from tripleoclient.workflows import plan_management
|
||||||
|
|
||||||
|
|
||||||
class ListPlans(lister.Lister):
|
class ListPlans(lister.Lister):
|
||||||
"""List overcloud deployment plans."""
|
"""List overcloud deployment plans."""
|
||||||
@ -74,3 +77,38 @@ class DeletePlan(command.Command):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception(
|
self.log.exception(
|
||||||
"Error parsing action result %s", execution.output)
|
"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