deb-python-fuelclient/fuelclient/commands/environment.py

887 lines
30 KiB
Python

# Copyright 2015 Mirantis, Inc.
#
# 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 abc
import argparse
import functools
import os
import shutil
import six
from cliff import show
from oslo_utils import fileutils
from fuelclient.cli import error
from fuelclient.commands import base
from fuelclient.common import data_utils
class EnvMixIn(object):
entity_name = 'environment'
supported_file_formats = ('json', 'yaml')
allowed_attr_types = ('network', 'settings')
@staticmethod
def source_dir(directory):
"""Check that the source directory exists and is readable.
:param directory: Path to source directory
:type directory: str
:return: Absolute path to source directory
:rtype: str
"""
path = os.path.abspath(directory)
if not os.path.isdir(path):
raise argparse.ArgumentTypeError(
'"{0}" is not a directory.'.format(path))
if not os.access(path, os.R_OK):
raise argparse.ArgumentTypeError(
'directory "{0}" is not readable'.format(path))
return path
@staticmethod
def destination_dir(directory):
"""Check that the destination directory exists and is writable.
:param directory: Path to destination directory
:type directory: str
:return: Absolute path to destination directory
:rtype: str
"""
path = os.path.abspath(directory)
if not os.path.isdir(path):
raise argparse.ArgumentTypeError(
'"{0}" is not a directory.'.format(path))
if not os.access(path, os.W_OK):
raise argparse.ArgumentTypeError(
'directory "{0}" is not writable'.format(path))
return path
@six.add_metaclass(abc.ABCMeta)
class BaseUploadCommand(EnvMixIn, base.BaseCommand):
@abc.abstractproperty
def uploader(self):
pass
@abc.abstractproperty
def attribute(self):
pass
def get_parser(self, prog_name):
parser = super(BaseUploadCommand, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of environment.')
parser.add_argument('-f',
'--format',
required=True,
choices=self.supported_file_formats,
help='Format of serialized '
'{}.'.format(self.attribute))
parser.add_argument('-d',
'--directory',
required=False,
default=os.curdir,
help='Source directory. Defaults to the '
'current directory.')
return parser
def take_action(self, parsed_args):
directory = parsed_args.directory
file_path = self.get_attributes_path(self.attribute,
parsed_args.format,
parsed_args.id,
directory)
try:
with open(file_path, 'r') as stream:
attribute = data_utils.safe_load(parsed_args.format, stream)
except (IOError, OSError):
msg = 'Could not read configuration of {} at {}.'
raise error.InvalidFileException(msg.format(self.attribute,
file_path))
self.uploader(parsed_args.id, attribute)
msg = ('Configuration of {t} for the environment with id '
'{env} was loaded from {path}\n')
self.app.stdout.write(msg.format(t=self.attribute,
env=parsed_args.id,
path=file_path))
@six.add_metaclass(abc.ABCMeta)
class BaseDownloadCommand(EnvMixIn, base.BaseCommand):
@abc.abstractproperty
def downloader(self):
pass
@abc.abstractproperty
def attribute(self):
pass
def get_parser(self, prog_name):
parser = super(BaseDownloadCommand, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of an environment.')
parser.add_argument('-f',
'--format',
required=True,
choices=self.supported_file_formats,
help='Format of serialized '
'{}.'.format(self.attribute))
parser.add_argument('-d',
'--directory',
required=False,
default=os.curdir,
help='Destination directory. Defaults to the '
'current directory.')
return parser
def take_action(self, parsed_args):
directory = parsed_args.directory or os.curdir
attributes = self.downloader(parsed_args.id)
file_path = self.get_attributes_path(self.attribute,
parsed_args.format,
parsed_args.id,
directory)
try:
fileutils.ensure_tree(os.path.dirname(file_path))
fileutils.delete_if_exists(file_path)
with open(file_path, 'w') as stream:
data_utils.safe_dump(parsed_args.format, stream, attributes)
except (IOError, OSError):
msg = 'Could not store configuration of {} at {}.'
raise error.InvalidFileException(msg.format(self.attribute,
file_path))
msg = ('Configuration of {t} for the environment with id '
'{env} was stored in {path}\n')
self.app.stdout.write(msg.format(t=self.attribute,
env=parsed_args.id,
path=file_path))
class EnvList(EnvMixIn, base.BaseListCommand):
"""Show list of all available environments."""
columns = ("id",
"status",
"name",
"release_id")
class EnvShow(EnvMixIn, base.BaseShowCommand):
"""Show info about environment with given id."""
columns = ("id",
"status",
"fuel_version",
"name",
"release_id",
"is_customized",
"changes")
class EnvCreate(EnvMixIn, base.BaseShowCommand):
"""Creates environment with given attributes."""
columns = EnvShow.columns
def get_parser(self, prog_name):
# Avoid adding id argument by BaseShowCommand
parser = show.ShowOne.get_parser(self, prog_name)
parser.add_argument(
'name',
type=str,
help='Name of the new environment'
)
parser.add_argument('-r',
'--release',
type=int,
required=True,
help='Id of the release for which will '
'be deployed')
parser.add_argument('-nst',
'--net-segmentation-type',
type=str,
choices=['vlan', 'gre', 'tun'],
dest='nst',
default='vlan',
help='Network segmentation type.\n'
'WARNING: GRE network segmentation type '
'is deprecated since 7.0 release.')
return parser
def take_action(self, parsed_args):
if parsed_args.nst == 'gre':
self.app.stderr.write('WARNING: GRE network segmentation type is '
'deprecated since 7.0 release')
new_env = self.client.create(name=parsed_args.name,
release_id=parsed_args.release,
net_segment_type=parsed_args.nst)
new_env = data_utils.get_display_data_single(self.columns, new_env)
return (self.columns, new_env)
class EnvDelete(EnvMixIn, base.BaseDeleteCommand):
"""Delete environment with given id."""
def get_parser(self, prog_name):
parser = super(EnvDelete, self).get_parser(prog_name)
parser.add_argument('-f',
'--force',
action='store_true',
help='Force-delete the environment.')
return parser
def take_action(self, parsed_args):
env = self.client.get_by_id(parsed_args.id)
if env['status'] == 'operational' and not parsed_args.force:
self.app.stdout.write("Deleting an operational environment is a "
"dangerous operation.\n"
"Please use --force to bypass this message.")
return
return super(EnvDelete, self).take_action(parsed_args)
class EnvUpdate(EnvMixIn, base.BaseShowCommand):
"""Change given attributes for an environment."""
columns = EnvShow.columns
def get_parser(self, prog_name):
# Avoid adding id argument by BaseShowCommand
parser = show.ShowOne.get_parser(self, prog_name)
parser.add_argument('id',
type=int,
help='Id of the nailgun entity to be processed.')
parser.add_argument('-n',
'--name',
type=str,
dest='name',
default=None,
help='New name for environment')
return parser
def take_action(self, parsed_args):
updates = {}
for attr in self.client._updatable_attributes:
if getattr(parsed_args, attr, None):
updates[attr] = getattr(parsed_args, attr)
updated_env = self.client.update(environment_id=parsed_args.id,
**updates)
updated_env = data_utils.get_display_data_single(self.columns,
updated_env)
return (self.columns, updated_env)
class EnvAddNodes(EnvMixIn, base.BaseCommand):
"""Adds nodes to an environment with the specified roles."""
def get_parser(self, prog_name):
parser = super(EnvAddNodes, self).get_parser(prog_name)
parser.add_argument('-e',
'--env',
type=int,
required=True,
help='Id of the environment to add nodes to')
parser.add_argument('-n',
'--nodes',
type=int,
nargs='+',
required=True,
help='Ids of the nodes to add.')
parser.add_argument('-r',
'--roles',
type=str,
nargs='+',
required=True,
help='Target roles of the nodes.')
return parser
def take_action(self, parsed_args):
env_id = parsed_args.env
self.client.add_nodes(environment_id=env_id,
nodes=parsed_args.nodes,
roles=parsed_args.roles)
msg = 'Nodes {n} were added to the environment {e} with roles {r}\n'
self.app.stdout.write(msg.format(n=parsed_args.nodes,
e=parsed_args.env,
r=parsed_args.roles))
class EnvRemoveNodes(EnvMixIn, base.BaseCommand):
"""Removes nodes from an environment."""
def get_parser(self, prog_name):
parser = super(EnvRemoveNodes, self).get_parser(prog_name)
parser.add_argument('-e',
'--env',
type=int,
required=True,
help='Id of the environment to remove nodes from')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-n',
'--nodes',
type=int,
nargs='+',
help='Ids of the nodes to remove.')
group.add_argument('--nodes-all',
action='store_true',
help='Remove all nodes from environment')
return parser
def take_action(self, parsed_args):
nodes = None if parsed_args.nodes_all else parsed_args.nodes
self.client.remove_nodes(environment_id=parsed_args.env,
nodes=nodes)
msg = 'Nodes were removed from the environment with id={e}\n'.format(
e=parsed_args.env)
self.app.stdout.write(msg)
class EnvDeploy(EnvMixIn, base.BaseCommand):
"""Deploys changes on the specified environment."""
def get_parser(self, prog_name):
parser = super(EnvDeploy, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of the environment to be deployed.')
dry_run_help_string = 'Specifies to dry-run a deployment by' \
'configuring task executor to dump the' \
'deployment graph to a dot file.' \
'Store cluster settings and serialized ' \
'data in the db and ask the task executor ' \
'to dump the resulting graph into a dot file'
noop_run_help_string = 'Specifies noop-run deployment ' \
'configuring tasks executor to run ' \
'puppet and shell tasks in noop mode and ' \
'skip all other. Stores noop-run result ' \
'summary in nailgun database'
parser.add_argument(
'-d', '--dry-run', dest="dry_run",
action='store_true', help=dry_run_help_string)
parser.add_argument(
'--noop', dest="noop_run",
action='store_true', help=noop_run_help_string)
return parser
def take_action(self, parsed_args):
task_id = self.client.deploy_changes(parsed_args.id,
dry_run=parsed_args.dry_run,
noop_run=parsed_args.noop_run)
msg = 'Deployment task with id {t} for the environment {e} '\
'has been started.\n'.format(t=task_id, e=parsed_args.id)
self.app.stdout.write(msg)
class EnvRedeploy(EnvDeploy):
"""Redeploys changes on the specified environment."""
def take_action(self, parsed_args):
task_id = self.client.redeploy_changes(parsed_args.id,
dry_run=parsed_args.dry_run,
noop_run=parsed_args.noop_run)
msg = 'Deployment task with id {t} for the environment {e} '\
'has been started.\n'.format(t=task_id, e=parsed_args.id)
self.app.stdout.write(msg)
class EnvProvisionNodes(EnvMixIn, base.BaseCommand):
"""Provision specified nodes for a specified environment."""
def get_parser(self, prog_name):
parser = super(EnvProvisionNodes, self).get_parser(prog_name)
parser.add_argument('-e',
'--env',
required=True,
type=int,
help='Id of the environment.')
parser.add_argument('-n',
'--nodes',
required=True,
type=int,
nargs='+',
help='Ids of nodes to provision.')
return parser
def take_action(self, parsed_args):
node_ids = parsed_args.nodes
task = self.client.provision_nodes(parsed_args.env, node_ids)
msg = ('Provisioning task with id {t} for the nodes {n} '
'within the environment {e} has been '
'started.\n').format(t=task['id'],
e=parsed_args.env,
n=', '.join(str(i) for i in node_ids))
self.app.stdout.write(msg)
class EnvDeployNodes(EnvMixIn, base.BaseCommand):
"""Deploy specified nodes for a specified environment."""
def get_parser(self, prog_name):
parser = super(EnvDeployNodes, self).get_parser(prog_name)
parser.add_argument('-e',
'--env',
required=True,
type=int,
help='Id of the environment.')
parser.add_argument('-n',
'--nodes',
required=True,
type=int,
nargs='+',
help='Ids of nodes to deploy.')
parser.add_argument('-f',
'--force',
action='store_true',
help='Force deploy nodes.')
noop_run_help_string = 'Specifies noop-run deployment ' \
'configuring tasks executor to run ' \
'puppet and shell tasks in noop mode and ' \
'skip all other. Stores noop-run result ' \
'summary in nailgun database'
parser.add_argument('--noop', dest="noop_run", action='store_true',
help=noop_run_help_string)
return parser
def take_action(self, parsed_args):
node_ids = parsed_args.nodes
task = self.client.deploy_nodes(parsed_args.env, node_ids,
force=parsed_args.force,
noop_run=parsed_args.noop_run)
msg = ('Deployment task with id {t} for the nodes {n} within '
'the environment {e} has been '
'started.\n').format(t=task['id'],
e=parsed_args.env,
n=', '.join(str(i) for i in node_ids))
self.app.stdout.write(msg)
class EnvSpawnVms(EnvMixIn, base.BaseCommand):
"""Provision specified environment."""
def get_parser(self, prog_name):
parser = super(EnvSpawnVms, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of the environment to be provision.')
return parser
def take_action(self, parsed_args):
return self.client.spawn_vms(parsed_args.id)
class EnvNetworkVerify(EnvMixIn, base.BaseCommand):
"""Run network verification for specified environment."""
def get_parser(self, prog_name):
parser = super(EnvNetworkVerify, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of the environment to verify network.')
return parser
def take_action(self, parsed_args):
task = self.client.verify_network(parsed_args.id)
msg = 'Network verification task with id {t} for the environment {e} '\
'has been started.\n'.format(t=task['id'], e=parsed_args.id)
self.app.stdout.write(msg)
class EnvNetworkUpload(BaseUploadCommand):
"""Upload network configuration and apply it to an environment."""
attribute = 'network'
@property
def uploader(self):
return self.client.set_network_configuration
class EnvNetworkDownload(BaseDownloadCommand):
"""Download and store network configuration of an environment."""
attribute = 'network'
@property
def downloader(self):
return self.client.get_network_configuration
class EnvSettingsUpload(BaseUploadCommand):
"""Upload and apply environment settings."""
attribute = 'settings'
@property
def uploader(self):
return functools.partial(self.client.set_settings,
force=self.force_flag)
def get_parser(self, prog_name):
parser = super(EnvSettingsUpload, self).get_parser(prog_name)
parser.add_argument('--force',
action='store_true',
help='Force applying the settings.')
return parser
def take_action(self, parsed_args):
self.force_flag = parsed_args.force
super(EnvSettingsUpload, self).take_action(parsed_args)
class EnvSettingsDownload(BaseDownloadCommand):
"""Download and store environment settings."""
attribute = 'settings'
@property
def downloader(self):
return self.client.get_settings
class FactsMixIn(object):
@staticmethod
def _get_fact_dir(env_id, fact_type, directory):
return os.path.join(directory, "{0}_{1}".format(fact_type, env_id))
@staticmethod
def _read_deployment_facts_from_file(directory, file_format):
return list(six.moves.map(
lambda f: data_utils.read_from_file(f),
[os.path.join(directory, file_name)
for file_name in os.listdir(directory)
if file_format == os.path.splitext(file_name)[1].lstrip('.')]
))
@staticmethod
def _read_provisioning_facts_from_file(directory, file_format):
node_facts = list(six.moves.map(
lambda f: data_utils.read_from_file(f),
[os.path.join(directory, file_name)
for file_name in os.listdir(directory)
if file_format == os.path.splitext(file_name)[1].lstrip('.')
and 'engine' != os.path.splitext(file_name)[0]]
))
engine_facts = None
engine_file = os.path.join(directory,
"{}.{}".format('engine', file_format))
if os.path.lexists(engine_file):
engine_facts = data_utils.read_from_file(engine_file)
return {'engine': engine_facts, 'nodes': node_facts}
@staticmethod
def _write_deployment_facts_to_file(facts, directory, file_format):
# from 9.0 the deployment info is serialized only per node
for _fact in facts:
file_name = "{role}_{uid}." if 'role' in _fact else "{uid}."
file_name += file_format
data_utils.write_to_file(
os.path.join(directory, file_name.format(**_fact)),
_fact)
@staticmethod
def _write_provisioning_facts_to_file(facts, directory, file_format):
file_name = "{uid}."
file_name += file_format
data_utils.write_to_file(
os.path.join(directory, file_name.format(uid='engine')),
facts['engine'])
for _fact in facts['nodes']:
data_utils.write_to_file(
os.path.join(directory, file_name.format(**_fact)),
_fact)
def download(self, env_id, fact_type, destination_dir, file_format,
nodes=None, default=False, split=None):
facts = self.client.download_facts(
env_id, fact_type, nodes=nodes, default=default, split=split)
facts_dir = self._get_fact_dir(env_id, fact_type, destination_dir)
if os.path.exists(facts_dir):
shutil.rmtree(facts_dir)
os.makedirs(facts_dir)
getattr(self, "_write_{0}_facts_to_file".format(fact_type))(
facts, facts_dir, file_format)
return facts_dir
def upload(self, env_id, fact_type, source_dir, file_format):
facts_dir = self._get_fact_dir(env_id, fact_type, source_dir)
facts = getattr(self, "_read_{0}_facts_from_file".format(fact_type))(
facts_dir, file_format)
if not facts \
or isinstance(facts, dict) and not six.moves.reduce(
lambda a, b: a or b, facts.values()):
raise error.ServerDataException(
"There are no {} facts for this environment!".format(
fact_type))
return self.client.upload_facts(env_id, fact_type, facts)
class BaseEnvFactsDelete(EnvMixIn, base.BaseCommand):
"""Delete current various facts for orchestrator."""
fact_type = ''
def get_parser(self, prog_name):
parser = super(BaseEnvFactsDelete, self).get_parser(prog_name)
parser.add_argument(
'id',
type=int,
help='ID of the environment')
return parser
def take_action(self, parsed_args):
self.client.delete_facts(parsed_args.id, self.fact_type)
self.app.stdout.write(
"{0} facts for the environment {1} were deleted "
"successfully.\n".format(self.fact_type.capitalize(),
parsed_args.id)
)
class EnvDeploymentFactsDelete(BaseEnvFactsDelete):
"""Delete current deployment facts."""
fact_type = 'deployment'
class EnvProvisioningFactsDelete(BaseEnvFactsDelete):
"""Delete current provisioning facts."""
fact_type = 'provisioning'
class BaseEnvFactsDownload(FactsMixIn, EnvMixIn, base.BaseCommand):
"""Download various facts for orchestrator."""
fact_type = ''
fact_default = False
def get_parser(self, prog_name):
parser = super(BaseEnvFactsDownload, self).get_parser(prog_name)
parser.add_argument(
'-e', '--env',
type=int,
required=True,
help='ID of the environment')
parser.add_argument(
'-d', '--directory',
type=self.destination_dir,
default=os.path.curdir,
help='Path to directory to save {} facts. '
'Defaults to the current directory'.format(self.fact_type))
parser.add_argument(
'-n', '--nodes',
type=int,
nargs='+',
help='Get {} facts for nodes with given IDs'.format(
self.fact_type))
parser.add_argument(
'-f', '--format',
choices=self.supported_file_formats,
required=True,
help='Format of serialized {} facts'.format(self.fact_type))
parser.add_argument(
'--no-split',
action='store_false',
dest='split',
default=True,
help='Do not split deployment info for node and cluster parts.'
)
return parser
def take_action(self, parsed_args):
facts_dir = self.download(
parsed_args.env,
self.fact_type,
parsed_args.directory,
parsed_args.format,
nodes=parsed_args.nodes,
default=self.fact_default,
split=parsed_args.split
)
self.app.stdout.write(
"{0} {1} facts for the environment {2} "
"were downloaded to {3}\n".format(
'Default' if self.fact_default else 'User-defined',
self.fact_type,
parsed_args.env,
facts_dir)
)
class EnvDeploymentFactsDownload(BaseEnvFactsDownload):
"""Download the user-defined deployment facts."""
fact_type = 'deployment'
fact_default = False
class EnvDeploymentFactsGetDefault(BaseEnvFactsDownload):
"""Download the default deployment facts."""
fact_type = 'deployment'
fact_default = True
class EnvProvisioningFactsDownload(BaseEnvFactsDownload):
"""Download the user-defined provisioning facts."""
fact_type = 'provisioning'
fact_default = False
class EnvProvisioningFactsGetDefault(BaseEnvFactsDownload):
"""Download the default provisioning facts."""
fact_type = 'provisioning'
fact_default = True
class BaseEnvFactsUpload(FactsMixIn, EnvMixIn, base.BaseCommand):
"""Upload various facts for orchestrator."""
fact_type = ''
def get_parser(self, prog_name):
parser = super(BaseEnvFactsUpload, self).get_parser(prog_name)
parser.add_argument(
'-e', '--env',
type=int,
required=True,
help='ID of the environment')
parser.add_argument(
'-d', '--directory',
type=self.source_dir,
default=os.path.curdir,
help='Path to directory to read {} facts. '
'Defaults to the current directory'.format(self.fact_type))
parser.add_argument(
'-f', '--format',
choices=self.supported_file_formats,
required=True,
help='Format of serialized {} facts'.format(self.fact_type))
return parser
def take_action(self, parsed_args):
self.upload(
parsed_args.env,
self.fact_type,
parsed_args.directory,
parsed_args.format
)
self.app.stdout.write(
"{0} facts for the environment {1} were uploaded "
"successfully.\n".format(self.fact_type.capitalize(),
parsed_args.env)
)
class EnvDeploymentFactsUpload(BaseEnvFactsUpload):
"""Upload deployment facts."""
fact_type = 'deployment'
class EnvProvisioningFactsUpload(BaseEnvFactsUpload):
"""Upload provisioning facts."""
fact_type = 'provisioning'