Add plan export action and workflow

Implements: blueprint plan-export-action

Change-Id: I789c960f61a30ccd4b076fcae4b3e1b80e825585
This commit is contained in:
Ana Krivokapic 2017-01-19 17:32:40 +01:00
parent 66a5f3bee4
commit a3e0464657
5 changed files with 255 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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