13f2704c8e
This patch enables the swift upload for big files (+5GB) in TripleO. Also, adds support to the undercloud backup CLI to upload big backups to swift. Change-Id: I80163c8df15377dd3b5a44b5439a23223d8cccee Depends-On: Iff73833f1d470750862873f70a4a9aaba50bd164 Closes-Bug: 1761412
509 lines
19 KiB
Python
509 lines
19 KiB
Python
# Copyright 2016 Red Hat, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 logging
|
|
from operator import itemgetter
|
|
import shutil
|
|
import tempfile
|
|
import yaml
|
|
|
|
from heatclient import exc as heatexceptions
|
|
from mistral_lib import actions
|
|
from oslo_concurrency import processutils
|
|
import six
|
|
from swiftclient import exceptions as swiftexceptions
|
|
|
|
from tripleo_common.actions import base
|
|
from tripleo_common import constants
|
|
from tripleo_common import exception
|
|
from tripleo_common.utils import plan as plan_utils
|
|
from tripleo_common.utils import roles as roles_utils
|
|
from tripleo_common.utils import swift as swiftutils
|
|
from tripleo_common.utils import tarball
|
|
from tripleo_common.utils.validations import pattern_validator
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
default_container_headers = {
|
|
constants.TRIPLEO_META_USAGE_KEY: 'plan'
|
|
}
|
|
|
|
|
|
class CreateContainerAction(base.TripleOAction):
|
|
"""Creates an object container
|
|
|
|
This action creates an object container for a given name. If a container
|
|
with the same name already exists an exception is raised.
|
|
"""
|
|
|
|
def __init__(self, container):
|
|
super(CreateContainerAction, self).__init__()
|
|
self.container = container
|
|
|
|
def run(self, context):
|
|
oc = self.get_object_client(context)
|
|
|
|
# checks to see if a container has a valid name
|
|
if not pattern_validator(constants.PLAN_NAME_PATTERN, self.container):
|
|
message = ("Unable to create plan. The plan name must "
|
|
"only contain letters, numbers or dashes")
|
|
return actions.Result(error=message)
|
|
|
|
# checks to see if a container with that name exists
|
|
if self.container in [container["name"] for container in
|
|
oc.get_account()[1]]:
|
|
result_string = ("A container with the name %s already"
|
|
" exists.") % self.container
|
|
return actions.Result(error=result_string)
|
|
oc.put_container(self.container, headers=default_container_headers)
|
|
|
|
|
|
class ListPlansAction(base.TripleOAction):
|
|
"""Lists deployment plans
|
|
|
|
This action lists all deployment plans residing in the undercloud. A
|
|
deployment plan consists of a container marked with metadata
|
|
'x-container-meta-usage-tripleo'.
|
|
"""
|
|
|
|
def run(self, context):
|
|
# Plans consist of a container object marked with metadata to ensure it
|
|
# isn't confused with another container
|
|
plan_list = []
|
|
oc = self.get_object_client(context)
|
|
|
|
for item in oc.get_account()[1]:
|
|
container = oc.get_container(item['name'])[0]
|
|
if constants.TRIPLEO_META_USAGE_KEY in container.keys():
|
|
plan_list.append(item['name'])
|
|
return list(set(plan_list))
|
|
|
|
|
|
class DeletePlanAction(base.TripleOAction):
|
|
"""Deletes a plan and associated files
|
|
|
|
Deletes a plan by deleting the container matching plan_name. It
|
|
will not delete the plan if a stack exists with the same name.
|
|
|
|
Raises StackInUseError if a stack with the same name as plan_name
|
|
exists.
|
|
"""
|
|
|
|
def __init__(self, container):
|
|
super(DeletePlanAction, self).__init__()
|
|
self.container = container
|
|
|
|
def run(self, context):
|
|
error_text = None
|
|
# heat throws HTTPNotFound if the stack is not found
|
|
try:
|
|
stack = self.get_orchestration_client(context).stacks.get(
|
|
self.container
|
|
)
|
|
except heatexceptions.HTTPNotFound:
|
|
pass
|
|
else:
|
|
if stack is not None:
|
|
raise exception.StackInUseError(name=self.container)
|
|
|
|
try:
|
|
swift = self.get_object_client(context)
|
|
swiftutils.delete_container(swift, self.container)
|
|
swiftutils.delete_container(swift,
|
|
"%s-swift-rings" % self.container)
|
|
except swiftexceptions.ClientException as ce:
|
|
LOG.exception("Swift error deleting plan.")
|
|
error_text = ce.msg
|
|
except Exception as err:
|
|
LOG.exception("Error deleting plan.")
|
|
error_text = six.text_type(err)
|
|
|
|
if error_text:
|
|
return actions.Result(error=error_text)
|
|
|
|
|
|
class ListRolesAction(base.TripleOAction):
|
|
"""Returns a deployment plan's roles
|
|
|
|
DEPRECATED, please use tripleo.plan_management.v1.list_roles workflow
|
|
|
|
Parses roles_data.yaml and returns the names of all available roles.
|
|
|
|
:param container: name of the Swift container / plan name
|
|
:param detail: if false(default), displays role names only. if true,
|
|
returns all roles data
|
|
:return: list of roles in the container's deployment plan
|
|
"""
|
|
|
|
def __init__(self, container=constants.DEFAULT_CONTAINER_NAME,
|
|
role_file_name=constants.OVERCLOUD_J2_ROLES_NAME,
|
|
detail=False):
|
|
super(ListRolesAction, self).__init__()
|
|
self.container = container
|
|
self.role_file_name = role_file_name
|
|
self.detail = detail
|
|
|
|
def run(self, context):
|
|
try:
|
|
swift = self.get_object_client(context)
|
|
roles_data = yaml.safe_load(swift.get_object(
|
|
self.container, self.role_file_name)[1])
|
|
except Exception as err:
|
|
err_msg = ("Error retrieving roles data from deployment plan: %s"
|
|
% err)
|
|
LOG.exception(err_msg)
|
|
return actions.Result(error=err_msg)
|
|
|
|
if self.detail:
|
|
return roles_data
|
|
else:
|
|
return [role['name'] for role in roles_data]
|
|
|
|
|
|
class ExportPlanAction(base.TripleOAction):
|
|
"""Exports a deployment plan
|
|
|
|
This action exports a deployment plan with a given name. The plan
|
|
templates are downloaded from the Swift container, 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 run(self, context):
|
|
swift = self.get_object_client(context)
|
|
swift_service = self.get_object_service(context)
|
|
|
|
tmp_dir = tempfile.mkdtemp()
|
|
tarball_name = '%s.tar.gz' % self.plan
|
|
|
|
try:
|
|
swiftutils.download_container(swift, self.plan, tmp_dir)
|
|
swiftutils.create_and_upload_tarball(
|
|
swift_service, tmp_dir, self.exports_container, tarball_name,
|
|
delete_after=self.delete_after)
|
|
except swiftexceptions.ClientException as err:
|
|
msg = "Error attempting an operation on container: %s" % err
|
|
return actions.Result(error=msg)
|
|
except (OSError, IOError) as err:
|
|
msg = "Error while writing file: %s" % err
|
|
return actions.Result(error=msg)
|
|
except processutils.ProcessExecutionError as err:
|
|
msg = "Error while creating a tarball: %s" % err
|
|
return actions.Result(error=msg)
|
|
except Exception as err:
|
|
msg = "Error exporting plan: %s" % err
|
|
return actions.Result(error=msg)
|
|
finally:
|
|
shutil.rmtree(tmp_dir)
|
|
|
|
|
|
class UpdatePlanFromDirAction(base.TripleOAction):
|
|
"""Updates a plan and associated files
|
|
|
|
Updates a plan by comparing the current files with the new ones
|
|
provided:
|
|
Updates only new files from the plan
|
|
Add new files from the plan
|
|
|
|
:param container: name of the Swift container / plan name
|
|
"""
|
|
|
|
def __init__(self, container=constants.DEFAULT_CONTAINER_NAME,
|
|
templates_dir=constants.DEFAULT_TEMPLATES_PATH):
|
|
super(UpdatePlanFromDirAction, self).__init__()
|
|
self.container = container
|
|
self.templates_dir = templates_dir
|
|
|
|
def run(self, context):
|
|
try:
|
|
swift = self.get_object_client(context)
|
|
# Upload template dir to tmp container
|
|
container_tmp = '%s-tmp' % self.container
|
|
with tempfile.NamedTemporaryFile() as tmp_tarball:
|
|
tarball.create_tarball(self.templates_dir, tmp_tarball.name)
|
|
tarball.tarball_extract_to_swift_container(
|
|
swift,
|
|
tmp_tarball.name,
|
|
container_tmp)
|
|
# Get all new templates:
|
|
new_templates = swift.get_object(container_tmp,
|
|
'')[1].splitlines()
|
|
old_templates = swift.get_object(self.container,
|
|
'')[1].splitlines()
|
|
exclude_user_data = [constants.PLAN_ENVIRONMENT,
|
|
constants.OVERCLOUD_J2_ROLES_NAME,
|
|
constants.OVERCLOUD_J2_NETWORKS_NAME,
|
|
constants.OVERCLOUD_J2_EXCLUDES]
|
|
# Update the old container
|
|
for new in new_templates:
|
|
# if doesn't exist, push it:
|
|
if new not in old_templates:
|
|
swift.put_object(
|
|
self.container,
|
|
new,
|
|
swift.get_object(container_tmp, new)[1])
|
|
else:
|
|
content_new = swift.get_object(container_tmp, new)
|
|
content_old = swift.get_object(self.container, new)
|
|
if (not content_new == content_old and
|
|
new not in exclude_user_data):
|
|
swift.put_object(
|
|
self.container,
|
|
new,
|
|
swift.get_object(container_tmp, new)[1])
|
|
except swiftexceptions.ClientException as err:
|
|
msg = "Error attempting an operation on container: %s" % err
|
|
LOG.exception(msg)
|
|
return actions.Result(error=msg)
|
|
except Exception as err:
|
|
msg = "Error while updating plan: %s" % err
|
|
LOG.exception(msg)
|
|
return actions.Result(error=msg)
|
|
|
|
|
|
class UpdatePlanEnvironmentAction(base.TripleOAction):
|
|
"""Updates the plan environment values
|
|
|
|
Updates a plan environment values with the given parameters:
|
|
Add new parameter
|
|
Delete parameter
|
|
|
|
:param parameter: key value of the parameter to add or delete
|
|
:param value: value of the parameter to add or delete
|
|
:param delete: True if the parameter should be deleted from the env
|
|
:param env_key: environment key that should be one of the keys present
|
|
in the plan environment dictionary:
|
|
'passwords',
|
|
'description',
|
|
'parameter_defaults',
|
|
'environments',
|
|
'version',
|
|
'template',
|
|
'resource_registry',
|
|
'name'
|
|
:param container: name of the Swift container / plan name
|
|
"""
|
|
|
|
def __init__(self, parameter, env_key, value=None, delete=False,
|
|
container=constants.DEFAULT_CONTAINER_NAME):
|
|
super(UpdatePlanEnvironmentAction, self).__init__()
|
|
self.container = container
|
|
self.parameter = parameter
|
|
self.value = value
|
|
self.delete = delete
|
|
self.env_key = env_key
|
|
|
|
def run(self, context):
|
|
try:
|
|
swift = self.get_object_client(context)
|
|
plan_env = plan_utils.get_env(swift, self.container)
|
|
if self.env_key in plan_env.keys():
|
|
if self.delete:
|
|
try:
|
|
plan_env[self.env_key].pop(self.parameter)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
plan_env[self.env_key].update({self.parameter: self.value})
|
|
else:
|
|
msg = "The environment key doesn't exist: %s" % self.env_key
|
|
return actions.Result(error=msg)
|
|
except swiftexceptions.ClientException as err:
|
|
msg = "Error attempting an operation on container: %s" % err
|
|
return actions.Result(error=msg)
|
|
except Exception as err:
|
|
msg = "Error while updating plan: %s" % err
|
|
return actions.Result(error=msg)
|
|
|
|
|
|
class UpdateNetworksAction(base.TripleOAction):
|
|
def __init__(self, networks, current_networks, replace_all=False):
|
|
super(UpdateNetworksAction, self).__init__()
|
|
self.networks = networks
|
|
self.current_networks = current_networks
|
|
self.replace_all = replace_all
|
|
|
|
def run(self, context):
|
|
network_data_to_save = self.networks or []
|
|
|
|
# if replace_all flag is true, discard current networks and save input
|
|
# if replace_all flag is false, merge input into current networks
|
|
if not self.replace_all:
|
|
# merge the networks_data and the network_input into networks
|
|
# to be saved
|
|
network_data_to_save = [net for net in {
|
|
x['name']: x for x in
|
|
self.current_networks + self.networks
|
|
}.values()]
|
|
|
|
return actions.Result(data={'network_data': network_data_to_save})
|
|
|
|
|
|
class ValidateRolesDataAction(base.TripleOAction):
|
|
"""Validates Roles Data
|
|
|
|
Validates the format of input (verify that each role in input has the
|
|
required attributes set. see README in roles directory in t-h-t),
|
|
validates that roles in input exist in roles directory in deployment plan
|
|
"""
|
|
|
|
def __init__(self, roles, available_roles,
|
|
container=constants.DEFAULT_CONTAINER_NAME):
|
|
super(ValidateRolesDataAction, self).__init__()
|
|
self.container = container
|
|
self.roles = roles
|
|
self.available_roles = available_roles
|
|
|
|
def run(self, context):
|
|
err_msg = ""
|
|
# validate roles in input exist in roles directory in t-h-t
|
|
try:
|
|
roles_utils.check_role_exists(
|
|
[role['name'] for role in self.available_roles],
|
|
[role['name'] for role in self.roles])
|
|
except Exception as chk_err:
|
|
err_msg = str(chk_err)
|
|
|
|
# validate role yaml
|
|
for role in self.roles:
|
|
try:
|
|
roles_utils.validate_role_yaml(yaml.safe_dump([role]))
|
|
except exception.RoleMetadataError as rme:
|
|
if 'name' in role:
|
|
err_msg += "\n%s for %s" % (str(rme), role['name'])
|
|
else:
|
|
err_msg += "\n%s" % str(rme)
|
|
|
|
if err_msg:
|
|
return actions.Result(error=err_msg)
|
|
return actions.Result(data=True)
|
|
|
|
|
|
class UpdateRolesAction(base.TripleOAction):
|
|
"""Updates roles_data.yaml object in plan with given roles.
|
|
|
|
:param roles: role input data (json)
|
|
:param current_roles: data from roles_data.yaml file in plan (json)
|
|
:param replace_all: boolean value indicating if input roles should merge
|
|
with or replace data from roles_data.yaml. Defaults to False (merge)
|
|
:param container: name of the Swift container / plan name
|
|
"""
|
|
|
|
def __init__(self, roles, current_roles, replace_all=False,
|
|
container=constants.DEFAULT_CONTAINER_NAME):
|
|
super(UpdateRolesAction, self).__init__()
|
|
self.container = container
|
|
self.roles = roles
|
|
self.current_roles = current_roles
|
|
self.replace_all = replace_all
|
|
|
|
def run(self, context):
|
|
role_data_to_save = self.roles
|
|
|
|
# if replace_all flag is true, discard current roles and save input
|
|
# if replace_all flag is false, merge input into current roles
|
|
if not self.replace_all:
|
|
# merge the roles_data and the role_input into roles to be saved
|
|
role_data_to_save = [role for role in {
|
|
x['name']: x for x in
|
|
self.current_roles + self.roles
|
|
}.values()]
|
|
|
|
# ensure required primary tag exists in roles to be saved
|
|
primary = [role for role in role_data_to_save if
|
|
'tags' in role and 'primary' in role['tags']]
|
|
if len(primary) < 1:
|
|
# throw error
|
|
raise exception.RoleMetadataError("At least one role must contain"
|
|
" a 'primary' tag.")
|
|
|
|
# sort the data to have a predictable result
|
|
save_roles = sorted(role_data_to_save, key=itemgetter('name'),
|
|
reverse=True)
|
|
return actions.Result(data={'roles': save_roles})
|
|
|
|
|
|
class GatherRolesAction(actions.Action):
|
|
"""Gather role definitions
|
|
|
|
Check each role name from the input, check if it exists in
|
|
roles_data.yaml, if yes, use that role definition, if not, get the
|
|
role definition from roles directory. Return the gathered role
|
|
definitions.
|
|
"""
|
|
|
|
def __init__(self, role_names, current_roles, available_roles):
|
|
super(GatherRolesAction, self).__init__()
|
|
self.role_names = role_names
|
|
self.current_roles = current_roles
|
|
self.available_roles = available_roles
|
|
|
|
def run(self, context):
|
|
err_msgs = []
|
|
# merge the two lists of dicts in the proper order. last in wins, so
|
|
# a current role shall be favored over an available role.
|
|
gathered_roles = [role for role in {
|
|
x['name']: x for x in self.available_roles + self.current_roles
|
|
}.values() if role['name'] in self.role_names]
|
|
|
|
if err_msgs:
|
|
return actions.Result(error="/n".join(err_msgs))
|
|
|
|
return actions.Result(data={'gathered_roles': gathered_roles})
|
|
|
|
|
|
class RemoveNoopDeployStepAction(base.TripleOAction):
|
|
"""Remove all the pre, post and deploy step in the plan-environment.
|
|
|
|
:param container: name of the Swift container / plan name
|
|
"""
|
|
|
|
def __init__(self, container=constants.DEFAULT_CONTAINER_NAME):
|
|
super(RemoveNoopDeployStepAction, self).__init__()
|
|
self.container = container
|
|
|
|
def run(self, context):
|
|
# get the stack. Error if doesn't exist
|
|
heat = self.get_orchestration_client(context)
|
|
try:
|
|
stack = heat.stacks.get(self.container)
|
|
except heatexceptions.HTTPNotFound:
|
|
msg = "Error retrieving stack: %s" % self.container
|
|
LOG.exception(msg)
|
|
return actions.Result(error=msg)
|
|
|
|
swift = self.get_object_client(context)
|
|
plan_env = plan_utils.get_env(swift, self.container)
|
|
|
|
# Get output and check if DeployStep are None
|
|
steps = ['OS::TripleO::DeploymentSteps']
|
|
for output in stack.to_dict().get('outputs', {}):
|
|
if output['output_key'] == 'RoleData':
|
|
for role in output['output_value']:
|
|
steps.append("OS::TripleO::Tasks::%sPreConfig" % role)
|
|
steps.append("OS::TripleO::Tasks::%sPostConfig" % role)
|
|
# Remove noop Steps
|
|
for step in steps:
|
|
if step in plan_env.get('resource_registry', {}):
|
|
if plan_env['resource_registry'][step] == 'OS::Heat::None':
|
|
plan_env['resource_registry'].pop(step)
|
|
# Push plan_env
|
|
plan_utils.put_env(swift, plan_env)
|