Add plan export action and workflow
Implements: blueprint plan-export-action Change-Id: I789c960f61a30ccd4b076fcae4b3e1b80e825585
This commit is contained in:
parent
66a5f3bee4
commit
a3e0464657
@ -16,6 +16,8 @@ features:
|
||||
Mistral environment, so it can easily be imported and exported. Root
|
||||
template and root environment settings (previously stored in the
|
||||
capabilities map file) are now being stored in this file.
|
||||
- Add a new plan export action which exports contents of a deployment plan
|
||||
to a tarball and uploads the tarball to Swift.
|
||||
fixes:
|
||||
- Fixes `bug 1644756 <https://bugs.launchpad.net/tripleo/+bug/1644756>`__ so
|
||||
that flavour matching works as expected with the object-storage role.
|
||||
|
@ -86,6 +86,7 @@ mistral.actions =
|
||||
tripleo.plan.create_container = tripleo_common.actions.plan:CreateContainerAction
|
||||
tripleo.plan.delete = tripleo_common.actions.plan:DeletePlanAction
|
||||
tripleo.plan.list = tripleo_common.actions.plan:ListPlansAction
|
||||
tripleo.plan.export = tripleo_common.actions.plan:ExportPlanAction
|
||||
tripleo.role.list = tripleo_common.actions.plan:ListRolesAction
|
||||
tripleo.scale.delete_node = tripleo_common.actions.scale:ScaleDownAction
|
||||
tripleo.swift.tempurl = tripleo_common.actions.swifthelper:SwiftTempUrlAction
|
||||
|
@ -14,11 +14,15 @@
|
||||
# under the License.
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import yaml
|
||||
|
||||
from heatclient import exc as heatexceptions
|
||||
from mistral.workflow import utils as mistral_workflow_utils
|
||||
from mistralclient.api import base as mistralclient_base
|
||||
from oslo_concurrency import processutils
|
||||
import six
|
||||
from swiftclient import exceptions as swiftexceptions
|
||||
|
||||
@ -26,6 +30,7 @@ from tripleo_common.actions import base
|
||||
from tripleo_common import constants
|
||||
from tripleo_common import exception
|
||||
from tripleo_common.utils import swift as swiftutils
|
||||
from tripleo_common.utils import tarball
|
||||
from tripleo_common.utils.validations import pattern_validator
|
||||
|
||||
|
||||
@ -337,3 +342,90 @@ class ListRolesAction(base.TripleOAction):
|
||||
if details['type'] == constants.RESOURCE_GROUP_TYPE:
|
||||
roles.append(resource)
|
||||
return roles
|
||||
|
||||
|
||||
class ExportPlanAction(base.TripleOAction):
|
||||
"""Exports a deployment plan
|
||||
|
||||
This action exports a deployment plan with a given name. First, the plan
|
||||
templates are downloaded from the Swift container. Then the plan
|
||||
environment file is generated from the associated Mistral environment.
|
||||
Finally, both the templates and the plan environment file are packaged up
|
||||
in a tarball and uploaded to Swift.
|
||||
"""
|
||||
|
||||
def __init__(self, plan, delete_after, exports_container):
|
||||
super(ExportPlanAction, self).__init__()
|
||||
self.plan = plan
|
||||
self.delete_after = delete_after
|
||||
self.exports_container = exports_container
|
||||
|
||||
def _download_templates(self, swift, tmp_dir):
|
||||
"""Download templates to a temp folder."""
|
||||
template_files = swift.get_container(self.plan)[1]
|
||||
|
||||
for tf in template_files:
|
||||
filename = tf['name']
|
||||
contents = swift.get_object(self.plan, filename)[1]
|
||||
path = os.path.join(tmp_dir, filename)
|
||||
dirname = os.path.dirname(path)
|
||||
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
with open(path, 'w') as f:
|
||||
f.write(contents)
|
||||
|
||||
def _generate_plan_env_file(self, mistral, tmp_dir):
|
||||
"""Generate plan environment file and add it to specified folder."""
|
||||
environment = mistral.environments.get(self.plan).variables
|
||||
yaml_string = yaml.safe_dump(environment, default_flow_style=False)
|
||||
path = os.path.join(tmp_dir, constants.PLAN_ENVIRONMENT)
|
||||
|
||||
with open(path, 'w') as f:
|
||||
f.write(yaml_string)
|
||||
|
||||
def _create_and_upload_tarball(self, swift, tmp_dir):
|
||||
"""Create a tarball containing the tmp_dir and upload it to Swift."""
|
||||
tarball_name = '%s.tar.gz' % self.plan
|
||||
headers = {'X-Delete-After': self.delete_after}
|
||||
|
||||
# make sure the root container which holds all plan exports exists
|
||||
try:
|
||||
swift.get_container(self.exports_container)
|
||||
except swiftexceptions.ClientException:
|
||||
swift.put_container(self.exports_container)
|
||||
|
||||
with tempfile.NamedTemporaryFile() as tmp_tarball:
|
||||
tarball.create_tarball(tmp_dir, tmp_tarball.name)
|
||||
|
||||
swift.put_object(self.exports_container, tarball_name, tmp_tarball,
|
||||
headers=headers)
|
||||
|
||||
def run(self):
|
||||
swift = self.get_object_client()
|
||||
mistral = self.get_workflow_client()
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
|
||||
try:
|
||||
self._download_templates(swift, tmp_dir)
|
||||
self._generate_plan_env_file(mistral, tmp_dir)
|
||||
self._create_and_upload_tarball(swift, tmp_dir)
|
||||
except swiftexceptions.ClientException as err:
|
||||
msg = "Error attempting an operation on container: %s" % err
|
||||
return mistral_workflow_utils.Result(error=msg)
|
||||
except mistralclient_base.APIException:
|
||||
msg = ("The Mistral environment %s could not be found."
|
||||
% self.plan)
|
||||
return mistral_workflow_utils.Result(error=msg)
|
||||
except (OSError, IOError) as err:
|
||||
msg = "Error while writing file: %s" % err
|
||||
return mistral_workflow_utils.Result(error=msg)
|
||||
except processutils.ProcessExecutionError as err:
|
||||
msg = "Error while creating a tarball: %s" % err
|
||||
return mistral_workflow_utils.Result(error=msg)
|
||||
except Exception as err:
|
||||
msg = "Error exporting plan: %s" % err
|
||||
return mistral_workflow_utils.Result(error=msg)
|
||||
finally:
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
@ -18,6 +18,7 @@ import mock
|
||||
from heatclient import exc as heatexceptions
|
||||
from mistral.workflow import utils as mistral_workflow_utils
|
||||
from mistralclient.api import base as mistral_base
|
||||
from oslo_concurrency import processutils
|
||||
from swiftclient import exceptions as swiftexceptions
|
||||
|
||||
from tripleo_common.actions import plan
|
||||
@ -489,3 +490,103 @@ class RoleListActionTest(base.TestCase):
|
||||
self.assertEqual(expected, result)
|
||||
self.assertEqual('overcloud.yaml', template_name)
|
||||
swift.get_object.assert_called_with(self.container, template_name)
|
||||
|
||||
|
||||
class ExportPlanActionTest(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ExportPlanActionTest, self).setUp()
|
||||
self.plan = 'overcloud'
|
||||
self.delete_after = 3600
|
||||
self.exports_container = 'plan-exports'
|
||||
|
||||
# setup swift
|
||||
self.template_files = (
|
||||
'some-name.yaml',
|
||||
'some-other-name.yaml',
|
||||
'yet-some-other-name.yaml',
|
||||
'finally-another-name.yaml'
|
||||
)
|
||||
self.swift = mock.MagicMock()
|
||||
self.swift.get_container.return_value = (
|
||||
{'x-container-meta-usage-tripleo': 'plan'}, [
|
||||
{'name': tf} for tf in self.template_files
|
||||
]
|
||||
)
|
||||
self.swift.get_object.return_value = ({}, RESOURCES_YAML_CONTENTS)
|
||||
swift_patcher = mock.patch(
|
||||
'tripleo_common.actions.base.TripleOAction.get_object_client',
|
||||
return_value=self.swift)
|
||||
swift_patcher.start()
|
||||
self.addCleanup(swift_patcher.stop)
|
||||
|
||||
# setup mistral
|
||||
self.mistral = mock.MagicMock()
|
||||
env_item = mock.Mock()
|
||||
env_item.variables = {
|
||||
'template': 'overcloud.yaml',
|
||||
'environments': [
|
||||
{'path': 'overcloud-resource-registry-puppet.yaml'}
|
||||
]
|
||||
}
|
||||
self.mistral.environments.get.return_value = env_item
|
||||
mistral_patcher = mock.patch(
|
||||
'tripleo_common.actions.base.TripleOAction.get_workflow_client',
|
||||
return_value=self.mistral)
|
||||
mistral_patcher.start()
|
||||
self.addCleanup(mistral_patcher.stop)
|
||||
|
||||
@mock.patch('tripleo_common.utils.tarball.create_tarball')
|
||||
@mock.patch('tempfile.mkdtemp')
|
||||
def test_run_success(self, mock_mkdtemp, mock_create_tarball):
|
||||
get_object_mock_calls = [
|
||||
mock.call(self.plan, tf) for tf in self.template_files
|
||||
]
|
||||
get_container_mock_calls = [
|
||||
mock.call(self.plan),
|
||||
mock.call('plan-exports')
|
||||
]
|
||||
mock_mkdtemp.return_value = '/tmp/test123'
|
||||
|
||||
action = plan.ExportPlanAction(self.plan, self.delete_after,
|
||||
self.exports_container)
|
||||
action.run()
|
||||
|
||||
self.swift.get_container.assert_has_calls(get_container_mock_calls)
|
||||
self.swift.get_object.assert_has_calls(
|
||||
get_object_mock_calls, any_order=True)
|
||||
self.mistral.environments.get.assert_called_once_with(self.plan)
|
||||
self.swift.put_object.assert_called_once()
|
||||
mock_create_tarball.assert_called_once()
|
||||
|
||||
def test_run_container_does_not_exist(self):
|
||||
self.swift.get_container.side_effect = swiftexceptions.ClientException(
|
||||
self.plan)
|
||||
|
||||
action = plan.ExportPlanAction(self.plan, self.delete_after,
|
||||
self.exports_container)
|
||||
result = action.run()
|
||||
|
||||
error = "Error attempting an operation on container: %s" % self.plan
|
||||
self.assertIn(error, result.error)
|
||||
|
||||
def test_run_environment_does_not_exist(self):
|
||||
self.mistral.environments.get.side_effect = mistral_base.APIException
|
||||
|
||||
action = plan.ExportPlanAction(self.plan, self.delete_after,
|
||||
self.exports_container)
|
||||
result = action.run()
|
||||
|
||||
error = "The Mistral environment %s could not be found." % self.plan
|
||||
self.assertEqual(error, result.error)
|
||||
|
||||
@mock.patch('tripleo_common.utils.tarball.create_tarball')
|
||||
def test_run_error_creating_tarball(self, mock_create_tarball):
|
||||
mock_create_tarball.side_effect = processutils.ProcessExecutionError
|
||||
|
||||
action = plan.ExportPlanAction(self.plan, self.delete_after,
|
||||
self.exports_container)
|
||||
result = action.run()
|
||||
|
||||
error = "Error while creating a tarball"
|
||||
self.assertIn(error, result.error)
|
||||
|
@ -291,3 +291,62 @@ workflows:
|
||||
execution: <% execution() %>
|
||||
on-success:
|
||||
- fail: <% $.get('status') = "FAILED" %>
|
||||
|
||||
export_deployment_plan:
|
||||
description: Creates an export tarball for a given plan
|
||||
input:
|
||||
- plan
|
||||
- queue_name: tripleo
|
||||
|
||||
tasks:
|
||||
|
||||
export_plan:
|
||||
action: tripleo.plan.export
|
||||
input:
|
||||
plan: <% $.plan %>
|
||||
delete_after: 3600
|
||||
exports_container: "plan-exports"
|
||||
on-success: create_tempurl
|
||||
on-error: export_plan_set_status_failed
|
||||
|
||||
create_tempurl:
|
||||
action: tripleo.swift.tempurl
|
||||
on-success: set_status_success
|
||||
on-error: create_tempurl_set_status_failed
|
||||
input:
|
||||
container: "plan-exports"
|
||||
obj: "<% $.plan %>.tar.gz"
|
||||
valid: 3600
|
||||
|
||||
set_status_success:
|
||||
on-success: notify_zaqar
|
||||
publish:
|
||||
status: SUCCESS
|
||||
message: <% task(create_tempurl).result %>
|
||||
|
||||
export_plan_set_status_failed:
|
||||
on-success: notify_zaqar
|
||||
publish:
|
||||
status: FAILED
|
||||
message: <% task(export_plan).result %>
|
||||
|
||||
create_tempurl_set_status_failed:
|
||||
on-success: notify_zaqar
|
||||
publish:
|
||||
status: FAILED
|
||||
message: <% task(create_tempurl).result %>
|
||||
|
||||
notify_zaqar:
|
||||
action: zaqar.queue_post
|
||||
input:
|
||||
queue_name: <% $.queue_name %>
|
||||
messages:
|
||||
body:
|
||||
type: tripleo.plan_management.v1.export_deployment_plan
|
||||
payload:
|
||||
status: <% $.status %>
|
||||
message: <% $.message or '' %>
|
||||
execution: <% execution() %>
|
||||
tempurl: <% task(create_tempurl).result %>
|
||||
on-success:
|
||||
- fail: <% $.get('status') = "FAILED" %>
|
||||
|
Loading…
x
Reference in New Issue
Block a user