Retire repository
Fuel (from openstack namespace) and fuel-ccp (in x namespace) repositories are unused and ready to retire. This change removes all content from the repository and adds the usual README file to point out that the repository is retired following the process from https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project See also http://lists.openstack.org/pipermail/openstack-discuss/2019-December/011647.html Depends-On: https://review.opendev.org/699362 Change-Id: I1cdc15fbd41ebea088c76729abf6b120165b9ed1
This commit is contained in:
parent
9d25aed1cf
commit
2b3cb11e88
31
.gitignore
vendored
31
.gitignore
vendored
@ -1,31 +0,0 @@
|
||||
*.pyc
|
||||
*.sqlite
|
||||
|
||||
*.gem
|
||||
|
||||
# vim swap files
|
||||
.*.swp
|
||||
|
||||
# services' runtime files
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
# Vagrant housekeeping file
|
||||
.vagrant
|
||||
|
||||
build
|
||||
dist
|
||||
lock
|
||||
|
||||
*.egg
|
||||
.testrepository
|
||||
.tox
|
||||
.venv
|
||||
.idea
|
||||
.DS_Store
|
||||
test_run/*
|
||||
|
||||
*.egg-info
|
||||
|
||||
fuelclient-*.xml
|
||||
.coverage
|
@ -1,5 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_LOG_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./fuelclient/tests/unit} $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
||||
test_run_concurrency=echo 1
|
69
MAINTAINERS
69
MAINTAINERS
@ -1,69 +0,0 @@
|
||||
---
|
||||
description:
|
||||
For Fuel team structure and contribution policy, see [1].
|
||||
|
||||
This is repository level MAINTAINERS file. All contributions to this
|
||||
repository must be approved by one or more Core Reviewers [2].
|
||||
If you are contributing to files (or create new directories) in
|
||||
root folder of this repository, please contact Core Reviewers for
|
||||
review and merge requests.
|
||||
|
||||
If you are contributing to subfolders of this repository, please
|
||||
check 'maintainers' section of this file in order to find maintainers
|
||||
for those specific modules.
|
||||
|
||||
It is mandatory to get +1 from one or more maintainers before asking
|
||||
Core Reviewers for review/merge in order to decrease a load on Core Reviewers [3].
|
||||
Exceptions are when maintainers are actually cores, or when maintainers
|
||||
are not available for some reason (e.g. on vacation).
|
||||
|
||||
[1] http://specs.openstack.org/openstack/fuel-specs/policy/team-structure.html
|
||||
[2] https://review.openstack.org/#/admin/groups/551,members
|
||||
[3] http://lists.openstack.org/pipermail/openstack-dev/2015-August/072406.html
|
||||
|
||||
Please keep this file in YAML format in order to allow helper scripts
|
||||
to read this as a configuration data.
|
||||
|
||||
maintainers:
|
||||
|
||||
- ./:
|
||||
- name: Alexander Saprykin
|
||||
email: asaprykin@mirantis.com
|
||||
IRC: asaprykin
|
||||
|
||||
- name: Bulat Gaifullin
|
||||
email: bgaifullin@mirantis.com
|
||||
IRC: bgaifullin
|
||||
|
||||
- name: Maciej Kwiek
|
||||
email: mkwiek@mirantis.com
|
||||
IRC: mkwiek
|
||||
|
||||
- name: Sylwester Brzeczkowski
|
||||
email: sbrzeczkowski@mirantis.com
|
||||
IRC: sylwesterB
|
||||
|
||||
- specs/:
|
||||
- name: Mikhail Ivanov
|
||||
email: mivanov@mirantis.com
|
||||
IRC: mivanov
|
||||
|
||||
- name: Artem Silenkov
|
||||
email: asilenkov@mirantis.com
|
||||
IRC: asilenkov
|
||||
|
||||
- name: Alexander Tsamutali
|
||||
email: atsamutali@mirantis.com
|
||||
IRC: astsmtl
|
||||
|
||||
- name: Daniil Trishkin
|
||||
email: dtrishkin@mirantis.com
|
||||
IRC: dtrishkin
|
||||
|
||||
- name: Ivan Udovichenko
|
||||
email: iudovichenko@mirantis.com
|
||||
IRC: tlbr
|
||||
|
||||
- name: Igor Yozhikov
|
||||
email: iyozhikov@mirantis.com
|
||||
IRC: IgorYozhikov
|
@ -1 +0,0 @@
|
||||
include fuelclient/fuel_client.yaml
|
44
README.rst
44
README.rst
@ -1,38 +1,10 @@
|
||||
========================
|
||||
Team and repository tags
|
||||
========================
|
||||
This project is no longer maintained.
|
||||
|
||||
.. image:: http://governance.openstack.org/badges/python-fuelclient.svg
|
||||
:target: http://governance.openstack.org/reference/tags/index.html
|
||||
|
||||
.. Change things from this point on
|
||||
|
||||
python-fuelclient
|
||||
=================
|
||||
|
||||
python-fuelclient provides a CLI tool and a Python API wrapper for interacting
|
||||
with `Fuel <https://github.com/stackforge/fuel-web>`_.
|
||||
|
||||
|
||||
-----------------
|
||||
Project resources
|
||||
-----------------
|
||||
|
||||
Project status, bugs, and blueprints are tracked on Launchpad:
|
||||
https://launchpad.net/fuel
|
||||
|
||||
Development documentation is hosted here:
|
||||
https://docs.fuel-infra.org/fuel-dev
|
||||
|
||||
User guide can be found here:
|
||||
http://docs.mirantis.com
|
||||
|
||||
Any additional information can be found on the Fuel's project wiki
|
||||
https://wiki.openstack.org/wiki/Fuel
|
||||
|
||||
Anyone wishing to contribute to python-fuelclient should follow the general
|
||||
OpenStack process. A good reference for it can be found here:
|
||||
https://wiki.openstack.org/wiki/How_To_Contribute
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
For any further questions, please email
|
||||
openstack-discuss@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
||||
|
@ -1,90 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
# DO NOT PUT ANY IMPORTS HERE BECAUSE THIS FILE IS USED
|
||||
# DURING THE INSTALLATION.
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
try:
|
||||
__version__ = pkg_resources.get_distribution(
|
||||
"python-fuelclient").version
|
||||
except pkg_resources.DistributionNotFound:
|
||||
__version__ = ""
|
||||
except ImportError:
|
||||
__version__ = ""
|
||||
|
||||
|
||||
def connect(host, port, http_proxy=None, os_username=None, os_password=None,
|
||||
os_tenant_name=None, debug=False):
|
||||
"""Creates API connection."""
|
||||
from fuelclient import client
|
||||
|
||||
return client.APIClient(
|
||||
host, port, http_proxy=http_proxy, os_username=os_username,
|
||||
os_password=os_password, os_tenant_name=os_tenant_name, debug=debug)
|
||||
|
||||
|
||||
def get_client(resource, version='v1', connection=None):
|
||||
"""Gets an API client for a resource
|
||||
|
||||
python-fuelclient provides access to Fuel's API
|
||||
through a set of per-resource facades. In order to
|
||||
get a proper facade it's necessary to specify the name
|
||||
of the API resource and the version of Fuel's API.
|
||||
|
||||
:param resource: Name of the resource to get a facade for.
|
||||
:type resource: str
|
||||
Valid values are environment, node and task
|
||||
:param version: Version of the Fuel's API
|
||||
:type version: str,
|
||||
Available: v1. Default: v1.
|
||||
:param connection: API connection
|
||||
:type connection: fuelclient.client.APIClient
|
||||
:return: Facade to the specified resource that wraps
|
||||
calls to the specified version of the API.
|
||||
|
||||
"""
|
||||
from fuelclient import v1
|
||||
|
||||
version_map = {
|
||||
'v1': {
|
||||
'cluster-settings': v1.cluster_settings,
|
||||
'deployment_history': v1.deployment_history,
|
||||
'deployment-info': v1.deployment_info,
|
||||
'environment': v1.environment,
|
||||
'extension': v1.extension,
|
||||
'fuel-version': v1.fuelversion,
|
||||
'graph': v1.graph,
|
||||
'health': v1.health,
|
||||
'network-configuration': v1.network_configuration,
|
||||
'network-group': v1.network_group,
|
||||
'node': v1.node,
|
||||
'openstack-config': v1.openstack_config,
|
||||
'plugins': v1.plugins,
|
||||
'release': v1.release,
|
||||
'role': v1.role,
|
||||
'sequence': v1.sequence,
|
||||
'snapshot': v1.snapshot,
|
||||
'task': v1.task,
|
||||
'tag': v1.tag,
|
||||
'vip': v1.vip
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
return version_map[version][resource].get_client(connection)
|
||||
except KeyError:
|
||||
msg = 'Cannot load API client for "{r}" in the API version "{v}".'
|
||||
raise ValueError(msg.format(r=resource, v=version))
|
@ -1,19 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
"""fuelclient.cli sub-module contains functionality of
|
||||
fuelclient command line interface
|
||||
|
||||
|
||||
"""
|
@ -1,80 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
"""fuelclient.cli.actions sub-module contains files with action classes
|
||||
which implement command line interface logic
|
||||
|
||||
All action classes must be added to action_tuple to be used by parser
|
||||
"""
|
||||
from fuelclient.cli.actions.deploy import DeployChangesAction
|
||||
from fuelclient.cli.actions.deploy import RedeployChangesAction
|
||||
from fuelclient.cli.actions.deployment_tasks import DeploymentTasksAction
|
||||
from fuelclient.cli.actions.environment import EnvironmentAction
|
||||
from fuelclient.cli.actions.fact import DeploymentAction
|
||||
from fuelclient.cli.actions.fact import ProvisioningAction
|
||||
from fuelclient.cli.actions.graph import GraphAction
|
||||
from fuelclient.cli.actions.token import TokenAction
|
||||
from fuelclient.cli.actions.health import HealthCheckAction
|
||||
from fuelclient.cli.actions.interrupt import ResetAction
|
||||
from fuelclient.cli.actions.interrupt import StopAction
|
||||
from fuelclient.cli.actions.network import NetworkAction
|
||||
from fuelclient.cli.actions.network import NetworkTemplateAction
|
||||
from fuelclient.cli.actions.network_group import NetworkGroupAction
|
||||
from fuelclient.cli.actions.node import NodeAction
|
||||
from fuelclient.cli.actions.nodegroup import NodeGroupAction
|
||||
from fuelclient.cli.actions.notifications import NotificationsAction
|
||||
from fuelclient.cli.actions.notifications import NotifyAction
|
||||
from fuelclient.cli.actions.openstack_config import OpenstackConfigAction
|
||||
from fuelclient.cli.actions.release import ReleaseAction
|
||||
from fuelclient.cli.actions.role import RoleAction
|
||||
from fuelclient.cli.actions.settings import SettingsAction
|
||||
from fuelclient.cli.actions.snapshot import SnapshotAction
|
||||
from fuelclient.cli.actions.user import UserAction
|
||||
from fuelclient.cli.actions.plugins import PluginAction
|
||||
from fuelclient.cli.actions.fuelversion import FuelVersionAction
|
||||
from fuelclient.cli.actions.vip import VIPAction
|
||||
|
||||
actions_tuple = (
|
||||
DeployChangesAction,
|
||||
DeploymentAction,
|
||||
DeploymentTasksAction,
|
||||
EnvironmentAction,
|
||||
FuelVersionAction,
|
||||
GraphAction,
|
||||
HealthCheckAction,
|
||||
NetworkAction,
|
||||
NetworkGroupAction,
|
||||
NetworkTemplateAction,
|
||||
NodeAction,
|
||||
NodeGroupAction,
|
||||
NotificationsAction,
|
||||
NotifyAction,
|
||||
OpenstackConfigAction,
|
||||
PluginAction,
|
||||
ProvisioningAction,
|
||||
RedeployChangesAction,
|
||||
ReleaseAction,
|
||||
ResetAction,
|
||||
RoleAction,
|
||||
SettingsAction,
|
||||
SnapshotAction,
|
||||
StopAction,
|
||||
TokenAction,
|
||||
UserAction,
|
||||
VIPAction,
|
||||
)
|
||||
|
||||
actions = dict(
|
||||
(action.action_name, action())
|
||||
for action in actions_tuple
|
||||
)
|
@ -1,129 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from functools import partial
|
||||
from functools import wraps
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.formatting import quote_and_join
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.client import DefaultAPIClient
|
||||
|
||||
|
||||
class Action(object):
|
||||
"""Action class generalizes logic of action execution
|
||||
method action_func - entry point of parser with parsed arguments
|
||||
|
||||
flag_func_map - is tuple of pairs ("flag", self.some_method) where
|
||||
"flag" is name of argument which causes "some_method" to be called.
|
||||
None is used as "flag" when method will be called without any flag.
|
||||
|
||||
serializer - is Serializer class instance which supposed to be the
|
||||
only way to read and write to output or file system.
|
||||
|
||||
args - tuple of function calls of functions from arguments module,
|
||||
is a manifest of all arguments used in action, and is used to initialize
|
||||
argparse subparser of that action.
|
||||
"""
|
||||
def __init__(self):
|
||||
# Mapping of flags to methods
|
||||
self.flag_func_map = None
|
||||
self.serializer = Serializer()
|
||||
|
||||
def action_func(self, params):
|
||||
"""Entry point for all actions subclasses
|
||||
"""
|
||||
DefaultAPIClient.debug_mode(debug=params.debug)
|
||||
|
||||
self.serializer = Serializer.from_params(params)
|
||||
if self.flag_func_map is not None:
|
||||
for flag, method in self.flag_func_map:
|
||||
if flag is None or getattr(params, flag):
|
||||
method(params)
|
||||
break
|
||||
|
||||
@property
|
||||
def examples(self):
|
||||
"""examples property is concatenation of __doc__ strings from
|
||||
methods in child action classes, and is added as epilog of help
|
||||
output
|
||||
"""
|
||||
methods_with_docs = set(
|
||||
method
|
||||
for _, method in self.flag_func_map
|
||||
)
|
||||
return "Examples:\n\n" + \
|
||||
"\n".join(
|
||||
six.moves.map(
|
||||
lambda method: (
|
||||
"\t" + method.__doc__.replace("\n ", "\n")
|
||||
),
|
||||
methods_with_docs
|
||||
)
|
||||
).format(
|
||||
action_name=self.action_name
|
||||
)
|
||||
|
||||
def full_path_directory(self, directory, base_name):
|
||||
full_path = os.path.join(directory, base_name)
|
||||
if not os.path.exists(full_path):
|
||||
try:
|
||||
os.mkdir(full_path)
|
||||
except OSError as e:
|
||||
raise error.ActionException(six.text_type(e))
|
||||
return full_path
|
||||
|
||||
def default_directory(self, directory=None):
|
||||
return os.path.abspath(os.curdir if directory is None else directory)
|
||||
|
||||
|
||||
def wrap(method, args, f):
|
||||
"""wrap - is second order function, purpose of which is to
|
||||
generalize argument checking for methods in actions in form
|
||||
of decorator with arguments.
|
||||
|
||||
'check_all' and 'check_any' are partial function of wrap.
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapped_f(self, params):
|
||||
if method(getattr(params, _arg) for _arg in args):
|
||||
return f(self, params)
|
||||
else:
|
||||
raise error.ArgumentException(
|
||||
"{0} required!".format(
|
||||
quote_and_join(
|
||||
"--" + arg for arg in args
|
||||
)
|
||||
)
|
||||
)
|
||||
return wrapped_f
|
||||
|
||||
|
||||
def check_all(*args):
|
||||
"""check_all - decorator with arguments, which checks that
|
||||
all arguments are given before running action method, if
|
||||
not all arguments are given, it raises an ArgumentException.
|
||||
"""
|
||||
return partial(wrap, all, args)
|
||||
|
||||
|
||||
def check_any(*args):
|
||||
"""check_any - decorator with arguments, which checks that
|
||||
at least one arguments is given before running action method,
|
||||
if no arguments were given, it raises an ArgumentException.
|
||||
"""
|
||||
return partial(wrap, any, args)
|
@ -1,74 +0,0 @@
|
||||
# Copyright 2014 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 six
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class ChangesAction(Action):
|
||||
|
||||
action_name = None
|
||||
actions_func_map = {}
|
||||
|
||||
def __init__(self):
|
||||
super(ChangesAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(required=True),
|
||||
Args.get_dry_run_deployment_arg(),
|
||||
)
|
||||
self.flag_func_map = (
|
||||
(None, self.deploy_changes),
|
||||
)
|
||||
|
||||
def print_deploy_info(self, deploy_task):
|
||||
six.print_("Deployment task with id {t} for the environment {e} "
|
||||
"has been started.".format(t=deploy_task.id,
|
||||
e=deploy_task.env.id)
|
||||
)
|
||||
|
||||
def deploy_changes(self, params):
|
||||
"""To apply all changes to some environment:
|
||||
fuel --env 1 {action_name}
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
|
||||
deploy_task = getattr(
|
||||
env, self.actions_func_map[self.action_name])(
|
||||
dry_run=params.dry_run)
|
||||
self.serializer.print_to_output(
|
||||
deploy_task.data,
|
||||
deploy_task,
|
||||
print_method=self.print_deploy_info)
|
||||
|
||||
|
||||
class DeployChangesAction(ChangesAction):
|
||||
"""Deploy changes to environments
|
||||
"""
|
||||
action_name = "deploy-changes"
|
||||
|
||||
def __init__(self):
|
||||
super(DeployChangesAction, self).__init__()
|
||||
self.actions_func_map[self.action_name] = 'deploy_changes'
|
||||
|
||||
|
||||
class RedeployChangesAction(ChangesAction):
|
||||
"""Redeploy changes to environment which is in the operational state
|
||||
"""
|
||||
action_name = "redeploy-changes"
|
||||
|
||||
def __init__(self):
|
||||
super(RedeployChangesAction, self).__init__()
|
||||
self.actions_func_map[self.action_name] = 'redeploy_changes'
|
@ -1,108 +0,0 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli.formatting import format_table
|
||||
|
||||
from fuelclient.v1.deployment_history import DeploymentHistoryClient
|
||||
|
||||
|
||||
class DeploymentTasksAction(Action):
|
||||
"""Show deployment tasks
|
||||
"""
|
||||
action_name = "deployment-tasks"
|
||||
|
||||
def __init__(self):
|
||||
super(DeploymentTasksAction, self).__init__()
|
||||
self.args = [
|
||||
group(
|
||||
Args.get_list_arg("List all deployment tasks"),
|
||||
),
|
||||
Args.get_single_task_arg(required=True),
|
||||
Args.get_deployment_node_arg(
|
||||
"Node ids."
|
||||
),
|
||||
Args.get_status_arg(
|
||||
"Statuses: pending, error, ready, running, skipped"
|
||||
),
|
||||
Args.get_tasks_names_arg(
|
||||
"Show deployment history for specific deployment tasks names "
|
||||
"and group output by task"
|
||||
),
|
||||
Args.get_show_parameters_arg(
|
||||
"Show deployment tasks parameters"
|
||||
),
|
||||
Args.get_include_summary_arg(
|
||||
"Show deployment tasks summary"
|
||||
),
|
||||
]
|
||||
self.flag_func_map = (
|
||||
(None, self.list),
|
||||
)
|
||||
|
||||
def list(self, params):
|
||||
"""To display all deployment tasks for task:
|
||||
fuel deployment-tasks --task-id 5
|
||||
|
||||
To display deployment tasks for some nodes:
|
||||
fuel deployment-tasks --task-id 5 --node 1,2
|
||||
|
||||
To display deployment tasks for some statuses(pending, error,
|
||||
ready, running):
|
||||
fuel deployment-tasks --task-id 5 --status pending,running
|
||||
|
||||
To display deployment tasks for some statuses(pending, error,
|
||||
ready, running) on some nodes:
|
||||
fuel deployment-tasks --task-id 5 --status error --node 1,2
|
||||
|
||||
To display certain deployment tasks results only:
|
||||
fuel deployment-tasks --task-id 5
|
||||
--task-name task-name1,task-name2
|
||||
|
||||
To display tasks parameters use:
|
||||
fuel deployment-tasks --task-id 5 --show-parameters
|
||||
|
||||
"""
|
||||
client = DeploymentHistoryClient()
|
||||
tasks_names = getattr(params, 'task-name', None)
|
||||
show_parameters = getattr(params, 'show-parameters')
|
||||
statuses = params.status.split(',') if params.status else []
|
||||
nodes = params.node.split(',') if params.node else []
|
||||
tasks_names = tasks_names.split(',') if tasks_names else []
|
||||
include_summary = getattr(params, 'include-summary')
|
||||
|
||||
data = client.get_all(
|
||||
transaction_id=params.task,
|
||||
nodes=nodes,
|
||||
statuses=statuses,
|
||||
tasks_names=tasks_names,
|
||||
show_parameters=show_parameters,
|
||||
include_summary=include_summary
|
||||
)
|
||||
|
||||
if show_parameters:
|
||||
table_keys = client.tasks_records_keys
|
||||
else:
|
||||
table_keys = client.history_records_keys
|
||||
if include_summary:
|
||||
table_keys += ('summary',)
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
format_table(
|
||||
data,
|
||||
acceptable_keys=table_keys
|
||||
)
|
||||
)
|
@ -1,228 +0,0 @@
|
||||
# Copyright 2014 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 sys
|
||||
|
||||
import six
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli.actions.base import check_all
|
||||
from fuelclient.cli.actions.base import check_any
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class EnvironmentAction(Action):
|
||||
"""Create, list and modify currently existing environments(clusters)
|
||||
"""
|
||||
action_name = "environment"
|
||||
|
||||
def __init__(self):
|
||||
super(EnvironmentAction, self).__init__()
|
||||
self.args = [
|
||||
Args.get_env_arg(),
|
||||
group(
|
||||
Args.get_list_arg(
|
||||
"List all available environments"
|
||||
),
|
||||
Args.get_set_arg(
|
||||
"Set environment parameters e.g., its name"
|
||||
),
|
||||
Args.get_delete_arg(
|
||||
"Delete environment with a specific id or name"
|
||||
),
|
||||
Args.get_create_arg(
|
||||
"Create a new environment with "
|
||||
"specific release id and name"
|
||||
),
|
||||
),
|
||||
Args.get_release_arg(
|
||||
"Release id"
|
||||
),
|
||||
Args.get_force_arg(
|
||||
"Do it anyway"
|
||||
),
|
||||
Args.get_name_arg(
|
||||
"Environment name"
|
||||
),
|
||||
Args.get_nst_arg(
|
||||
"Set network segment type"
|
||||
),
|
||||
Args.get_deployment_tasks_arg("Environment tasks configuration"),
|
||||
Args.get_attributes_arg("Environment attributes"),
|
||||
group(
|
||||
Args.get_download_arg(
|
||||
"Download configuration of specific cluster"),
|
||||
Args.get_upload_arg(
|
||||
"Upload configuration to specific cluster")
|
||||
),
|
||||
Args.get_dir_arg(
|
||||
"Select directory to which download release attributes"),
|
||||
]
|
||||
self.flag_func_map = (
|
||||
("deployment-tasks", self.deployment_tasks),
|
||||
("attributes", self.attributes),
|
||||
("create", self.create),
|
||||
("set", self.set),
|
||||
("delete", self.delete),
|
||||
(None, self.list)
|
||||
)
|
||||
|
||||
@check_all("name", "release")
|
||||
def create(self, params):
|
||||
"""To create an environment with name MyEnv and release id=1 run:
|
||||
fuel env create --name MyEnv --rel 1
|
||||
|
||||
By default, it creates environment setting neutron with VLAN
|
||||
network segmentation as network provider
|
||||
To specify other modes add optional arguments:
|
||||
fuel env create --name MyEnv --rel 1 --net-segment-type vlan
|
||||
"""
|
||||
|
||||
if params.nst == 'gre':
|
||||
six.print_(
|
||||
"WARNING: GRE network segmentation type is deprecated "
|
||||
"since 7.0 release.", file=sys.stderr)
|
||||
|
||||
env = Environment.create(
|
||||
params.name,
|
||||
params.release,
|
||||
params.nst,
|
||||
)
|
||||
|
||||
data = env.get_fresh_data()
|
||||
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
u"Environment '{name}' with id={id} was created!"
|
||||
.format(**data)
|
||||
)
|
||||
|
||||
@check_all("env")
|
||||
def set(self, params):
|
||||
"""To change environment name:
|
||||
fuel --env 1 env set --name NewEnvName
|
||||
"""
|
||||
acceptable_params = ('name', )
|
||||
|
||||
env = Environment(params.env, params=params)
|
||||
|
||||
# forming message for output and data structure for request body
|
||||
# TODO(aroma): make it less ugly
|
||||
msg_template = ("Following attributes are changed for "
|
||||
"the environment: {env_attributes}")
|
||||
|
||||
env_attributes = []
|
||||
update_kwargs = dict()
|
||||
for param_name in acceptable_params:
|
||||
attr_value = getattr(params, param_name, None)
|
||||
if attr_value:
|
||||
update_kwargs[param_name] = attr_value
|
||||
env_attributes.append(
|
||||
''.join([param_name, '=', str(attr_value)])
|
||||
)
|
||||
|
||||
data = env.set(update_kwargs)
|
||||
env_attributes = ', '.join(env_attributes)
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
msg_template.format(env_attributes=env_attributes)
|
||||
)
|
||||
|
||||
@check_all("env")
|
||||
def delete(self, params):
|
||||
"""To delete the environment:
|
||||
fuel --env 1 env --force delete
|
||||
"""
|
||||
env = Environment(params.env, params=params)
|
||||
|
||||
if env.status == "operational" and not params.force:
|
||||
self.serializer.print_to_output(env.data,
|
||||
"Deleting an operational"
|
||||
"environment is a dangerous "
|
||||
"operation. Please use --force to "
|
||||
"bypass this message.")
|
||||
return
|
||||
|
||||
data = env.delete()
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
"Environment with id={0} was deleted"
|
||||
.format(env.id)
|
||||
)
|
||||
|
||||
def list(self, params):
|
||||
"""Print all available environments:
|
||||
fuel env
|
||||
"""
|
||||
acceptable_keys = ("id", "status", "name", "release_id", )
|
||||
data = Environment.get_all_data()
|
||||
if params.env:
|
||||
data = filter(
|
||||
lambda x: x[u"id"] == int(params.env),
|
||||
data
|
||||
)
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
format_table(
|
||||
data,
|
||||
acceptable_keys=acceptable_keys
|
||||
)
|
||||
)
|
||||
|
||||
@check_all("env")
|
||||
@check_any("download", "upload")
|
||||
def deployment_tasks(self, params):
|
||||
"""Modify deployment_tasks for environment:
|
||||
fuel env --env 1 --deployment-tasks --download
|
||||
fuel env --env 1 --deployment-tasks --upload
|
||||
"""
|
||||
cluster = Environment(params.env)
|
||||
dir_path = self.full_path_directory(
|
||||
params.dir, 'cluster_{0}'.format(params.env))
|
||||
full_path = '{0}/deployment_tasks'.format(dir_path)
|
||||
if params.download:
|
||||
tasks = cluster.get_deployment_tasks()
|
||||
self.serializer.write_to_path(full_path, tasks)
|
||||
print("Deployment tasks for cluster {0} "
|
||||
"downloaded into {1}.yaml.".format(cluster.id, full_path))
|
||||
elif params.upload:
|
||||
tasks = self.serializer.read_from_file(full_path)
|
||||
cluster.update_deployment_tasks(tasks)
|
||||
print("Deployment tasks for cluster {0} "
|
||||
"uploaded from {1}.yaml".format(cluster.id, full_path))
|
||||
|
||||
@check_all("env")
|
||||
@check_any("download", "upload")
|
||||
def attributes(self, params):
|
||||
"""Modify attributes of the environment:
|
||||
fuel env --env 1 --attributes --download
|
||||
fuel env --env 1 --attributes --upload
|
||||
"""
|
||||
cluster = Environment(params.env)
|
||||
dir_path = self.full_path_directory(
|
||||
params.dir, 'cluster_{0}'.format(params.env))
|
||||
full_path = '{0}/attributes'.format(dir_path)
|
||||
|
||||
if params.download:
|
||||
attributes = cluster.get_attributes()
|
||||
self.serializer.write_to_path(full_path, attributes)
|
||||
print("Attributes of cluster {0} "
|
||||
"downloaded into {1}.yaml.".format(cluster.id, full_path))
|
||||
elif params.upload:
|
||||
attributes = self.serializer.read_from_file(full_path)
|
||||
cluster.update_attributes(attributes, params.force)
|
||||
print("Attributes of cluster {0} "
|
||||
"uploaded from {1}.yaml".format(cluster.id, full_path))
|
@ -1,136 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class FactAction(Action):
|
||||
|
||||
action_name = None
|
||||
|
||||
def __init__(self):
|
||||
super(FactAction, self).__init__()
|
||||
self.args = [
|
||||
Args.get_env_arg(required=True),
|
||||
group(
|
||||
Args.get_delete_arg(
|
||||
"Delete current {0} data.".format(self.action_name)
|
||||
),
|
||||
Args.get_download_arg(
|
||||
"Download current {0} data.".format(self.action_name)
|
||||
),
|
||||
Args.get_upload_arg(
|
||||
"Upload current {0} data.".format(self.action_name)
|
||||
),
|
||||
Args.get_default_arg(
|
||||
"Download default {0} data.".format(self.action_name)
|
||||
),
|
||||
required=True
|
||||
),
|
||||
Args.get_dir_arg(
|
||||
"Directory with {0} data.".format(self.action_name)
|
||||
),
|
||||
Args.get_node_arg(
|
||||
"Node ids."
|
||||
),
|
||||
Args.get_not_split_facts_args(),
|
||||
]
|
||||
self.flag_func_map = (
|
||||
("default", self.default),
|
||||
("upload", self.upload),
|
||||
("delete", self.delete),
|
||||
("download", self.download)
|
||||
)
|
||||
|
||||
def default(self, params):
|
||||
"""To get default {action_name} information for some environment:
|
||||
fuel --env 1 {action_name} --default
|
||||
|
||||
It's possible to get default {action_name} information
|
||||
just for some nodes:
|
||||
fuel --env 1 {action_name} --default --node 1,2,3
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
dir_name = env.write_facts_to_dir(
|
||||
self.action_name,
|
||||
env.get_default_facts(
|
||||
self.action_name, nodes=params.node, split=params.split
|
||||
),
|
||||
directory=params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
print(
|
||||
"Default {0} info was downloaded to {1}".format(
|
||||
self.action_name,
|
||||
dir_name
|
||||
)
|
||||
)
|
||||
|
||||
def upload(self, params):
|
||||
"""To upload {action_name} information for some environment:
|
||||
fuel --env 1 {action_name} --upload
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
facts = env.read_fact_info(
|
||||
self.action_name,
|
||||
directory=params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
env.upload_facts(self.action_name, facts)
|
||||
print("{0} facts were uploaded.".format(self.action_name))
|
||||
|
||||
def delete(self, params):
|
||||
"""Also {action_name} information can be left or
|
||||
taken from specific directory:
|
||||
fuel --env 1 {action_name} --upload \\
|
||||
--dir path/to/some/directory
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
env.delete_facts(self.action_name)
|
||||
print("{0} facts deleted.".format(self.action_name))
|
||||
|
||||
def download(self, params):
|
||||
"""To download {action_name} information for some environment:
|
||||
fuel --env 1 {action_name} --download
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
dir_name = env.write_facts_to_dir(
|
||||
self.action_name,
|
||||
env.get_facts(
|
||||
self.action_name, nodes=params.node, split=params.split
|
||||
),
|
||||
directory=params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
print(
|
||||
"Current {0} info was downloaded to {1}".format(
|
||||
self.action_name,
|
||||
dir_name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DeploymentAction(FactAction):
|
||||
"""Show computed deployment facts for orchestrator
|
||||
"""
|
||||
action_name = "deployment"
|
||||
|
||||
|
||||
class ProvisioningAction(FactAction):
|
||||
"""Show computed provisioning facts for orchestrator
|
||||
"""
|
||||
action_name = "provisioning"
|
@ -1,42 +0,0 @@
|
||||
# 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 fuelclient.cli.arguments as Args
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.objects.fuelversion import FuelVersion
|
||||
|
||||
|
||||
class FuelVersionAction(Action):
|
||||
"""Show Fuel server's version
|
||||
"""
|
||||
|
||||
action_name = "fuel-version"
|
||||
|
||||
def __init__(self):
|
||||
super(FuelVersionAction, self).__init__()
|
||||
|
||||
self.args = [
|
||||
Args.get_list_arg("Show fuel version"),
|
||||
]
|
||||
self.flag_func_map = (
|
||||
(None, self.version),
|
||||
)
|
||||
|
||||
def version(self, params):
|
||||
"""To show fuel version data:
|
||||
fuel fuel-version
|
||||
"""
|
||||
version = FuelVersion.get_all_data()
|
||||
print(self.serializer.serialize(version))
|
@ -1,163 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 os
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from fuelclient.cli.actions import base
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.objects import environment
|
||||
|
||||
|
||||
class GraphAction(base.Action):
|
||||
"""Manipulate deployment graph's representation."""
|
||||
|
||||
action_name = 'graph'
|
||||
|
||||
task_types = ('skipped', 'group', 'stage')
|
||||
|
||||
def __init__(self):
|
||||
super(GraphAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(),
|
||||
Args.get_render_arg(
|
||||
"Render graph from DOT to PNG"
|
||||
),
|
||||
Args.get_download_arg(
|
||||
"Download graph of specific cluster"
|
||||
),
|
||||
Args.get_dir_arg(
|
||||
"Select target dir to render graph."
|
||||
),
|
||||
Args.group(
|
||||
Args.get_skip_tasks(),
|
||||
Args.get_tasks()
|
||||
),
|
||||
Args.get_graph_endpoint(),
|
||||
Args.get_graph_startpoint(),
|
||||
Args.get_remove_type_arg(self.task_types),
|
||||
Args.get_parents_arg(),
|
||||
Args.get_tred_arg("Apply transitive reduction filter for graph."),
|
||||
)
|
||||
self.flag_func_map = (
|
||||
('render', self.render),
|
||||
('download', self.download),
|
||||
)
|
||||
|
||||
@base.check_all("env")
|
||||
def download(self, params):
|
||||
"""Download deployment graph to stdout
|
||||
|
||||
fuel graph --env 1 --download
|
||||
fuel graph --env 1 --download --tasks A B C
|
||||
fuel graph --env 1 --download --skip X Y --end pre_deployment
|
||||
fuel graph --env 1 --download --skip X Y --start post_deployment
|
||||
|
||||
Specify output:
|
||||
fuel graph --env 1 --download > output/dir/file.gv
|
||||
|
||||
Get parents only for task A:
|
||||
|
||||
fuel graph --env 1 --download --parents-for A
|
||||
"""
|
||||
env = environment.Environment(params.env)
|
||||
|
||||
parents_for = getattr(params, 'parents-for')
|
||||
|
||||
used_params = "# params:\n"
|
||||
for param in ('start', 'end', 'skip', 'tasks', 'parents-for',
|
||||
'remove'):
|
||||
used_params += "# - {0}: {1}\n".format(param,
|
||||
getattr(params, param))
|
||||
|
||||
tasks = params.tasks
|
||||
|
||||
if not tasks or (params.skip or params.end or params.start):
|
||||
tasks = env.get_tasks(
|
||||
skip=params.skip, end=params.end,
|
||||
start=params.start, include=params.tasks)
|
||||
|
||||
dotraph = env.get_deployment_tasks_graph(tasks,
|
||||
parents_for=parents_for,
|
||||
remove=params.remove)
|
||||
sys.stdout.write(six.text_type(used_params))
|
||||
sys.stdout.write(six.text_type(dotraph))
|
||||
|
||||
@base.check_all("render")
|
||||
def render(self, params):
|
||||
"""Render graph in PNG format
|
||||
|
||||
fuel graph --render graph.gv
|
||||
fuel graph --render graph.gv --dir ./output/dir/
|
||||
|
||||
To apply transitive reduction filter on rendered graph:
|
||||
|
||||
fuel graph --render graph.gv --tred
|
||||
fuel graph --render graph.gv --dir ./output/dir/ --tred
|
||||
|
||||
Read graph from stdin
|
||||
some_process | fuel graph --render -
|
||||
"""
|
||||
if params.render == '-':
|
||||
dot_data = sys.stdin.read()
|
||||
out_filename = 'graph.gv'
|
||||
elif not os.path.exists(params.render):
|
||||
raise error.ArgumentException(
|
||||
"Input file does not exist"
|
||||
)
|
||||
else:
|
||||
out_filename = os.path.basename(params.render)
|
||||
with open(params.render, 'r') as f:
|
||||
dot_data = f.read()
|
||||
|
||||
target_dir = self.full_path_directory(
|
||||
self.default_directory(params.dir),
|
||||
''
|
||||
)
|
||||
target_file = os.path.join(
|
||||
target_dir,
|
||||
'{0}.png'.format(out_filename),
|
||||
)
|
||||
|
||||
if not os.access(os.path.dirname(target_file), os.W_OK):
|
||||
raise error.ActionException(
|
||||
'Path {0} is not writable'.format(target_file))
|
||||
render_graph(dot_data, target_file, params.tred)
|
||||
print('Graph saved in "{0}"'.format(target_file))
|
||||
|
||||
|
||||
def render_graph(input_data, output_path, tred=False):
|
||||
"""Renders DOT graph using pygraphviz.
|
||||
|
||||
:param input_data: DOT graph representation
|
||||
:param output_path: path to the rendered graph
|
||||
:param tred: applies transitive reduction of graph
|
||||
"""
|
||||
try:
|
||||
import pygraphviz
|
||||
except ImportError:
|
||||
raise error.WrongEnvironmentError(
|
||||
"This action requires Graphviz installed "
|
||||
"together with 'pygraphviz' Python library")
|
||||
|
||||
graph = pygraphviz.AGraph(string=input_data)
|
||||
if tred:
|
||||
graph = graph.tred()
|
||||
|
||||
graph.draw(output_path, prog='dot', format='png')
|
@ -1,105 +0,0 @@
|
||||
# Copyright 2014 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 sys
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.error import EnvironmentException
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.cli.formatting import print_health_check
|
||||
from fuelclient.objects.environment import Environment
|
||||
import six
|
||||
|
||||
|
||||
class HealthCheckAction(Action):
|
||||
"""Run health check on environment
|
||||
"""
|
||||
action_name = "health"
|
||||
|
||||
_allowed_statuses = (
|
||||
'error',
|
||||
'operational',
|
||||
'update_error',
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super(HealthCheckAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(required=True),
|
||||
Args.get_list_arg("List all available checks"),
|
||||
Args.get_force_arg("Forced test run"),
|
||||
Args.get_check_arg("Run check for some testset."),
|
||||
Args.get_ostf_username_arg(),
|
||||
Args.get_ostf_password_arg(),
|
||||
Args.get_ostf_tenant_name_arg()
|
||||
)
|
||||
|
||||
self.flag_func_map = (
|
||||
("check", self.check),
|
||||
(None, self.list)
|
||||
)
|
||||
|
||||
def check(self, params):
|
||||
"""To run some health checks:
|
||||
fuel --env 1 health --check smoke,sanity
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
|
||||
if env.status not in self._allowed_statuses and not params.force:
|
||||
raise EnvironmentException(
|
||||
"Environment is not ready to run health check "
|
||||
"because it is in {0} state. "
|
||||
"Health check is likely to fail because of "
|
||||
"this. Use --force flag to proceed anyway.". format(env.status)
|
||||
)
|
||||
|
||||
if env.is_customized and not params.force:
|
||||
raise EnvironmentException(
|
||||
"Environment deployment facts were updated. "
|
||||
"Health check is likely to fail because of "
|
||||
"that. Use --force flag to proceed anyway."
|
||||
)
|
||||
test_sets_to_check = params.check or set(
|
||||
ts["id"] for ts in env.get_testsets())
|
||||
ostf_credentials = {}
|
||||
if params.ostf_tenant_name is not None:
|
||||
ostf_credentials['tenant'] = params.ostf_tenant_name
|
||||
if params.ostf_username is not None:
|
||||
ostf_credentials['username'] = params.ostf_username
|
||||
if params.ostf_password is not None:
|
||||
ostf_credentials['password'] = params.ostf_password
|
||||
if not ostf_credentials:
|
||||
six.print_("WARNING: ostf credentials are going to be",
|
||||
"mandatory in the next release.", file=sys.stderr)
|
||||
env.run_test_sets(test_sets_to_check, ostf_credentials)
|
||||
tests_state = env.get_state_of_tests()
|
||||
self.serializer.print_to_output(
|
||||
tests_state,
|
||||
env,
|
||||
print_method=print_health_check
|
||||
)
|
||||
|
||||
def list(self, params):
|
||||
"""To list all health check test sets:
|
||||
fuel --env 1 health
|
||||
or:
|
||||
fuel --env 1 health --list
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
test_sets = env.get_testsets()
|
||||
self.serializer.print_to_output(
|
||||
test_sets,
|
||||
format_table(test_sets)
|
||||
)
|
@ -1,58 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class InterruptAction(Action):
|
||||
|
||||
def __init__(self):
|
||||
super(InterruptAction, self).__init__()
|
||||
self.args = [
|
||||
Args.get_env_arg(required=True)
|
||||
]
|
||||
self.flag_func_map = (
|
||||
(None, self.interrupt),
|
||||
)
|
||||
|
||||
def interrupt(self, params):
|
||||
"""To {action_name} some environment:
|
||||
fuel --env 1 {action_name}
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
intercept_task = getattr(env, self.action_name)()
|
||||
self.serializer.print_to_output(
|
||||
intercept_task.data,
|
||||
"{0} task of environment with id={1} started. "
|
||||
"To check task status run 'fuel task --tid {2}'.".format(
|
||||
self.action_name.title(),
|
||||
params.env,
|
||||
intercept_task.data["id"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class StopAction(InterruptAction):
|
||||
"""Stop deployment process for specific environment
|
||||
"""
|
||||
action_name = "stop"
|
||||
|
||||
|
||||
class ResetAction(InterruptAction):
|
||||
"""Reset deployed process for specific environment
|
||||
"""
|
||||
action_name = "reset"
|
@ -1,151 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class NetworkAction(Action):
|
||||
"""Show or modify network settings of specific environments
|
||||
"""
|
||||
action_name = "network"
|
||||
|
||||
def __init__(self):
|
||||
super(NetworkAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(required=True),
|
||||
Args.get_dir_arg("Directory with network data."),
|
||||
group(
|
||||
Args.get_download_arg(
|
||||
"Download current network configuration."),
|
||||
Args.get_verify_arg(
|
||||
"Verify current network configuration."),
|
||||
Args.get_upload_arg(
|
||||
"Upload changed network configuration."),
|
||||
required=True
|
||||
)
|
||||
)
|
||||
self.flag_func_map = (
|
||||
("upload", self.upload),
|
||||
("verify", self.verify),
|
||||
("download", self.download)
|
||||
)
|
||||
|
||||
def upload(self, params):
|
||||
"""To upload network configuration from some
|
||||
directory for some environment:
|
||||
fuel --env 1 network --upload --dir path/to/directory
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
network_data = env.read_network_data(
|
||||
directory=params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
env.set_network_data(network_data)
|
||||
print("Network configuration uploaded.")
|
||||
|
||||
def verify(self, params):
|
||||
"""To verify network configuration from some directory
|
||||
for some environment:
|
||||
fuel --env 1 network --verify --dir path/to/directory
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
response = env.verify_network()
|
||||
print(
|
||||
"Verification status is '{status}'. message: {message}"
|
||||
.format(**response)
|
||||
)
|
||||
|
||||
def download(self, params):
|
||||
"""To download network configuration in this
|
||||
directory for some environment:
|
||||
fuel --env 1 network --download
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
network_data = env.get_network_data()
|
||||
network_file_path = env.write_network_data(
|
||||
network_data,
|
||||
directory=params.dir,
|
||||
serializer=self.serializer)
|
||||
print(
|
||||
"Network configuration for environment with id={0}"
|
||||
" downloaded to {1}"
|
||||
.format(env.id, network_file_path)
|
||||
)
|
||||
|
||||
|
||||
class NetworkTemplateAction(Action):
|
||||
"""Manipulate network templates for a specific environment
|
||||
"""
|
||||
action_name = 'network-template'
|
||||
|
||||
def __init__(self):
|
||||
super(NetworkTemplateAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(required=True),
|
||||
Args.get_dir_arg("Directory with network templates."),
|
||||
group(
|
||||
Args.get_download_arg(
|
||||
"Download current network template configuration."),
|
||||
Args.get_upload_arg(
|
||||
"Upload changed network template configuration."),
|
||||
Args.get_delete_arg(
|
||||
"Delete network template configuration."),
|
||||
required=True))
|
||||
|
||||
self.flag_func_map = (
|
||||
("upload", self.upload),
|
||||
("download", self.download),
|
||||
("delete", self.delete))
|
||||
|
||||
def upload(self, params):
|
||||
"""Uploads network template from filesystem path
|
||||
for specified environment:
|
||||
fuel --env 1 network-template --upload --dir path/to/directory
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
network_template_data = env.read_network_template_data(
|
||||
directory=params.dir,
|
||||
serializer=self.serializer)
|
||||
env.set_network_template_data(network_template_data)
|
||||
full_path = self.serializer.prepare_path(
|
||||
env.get_network_template_data_path(directory=params.dir))
|
||||
print("Network template {0} has been uploaded.".format(full_path))
|
||||
|
||||
def download(self, params):
|
||||
"""Downloads network template in current
|
||||
directory for specified environment:
|
||||
fuel --env 1 network-template --download
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
template_data = env.get_network_template_data()
|
||||
network_template_file_path = env.write_network_template_data(
|
||||
template_data,
|
||||
directory=params.dir,
|
||||
serializer=self.serializer)
|
||||
|
||||
print("Network template configuration for environment with id={0}"
|
||||
" downloaded to {1}".format(env.id, network_template_file_path))
|
||||
|
||||
def delete(self, params):
|
||||
"""Deletes network template for specified environment:
|
||||
fuel --env 1 --network-template --delete
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
env.delete_network_template_data()
|
||||
|
||||
print("Network template configuration for environment id={0}"
|
||||
" has been deleted.".format(env.id))
|
@ -1,147 +0,0 @@
|
||||
# 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 sys
|
||||
|
||||
import six
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli.actions.base import check_all
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.commands.network_group import get_args_for_update
|
||||
from fuelclient.objects.network_group import NetworkGroup
|
||||
from fuelclient.objects.network_group import NetworkGroupCollection
|
||||
|
||||
|
||||
class NetworkGroupAction(Action):
|
||||
"""Show or modify network groups
|
||||
"""
|
||||
action_name = "network-group"
|
||||
acceptable_keys = ("id", "name", "vlan_start", "cidr",
|
||||
"gateway", "group_id")
|
||||
|
||||
def __init__(self):
|
||||
super(NetworkGroupAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(),
|
||||
Args.get_name_arg("Name of new network group."),
|
||||
Args.get_node_group_arg("ID of node group."),
|
||||
Args.get_release_arg("Release ID this network group belongs to."),
|
||||
Args.get_vlan_arg("VLAN of network."),
|
||||
Args.get_cidr_arg("CIDR of network."),
|
||||
Args.get_gateway_arg("Gateway of network."),
|
||||
Args.get_network_group_arg("ID of network group."),
|
||||
Args.get_meta_arg("Metadata in JSON format to override default "
|
||||
"network metadata."),
|
||||
group(
|
||||
Args.get_create_network_arg(
|
||||
"Create a new network group for the specified "
|
||||
" node group."
|
||||
),
|
||||
Args.get_delete_arg("Delete specified network groups."),
|
||||
Args.get_list_arg("List all network groups."),
|
||||
Args.get_set_arg("Set network group parameters.")
|
||||
)
|
||||
)
|
||||
self.flag_func_map = (
|
||||
("create", self.create),
|
||||
("delete", self.delete),
|
||||
("set", self.set),
|
||||
(None, self.list),
|
||||
)
|
||||
|
||||
@check_all('nodegroup', 'name', 'cidr')
|
||||
def create(self, params):
|
||||
"""Create a new network group
|
||||
fuel network-group --create --node-group 1 --name "new network"
|
||||
--release 2 --vlan 100 --cidr 10.0.0.0/24
|
||||
|
||||
fuel network-group --create --node-group 2 --name "new network"
|
||||
--release 2 --vlan 100 --cidr 10.0.0.0/24 --gateway 10.0.0.1
|
||||
--meta 'meta information in JSON format'
|
||||
"""
|
||||
meta = self.serializer.deserialize(params.meta) if params.meta else {}
|
||||
|
||||
NetworkGroup.create(
|
||||
params.name,
|
||||
params.release,
|
||||
params.vlan,
|
||||
params.cidr,
|
||||
params.gateway,
|
||||
int(params.nodegroup.pop()),
|
||||
meta
|
||||
)
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Network group {0} has been created".format(params.name)
|
||||
)
|
||||
|
||||
@check_all('network')
|
||||
def delete(self, params):
|
||||
"""Delete the specified network groups
|
||||
fuel network-group --delete --network 1
|
||||
fuel network-group --delete --network 2,3,4
|
||||
"""
|
||||
ngs = NetworkGroup.get_by_ids(params.network)
|
||||
for network_group in ngs:
|
||||
network_group.delete()
|
||||
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Network groups with IDS {0} have been deleted.".format(
|
||||
','.join(params.network))
|
||||
)
|
||||
|
||||
@check_all('network')
|
||||
def set(self, params):
|
||||
"""Set parameters for the specified network group:
|
||||
fuel network-group --set --network 1 --name new_name
|
||||
"""
|
||||
# Since network has set type and we cannot update multiple network
|
||||
# groups at once, we pick first network group id from set.
|
||||
ng_id = next(iter(params.network))
|
||||
|
||||
if len(params.network) > 1:
|
||||
msg = ("Warning: Only first network with id={0}"
|
||||
" will be updated.".format(ng_id))
|
||||
six.print_(msg, file=sys.stderr)
|
||||
|
||||
ng = NetworkGroup(ng_id)
|
||||
|
||||
update_params = get_args_for_update(params, self.serializer)
|
||||
data = ng.set(update_params)
|
||||
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
"Network group id={0} has been updated".format(ng_id))
|
||||
|
||||
def list(self, params):
|
||||
"""To list all available network groups:
|
||||
fuel network-group list
|
||||
|
||||
To filter them by node group:
|
||||
fuel network-group --node-group 1
|
||||
"""
|
||||
group_collection = NetworkGroupCollection.get_all()
|
||||
if params.nodegroup:
|
||||
group_collection.filter_by_group_id(int(params.nodegroup.pop()))
|
||||
self.serializer.print_to_output(
|
||||
group_collection.data,
|
||||
format_table(
|
||||
group_collection.data,
|
||||
acceptable_keys=self.acceptable_keys,
|
||||
)
|
||||
)
|
@ -1,418 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from itertools import groupby
|
||||
from operator import attrgetter
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli.actions.base import check_all
|
||||
from fuelclient.cli.actions.base import check_any
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects.environment import Environment
|
||||
from fuelclient.objects.node import Node
|
||||
from fuelclient.objects.node import NodeCollection
|
||||
|
||||
|
||||
class NodeAction(Action):
|
||||
"""List and assign available nodes to environments
|
||||
"""
|
||||
action_name = "node"
|
||||
acceptable_keys = ("id", "status", "name", "cluster", "ip",
|
||||
"mac", "roles", "pending_roles", "online", "group_id")
|
||||
|
||||
def __init__(self):
|
||||
super(NodeAction, self).__init__()
|
||||
self.args = [
|
||||
Args.get_env_arg(),
|
||||
group(
|
||||
Args.get_list_arg("List all nodes."),
|
||||
Args.get_set_arg("Set role for specific node."),
|
||||
Args.get_delete_arg("Delete specific node from environment."),
|
||||
Args.get_attributes_arg("Node attributes."),
|
||||
Args.get_network_arg("Node network configuration."),
|
||||
Args.get_disk_arg("Node disk configuration."),
|
||||
Args.get_deploy_arg("Deploy specific nodes."),
|
||||
Args.get_hostname_arg("Set node hostname."),
|
||||
Args.get_node_name_arg("Set node name."),
|
||||
Args.get_delete_from_db_arg(
|
||||
"Delete specific nodes only from fuel db.\n"
|
||||
"User should still delete node from cobbler"),
|
||||
Args.get_provision_arg("Provision specific nodes."),
|
||||
),
|
||||
group(
|
||||
Args.get_default_arg(
|
||||
"Get default configuration of some node"),
|
||||
Args.get_download_arg(
|
||||
"Download configuration of specific node"),
|
||||
Args.get_upload_arg(
|
||||
"Upload configuration to specific node")
|
||||
),
|
||||
Args.get_dir_arg(
|
||||
"Select directory to which download node attributes"),
|
||||
Args.get_node_arg("Node id."),
|
||||
Args.get_force_arg("Bypassing parameter validation."),
|
||||
Args.get_noop_run_deployment_arg(),
|
||||
Args.get_all_arg("Select all nodes."),
|
||||
Args.get_role_arg("Role to assign for node."),
|
||||
group(
|
||||
Args.get_skip_tasks(),
|
||||
Args.get_tasks()
|
||||
),
|
||||
Args.get_graph_endpoint(),
|
||||
Args.get_graph_startpoint(),
|
||||
]
|
||||
|
||||
self.flag_func_map = (
|
||||
("set", self.set),
|
||||
("delete", self.delete),
|
||||
("network", self.attributes),
|
||||
("disk", self.attributes),
|
||||
("deploy", self.start),
|
||||
("provision", self.start),
|
||||
("hostname", self.set_hostname),
|
||||
("name", self.set_name),
|
||||
("delete-from-db", self.delete_from_db),
|
||||
("tasks", self.execute_tasks),
|
||||
("skip", self.execute_tasks),
|
||||
("end", self.execute_tasks),
|
||||
("start", self.execute_tasks),
|
||||
("attributes", self.node_attributes),
|
||||
(None, self.list)
|
||||
)
|
||||
|
||||
@check_all("node", "role", "env")
|
||||
def set(self, params):
|
||||
"""Assign some nodes to environment with with specific roles:
|
||||
fuel --env 1 node set --node 1 --role controller
|
||||
fuel --env 1 node set --node 2,3,4 --role compute,cinder
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
nodes = Node.get_by_ids(params.node)
|
||||
roles = map(str.lower, params.role)
|
||||
env.assign(nodes, roles)
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Nodes {0} with roles {1} "
|
||||
"were added to environment {2}"
|
||||
.format(params.node, roles, params.env)
|
||||
)
|
||||
|
||||
@check_any("node", "env")
|
||||
def delete(self, params):
|
||||
"""Remove some nodes from environment:
|
||||
fuel --env 1 node remove --node 2,3
|
||||
|
||||
Remove nodes no matter to which environment they were assigned:
|
||||
fuel node remove --node 2,3,6,7
|
||||
|
||||
Remove all nodes from some environment:
|
||||
fuel --env 1 node remove --all
|
||||
"""
|
||||
if params.env:
|
||||
env = Environment(params.env)
|
||||
if params.node:
|
||||
env.unassign(params.node)
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Nodes with ids {0} were removed "
|
||||
"from environment with id {1}."
|
||||
.format(params.node, params.env))
|
||||
else:
|
||||
if params.all:
|
||||
env.unassign_all()
|
||||
else:
|
||||
raise error.ArgumentException(
|
||||
"You have to select which nodes to remove "
|
||||
"with --node-id. Try --all for removing all nodes."
|
||||
)
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"All nodes from environment with id {0} were removed."
|
||||
.format(params.env))
|
||||
else:
|
||||
nodes = map(Node, params.node)
|
||||
for env_id, _nodes in groupby(nodes, attrgetter("env_id")):
|
||||
list_of_nodes = [n.id for n in _nodes]
|
||||
if env_id:
|
||||
Environment(env_id).unassign(list_of_nodes)
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Nodes with ids {0} were removed "
|
||||
"from environment with id {1}."
|
||||
.format(list_of_nodes, env_id)
|
||||
)
|
||||
else:
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Nodes with ids {0} aren't added to "
|
||||
"any environment.".format(list_of_nodes)
|
||||
)
|
||||
|
||||
@check_all("node")
|
||||
@check_any("default", "download", "upload")
|
||||
def attributes(self, params):
|
||||
"""Download current or default disk, network,
|
||||
configuration for some node:
|
||||
fuel node --node-id 2 --disk --default
|
||||
fuel node --node-id 2 --network --download \\
|
||||
--dir path/to/directory
|
||||
|
||||
Upload disk, network, configuration for some node:
|
||||
fuel node --node-id 2 --network --upload
|
||||
fuel node --node-id 2 --disk --upload --dir path/to/directory
|
||||
"""
|
||||
nodes = Node.get_by_ids(params.node)
|
||||
attribute_type = "interfaces" if params.network else "disks"
|
||||
attributes = []
|
||||
files = []
|
||||
if params.default:
|
||||
for node in nodes:
|
||||
default_attribute = node.get_default_attribute(attribute_type)
|
||||
file_path = node.write_attribute(
|
||||
attribute_type,
|
||||
default_attribute,
|
||||
params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
files.append(file_path)
|
||||
attributes.append(default_attribute)
|
||||
message = "Default node attributes for {0} were written" \
|
||||
" to:\n{1}".format(attribute_type, "\n".join(files))
|
||||
elif params.upload:
|
||||
for node in nodes:
|
||||
attribute = node.read_attribute(
|
||||
attribute_type,
|
||||
params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
node.upload_node_attribute(
|
||||
attribute_type,
|
||||
attribute
|
||||
)
|
||||
attributes.append(attribute)
|
||||
message = "Node attributes for {0} were uploaded" \
|
||||
" from {1}".format(attribute_type, params.dir)
|
||||
else:
|
||||
for node in nodes:
|
||||
downloaded_attribute = node.get_attribute(attribute_type)
|
||||
file_path = node.write_attribute(
|
||||
attribute_type,
|
||||
downloaded_attribute,
|
||||
params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
attributes.append(downloaded_attribute)
|
||||
files.append(file_path)
|
||||
message = "Node attributes for {0} were written" \
|
||||
" to:\n{1}".format(attribute_type, "\n".join(files))
|
||||
print(message)
|
||||
|
||||
def get_env_id(self, node_collection):
|
||||
env_ids = set(n.env_id for n in node_collection)
|
||||
if len(env_ids) != 1:
|
||||
raise error.ActionException(
|
||||
"Inputed nodes assigned to multiple environments!")
|
||||
else:
|
||||
return env_ids.pop()
|
||||
|
||||
@check_all("node")
|
||||
def start(self, params):
|
||||
"""Deploy/Provision some node:
|
||||
fuel node --node-id 2 --provision
|
||||
fuel node --node-id 2 --deploy
|
||||
"""
|
||||
node_collection = NodeCollection.init_with_ids(params.node)
|
||||
method_type = "deploy" if params.deploy else "provision"
|
||||
env_id_to_start = self.get_env_id(node_collection)
|
||||
|
||||
if not env_id_to_start:
|
||||
raise error.ActionException(
|
||||
"Input nodes are not assigned to any environment!")
|
||||
|
||||
task = Environment(env_id_to_start).install_selected_nodes(
|
||||
method_type, node_collection.collection)
|
||||
|
||||
self.serializer.print_to_output(
|
||||
task.data,
|
||||
"Started {0}ing {1}."
|
||||
.format(method_type, node_collection))
|
||||
|
||||
@check_all("node")
|
||||
def execute_tasks(self, params):
|
||||
"""Execute deployment tasks
|
||||
fuel node --node 2 --tasks hiera netconfig
|
||||
fuel node --node 2 --tasks netconfig --force
|
||||
fuel node --node 2 --skip hiera netconfig
|
||||
fuel node --node 2 --skip rsync --end pre_deployment_end
|
||||
fuel node --node 2 --end netconfig
|
||||
fuel node --node 2 --start hiera --end neutron
|
||||
fuel node --node 2 --start post_deployment_start
|
||||
"""
|
||||
node_collection = NodeCollection.init_with_ids(params.node)
|
||||
env_id_to_start = self.get_env_id(node_collection)
|
||||
|
||||
env = Environment(env_id_to_start)
|
||||
|
||||
tasks = params.tasks or None
|
||||
force = params.force or None
|
||||
noop_run = params.noop_run or None
|
||||
|
||||
if params.skip or params.end or params.start:
|
||||
tasks = env.get_tasks(
|
||||
skip=params.skip,
|
||||
end=params.end,
|
||||
start=params.start,
|
||||
include=tasks)
|
||||
|
||||
if not tasks:
|
||||
self.serializer.print_to_output({}, "Nothing to run.")
|
||||
return
|
||||
|
||||
task = env.execute_tasks(
|
||||
node_collection.collection, tasks=tasks, force=force,
|
||||
noop_run=noop_run)
|
||||
|
||||
self.serializer.print_to_output(
|
||||
task.data,
|
||||
"Started tasks {0} for nodes {1}.".format(tasks, node_collection))
|
||||
|
||||
def list(self, params):
|
||||
"""To list all available nodes:
|
||||
fuel node
|
||||
|
||||
To filter them by environment:
|
||||
fuel --env-id 1 node
|
||||
|
||||
It's Possible to manipulate nodes with their short mac addresses:
|
||||
fuel node --node-id 80:ac
|
||||
fuel node remove --node-id 80:ac,5d:a2
|
||||
"""
|
||||
if params.node:
|
||||
node_collection = NodeCollection.init_with_ids(params.node)
|
||||
else:
|
||||
node_collection = NodeCollection.get_all()
|
||||
if params.env:
|
||||
node_collection.filter_by_env_id(int(params.env))
|
||||
self.serializer.print_to_output(
|
||||
node_collection.data,
|
||||
format_table(
|
||||
node_collection.data,
|
||||
acceptable_keys=self.acceptable_keys,
|
||||
column_to_join=("roles", "pending_roles")
|
||||
)
|
||||
)
|
||||
|
||||
@check_all("node")
|
||||
def delete_from_db(self, params):
|
||||
"""To delete nodes from fuel db:
|
||||
fuel node --node-id 1 --delete-from-db
|
||||
fuel node --node-id 1 2 --delete-from-db
|
||||
(this works only for offline nodes)
|
||||
fuel node --node-id 1 --delete-from-db --force
|
||||
(this forces deletion of nodes regardless of their state)
|
||||
"""
|
||||
if not params.force:
|
||||
node_collection = NodeCollection.init_with_ids(params.node)
|
||||
|
||||
online_nodes = [node for node in node_collection.data
|
||||
if node['online']]
|
||||
|
||||
if online_nodes:
|
||||
raise error.ActionException(
|
||||
"Nodes with ids {0} cannot be deleted from cluster "
|
||||
"because they are online. You might want to use the "
|
||||
"--force option.".format(
|
||||
[node['id'] for node in online_nodes]))
|
||||
|
||||
NodeCollection.delete_by_ids(params.node)
|
||||
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Nodes with ids {0} have been deleted from Fuel db.".format(
|
||||
params.node)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_one_node(params):
|
||||
"""Ensures that only one node was passed in the command and returns it.
|
||||
|
||||
:raises ArgumentException: When more than 1 node provided.
|
||||
"""
|
||||
if len(params.node) > 1:
|
||||
raise error.ArgumentException(
|
||||
"You should select only one node to change.")
|
||||
|
||||
return Node(params.node[0])
|
||||
|
||||
@check_all("node", "name")
|
||||
def set_name(self, params):
|
||||
"""To set node name:
|
||||
fuel node --node-id 1 --name NewName
|
||||
"""
|
||||
node = self._get_one_node(params)
|
||||
node.set({"name": params.name})
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
u"Name for node with id {0} has been changed to {1}."
|
||||
.format(node.id, params.name)
|
||||
)
|
||||
|
||||
@check_all("node", "hostname")
|
||||
def set_hostname(self, params):
|
||||
"""To set node hostname:
|
||||
fuel node --node-id 1 --hostname ctrl-01
|
||||
"""
|
||||
node = self._get_one_node(params)
|
||||
node.set({"hostname": params.hostname})
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Hostname for node with id {0} has been changed to {1}."
|
||||
.format(node.id, params.hostname)
|
||||
)
|
||||
|
||||
@check_all("node")
|
||||
@check_any("upload", "download")
|
||||
def node_attributes(self, params):
|
||||
"""Download node attributes for specified node:
|
||||
fuel node --node-id 1 --attributes --download [--dir download-dir]
|
||||
|
||||
Upload node attributes for specified node
|
||||
fuel node --node-id 1 --attributes --upload [--dir upload-dir]
|
||||
|
||||
"""
|
||||
node = self._get_one_node(params)
|
||||
if params.upload:
|
||||
data = node.read_attribute(
|
||||
'attributes', params.dir, serializer=self.serializer)
|
||||
node.update_node_attributes(data)
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Attributes for node {0} were uploaded."
|
||||
.format(node.id))
|
||||
elif params.download:
|
||||
attributes = node.get_node_attributes()
|
||||
file_path = node.write_attribute(
|
||||
'attributes', attributes,
|
||||
params.dir, serializer=self.serializer)
|
||||
self.serializer.print_to_output(
|
||||
{},
|
||||
"Attributes for node {0} were written to {1}"
|
||||
.format(node.id, file_path))
|
||||
|
||||
else:
|
||||
raise error.ArgumentException(
|
||||
"--upload or --download action should be specified.")
|
@ -1,137 +0,0 @@
|
||||
# Copyright 2014 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 sys
|
||||
|
||||
import six
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli.actions.base import check_all
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli.error import ActionException
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects import Environment
|
||||
from fuelclient.objects.nodegroup import NodeGroup
|
||||
from fuelclient.objects.nodegroup import NodeGroupCollection
|
||||
|
||||
|
||||
class NodeGroupAction(Action):
|
||||
"""Show or modify node groups
|
||||
"""
|
||||
action_name = "nodegroup"
|
||||
acceptable_keys = ("id", "cluster_id", "name")
|
||||
|
||||
def __init__(self):
|
||||
super(NodeGroupAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(),
|
||||
Args.get_list_arg("List all node groups."),
|
||||
Args.get_name_arg("Name of new node group."),
|
||||
Args.get_group_arg("ID of node group."),
|
||||
Args.get_node_arg("List of nodes to assign specified group to."),
|
||||
group(
|
||||
Args.get_create_arg(
|
||||
"Create a new node group in the specified environment."
|
||||
),
|
||||
Args.get_assign_arg(
|
||||
"Assign nodes to the specified node group."),
|
||||
Args.get_delete_arg(
|
||||
"Delete specified node groups."),
|
||||
)
|
||||
)
|
||||
self.flag_func_map = (
|
||||
("create", self.create),
|
||||
("delete", self.delete),
|
||||
("assign", self.assign),
|
||||
(None, self.list)
|
||||
)
|
||||
|
||||
@check_all("env", "name")
|
||||
def create(self, params):
|
||||
"""Create a new node group
|
||||
fuel --env 1 nodegroup --create --name "group 1"
|
||||
"""
|
||||
env_id = int(params.env)
|
||||
data = NodeGroup.create(params.name, env_id)
|
||||
env = Environment(env_id)
|
||||
network_data = env.get_network_data()
|
||||
seg_type = network_data['networking_parameters'].get(
|
||||
'segmentation_type'
|
||||
)
|
||||
if seg_type == 'vlan':
|
||||
six.print_("WARNING: In VLAN segmentation type, there will be no "
|
||||
"connectivity over private network between instances "
|
||||
"running on hypervisors in different segments and that "
|
||||
"it's a user's responsibility to handle this "
|
||||
"situation.",
|
||||
file=sys.stderr)
|
||||
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
u"Node group '{name}' with id={id} "
|
||||
u"in environment {env} was created!"
|
||||
.format(env=env_id, **data)
|
||||
)
|
||||
|
||||
@check_all("group")
|
||||
def delete(self, params):
|
||||
"""Delete the specified node groups
|
||||
fuel nodegroup --delete --group 1
|
||||
fuel nodegroup --delete --group 2,3,4
|
||||
"""
|
||||
ngs = NodeGroup.get_by_ids(params.group)
|
||||
for n in ngs:
|
||||
if n.name == "default":
|
||||
raise ActionException(
|
||||
"Default node groups cannot be deleted."
|
||||
)
|
||||
data = NodeGroup.delete(n.id)
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
u"Node group with id={id} was deleted!"
|
||||
.format(id=n.id)
|
||||
)
|
||||
|
||||
@check_all("node", "group")
|
||||
def assign(self, params):
|
||||
"""Assign nodes to specified node group:
|
||||
fuel nodegroup --assign --node 1 --group 1
|
||||
fuel nodegroup --assign --node 2,3,4 --group 1
|
||||
"""
|
||||
if len(params.group) > 1:
|
||||
raise ActionException(
|
||||
"Nodes can only be assigned to one node group."
|
||||
)
|
||||
|
||||
group = NodeGroup(params.group.pop())
|
||||
group.assign(params.node)
|
||||
|
||||
def list(self, params):
|
||||
"""To list all available node groups:
|
||||
fuel nodegroup
|
||||
|
||||
To filter them by environment:
|
||||
fuel --env-id 1 nodegroup
|
||||
"""
|
||||
group_collection = NodeGroupCollection.get_all()
|
||||
if params.env:
|
||||
group_collection.filter_by_env_id(int(params.env))
|
||||
self.serializer.print_to_output(
|
||||
group_collection.data,
|
||||
format_table(
|
||||
group_collection.data,
|
||||
acceptable_keys=self.acceptable_keys,
|
||||
)
|
||||
)
|
@ -1,113 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects.notifications import Notifications
|
||||
|
||||
|
||||
class NotificationsAction(Action):
|
||||
"""List and create notifications
|
||||
"""
|
||||
action_name = "notifications"
|
||||
|
||||
acceptable_keys = (
|
||||
"id",
|
||||
"message",
|
||||
"status",
|
||||
"topic",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super(NotificationsAction, self).__init__()
|
||||
self.args = [
|
||||
Args.group(
|
||||
Args.get_list_arg("List all available notifications."),
|
||||
Args.get_notify_send_arg("Send notification"),
|
||||
Args.get_notify_mark_as_read_arg(
|
||||
"Mark notification(s) as read ('*' to mark all as read)."),
|
||||
),
|
||||
Args.group(
|
||||
Args.get_notify_topic_arg("Notification topic (severity)"),
|
||||
),
|
||||
Args.get_notify_all_messages_arg(
|
||||
"Select all messages (only unread by default)."),
|
||||
]
|
||||
self.flag_func_map = (
|
||||
("send", self.send),
|
||||
("mark-as-read", self.mark_as_read),
|
||||
(None, self.list),
|
||||
)
|
||||
|
||||
def list(self, params):
|
||||
"""Print all available notifications:
|
||||
fuel notifications
|
||||
fuel notifications --list
|
||||
"""
|
||||
notifications = Notifications.get_all_data()
|
||||
|
||||
if not params.all:
|
||||
notifications = [notification for notification in notifications
|
||||
if notification['status'] == 'unread']
|
||||
|
||||
self.serializer.print_to_output(
|
||||
notifications,
|
||||
format_table(
|
||||
notifications,
|
||||
acceptable_keys=self.acceptable_keys
|
||||
)
|
||||
)
|
||||
|
||||
def mark_as_read(self, params):
|
||||
"""Mark given notifications as read.
|
||||
fuel notifications --mark-as-read 1 2
|
||||
fuel notifications -r 1 2
|
||||
"""
|
||||
result = Notifications.mark_as_read(
|
||||
ids=getattr(params, 'mark-as-read'))
|
||||
|
||||
self.serializer.print_to_output(
|
||||
result,
|
||||
'Notification(s) marked as read'
|
||||
)
|
||||
|
||||
def send(self, params):
|
||||
"""Send notification:
|
||||
fuel notifications --send "message" --topic done
|
||||
"""
|
||||
message = params.send
|
||||
if isinstance(message, list):
|
||||
message = ' '.join(message)
|
||||
result = Notifications.send(message, topic=params.topic)
|
||||
self.serializer.print_to_output(
|
||||
result,
|
||||
"Notification sent")
|
||||
|
||||
|
||||
class NotifyAction(NotificationsAction):
|
||||
"""Shortcut for quickly sending a notification.
|
||||
"""
|
||||
|
||||
action_name = "notify"
|
||||
|
||||
def __init__(self):
|
||||
super(NotifyAction, self).__init__()
|
||||
self.args = [
|
||||
Args.get_notify_message_arg("Notification message"),
|
||||
Args.get_notify_topic_arg("Notification topic (severity)"),
|
||||
]
|
||||
self.flag_func_map = (
|
||||
(None, self.send),
|
||||
)
|
@ -1,155 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli.actions.base import check_all
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects.openstack_config import OpenstackConfig
|
||||
|
||||
|
||||
class OpenstackConfigAction(Action):
|
||||
"""Manage openstack configuration"""
|
||||
|
||||
action_name = 'openstack-config'
|
||||
acceptable_keys = ('id', 'is_active', 'config_type',
|
||||
'cluster_id', 'node_id', 'node_role')
|
||||
|
||||
def __init__(self):
|
||||
super(OpenstackConfigAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(),
|
||||
Args.get_file_arg("Openstack configuration file"),
|
||||
Args.get_node_arg("Node IDs list"),
|
||||
Args.get_single_role_arg("Node role"),
|
||||
Args.get_config_id_arg("Openstack config ID"),
|
||||
Args.get_deleted_arg("Get deleted configurations"),
|
||||
Args.get_force_arg("Force configuration update"),
|
||||
group(
|
||||
Args.get_list_arg("List openstack configurations"),
|
||||
Args.get_download_arg(
|
||||
"Download current openstack configuration"),
|
||||
Args.get_upload_arg("Upload new openstack configuration"),
|
||||
Args.get_delete_arg("Delete openstack configuration"),
|
||||
Args.get_execute_arg("Apply openstack configuration"),
|
||||
required=True,
|
||||
)
|
||||
)
|
||||
|
||||
self.flag_func_map = (
|
||||
('list', self.list),
|
||||
('download', self.download),
|
||||
('upload', self.upload),
|
||||
('delete', self.delete),
|
||||
('execute', self.execute)
|
||||
)
|
||||
|
||||
@check_all('env')
|
||||
def list(self, params):
|
||||
"""List all available configurations:
|
||||
fuel openstack-config --list --env 1
|
||||
fuel openstack-config --list --env 1 --node 1[,2,3,...]
|
||||
fuel openstack-config --list --env 1 --deleted
|
||||
"""
|
||||
filters = {'cluster_id': params.env}
|
||||
|
||||
if 'deleted' in params:
|
||||
filters['is_active'] = int(not params.deleted)
|
||||
|
||||
if 'node' in params:
|
||||
filters['node_ids'] = params.node
|
||||
|
||||
if 'role' in params:
|
||||
filters['node_role'] = params.role
|
||||
|
||||
configs = OpenstackConfig.get_filtered_data(**filters)
|
||||
|
||||
self.serializer.print_to_output(
|
||||
configs,
|
||||
format_table(
|
||||
configs,
|
||||
acceptable_keys=self.acceptable_keys
|
||||
)
|
||||
)
|
||||
|
||||
@check_all('config-id', 'file')
|
||||
def download(self, params):
|
||||
"""Download an existing configuration to file:
|
||||
fuel openstack-config --download --config-id 1 --file config.yaml
|
||||
"""
|
||||
config_id = getattr(params, 'config-id')
|
||||
config = OpenstackConfig(config_id)
|
||||
data = config.data
|
||||
OpenstackConfig.write_file(params.file, {
|
||||
'configuration': data['configuration']})
|
||||
|
||||
@check_all('env', 'file')
|
||||
def upload(self, params):
|
||||
"""Upload new configuration from file:
|
||||
fuel openstack-config --upload --env 1 --file config.yaml
|
||||
fuel openstack-config --upload --env 1 --node 1[,2,3,...]
|
||||
--file config.yaml
|
||||
fuel openstack-config --upload --env 1
|
||||
--role controller --file config.yaml
|
||||
"""
|
||||
node_ids = getattr(params, 'node', None)
|
||||
node_role = getattr(params, 'role', None)
|
||||
data = OpenstackConfig.read_file(params.file)
|
||||
|
||||
configs = OpenstackConfig.create(
|
||||
cluster_id=params.env,
|
||||
configuration=data['configuration'],
|
||||
node_ids=node_ids, node_role=node_role)
|
||||
configs = [c.data for c in configs]
|
||||
self.serializer.print_to_output(
|
||||
configs,
|
||||
format_table(
|
||||
configs,
|
||||
acceptable_keys=self.acceptable_keys
|
||||
)
|
||||
)
|
||||
|
||||
@check_all('config-id')
|
||||
def delete(self, params):
|
||||
"""Delete an existing configuration:
|
||||
fuel openstack-config --delete --config 1
|
||||
"""
|
||||
config_id = getattr(params, 'config-id')
|
||||
config = OpenstackConfig(config_id)
|
||||
config.delete()
|
||||
print("Openstack configuration '{0}' "
|
||||
"has been deleted.".format(config_id))
|
||||
|
||||
@check_all('env')
|
||||
def execute(self, params):
|
||||
"""Deploy configuration:
|
||||
fuel openstack-config --execute --env 1
|
||||
fuel openstack-config --execute --env 1 --node 1[,2,3,...]
|
||||
fuel openstack-config --execute --env 1 --role controller
|
||||
fuel openstack-config --execute --env 1 --force
|
||||
"""
|
||||
node_ids = getattr(params, 'node', None)
|
||||
node_role = getattr(params, 'role', None)
|
||||
force = getattr(params, 'force', False)
|
||||
task_result = OpenstackConfig.execute(
|
||||
cluster_id=params.env, node_ids=node_ids,
|
||||
node_role=node_role, force=force)
|
||||
if task_result['status'] == 'error':
|
||||
print(
|
||||
'Error applying openstack configuration: {0}.'.format(
|
||||
task_result['message'])
|
||||
)
|
||||
else:
|
||||
print('Openstack configuration update is started.')
|
@ -1,209 +0,0 @@
|
||||
# Copyright 2014 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 collections
|
||||
import six
|
||||
|
||||
import fuelclient.cli.arguments as Args
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects.plugins import Plugins
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class PluginAction(Action):
|
||||
"""List and modify currently available releases
|
||||
"""
|
||||
action_name = "plugins"
|
||||
|
||||
acceptable_keys = (
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"package_version",
|
||||
"releases"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super(PluginAction, self).__init__()
|
||||
self.args = [
|
||||
Args.group(
|
||||
Args.get_list_arg(
|
||||
"List of all registered plugins."),
|
||||
Args.get_plugin_install_arg(
|
||||
"Install and register plugin package"),
|
||||
Args.get_plugin_remove_arg(
|
||||
"Remove and unregister plugin"),
|
||||
Args.get_plugin_register_arg(
|
||||
"Register installed plugin"),
|
||||
Args.get_plugin_unregister_arg(
|
||||
"Unregister plugin"),
|
||||
Args.get_plugin_update_arg(
|
||||
"Update installed plugin"),
|
||||
Args.get_plugin_downgrade_arg(
|
||||
"Downgrade installed plugin"),
|
||||
Args.get_plugin_sync_arg(
|
||||
"Synchronise plugins with API service")),
|
||||
Args.get_plugin_arg("Plugin id."),
|
||||
Args.get_force_arg("Force action")
|
||||
]
|
||||
self.flag_func_map = (
|
||||
("install", self.install),
|
||||
("remove", self.remove),
|
||||
("update", self.update),
|
||||
("downgrade", self.downgrade),
|
||||
("sync", self.sync),
|
||||
("register", self.register),
|
||||
("unregister", self.unregister),
|
||||
(None, self.list),
|
||||
)
|
||||
|
||||
def list(self, params):
|
||||
"""Print all available plugins
|
||||
|
||||
fuel plugins
|
||||
fuel plugins --list
|
||||
"""
|
||||
plugins = Plugins.get_all_data()
|
||||
# Replace original nested 'release' dictionary (from plugins meta
|
||||
# dictionary) to flat one with necessary release info (os, version)
|
||||
for plugin in plugins:
|
||||
releases = collections.defaultdict(list)
|
||||
for key in plugin['releases']:
|
||||
releases[key['os']].append(key['version'])
|
||||
plugin['releases'] = ', '.join('{} ({})'.format(k, ', '.join(v))
|
||||
for k, v in six.iteritems(releases))
|
||||
self.serializer.print_to_output(
|
||||
plugins,
|
||||
format_table(plugins, acceptable_keys=self.acceptable_keys))
|
||||
|
||||
def install(self, params):
|
||||
"""Install plugin archive and register in API service
|
||||
|
||||
fuel plugins --install plugin-name-2.0-2.0.1-0.noarch.rpm
|
||||
"""
|
||||
file_path = params.install
|
||||
self.check_file(file_path)
|
||||
results = Plugins.install(file_path, force=params.force)
|
||||
self.serializer.print_to_output(
|
||||
results,
|
||||
"Plugin {0} was successfully installed.".format(
|
||||
params.install))
|
||||
|
||||
def remove(self, params):
|
||||
"""Remove plugin from file system and from API service
|
||||
|
||||
fuel plugins --remove plugin-name==1.0.1
|
||||
"""
|
||||
name, version = self.parse_name_version(params.remove)
|
||||
results = Plugins.remove(name, version)
|
||||
|
||||
self.serializer.print_to_output(
|
||||
results,
|
||||
"Plugin {0} was successfully removed.".format(params.remove))
|
||||
|
||||
def update(self, params):
|
||||
"""Update plugin from one minor version to another.
|
||||
For example if there is a plugin with version 2.0.0,
|
||||
plugin with version 2.0.1 can be used as update. But
|
||||
plugin with version 2.1.0, cannot be used to update
|
||||
plugin. Note that update is supported for plugins
|
||||
beginning from package_version 2.0.0
|
||||
|
||||
fuel plugins --update plugin-name-2.0-2.0.1-0.noarch.rpm
|
||||
"""
|
||||
plugin_path = params.update
|
||||
self.check_file(plugin_path)
|
||||
result = Plugins.update(plugin_path)
|
||||
self.serializer.print_to_output(
|
||||
result,
|
||||
"Plugin {0} was successfully updated.".format(plugin_path))
|
||||
|
||||
def downgrade(self, params):
|
||||
"""Downgrade plugin from one minor version to another.
|
||||
For example if there is a plugin with version 2.0.1,
|
||||
plugin with version 2.0.0 can be used to perform downgrade.
|
||||
Plugin with version 1.0.0, cannot be used to perform downgrade
|
||||
plugin. Note that downgrade is supported for plugins
|
||||
beginning from package_version 2.0.0
|
||||
|
||||
fuel plugins --downgrade plugin-name-2.0-2.0.1-0.noarch.rpm
|
||||
"""
|
||||
plugin_path = params.downgrade
|
||||
self.check_file(plugin_path)
|
||||
result = Plugins.downgrade(plugin_path)
|
||||
self.serializer.print_to_output(
|
||||
result,
|
||||
"Plugin {0} was successfully downgraded.".format(plugin_path))
|
||||
|
||||
def sync(self, params):
|
||||
"""Synchronise plugins on file system with plugins in
|
||||
API service, creates plugin if it is not exists,
|
||||
updates existent plugins
|
||||
|
||||
fuel plugins --sync
|
||||
fuel plugins --sync --plugin-id=1,2
|
||||
"""
|
||||
Plugins.sync(plugin_ids=params.plugin)
|
||||
self.serializer.print_to_output(
|
||||
None, "Plugins were successfully synchronized.")
|
||||
|
||||
def register(self, params):
|
||||
"""Register plugin in API service
|
||||
|
||||
fuel plugins --register plugin-name==1.0.1
|
||||
"""
|
||||
name, version = self.parse_name_version(params.register)
|
||||
result = Plugins.register(name, version, force=params.force)
|
||||
self.serializer.print_to_output(
|
||||
result,
|
||||
"Plugin {0} was successfully registered.".format(params.register))
|
||||
|
||||
def unregister(self, params):
|
||||
"""Deletes plugin from API service
|
||||
|
||||
fuel plugins --unregister plugin-name==1.0.1
|
||||
"""
|
||||
name, version = self.parse_name_version(params.unregister)
|
||||
result = Plugins.unregister(name, version)
|
||||
self.serializer.print_to_output(
|
||||
result,
|
||||
"Plugin {0} was successfully unregistered."
|
||||
"".format(params.unregister))
|
||||
|
||||
def parse_name_version(self, param):
|
||||
"""Takes the string and returns name and version
|
||||
|
||||
:param str param: string with name and version
|
||||
:raises: error.ArgumentException if version is not specified
|
||||
"""
|
||||
attrs = param.split('==')
|
||||
|
||||
if len(attrs) != 2:
|
||||
raise error.ArgumentException(
|
||||
'Syntax: fuel plugins <action> fuel_plugin==1.0.0')
|
||||
|
||||
return attrs
|
||||
|
||||
def check_file(self, file_path):
|
||||
"""Checks if file exists
|
||||
|
||||
:param str file_path: path to the file
|
||||
:raises: error.ArgumentException if file does not exist
|
||||
"""
|
||||
if not utils.file_exists(file_path):
|
||||
raise error.ArgumentException(
|
||||
'File "{0}" does not exists'.format(file_path))
|
@ -1,172 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from collections import defaultdict
|
||||
import os
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli.actions.base import check_all
|
||||
from fuelclient.cli.actions.base import check_any
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects.release import Release
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class ReleaseAction(Action):
|
||||
"""List and modify currently available releases
|
||||
"""
|
||||
action_name = "release"
|
||||
|
||||
def __init__(self):
|
||||
super(ReleaseAction, self).__init__()
|
||||
self.args = [
|
||||
Args.get_release_arg('Specify particular release id'),
|
||||
Args.get_list_arg("List all available releases."),
|
||||
Args.get_network_arg("Release network configuration."),
|
||||
Args.get_deployment_tasks_arg("Release tasks configuration."),
|
||||
Args.get_sync_deployment_tasks_arg(),
|
||||
Args.get_file_pattern_arg(),
|
||||
Args.get_dir_arg(
|
||||
"Select directory to which download release attributes"),
|
||||
group(
|
||||
Args.get_download_arg(
|
||||
"Download configuration of specific release"),
|
||||
Args.get_upload_arg(
|
||||
"Upload configuration to specific release")
|
||||
)
|
||||
]
|
||||
self.flag_func_map = (
|
||||
('sync-deployment-tasks', self.sync_deployment_tasks),
|
||||
('deployment-tasks', self.deployment_tasks),
|
||||
('network', self.network),
|
||||
(None, self.list),
|
||||
)
|
||||
|
||||
def list(self, params):
|
||||
"""Print all available releases:
|
||||
fuel release --list
|
||||
|
||||
Print release with specific id=1:
|
||||
fuel release --rel 1
|
||||
"""
|
||||
acceptable_keys = (
|
||||
"id",
|
||||
"name",
|
||||
"state",
|
||||
"operating_system",
|
||||
"version"
|
||||
)
|
||||
if params.release:
|
||||
release = Release(params.release)
|
||||
data = [release.get_fresh_data()]
|
||||
else:
|
||||
data = Release.get_all_data()
|
||||
self.serializer.print_to_output(
|
||||
data,
|
||||
format_table(
|
||||
data,
|
||||
acceptable_keys=acceptable_keys
|
||||
)
|
||||
)
|
||||
|
||||
@check_all("release")
|
||||
@check_any("download", "upload")
|
||||
def network(self, params):
|
||||
"""Modify release networks configuration.
|
||||
fuel rel --rel 1 --network --download
|
||||
fuel rel --rel 2 --network --upload
|
||||
"""
|
||||
release = Release(params.release)
|
||||
dir_path = self.full_path_directory(
|
||||
params.dir, 'release_{0}'.format(params.release))
|
||||
full_path = '{0}/networks'.format(dir_path)
|
||||
if params.download:
|
||||
networks = release.get_networks()
|
||||
self.serializer.write_to_path(full_path, networks)
|
||||
print("Networks for release {0} "
|
||||
"downloaded into {1}.yaml".format(release.id, full_path))
|
||||
elif params.upload:
|
||||
networks = self.serializer.read_from_file(full_path)
|
||||
release.update_networks(networks)
|
||||
print("Networks for release {0} uploaded from {1}.yaml".format(
|
||||
release.id, full_path))
|
||||
|
||||
@check_all("release")
|
||||
@check_any("download", "upload")
|
||||
def deployment_tasks(self, params):
|
||||
"""Modify deployment_tasks for release.
|
||||
fuel rel --rel 1 --deployment-tasks --download
|
||||
fuel rel --rel 1 --deployment-tasks --upload
|
||||
"""
|
||||
release = Release(params.release)
|
||||
dir_path = self.full_path_directory(
|
||||
params.dir, 'release_{0}'.format(params.release))
|
||||
full_path = '{0}/deployment_tasks'.format(dir_path)
|
||||
if params.download:
|
||||
tasks = release.get_deployment_tasks()
|
||||
self.serializer.write_to_path(full_path, tasks)
|
||||
print("Deployment tasks for release {0} "
|
||||
"downloaded into {1}.yaml.".format(release.id, full_path))
|
||||
elif params.upload:
|
||||
tasks = self.serializer.read_from_file(full_path)
|
||||
release.update_deployment_tasks(tasks)
|
||||
print("Deployment tasks for release {0}"
|
||||
" uploaded from {1}.yaml".format(release.id, dir_path))
|
||||
|
||||
@check_all("dir")
|
||||
def sync_deployment_tasks(self, params):
|
||||
"""Upload tasks for different releases based on directories.
|
||||
The string identifier for the release(s) to update is expected to be
|
||||
found in the path (see `fuel release`), like:
|
||||
|
||||
/etc/puppet/<release>/
|
||||
/etc/puppet/liberty-9.0
|
||||
|
||||
fuel rel --sync-deployment-tasks --dir /etc/puppet/liberty-9.0/
|
||||
fuel rel --sync-deployment-tasks --fp '*tasks.yaml'
|
||||
|
||||
In case no directory was provided:
|
||||
|
||||
fuel rel --sync-deployment-tasks
|
||||
|
||||
The current directory will be used
|
||||
"""
|
||||
all_rels = Release.get_all_data()
|
||||
real_path = os.path.realpath(params.dir)
|
||||
serialized_tasks = defaultdict(list)
|
||||
versions = set([r['version'] for r in all_rels])
|
||||
|
||||
for file_name in utils.iterfiles(real_path, params.filepattern):
|
||||
for version in versions:
|
||||
if version in file_name:
|
||||
serialized_tasks[version].extend(
|
||||
self.serializer.read_from_full_path(file_name))
|
||||
|
||||
for rel in all_rels:
|
||||
release = Release(rel['id'])
|
||||
data = serialized_tasks.get(rel['version'])
|
||||
if data:
|
||||
release.update_deployment_tasks(data)
|
||||
print("Deployment tasks synchronized for release"
|
||||
" {0} of version {1}".format(rel['name'],
|
||||
rel['version']))
|
||||
else:
|
||||
print("No tasks were synchronized for release {0} "
|
||||
"of version {1}.(Hint: nothing matched "
|
||||
"{2}/{1}/{3})".format(rel['name'],
|
||||
rel['version'],
|
||||
real_path,
|
||||
params.filepattern))
|
@ -1,155 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.cli.actions.base import check_all
|
||||
from fuelclient.cli.actions.base import check_any
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.cli.serializers import FileFormatBasedSerializer
|
||||
from fuelclient.objects.role import Role
|
||||
|
||||
|
||||
class RoleAction(Action):
|
||||
"""List all roles for specific release or cluster
|
||||
"""
|
||||
action_name = "role"
|
||||
|
||||
fields_mapper = (
|
||||
('env', 'clusters'),
|
||||
('release', 'releases')
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
# NOTE(dshulyak) this serializers are really messed up
|
||||
# it gets overwritten in several places
|
||||
self.file_serializer = FileFormatBasedSerializer()
|
||||
self.args = [
|
||||
Args.get_list_arg("List all roles"),
|
||||
group(
|
||||
Args.get_env_arg(),
|
||||
Args.get_release_arg("Release id"),
|
||||
required=True
|
||||
),
|
||||
Args.get_str_arg("role", help="Name of the role"),
|
||||
Args.get_file_arg("File with role description"),
|
||||
|
||||
group(
|
||||
Args.get_create_arg("Create role from file"),
|
||||
Args.get_boolean_arg("update", help="Update role from file"),
|
||||
Args.get_delete_arg("Delete role from fuel")
|
||||
)
|
||||
]
|
||||
self.flag_func_map = (
|
||||
("delete", self.delete),
|
||||
("create", self.create),
|
||||
("update", self.update),
|
||||
("role", self.item),
|
||||
(None, self.list),
|
||||
)
|
||||
|
||||
def parse_model(self, args):
|
||||
for param, role_class in self.fields_mapper:
|
||||
model_id = getattr(args, param)
|
||||
if model_id:
|
||||
return role_class, model_id
|
||||
|
||||
@check_any('release', 'env')
|
||||
def list(self, params):
|
||||
"""Print all available roles for release or cluster
|
||||
|
||||
fuel role --rel 1
|
||||
fuel role --env 1
|
||||
"""
|
||||
model, model_id = self.parse_model(params)
|
||||
roles = Role(owner_type=model, owner_id=model_id).get_all()
|
||||
|
||||
acceptable_keys = ("name", )
|
||||
|
||||
self.serializer.print_to_output(
|
||||
roles,
|
||||
format_table(
|
||||
roles,
|
||||
acceptable_keys=acceptable_keys
|
||||
)
|
||||
)
|
||||
|
||||
@check_all('role', 'file')
|
||||
@check_any('release', 'env')
|
||||
def item(self, params):
|
||||
"""Save full role description to file
|
||||
fuel role --rel 1 --role controller --file some.yaml
|
||||
fuel role --env 1 --role controller --file some.yaml
|
||||
"""
|
||||
model, model_id = self.parse_model(params)
|
||||
role = Role(owner_type=model, owner_id=model_id).get_role(params.role)
|
||||
self.file_serializer.write_to_file(params.file, role)
|
||||
self.file_serializer.print_to_output(
|
||||
role,
|
||||
"Role {0} for {1} successfully saved to {2}.".format(
|
||||
params.role,
|
||||
model,
|
||||
params.file))
|
||||
|
||||
@check_all('file')
|
||||
@check_any('release', 'env')
|
||||
def create(self, params):
|
||||
"""Create a role from file description
|
||||
fuel role --rel 1 --create --file some.yaml
|
||||
fuel role --env 1 --create --file some.yaml
|
||||
"""
|
||||
model, model_id = self.parse_model(params)
|
||||
role = self.file_serializer.read_from_file(params.file)
|
||||
role = Role(owner_type=model, owner_id=model_id).create_role(role)
|
||||
self.file_serializer.print_to_output(
|
||||
role,
|
||||
"Role {0} for {1} successfully created from {2}.".format(
|
||||
role['name'], model, params.file))
|
||||
|
||||
@check_all('file')
|
||||
@check_any('release', 'env')
|
||||
def update(self, params):
|
||||
"""Update a role from file description
|
||||
fuel role --rel 1 --create --file some.yaml
|
||||
fuel role --env 1 --create --file some.yaml
|
||||
"""
|
||||
model, model_id = self.parse_model(params)
|
||||
role = self.file_serializer.read_from_file(params.file)
|
||||
role = Role(owner_type=model, owner_id=model_id).update_role(
|
||||
role['name'],
|
||||
role)
|
||||
self.file_serializer.print_to_output(
|
||||
role,
|
||||
"Role {0} for {1} successfully updated from {2}.".format(
|
||||
params.role,
|
||||
model,
|
||||
params.file))
|
||||
|
||||
@check_all('role')
|
||||
@check_any('release', 'env')
|
||||
def delete(self, params):
|
||||
"""Delete role from fuel
|
||||
fuel role --delete --role controller --rel 1
|
||||
fuel role --delete --role controller --env 1
|
||||
"""
|
||||
model, model_id = self.parse_model(params)
|
||||
Role(owner_type=model, owner_id=model_id).delete_role(params.role)
|
||||
self.file_serializer.print_to_output(
|
||||
{},
|
||||
"Role {0} for {1} with id {2} successfully deleted.".format(
|
||||
params.role,
|
||||
model,
|
||||
model_id))
|
@ -1,86 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.arguments import group
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class SettingsAction(Action):
|
||||
"""Show or modify environment settings
|
||||
"""
|
||||
action_name = "settings"
|
||||
|
||||
def __init__(self):
|
||||
super(SettingsAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_env_arg(required=True),
|
||||
group(
|
||||
Args.get_download_arg("Modify current configuration."),
|
||||
Args.get_default_arg("Open default configuration."),
|
||||
Args.get_upload_arg("Save current changes in configuration."),
|
||||
required=True
|
||||
),
|
||||
Args.get_dir_arg("Directory with configuration data."),
|
||||
Args.get_force_arg("Force settings upload.")
|
||||
)
|
||||
self.flag_func_map = (
|
||||
("upload", self.upload),
|
||||
("default", self.default),
|
||||
("download", self.download)
|
||||
)
|
||||
|
||||
def upload(self, params):
|
||||
"""To upload settings for some environment from some directory:
|
||||
fuel --env 1 settings --upload --dir path/to/directory
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
settings_data = env.read_settings_data(
|
||||
directory=params.dir,
|
||||
serializer=self.serializer
|
||||
)
|
||||
env.set_settings_data(settings_data, params.force)
|
||||
print("Settings configuration uploaded.")
|
||||
|
||||
def default(self, params):
|
||||
"""To download default settings for some environment in some directory:
|
||||
fuel --env 1 settings --default --dir path/to/directory
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
default_data = env.get_default_settings_data()
|
||||
settings_file_path = env.write_settings_data(
|
||||
default_data,
|
||||
directory=params.dir,
|
||||
serializer=self.serializer)
|
||||
print(
|
||||
"Default settings configuration downloaded to {0}."
|
||||
.format(settings_file_path)
|
||||
)
|
||||
|
||||
def download(self, params):
|
||||
"""To download settings for some environment in this directory:
|
||||
fuel --env 1 settings --download
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
settings_data = env.get_settings_data()
|
||||
settings_file_path = env.write_settings_data(
|
||||
settings_data,
|
||||
directory=params.dir,
|
||||
serializer=self.serializer)
|
||||
print(
|
||||
"Settings configuration for environment with id={0}"
|
||||
" downloaded to {1}"
|
||||
.format(env.id, settings_file_path)
|
||||
)
|
@ -1,84 +0,0 @@
|
||||
# Copyright 2014 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 sys
|
||||
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.objects.task import SnapshotTask
|
||||
|
||||
|
||||
class SnapshotAction(Action):
|
||||
"""Generate and download snapshot.
|
||||
"""
|
||||
action_name = "snapshot"
|
||||
|
||||
def __init__(self):
|
||||
super(SnapshotAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_boolean_arg("conf",
|
||||
help_="Provide this flag to generate conf"),
|
||||
)
|
||||
self.flag_func_map = (
|
||||
('conf', self.get_snapshot_config),
|
||||
(None, self.create_snapshot),
|
||||
)
|
||||
|
||||
def create_snapshot(self, params):
|
||||
"""To create diagnostic snapshot:
|
||||
fuel snapshot
|
||||
|
||||
To specify config for snapshotting:
|
||||
fuel snapshot < conf.yaml
|
||||
|
||||
"""
|
||||
if sys.stdin.isatty():
|
||||
conf = {}
|
||||
else:
|
||||
conf = yaml.load(sys.stdin.read())
|
||||
|
||||
snapshot_task = SnapshotTask.start_snapshot_task(conf)
|
||||
self.serializer.print_to_output(
|
||||
snapshot_task.data,
|
||||
"Generating diagnostic snapshot..."
|
||||
)
|
||||
snapshot_task.wait()
|
||||
|
||||
if snapshot_task.status == 'ready':
|
||||
self.serializer.print_to_output(
|
||||
snapshot_task.data,
|
||||
"...Completed...\n"
|
||||
"Diagnostic snapshot can be downloaded from " +
|
||||
snapshot_task.connection.root +
|
||||
snapshot_task.data["message"]
|
||||
)
|
||||
elif snapshot_task.status == 'error':
|
||||
six.print_(
|
||||
"Snapshot generating task ended with error. Task message: {0}"
|
||||
.format(snapshot_task.data["message"]),
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
def get_snapshot_config(self, params):
|
||||
"""Download default config for snapshot:
|
||||
fuel snapshot --conf > dump_conf.yaml
|
||||
|
||||
To use json formatter:
|
||||
fuel snapshot --conf --json
|
||||
"""
|
||||
conf = SnapshotTask.get_default_config()
|
||||
self.serializer.write_to_file(sys.stdout, conf)
|
@ -1,37 +0,0 @@
|
||||
# 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 sys
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
from fuelclient.client import DefaultAPIClient
|
||||
|
||||
|
||||
class TokenAction(Action):
|
||||
"""Return a valid keystone auth token
|
||||
"""
|
||||
action_name = "token"
|
||||
|
||||
def __init__(self):
|
||||
super(TokenAction, self).__init__()
|
||||
|
||||
self.args = []
|
||||
self.flag_func_map = (
|
||||
(None, self.get_token),
|
||||
)
|
||||
|
||||
def get_token(self, params):
|
||||
"""Print out a valid Keystone auth token
|
||||
"""
|
||||
sys.stdout.write(DefaultAPIClient.auth_token)
|
@ -1,65 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from getpass import getpass
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.error import ArgumentException
|
||||
from fuelclient.client import DefaultAPIClient
|
||||
from fuelclient import fuelclient_settings
|
||||
|
||||
|
||||
class UserAction(Action):
|
||||
"""Change password for user
|
||||
"""
|
||||
action_name = "user"
|
||||
|
||||
def __init__(self):
|
||||
super(UserAction, self).__init__()
|
||||
self.args = (
|
||||
Args.get_new_password_arg(
|
||||
"WARNING: This method of changing the "
|
||||
"password is dangerous - it may be saved in bash history."),
|
||||
Args.get_change_password_arg(
|
||||
"Change user password using interactive prompt")
|
||||
)
|
||||
|
||||
self.flag_func_map = (
|
||||
("change-password", self.change_password),
|
||||
)
|
||||
|
||||
def _get_password_from_prompt(self):
|
||||
password1 = getpass("Changing password for Fuel User.\nNew Password:")
|
||||
password2 = getpass("Retype new Password:")
|
||||
if password1 != password2:
|
||||
raise ArgumentException("Passwords are not the same.")
|
||||
return password1
|
||||
|
||||
def change_password(self, params):
|
||||
"""To change user password:
|
||||
fuel user change-password
|
||||
"""
|
||||
if params.newpass:
|
||||
password = params.newpass
|
||||
else:
|
||||
password = self._get_password_from_prompt()
|
||||
|
||||
DefaultAPIClient.update_own_password(password)
|
||||
settings = fuelclient_settings.get_settings()
|
||||
self.serializer.print_to_output(
|
||||
None, "\nPassword changed.\nPlease note that configuration "
|
||||
"is not automatically updated.\nYou may want to update "
|
||||
"{0}.".format(
|
||||
settings.user_settings))
|
@ -1,106 +0,0 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli import serializers
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class VIPAction(Action):
|
||||
"""Download or upload VIP settings of specific environments.
|
||||
"""
|
||||
action_name = "vip"
|
||||
acceptable_keys = ("id", "upload", "download", "network", "network-role",)
|
||||
|
||||
def __init__(self):
|
||||
super(VIPAction, self).__init__()
|
||||
# NOTE(aroma): 'serializer' attribute for action objects is
|
||||
# overwritten while building parser object
|
||||
# (fuelclient.cli.parser.Parser)
|
||||
self.file_serializer = serializers.FileFormatBasedSerializer()
|
||||
self.args = (
|
||||
Args.get_env_arg(required=True),
|
||||
Args.get_create_arg("Create VIP"),
|
||||
Args.get_upload_file_arg("Upload changed VIP configuration "
|
||||
"from given file"),
|
||||
Args.get_download_arg("Download VIP configuration"),
|
||||
Args.get_file_arg("Target file with vip data."),
|
||||
Args.get_ip_id_arg("IP address entity identifier"),
|
||||
Args.get_ip_address_arg("IP address string"),
|
||||
Args.get_network_id_arg("Network identifier"),
|
||||
Args.get_network_role_arg("Network role string"),
|
||||
Args.get_vip_name_arg("VIP name string"),
|
||||
Args.get_vip_namespace_arg("VIP namespace string"),
|
||||
)
|
||||
self.flag_func_map = (
|
||||
("create", self.create),
|
||||
("upload", self.upload),
|
||||
("download", self.download)
|
||||
)
|
||||
|
||||
def create(self, params):
|
||||
"""To create VIP for environment:
|
||||
fuel --env 1 vip create --address 172.16.0.10 --network 1 \\
|
||||
--name public_vip --namespace haproxy
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
vip_kwargs = {
|
||||
"ip_addr": getattr(params, 'ip-address'),
|
||||
"network": getattr(params, 'network'),
|
||||
"vip_name": getattr(params, 'vip-name'),
|
||||
}
|
||||
|
||||
vip_namespace = getattr(params, 'vip-namespace', None)
|
||||
if vip_namespace is not None:
|
||||
vip_kwargs['vip_namespace'] = vip_namespace
|
||||
|
||||
env.create_vip(**vip_kwargs)
|
||||
print("VIP has been created")
|
||||
|
||||
def upload(self, params):
|
||||
"""To upload VIP configuration from some
|
||||
file for some environment:
|
||||
fuel --env 1 vip --upload vip.yaml
|
||||
"""
|
||||
env = Environment(params.env)
|
||||
vips_data = env.read_vips_data_from_file(
|
||||
file_path=params.upload,
|
||||
serializer=self.file_serializer
|
||||
)
|
||||
env.set_vips_data(vips_data)
|
||||
print("VIP configuration uploaded.")
|
||||
|
||||
def download(self, params):
|
||||
"""To download VIP configuration in this
|
||||
file for some environment:
|
||||
fuel --env 1 vip --download --file vip.yaml
|
||||
where --file param is optional
|
||||
"""
|
||||
|
||||
env = Environment(params.env)
|
||||
vips_data = env.get_vips_data(
|
||||
ip_address_id=getattr(params, 'ip-address-id'),
|
||||
network=getattr(params, 'network'),
|
||||
network_role=getattr(params, 'network-role')
|
||||
)
|
||||
vips_data_file_path = env.write_vips_data_to_file(
|
||||
vips_data,
|
||||
file_path=params.file,
|
||||
serializer=self.serializer
|
||||
)
|
||||
print(
|
||||
"VIP configuration for environment with id={0}"
|
||||
" downloaded to {1}".format(env.id, vips_data_file_path)
|
||||
)
|
@ -1,787 +0,0 @@
|
||||
# Copyright 2014 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 argparse
|
||||
from itertools import chain
|
||||
import os
|
||||
|
||||
from fuelclient import __version__
|
||||
from fuelclient.cli.error import ArgumentException
|
||||
from fuelclient.client import DefaultAPIClient
|
||||
|
||||
substitutions = {
|
||||
# replace from: to
|
||||
"env": "environment",
|
||||
"nodes": "node",
|
||||
"statuses": "status",
|
||||
"net": "network",
|
||||
"rel": "release",
|
||||
"list": "--list",
|
||||
"set": "--set",
|
||||
"delete": "--delete",
|
||||
"download": "--download",
|
||||
"upload": "--upload",
|
||||
"default": "--default",
|
||||
"create": "--create",
|
||||
"remove": "--delete",
|
||||
"config": "--config",
|
||||
"--roles": "--role",
|
||||
"help": "--help",
|
||||
"change-password": "--change-password",
|
||||
"hostname": "--hostname",
|
||||
}
|
||||
|
||||
|
||||
def group(*args, **kwargs):
|
||||
required = kwargs.get("required", False)
|
||||
return (required,) + args
|
||||
|
||||
|
||||
class ArrayAction(argparse.Action):
|
||||
"""Custom argparse.Action subclass to store ids
|
||||
|
||||
:returns: list of ids
|
||||
"""
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
list_ids = [int(value) for value in chain(*values)]
|
||||
setattr(namespace, self.dest, list_ids)
|
||||
|
||||
|
||||
class NodeAction(argparse.Action):
|
||||
"""Custom argparse.Action subclass to store node identity
|
||||
|
||||
:returns: list of ids
|
||||
"""
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if values:
|
||||
node_identities = set(chain(*values))
|
||||
input_macs = set(n for n in node_identities if ":" in n)
|
||||
only_ids = set()
|
||||
for _id in (node_identities - input_macs):
|
||||
try:
|
||||
only_ids.add(int(_id))
|
||||
except ValueError:
|
||||
raise ArgumentException(
|
||||
"'{0}' is not valid node id.".format(_id))
|
||||
if input_macs:
|
||||
nodes_mac_to_id_map = dict(
|
||||
(n["mac"], n["id"])
|
||||
for n in DefaultAPIClient.get_request("nodes/")
|
||||
)
|
||||
for short_mac in input_macs:
|
||||
target_node = None
|
||||
for mac in nodes_mac_to_id_map:
|
||||
if mac.endswith(short_mac):
|
||||
target_node = mac
|
||||
break
|
||||
if target_node:
|
||||
only_ids.add(nodes_mac_to_id_map[target_node])
|
||||
else:
|
||||
raise ArgumentException(
|
||||
'Node with mac endfix "{0}" was not found.'
|
||||
.format(short_mac)
|
||||
)
|
||||
node_ids = [int(node_id) for node_id in only_ids]
|
||||
setattr(namespace, self.dest, node_ids)
|
||||
|
||||
|
||||
class SetAction(argparse.Action):
|
||||
"""Custom argparse.Action subclass to store distinct values
|
||||
|
||||
:returns: Set of arguments
|
||||
"""
|
||||
def __call__(self, _parser, namespace, values, option_string=None):
|
||||
try:
|
||||
getattr(namespace, self.dest).update(values)
|
||||
except AttributeError:
|
||||
setattr(namespace, self.dest, set(values))
|
||||
|
||||
|
||||
def get_debug_arg():
|
||||
return {
|
||||
"args": ["--debug"],
|
||||
"params": {
|
||||
"dest": "debug",
|
||||
"action": "store_true",
|
||||
"help": "prints details of all HTTP request",
|
||||
"default": False
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_version_arg():
|
||||
return {
|
||||
"args": ["-v", "--version"],
|
||||
"params": {
|
||||
"action": "version",
|
||||
"version": __version__
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_arg(name, flags=None, aliases=None, help_=None, **kwargs):
|
||||
name = name.replace("_", "-")
|
||||
args = ["--" + name, ]
|
||||
if flags is not None:
|
||||
args.extend(flags)
|
||||
if aliases is not None:
|
||||
substitutions.update(
|
||||
dict((alias, args[0]) for alias in aliases)
|
||||
)
|
||||
all_args = {
|
||||
"args": args,
|
||||
"params": {
|
||||
"dest": name,
|
||||
"help": help_ or name
|
||||
}
|
||||
}
|
||||
all_args["params"].update(kwargs)
|
||||
return all_args
|
||||
|
||||
|
||||
def get_boolean_arg(name, **kwargs):
|
||||
kwargs.update({
|
||||
"action": "store_true",
|
||||
"default": False
|
||||
})
|
||||
return get_arg(name, **kwargs)
|
||||
|
||||
|
||||
def get_env_arg(required=False):
|
||||
return get_int_arg(
|
||||
"env",
|
||||
flags=("--env-id",),
|
||||
help="environment id",
|
||||
required=required
|
||||
)
|
||||
|
||||
|
||||
def get_single_task_arg(required=False):
|
||||
return get_int_arg(
|
||||
"task",
|
||||
flags=("--task-id", "--tid"),
|
||||
help="task id",
|
||||
required=required
|
||||
)
|
||||
|
||||
|
||||
def get_new_password_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"newpass",
|
||||
flags=("--new-pass",),
|
||||
help=help_msg,
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
def get_str_arg(name, **kwargs):
|
||||
default_kwargs = {
|
||||
"action": "store",
|
||||
"type": str,
|
||||
"default": None
|
||||
}
|
||||
default_kwargs.update(kwargs)
|
||||
return get_arg(name, **default_kwargs)
|
||||
|
||||
|
||||
def get_int_arg(name, **kwargs):
|
||||
default_kwargs = {
|
||||
"action": "store",
|
||||
"type": int,
|
||||
"default": None
|
||||
}
|
||||
default_kwargs.update(kwargs)
|
||||
return get_arg(name, **default_kwargs)
|
||||
|
||||
|
||||
def get_array_arg(name, **kwargs):
|
||||
default_kwargs = {
|
||||
"action": ArrayAction,
|
||||
"nargs": '+',
|
||||
"type": lambda v: v.split(","),
|
||||
"default": None
|
||||
}
|
||||
default_kwargs.update(kwargs)
|
||||
return get_arg(name, **default_kwargs)
|
||||
|
||||
|
||||
def get_set_type_arg(name, **kwargs):
|
||||
default_kwargs = {
|
||||
"type": lambda v: v.split(','),
|
||||
"action": SetAction,
|
||||
"default": None
|
||||
}
|
||||
default_kwargs.update(kwargs)
|
||||
return get_arg(name, **default_kwargs)
|
||||
|
||||
|
||||
def get_delete_from_db_arg(help_msg):
|
||||
return get_boolean_arg("delete-from-db", help=help_msg)
|
||||
|
||||
|
||||
def get_deployment_tasks_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
"deployment-tasks", help=help_msg)
|
||||
|
||||
|
||||
def get_attributes_arg(help_msg):
|
||||
return get_boolean_arg("attributes", help=help_msg)
|
||||
|
||||
|
||||
def get_sync_deployment_tasks_arg():
|
||||
return get_boolean_arg(
|
||||
"sync-deployment-tasks",
|
||||
help="Update tasks for each release.")
|
||||
|
||||
|
||||
def get_dry_run_deployment_arg():
|
||||
return get_boolean_arg(
|
||||
"dry-run",
|
||||
dest='dry_run',
|
||||
help="Specifies to dry-run a deployment by configuring task executor"
|
||||
"to dump the deployment graph to a dot file.")
|
||||
|
||||
|
||||
def get_noop_run_deployment_arg():
|
||||
return get_boolean_arg(
|
||||
"noop",
|
||||
dest='noop_run',
|
||||
help="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")
|
||||
|
||||
|
||||
def get_file_pattern_arg():
|
||||
return get_str_arg(
|
||||
"filepattern",
|
||||
flags=("--fp", "--file-pattern"),
|
||||
default="*tasks.yaml",
|
||||
help="Provide unix file pattern to filter tasks with files.")
|
||||
|
||||
|
||||
def get_node_name_arg(help_msg):
|
||||
return get_str_arg("name", help=help_msg)
|
||||
|
||||
|
||||
def get_hostname_arg(help_msg):
|
||||
return get_str_arg("hostname", help=help_msg)
|
||||
|
||||
|
||||
def get_network_arg(help_msg):
|
||||
return get_boolean_arg("network", flags=("--net",), help=help_msg)
|
||||
|
||||
|
||||
def get_force_arg(help_msg):
|
||||
return get_boolean_arg("force", flags=("-f",), help=help_msg)
|
||||
|
||||
|
||||
def get_disk_arg(help_msg):
|
||||
return get_boolean_arg("disk", help=help_msg)
|
||||
|
||||
|
||||
def get_deploy_arg(help_msg):
|
||||
return get_boolean_arg("deploy", help=help_msg)
|
||||
|
||||
|
||||
def get_provision_arg(help_msg):
|
||||
return get_boolean_arg("provision", help=help_msg)
|
||||
|
||||
|
||||
def get_role_arg(help_msg):
|
||||
return get_set_type_arg("role", flags=("-r",), help=help_msg)
|
||||
|
||||
|
||||
def get_single_role_arg(help_msg):
|
||||
return get_str_arg("role", flags=('--role', ), help=help_msg)
|
||||
|
||||
|
||||
def get_check_arg(help_msg):
|
||||
return get_set_type_arg("check", help=help_msg)
|
||||
|
||||
|
||||
def get_ostf_username_arg():
|
||||
return get_str_arg(
|
||||
"ostf_username",
|
||||
dest="ostf_username",
|
||||
help="OSTF username",
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
def get_ostf_password_arg():
|
||||
return get_str_arg(
|
||||
"ostf_password",
|
||||
dest="ostf_password",
|
||||
help="OSTF password",
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
def get_ostf_tenant_name_arg():
|
||||
return get_str_arg(
|
||||
"ostf_tenant_name",
|
||||
dest="ostf_tenant_name",
|
||||
help="OSTF tenant name",
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
def get_change_password_arg(help_msg):
|
||||
return get_boolean_arg("change-password", help=help_msg)
|
||||
|
||||
|
||||
def get_name_arg(help_msg):
|
||||
return get_str_arg("name", flags=("--env-name",), help=help_msg)
|
||||
|
||||
|
||||
def get_graph_endpoint():
|
||||
return get_arg(
|
||||
'end',
|
||||
action="store",
|
||||
default=None,
|
||||
help="Specify endpoint for the graph traversal.",
|
||||
metavar='TASK',
|
||||
)
|
||||
|
||||
|
||||
def get_graph_startpoint():
|
||||
return get_arg(
|
||||
'start',
|
||||
action="store",
|
||||
default=None,
|
||||
help="Specify start point for the graph traversal.",
|
||||
metavar='TASK',
|
||||
)
|
||||
|
||||
|
||||
def get_skip_tasks():
|
||||
return get_arg(
|
||||
'skip',
|
||||
nargs='+',
|
||||
default=[],
|
||||
help="Get list of tasks to be skipped.",
|
||||
metavar='TASK',
|
||||
)
|
||||
|
||||
|
||||
def get_tasks():
|
||||
return get_arg(
|
||||
'tasks',
|
||||
nargs='+',
|
||||
default=[],
|
||||
help="Get list of tasks to be executed.",
|
||||
metavar='TASK',
|
||||
)
|
||||
|
||||
|
||||
def get_parents_arg():
|
||||
return get_arg(
|
||||
'parents-for',
|
||||
help="Get parent for given task",
|
||||
metavar='TASK',
|
||||
)
|
||||
|
||||
|
||||
def get_remove_type_arg(types):
|
||||
return get_arg(
|
||||
'remove',
|
||||
nargs='+',
|
||||
default=[],
|
||||
choices=types,
|
||||
help="Select task types to remove from graph.",
|
||||
)
|
||||
|
||||
|
||||
def get_nst_arg(help_msg):
|
||||
return get_arg("nst",
|
||||
flags=("--net-segment-type",),
|
||||
action="store",
|
||||
choices=("gre", "vlan", "tun"),
|
||||
help_=help_msg,
|
||||
default="vlan")
|
||||
|
||||
|
||||
def get_all_arg(help_msg):
|
||||
return get_boolean_arg("all", help=help_msg)
|
||||
|
||||
|
||||
def get_create_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
"create",
|
||||
flags=("-c", "--env-create"),
|
||||
help=help_msg)
|
||||
|
||||
|
||||
def get_download_arg(help_msg):
|
||||
return get_boolean_arg("download", flags=("-d",), help=help_msg)
|
||||
|
||||
|
||||
def get_list_arg(help_msg):
|
||||
return get_boolean_arg("list", flags=("-l",), help=help_msg)
|
||||
|
||||
|
||||
def get_dir_arg(help_msg):
|
||||
return get_str_arg("dir", default=os.curdir, help=help_msg)
|
||||
|
||||
|
||||
def get_file_arg(help_msg):
|
||||
return get_str_arg("file", help=help_msg)
|
||||
|
||||
|
||||
def get_verify_arg(help_msg):
|
||||
return get_boolean_arg("verify", flags=("-v",), help=help_msg)
|
||||
|
||||
|
||||
def get_upload_arg(help_msg):
|
||||
return get_boolean_arg("upload", flags=("-u",), help=help_msg)
|
||||
|
||||
|
||||
def get_default_arg(help_msg):
|
||||
return get_boolean_arg("default", help=help_msg)
|
||||
|
||||
|
||||
def get_set_arg(help_msg):
|
||||
return get_boolean_arg("set", flags=("-s",), help=help_msg)
|
||||
|
||||
|
||||
def get_delete_arg(help_msg):
|
||||
return get_boolean_arg("delete", help=help_msg)
|
||||
|
||||
|
||||
def get_execute_arg(help_msg):
|
||||
return get_boolean_arg("execute", help=help_msg)
|
||||
|
||||
|
||||
def get_assign_arg(help_msg):
|
||||
return get_boolean_arg("assign", help=help_msg)
|
||||
|
||||
|
||||
def get_group_arg(help_msg):
|
||||
return get_set_type_arg("group", help=help_msg)
|
||||
|
||||
|
||||
def get_node_group_arg(help_msg):
|
||||
return get_set_type_arg("nodegroup", flags=("--node-group",),
|
||||
help=help_msg)
|
||||
|
||||
|
||||
def get_vlan_arg(help_msg):
|
||||
return get_int_arg("vlan", help=help_msg)
|
||||
|
||||
|
||||
def get_cidr_arg(help_msg):
|
||||
return get_str_arg("cidr", help=help_msg)
|
||||
|
||||
|
||||
def get_gateway_arg(help_msg):
|
||||
return get_str_arg("gateway", help=help_msg)
|
||||
|
||||
|
||||
def get_meta_arg(help_msg):
|
||||
return get_str_arg("meta", help=help_msg)
|
||||
|
||||
|
||||
def get_create_network_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
"create",
|
||||
flags=("-c", "--create"),
|
||||
help=help_msg)
|
||||
|
||||
|
||||
def get_network_group_arg(help_msg):
|
||||
return get_set_type_arg("network", help=help_msg)
|
||||
|
||||
|
||||
def get_release_arg(help_msg, required=False):
|
||||
return get_int_arg(
|
||||
"release",
|
||||
flags=("--rel",),
|
||||
required=required,
|
||||
help=help_msg)
|
||||
|
||||
|
||||
def get_render_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"render",
|
||||
metavar='INPUT',
|
||||
help=help_msg)
|
||||
|
||||
|
||||
def get_tred_arg(help_msg):
|
||||
return get_boolean_arg("tred", help=help_msg)
|
||||
|
||||
|
||||
def get_node_arg(help_msg):
|
||||
default_kwargs = {
|
||||
"action": NodeAction,
|
||||
"flags": ("--node-id",),
|
||||
"nargs": '+',
|
||||
"type": lambda v: v.split(","),
|
||||
"default": None,
|
||||
"help": help_msg
|
||||
}
|
||||
return get_arg("node", **default_kwargs)
|
||||
|
||||
|
||||
def get_single_node_arg(help_msg):
|
||||
return get_int_arg('node', flags=('--node-id',), help=help_msg)
|
||||
|
||||
|
||||
def get_task_arg(help_msg):
|
||||
return get_array_arg(
|
||||
'task',
|
||||
flags=("--task-id", "--tid"),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_config_id_arg(help_msg):
|
||||
return get_int_arg(
|
||||
'config-id',
|
||||
help=help_msg)
|
||||
|
||||
|
||||
def get_deleted_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
'deleted', help=help_msg)
|
||||
|
||||
|
||||
def get_plugin_install_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"install",
|
||||
metavar='PLUGIN_FILE',
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_remove_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"remove",
|
||||
metavar='PLUGIN_NAME==VERSION',
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_register_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"register",
|
||||
metavar='PLUGIN_NAME==VERSION',
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_unregister_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"unregister",
|
||||
metavar='PLUGIN_NAME==VERSION',
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_update_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"update",
|
||||
metavar='PLUGIN_FILE',
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_downgrade_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"downgrade",
|
||||
metavar='PLUGIN_FILE',
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_sync_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
"sync",
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_arg(help_msg):
|
||||
return get_array_arg(
|
||||
'plugin',
|
||||
flags=('--plugin-id',),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_notify_all_messages_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
'all',
|
||||
flags=('-a',),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_notify_mark_as_read_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"mark-as-read",
|
||||
flags=('-r',),
|
||||
nargs='+',
|
||||
help=help_msg,
|
||||
)
|
||||
|
||||
|
||||
def get_notify_message_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"send",
|
||||
nargs='+',
|
||||
flags=('-m',),
|
||||
help=help_msg,
|
||||
)
|
||||
|
||||
|
||||
def get_notify_send_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"send",
|
||||
flags=("--send",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_notify_topic_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"topic",
|
||||
flags=("--topic",),
|
||||
choices=(
|
||||
'discover',
|
||||
'done',
|
||||
'error',
|
||||
'warning',
|
||||
'release'
|
||||
),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_vip_arg(help_msg):
|
||||
return get_boolean_arg(
|
||||
"vip",
|
||||
flags=("--vip",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_vip_name_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"vip-name",
|
||||
flags=("--name",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_vip_namespace_arg(help_msg, required=False):
|
||||
return get_str_arg(
|
||||
"vip-namespace",
|
||||
flags=("--namespace",),
|
||||
required=required,
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_ip_address_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"ip-address",
|
||||
flags=("--address", "--ip-addr"),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_ip_id_arg(help_msg):
|
||||
return get_int_arg(
|
||||
"ip-address-id",
|
||||
flags=("--ip-address-id",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_network_id_arg(help_msg):
|
||||
return get_int_arg(
|
||||
"network",
|
||||
flags=("--network",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_network_role_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"network-role",
|
||||
flags=("--network-role",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_upload_file_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"upload",
|
||||
flags=("-u", "--upload",),
|
||||
help=help_msg
|
||||
)
|
||||
|
||||
|
||||
def get_status_arg(help_msg):
|
||||
default_kwargs = {
|
||||
"flags": ("--status",),
|
||||
"default": None,
|
||||
"help": help_msg
|
||||
}
|
||||
return get_arg("status", **default_kwargs)
|
||||
|
||||
|
||||
def get_deployment_node_arg(help_msg):
|
||||
default_kwargs = {
|
||||
"flags": ("--node-id",),
|
||||
"default": None,
|
||||
"help": help_msg
|
||||
}
|
||||
return get_arg("node", **default_kwargs)
|
||||
|
||||
|
||||
def get_tasks_names_arg(help_msg):
|
||||
default_kwargs = {
|
||||
"flags": ("-d", "--task-name",),
|
||||
"default": None,
|
||||
"help": help_msg
|
||||
}
|
||||
return get_arg("task-name", **default_kwargs)
|
||||
|
||||
|
||||
def get_show_parameters_arg(help_msg):
|
||||
default_kwargs = {
|
||||
"flags": ("-p", "--show-parameters",),
|
||||
"help": help_msg
|
||||
}
|
||||
return get_boolean_arg("show-parameters", **default_kwargs)
|
||||
|
||||
|
||||
def get_include_summary_arg(help_msg):
|
||||
default_kwargs = {
|
||||
"flags": ("--include-summary",),
|
||||
"help": help_msg
|
||||
}
|
||||
return get_boolean_arg("include-summary", **default_kwargs)
|
||||
|
||||
|
||||
def get_not_split_facts_args():
|
||||
kwargs = {
|
||||
"action": "store_false",
|
||||
"default": True,
|
||||
"dest": "split",
|
||||
"help": "Do not split deployment info for node and cluster parts."
|
||||
}
|
||||
return get_arg('no-split', **kwargs)
|
@ -1,155 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from functools import wraps
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from keystoneclient.exceptions import Unauthorized
|
||||
import requests
|
||||
import textwrap
|
||||
|
||||
|
||||
def exit_with_error(message):
|
||||
"""exit_with_error - writes message to stderr and exits with exit code 1.
|
||||
"""
|
||||
sys.stderr.write("{}{}".format(message, os.linesep))
|
||||
exit(1)
|
||||
|
||||
|
||||
class FuelClientException(Exception):
|
||||
"""Base Exception for Fuel-Client
|
||||
|
||||
All child classes must be instantiated before raising.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FuelClientException, self).__init__(*args, **kwargs)
|
||||
self.message = args[0]
|
||||
|
||||
|
||||
class BadDataException(FuelClientException):
|
||||
"""Should be raised when user provides corrupted data."""
|
||||
|
||||
|
||||
class WrongEnvironmentError(FuelClientException):
|
||||
"""Raised when particular action is not supported on environment."""
|
||||
|
||||
|
||||
class ServerDataException(FuelClientException):
|
||||
"""ServerDataException - must be raised when
|
||||
data returned from server cannot be processed by Fuel-Client methods.
|
||||
"""
|
||||
|
||||
|
||||
class DeployProgressError(FuelClientException):
|
||||
"""DeployProgressError - must be raised when
|
||||
deployment process interrupted on server.
|
||||
"""
|
||||
|
||||
|
||||
class ArgumentException(FuelClientException):
|
||||
"""ArgumentException - must be raised when
|
||||
incorrect arguments inputted through argparse or some function.
|
||||
"""
|
||||
|
||||
|
||||
class ActionException(FuelClientException):
|
||||
"""ActionException - must be raised when
|
||||
though arguments inputted to action are correct but they contradict
|
||||
to logic in action.
|
||||
"""
|
||||
|
||||
|
||||
class ParserException(FuelClientException):
|
||||
"""ParserException - must be raised when
|
||||
some problem occurred in process of argument parsing,
|
||||
in argparse extension or in Fuel-Client Parser submodule.
|
||||
"""
|
||||
|
||||
|
||||
class ProfilingError(FuelClientException):
|
||||
"""Indicates errors and other issues related to performance profiling."""
|
||||
|
||||
|
||||
class SettingsException(FuelClientException):
|
||||
"""Indicates errors or unexpected behaviour in processing settings."""
|
||||
|
||||
|
||||
class ExecutedErrorNonZeroExitCode(FuelClientException):
|
||||
"""Subshell command returned non-zero exit code."""
|
||||
|
||||
|
||||
class LabelEmptyKeyError(BadDataException):
|
||||
"""Should be raised when user provides labels with empty key."""
|
||||
|
||||
|
||||
class InvalidDirectoryException(FuelClientException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFileException(FuelClientException):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPError(FuelClientException):
|
||||
pass
|
||||
|
||||
|
||||
class EnvironmentException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def exceptions_decorator(func):
|
||||
"""Handles HTTP errors and expected exceptions that may occur
|
||||
in methods of DefaultAPIClient class
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# when server returns to us bad request check that
|
||||
# and print meaningful reason
|
||||
except HTTPError as exc:
|
||||
exit_with_error(exc)
|
||||
except requests.ConnectionError:
|
||||
message = """
|
||||
Can't connect to Nailgun server!
|
||||
Please check connection settings in your configuration file."""
|
||||
exit_with_error(textwrap.dedent(message).strip())
|
||||
except Unauthorized:
|
||||
message = """
|
||||
Unauthorized: need authentication!
|
||||
Please provide user and password via client
|
||||
fuel --os-username=user --os-password=pass [action]
|
||||
or modify your credentials in your configuration file."""
|
||||
exit_with_error(textwrap.dedent(message).strip())
|
||||
except FuelClientException as exc:
|
||||
exit_with_error(exc.message)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def get_error_body(error):
|
||||
try:
|
||||
error_body = json.loads(error.response.text)['message']
|
||||
except (ValueError, TypeError, KeyError):
|
||||
error_body = error.response.text
|
||||
|
||||
return error_body
|
||||
|
||||
|
||||
def get_full_error_message(error):
|
||||
return "{} ({})".format(error, get_error_body(error))
|
@ -1,161 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from itertools import chain
|
||||
from operator import itemgetter
|
||||
from time import sleep
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def format_table(data, acceptable_keys=None, column_to_join=None):
|
||||
"""Format list of dicts to table in a string form
|
||||
|
||||
:acceptable_keys list(str): list of keys for which to create table
|
||||
also specifies their order
|
||||
"""
|
||||
|
||||
# prepare columns
|
||||
if column_to_join is not None:
|
||||
for data_dict in data:
|
||||
for column_name in column_to_join:
|
||||
data_dict[column_name] = u", ".join(
|
||||
sorted(data_dict[column_name])
|
||||
)
|
||||
if acceptable_keys is not None:
|
||||
rows = [tuple(value.get(key, "") for key in acceptable_keys)
|
||||
for value in data]
|
||||
header = tuple(acceptable_keys)
|
||||
else:
|
||||
rows = [tuple(x.values()) for x in data]
|
||||
header = tuple(data[0].keys())
|
||||
number_of_columns = len(header)
|
||||
|
||||
# split multi-lines cells if there is no automatic columns merge
|
||||
if column_to_join:
|
||||
def format_cell(cell):
|
||||
return [cell or ""]
|
||||
else:
|
||||
def format_cell(cell):
|
||||
return six.text_type(cell).split('\n')
|
||||
rows = [
|
||||
[format_cell(cell) if cell is not None else [''] for cell in row]
|
||||
for row in rows
|
||||
]
|
||||
|
||||
# calculate columns widths
|
||||
column_widths = dict(
|
||||
zip(
|
||||
range(number_of_columns),
|
||||
(len(str(x)) for x in header)
|
||||
)
|
||||
)
|
||||
for row in rows:
|
||||
column_widths.update(
|
||||
(
|
||||
index,
|
||||
max(
|
||||
column_widths[index],
|
||||
max(len(six.text_type(line)) for line in cell)
|
||||
)
|
||||
)
|
||||
for index, cell in enumerate(row)
|
||||
)
|
||||
|
||||
# make output
|
||||
hor_delimeter = u'-+-'.join(column_widths[column_index] * u'-'
|
||||
for column_index in range(number_of_columns))
|
||||
|
||||
row_template = u' | '.join(
|
||||
u"{{{0}:{1}}}".format(idx, width)
|
||||
for idx, width in column_widths.items()
|
||||
)
|
||||
|
||||
output_lines = [
|
||||
row_template.format(*header),
|
||||
hor_delimeter
|
||||
]
|
||||
|
||||
for row in rows:
|
||||
max_cell_lines = max(len(cell) for cell in row)
|
||||
for cell_line_no in range(max_cell_lines):
|
||||
output_lines.append(
|
||||
row_template.format(
|
||||
*(
|
||||
cell[cell_line_no] if len(cell) > cell_line_no else u""
|
||||
for cell in row
|
||||
)
|
||||
)
|
||||
)
|
||||
return u'\n'.join(output_lines)
|
||||
|
||||
|
||||
def quote_and_join(words):
|
||||
"""quote_and_join - performs listing of objects and returns string.
|
||||
"""
|
||||
words = list(words)
|
||||
if len(words) > 1:
|
||||
return '{0} and "{1}"'.format(
|
||||
", ".join(
|
||||
['"{0}"'.format(x) for x in words][:-1]
|
||||
),
|
||||
words[-1]
|
||||
)
|
||||
else:
|
||||
return '"{0}"'.format(words[0])
|
||||
|
||||
|
||||
# TODO(vkulanov): remove when deprecate old cli
|
||||
def print_health_check(env):
|
||||
tests_states = [{"status": "not finished"}]
|
||||
finished_tests = set()
|
||||
test_counter, total_tests_count = 1, None
|
||||
while not all(map(
|
||||
lambda t: t["status"] == "finished",
|
||||
tests_states
|
||||
)):
|
||||
tests_states = env.get_state_of_tests()
|
||||
all_tests = list(chain(*map(
|
||||
itemgetter("tests"),
|
||||
filter(
|
||||
env.is_in_running_test_sets,
|
||||
tests_states
|
||||
))))
|
||||
if total_tests_count is None:
|
||||
total_tests_count = len(all_tests)
|
||||
all_finished_tests = filter(
|
||||
lambda t: "running" not in t["status"],
|
||||
all_tests
|
||||
)
|
||||
new_finished_tests = filter(
|
||||
lambda t: t["name"] not in finished_tests,
|
||||
all_finished_tests
|
||||
)
|
||||
finished_tests.update(
|
||||
map(
|
||||
itemgetter("name"),
|
||||
new_finished_tests
|
||||
)
|
||||
)
|
||||
for test in new_finished_tests:
|
||||
print(
|
||||
u"[{0:2} of {1}] [{status}] '{name}' "
|
||||
u"({taken:.4} s) {message}".format(
|
||||
test_counter,
|
||||
total_tests_count,
|
||||
**test
|
||||
)
|
||||
)
|
||||
test_counter += 1
|
||||
sleep(1)
|
@ -1,246 +0,0 @@
|
||||
# Copyright 2014 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 argparse
|
||||
import sys
|
||||
|
||||
from fuelclient.cli.actions import actions
|
||||
from fuelclient.cli.arguments import get_version_arg
|
||||
from fuelclient.cli.arguments import substitutions
|
||||
from fuelclient.cli.error import exceptions_decorator
|
||||
from fuelclient.cli.error import ParserException
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient import consts
|
||||
from fuelclient import fuelclient_settings
|
||||
from fuelclient import profiler
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class Parser(object):
|
||||
"""Parser class - encapsulates argparse's ArgumentParser
|
||||
and based on available actions, serializers and additional flags
|
||||
populates it.
|
||||
"""
|
||||
def __init__(self, argv):
|
||||
self.args = argv
|
||||
self.parser = argparse.ArgumentParser(
|
||||
usage="""
|
||||
fuel [optional args] <namespace> [action] [flags]
|
||||
|
||||
DEPRECATION WARNING:
|
||||
|
||||
In an upcoming release of Fuel Client, the syntax will
|
||||
be changed to the following:
|
||||
|
||||
fuel [general flags] <entity> <action> [action flags]
|
||||
|
||||
where both [general flags] and [action flags] are derivatives
|
||||
from [optional args] and [flags]; <entity> is a derivative from
|
||||
<namespace>. Keep in mind that specifying <action> will be
|
||||
mandatory.
|
||||
|
||||
Some of the [optional args] are going to specific to a
|
||||
particular <entity> and <action> context in the upcoming
|
||||
release of Fuel Client, so specifying them
|
||||
before either <namespace> or <action> will not be possible.
|
||||
|
||||
Example:
|
||||
Correct: fuel node list --env 1
|
||||
Wrong: fuel --env 1 node list
|
||||
|
||||
The table below describes the upcoming changes to commands
|
||||
which will be removed or changed significantly.
|
||||
|
||||
+--------------------------------------------------------+
|
||||
| Old command | New command |
|
||||
+------------------------+-------------------------------+
|
||||
| fuel deploy-changes | fuel env deploy |
|
||||
+------------------------+-------------------------------+
|
||||
| fuel node --set --env | fuel env add nodes |
|
||||
+------------------------+-------------------------------+
|
||||
| fuel network <> --env | fuel env network <> |
|
||||
+------------------------+-------------------------------+
|
||||
| fuel settings <> --env | fuel env settings <> |
|
||||
+------------------------+-------------------------------+
|
||||
| fuel stop | fuel env stop-deploy |
|
||||
+------------------------+-------------------------------+
|
||||
|
||||
Further information will be located in Fuel Documentation and
|
||||
on our Wiki page: https://wiki.openstack.org/wiki/Fuel_CLI
|
||||
|
||||
You can check out an experimental version of the new
|
||||
Fuel Client by using the following command:
|
||||
|
||||
fuel2 --help
|
||||
|
||||
"""
|
||||
)
|
||||
self.universal_flags = []
|
||||
self.credential_flags = []
|
||||
self.subparsers = self.parser.add_subparsers(
|
||||
title="Namespaces",
|
||||
metavar="",
|
||||
dest="action",
|
||||
help='actions'
|
||||
)
|
||||
self.generate_actions()
|
||||
self.add_version_args()
|
||||
self.add_debug_arg()
|
||||
self.add_serializers_args()
|
||||
utils.add_os_cli_parameters(self.parser)
|
||||
|
||||
def generate_actions(self):
|
||||
for action, action_object in actions.items():
|
||||
action_parser = self.subparsers.add_parser(
|
||||
action,
|
||||
prog="fuel {0}".format(action),
|
||||
help=action_object.__doc__,
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
epilog=action_object.examples
|
||||
)
|
||||
for argument in action_object.args:
|
||||
if isinstance(argument, dict):
|
||||
action_parser.add_argument(
|
||||
*argument["args"],
|
||||
**argument["params"]
|
||||
)
|
||||
elif isinstance(argument, tuple):
|
||||
required = argument[0]
|
||||
group = action_parser.add_mutually_exclusive_group(
|
||||
required=required)
|
||||
for argument_in_group in argument[1:]:
|
||||
group.add_argument(
|
||||
*argument_in_group["args"],
|
||||
**argument_in_group["params"]
|
||||
)
|
||||
|
||||
def parse(self):
|
||||
self.prepare_args()
|
||||
if len(self.args) < 2:
|
||||
self.parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
parsed_params = self.parser.parse_args(self.args[1:])
|
||||
|
||||
settings = fuelclient_settings.get_settings()
|
||||
settings.update_from_command_line_options(parsed_params)
|
||||
|
||||
if parsed_params.action not in actions:
|
||||
self.parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
if profiler.profiling_enabled():
|
||||
handler_name = parsed_params.action
|
||||
method_name = ''.join([method for method in parsed_params.__dict__
|
||||
if getattr(parsed_params, method) is True])
|
||||
prof = profiler.Profiler(method_name, handler_name)
|
||||
|
||||
actions[parsed_params.action].action_func(parsed_params)
|
||||
|
||||
if profiler.profiling_enabled():
|
||||
prof.save_data()
|
||||
|
||||
def add_serializers_args(self):
|
||||
serializers = self.parser.add_mutually_exclusive_group()
|
||||
for format_name in Serializer.serializers.keys():
|
||||
serialization_flag = "--{0}".format(format_name)
|
||||
self.universal_flags.append(serialization_flag)
|
||||
serializers.add_argument(
|
||||
serialization_flag,
|
||||
dest=consts.SERIALIZATION_FORMAT_FLAG,
|
||||
action="store_const",
|
||||
const=format_name,
|
||||
help="prints only {0} to stdout".format(format_name),
|
||||
default=False
|
||||
)
|
||||
|
||||
def add_debug_arg(self):
|
||||
self.universal_flags.append("--debug")
|
||||
self.parser.add_argument(
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help="prints details of all HTTP request",
|
||||
default=False
|
||||
)
|
||||
|
||||
def add_version_args(self):
|
||||
arg = get_version_arg()
|
||||
self.parser.add_argument(*arg["args"], **arg["params"])
|
||||
|
||||
def prepare_args(self):
|
||||
# replace some args from dict substitutions
|
||||
self.args = [substitutions.get(arg, arg) for arg in self.args]
|
||||
|
||||
# move general used flags before actions, otherwise they will be used
|
||||
# as a part of action by action_generator
|
||||
for flag in self.credential_flags:
|
||||
self.move_argument_before_action(flag)
|
||||
|
||||
for flag in self.universal_flags:
|
||||
self.move_argument_before_action(flag, has_value=False)
|
||||
|
||||
self.move_argument_after_action("--env",)
|
||||
|
||||
def move_argument_before_action(self, flag, has_value=True):
|
||||
"""We need to move general argument before action, we use them
|
||||
not directly in action but in DefaultAPIClient.
|
||||
"""
|
||||
for arg in self.args:
|
||||
if flag in arg:
|
||||
if "=" in arg or not has_value:
|
||||
index_of_flag = self.args.index(arg)
|
||||
flag = self.args.pop(index_of_flag)
|
||||
self.args.insert(1, flag)
|
||||
else:
|
||||
try:
|
||||
index_of_flag = self.args.index(arg)
|
||||
flag = self.args.pop(index_of_flag)
|
||||
value = self.args.pop(index_of_flag)
|
||||
self.args.insert(1, value)
|
||||
self.args.insert(1, flag)
|
||||
except IndexError:
|
||||
raise ParserException(
|
||||
'Corresponding value must follow "{0}" flag'
|
||||
.format(arg)
|
||||
)
|
||||
break
|
||||
|
||||
def move_argument_after_action(self, flag):
|
||||
for arg in self.args:
|
||||
if flag in arg:
|
||||
# if declaration with '=' sign (e.g. --env-id=1)
|
||||
if "=" in arg:
|
||||
index_of_flag = self.args.index(arg)
|
||||
flag = self.args.pop(index_of_flag)
|
||||
self.args.append(flag)
|
||||
else:
|
||||
try:
|
||||
index_of_flag = self.args.index(arg)
|
||||
self.args.pop(index_of_flag)
|
||||
flag = self.args.pop(index_of_flag)
|
||||
self.args.append(arg)
|
||||
self.args.append(flag)
|
||||
except IndexError:
|
||||
raise ParserException(
|
||||
'Corresponding value must follow "{0}" flag'
|
||||
.format(arg)
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
@exceptions_decorator
|
||||
def main(args=sys.argv):
|
||||
parser = Parser(args)
|
||||
parser.parse()
|
@ -1,152 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient import consts
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class Serializer(object):
|
||||
"""Serializer class - contains all logic responsible for
|
||||
printing to stdout, reading and writing files to file system.
|
||||
"""
|
||||
serializers = {
|
||||
"json": {
|
||||
"w": lambda d: json.dumps(d, indent=4),
|
||||
"r": utils.safe_deserialize(json.loads)
|
||||
},
|
||||
"yaml": {
|
||||
"w": lambda d: yaml.safe_dump(d, default_flow_style=False),
|
||||
"r": utils.safe_deserialize(yaml.load)
|
||||
}
|
||||
}
|
||||
|
||||
format_flags = False
|
||||
default_format = "yaml"
|
||||
format = default_format
|
||||
|
||||
def __init__(self, format=None):
|
||||
if format and format in self.serializers:
|
||||
self.format = format
|
||||
self.format_flags = True
|
||||
|
||||
@property
|
||||
def serializer(self):
|
||||
"""Returns dicts with methods for loadin/dumping current fromat.
|
||||
|
||||
Returned dict's keys:
|
||||
* 'w' - from 'writing', method for serializing/dumping data
|
||||
* 'r' - from 'reading', method for deserializing/loading data
|
||||
"""
|
||||
return self.serializers[self.format]
|
||||
|
||||
def serialize(self, data):
|
||||
"""Shortcut for serializing data with current format."""
|
||||
return self.serializer['w'](data)
|
||||
|
||||
def deserialize(self, data):
|
||||
"""Shortcut for deserializing data with current format."""
|
||||
return self.serializer['r'](data)
|
||||
|
||||
@classmethod
|
||||
def from_params(cls, params):
|
||||
return cls(format=getattr(params,
|
||||
consts.SERIALIZATION_FORMAT_FLAG, None))
|
||||
|
||||
def print_formatted(self, data):
|
||||
print(self.serializer["w"](data))
|
||||
|
||||
def print_to_output(self, formatted_data, arg, print_method=print):
|
||||
if self.format_flags:
|
||||
self.print_formatted(formatted_data)
|
||||
else:
|
||||
if six.PY2 and isinstance(arg, six.text_type):
|
||||
arg = arg.encode('utf-8')
|
||||
print_method(arg)
|
||||
|
||||
def prepare_path(self, path):
|
||||
return "{0}.{1}".format(
|
||||
path, self.format
|
||||
)
|
||||
|
||||
def write_to_path(self, path, data):
|
||||
full_path = self.prepare_path(path)
|
||||
return self.write_to_full_path(full_path, data)
|
||||
|
||||
def write_to_full_path(self, path, data):
|
||||
try:
|
||||
with open(path, "w") as file_to_write:
|
||||
self.write_to_file(file_to_write, data)
|
||||
except IOError as e:
|
||||
raise error.InvalidFileException(
|
||||
"Can't write to file '{0}': {1}.".format(
|
||||
path, e.strerror))
|
||||
return path
|
||||
|
||||
def read_from_file(self, path):
|
||||
return self.read_from_full_path(self.prepare_path(path))
|
||||
|
||||
def read_from_full_path(self, full_path):
|
||||
try:
|
||||
with open(full_path, "r") as file_to_read:
|
||||
return self.serializer["r"](file_to_read.read())
|
||||
except IOError as e:
|
||||
raise error.InvalidFileException(
|
||||
"Can't open file '{0}': {1}.".format(full_path, e.strerror))
|
||||
|
||||
def write_to_file(self, file_obj, data):
|
||||
"""Writes to opened file or file like object
|
||||
:param file_obj: opened file
|
||||
:param data: any serializable object
|
||||
"""
|
||||
serialized = self.serializer["w"](data)
|
||||
file_obj.write(serialized)
|
||||
|
||||
|
||||
class FileFormatBasedSerializer(Serializer):
|
||||
|
||||
def get_serializer(self, path):
|
||||
extension = os.path.splitext(path)[1][1:]
|
||||
if extension not in self.serializers:
|
||||
raise error.BadDataException(
|
||||
'No serializer for provided file {0}'.format(path))
|
||||
return self.serializers[extension]
|
||||
|
||||
def write_to_file(self, full_path, data):
|
||||
serializer = self.get_serializer(full_path)
|
||||
with open(full_path, "w+") as f:
|
||||
f.write(serializer["w"](data))
|
||||
return full_path
|
||||
|
||||
def read_from_file(self, full_path):
|
||||
serializer = self.get_serializer(full_path)
|
||||
with open(full_path, "r") as f:
|
||||
return serializer["r"](f.read())
|
||||
|
||||
|
||||
def listdir_without_extensions(dir_path):
|
||||
return six.moves.filter(
|
||||
lambda f: f != "",
|
||||
six.moves.map(
|
||||
lambda f: f.split(".")[0],
|
||||
os.listdir(dir_path)
|
||||
)
|
||||
)
|
@ -1,248 +0,0 @@
|
||||
# Copyright 2014 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 json
|
||||
import requests
|
||||
|
||||
from keystoneclient.v2_0 import client as auth_client
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient import fuelclient_settings
|
||||
|
||||
|
||||
class APIClient(object):
|
||||
"""This class handles API requests
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, http_proxy=None, http_timeout=None,
|
||||
os_username=None, os_password=None,
|
||||
os_tenant_name=None, debug=False):
|
||||
self.debug = debug
|
||||
|
||||
self._http_proxy = http_proxy
|
||||
self._http_timeout = http_timeout
|
||||
self._os_username = os_username
|
||||
self._os_password = os_password
|
||||
self._os_tenant_name = os_tenant_name
|
||||
|
||||
self.root = "http://{host}:{port}".format(host=host, port=port)
|
||||
|
||||
self.keystone_base = urlparse.urljoin(self.root, "/keystone/v2.0")
|
||||
self.api_root = urlparse.urljoin(self.root, "/api/v1/")
|
||||
self.ostf_root = urlparse.urljoin(self.root, "/ostf/")
|
||||
|
||||
self._keystone_client = None
|
||||
self._auth_required = None
|
||||
self._session = None
|
||||
|
||||
@classmethod
|
||||
def default_client(cls):
|
||||
conf = fuelclient_settings.get_settings()
|
||||
return cls(
|
||||
host=conf.SERVER_ADDRESS,
|
||||
port=conf.SERVER_PORT,
|
||||
http_proxy=conf.HTTP_PROXY,
|
||||
http_timeout=conf.HTTP_TIMEOUT,
|
||||
os_username=conf.OS_USERNAME,
|
||||
os_password=conf.OS_PASSWORD,
|
||||
os_tenant_name=conf.OS_TENANT_NAME
|
||||
)
|
||||
|
||||
def _make_common_headers(self):
|
||||
"""Returns a dict of HTTP headers common for all requests."""
|
||||
|
||||
return {'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Auth-Token': self.auth_token}
|
||||
|
||||
def _make_proxies(self):
|
||||
"""Provides HTTP proxy configuration for requests module."""
|
||||
if self._http_proxy is None:
|
||||
return None
|
||||
|
||||
return {'http': self._http_proxy,
|
||||
'https': self._http_proxy}
|
||||
|
||||
def _make_session(self):
|
||||
"""Initializes a HTTP session."""
|
||||
session = requests.Session()
|
||||
session.headers.update(self._make_common_headers())
|
||||
session.timeout = self._http_timeout
|
||||
session.proxies = self._make_proxies()
|
||||
|
||||
return session
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""Lazy initialization of a session
|
||||
|
||||
Since HTTP client is a singleton test runners cannot
|
||||
collect tests due to keystone authentication issues.
|
||||
|
||||
TODO(romcheg): remove lazy initialization for session
|
||||
when HTTP client is not a singleton.
|
||||
|
||||
"""
|
||||
if self._session is None:
|
||||
self._session = self._make_session()
|
||||
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def auth_token(self):
|
||||
if self.auth_required:
|
||||
if not self.keystone_client.auth_token:
|
||||
self.keystone_client.authenticate()
|
||||
return self.keystone_client.auth_token
|
||||
return ''
|
||||
|
||||
@property
|
||||
def auth_required(self):
|
||||
if self._auth_required is None:
|
||||
url = self.api_root + 'version'
|
||||
resp = requests.get(url)
|
||||
if resp.status_code == 401:
|
||||
self._auth_required = True
|
||||
else:
|
||||
self._raise_for_status_with_info(resp)
|
||||
self._auth_required = resp.json().get('auth_required', False)
|
||||
|
||||
return self._auth_required
|
||||
|
||||
@property
|
||||
def keystone_client(self):
|
||||
if not self._keystone_client:
|
||||
self.initialize_keystone_client()
|
||||
return self._keystone_client
|
||||
|
||||
def update_own_password(self, new_pass):
|
||||
if self.auth_token:
|
||||
self.keystone_client.users.update_own_password(
|
||||
self._os_password, new_pass)
|
||||
|
||||
def initialize_keystone_client(self):
|
||||
if self.auth_required:
|
||||
self._keystone_client = auth_client.Client(
|
||||
auth_url=self.keystone_base,
|
||||
username=self._os_username,
|
||||
password=self._os_password,
|
||||
tenant_name=self._os_tenant_name)
|
||||
|
||||
self._keystone_client.session.auth = self._keystone_client
|
||||
self._keystone_client.authenticate()
|
||||
|
||||
def debug_mode(self, debug=False):
|
||||
self.debug = debug
|
||||
return self
|
||||
|
||||
def print_debug(self, message):
|
||||
if self.debug:
|
||||
print(message)
|
||||
|
||||
def delete_request(self, api):
|
||||
"""Make DELETE request to specific API with some data."""
|
||||
|
||||
url = self.api_root + api
|
||||
self.print_debug('DELETE {0}'.format(url))
|
||||
|
||||
resp = self.session.delete(url)
|
||||
self._raise_for_status_with_info(resp)
|
||||
|
||||
return self._decode_content(resp)
|
||||
|
||||
def put_request(self, api, data, ostf=False, **params):
|
||||
"""Make PUT request to specific API with some data.
|
||||
|
||||
:param api: API endpoint (path)
|
||||
:param data: Data send in request, will be serialized to JSON
|
||||
:param ostf: is this a call to OSTF API
|
||||
:param params: Params of query string
|
||||
"""
|
||||
url = (self.ostf_root if ostf else self.api_root) + api
|
||||
data_json = json.dumps(data)
|
||||
resp = self.session.put(url, data=data_json, params=params)
|
||||
|
||||
self.print_debug('PUT {0} data={1}'.format(resp.url, data_json))
|
||||
self._raise_for_status_with_info(resp)
|
||||
return self._decode_content(resp)
|
||||
|
||||
def get_request_raw(self, api, ostf=False, params=None):
|
||||
"""Make a GET request to specific API and return raw response
|
||||
|
||||
:param api: API endpoint (path)
|
||||
:param ostf: is this a call to OSTF API
|
||||
:param params: params passed to GET request
|
||||
|
||||
"""
|
||||
url = (self.ostf_root if ostf else self.api_root) + api
|
||||
self.print_debug('GET {0}'.format(url))
|
||||
|
||||
return self.session.get(url, params=params)
|
||||
|
||||
def get_request(self, api, ostf=False, params=None):
|
||||
"""Make GET request to specific API."""
|
||||
|
||||
params = params or {}
|
||||
|
||||
resp = self.get_request_raw(api, ostf, params)
|
||||
self._raise_for_status_with_info(resp)
|
||||
|
||||
return resp.json()
|
||||
|
||||
def post_request_raw(self, api, data=None, ostf=False):
|
||||
"""Make a POST request to specific API and return raw response.
|
||||
|
||||
:param api: API endpoint (path)
|
||||
:param data: data send in request, will be serialzied to JSON
|
||||
:param ostf: is this a call to OSTF API
|
||||
|
||||
"""
|
||||
url = (self.ostf_root if ostf else self.api_root) + api
|
||||
data_json = None if data is None else json.dumps(data)
|
||||
|
||||
self.print_debug('POST {0} data={1}'.format(url, data_json))
|
||||
|
||||
return self.session.post(url, data=data_json)
|
||||
|
||||
def post_request(self, api, data=None, ostf=False):
|
||||
"""Make POST request to specific API with some data
|
||||
"""
|
||||
resp = self.post_request_raw(api, data, ostf=ostf)
|
||||
self._raise_for_status_with_info(resp)
|
||||
return self._decode_content(resp)
|
||||
|
||||
def get_fuel_version(self):
|
||||
return self.get_request("version")
|
||||
|
||||
def _raise_for_status_with_info(self, response):
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise error.HTTPError(error.get_full_error_message(e))
|
||||
|
||||
def _decode_content(self, response):
|
||||
if response.status_code == 204:
|
||||
return {}
|
||||
|
||||
self.print_debug(response.text)
|
||||
return response.json()
|
||||
|
||||
|
||||
# This line is single point of instantiation for 'APIClient' class,
|
||||
# which intended to implement Singleton design pattern.
|
||||
DefaultAPIClient = APIClient.default_client()
|
||||
"""
|
||||
.. deprecated:: Use fuelclient.client.APIClient instead
|
||||
"""
|
@ -1,253 +0,0 @@
|
||||
# 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 os
|
||||
|
||||
from cliff import command
|
||||
from cliff import lister
|
||||
from cliff import show
|
||||
import six
|
||||
|
||||
import fuelclient
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
VERSION = 'v1'
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseCommand(command.Command):
|
||||
"""Base Fuel Client command."""
|
||||
|
||||
def get_attributes_path(self, attr_type, file_format, ent_id, directory):
|
||||
"""Returnes a path for attributes of an entity
|
||||
|
||||
:param attr_type: Type of the attribute, e. g., disks, networks.
|
||||
:param file_format: The format of the file that contains or will
|
||||
contain the attributes, e. g., json or yaml.
|
||||
:param ent_id: Id of an entity
|
||||
:param directory: Directory that is used to store attributes.
|
||||
|
||||
"""
|
||||
if attr_type not in self.allowed_attr_types:
|
||||
raise ValueError('attr_type must be '
|
||||
'one of {}'.format(self.allowed_attr_types))
|
||||
|
||||
if file_format not in self.supported_file_formats:
|
||||
raise ValueError('file_format must be '
|
||||
'one of {}'.format(self.supported_file_formats))
|
||||
|
||||
return os.path.join(os.path.abspath(directory),
|
||||
'{ent}_{id}'.format(ent=self.entity_name,
|
||||
id=ent_id),
|
||||
'{}.{}'.format(attr_type, file_format))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BaseCommand, self).__init__(*args, **kwargs)
|
||||
self.client = fuelclient.get_client(self.entity_name, VERSION)
|
||||
|
||||
@abc.abstractproperty
|
||||
def entity_name(self):
|
||||
"""Name of the Fuel entity."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def supported_file_formats(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def allowed_attr_types(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseListCommand(lister.Lister, BaseCommand):
|
||||
"""Lists all entities showing some information."""
|
||||
|
||||
filters = {}
|
||||
|
||||
@property
|
||||
def default_sorting_by(self):
|
||||
return ['id']
|
||||
|
||||
@abc.abstractproperty
|
||||
def columns(self):
|
||||
"""Names of columns in the resulting table."""
|
||||
pass
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseListCommand, self).get_parser(prog_name)
|
||||
|
||||
# Add sorting key argument to the output formatters group
|
||||
# if it exists. If not -- add is to the general group.
|
||||
matching_groups = (gr
|
||||
for gr in parser._action_groups
|
||||
if gr.title == 'output formatters')
|
||||
|
||||
group = next(matching_groups, None) or parser
|
||||
|
||||
group.add_argument('-s',
|
||||
'--sort-columns',
|
||||
type=str,
|
||||
nargs='+',
|
||||
choices=self.columns,
|
||||
metavar='SORT_COLUMN',
|
||||
default=self.default_sorting_by,
|
||||
help='Space separated list of keys for sorting '
|
||||
'the data. Defaults to {}. Wrong values '
|
||||
'are ignored.'.format(
|
||||
', '.join(self.default_sorting_by)))
|
||||
|
||||
return parser
|
||||
|
||||
def _sort_data(self, parsed_args, data):
|
||||
scolumn_ids = [self.columns.index(col)
|
||||
for col in parsed_args.sort_columns]
|
||||
data.sort(key=lambda x: [x[scolumn_id] for scolumn_id in scolumn_ids])
|
||||
return data
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
filters = {}
|
||||
for name, prop in self.filters.items():
|
||||
value = getattr(parsed_args, prop, None)
|
||||
if value is not None:
|
||||
filters[name] = value
|
||||
|
||||
data = self.client.get_all(**filters)
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
data = self._sort_data(parsed_args, data)
|
||||
|
||||
return self.columns, data
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseShowCommand(show.ShowOne, BaseCommand):
|
||||
"""Shows detailed information about the entity."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def columns(self):
|
||||
"""Names of columns in the resulting table."""
|
||||
pass
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseShowCommand, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument('id', type=int,
|
||||
help='Id of the {0}.'.format(self.entity_name))
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_by_id(parsed_args.id)
|
||||
data = data_utils.get_display_data_single(self.columns, data)
|
||||
|
||||
return (self.columns, data)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseDeleteCommand(BaseCommand):
|
||||
"""Deletes entity with the specified id."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseDeleteCommand, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'id',
|
||||
type=int,
|
||||
help='Id of the {0} to delete.'.format(self.entity_name))
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.delete_by_id(parsed_args.id)
|
||||
|
||||
msg = '{ent} with id {ent_id} was deleted\n'
|
||||
|
||||
self.app.stdout.write(
|
||||
msg.format(
|
||||
ent=self.entity_name.capitalize(),
|
||||
ent_id=parsed_args.id))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseTasksExecuteCommand(BaseCommand):
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseTasksExecuteCommand, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'-e', '--env',
|
||||
type=int,
|
||||
required=True,
|
||||
help='Id of the environment'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--force',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Force run all deployment tasks without skipping.')
|
||||
|
||||
parser.add_argument(
|
||||
'--trace',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Enable debugging mode in tasks executor.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
choices=['json', 'yaml'],
|
||||
help='Select output format, by default text message will produce.'
|
||||
)
|
||||
|
||||
mode_group = parser.add_mutually_exclusive_group()
|
||||
mode_group.add_argument(
|
||||
'--dry-run',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Specifies to dry-run a deployment by configuring '
|
||||
'task executor to dump the deployment graph to a dot file.'
|
||||
)
|
||||
mode_group.add_argument(
|
||||
'--noop',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Specifies noop-run deployment configuring tasks executor '
|
||||
'to run all tasks in noop mode. '
|
||||
'Execution result summary can be got via history of tasks.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
task = self.client.execute(
|
||||
env_id=parsed_args.env,
|
||||
dry_run=parsed_args.dry_run,
|
||||
noop_run=parsed_args.noop,
|
||||
force=parsed_args.force,
|
||||
debug=parsed_args.trace,
|
||||
**self.get_options(parsed_args)
|
||||
)
|
||||
if parsed_args.format:
|
||||
msg = Serializer(parsed_args.format).serialize(task.data) + '\n'
|
||||
else:
|
||||
msg = (
|
||||
'Deployment task with id {0} for the environment {1} '
|
||||
'has been started.\n'
|
||||
.format(task.data['id'], task.data['cluster'])
|
||||
)
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
def get_options(self, parsed_args):
|
||||
"""Produce additional options from cmdline arguments."""
|
||||
raise NotImplementedError
|
@ -1,933 +0,0 @@
|
||||
# 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 EnvReset(EnvMixIn, base.BaseCommand):
|
||||
"""Reset deployed environment."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(EnvReset, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument('id',
|
||||
type=int,
|
||||
help='Id of the environment to reset.')
|
||||
parser.add_argument('-f',
|
||||
'--force',
|
||||
action='store_true',
|
||||
help='Force reset environment.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
result = self.client.reset(parsed_args.id, force=parsed_args.force)
|
||||
|
||||
msg = ('Reset task with id {t} for the environment {e} '
|
||||
'has been started.\n'.format(t=result.data['id'],
|
||||
e=result.data['cluster']))
|
||||
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class EnvStopDeploy(EnvMixIn, base.BaseCommand):
|
||||
"""Stop deployment process for specific environment."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(EnvStopDeploy, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument('id',
|
||||
type=int,
|
||||
help='Id of the environment to stop deployment.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
result = self.client.stop(parsed_args.id)
|
||||
|
||||
msg = ('Stop deployment task with id {t} for the environment '
|
||||
'{e} has been started.\n'.format(t=result.data['id'],
|
||||
e=result.data['cluster']))
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
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'
|
@ -1,98 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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.
|
||||
|
||||
from cliff import show
|
||||
|
||||
from fuelclient.commands import base
|
||||
|
||||
|
||||
class ExtensionMixIn(object):
|
||||
entity_name = 'extension'
|
||||
|
||||
|
||||
class ExtensionList(ExtensionMixIn, base.BaseListCommand):
|
||||
"""Show list of all available extensions."""
|
||||
|
||||
columns = ("name",
|
||||
"version",
|
||||
"description",
|
||||
"provides")
|
||||
default_sorting_by = ["name"]
|
||||
|
||||
|
||||
class EnvExtensionShow(ExtensionMixIn, base.BaseShowCommand):
|
||||
"""Show list of enabled extensions for environment with given id."""
|
||||
|
||||
columns = ("extensions", )
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
# Avoid adding id argument by BaseShowCommand
|
||||
# Because it adds 'id' with wrong help message for this class
|
||||
parser = show.ShowOne.get_parser(self, prog_name)
|
||||
|
||||
parser.add_argument('id', type=int, help='Id of the environment.')
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
class EnvExtensionEnable(ExtensionMixIn, base.BaseCommand):
|
||||
"""Enable specified extensions for environment with given id."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(EnvExtensionEnable, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument('id', type=int, help='Id of the environment.')
|
||||
parser.add_argument('-E',
|
||||
'--extensions',
|
||||
required=True,
|
||||
nargs='+',
|
||||
help='Names of extensions to enable.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.enable_extensions(parsed_args.id, parsed_args.extensions)
|
||||
|
||||
msg = ('The following extensions: {e} have been enabled for '
|
||||
'the environment with id {id}.\n'.format(
|
||||
e=', '.join(parsed_args.extensions), id=parsed_args.id))
|
||||
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class EnvExtensionDisable(ExtensionMixIn, base.BaseCommand):
|
||||
"""Disable specified extensions for environment with given id."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(EnvExtensionDisable, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument('id', type=int, help='Id of the environment.')
|
||||
parser.add_argument('-E',
|
||||
'--extensions',
|
||||
required=True,
|
||||
nargs='+',
|
||||
help='Names of extensions to disable.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.disable_extensions(parsed_args.id, parsed_args.extensions)
|
||||
|
||||
msg = ('The following extensions: {e} have been disabled for '
|
||||
'the environment with id {id}.\n'.format(
|
||||
e=', '.join(parsed_args.extensions), id=parsed_args.id))
|
||||
|
||||
self.app.stdout.write(msg)
|
@ -1,35 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from cliff import show
|
||||
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
class FuelVersion(show.ShowOne, base.BaseCommand):
|
||||
"""Show the version of Fuel."""
|
||||
|
||||
entity_name = 'fuel-version'
|
||||
columns = ('api',
|
||||
'auth_required',
|
||||
'feature_groups',
|
||||
'openstack_version',
|
||||
'release')
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_all()
|
||||
data = data_utils.get_display_data_single(self.columns, data)
|
||||
|
||||
return (self.columns, data)
|
@ -1,414 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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 os
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
from fuelclient.utils import iterfiles
|
||||
|
||||
|
||||
class FileMethodsMixin(object):
|
||||
@classmethod
|
||||
def check_file_path(cls, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
raise error.InvalidFileException(
|
||||
"File '{0}' doesn't exist.".format(file_path))
|
||||
|
||||
@classmethod
|
||||
def check_dir(cls, directory):
|
||||
if not os.path.exists(directory):
|
||||
raise error.InvalidDirectoryException(
|
||||
"Directory '{0}' doesn't exist.".format(directory))
|
||||
if not os.path.isdir(directory):
|
||||
raise error.InvalidDirectoryException(
|
||||
"Error: '{0}' is not a directory.".format(directory))
|
||||
|
||||
|
||||
class GraphUpload(base.BaseCommand, FileMethodsMixin):
|
||||
"""Upload deployment graph configuration."""
|
||||
entity_name = 'graph'
|
||||
|
||||
@classmethod
|
||||
def read_data_from_file(cls, file_path=None, serializer=None):
|
||||
"""Read graph data from given path.
|
||||
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:param serializer: serializer object
|
||||
:type serializer: object
|
||||
:return: data
|
||||
:rtype: list|object
|
||||
"""
|
||||
cls.check_file_path(file_path)
|
||||
return (serializer or Serializer()).read_from_full_path(file_path)
|
||||
|
||||
@classmethod
|
||||
def read_data_from_dir(cls, dir_path=None, serializer=None):
|
||||
"""Read graph data from directory.
|
||||
|
||||
:param dir_path: path
|
||||
:type dir_path: str
|
||||
:param serializer: serializer object
|
||||
:type serializer: object
|
||||
:return: data
|
||||
:rtype: list|object
|
||||
"""
|
||||
cls.check_dir(dir_path)
|
||||
serializer = serializer or Serializer()
|
||||
|
||||
metadata_filepath = os.path.join(dir_path, 'metadata.yaml')
|
||||
if os.path.exists(metadata_filepath):
|
||||
data = serializer.read_from_full_path(metadata_filepath)
|
||||
else:
|
||||
data = {}
|
||||
|
||||
tasks = []
|
||||
for file_name in iterfiles(dir_path, 'tasks.yaml'):
|
||||
task_data = serializer.read_from_full_path(file_name)
|
||||
if task_data:
|
||||
tasks.extend(task_data)
|
||||
|
||||
if tasks:
|
||||
data['tasks'] = tasks
|
||||
|
||||
if not data:
|
||||
msg = ("Nothing to upload. Check if at least one 'tasks.yaml' "
|
||||
"file is not empty and exists in '{path}' directory "
|
||||
"path".format(path=dir_path))
|
||||
raise error.ActionException(msg)
|
||||
return data
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(GraphUpload, self).get_parser(prog_name)
|
||||
graph_class = parser.add_mutually_exclusive_group(required=True)
|
||||
|
||||
graph_class.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=False,
|
||||
help='Id of the environment')
|
||||
graph_class.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
required=False,
|
||||
help='Id of the release')
|
||||
graph_class.add_argument('-p',
|
||||
'--plugin',
|
||||
type=int,
|
||||
required=False,
|
||||
help='Id of the plugin')
|
||||
|
||||
parser.add_argument('-t',
|
||||
'--graph-type',
|
||||
required=True,
|
||||
help='Type of the deployment graph')
|
||||
|
||||
graph_source = parser.add_mutually_exclusive_group(required=True)
|
||||
graph_source.add_argument(
|
||||
'-f',
|
||||
'--file',
|
||||
default=None,
|
||||
help='YAML file that contains deployment graph data.'
|
||||
)
|
||||
graph_source.add_argument(
|
||||
'-d',
|
||||
'--dir',
|
||||
default=None,
|
||||
help='The directory that includes tasks.yaml and metadata.yaml.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
parameters_to_graph_class = (
|
||||
('env', 'clusters'),
|
||||
('release', 'releases'),
|
||||
('plugin', 'plugins'),
|
||||
)
|
||||
|
||||
if args.file:
|
||||
data = self.read_data_from_file(args.file)
|
||||
else:
|
||||
data = self.read_data_from_dir(args.dir)
|
||||
|
||||
for parameter, graph_class in parameters_to_graph_class:
|
||||
model_id = getattr(args, parameter)
|
||||
if model_id:
|
||||
self.client.upload(
|
||||
data=data,
|
||||
related_model=graph_class,
|
||||
related_id=model_id,
|
||||
graph_type=args.graph_type
|
||||
)
|
||||
break
|
||||
|
||||
self.app.stdout.write("Deployment graph was successfully uploaded.\n")
|
||||
|
||||
|
||||
class GraphExecute(base.BaseTasksExecuteCommand):
|
||||
"""Start deployment with given graph type."""
|
||||
entity_name = 'graph'
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(GraphExecute, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'-t',
|
||||
'--graph-types',
|
||||
nargs='+',
|
||||
required=True,
|
||||
help='Types of the deployment graph in order of execution'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--nodes',
|
||||
type=int,
|
||||
nargs='+',
|
||||
help='Ids of the nodes to use for deployment.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-T',
|
||||
'--task-names',
|
||||
nargs='+',
|
||||
help='List of deployment tasks to run.'
|
||||
)
|
||||
parser.add_argument('-S',
|
||||
'--subgraphs',
|
||||
type=str,
|
||||
nargs='+',
|
||||
required=False,
|
||||
help='List of subgraphs to execute'
|
||||
'Format is: '
|
||||
'[<start_task>[/<node_ids>]]\
|
||||
[:<end_task>/[<node_ids>]]')
|
||||
return parser
|
||||
|
||||
def get_options(self, parsed_args):
|
||||
return {
|
||||
'graph_types': parsed_args.graph_types,
|
||||
'nodes': parsed_args.nodes,
|
||||
'task_names': parsed_args.task_names,
|
||||
'subgraphs': parsed_args.subgraphs
|
||||
}
|
||||
|
||||
|
||||
class GraphDownload(base.BaseCommand):
|
||||
"""Download deployment graph configuration."""
|
||||
entity_name = 'graph'
|
||||
supported_file_formats = ('json', 'yaml')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(GraphDownload, self).get_parser(prog_name)
|
||||
tasks_level = parser.add_mutually_exclusive_group(required=True)
|
||||
parser.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=True,
|
||||
help='Id of the environment')
|
||||
|
||||
tasks_level.add_argument('-a',
|
||||
'--all',
|
||||
action="store_true",
|
||||
required=False,
|
||||
default=False,
|
||||
help='Download merged graph for the '
|
||||
'environment')
|
||||
tasks_level.add_argument('-c',
|
||||
'--cluster',
|
||||
action="store_true",
|
||||
required=False,
|
||||
default=False,
|
||||
help='Download cluster-specific tasks')
|
||||
tasks_level.add_argument('-p',
|
||||
'--plugins',
|
||||
action="store_true",
|
||||
required=False,
|
||||
default=False,
|
||||
help='Download plugins-specific tasks')
|
||||
tasks_level.add_argument('-r',
|
||||
'--release',
|
||||
action="store_true",
|
||||
required=False,
|
||||
default=False,
|
||||
help='Download release-specific tasks')
|
||||
|
||||
parser.add_argument('-t',
|
||||
'--graph-type',
|
||||
type=str,
|
||||
default=None,
|
||||
required=False,
|
||||
help='Graph type string')
|
||||
parser.add_argument('-f',
|
||||
'--file',
|
||||
type=str,
|
||||
required=False,
|
||||
default=None,
|
||||
help='File in {} format that contains tasks '
|
||||
'data.'.format(self.supported_file_formats))
|
||||
parser.add_argument('--format',
|
||||
required=False,
|
||||
choices=self.supported_file_formats,
|
||||
default='yaml',
|
||||
help='Format of serialized tasks data. '
|
||||
'Defaults to YAML.')
|
||||
return parser
|
||||
|
||||
@classmethod
|
||||
def get_default_tasks_data_path(cls, env_id, task_level_name, file_format):
|
||||
return os.path.join(
|
||||
os.path.abspath(os.curdir),
|
||||
'{}_graph_{}.{}'.format(task_level_name, env_id, file_format)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def write_tasks_to_file(cls, tasks_data, serializer, file_path):
|
||||
return serializer.write_to_full_path(file_path, tasks_data)
|
||||
|
||||
def take_action(self, args):
|
||||
tasks_data = []
|
||||
tasks_level_name = ''
|
||||
for tasks_level_name in ('all', 'cluster', 'release', 'plugins'):
|
||||
if getattr(args, tasks_level_name):
|
||||
tasks_data = self.client.download(
|
||||
env_id=args.env,
|
||||
level=tasks_level_name,
|
||||
graph_type=args.graph_type
|
||||
)
|
||||
break
|
||||
|
||||
# write to file
|
||||
file_path = args.file or self.get_default_tasks_data_path(
|
||||
args.env, tasks_level_name, args.format)
|
||||
graph_data_file_path = self.write_tasks_to_file(
|
||||
tasks_data=tasks_data,
|
||||
serializer=Serializer(format=args.format),
|
||||
file_path=file_path)
|
||||
|
||||
self.app.stdout.write(
|
||||
"Tasks were downloaded to {0}\n".format(graph_data_file_path)
|
||||
)
|
||||
|
||||
|
||||
class GraphList(base.BaseListCommand):
|
||||
"""List deployment graphs."""
|
||||
entity_name = 'graph'
|
||||
columns = ("id",
|
||||
"name",
|
||||
"tasks",
|
||||
"relations")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(GraphList, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--cluster',
|
||||
dest='filters',
|
||||
action='append_const',
|
||||
const='cluster',
|
||||
help='Include cluster-specific graphs'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--plugins',
|
||||
dest='filters',
|
||||
action='append_const',
|
||||
const='plugin',
|
||||
help='Include plugins-specific graphs'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--release',
|
||||
dest='filters',
|
||||
action='append_const',
|
||||
const='release',
|
||||
help='Include release-specific graphs'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
data = self.client.list(env_id=args.env, filters=args.filters)
|
||||
|
||||
# make table context applying special formatting to data copy
|
||||
display_data = []
|
||||
for d in data:
|
||||
d = d.copy()
|
||||
d.update({
|
||||
'relations': "\n".join(
|
||||
'as "{type}" to {model}(ID={model_id})'.format(**r)
|
||||
for r in d['relations']
|
||||
),
|
||||
'tasks': ', '.join(sorted(t['id'] for t in d['tasks']))
|
||||
})
|
||||
display_data.append(d)
|
||||
|
||||
data = data_utils.get_display_data_multi(self.columns, display_data)
|
||||
scolumn_ids = [self.columns.index(col) for col in args.sort_columns]
|
||||
data.sort(key=lambda x: [x[scolumn_id] for scolumn_id in scolumn_ids])
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class GraphDelete(base.BaseCommand):
|
||||
"""Delete deployment graph."""
|
||||
entity_name = 'graph'
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(GraphDelete, self).get_parser(prog_name)
|
||||
graph_class = parser.add_mutually_exclusive_group(required=True)
|
||||
graph_class.add_argument('-e',
|
||||
'--environment',
|
||||
type=int,
|
||||
help='Id of the environment')
|
||||
graph_class.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
graph_class.add_argument('-p',
|
||||
'--plugin',
|
||||
type=int,
|
||||
help='Id of the plugin')
|
||||
parser.add_argument('-t',
|
||||
'--graph-type',
|
||||
required=True,
|
||||
help='Type of the deployment graph')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
parameters_to_graph_class = (
|
||||
('environment', 'clusters'),
|
||||
('release', 'releases'),
|
||||
('plugin', 'plugins'),
|
||||
)
|
||||
|
||||
msg = ''
|
||||
for parameter, graph_class in parameters_to_graph_class:
|
||||
model_id = getattr(parsed_args, parameter)
|
||||
if model_id:
|
||||
self.client.delete(
|
||||
related_model=graph_class,
|
||||
related_id=model_id,
|
||||
graph_type=parsed_args.graph_type
|
||||
)
|
||||
msg = ("Deployment graph '{0}' for {1} with id {2} was "
|
||||
"deleted.\n".format(parsed_args.graph_type,
|
||||
parameter,
|
||||
model_id))
|
||||
break
|
||||
|
||||
self.app.stdout.write(msg)
|
@ -1,177 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 Vitalii Kulanov
|
||||
#
|
||||
# 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 six
|
||||
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
class HealthMixIn(object):
|
||||
|
||||
entity_name = 'health'
|
||||
|
||||
|
||||
class HealthTestSetsList(HealthMixIn, base.BaseListCommand):
|
||||
"""List of all available test sets for a given environment."""
|
||||
|
||||
columns = ("id",
|
||||
"name")
|
||||
|
||||
filters = {'environment_id': 'env'}
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(HealthTestSetsList, self).get_parser(prog_name)
|
||||
parser.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=True,
|
||||
help='Id of the environment.')
|
||||
return parser
|
||||
|
||||
|
||||
class HealthCheckStart(HealthMixIn, base.BaseListCommand):
|
||||
"""Run specified test sets for a given environment."""
|
||||
|
||||
columns = ("id",
|
||||
"testset",
|
||||
"cluster_id")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(HealthCheckStart, self).get_parser(prog_name)
|
||||
parser.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=True,
|
||||
help='Id of the environment.')
|
||||
parser.add_argument('--force',
|
||||
action='store_true',
|
||||
help='Force run health test sets.')
|
||||
parser.add_argument('-t',
|
||||
'--tests',
|
||||
nargs='+',
|
||||
help='Name of the test sets to run.')
|
||||
parser.add_argument('--ostf-username',
|
||||
default=None,
|
||||
help='OSTF username.')
|
||||
parser.add_argument('--ostf-password',
|
||||
default=None,
|
||||
help='OSTF password.')
|
||||
parser.add_argument('--ostf-tenant-name',
|
||||
default=None,
|
||||
help='OSTF tenant name.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
ostf_credentials = {}
|
||||
if parsed_args.ostf_tenant_name is not None:
|
||||
ostf_credentials['tenant'] = parsed_args.ostf_tenant_name
|
||||
if parsed_args.ostf_username is not None:
|
||||
ostf_credentials['username'] = parsed_args.ostf_username
|
||||
if parsed_args.ostf_password is not None:
|
||||
ostf_credentials['password'] = parsed_args.ostf_password
|
||||
|
||||
if not ostf_credentials:
|
||||
self.app.stdout.write("WARNING: ostf credentials are going to be "
|
||||
"mandatory in the next release.\n")
|
||||
|
||||
data = self.client.start(parsed_args.env,
|
||||
ostf_credentials=ostf_credentials,
|
||||
test_sets=parsed_args.tests,
|
||||
force=parsed_args.force)
|
||||
|
||||
msg = ("\nHealth check tests for environment with id {0} has been "
|
||||
"started:\n".format(parsed_args.env))
|
||||
self.app.stdout.write(msg)
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class HealthCheckBaseAction(HealthMixIn, base.BaseShowCommand):
|
||||
"""Base class for implementing action over a given test set."""
|
||||
|
||||
columns = ("id",
|
||||
"testset",
|
||||
"cluster_id",
|
||||
"status")
|
||||
|
||||
@abc.abstractproperty
|
||||
def action_status(self):
|
||||
"""String with the name of the action."""
|
||||
pass
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.action(parsed_args.id, self.action_status)
|
||||
|
||||
data = data_utils.get_display_data_single(self.columns, data)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class HealthCheckStop(HealthCheckBaseAction):
|
||||
"""Stop test set with given id."""
|
||||
|
||||
action_status = "stopped"
|
||||
|
||||
|
||||
class HealthCheckRestart(HealthCheckBaseAction):
|
||||
"""Restart test set with given id."""
|
||||
|
||||
action_status = "restarted"
|
||||
|
||||
|
||||
class HealthTestSetsStatusList(HealthMixIn, base.BaseListCommand):
|
||||
"""Show list of statuses of all test sets ever been executed in Fuel."""
|
||||
|
||||
columns = ("id",
|
||||
"testset",
|
||||
"cluster_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(HealthTestSetsStatusList, self).get_parser(prog_name)
|
||||
parser.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_status_all(parsed_args.env)
|
||||
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class HealthTestSetsStatusShow(HealthMixIn, base.BaseShowCommand):
|
||||
"""Show status about a test set with given id."""
|
||||
|
||||
columns = ("id",
|
||||
"testset",
|
||||
"cluster_id",
|
||||
"status",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"tests")
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_status_single(parsed_args.id)
|
||||
|
||||
data = data_utils.get_display_data_single(self.columns, data)
|
||||
return self.columns, data
|
@ -1,174 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from cliff import show
|
||||
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
_updatable_keys = (
|
||||
'name', 'vlan', 'cidr', 'gateway', 'group_id', 'meta')
|
||||
|
||||
|
||||
def get_args_for_update(params, serializer=None):
|
||||
result = {}
|
||||
for attr in _updatable_keys:
|
||||
value = getattr(params, attr, None)
|
||||
if value is not None:
|
||||
result[attr] = value
|
||||
|
||||
if 'meta' in result:
|
||||
serializer = serializer or Serializer.from_params(params)
|
||||
result['meta'] = serializer.deserialize(result['meta'])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class NetworkGroupMixin(object):
|
||||
entity_name = 'network-group'
|
||||
|
||||
@staticmethod
|
||||
def add_parser_arguments(parser, for_update=False):
|
||||
parser.add_argument(
|
||||
'-N', '--node-group',
|
||||
type=int,
|
||||
required=not for_update,
|
||||
help='ID of the network group'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-C', '--cidr',
|
||||
type=str,
|
||||
required=not for_update,
|
||||
help='CIDR of the network'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-V', '--vlan',
|
||||
type=int,
|
||||
help='VLAN of the network',
|
||||
)
|
||||
|
||||
if not for_update:
|
||||
parser.add_argument(
|
||||
'-r', '--release',
|
||||
type=int,
|
||||
help='Release ID this network group belongs to'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-g', '--gateway',
|
||||
type=str,
|
||||
help='Gateway of the network'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-m', '--meta',
|
||||
type=str,
|
||||
help='Metadata in JSON format to override default network metadata'
|
||||
)
|
||||
|
||||
|
||||
class NetworkGroupList(NetworkGroupMixin, base.BaseListCommand):
|
||||
"""List all network groups."""
|
||||
|
||||
columns = (
|
||||
'id',
|
||||
'name',
|
||||
'vlan_start',
|
||||
'cidr',
|
||||
'gateway',
|
||||
'group_id'
|
||||
)
|
||||
|
||||
|
||||
class NetworkGroupShow(NetworkGroupMixin, base.BaseShowCommand):
|
||||
"""Show network group."""
|
||||
|
||||
columns = NetworkGroupList.columns + ('meta',)
|
||||
|
||||
|
||||
class NetworkGroupCreate(NetworkGroupMixin, base.BaseShowCommand):
|
||||
"""Create a new network group."""
|
||||
|
||||
columns = NetworkGroupList.columns
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = show.ShowOne.get_parser(self, prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'name',
|
||||
type=str,
|
||||
help='Name of the new network group'
|
||||
)
|
||||
|
||||
self.add_parser_arguments(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
meta = None
|
||||
if parsed_args.meta:
|
||||
serializer = Serializer.from_params(parsed_args)
|
||||
meta = serializer.deserialize(parsed_args.meta)
|
||||
|
||||
net_group = self.client.create(
|
||||
name=parsed_args.name,
|
||||
release=parsed_args.release,
|
||||
vlan=parsed_args.vlan,
|
||||
cidr=parsed_args.cidr,
|
||||
gateway=parsed_args.gateway,
|
||||
group_id=parsed_args.node_group,
|
||||
meta=meta)
|
||||
|
||||
net_group = data_utils.get_display_data_single(self.columns, net_group)
|
||||
return self.columns, net_group
|
||||
|
||||
|
||||
class NetworkGroupUpdate(NetworkGroupMixin, base.BaseShowCommand):
|
||||
"""Set parameters for the specified network group."""
|
||||
|
||||
columns = NetworkGroupList.columns
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = show.ShowOne.get_parser(self, prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'id',
|
||||
type=int,
|
||||
help='ID of the network group to update')
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--name',
|
||||
type=str,
|
||||
help='New name for network group')
|
||||
|
||||
self.add_parser_arguments(parser, for_update=True)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
to_update = get_args_for_update(parsed_args)
|
||||
|
||||
network_group = self.client.update(parsed_args.id, **to_update)
|
||||
network_group = data_utils.get_display_data_single(
|
||||
self.columns, network_group)
|
||||
|
||||
return self.columns, network_group
|
||||
|
||||
|
||||
class NetworkGroupDelete(NetworkGroupMixin, base.BaseDeleteCommand):
|
||||
"""Delete specified network group."""
|
@ -1,103 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from fuelclient.commands import base
|
||||
|
||||
|
||||
class NetworkTemplateMixin(object):
|
||||
|
||||
entity_name = 'environment'
|
||||
|
||||
@staticmethod
|
||||
def add_env_argument(parser):
|
||||
parser.add_argument(
|
||||
'env',
|
||||
type=int,
|
||||
help='Id of the environment'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_dir_argument(parser):
|
||||
parser.add_argument(
|
||||
'-d', '--dir',
|
||||
type=str,
|
||||
help='Directory for the network template'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_file_argument(parser):
|
||||
parser.add_argument(
|
||||
'-f', '--file',
|
||||
required=True,
|
||||
type=str,
|
||||
help='Yaml file containing the network template'
|
||||
)
|
||||
|
||||
|
||||
class NetworkTemplateUpload(NetworkTemplateMixin, base.BaseCommand):
|
||||
"""Upload network configuration for specified environment."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NetworkTemplateUpload, self).get_parser(prog_name)
|
||||
|
||||
self.add_env_argument(parser)
|
||||
self.add_file_argument(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
|
||||
file_path = self.client.upload_network_template(
|
||||
parsed_args.env, parsed_args.file)
|
||||
msg = "Network template {0} has been uploaded.\n".format(file_path)
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class NetworkTemplateDownload(NetworkTemplateMixin, base.BaseCommand):
|
||||
"""Download network configuration for specified environment."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NetworkTemplateDownload, self).get_parser(prog_name)
|
||||
|
||||
self.add_dir_argument(parser)
|
||||
self.add_env_argument(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
file_path = self.client.download_network_template(
|
||||
parsed_args.env, parsed_args.dir)
|
||||
|
||||
msg = ("Network template configuration for environment with id={0}"
|
||||
" downloaded to {1}\n").format(
|
||||
parsed_args.env, file_path)
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class NetworkTemplateDelete(NetworkTemplateMixin, base.BaseCommand):
|
||||
"""Delete the network template of the specified environment."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NetworkTemplateDelete, self).get_parser(prog_name)
|
||||
|
||||
self.add_env_argument(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.delete_network_template(parsed_args.env)
|
||||
|
||||
msg = ("Network template for environment id={0}"
|
||||
" has been deleted.\n".format(parsed_args.env))
|
||||
self.app.stdout.write(msg)
|
@ -1,609 +0,0 @@
|
||||
# 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 collections
|
||||
import json
|
||||
import operator
|
||||
import os
|
||||
|
||||
from oslo_utils import fileutils
|
||||
import six
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class NodeMixIn(object):
|
||||
entity_name = 'node'
|
||||
|
||||
numa_fields = (
|
||||
'numa_nodes',
|
||||
'supported_hugepages',
|
||||
'distances')
|
||||
|
||||
supported_file_formats = ('json', 'yaml')
|
||||
allowed_attr_types = ('attributes', 'disks', 'interfaces')
|
||||
|
||||
@classmethod
|
||||
def get_numa_topology_info(cls, data):
|
||||
numa_topology_info = {}
|
||||
numa_topology = data['meta'].get('numa_topology', {})
|
||||
for key in cls.numa_fields:
|
||||
numa_topology_info[key] = numa_topology.get(key)
|
||||
return numa_topology_info
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseUploadCommand(NodeMixIn, base.BaseCommand):
|
||||
"""Base class for uploading attributes of a node."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def attribute(self):
|
||||
"""String with the name of the attribute."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def uploader(self):
|
||||
"""Callable for uploading data."""
|
||||
pass
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseUploadCommand, self).get_parser(prog_name)
|
||||
parser.add_argument('id',
|
||||
type=int,
|
||||
help='Id of a node.')
|
||||
parser.add_argument('-f',
|
||||
'--format',
|
||||
required=True,
|
||||
choices=self.supported_file_formats,
|
||||
help='Format of serialized '
|
||||
'{} data.'.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:
|
||||
attributes = data_utils.safe_load(parsed_args.format,
|
||||
stream)
|
||||
self.uploader(parsed_args.id, attributes)
|
||||
except (OSError, IOError):
|
||||
msg = 'Could not read configuration of {} at {}.'
|
||||
raise error.InvalidFileException(msg.format(self.attribute,
|
||||
file_path))
|
||||
|
||||
msg = ('Configuration of {t} for node with id '
|
||||
'{node} was loaded from {path}\n')
|
||||
self.app.stdout.write(msg.format(t=self.attribute,
|
||||
node=parsed_args.id,
|
||||
path=file_path))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseDownloadCommand(NodeMixIn, base.BaseCommand):
|
||||
"""Base class for downloading attributes of a node."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def attribute(self):
|
||||
"""String with the name of the attribute."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def downloader(self):
|
||||
"""Callable for downloading data."""
|
||||
pass
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseDownloadCommand, self).get_parser(prog_name)
|
||||
parser.add_argument('id',
|
||||
type=int,
|
||||
help='Id of a node.')
|
||||
parser.add_argument('-f',
|
||||
'--format',
|
||||
required=True,
|
||||
choices=self.supported_file_formats,
|
||||
help='Format of serialized '
|
||||
'{} data.'.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
|
||||
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 (OSError, IOError):
|
||||
msg = 'Could not store configuration of {} at {}.'
|
||||
raise error.InvalidFileException(msg.format(self.attribute,
|
||||
file_path))
|
||||
|
||||
msg = ('Configuration of {t} for node with id '
|
||||
'{node} was stored in {path}\n')
|
||||
self.app.stdout.write(msg.format(t=self.attribute,
|
||||
node=parsed_args.id,
|
||||
path=file_path))
|
||||
|
||||
|
||||
class NodeList(NodeMixIn, base.BaseListCommand):
|
||||
"""Show list of all available nodes."""
|
||||
|
||||
columns = ('id',
|
||||
'name',
|
||||
'status',
|
||||
'os_platform',
|
||||
'roles',
|
||||
'ip',
|
||||
'mac',
|
||||
'cluster',
|
||||
'platform_name',
|
||||
'online')
|
||||
|
||||
filters = {
|
||||
'environment_id': 'env',
|
||||
'labels': 'labels'
|
||||
}
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeList, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Show only nodes that are in the specified environment')
|
||||
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--labels',
|
||||
type=utils.str_to_unicode,
|
||||
nargs='+',
|
||||
help='Show only nodes that have specific labels')
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
class NodeShow(NodeMixIn, base.BaseShowCommand):
|
||||
"""Show info about node with given id."""
|
||||
|
||||
columns = ('id',
|
||||
'name',
|
||||
'status',
|
||||
'os_platform',
|
||||
'roles',
|
||||
'kernel_params',
|
||||
'pending_roles',
|
||||
'ip',
|
||||
'mac',
|
||||
'error_type',
|
||||
'pending_addition',
|
||||
'hostname',
|
||||
'fqdn',
|
||||
'platform_name',
|
||||
'cluster',
|
||||
'online',
|
||||
'progress',
|
||||
'pending_deletion',
|
||||
'group_id',
|
||||
# TODO(romcheg): network_data mostly never fits the screen
|
||||
# 'network_data',
|
||||
'manufacturer')
|
||||
columns += NodeMixIn.numa_fields
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_by_id(parsed_args.id)
|
||||
numa_topology = self.get_numa_topology_info(data)
|
||||
data.update(numa_topology)
|
||||
data = data_utils.get_display_data_single(self.columns, data)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class NodeUpdate(NodeMixIn, base.BaseShowCommand):
|
||||
"""Change given attributes for a node."""
|
||||
|
||||
columns = NodeShow.columns
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeUpdate, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'-H',
|
||||
'--hostname',
|
||||
type=str,
|
||||
default=None,
|
||||
help='New hostname for node')
|
||||
|
||||
parser.add_argument(
|
||||
'--name',
|
||||
type=lambda x: x.decode('utf-8') if six.PY2 else x,
|
||||
default=None,
|
||||
help='New name for node')
|
||||
|
||||
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_node = self.client.update(
|
||||
parsed_args.id, **updates)
|
||||
numa_topology = self.get_numa_topology_info(updated_node)
|
||||
updated_node.update(numa_topology)
|
||||
updated_node = data_utils.get_display_data_single(
|
||||
self.columns, updated_node)
|
||||
|
||||
return self.columns, updated_node
|
||||
|
||||
|
||||
class NodeVmsList(NodeMixIn, base.BaseShowCommand):
|
||||
"""Show list vms for node."""
|
||||
|
||||
columns = ('vms_conf',)
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_node_vms_conf(parsed_args.id)
|
||||
data = data_utils.get_display_data_single(self.columns, data)
|
||||
|
||||
return (self.columns, data)
|
||||
|
||||
|
||||
class NodeCreateVMsConf(NodeMixIn, base.BaseCommand):
|
||||
"""Create vms config in metadata for selected node."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeCreateVMsConf, self).get_parser(prog_name)
|
||||
parser.add_argument('id', type=int,
|
||||
help='Id of the {0}.'.format(self.entity_name))
|
||||
parser.add_argument(
|
||||
'--conf',
|
||||
type=json.loads,
|
||||
required=True,
|
||||
nargs='+',
|
||||
help='JSONs with VMs configuration',
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
try:
|
||||
confs = utils.parse_to_list_of_dicts(parsed_args.conf)
|
||||
except TypeError:
|
||||
raise error.BadDataException(
|
||||
'VM configuration should be a dictionary '
|
||||
'or a list of dictionaries')
|
||||
data = self.client.node_vms_create(parsed_args.id, confs)
|
||||
msg = "{0}".format(data)
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class NodeLabelList(NodeMixIn, base.BaseListCommand):
|
||||
"""Show list of all labels."""
|
||||
|
||||
columns = (
|
||||
'node_id',
|
||||
'label_name',
|
||||
'label_value')
|
||||
|
||||
@property
|
||||
def default_sorting_by(self):
|
||||
return ['node_id']
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeLabelList, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--nodes',
|
||||
nargs='+',
|
||||
help='Show labels for specific nodes')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_all_labels_for_nodes(
|
||||
node_ids=parsed_args.nodes)
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
data = self._sort_data(parsed_args, data)
|
||||
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class NodeLabelSet(NodeMixIn, base.BaseCommand):
|
||||
"""Create or update specifc labels on nodes."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeLabelSet, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--labels',
|
||||
required=True,
|
||||
nargs='+',
|
||||
help='List of labels for create or update')
|
||||
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
'-n',
|
||||
'--nodes',
|
||||
nargs='+',
|
||||
help='Create or update labels only for specific nodes')
|
||||
group.add_argument(
|
||||
'--nodes-all',
|
||||
action='store_true',
|
||||
help='Create or update labels for all nodes')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
nodes_ids = None if parsed_args.nodes_all else parsed_args.nodes
|
||||
data = self.client.set_labels_for_nodes(
|
||||
labels=parsed_args.labels, node_ids=nodes_ids)
|
||||
msg = "Labels have been updated on nodes: {0} \n".format(
|
||||
','.join(data))
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class NodeLabelDelete(NodeMixIn, base.BaseCommand):
|
||||
"""Delete specific labels on nodes."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeLabelDelete, self).get_parser(prog_name)
|
||||
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
'-l',
|
||||
'--labels',
|
||||
nargs='+',
|
||||
help='List of labels keys for delete')
|
||||
group.add_argument(
|
||||
'--labels-all',
|
||||
action='store_true',
|
||||
help='Delete all labels for node')
|
||||
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
'-n',
|
||||
'--nodes',
|
||||
nargs='+',
|
||||
help='Delete labels only for specific nodes')
|
||||
group.add_argument(
|
||||
'--nodes-all',
|
||||
action='store_true',
|
||||
help='Delete labels for all nodes')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
nodes_ids = None if parsed_args.nodes_all else parsed_args.nodes
|
||||
labels = None if parsed_args.labels_all \
|
||||
else parsed_args.labels
|
||||
data = self.client.delete_labels_for_nodes(
|
||||
labels=labels, node_ids=nodes_ids)
|
||||
msg = "Labels have been deleted on nodes: {0} \n".format(
|
||||
','.join(data))
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class NodeAttributesDownload(NodeMixIn, base.BaseCommand):
|
||||
"""Download node attributes."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeAttributesDownload, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'id', type=int, help='Node ID')
|
||||
parser.add_argument(
|
||||
'--dir', type=str, help='Directory to save attributes')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
file_path = self.client.download_attributes(
|
||||
parsed_args.id, parsed_args.dir)
|
||||
self.app.stdout.write(
|
||||
"Attributes for node {0} were written to {1}"
|
||||
.format(parsed_args.id, file_path) + os.linesep)
|
||||
|
||||
|
||||
class NodeAttributesUpload(NodeMixIn, base.BaseCommand):
|
||||
"""Upload node attributes."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeAttributesUpload, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'id', type=int, help='Node ID')
|
||||
parser.add_argument(
|
||||
'--dir', type=str, help='Directory to read attributes from')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.upload_attributes(parsed_args.id, parsed_args.dir)
|
||||
self.app.stdout.write(
|
||||
"Attributes for node {0} were uploaded."
|
||||
.format(parsed_args.id) + os.linesep)
|
||||
|
||||
|
||||
class NodeAnsibleInventory(NodeMixIn, base.BaseCommand):
|
||||
"""Generate ansible inventory file based on the nodes list."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeAnsibleInventory, self).get_parser(prog_name)
|
||||
|
||||
# if this is a required argument, we'll avoid ambiguity of having nodes
|
||||
# of multiple different clusters in the same inventory file
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=True,
|
||||
help='Use only nodes that are in the specified environment')
|
||||
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--labels',
|
||||
type=utils.str_to_unicode,
|
||||
nargs='+',
|
||||
help='Use only nodes that have specific labels')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_all(environment_id=parsed_args.env,
|
||||
labels=parsed_args.labels)
|
||||
|
||||
nodes_by_role = collections.defaultdict(list)
|
||||
for node in data:
|
||||
for role in node['roles']:
|
||||
nodes_by_role[role].append(node)
|
||||
|
||||
for role, nodes in sorted(nodes_by_role.items()):
|
||||
self.app.stdout.write(u'[{role}]\n'.format(role=role))
|
||||
self.app.stdout.write(
|
||||
u'\n'.join(
|
||||
u'{name} ansible_host={ip}'.format(name=node['hostname'],
|
||||
ip=node['ip'])
|
||||
for node in sorted(nodes_by_role[role],
|
||||
key=operator.itemgetter('hostname'))
|
||||
)
|
||||
)
|
||||
self.app.stdout.write(u'\n\n')
|
||||
|
||||
|
||||
class NodeInterfacesDownload(BaseDownloadCommand):
|
||||
"""Download and store configuration of interfaces for a node to a file."""
|
||||
|
||||
attribute = 'interfaces'
|
||||
|
||||
@property
|
||||
def downloader(self):
|
||||
return self.client.get_interfaces
|
||||
|
||||
|
||||
class NodeInterfacesGetDefault(BaseDownloadCommand):
|
||||
"""Download default configuration of interfaces for a node to a file."""
|
||||
|
||||
attribute = 'interfaces'
|
||||
|
||||
@property
|
||||
def downloader(self):
|
||||
return self.client.get_default_interfaces
|
||||
|
||||
|
||||
class NodeInterfacesUpload(BaseUploadCommand):
|
||||
"""Upload stored configuration of interfaces for a node from a file."""
|
||||
|
||||
attribute = 'interfaces'
|
||||
|
||||
@property
|
||||
def uploader(self):
|
||||
return self.client.set_interfaces
|
||||
|
||||
|
||||
class NodeDisksDownload(BaseDownloadCommand):
|
||||
"""Download and store configuration of disks for a node to a file."""
|
||||
|
||||
attribute = 'disks'
|
||||
|
||||
@property
|
||||
def downloader(self):
|
||||
return self.client.get_disks
|
||||
|
||||
|
||||
class NodeDisksGetDefault(BaseDownloadCommand):
|
||||
"""Download default configuration of disks for a node to a file."""
|
||||
|
||||
attribute = 'disks'
|
||||
|
||||
@property
|
||||
def downloader(self):
|
||||
return self.client.get_default_disks
|
||||
|
||||
|
||||
class NodeDisksUpload(BaseUploadCommand):
|
||||
"""Upload stored configuration of disks for a node from a file."""
|
||||
|
||||
attribute = 'disks'
|
||||
|
||||
@property
|
||||
def uploader(self):
|
||||
return self.client.set_disks
|
||||
|
||||
|
||||
class NodeUndiscover(NodeMixIn, base.BaseCommand):
|
||||
"""Remove nodes from database."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(NodeUndiscover, self).get_parser(prog_name)
|
||||
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of environment to remove all nodes '
|
||||
'from database.')
|
||||
group.add_argument('-n',
|
||||
'--node',
|
||||
type=int,
|
||||
help='Id of the node to remove from database.')
|
||||
|
||||
parser.add_argument(
|
||||
'-f',
|
||||
'--force',
|
||||
action='store_true',
|
||||
help='Forces deletion of nodes from database '
|
||||
'regardless of their state.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
node_ids = self.client.undiscover_nodes(env_id=parsed_args.env,
|
||||
node_id=parsed_args.node,
|
||||
force=parsed_args.force)
|
||||
|
||||
self.app.stdout.write(
|
||||
'Nodes {0} were deleted from the database\n'.format(node_ids)
|
||||
)
|
@ -1,182 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
class OpenstackConfigMixin(object):
|
||||
entity_name = 'openstack-config'
|
||||
|
||||
columns = (
|
||||
'id', 'is_active', 'config_type',
|
||||
'cluster_id', 'node_id', 'node_role')
|
||||
|
||||
@staticmethod
|
||||
def add_env_arg(parser):
|
||||
parser.add_argument(
|
||||
'-e', '--env',
|
||||
type=int, required=True,
|
||||
help='Environment ID.')
|
||||
|
||||
@staticmethod
|
||||
def add_file_arg(parser):
|
||||
parser.add_argument(
|
||||
'--file', required=True,
|
||||
type=str, help='YAML file that contains openstack configuration.')
|
||||
|
||||
@staticmethod
|
||||
def add_config_id_arg(parser):
|
||||
parser.add_argument(
|
||||
'config',
|
||||
type=int, help='Id of the OpenStack configuration.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_node_ids_arg(parser):
|
||||
parser.add_argument(
|
||||
'-n', '--node',
|
||||
type=int, nargs='+', default=None, help='Ids of the nodes.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_node_role_arg(parser):
|
||||
parser.add_argument(
|
||||
'-r', '--role',
|
||||
type=str, default=None, help='Role of the nodes.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_deleted_arg(parser):
|
||||
parser.add_argument(
|
||||
'-D', '--deleted',
|
||||
type=bool, default=False, help='Show deleted configurations.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_force_arg(parser):
|
||||
parser.add_argument(
|
||||
'-f', '--force',
|
||||
action='store_true', help='Force the update of the configuration.'
|
||||
)
|
||||
|
||||
|
||||
class OpenstackConfigList(OpenstackConfigMixin, base.BaseListCommand):
|
||||
"""List all OpenStack configurations."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(OpenstackConfigList, self).get_parser(prog_name)
|
||||
|
||||
self.add_env_arg(parser)
|
||||
self.add_node_ids_arg(parser)
|
||||
self.add_node_role_arg(parser)
|
||||
self.add_deleted_arg(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
data = self.client.get_filtered(
|
||||
cluster_id=args.env, node_ids=args.node,
|
||||
node_role=args.role, is_active=(not args.deleted))
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
data = self._sort_data(args, data)
|
||||
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class OpenstackConfigDownload(OpenstackConfigMixin, base.BaseCommand):
|
||||
"""Download specified configuration file."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(OpenstackConfigDownload, self).get_parser(prog_name)
|
||||
|
||||
self.add_config_id_arg(parser)
|
||||
self.add_file_arg(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
file_path = self.client.download(args.config, args.file)
|
||||
|
||||
msg = 'OpenStack configuration with id={c} '\
|
||||
'downloaded to {p}.\n'.format(c=args.config, p=file_path)
|
||||
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class OpenstackConfigUpload(OpenstackConfigMixin, base.BaseListCommand):
|
||||
"""Upload new OpenStack configuration from file."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(OpenstackConfigUpload, self).get_parser(prog_name)
|
||||
|
||||
self.add_env_arg(parser)
|
||||
self.add_node_ids_arg(parser)
|
||||
self.add_node_role_arg(parser)
|
||||
self.add_file_arg(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
configs = self.client.upload(path=args.file,
|
||||
cluster_id=args.env,
|
||||
node_ids=args.node,
|
||||
node_role=args.role)
|
||||
|
||||
data = data_utils.get_display_data_multi(self.columns, configs)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class OpenstackConfigExecute(OpenstackConfigMixin, base.BaseCommand):
|
||||
"""Execute OpenStack configuration deployment."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(OpenstackConfigExecute, self).get_parser(prog_name)
|
||||
|
||||
self.add_env_arg(parser)
|
||||
self.add_node_ids_arg(parser)
|
||||
self.add_node_role_arg(parser)
|
||||
self.add_force_arg(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
task = self.client.execute(cluster_id=args.env,
|
||||
node_ids=args.node,
|
||||
node_role=args.role,
|
||||
force=args.force)
|
||||
|
||||
msg = ('Deployment of the OpenStack configuration was started within '
|
||||
'task with id {task_id}.\n').format(task_id=task['id'])
|
||||
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class OpenstackConfigDelete(OpenstackConfigMixin, base.BaseCommand):
|
||||
"""Delete OpenStack configuration with given id."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(OpenstackConfigDelete, self).get_parser(prog_name)
|
||||
|
||||
self.add_config_id_arg(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
self.client.delete(args.config)
|
||||
|
||||
msg = 'Openstack configuration with id {c} '\
|
||||
'was deleted.\n'.format(c=args.config)
|
||||
|
||||
self.app.stdout.write(msg)
|
@ -1,117 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from fuelclient.commands import base
|
||||
|
||||
|
||||
class PluginsMixIn(object):
|
||||
entity_name = 'plugins'
|
||||
|
||||
@staticmethod
|
||||
def add_plugin_file_argument(parser):
|
||||
parser.add_argument(
|
||||
'file',
|
||||
type=str,
|
||||
help='Path to plugin file to install'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_plugin_name_argument(parser):
|
||||
parser.add_argument(
|
||||
'name',
|
||||
type=str,
|
||||
help='Name of plugin to remove'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_plugin_version_argument(parser):
|
||||
parser.add_argument(
|
||||
'version',
|
||||
type=str,
|
||||
help='Version of plugin to remove'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_plugin_ids_argument(parser):
|
||||
parser.add_argument(
|
||||
'ids',
|
||||
type=int,
|
||||
nargs='*',
|
||||
metavar='plugin-id',
|
||||
help='Synchronise only plugins with specified ids'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_plugin_install_force_argument(parser):
|
||||
parser.add_argument(
|
||||
'-f', '--force',
|
||||
action='store_true',
|
||||
help='Used for reinstall plugin with the same version'
|
||||
)
|
||||
|
||||
|
||||
class PluginsList(PluginsMixIn, base.BaseListCommand):
|
||||
"""Show list of all available plugins."""
|
||||
|
||||
columns = ('id',
|
||||
'name',
|
||||
'version',
|
||||
'package_version',
|
||||
'releases')
|
||||
|
||||
|
||||
class PluginsSync(PluginsMixIn, base.BaseCommand):
|
||||
"""Synchronise plugins on file system with plugins in API service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(PluginsSync, self).get_parser(prog_name)
|
||||
self.add_plugin_ids_argument(parser)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
ids = parsed_args.ids if len(parsed_args.ids) > 0 else None
|
||||
self.client.sync(ids=ids)
|
||||
self.app.stdout.write("Plugins were successfully synchronized.\n")
|
||||
|
||||
|
||||
class PluginInstall(PluginsMixIn, base.BaseCommand):
|
||||
"""Install plugin archive and register in API service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(PluginInstall, self).get_parser(prog_name)
|
||||
self.add_plugin_file_argument(parser)
|
||||
self.add_plugin_install_force_argument(parser)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.install(parsed_args.file, force=parsed_args.force)
|
||||
self.app.stdout.write(
|
||||
"Plugin {0} was successfully installed.\n".format(parsed_args.file)
|
||||
)
|
||||
|
||||
|
||||
class PluginRemove(PluginsMixIn, base.BaseCommand):
|
||||
"""Remove the plugin package, and update data in API service."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(PluginRemove, self).get_parser(prog_name)
|
||||
self.add_plugin_name_argument(parser)
|
||||
self.add_plugin_version_argument(parser)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.remove(parsed_args.name, parsed_args.version)
|
||||
self.app.stdout.write(
|
||||
"Plugin {0} was successfully removed.\n".format(parsed_args.name)
|
||||
)
|
@ -1,146 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class ReleaseMixIn(object):
|
||||
entity_name = 'release'
|
||||
|
||||
|
||||
class ReleaseList(ReleaseMixIn, base.BaseListCommand):
|
||||
"""Show list of all available releases."""
|
||||
|
||||
columns = ("id",
|
||||
"name",
|
||||
"state",
|
||||
"operating_system",
|
||||
"version")
|
||||
|
||||
|
||||
class ReleaseReposList(ReleaseMixIn, base.BaseListCommand):
|
||||
"""Show repos for a given release."""
|
||||
|
||||
columns = (
|
||||
'name',
|
||||
'priority',
|
||||
'uri',
|
||||
'section',
|
||||
'suite',
|
||||
'type')
|
||||
|
||||
@property
|
||||
def default_sorting_by(self):
|
||||
return ['priority']
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ReleaseReposList, self).get_parser(prog_name)
|
||||
parser.add_argument('id', type=int,
|
||||
help='Id of the {0}.'.format(self.entity_name))
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_attributes_metadata_by_id(parsed_args.id)
|
||||
repos = data["editable"]["repo_setup"]["repos"]["value"]
|
||||
repos = data_utils.get_display_data_multi(self.columns, repos)
|
||||
repos = self._sort_data(parsed_args, repos)
|
||||
return self.columns, repos
|
||||
|
||||
|
||||
class ReleaseReposUpdate(ReleaseMixIn, base.BaseCommand):
|
||||
"""Update repos for a given release."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ReleaseReposUpdate, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'-f', '--file', action='store', required=True,
|
||||
help='Input yaml file with list of repositories')
|
||||
parser.add_argument(
|
||||
'id', type=int, help='Id of the {0}.'.format(self.entity_name))
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_attributes_metadata_by_id(parsed_args.id)
|
||||
new_repos = utils.parse_yaml_file(parsed_args.file)
|
||||
data["editable"]["repo_setup"]["repos"]["value"] = new_repos
|
||||
self.client.update_attributes_metadata_by_id(parsed_args.id, data)
|
||||
self.app.stdout.write(
|
||||
"Repositories for the release with "
|
||||
"id {rel_id} were set from {file}.\n".format(
|
||||
rel_id=parsed_args.id,
|
||||
file=parsed_args.file
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ReleaseComponentList(ReleaseMixIn, base.BaseListCommand):
|
||||
"""Show list of components for a given release."""
|
||||
|
||||
columns = ("name",
|
||||
"requires",
|
||||
"compatible",
|
||||
"incompatible",
|
||||
"default")
|
||||
|
||||
@property
|
||||
def default_sorting_by(self):
|
||||
return ['name']
|
||||
|
||||
@staticmethod
|
||||
def retrieve_predicates(statement):
|
||||
"""Retrieve predicates with respective 'items' components
|
||||
|
||||
:param statement: the dictionary to extract predicate values from
|
||||
:return: retrieval result as a string
|
||||
"""
|
||||
predicates = ('any_of', 'all_of', 'one_of', 'none_of')
|
||||
for predicate in predicates:
|
||||
if predicate in statement:
|
||||
result = ', '.join(statement[predicate].get('items'))
|
||||
return "{0} ({1})".format(predicate, result)
|
||||
raise ValueError('Predicates not found.')
|
||||
|
||||
@classmethod
|
||||
def retrieve_data(cls, value):
|
||||
"""Retrieve names of components or predicates from nested data
|
||||
|
||||
:param value: data to extract name or to retrieve predicates from
|
||||
:return: names of components or predicates as a string
|
||||
"""
|
||||
if isinstance(value, list):
|
||||
# get only "name" of components otherwise retrieve predicates
|
||||
return ', '.join([v['name'] if 'name' in v
|
||||
else cls.retrieve_predicates(v)
|
||||
for v in value])
|
||||
return value
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ReleaseComponentList, self).get_parser(prog_name)
|
||||
parser.add_argument('id', type=int,
|
||||
help='Id of the {0}.'.format(self.entity_name))
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_components_by_id(parsed_args.id)
|
||||
# some keys (columns) can be missed in origin data
|
||||
# then create them with respective '-' value
|
||||
data = [{k: self.retrieve_data(d.get(k, '-')) for k in self.columns}
|
||||
for d in data]
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
data = self._sort_data(parsed_args, data)
|
||||
return self.columns, data
|
@ -1,268 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 Vitalii Kulanov
|
||||
#
|
||||
# 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 os
|
||||
|
||||
from oslo_utils import fileutils
|
||||
import six
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
class RoleMixIn(object):
|
||||
entity_name = 'role'
|
||||
supported_file_formats = ('json', 'yaml')
|
||||
fields_mapper = (
|
||||
('env', 'clusters'),
|
||||
('release', 'releases')
|
||||
)
|
||||
|
||||
def parse_model(self, args):
|
||||
for param, role_class in self.fields_mapper:
|
||||
model_id = getattr(args, param)
|
||||
if model_id:
|
||||
return role_class, model_id
|
||||
|
||||
@staticmethod
|
||||
def get_file_path(directory, owner_type, owner_id, role_name, file_format):
|
||||
return os.path.join(os.path.abspath(directory),
|
||||
'{owner}_{id}'.format(owner=owner_type,
|
||||
id=owner_id),
|
||||
'{}.{}'.format(role_name, file_format))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseUploadCommand(RoleMixIn, base.BaseCommand):
|
||||
"""Base class for uploading metadata of a role."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def action(self):
|
||||
"""String with the name of the action."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def uploader(self):
|
||||
"""Callable for uploading data."""
|
||||
pass
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseUploadCommand, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment')
|
||||
parser.add_argument('-n',
|
||||
'--name',
|
||||
required=True,
|
||||
help='Name of role.')
|
||||
parser.add_argument('-f',
|
||||
'--format',
|
||||
required=True,
|
||||
choices=self.supported_file_formats,
|
||||
help='Format of serialized role description.')
|
||||
parser.add_argument('-d',
|
||||
'--directory',
|
||||
required=False,
|
||||
default=os.path.curdir,
|
||||
help='Source directory. Defaults to '
|
||||
'the current directory.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
params = {"owner_type": model,
|
||||
"owner_id": model_id,
|
||||
"role_name": parsed_args.name}
|
||||
|
||||
file_path = self.get_file_path(parsed_args.directory,
|
||||
model,
|
||||
model_id,
|
||||
parsed_args.name,
|
||||
parsed_args.format)
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as stream:
|
||||
data = data_utils.safe_load(parsed_args.format, stream)
|
||||
self.uploader(data, **params)
|
||||
except (OSError, IOError):
|
||||
msg = "Could not read description for role '{}' at {}".format(
|
||||
parsed_args.name, file_path)
|
||||
raise error.InvalidFileException(msg)
|
||||
|
||||
msg = ("Description of role '{role}' for {owner} with id {id} was "
|
||||
"{action}d from {file_path}\n".format(role=parsed_args.name,
|
||||
owner=model,
|
||||
id=model_id,
|
||||
action=self.action,
|
||||
file_path=file_path))
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class RoleList(RoleMixIn, base.BaseListCommand):
|
||||
"""Show list of all available roles for release or cluster."""
|
||||
|
||||
columns = ("name",
|
||||
"group",
|
||||
"conflicts",
|
||||
"description")
|
||||
|
||||
@property
|
||||
def default_sorting_by(self):
|
||||
return ['name']
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(RoleList, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
data = self.client.get_all(model, model_id)
|
||||
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
data = self._sort_data(parsed_args, data)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class RoleDownload(RoleMixIn, base.BaseCommand):
|
||||
"""Download full role description to file."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(RoleDownload, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment'
|
||||
)
|
||||
parser.add_argument('-n',
|
||||
'--name',
|
||||
required=True,
|
||||
help='Name of role.')
|
||||
parser.add_argument('-f',
|
||||
'--format',
|
||||
required=True,
|
||||
choices=self.supported_file_formats,
|
||||
help='Format of serialized role description.')
|
||||
parser.add_argument('-d',
|
||||
'--directory',
|
||||
required=False,
|
||||
default=os.path.curdir,
|
||||
help='Destination directory. Defaults to '
|
||||
'the current directory.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
file_path = self.get_file_path(parsed_args.directory,
|
||||
model,
|
||||
model_id,
|
||||
parsed_args.name,
|
||||
parsed_args.format)
|
||||
data = self.client.get_one(model,
|
||||
model_id,
|
||||
parsed_args.name)
|
||||
|
||||
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, data)
|
||||
except (OSError, IOError):
|
||||
msg = ("Could not store description data "
|
||||
"for role {} at {}".format(parsed_args.name, file_path))
|
||||
raise error.InvalidFileException(msg)
|
||||
|
||||
msg = ("Description data of role '{}' within {} id {} "
|
||||
"was stored in {}\n".format(parsed_args.name,
|
||||
model,
|
||||
model_id,
|
||||
file_path))
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class RoleUpdate(BaseUploadCommand):
|
||||
"""Update a role from file description."""
|
||||
|
||||
action = "update"
|
||||
|
||||
@property
|
||||
def uploader(self):
|
||||
return self.client.update
|
||||
|
||||
|
||||
class RoleCreate(BaseUploadCommand):
|
||||
"""Create a role from file description"""
|
||||
|
||||
action = "create"
|
||||
|
||||
@property
|
||||
def uploader(self):
|
||||
return self.client.create
|
||||
|
||||
|
||||
class RoleDelete(RoleMixIn, base.BaseCommand):
|
||||
"""Delete a role from release or cluster"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(RoleDelete, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment'
|
||||
)
|
||||
parser.add_argument('-n',
|
||||
'--name',
|
||||
required=True,
|
||||
help='Name of role.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
self.client.delete(model,
|
||||
model_id,
|
||||
parsed_args.name)
|
||||
|
||||
msg = "Role '{}' was deleted from {} with id {}\n".format(
|
||||
parsed_args.name, model, model_id)
|
||||
self.app.stdout.write(msg)
|
@ -1,203 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.cli import serializers
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
class SequenceMixIn(object):
|
||||
entity_name = 'sequence'
|
||||
|
||||
|
||||
class SequenceCreate(SequenceMixIn, base.show.ShowOne, base.BaseCommand):
|
||||
"""Create a new deployment sequence."""
|
||||
|
||||
columns = ("id", "release_id", "name")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SequenceCreate, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
"-r", "--release",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Release object id, sequence will be linked to."
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--name',
|
||||
required=True,
|
||||
help='The unique name for sequence.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t', '--graph-type',
|
||||
dest='graph_types',
|
||||
nargs='+',
|
||||
required=True,
|
||||
help='Graph types, which will be included to sequence.\n'
|
||||
'Note: Order is important.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
new_sequence = self.client.create(
|
||||
args.release, args.name, args.graph_types
|
||||
)
|
||||
self.app.stdout.write("Sequence was successfully created:\n")
|
||||
data = data_utils.get_display_data_single(self.columns, new_sequence)
|
||||
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class SequenceUpload(SequenceMixIn, base.show.ShowOne, base.BaseCommand):
|
||||
"""Upload a new deployment sequence."""
|
||||
|
||||
columns = ("id", "release_id", "name")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SequenceUpload, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
"-r", "--release",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Release object id, sequence will be linked to."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
required=True,
|
||||
help='YAML file which contains deployment sequence properties.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
serializer = serializers.FileFormatBasedSerializer()
|
||||
new_sequence = self.client.upload(
|
||||
args.release, serializer.read_from_file(args.file)
|
||||
)
|
||||
self.app.stdout.write("Sequence was successfully created:\n")
|
||||
data = data_utils.get_display_data_single(self.columns, new_sequence)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class SequenceDownload(SequenceMixIn, base.BaseCommand):
|
||||
"""Download deployment sequence data."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SequenceDownload, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
"id",
|
||||
type=int,
|
||||
help="Sequence ID."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
help='The file path where data will be saved.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
data = self.client.download(args.id)
|
||||
if args.file:
|
||||
serializer = serializers.FileFormatBasedSerializer()
|
||||
serializer.write_to_file(args.file, data)
|
||||
else:
|
||||
serializer = serializers.Serializer("yaml")
|
||||
serializer.write_to_file(self.app.stdout, data)
|
||||
|
||||
|
||||
class SequenceUpdate(SequenceMixIn, base.BaseShowCommand):
|
||||
"""Update existing sequence."""
|
||||
|
||||
columns = ("id", "name")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SequenceUpdate, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'-n', '--name',
|
||||
required=False,
|
||||
help='The unique name for sequence.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t', '--graph-type',
|
||||
dest='graph_types',
|
||||
nargs='+',
|
||||
required=False,
|
||||
help='Graph types, which will be included to sequence.\n'
|
||||
'Note: Order is important.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
sequence = self.client.update(
|
||||
args.id, name=args.name, graph_types=args.graph_types
|
||||
)
|
||||
|
||||
if sequence:
|
||||
self.app.stdout.write("Sequence was successfully updated:\n")
|
||||
data = data_utils.get_display_data_single(self.columns, sequence)
|
||||
return self.columns, data
|
||||
else:
|
||||
self.app.stdout.write("Nothing to update.\n")
|
||||
|
||||
|
||||
class SequenceDelete(SequenceMixIn, base.BaseDeleteCommand):
|
||||
"""Delete existing sequence."""
|
||||
|
||||
|
||||
class SequenceShow(SequenceMixIn, base.BaseShowCommand):
|
||||
"""Display information about sequence."""
|
||||
columns = ("id", "release_id", "name", "graphs")
|
||||
|
||||
|
||||
class SequenceList(SequenceMixIn, base.BaseListCommand):
|
||||
"""Show list of all existing sequences."""
|
||||
columns = ("id", "release_id", "name")
|
||||
filters = {'release': 'release', 'cluster': 'env'}
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SequenceList, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument(
|
||||
'-r', '--release',
|
||||
type=int,
|
||||
help='The Release object ID.'
|
||||
)
|
||||
group.add_argument(
|
||||
'-e', '--env',
|
||||
type=int,
|
||||
help='The environment object id.'
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
class SequenceExecute(SequenceMixIn, base.BaseTasksExecuteCommand):
|
||||
"""Executes sequence on specified environment."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SequenceExecute, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'id',
|
||||
type=int,
|
||||
help='Id of the Sequence.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def get_options(self, parsed_args):
|
||||
return {
|
||||
'sequence_id': parsed_args.id,
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 Vitalii Kulanov
|
||||
#
|
||||
# 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 argparse
|
||||
import os
|
||||
|
||||
from oslo_utils import fileutils
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class SnapshotMixIn(object):
|
||||
|
||||
entity_name = 'snapshot'
|
||||
supported_file_formats = ('json', 'yaml')
|
||||
|
||||
@staticmethod
|
||||
def config_file(file_path):
|
||||
if not utils.file_exists(file_path):
|
||||
raise argparse.ArgumentTypeError(
|
||||
'File "{0}" does not exist'.format(file_path))
|
||||
return file_path
|
||||
|
||||
@staticmethod
|
||||
def get_config_path(directory, file_format):
|
||||
return os.path.join(os.path.abspath(directory),
|
||||
'snapshot_conf.{}'.format(file_format))
|
||||
|
||||
|
||||
class SnapshotGenerate(SnapshotMixIn, base.BaseCommand):
|
||||
"""Generate diagnostic snapshot."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SnapshotGenerate, self).get_parser(prog_name)
|
||||
parser.add_argument('-c',
|
||||
'--config',
|
||||
required=False,
|
||||
type=self.config_file,
|
||||
help='Configuration file.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
file_path = parsed_args.config
|
||||
|
||||
config = dict()
|
||||
if file_path:
|
||||
file_format = os.path.splitext(file_path)[1].lstrip('.')
|
||||
try:
|
||||
with open(file_path, 'r') as stream:
|
||||
config = data_utils.safe_load(file_format, stream)
|
||||
except (OSError, IOError):
|
||||
msg = 'Could not read configuration at {}.'
|
||||
raise error.InvalidFileException(msg.format(file_path))
|
||||
|
||||
result = self.client.create_snapshot(config)
|
||||
|
||||
msg = "Diagnostic snapshot generation task with id {id} was started\n"
|
||||
self.app.stdout.write(msg.format(id=result.id))
|
||||
|
||||
|
||||
class SnapshotConfigGetDefault(SnapshotMixIn, base.BaseCommand):
|
||||
"""Download default config to generate custom diagnostic snapshot."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SnapshotConfigGetDefault, self).get_parser(prog_name)
|
||||
parser.add_argument('-f',
|
||||
'--format',
|
||||
required=True,
|
||||
choices=self.supported_file_formats,
|
||||
help='Format of serialized diagnostic snapshot '
|
||||
'configuration data.')
|
||||
parser.add_argument('-d',
|
||||
'--directory',
|
||||
required=False,
|
||||
default=os.path.curdir,
|
||||
help='Destination directory. Defaults to '
|
||||
'the current directory.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
file_path = self.get_config_path(parsed_args.directory,
|
||||
parsed_args.format)
|
||||
config = self.client.get_default_config()
|
||||
|
||||
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, config)
|
||||
except (OSError, IOError):
|
||||
msg = 'Could not store configuration at {}.'
|
||||
raise error.InvalidFileException(msg.format(file_path))
|
||||
|
||||
msg = "Configuration was stored in {path}\n"
|
||||
self.app.stdout.write(msg.format(path=file_path))
|
||||
|
||||
|
||||
class SnapshotGetLink(SnapshotMixIn, base.BaseShowCommand):
|
||||
"""Show link to download diagnostic snapshot."""
|
||||
|
||||
columns = ('status',
|
||||
'link')
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
data = self.client.get_by_id(parsed_args.id)
|
||||
if data['name'] != 'dump':
|
||||
msg = "Task with id {0} is not a snapshot generation task"
|
||||
raise error.ActionException(msg.format(data['id']))
|
||||
if data['status'] != 'ready':
|
||||
data['link'] = None
|
||||
else:
|
||||
data['link'] = self.client.connection.root + data['message']
|
||||
|
||||
data = data_utils.get_display_data_single(self.columns, data)
|
||||
return self.columns, data
|
@ -1,265 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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 os
|
||||
|
||||
from oslo_utils import fileutils
|
||||
import six
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
class TagMixIn(object):
|
||||
entity_name = 'tag'
|
||||
supported_file_formats = ('json', 'yaml')
|
||||
fields_mapper = (
|
||||
('env', 'clusters'),
|
||||
('release', 'releases')
|
||||
)
|
||||
|
||||
def parse_model(self, args):
|
||||
for param, tag_class in self.fields_mapper:
|
||||
model_id = getattr(args, param)
|
||||
if model_id:
|
||||
return tag_class, model_id
|
||||
|
||||
@staticmethod
|
||||
def get_file_path(directory, owner_type, owner_id, tag_name, file_format):
|
||||
return os.path.join(os.path.abspath(directory),
|
||||
'{owner}_{id}'.format(owner=owner_type,
|
||||
id=owner_id),
|
||||
'{}.{}'.format(tag_name, file_format))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseUploadCommand(TagMixIn, base.BaseCommand):
|
||||
"""Base class for uploading metadata of a tag."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def action(self):
|
||||
"""String with the name of the action."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def uploader(self):
|
||||
"""Callable for uploading data."""
|
||||
pass
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseUploadCommand, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment')
|
||||
parser.add_argument('-n',
|
||||
'--name',
|
||||
required=True,
|
||||
help='Name of tag.')
|
||||
parser.add_argument('-f',
|
||||
'--format',
|
||||
required=True,
|
||||
choices=self.supported_file_formats,
|
||||
help='Format of serialized tag description.')
|
||||
parser.add_argument('-d',
|
||||
'--directory',
|
||||
required=False,
|
||||
default=os.path.curdir,
|
||||
help='Source directory. Defaults to '
|
||||
'the current directory.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
params = {"owner_type": model,
|
||||
"owner_id": model_id,
|
||||
"tag_name": parsed_args.name}
|
||||
|
||||
file_path = self.get_file_path(parsed_args.directory,
|
||||
model,
|
||||
model_id,
|
||||
parsed_args.name,
|
||||
parsed_args.format)
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as stream:
|
||||
data = data_utils.safe_load(parsed_args.format, stream)
|
||||
self.uploader(data, **params)
|
||||
except (OSError, IOError):
|
||||
msg = "Could not read description for tag '{}' at {}".format(
|
||||
parsed_args.name, file_path)
|
||||
raise error.InvalidFileException(msg)
|
||||
|
||||
msg = ("Description of tag '{tag}' for {owner} with id {id} was "
|
||||
"{action}d from {file_path}\n".format(tag=parsed_args.name,
|
||||
owner=model,
|
||||
id=model_id,
|
||||
action=self.action,
|
||||
file_path=file_path))
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class TagList(TagMixIn, base.BaseListCommand):
|
||||
"""Show list of all available tags for release or cluster."""
|
||||
|
||||
columns = ("name",
|
||||
"group",
|
||||
"conflicts",
|
||||
"description")
|
||||
|
||||
@property
|
||||
def default_sorting_by(self):
|
||||
return ['name']
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TagList, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
data = self.client.get_all(model, model_id)
|
||||
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
data = self._sort_data(parsed_args, data)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class TagDownload(TagMixIn, base.BaseCommand):
|
||||
"""Download full tag description to file."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TagDownload, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment')
|
||||
parser.add_argument('-n',
|
||||
'--name',
|
||||
required=True,
|
||||
help='Name of tag.')
|
||||
parser.add_argument('-f',
|
||||
'--format',
|
||||
required=True,
|
||||
choices=self.supported_file_formats,
|
||||
help='Format of serialized tag description.')
|
||||
parser.add_argument('-d',
|
||||
'--directory',
|
||||
required=False,
|
||||
default=os.path.curdir,
|
||||
help='Destination directory. Defaults to '
|
||||
'the current directory.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
file_path = self.get_file_path(parsed_args.directory,
|
||||
model,
|
||||
model_id,
|
||||
parsed_args.name,
|
||||
parsed_args.format)
|
||||
data = self.client.get_tag(model,
|
||||
model_id,
|
||||
parsed_args.name)
|
||||
|
||||
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, data)
|
||||
except (OSError, IOError):
|
||||
msg = ("Could not store description data "
|
||||
"for tag {} at {}".format(parsed_args.name, file_path))
|
||||
raise error.InvalidFileException(msg)
|
||||
|
||||
msg = ("Description data of tag '{}' within {} id {} "
|
||||
"was stored in {}\n".format(parsed_args.name,
|
||||
model,
|
||||
model_id,
|
||||
file_path))
|
||||
self.app.stdout.write(msg)
|
||||
|
||||
|
||||
class TagUpdate(BaseUploadCommand):
|
||||
"""Update a tag from file description."""
|
||||
|
||||
action = "update"
|
||||
|
||||
@property
|
||||
def uploader(self):
|
||||
return self.client.update
|
||||
|
||||
|
||||
class TagCreate(BaseUploadCommand):
|
||||
"""Create a tag from file description"""
|
||||
|
||||
action = "create"
|
||||
|
||||
@property
|
||||
def uploader(self):
|
||||
return self.client.create
|
||||
|
||||
|
||||
class TagDelete(TagMixIn, base.BaseCommand):
|
||||
"""Delete a tag from release or cluster"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TagDelete, self).get_parser(prog_name)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-r',
|
||||
'--release',
|
||||
type=int,
|
||||
help='Id of the release')
|
||||
group.add_argument('-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Id of the environment')
|
||||
parser.add_argument('-n',
|
||||
'--name',
|
||||
required=True,
|
||||
help='Name of tag.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
model, model_id = self.parse_model(parsed_args)
|
||||
self.client.delete(model,
|
||||
model_id,
|
||||
parsed_args.name)
|
||||
|
||||
msg = "Tag '{}' was deleted from {} with id {}\n".format(
|
||||
parsed_args.name, model, model_id)
|
||||
self.app.stdout.write(msg)
|
@ -1,309 +0,0 @@
|
||||
# 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 os
|
||||
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.commands import base
|
||||
from fuelclient.common import data_utils
|
||||
|
||||
|
||||
class TaskMixIn(object):
|
||||
entity_name = 'task'
|
||||
|
||||
@staticmethod
|
||||
def add_file_arg(parser):
|
||||
parser.add_argument(
|
||||
'-f',
|
||||
'--file',
|
||||
required=False,
|
||||
type=str,
|
||||
help='Output file in YAML format.'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def write_info_to_file(cls, info_type, data, transaction_id,
|
||||
serializer=None, file_path=None):
|
||||
"""Write additional info to the given path.
|
||||
|
||||
:param info_type: deployment_info | cluster_settings |
|
||||
network_configuration
|
||||
:type info_type: str
|
||||
:param data: data
|
||||
:type data: list of dict
|
||||
:param serializer: serializer
|
||||
:param transaction_id: Transaction ID
|
||||
:type transaction_id: str or int
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:return: path to resulting file
|
||||
:rtype: str
|
||||
"""
|
||||
serializer = serializer or Serializer()
|
||||
if file_path:
|
||||
return serializer.write_to_full_path(
|
||||
file_path,
|
||||
data
|
||||
)
|
||||
else:
|
||||
return serializer.write_to_path(
|
||||
cls.get_default_info_path(info_type, transaction_id),
|
||||
data
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_default_info_path(info_type, transaction_id):
|
||||
"""Generate default path for task additional info e.g. deployment info
|
||||
|
||||
:param info_type: deployment_info | cluster_settings |
|
||||
network_configuration
|
||||
:type info_type: str
|
||||
:param transaction_id: Transaction ID
|
||||
:type transaction_id: str or int
|
||||
:return: path
|
||||
:rtype: str
|
||||
"""
|
||||
return os.path.join(
|
||||
os.path.abspath(os.curdir),
|
||||
"{info_type}_{transaction_id}".format(
|
||||
info_type=info_type,
|
||||
transaction_id=transaction_id)
|
||||
)
|
||||
|
||||
def download_info_to_file(self, transaction_id, info_type, file_path):
|
||||
"""Get and save to path for task additional info e.g. deployment info
|
||||
|
||||
:param transaction_id: Transaction ID
|
||||
:type transaction_id: str or int
|
||||
:param info_type: deployment_info | cluster_settings |
|
||||
network_configuration
|
||||
:type info_type: str
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:return: path
|
||||
:rtype: str
|
||||
"""
|
||||
data = self.client.download(transaction_id=transaction_id)
|
||||
return self.write_info_to_file(
|
||||
info_type=info_type,
|
||||
data=data,
|
||||
transaction_id=transaction_id,
|
||||
serializer=Serializer(),
|
||||
file_path=file_path)
|
||||
|
||||
|
||||
class TaskInfoFileMixIn(TaskMixIn):
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TaskInfoFileMixIn, self).get_parser(
|
||||
prog_name)
|
||||
parser.add_argument('id', type=int, help='Id of the Task.')
|
||||
self.add_file_arg(parser)
|
||||
return parser
|
||||
|
||||
def download_info(self, parsed_args):
|
||||
data_file_path = self.download_info_to_file(
|
||||
transaction_id=parsed_args.id,
|
||||
info_type=self.info_type,
|
||||
file_path=parsed_args.file)
|
||||
|
||||
return data_file_path
|
||||
|
||||
|
||||
class TaskList(TaskMixIn, base.BaseListCommand):
|
||||
"""Show list of all available tasks."""
|
||||
columns = ('id',
|
||||
'status',
|
||||
'name',
|
||||
'graph_type',
|
||||
'cluster',
|
||||
'result',
|
||||
'dry_run',
|
||||
'progress')
|
||||
filters = {'cluster_id': 'env',
|
||||
'statuses': 'statuses',
|
||||
'transaction_types': 'names'}
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TaskList, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--env',
|
||||
type=int,
|
||||
help='Show list of tasks that belong to specified environment')
|
||||
|
||||
parser.add_argument(
|
||||
'-t',
|
||||
'--statuses',
|
||||
type=str,
|
||||
choices=['pending', 'error', 'ready', 'running'],
|
||||
nargs='+',
|
||||
help='Show list of tasks with specified statuses')
|
||||
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--names',
|
||||
type=str,
|
||||
nargs='+',
|
||||
help='Show list of tasks with specified names')
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
class TaskShow(TaskMixIn, base.BaseShowCommand):
|
||||
"""Show info about task with given id."""
|
||||
columns = ('id',
|
||||
'uuid',
|
||||
'status',
|
||||
'name',
|
||||
'graph_type',
|
||||
'cluster',
|
||||
'result',
|
||||
'dry_run',
|
||||
'progress',
|
||||
'message')
|
||||
|
||||
|
||||
class TaskDelete(TaskMixIn, base.BaseDeleteCommand):
|
||||
"""Delete task with given id."""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TaskDelete, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument('-f',
|
||||
'--force',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Force deletion of a task without '
|
||||
'considering its state.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.client.delete_by_id(parsed_args.id, parsed_args.force)
|
||||
|
||||
msg = 'Task with id {ent_id} was deleted\n'
|
||||
self.app.stdout.write(msg.format(ent_id=parsed_args.id))
|
||||
|
||||
|
||||
class TaskHistoryShow(TaskMixIn, base.BaseListCommand):
|
||||
"""Show deployment history about task with given ID."""
|
||||
|
||||
entity_name = 'deployment_history'
|
||||
|
||||
columns = ()
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(TaskHistoryShow, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument('id', type=int, help='Id of the Task')
|
||||
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--nodes',
|
||||
type=str,
|
||||
nargs='+',
|
||||
help='Show deployment history for specific nodes')
|
||||
|
||||
parser.add_argument(
|
||||
'-t',
|
||||
'--statuses',
|
||||
type=str,
|
||||
choices=['pending', 'error', 'ready', 'running', 'skipped'],
|
||||
nargs='+',
|
||||
help='Show deployment history for specific statuses')
|
||||
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--tasks-names',
|
||||
type=str,
|
||||
nargs='+',
|
||||
help='Show deployment history for specific deployment tasks names')
|
||||
|
||||
parser.add_argument(
|
||||
'-p',
|
||||
'--show-parameters',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Show deployment tasks parameters')
|
||||
parser.add_argument(
|
||||
'--include-summary',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Show deployment tasks summary')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
# print parser
|
||||
show_parameters = parsed_args.show_parameters
|
||||
include_summary = parsed_args.include_summary
|
||||
data = self.client.get_all(
|
||||
transaction_id=parsed_args.id,
|
||||
nodes=parsed_args.nodes,
|
||||
statuses=parsed_args.statuses,
|
||||
tasks_names=parsed_args.tasks_names,
|
||||
include_summary=include_summary,
|
||||
show_parameters=show_parameters
|
||||
)
|
||||
if show_parameters:
|
||||
self.columns = self.client.tasks_records_keys
|
||||
else:
|
||||
self.columns = self.client.history_records_keys
|
||||
if include_summary:
|
||||
self.columns += ('summary',)
|
||||
data = data_utils.get_display_data_multi(self.columns, data)
|
||||
return self.columns, data
|
||||
|
||||
|
||||
class TaskNetworkConfigurationDownload(TaskInfoFileMixIn, base.BaseCommand):
|
||||
"""Save task network configuration to a file."""
|
||||
|
||||
entity_name = 'network-configuration'
|
||||
info_type = 'network_configuration'
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.app.stdout.write(
|
||||
"Network configuration for task with id={0}"
|
||||
" downloaded to {1}\n".format(parsed_args.id,
|
||||
self.download_info(parsed_args))
|
||||
)
|
||||
|
||||
|
||||
class TaskDeploymentInfoDownload(TaskInfoFileMixIn, base.BaseCommand):
|
||||
"""Save task deployment info to a file."""
|
||||
|
||||
entity_name = 'deployment-info'
|
||||
info_type = 'deployment_info'
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.app.stdout.write(
|
||||
"Deployment info for task with id={0}"
|
||||
" downloaded to {1}\n".format(parsed_args.id,
|
||||
self.download_info(parsed_args))
|
||||
)
|
||||
|
||||
|
||||
class TaskClusterSettingsDownload(TaskInfoFileMixIn, base.BaseCommand):
|
||||
"""Save task settings to a file."""
|
||||
|
||||
entity_name = 'cluster-settings'
|
||||
info_type = 'cluster_settings'
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.app.stdout.write(
|
||||
"Cluster settings for task with id={0}"
|
||||
" downloaded to {1}\n".format(parsed_args.id,
|
||||
self.download_info(parsed_args))
|
||||
)
|
@ -1,182 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.commands import base
|
||||
|
||||
|
||||
class VipMixIn(object):
|
||||
entity_name = 'vip'
|
||||
|
||||
@staticmethod
|
||||
def add_env_id_arg(parser):
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--env',
|
||||
type=int,
|
||||
required=True,
|
||||
help='Environment identifier'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_network_id_arg(parser):
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--network",
|
||||
type=int,
|
||||
default=None,
|
||||
required=False,
|
||||
help="Network identifier"
|
||||
)
|
||||
|
||||
|
||||
class VipDownload(VipMixIn, base.BaseCommand):
|
||||
"""Download VIPs configuration."""
|
||||
|
||||
@staticmethod
|
||||
def add_ip_address_id_arg(parser):
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--ip-address-id",
|
||||
type=int,
|
||||
default=None,
|
||||
required=False,
|
||||
help="IP address entity identifier"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_network_role_arg(parser):
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--network-role",
|
||||
type=str,
|
||||
default=None,
|
||||
required=False,
|
||||
help="Network role string"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_file_arg(parser):
|
||||
parser.add_argument(
|
||||
'-f',
|
||||
'--file',
|
||||
type=str,
|
||||
required=False,
|
||||
default=None,
|
||||
help='YAML file that contains openstack configuration.'
|
||||
)
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(VipDownload, self).get_parser(prog_name)
|
||||
self.add_env_id_arg(parser)
|
||||
self.add_ip_address_id_arg(parser)
|
||||
self.add_file_arg(parser)
|
||||
self.add_network_id_arg(parser)
|
||||
self.add_network_role_arg(parser)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
vips_data_file_path = self.client.download(
|
||||
env_id=args.env,
|
||||
ip_addr_id=args.ip_address_id,
|
||||
network_id=args.network,
|
||||
network_role=args.network_role,
|
||||
file_path=args.file
|
||||
)
|
||||
|
||||
self.app.stdout.write(
|
||||
"VIP configuration for environment with id={0}"
|
||||
" downloaded to {1}".format(args.env, vips_data_file_path)
|
||||
)
|
||||
|
||||
|
||||
class VipUpload(VipMixIn, base.BaseCommand):
|
||||
"""Upload new VIPs configuration from file."""
|
||||
|
||||
@staticmethod
|
||||
def add_file_arg(parser):
|
||||
parser.add_argument(
|
||||
'-f',
|
||||
'--file',
|
||||
required=True,
|
||||
type=str,
|
||||
help='YAML file that contains openstack configuration.'
|
||||
)
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(VipUpload, self).get_parser(prog_name)
|
||||
self.add_env_id_arg(parser)
|
||||
self.add_file_arg(parser)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
self.client.upload(env_id=args.env, file_path=args.file)
|
||||
self.app.stdout.write("VIP configuration uploaded.")
|
||||
|
||||
|
||||
class VipCreate(VipMixIn, base.BaseCommand):
|
||||
"""Create VIP"""
|
||||
|
||||
@staticmethod
|
||||
def add_vip_name_arg(parser):
|
||||
parser.add_argument(
|
||||
'-N',
|
||||
'--name',
|
||||
required=True,
|
||||
type=str,
|
||||
help="VIP name"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_ip_addr_arg(parser):
|
||||
parser.add_argument(
|
||||
'-a',
|
||||
'--address',
|
||||
required=True,
|
||||
type=str,
|
||||
help="IP-address for the VIP"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_vip_namespace_arg(parser):
|
||||
parser.add_argument(
|
||||
'--namespace',
|
||||
required=False,
|
||||
type=str,
|
||||
help="VIP namespace"
|
||||
)
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(VipCreate, self).get_parser(prog_name)
|
||||
self.add_env_id_arg(parser)
|
||||
self.add_network_id_arg(parser)
|
||||
self.add_vip_name_arg(parser)
|
||||
self.add_ip_addr_arg(parser)
|
||||
self.add_vip_namespace_arg(parser)
|
||||
return parser
|
||||
|
||||
def take_action(self, args):
|
||||
vip_kwargs = {
|
||||
"env_id": args.env,
|
||||
"ip_addr": args.address,
|
||||
"network": args.network,
|
||||
"vip_name": args.name,
|
||||
}
|
||||
if args.namespace is not None:
|
||||
vip_kwargs['vip_namespace'] = args.namespace
|
||||
|
||||
self.client.create(**vip_kwargs)
|
||||
|
||||
self.app.stdout.write("VIP has been created.")
|
@ -1,81 +0,0 @@
|
||||
# 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 json
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
def get_display_data_single(fields, data, missing_field_value=None):
|
||||
"""Performs slicing of data by set of given fields
|
||||
|
||||
:param fields: Iterable containing names of fields to be retrieved
|
||||
from data
|
||||
:param data: Collection of JSON objects representing some
|
||||
external entities
|
||||
:param missing_field_value: the value will be used for all missing fields
|
||||
|
||||
:return: list containing the collection of values of the
|
||||
supplied attributes.
|
||||
|
||||
"""
|
||||
return [data.get(field, missing_field_value) for field in fields]
|
||||
|
||||
|
||||
def get_display_data_multi(fields, data):
|
||||
"""Performs slice of data by set of given fields for multiple objects."""
|
||||
|
||||
return [get_display_data_single(fields, elem) for elem in data]
|
||||
|
||||
|
||||
def safe_load(data_format, stream):
|
||||
loaders = {'json': utils.safe_deserialize(json.load),
|
||||
'yaml': utils.safe_deserialize(yaml.safe_load)}
|
||||
|
||||
if data_format not in loaders:
|
||||
raise ValueError('Unsupported data format.')
|
||||
|
||||
loader = loaders[data_format]
|
||||
return loader(stream)
|
||||
|
||||
|
||||
def safe_dump(data_format, stream, data):
|
||||
# The reason these dumpers are assigned to individual variables is
|
||||
# making PEP8 check happy.
|
||||
yaml_dumper = lambda data, stream: yaml.safe_dump(data,
|
||||
stream,
|
||||
default_flow_style=False)
|
||||
json_dumper = lambda data, stream: json.dump(data, stream, indent=4)
|
||||
dumpers = {'json': json_dumper,
|
||||
'yaml': yaml_dumper}
|
||||
|
||||
if data_format not in dumpers:
|
||||
raise ValueError('Unsupported data format.')
|
||||
|
||||
dumper = dumpers[data_format]
|
||||
dumper(data, stream)
|
||||
|
||||
|
||||
def read_from_file(file_path):
|
||||
data_format = os.path.splitext(file_path)[1].lstrip('.')
|
||||
with open(file_path, 'r') as stream:
|
||||
return safe_load(data_format, stream)
|
||||
|
||||
|
||||
def write_to_file(file_path, data):
|
||||
data_format = os.path.splitext(file_path)[1].lstrip('.')
|
||||
with open(file_path, 'w') as stream:
|
||||
safe_dump(data_format, stream, data)
|
@ -1,34 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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.
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
def Enum(*values, **kwargs):
|
||||
names = kwargs.get('names')
|
||||
if names:
|
||||
return namedtuple('Enum', names)(*values)
|
||||
return namedtuple('Enum', values)(*values)
|
||||
|
||||
|
||||
SERIALIZATION_FORMAT_FLAG = 'serialization_format'
|
||||
|
||||
TASK_STATUSES = Enum(
|
||||
'error',
|
||||
'pending',
|
||||
'ready',
|
||||
'running'
|
||||
)
|
@ -1,18 +0,0 @@
|
||||
# Connection settings
|
||||
SERVER_ADDRESS: "127.0.0.1"
|
||||
SERVER_PORT: "8000"
|
||||
OS_USERNAME:
|
||||
OS_PASSWORD:
|
||||
# There's a need to provide default value for
|
||||
# tenant name until fuel-library is updated.
|
||||
# After that, it will be removed.
|
||||
OS_TENANT_NAME: "admin"
|
||||
HTTP_PROXY: null
|
||||
HTTP_TIMEOUT: 10
|
||||
|
||||
# Performance tests settings
|
||||
PERFORMANCE_PROFILING_TESTS: 0
|
||||
PERF_TESTS_PATHS:
|
||||
perf_tests_base: "/tmp/fuelclient_performance_tests/tests/"
|
||||
last_performance_test: "/tmp/fuelclient_performance_tests/tests/last/"
|
||||
perf_tests_results: "/tmp/fuelclient_performance_tests/results/"
|
@ -1,198 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2013-2014 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 os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from fuelclient.cli import error
|
||||
|
||||
|
||||
_SETTINGS = None
|
||||
|
||||
# Format: old parameter: new parameter or None
|
||||
DEPRECATION_TABLE = {'LISTEN_PORT': 'SERVER_PORT',
|
||||
'KEYSTONE_USER': 'OS_USERNAME',
|
||||
'KEYSTONE_PASS': 'OS_PASSWORD'}
|
||||
|
||||
# Format: parameter: fallback parameter
|
||||
FALLBACK_TABLE = {DEPRECATION_TABLE[p]: p for p in DEPRECATION_TABLE}
|
||||
|
||||
|
||||
class FuelClientSettings(object):
|
||||
"""Represents a model of Fuel Clients settings
|
||||
|
||||
Default settings are saved to $XDG_CONFIG_HOME/fuel/fuel_client.yaml on
|
||||
the first run. If $XDG_CONFIG_HOME is not set, ~/.config/ directory is
|
||||
used by default.
|
||||
|
||||
Custom settings may be stored in any YAML-formatted file the path to
|
||||
which should be supplied via the $FUELCLIENT_CUSTOM_SETTINGS environment
|
||||
variable. Custom settings override default ones.
|
||||
|
||||
Top level values may also be set as environment variables, e.g.
|
||||
export SERVER_PORT=8080. These values have the highest priority.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
settings_files = []
|
||||
|
||||
user_conf_dir = os.getenv('XDG_CONFIG_HOME',
|
||||
os.path.expanduser('~/.config/'))
|
||||
|
||||
# Look up for a default file distributed with the source code
|
||||
default_settings = pkg_resources.resource_filename('fuelclient',
|
||||
'fuel_client.yaml')
|
||||
|
||||
self.user_settings = os.path.join(user_conf_dir, 'fuel',
|
||||
'fuel_client.yaml')
|
||||
custom_settings = os.getenv('FUELCLIENT_CUSTOM_SETTINGS')
|
||||
|
||||
if not os.path.exists(self.user_settings) and not custom_settings:
|
||||
self.populate_default_settings(default_settings,
|
||||
self.user_settings)
|
||||
six.print_('Settings for Fuel Client have been saved to {0}.\n'
|
||||
'Consider changing default values to the ones which '
|
||||
'are appropriate for you.'.format(self.user_settings))
|
||||
|
||||
self._add_file_if_exists(default_settings, settings_files)
|
||||
self._add_file_if_exists(self.user_settings, settings_files)
|
||||
|
||||
# Add a custom settings file specified by user
|
||||
self._add_file_if_exists(custom_settings, settings_files)
|
||||
|
||||
self.config = {}
|
||||
for sf in settings_files:
|
||||
try:
|
||||
self._update_from_file(sf)
|
||||
except Exception as e:
|
||||
msg = ('Error while reading config file '
|
||||
'%(file)s: %(err)s') % {'file': sf, 'err': str(e)}
|
||||
|
||||
raise error.SettingsException(msg)
|
||||
|
||||
self._update_from_env()
|
||||
self._check_deprecated()
|
||||
|
||||
def _add_file_if_exists(self, path_to_file, file_list):
|
||||
if path_to_file and os.access(path_to_file, os.R_OK):
|
||||
file_list.append(path_to_file)
|
||||
|
||||
def _update_from_file(self, path):
|
||||
with open(path, 'r') as custom_config:
|
||||
self.config.update(
|
||||
yaml.load(custom_config.read())
|
||||
)
|
||||
|
||||
def _update_from_env(self):
|
||||
for k in self.config:
|
||||
if k in os.environ:
|
||||
self.config[k] = os.environ[k]
|
||||
|
||||
def _print_deprecation_warning(self, old_option, new_option=None):
|
||||
"""Print deprecation warning for an option."""
|
||||
|
||||
deprecation_tpl = ('DEPRECATION WARNING: {} parameter was '
|
||||
'deprecated and will not be supported in the next '
|
||||
'version of python-fuelclient.')
|
||||
replace_tpl = ' Please replace this parameter with {}'
|
||||
|
||||
deprecation = deprecation_tpl.format(old_option)
|
||||
replace = '' if new_option is None else replace_tpl.format(new_option)
|
||||
|
||||
six.print_(deprecation, end='', file=sys.stderr)
|
||||
six.print_(replace, file=sys.stderr)
|
||||
|
||||
def _check_deprecated(self):
|
||||
"""Looks for deprecated options in user's configuration."""
|
||||
|
||||
dep_opts = [opt for opt in self.config if opt in DEPRECATION_TABLE]
|
||||
|
||||
for opt in dep_opts:
|
||||
|
||||
new_opt = DEPRECATION_TABLE.get(opt)
|
||||
|
||||
# Clean up new option if it was not set by a user
|
||||
# Produce a warning, if both old and new options are set.
|
||||
if self.config.get(new_opt) is None:
|
||||
self.config.pop(new_opt, None)
|
||||
else:
|
||||
six.print_('WARNING: configuration contains both {old} and '
|
||||
'{new} options set. Since {old} was deprecated, '
|
||||
'only the value of {new} '
|
||||
'will be used.'.format(old=opt, new=new_opt),
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
self._print_deprecation_warning(opt, new_opt)
|
||||
|
||||
def populate_default_settings(self, source, destination):
|
||||
"""Puts default configuration file to a user's home directory."""
|
||||
|
||||
try:
|
||||
dst_dir = os.path.dirname(destination)
|
||||
|
||||
if not os.path.exists(dst_dir):
|
||||
os.makedirs(dst_dir, 0o700)
|
||||
|
||||
shutil.copy(source, destination)
|
||||
os.chmod(destination, 0o600)
|
||||
except (IOError, OSError):
|
||||
msg = ('Could not save settings to {0}. Please make sure the '
|
||||
'directory is writable')
|
||||
raise error.SettingsException(msg.format(dst_dir))
|
||||
|
||||
def update_from_command_line_options(self, options):
|
||||
"""Update parameters from valid command line options."""
|
||||
|
||||
for param in self.config:
|
||||
opt_name = param.lower()
|
||||
|
||||
value = getattr(options, opt_name, None)
|
||||
if value is not None:
|
||||
self.config[param] = value
|
||||
|
||||
def dump(self):
|
||||
return yaml.dump(self.config)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.config:
|
||||
return self.config[name]
|
||||
|
||||
if name in FALLBACK_TABLE:
|
||||
return self.config[FALLBACK_TABLE[name]]
|
||||
|
||||
raise error.SettingsException('Value for {0} option is not '
|
||||
'configured'.format(name))
|
||||
|
||||
def __repr__(self):
|
||||
return '<settings object>'
|
||||
|
||||
|
||||
def _init_settings():
|
||||
global _SETTINGS
|
||||
_SETTINGS = FuelClientSettings()
|
||||
|
||||
|
||||
def get_settings():
|
||||
if _SETTINGS is None:
|
||||
_init_settings()
|
||||
|
||||
return _SETTINGS
|
@ -1,21 +0,0 @@
|
||||
# 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.
|
||||
|
||||
|
||||
def setup_hook(config):
|
||||
import pbr
|
||||
import pbr.packaging
|
||||
|
||||
# this monkey patch is to avoid appending git version to version
|
||||
pbr.packaging._get_version_from_git = lambda pre_version: pre_version
|
@ -1,81 +0,0 @@
|
||||
# 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 logging
|
||||
import sys
|
||||
|
||||
from cliff import app
|
||||
from cliff.commandmanager import CommandManager
|
||||
|
||||
from fuelclient import fuelclient_settings
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FuelClient(app.App):
|
||||
"""Main cliff application class.
|
||||
|
||||
Performs initialization of the command manager and
|
||||
configuration of basic engines.
|
||||
|
||||
"""
|
||||
|
||||
def build_option_parser(self, description, version, argparse_kwargs=None):
|
||||
"""Overrides default options for backwards compatibility."""
|
||||
|
||||
p_inst = super(FuelClient, self)
|
||||
parser = p_inst.build_option_parser(description=description,
|
||||
version=version,
|
||||
argparse_kwargs=argparse_kwargs)
|
||||
|
||||
utils.add_os_cli_parameters(parser)
|
||||
|
||||
return parser
|
||||
|
||||
def configure_logging(self):
|
||||
super(FuelClient, self).configure_logging()
|
||||
|
||||
# there is issue with management url processing by keystone client
|
||||
# code in our workflow, so we have to mute appropriate keystone
|
||||
# loggers in order to get rid from unprocessable errors
|
||||
logging.getLogger('keystoneclient.httpclient').setLevel(logging.ERROR)
|
||||
|
||||
# increase level of loggin for urllib3 to avoid of displaying
|
||||
# of useless messages. List of logger names is needed for
|
||||
# consistency on different execution environments that could have
|
||||
# installed requests packages (which is used urllib3) of different
|
||||
# versions in turn
|
||||
for logger_name in ('requests.packages.urllib3.connectionpool',
|
||||
'urllib3.connectionpool'):
|
||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
||||
|
||||
def run(self, argv):
|
||||
options, _ = self.parser.parse_known_args(argv)
|
||||
|
||||
settings = fuelclient_settings.get_settings()
|
||||
settings.update_from_command_line_options(options)
|
||||
|
||||
return super(FuelClient, self).run(argv)
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
fuelclient_app = FuelClient(
|
||||
description='Command line interface and Python API wrapper for Fuel.',
|
||||
version='10.0.0',
|
||||
command_manager=CommandManager('fuelclient', convert_underscores=True),
|
||||
deferred_help=True
|
||||
)
|
||||
return fuelclient_app.run(argv)
|
@ -1,35 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
"""fuelclient.objects sub-module contains classes that mirror
|
||||
functionality from nailgun objects.
|
||||
"""
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
from fuelclient.objects.environment import Environment
|
||||
from fuelclient.objects.extension import Extension
|
||||
from fuelclient.objects.health import Health
|
||||
from fuelclient.objects.node import Node
|
||||
from fuelclient.objects.node import NodeCollection
|
||||
from fuelclient.objects.openstack_config import OpenstackConfig
|
||||
from fuelclient.objects.release import Release
|
||||
from fuelclient.objects.role import Role
|
||||
from fuelclient.objects.task import DeployTask
|
||||
from fuelclient.objects.task import SnapshotTask
|
||||
from fuelclient.objects.task import Task
|
||||
from fuelclient.objects.tag import Tag
|
||||
from fuelclient.objects.fuelversion import FuelVersion
|
||||
from fuelclient.objects.network_group import NetworkGroup
|
||||
from fuelclient.objects.plugins import Plugins
|
||||
from fuelclient.objects.sequence import Sequence
|
@ -1,68 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.client import DefaultAPIClient
|
||||
|
||||
|
||||
class BaseObject(object):
|
||||
"""BaseObject class - base class for fuelclient.objects object classes
|
||||
|
||||
'class_api_path' - url path to object handler on Nailgun server.
|
||||
'instance_api_path' - url path template which formatted with object id
|
||||
returns only one serialized object.
|
||||
'connection' - 'APIClient' class instance from fuelclient.client
|
||||
"""
|
||||
class_api_path = None
|
||||
instance_api_path = None
|
||||
connection = DefaultAPIClient
|
||||
|
||||
def __init__(self, obj_id, **kwargs):
|
||||
self.connection = DefaultAPIClient
|
||||
self.serializer = Serializer.from_params(kwargs.get('params'))
|
||||
self.id = obj_id
|
||||
self._data = None
|
||||
|
||||
@classmethod
|
||||
def init_with_data(cls, data):
|
||||
instance = cls(data["id"])
|
||||
instance._data = data
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def get_by_ids(cls, ids):
|
||||
return map(cls, ids)
|
||||
|
||||
def update(self):
|
||||
self._data = self.connection.get_request(
|
||||
self.instance_api_path.format(self.id))
|
||||
|
||||
def get_fresh_data(self):
|
||||
self.update()
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
if self._data is None:
|
||||
return self.get_fresh_data()
|
||||
else:
|
||||
return self._data
|
||||
|
||||
@classmethod
|
||||
def get_all_data(cls, **kwargs):
|
||||
return cls.connection.get_request(cls.class_api_path, params=kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, **kwargs):
|
||||
return map(cls.init_with_data, cls.get_all_data(**kwargs))
|
@ -1,646 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from operator import attrgetter
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.serializers import listdir_without_extensions
|
||||
from fuelclient.objects.base import BaseObject
|
||||
from fuelclient.objects.task import DeployTask
|
||||
from fuelclient.objects.task import Task
|
||||
|
||||
|
||||
class Environment(BaseObject):
|
||||
|
||||
class_api_path = "clusters/"
|
||||
instance_api_path = "clusters/{0}/"
|
||||
deployment_tasks_path = 'clusters/{0}/deployment_tasks'
|
||||
deployment_tasks_graph_path = 'clusters/{0}/deploy_tasks/graph.gv'
|
||||
attributes_path = 'clusters/{0}/attributes'
|
||||
network_template_path = 'clusters/{0}/network_configuration/template'
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, release_id, net_segment_type):
|
||||
data = {
|
||||
"nodes": [],
|
||||
"tasks": [],
|
||||
"name": name,
|
||||
"release_id": release_id,
|
||||
"net_segment_type": net_segment_type,
|
||||
}
|
||||
|
||||
data = cls.connection.post_request("clusters/", data)
|
||||
return cls.init_with_data(data)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Environment, self).__init__(*args, **kwargs)
|
||||
self._testruns_ids = []
|
||||
|
||||
def set(self, data):
|
||||
return self.connection.put_request(
|
||||
"clusters/{0}/".format(self.id),
|
||||
data
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
return self.connection.delete_request(
|
||||
"clusters/{0}/".format(self.id)
|
||||
)
|
||||
|
||||
def assign(self, nodes, roles):
|
||||
return self.connection.post_request(
|
||||
"clusters/{0}/assignment/".format(self.id),
|
||||
[{'id': node.id, 'roles': roles} for node in nodes]
|
||||
)
|
||||
|
||||
def unassign(self, nodes):
|
||||
return self.connection.post_request(
|
||||
"clusters/{0}/unassignment/".format(self.id),
|
||||
[{"id": n} for n in nodes]
|
||||
)
|
||||
|
||||
def get_all_nodes(self):
|
||||
from fuelclient.objects.node import Node
|
||||
return sorted(map(
|
||||
Node.init_with_data,
|
||||
self.connection.get_request(
|
||||
"nodes/?cluster_id={0}".format(self.id)
|
||||
)
|
||||
), key=attrgetter('id'))
|
||||
|
||||
def unassign_all(self):
|
||||
nodes = self.get_all_nodes()
|
||||
if not nodes:
|
||||
raise error.ActionException(
|
||||
"Environment with id={0} doesn't have nodes to remove."
|
||||
.format(self.id)
|
||||
)
|
||||
return self.connection.post_request(
|
||||
"clusters/{0}/unassignment/".format(self.id),
|
||||
[{"id": n.id} for n in nodes]
|
||||
)
|
||||
|
||||
def deploy_changes(self, dry_run=False, noop_run=False):
|
||||
deploy_data = self.connection.put_request(
|
||||
"clusters/{0}/changes".format(self.id),
|
||||
{}, dry_run=int(dry_run), noop_run=int(noop_run)
|
||||
)
|
||||
return DeployTask.init_with_data(deploy_data)
|
||||
|
||||
def redeploy_changes(self, dry_run=False, noop_run=False):
|
||||
deploy_data = self.connection.put_request(
|
||||
"clusters/{0}/changes/redeploy".format(self.id),
|
||||
{}, dry_run=int(dry_run), noop_run=int(noop_run)
|
||||
)
|
||||
return DeployTask.init_with_data(deploy_data)
|
||||
|
||||
def get_network_data_path(self, directory=os.curdir):
|
||||
return os.path.join(
|
||||
os.path.abspath(directory),
|
||||
"network_{0}".format(self.id)
|
||||
)
|
||||
|
||||
def get_settings_data_path(self, directory=os.curdir):
|
||||
return os.path.join(
|
||||
os.path.abspath(directory),
|
||||
"settings_{0}".format(self.id)
|
||||
)
|
||||
|
||||
def get_network_template_data_path(self, directory=None):
|
||||
directory = directory or os.curdir
|
||||
return os.path.join(
|
||||
os.path.abspath(directory),
|
||||
"network_template_{0}".format(self.id)
|
||||
)
|
||||
|
||||
def write_network_data(self, network_data, directory=os.curdir,
|
||||
serializer=None):
|
||||
self._check_dir(directory)
|
||||
return (serializer or self.serializer).write_to_path(
|
||||
self.get_network_data_path(directory),
|
||||
network_data
|
||||
)
|
||||
|
||||
def write_settings_data(self, settings_data, directory=os.curdir,
|
||||
serializer=None):
|
||||
self._check_dir(directory)
|
||||
return (serializer or self.serializer).write_to_path(
|
||||
self.get_settings_data_path(directory),
|
||||
settings_data
|
||||
)
|
||||
|
||||
def write_network_template_data(self, template_data, directory=None,
|
||||
serializer=None):
|
||||
directory = directory or os.curdir
|
||||
return (serializer or self.serializer).write_to_path(
|
||||
self.get_network_template_data_path(directory),
|
||||
template_data
|
||||
)
|
||||
|
||||
def read_network_data(self, directory=os.curdir,
|
||||
serializer=None):
|
||||
self._check_dir(directory)
|
||||
network_file_path = self.get_network_data_path(directory)
|
||||
return (serializer or self.serializer).read_from_file(
|
||||
network_file_path)
|
||||
|
||||
def read_settings_data(self, directory=os.curdir, serializer=None):
|
||||
self._check_dir(directory)
|
||||
settings_file_path = self.get_settings_data_path(directory)
|
||||
return (serializer or self.serializer).read_from_file(
|
||||
settings_file_path)
|
||||
|
||||
def _check_file_path(self, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
raise error.InvalidFileException(
|
||||
"File '{0}' doesn't exist.".format(file_path))
|
||||
|
||||
def _check_dir(self, directory):
|
||||
if not os.path.exists(directory):
|
||||
raise error.InvalidDirectoryException(
|
||||
"Directory '{0}' doesn't exist.".format(directory))
|
||||
if not os.path.isdir(directory):
|
||||
raise error.InvalidDirectoryException(
|
||||
"Error: '{0}' is not a directory.".format(directory))
|
||||
|
||||
def read_network_template_data(self, directory=os.curdir,
|
||||
serializer=None):
|
||||
"""Used by 'fuel' command line utility."""
|
||||
self._check_dir(directory)
|
||||
network_template_file_path = self.get_network_template_data_path(
|
||||
directory)
|
||||
return (serializer or self.serializer).\
|
||||
read_from_file(network_template_file_path)
|
||||
|
||||
def read_network_template_data_from_file(self, file_path=None,
|
||||
serializer=None):
|
||||
"""Used by 'fuel2' command line utility."""
|
||||
return (serializer or self.serializer).\
|
||||
read_from_full_path(file_path)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.get_fresh_data()['status']
|
||||
|
||||
@property
|
||||
def settings_url(self):
|
||||
return self.attributes_path.format(self.id)
|
||||
|
||||
@property
|
||||
def default_settings_url(self):
|
||||
return self.settings_url + "/defaults"
|
||||
|
||||
@property
|
||||
def network_url(self):
|
||||
return "clusters/{id}/network_configuration/neutron".format(
|
||||
**self.data
|
||||
)
|
||||
|
||||
@property
|
||||
def network_template_url(self):
|
||||
return self.network_template_path.format(self.id)
|
||||
|
||||
@property
|
||||
def network_verification_url(self):
|
||||
return self.network_url + "/verify"
|
||||
|
||||
def get_network_data(self):
|
||||
return self.connection.get_request(self.network_url)
|
||||
|
||||
def get_settings_data(self):
|
||||
return self.connection.get_request(self.settings_url)
|
||||
|
||||
def get_default_settings_data(self):
|
||||
return self.connection.get_request(self.default_settings_url)
|
||||
|
||||
def get_network_template_data(self):
|
||||
return self.connection.get_request(self.network_template_url)
|
||||
|
||||
def set_network_data(self, data):
|
||||
return self.connection.put_request(
|
||||
self.network_url, data)
|
||||
|
||||
def set_settings_data(self, data, force=False):
|
||||
if force:
|
||||
result = self.connection.put_request(
|
||||
self.settings_url, data, force=1)
|
||||
else:
|
||||
result = self.connection.put_request(
|
||||
self.settings_url, data)
|
||||
return result
|
||||
|
||||
def verify_network(self):
|
||||
return self.connection.put_request(
|
||||
self.network_verification_url, self.get_network_data())
|
||||
|
||||
def set_network_template_data(self, data):
|
||||
return self.connection.put_request(
|
||||
self.network_template_url, data)
|
||||
|
||||
def delete_network_template_data(self):
|
||||
return self.connection.delete_request(self.network_template_url)
|
||||
|
||||
def _get_fact_dir_name(self, fact_type, directory=os.path.curdir):
|
||||
return os.path.join(
|
||||
os.path.abspath(directory),
|
||||
"{0}_{1}".format(fact_type, self.id))
|
||||
|
||||
def _get_fact_url(self, fact_type, default=False):
|
||||
fact_url = "clusters/{0}/orchestrator/{1}/{2}".format(
|
||||
self.id, fact_type, 'defaults/' if default else ''
|
||||
)
|
||||
return fact_url
|
||||
|
||||
def get_default_facts(self, fact_type, **kwargs):
|
||||
"""Gets default facts for cluster.
|
||||
:param fact_type: the type of facts (deployment, provision)
|
||||
"""
|
||||
return self.get_facts(fact_type, default=True, **kwargs)
|
||||
|
||||
def get_facts(self, fact_type, default=False, nodes=None, split=None):
|
||||
"""Gets facts for cluster.
|
||||
:param fact_type: the type of facts (deployment, provision)
|
||||
:param default: if True, the default facts will be retrieved
|
||||
:param nodes: if specified, get facts only for selected nodes
|
||||
:param split: if True, the node part and common part will be split
|
||||
"""
|
||||
params = {}
|
||||
if nodes is not None:
|
||||
params['nodes'] = ','.join(str(x) for x in nodes)
|
||||
if split is not None:
|
||||
params['split'] = str(int(split))
|
||||
|
||||
facts = self.connection.get_request(
|
||||
self._get_fact_url(fact_type, default=default), params=params
|
||||
)
|
||||
if not facts:
|
||||
raise error.ServerDataException(
|
||||
"There is no {0} info for this "
|
||||
"environment!".format(fact_type)
|
||||
)
|
||||
return facts
|
||||
|
||||
def upload_facts(self, fact_type, facts):
|
||||
self.connection.put_request(self._get_fact_url(fact_type), facts)
|
||||
|
||||
def delete_facts(self, fact_type):
|
||||
self.connection.delete_request(self._get_fact_url(fact_type))
|
||||
|
||||
def read_fact_info(self, fact_type, directory, serializer=None):
|
||||
return getattr(
|
||||
self, "read_{0}_info".format(fact_type)
|
||||
)(fact_type, directory=directory, serializer=serializer)
|
||||
|
||||
def write_facts_to_dir(self, fact_type, facts,
|
||||
directory=os.path.curdir, serializer=None):
|
||||
dir_name = self._get_fact_dir_name(fact_type, directory=directory)
|
||||
if os.path.exists(dir_name):
|
||||
shutil.rmtree(dir_name)
|
||||
os.makedirs(dir_name)
|
||||
if isinstance(facts, dict):
|
||||
engine_file_path = os.path.join(dir_name, "engine")
|
||||
(serializer or self.serializer).write_to_path(
|
||||
engine_file_path, facts["engine"])
|
||||
facts = facts["nodes"]
|
||||
|
||||
def name_builder(fact):
|
||||
return fact['name']
|
||||
else:
|
||||
def name_builder(fact):
|
||||
if 'role' in fact:
|
||||
# from 9.0 the deployment info is serialized only per node
|
||||
return "{role}_{uid}".format(**fact)
|
||||
return fact['uid']
|
||||
|
||||
for _fact in facts:
|
||||
fact_path = os.path.join(
|
||||
dir_name,
|
||||
name_builder(_fact)
|
||||
)
|
||||
(serializer or self.serializer).write_to_path(fact_path, _fact)
|
||||
return dir_name
|
||||
|
||||
def read_deployment_info(self, fact_type,
|
||||
directory=os.path.curdir, serializer=None):
|
||||
self._check_dir(directory)
|
||||
dir_name = self._get_fact_dir_name(fact_type, directory=directory)
|
||||
self._check_dir(dir_name)
|
||||
return map(
|
||||
lambda f: (serializer or self.serializer).read_from_file(f),
|
||||
[os.path.join(dir_name, json_file)
|
||||
for json_file in listdir_without_extensions(dir_name)]
|
||||
)
|
||||
|
||||
def read_provisioning_info(self, fact_type,
|
||||
directory=os.path.curdir, serializer=None):
|
||||
dir_name = self._get_fact_dir_name(fact_type, directory=directory)
|
||||
node_facts = map(
|
||||
lambda f: (serializer or self.serializer).read_from_file(f),
|
||||
[os.path.join(dir_name, fact_file)
|
||||
for fact_file in listdir_without_extensions(dir_name)
|
||||
if "engine" != fact_file]
|
||||
)
|
||||
engine = (serializer or self.serializer).read_from_file(
|
||||
os.path.join(dir_name, "engine"))
|
||||
return {
|
||||
"engine": engine,
|
||||
"nodes": node_facts
|
||||
}
|
||||
|
||||
# TODO(vkulanov): remove method when deprecate old cli
|
||||
def get_testsets(self):
|
||||
return self.connection.get_request(
|
||||
'testsets/{0}'.format(self.id),
|
||||
ostf=True
|
||||
)
|
||||
|
||||
@property
|
||||
def is_customized(self):
|
||||
data = self.get_fresh_data()
|
||||
return data["is_customized"]
|
||||
|
||||
# TODO(vkulanov): remove method when deprecate old cli
|
||||
def is_in_running_test_sets(self, test_set):
|
||||
return test_set["testset"] in self._test_sets_to_run
|
||||
|
||||
# TODO(vkulanov): remove method when deprecate old cli
|
||||
def run_test_sets(self, test_sets_to_run, ostf_credentials=None):
|
||||
self._test_sets_to_run = test_sets_to_run
|
||||
|
||||
def make_test_set(name):
|
||||
result = {
|
||||
"testset": name,
|
||||
"metadata": {
|
||||
"config": {},
|
||||
"cluster_id": self.id,
|
||||
}
|
||||
}
|
||||
if ostf_credentials:
|
||||
creds = result['metadata'].setdefault(
|
||||
'ostf_os_access_creds', {})
|
||||
if 'tenant' in ostf_credentials:
|
||||
creds['ostf_os_tenant_name'] = ostf_credentials['tenant']
|
||||
if 'username' in ostf_credentials:
|
||||
creds['ostf_os_username'] = ostf_credentials['username']
|
||||
if 'password' in ostf_credentials:
|
||||
creds['ostf_os_password'] = ostf_credentials['password']
|
||||
return result
|
||||
|
||||
tests_data = [make_test_set(ts) for ts in test_sets_to_run]
|
||||
testruns = self.connection.post_request(
|
||||
"testruns", tests_data, ostf=True)
|
||||
self._testruns_ids = [tr['id'] for tr in testruns]
|
||||
return testruns
|
||||
|
||||
# TODO(vkulanov): remove method when deprecate old cli
|
||||
def get_state_of_tests(self):
|
||||
return [
|
||||
self.connection.get_request(
|
||||
"testruns/{0}".format(testrun_id), ostf=True)
|
||||
for testrun_id in self._testruns_ids
|
||||
]
|
||||
|
||||
def stop(self):
|
||||
return Task.init_with_data(
|
||||
self.connection.put_request(
|
||||
"clusters/{0}/stop_deployment/".format(self.id),
|
||||
{}
|
||||
)
|
||||
)
|
||||
|
||||
def reset(self, force=False):
|
||||
return Task.init_with_data(
|
||||
self.connection.put_request(
|
||||
"clusters/{0}/reset/?force={force}".format(self.id,
|
||||
force=int(force)),
|
||||
{}
|
||||
)
|
||||
)
|
||||
|
||||
def _get_method_url(self, method_type, nodes, force=False, noop_run=False):
|
||||
endpoint = "clusters/{0}/{1}/?nodes={2}".format(
|
||||
self.id,
|
||||
method_type,
|
||||
','.join(map(lambda n: str(n.id), nodes)))
|
||||
|
||||
if force:
|
||||
endpoint += '&force=1'
|
||||
if noop_run:
|
||||
endpoint += '&noop_run=1'
|
||||
|
||||
return endpoint
|
||||
|
||||
def install_selected_nodes(self, method_type, nodes):
|
||||
return Task.init_with_data(
|
||||
self.connection.put_request(
|
||||
self._get_method_url(method_type, nodes),
|
||||
{}
|
||||
)
|
||||
)
|
||||
|
||||
def execute_tasks(self, nodes, tasks, force, noop_run):
|
||||
return Task.init_with_data(
|
||||
self.connection.put_request(
|
||||
self._get_method_url('deploy_tasks', nodes=nodes, force=force,
|
||||
noop_run=noop_run),
|
||||
tasks
|
||||
)
|
||||
)
|
||||
|
||||
def get_tasks(self, skip=None, end=None, start=None, include=None):
|
||||
"""Stores logic to filter tasks by known parameters.
|
||||
|
||||
:param skip: list of tasks or None
|
||||
:param end: string or None
|
||||
:param start: string or None
|
||||
:param include: list or None
|
||||
"""
|
||||
tasks = [t['id'] for t in self.get_deployment_tasks(
|
||||
end=end, start=start, include=include)]
|
||||
if skip:
|
||||
tasks_to_execute = set(tasks) - set(skip)
|
||||
return list(tasks_to_execute)
|
||||
return tasks
|
||||
|
||||
def get_deployment_tasks(self, end=None, start=None, include=None):
|
||||
url = self.deployment_tasks_path.format(self.id)
|
||||
return self.connection.get_request(
|
||||
url, params={
|
||||
'end': end,
|
||||
'start': start,
|
||||
'include': include})
|
||||
|
||||
def update_deployment_tasks(self, data):
|
||||
url = self.deployment_tasks_path.format(self.id)
|
||||
return self.connection.put_request(url, data)
|
||||
|
||||
def get_attributes(self):
|
||||
return self.connection.get_request(self.settings_url)
|
||||
|
||||
def update_attributes(self, data, force=False):
|
||||
if force:
|
||||
result = self.connection.put_request(
|
||||
self.settings_url, data, force=1)
|
||||
else:
|
||||
result = self.connection.put_request(
|
||||
self.settings_url, data)
|
||||
return result
|
||||
|
||||
def get_deployment_tasks_graph(self, tasks, parents_for=None, remove=None):
|
||||
url = self.deployment_tasks_graph_path.format(self.id)
|
||||
params = {
|
||||
'tasks': ','.join(tasks),
|
||||
'parents_for': parents_for,
|
||||
'remove': ','.join(remove) if remove else None,
|
||||
}
|
||||
resp = self.connection.get_request_raw(url, params=params)
|
||||
resp.raise_for_status()
|
||||
return resp.text
|
||||
|
||||
def spawn_vms(self):
|
||||
url = 'clusters/{0}/spawn_vms/'.format(self.id)
|
||||
return self.connection.put_request(url, {})
|
||||
|
||||
def _get_ip_addrs_url(self, vips=True, ip_addr_id=None):
|
||||
"""Generate ip address management url.
|
||||
|
||||
:param vips: manage vip properties of ip address
|
||||
:type vips: bool
|
||||
:param ip_addr_id: ip address identifier
|
||||
:type ip_addr_id: int
|
||||
:return: url
|
||||
:rtype: str
|
||||
"""
|
||||
ip_addr_url = "clusters/{0}/network_configuration/ips/".format(self.id)
|
||||
if ip_addr_id:
|
||||
ip_addr_url += '{0}/'.format(ip_addr_id)
|
||||
if vips:
|
||||
ip_addr_url += 'vips/'
|
||||
|
||||
return ip_addr_url
|
||||
|
||||
def get_default_vips_data_path(self):
|
||||
"""Get path where VIPs data is located.
|
||||
:return: path
|
||||
:rtype: str
|
||||
"""
|
||||
return os.path.join(
|
||||
os.path.abspath(os.curdir),
|
||||
"vips_{0}".format(self.id)
|
||||
)
|
||||
|
||||
def get_vips_data(self, ip_address_id=None, network=None,
|
||||
network_role=None):
|
||||
"""Get one or multiple vip data records.
|
||||
|
||||
:param ip_address_id: ip addr id could be specified to download single
|
||||
vip if no ip_addr_id specified multiple entities is
|
||||
returned respecting network and network_role
|
||||
filters
|
||||
:type ip_address_id: int
|
||||
:param network: network id could be specified to filter vips
|
||||
:type network: int
|
||||
:param network_role: network role could be specified to filter vips
|
||||
:type network_role: string
|
||||
:return: response JSON
|
||||
:rtype: list of dict
|
||||
"""
|
||||
params = {}
|
||||
if network:
|
||||
params['network'] = network
|
||||
if network_role:
|
||||
params['network-role'] = network_role
|
||||
|
||||
result = self.connection.get_request(
|
||||
self._get_ip_addrs_url(True, ip_addr_id=ip_address_id),
|
||||
params=params
|
||||
)
|
||||
if ip_address_id is not None: # single vip is returned
|
||||
# wrapping with list is required to respect case when administrator
|
||||
# is downloading vip address info to change it and upload
|
||||
# back. Uploading works only with lists of records.
|
||||
result = [result]
|
||||
return result
|
||||
|
||||
def write_vips_data_to_file(self, vips_data, serializer=None,
|
||||
file_path=None):
|
||||
"""Write VIP data to the given path.
|
||||
|
||||
:param vips_data: vip data
|
||||
:type vips_data: list of dict
|
||||
:param serializer: serializer
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:return: path to resulting file
|
||||
:rtype: str
|
||||
"""
|
||||
serializer = serializer or self.serializer
|
||||
|
||||
if file_path:
|
||||
return serializer.write_to_full_path(
|
||||
file_path,
|
||||
vips_data
|
||||
)
|
||||
else:
|
||||
return serializer.write_to_path(
|
||||
self.get_default_vips_data_path(),
|
||||
vips_data
|
||||
)
|
||||
|
||||
def read_vips_data_from_file(self, file_path=None, serializer=None):
|
||||
"""Read VIPs data from given path.
|
||||
|
||||
:param file_path: path
|
||||
:type file_path: str
|
||||
:param serializer: serializer object
|
||||
:type serializer: object
|
||||
:return: data
|
||||
:rtype: list|object
|
||||
"""
|
||||
self._check_file_path(file_path)
|
||||
return (serializer or self.serializer).read_from_file(file_path)
|
||||
|
||||
def set_vips_data(self, data):
|
||||
"""Sending VIPs data to the Nailgun API.
|
||||
|
||||
:param data: VIPs data
|
||||
:type data: list of dict
|
||||
:return: request result
|
||||
:rtype: object
|
||||
"""
|
||||
return self.connection.put_request(self._get_ip_addrs_url(), data)
|
||||
|
||||
def create_vip(self, **vip_kwargs):
|
||||
"""Create VIP through request to Nailgun API
|
||||
|
||||
:param vip_data: attributes of the VIP to be created
|
||||
"""
|
||||
return self.connection.post_request(self._get_ip_addrs_url(),
|
||||
vip_kwargs)
|
||||
|
||||
def get_enabled_plugins(self):
|
||||
"""Get list of enabled plugins ids.
|
||||
|
||||
:returns: plugins ids list
|
||||
:rtype: list[int]
|
||||
"""
|
||||
attrs = self.get_attributes()['editable']
|
||||
enabled_plugins_ids = []
|
||||
for attr_name in attrs:
|
||||
metadata = attrs[attr_name].get('metadata', {})
|
||||
if metadata.get('class') == 'plugin' and metadata.get('enabled'):
|
||||
enabled_plugins_ids.append(metadata['chosen_id'])
|
||||
return enabled_plugins_ids
|
@ -1,47 +0,0 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class Extension(BaseObject):
|
||||
|
||||
class_api_path = "extensions/"
|
||||
instance_api_path = "clusters/{0}/extensions/"
|
||||
|
||||
@property
|
||||
def extensions_url(self):
|
||||
return self.instance_api_path.format(self.id)
|
||||
|
||||
def get_env_extensions(self):
|
||||
"""Get list of extensions through request to the Nailgun API
|
||||
|
||||
"""
|
||||
return self.connection.get_request(self.extensions_url)
|
||||
|
||||
def enable_env_extensions(self, extensions):
|
||||
"""Enable extensions through request to the Nailgun API
|
||||
|
||||
:param extensions: list of extenstion to be enabled
|
||||
"""
|
||||
return self.connection.put_request(self.extensions_url, extensions)
|
||||
|
||||
def disable_env_extensions(self, extensions):
|
||||
"""Disable extensions through request to the Nailgun API
|
||||
|
||||
:param extensions: list of extenstion to be disabled
|
||||
"""
|
||||
url = '{0}?extension_names={1}'.format(self.extensions_url,
|
||||
','.join(extensions))
|
||||
return self.connection.delete_request(url)
|
@ -1,24 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class FuelVersion(BaseObject):
|
||||
|
||||
class_api_path = "version/"
|
||||
|
||||
@classmethod
|
||||
def get_feature_groups(cls):
|
||||
return cls.get_all_data()['feature_groups']
|
@ -1,86 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 Vitalii Kulanov
|
||||
#
|
||||
# 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.
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class Health(BaseObject):
|
||||
|
||||
class_api_path = "testruns/"
|
||||
instance_api_path = "testruns/{0}/"
|
||||
test_sets_api_path = "testsets/{0}/"
|
||||
|
||||
@classmethod
|
||||
def get_test_sets(cls, environment_id):
|
||||
return cls.connection.get_request(
|
||||
cls.test_sets_api_path.format(environment_id),
|
||||
ostf=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_tests_status_all(cls):
|
||||
return cls.connection.get_request(cls.class_api_path, ostf=True)
|
||||
|
||||
def get_tests_status_single(self):
|
||||
return self.connection.get_request(
|
||||
self.instance_api_path.format(self.id),
|
||||
ostf=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_last_tests_status(cls, environment_id):
|
||||
return cls.connection.get_request(
|
||||
'testruns/last/{0}'.format(environment_id),
|
||||
ostf=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def run_test_sets(cls, environment_id, test_sets_to_run,
|
||||
ostf_credentials=None):
|
||||
|
||||
def make_test_set(name):
|
||||
result = {
|
||||
"testset": name,
|
||||
"metadata": {
|
||||
"config": {},
|
||||
"cluster_id": environment_id,
|
||||
}
|
||||
}
|
||||
if ostf_credentials:
|
||||
creds = result['metadata'].setdefault(
|
||||
'ostf_os_access_creds', {})
|
||||
if 'tenant' in ostf_credentials:
|
||||
creds['ostf_os_tenant_name'] = ostf_credentials['tenant']
|
||||
if 'username' in ostf_credentials:
|
||||
creds['ostf_os_username'] = ostf_credentials['username']
|
||||
if 'password' in ostf_credentials:
|
||||
creds['ostf_os_password'] = ostf_credentials['password']
|
||||
return result
|
||||
|
||||
tests_data = [make_test_set(ts) for ts in test_sets_to_run]
|
||||
test_runs = cls.connection.post_request(cls.class_api_path,
|
||||
tests_data,
|
||||
ostf=True)
|
||||
return test_runs
|
||||
|
||||
def action_test(self, action_status):
|
||||
data = [{
|
||||
"id": self.id,
|
||||
"status": action_status
|
||||
}]
|
||||
return self.connection.put_request(
|
||||
'testruns/', data, ostf=True
|
||||
)
|
@ -1,108 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class NetworkGroup(BaseObject):
|
||||
|
||||
class_api_path = "networks/"
|
||||
instance_api_path = "networks/{0}/"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_fresh_data()["name"]
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, release, vlan, cidr, gateway,
|
||||
group_id, meta=None):
|
||||
|
||||
metadata = {
|
||||
'notation': 'cidr',
|
||||
'render_type': None,
|
||||
'map_priority': 2,
|
||||
'configurable': True,
|
||||
'use_gateway': False,
|
||||
'name': name,
|
||||
'cidr': cidr,
|
||||
'vlan_start': vlan
|
||||
}
|
||||
if meta:
|
||||
metadata.update(meta)
|
||||
|
||||
network_group = {
|
||||
'name': name,
|
||||
'release': release,
|
||||
'vlan_start': vlan,
|
||||
'cidr': cidr,
|
||||
'gateway': gateway,
|
||||
'meta': metadata,
|
||||
'group_id': group_id,
|
||||
}
|
||||
|
||||
data = cls.connection.post_request(
|
||||
cls.class_api_path,
|
||||
network_group,
|
||||
)
|
||||
return cls.init_with_data(data)
|
||||
|
||||
def set(self, data):
|
||||
vlan = data.pop('vlan', None)
|
||||
if vlan is not None:
|
||||
data['vlan_start'] = vlan
|
||||
|
||||
return self.connection.put_request(
|
||||
self.instance_api_path.format(self.id), data)
|
||||
|
||||
def delete(self):
|
||||
return self.connection.delete_request(
|
||||
self.instance_api_path.format(self.id)
|
||||
)
|
||||
|
||||
|
||||
class NetworkGroupCollection(object):
|
||||
|
||||
def __init__(self, networks):
|
||||
self.collection = networks
|
||||
|
||||
@classmethod
|
||||
def init_with_ids(cls, ids):
|
||||
return cls(map(NetworkGroup, ids))
|
||||
|
||||
@classmethod
|
||||
def init_with_data(cls, data):
|
||||
return cls(map(NetworkGroup.init_with_data, data))
|
||||
|
||||
def __str__(self):
|
||||
return "{0} [{1}]".format(
|
||||
self.__class__.__name__,
|
||||
", ".join(map(lambda n: str(n.id), self.collection))
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.collection)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return map(attrgetter("data"), self.collection)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
return cls(NetworkGroup.get_all())
|
||||
|
||||
def filter_by_group_id(self, group_id):
|
||||
self.collection = filter(lambda net: net.data['group_id'] == group_id,
|
||||
self.collection)
|
@ -1,206 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from operator import attrgetter
|
||||
import os
|
||||
|
||||
from fuelclient.cli.error import InvalidDirectoryException
|
||||
from fuelclient.objects.base import BaseObject
|
||||
from fuelclient.objects.environment import Environment
|
||||
|
||||
|
||||
class Node(BaseObject):
|
||||
|
||||
class_api_path = "nodes/"
|
||||
instance_api_path = "nodes/{0}/"
|
||||
attributes_api_path = "nodes/{0}/attributes/"
|
||||
|
||||
attributes_urls = {
|
||||
"interfaces": ("interfaces", "default_assignment"),
|
||||
"disks": ("disks", "defaults")
|
||||
}
|
||||
|
||||
@property
|
||||
def env_id(self):
|
||||
return self.get_fresh_data()["cluster"]
|
||||
|
||||
@property
|
||||
def env(self):
|
||||
return Environment(self.env_id)
|
||||
|
||||
def get_attributes_path(self, directory):
|
||||
return os.path.join(
|
||||
os.path.abspath(
|
||||
os.curdir if directory is None else directory),
|
||||
"node_{0}".format(self.id)
|
||||
)
|
||||
|
||||
def is_finished(self, latest=True):
|
||||
if latest:
|
||||
data = self.get_fresh_data()
|
||||
else:
|
||||
data = self.data
|
||||
return data["status"] in ("ready", "error")
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
data = self.get_fresh_data()
|
||||
return data["progress"]
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
return self.get_fresh_data().get('labels', {})
|
||||
|
||||
def get_attribute_default_url(self, attributes_type):
|
||||
url_path, default_url_path = self.attributes_urls[attributes_type]
|
||||
return "nodes/{0}/{1}/{2}".format(self.id, url_path, default_url_path)
|
||||
|
||||
def get_attribute_url(self, attributes_type):
|
||||
url_path, _ = self.attributes_urls[attributes_type]
|
||||
return "nodes/{0}/{1}/".format(self.id, url_path)
|
||||
|
||||
def get_default_attribute(self, attributes_type):
|
||||
return self.connection.get_request(
|
||||
self.get_attribute_default_url(attributes_type)
|
||||
)
|
||||
|
||||
def get_node_attributes(self):
|
||||
return self.connection.get_request(
|
||||
self.attributes_api_path.format(self.id)
|
||||
)
|
||||
|
||||
def update_node_attributes(self, data):
|
||||
return self.connection.put_request(
|
||||
self.attributes_api_path.format(self.id),
|
||||
data
|
||||
)
|
||||
|
||||
def get_attribute(self, attributes_type):
|
||||
return self.connection.get_request(
|
||||
self.get_attribute_url(attributes_type)
|
||||
)
|
||||
|
||||
def upload_node_attribute(self, attributes_type, attributes):
|
||||
url = self.get_attribute_url(attributes_type)
|
||||
return self.connection.put_request(
|
||||
url,
|
||||
attributes
|
||||
)
|
||||
|
||||
def write_attribute(self, attribute_type, attributes,
|
||||
directory, serializer=None):
|
||||
attributes_directory = self.get_attributes_path(directory)
|
||||
if not os.path.exists(attributes_directory):
|
||||
os.mkdir(attributes_directory)
|
||||
attribute_path = os.path.join(
|
||||
attributes_directory,
|
||||
attribute_type
|
||||
)
|
||||
if os.path.exists(attribute_path):
|
||||
os.remove(attribute_path)
|
||||
return (serializer or self.serializer).write_to_path(
|
||||
attribute_path,
|
||||
attributes
|
||||
)
|
||||
|
||||
def read_attribute(self, attributes_type, directory, serializer=None):
|
||||
attributes_directory = self.get_attributes_path(directory)
|
||||
if not os.path.exists(attributes_directory):
|
||||
raise InvalidDirectoryException(
|
||||
"Folder {0} doesn't contain node folder '{1}'"
|
||||
.format(directory, "node_{0}".format(self.id))
|
||||
)
|
||||
return (serializer or self.serializer).read_from_file(
|
||||
os.path.join(
|
||||
attributes_directory,
|
||||
attributes_type
|
||||
)
|
||||
)
|
||||
|
||||
def deploy(self):
|
||||
self.env.install_selected_nodes("deploy", (self,))
|
||||
|
||||
def provision(self):
|
||||
self.env.install_selected_nodes("provision", (self,))
|
||||
|
||||
def delete(self):
|
||||
self.connection.delete_request(self.instance_api_path.format(self.id))
|
||||
|
||||
def node_vms_create(self, config):
|
||||
url = "nodes/{0}/vms_conf/".format(self.id)
|
||||
return self.connection.put_request(url, {'vms_conf': config})
|
||||
|
||||
def get_node_vms_conf(self):
|
||||
url = "nodes/{0}/vms_conf/".format(self.id)
|
||||
return self.connection.get_request(url)
|
||||
|
||||
def set(self, data):
|
||||
return self.connection.put_request(
|
||||
self.instance_api_path.format(self.id),
|
||||
data
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_by_env_id(cls, cluster_id):
|
||||
params = {'cluster_id': cluster_id}
|
||||
return cls.connection.get_request(cls.class_api_path, params=params)
|
||||
|
||||
|
||||
class NodeCollection(object):
|
||||
|
||||
class_api_path = "nodes/"
|
||||
|
||||
def __init__(self, nodes):
|
||||
self.collection = nodes
|
||||
|
||||
@classmethod
|
||||
def init_with_ids(cls, ids):
|
||||
return cls(list(map(Node, ids)))
|
||||
|
||||
@classmethod
|
||||
def init_with_data(cls, data):
|
||||
return cls(list(map(Node.init_with_data, data)))
|
||||
|
||||
def __str__(self):
|
||||
return "nodes [{0}]".format(
|
||||
", ".join(map(lambda n: str(n.id), self.collection))
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.collection)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return map(attrgetter("data"), self.collection)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
return cls(Node.get_all())
|
||||
|
||||
@classmethod
|
||||
def update(cls, data):
|
||||
return BaseObject.connection.put_request(cls.class_api_path, data)
|
||||
|
||||
@classmethod
|
||||
def delete_by_ids(cls, ids):
|
||||
url = '{0}?ids={1}'.format(
|
||||
cls.class_api_path,
|
||||
','.join(map(str, ids))
|
||||
)
|
||||
|
||||
return BaseObject.connection.delete_request(url)
|
||||
|
||||
def filter_by_env_id(self, env_id):
|
||||
predicate = lambda node: node.data['cluster'] == env_id
|
||||
self.collection = filter(predicate, self.collection)
|
@ -1,83 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
from fuelclient.objects import NodeCollection
|
||||
|
||||
|
||||
class NodeGroup(BaseObject):
|
||||
|
||||
class_api_path = "nodegroups/"
|
||||
instance_api_path = "nodegroups/{0}/"
|
||||
|
||||
@property
|
||||
def env_id(self):
|
||||
return self.get_fresh_data()["cluster_id"]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_fresh_data()["name"]
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, cluster_id):
|
||||
return cls.connection.post_request(
|
||||
cls.class_api_path,
|
||||
{'cluster_id': cluster_id, 'name': name},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, group_id):
|
||||
return cls.connection.delete_request(
|
||||
cls.instance_api_path.format(group_id)
|
||||
)
|
||||
|
||||
def assign(self, nodes):
|
||||
data = [{"id": n, "group_id": int(self.id)} for n in nodes]
|
||||
NodeCollection.update(data)
|
||||
|
||||
|
||||
class NodeGroupCollection(object):
|
||||
|
||||
def __init__(self, groups):
|
||||
self.collection = groups
|
||||
|
||||
@classmethod
|
||||
def init_with_ids(cls, ids):
|
||||
return cls(map(NodeGroup, ids))
|
||||
|
||||
@classmethod
|
||||
def init_with_data(cls, data):
|
||||
return cls(map(NodeGroup.init_with_data, data))
|
||||
|
||||
def __str__(self):
|
||||
return "node groups [{0}]".format(
|
||||
", ".join(map(lambda n: str(n.id), self.collection))
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.collection)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return map(attrgetter("data"), self.collection)
|
||||
|
||||
@classmethod
|
||||
def get_all(cls):
|
||||
return cls(NodeGroup.get_all())
|
||||
|
||||
def filter_by_env_id(self, env_id):
|
||||
predicate = lambda group: group.data['cluster_id'] == env_id
|
||||
self.collection = filter(predicate, self.collection)
|
@ -1,67 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from fuelclient.cli import error
|
||||
|
||||
from fuelclient.objects import base
|
||||
|
||||
|
||||
class Notifications(base.BaseObject):
|
||||
|
||||
class_api_path = "notifications/"
|
||||
instance_api_path = "notifications/{0}"
|
||||
|
||||
default_topic = 'done'
|
||||
|
||||
@classmethod
|
||||
def mark_as_read(cls, ids=None):
|
||||
if not ids:
|
||||
raise error.BadDataException('Message id not specified.')
|
||||
|
||||
if '*' in ids:
|
||||
data = Notifications.get_all_data()
|
||||
else:
|
||||
try:
|
||||
ids = map(int, ids)
|
||||
except ValueError:
|
||||
raise error.BadDataException(
|
||||
"Numerical ids expected or the '*' symbol.")
|
||||
notifications = Notifications.get_by_ids(ids)
|
||||
|
||||
data = [notification.get_fresh_data()
|
||||
for notification in notifications]
|
||||
|
||||
for notification in data:
|
||||
notification['status'] = 'read'
|
||||
|
||||
resp = cls.connection.put_request(
|
||||
cls.class_api_path, data)
|
||||
|
||||
return resp
|
||||
|
||||
@classmethod
|
||||
def send(cls, message, topic=default_topic):
|
||||
if not topic:
|
||||
topic = cls.default_topic
|
||||
|
||||
if not message:
|
||||
raise error.BadDataException('Message not specified.')
|
||||
|
||||
resp = cls.connection.post_request(
|
||||
cls.class_api_path, {
|
||||
'message': message,
|
||||
'topic': topic,
|
||||
})
|
||||
|
||||
return resp
|
@ -1,72 +0,0 @@
|
||||
# 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 os
|
||||
|
||||
import six
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class OpenstackConfig(BaseObject):
|
||||
|
||||
class_api_path = 'openstack-config/'
|
||||
instance_api_path = 'openstack-config/{0}/'
|
||||
execute_api_path = 'openstack-config/execute/'
|
||||
|
||||
@classmethod
|
||||
def _prepare_params(cls, filters):
|
||||
return dict((k, v) for k, v in six.iteritems(filters) if v is not None)
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
params = cls._prepare_params(kwargs)
|
||||
data = cls.connection.post_request(cls.class_api_path, params)
|
||||
return [cls.init_with_data(item) for item in data]
|
||||
|
||||
def delete(self):
|
||||
return self.connection.delete_request(
|
||||
self.instance_api_path.format(self.id))
|
||||
|
||||
@classmethod
|
||||
def execute(cls, **kwargs):
|
||||
params = cls._prepare_params(kwargs)
|
||||
return cls.connection.put_request(cls.execute_api_path, params)
|
||||
|
||||
@classmethod
|
||||
def get_filtered_data(cls, **kwargs):
|
||||
url = cls.class_api_path
|
||||
params = cls._prepare_params(kwargs)
|
||||
|
||||
node_ids = params.get('node_ids')
|
||||
if node_ids is not None:
|
||||
params['node_ids'] = ','.join([str(n) for n in node_ids])
|
||||
|
||||
return cls.connection.get_request(url, params=params)
|
||||
|
||||
@classmethod
|
||||
def read_file(cls, path):
|
||||
if not os.path.exists(path):
|
||||
raise error.InvalidFileException(
|
||||
"File '{0}' doesn't exist.".format(path))
|
||||
|
||||
serializer = Serializer()
|
||||
return serializer.read_from_full_path(path)
|
||||
|
||||
@classmethod
|
||||
def write_file(cls, path, data):
|
||||
serializer = Serializer()
|
||||
return serializer.write_to_full_path(path, data)
|
@ -1,526 +0,0 @@
|
||||
# Copyright 2014 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 json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.objects import base
|
||||
from fuelclient import utils
|
||||
|
||||
IS_MASTER = None
|
||||
FUEL_PACKAGE = 'fuel'
|
||||
PLUGINS_PATH = '/var/www/nailgun/plugins/'
|
||||
METADATA_MASK = '/var/www/nailgun/plugins/*/metadata.yaml'
|
||||
|
||||
|
||||
def raise_error_if_not_master():
|
||||
"""Raises error if it's not Fuel master
|
||||
|
||||
:raises: error.WrongEnvironmentError
|
||||
"""
|
||||
msg_tail = 'Action can be performed from Fuel master node only.'
|
||||
global IS_MASTER
|
||||
if IS_MASTER is None:
|
||||
IS_MASTER = False
|
||||
rpm_exec = utils.find_exec('rpm')
|
||||
if not rpm_exec:
|
||||
msg = 'Command "rpm" not found. ' + msg_tail
|
||||
raise error.WrongEnvironmentError(msg)
|
||||
command = [rpm_exec, '-q', FUEL_PACKAGE]
|
||||
p = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
if p.poll() == 0:
|
||||
IS_MASTER = True
|
||||
if not IS_MASTER:
|
||||
msg = 'Package "fuel" is not installed. ' + msg_tail
|
||||
raise error.WrongEnvironmentError(msg)
|
||||
|
||||
|
||||
def master_only(f):
|
||||
"""Decorator for the method, which raises error, if method
|
||||
is called on the node which is not Fuel master
|
||||
"""
|
||||
@six.wraps(f)
|
||||
def print_message(*args, **kwargs):
|
||||
raise_error_if_not_master()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return print_message
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BasePlugin(object):
|
||||
|
||||
@abc.abstractmethod
|
||||
def install(cls, plugin_path, force=False):
|
||||
"""Installs plugin package
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update(cls, plugin_path):
|
||||
"""Updates the plugin
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove(cls, plugin_name, plugin_version):
|
||||
"""Removes the plugin from file system
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def downgrade(cls, plugin_path):
|
||||
"""Downgrades the plugin
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def name_from_file(cls, file_path):
|
||||
"""Retrieves name from plugin package
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def version_from_file(cls, file_path):
|
||||
"""Retrieves version from plugin package
|
||||
"""
|
||||
|
||||
|
||||
class PluginV1(BasePlugin):
|
||||
|
||||
metadata_config = 'metadata.yaml'
|
||||
|
||||
def deprecated(f):
|
||||
"""Prints deprecation warning for old plugins
|
||||
"""
|
||||
@six.wraps(f)
|
||||
def print_message(*args, **kwargs):
|
||||
six.print_(
|
||||
'DEPRECATION WARNING: The plugin has old 1.0 package format, '
|
||||
'this format does not support many features, such as '
|
||||
'plugins updates, find plugin in new format or migrate '
|
||||
'and rebuild this one.', file=sys.stderr)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return print_message
|
||||
|
||||
@classmethod
|
||||
@master_only
|
||||
@deprecated
|
||||
def install(cls, plugin_path, force=False):
|
||||
plugin_tar = tarfile.open(plugin_path, 'r')
|
||||
try:
|
||||
plugin_tar.extractall(PLUGINS_PATH)
|
||||
finally:
|
||||
plugin_tar.close()
|
||||
|
||||
@classmethod
|
||||
@master_only
|
||||
@deprecated
|
||||
def remove(cls, plugin_name, plugin_version):
|
||||
plugin_path = os.path.join(
|
||||
PLUGINS_PATH, '{0}-{1}'.format(plugin_name, plugin_version))
|
||||
shutil.rmtree(plugin_path)
|
||||
|
||||
@classmethod
|
||||
def update(cls, _):
|
||||
raise error.BadDataException(
|
||||
'Update action is not supported for old plugins with '
|
||||
'package version "1.0.0", you can install your plugin '
|
||||
'or use newer plugin format.')
|
||||
|
||||
@classmethod
|
||||
def downgrade(cls, _):
|
||||
raise error.BadDataException(
|
||||
'Downgrade action is not supported for old plugins with '
|
||||
'package version "1.0.0", you can install your plugin '
|
||||
'or use newer plugin format.')
|
||||
|
||||
@classmethod
|
||||
def name_from_file(cls, file_path):
|
||||
"""Retrieves plugin name from plugin archive.
|
||||
|
||||
:param str plugin_path: path to the plugin
|
||||
:returns: plugin name
|
||||
"""
|
||||
return cls._get_metadata(file_path)['name']
|
||||
|
||||
@classmethod
|
||||
def version_from_file(cls, file_path):
|
||||
"""Retrieves plugin version from plugin archive.
|
||||
|
||||
:param str plugin_path: path to the plugin
|
||||
:returns: plugin version
|
||||
"""
|
||||
return cls._get_metadata(file_path)['version']
|
||||
|
||||
@classmethod
|
||||
def _get_metadata(cls, plugin_path):
|
||||
"""Retrieves metadata from plugin archive
|
||||
|
||||
:param str plugin_path: path to the plugin
|
||||
:returns: metadata from the plugin
|
||||
"""
|
||||
plugin_tar = tarfile.open(plugin_path, 'r')
|
||||
|
||||
try:
|
||||
for member_name in plugin_tar.getnames():
|
||||
if cls.metadata_config in member_name:
|
||||
return yaml.load(
|
||||
plugin_tar.extractfile(member_name).read())
|
||||
finally:
|
||||
plugin_tar.close()
|
||||
|
||||
|
||||
class PluginV2(BasePlugin):
|
||||
|
||||
@classmethod
|
||||
@master_only
|
||||
def install(cls, plugin_path, force=False):
|
||||
if force:
|
||||
utils.exec_cmd(
|
||||
'yum -y install --disablerepo=\'*\' {0} || '
|
||||
'yum -y reinstall --disablerepo=\'*\' {0}'
|
||||
.format(plugin_path))
|
||||
else:
|
||||
utils.exec_cmd('yum -y install --disablerepo=\'*\' {0}'
|
||||
.format(plugin_path))
|
||||
|
||||
@classmethod
|
||||
@master_only
|
||||
def remove(cls, name, version):
|
||||
rpm_name = '{0}-{1}'.format(name, utils.major_plugin_version(version))
|
||||
utils.exec_cmd('yum -y remove {0}'.format(rpm_name))
|
||||
|
||||
@classmethod
|
||||
@master_only
|
||||
def update(cls, plugin_path):
|
||||
utils.exec_cmd('yum -y update {0}'.format(plugin_path))
|
||||
|
||||
@classmethod
|
||||
@master_only
|
||||
def downgrade(cls, plugin_path):
|
||||
utils.exec_cmd('yum -y downgrade {0}'.format(plugin_path))
|
||||
|
||||
@classmethod
|
||||
def name_from_file(cls, file_path):
|
||||
"""Retrieves plugin name from RPM. RPM name contains
|
||||
the version of the plugin, which should be removed.
|
||||
|
||||
:param str file_path: path to rpm file
|
||||
:returns: name of the plugin
|
||||
"""
|
||||
for line in utils.exec_cmd_iterator(
|
||||
"rpm -qp --queryformat '%{{name}}' {0}".format(file_path)):
|
||||
name = line
|
||||
break
|
||||
|
||||
return cls._remove_major_plugin_version(name)
|
||||
|
||||
@classmethod
|
||||
def version_from_file(cls, file_path):
|
||||
"""Retrieves plugin version from RPM.
|
||||
|
||||
:param str file_path: path to rpm file
|
||||
:returns: version of the plugin
|
||||
"""
|
||||
for line in utils.exec_cmd_iterator(
|
||||
"rpm -qp --queryformat '%{{version}}' {0}".format(file_path)):
|
||||
version = line
|
||||
break
|
||||
|
||||
return version
|
||||
|
||||
@classmethod
|
||||
def _remove_major_plugin_version(cls, name):
|
||||
"""Removes the version from plugin name.
|
||||
Here is an example: "name-1.0" -> "name"
|
||||
|
||||
:param str name: plugin name
|
||||
:returns: the name withot version
|
||||
"""
|
||||
name_wo_version = name
|
||||
|
||||
if '-' in name_wo_version:
|
||||
name_wo_version = '-'.join(name.split('-')[:-1])
|
||||
|
||||
return name_wo_version
|
||||
|
||||
|
||||
class Plugins(base.BaseObject):
|
||||
|
||||
class_api_path = 'plugins/'
|
||||
class_instance_path = 'plugins/{id}'
|
||||
|
||||
@classmethod
|
||||
def register(cls, name, version, force=False):
|
||||
"""Tries to find plugin on file system, creates
|
||||
it in API service if it exists.
|
||||
|
||||
:param str name: plugin name
|
||||
:param str version: plugin version
|
||||
:param bool force: if True updates meta information
|
||||
about the plugin even it does not
|
||||
support updates
|
||||
"""
|
||||
metadata = None
|
||||
for m in utils.glob_and_parse_yaml(METADATA_MASK):
|
||||
if m.get('version') == version and \
|
||||
m.get('name') == name:
|
||||
metadata = m
|
||||
break
|
||||
|
||||
if not metadata:
|
||||
raise error.BadDataException(
|
||||
'Plugin {0} with version {1} does '
|
||||
'not exist, install it and try again'.format(
|
||||
name, version))
|
||||
|
||||
return cls.update_or_create(metadata, force=force)
|
||||
|
||||
@classmethod
|
||||
def sync(cls, plugin_ids=None):
|
||||
"""Checks all of the plugins on file systems,
|
||||
and makes sure that they have consistent information
|
||||
in API service.
|
||||
|
||||
:params plugin_ids: list of ids for plugins which should be synced
|
||||
:type plugin_ids: list
|
||||
:returns: None
|
||||
"""
|
||||
post_data = None
|
||||
if plugin_ids is not None:
|
||||
post_data = {'ids': plugin_ids}
|
||||
|
||||
cls.connection.post_request(
|
||||
api='plugins/sync/', data=post_data)
|
||||
|
||||
@classmethod
|
||||
def unregister(cls, name, version):
|
||||
"""Removes the plugin from API service
|
||||
|
||||
:param str name: plugin name
|
||||
:param str version: plugin version
|
||||
"""
|
||||
plugin = cls.get_plugin(name, version)
|
||||
return cls.connection.delete_request(
|
||||
cls.class_instance_path.format(**plugin))
|
||||
|
||||
@classmethod
|
||||
def install(cls, plugin_path, force=False):
|
||||
"""Installs the package, and creates data in API service
|
||||
|
||||
:param str plugin_path: Name of plugin file
|
||||
:param bool force: Updates existent plugin even if it is not updatable
|
||||
:return: Plugins information
|
||||
:rtype: dict
|
||||
"""
|
||||
if not utils.file_exists(plugin_path):
|
||||
raise error.BadDataException(
|
||||
"No such plugin file: {0}".format(plugin_path)
|
||||
)
|
||||
plugin = cls.make_obj_by_file(plugin_path)
|
||||
|
||||
name = plugin.name_from_file(plugin_path)
|
||||
version = plugin.version_from_file(plugin_path)
|
||||
|
||||
plugin.install(plugin_path, force=force)
|
||||
response = cls.register(name, version, force=force)
|
||||
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def remove(cls, plugin_name, plugin_version):
|
||||
"""Removes the package, and updates data in API service
|
||||
|
||||
:param str name: plugin name
|
||||
:param str version: plugin version
|
||||
"""
|
||||
plugin = cls.make_obj_by_name(plugin_name, plugin_version)
|
||||
cls.unregister(plugin_name, plugin_version)
|
||||
return plugin.remove(plugin_name, plugin_version)
|
||||
|
||||
@classmethod
|
||||
def update(cls, plugin_path):
|
||||
"""Updates the package, and updates data in API service
|
||||
|
||||
:param str plugin_path: path to the plugin
|
||||
"""
|
||||
plugin = cls.make_obj_by_file(plugin_path)
|
||||
|
||||
name = plugin.name_from_file(plugin_path)
|
||||
version = plugin.version_from_file(plugin_path)
|
||||
|
||||
plugin.update(plugin_path)
|
||||
return cls.register(name, version)
|
||||
|
||||
@classmethod
|
||||
def downgrade(cls, plugin_path):
|
||||
"""Downgrades the package, and updates data in API service
|
||||
|
||||
:param str plugin_path: path to the plugin
|
||||
"""
|
||||
plugin = cls.make_obj_by_file(plugin_path)
|
||||
|
||||
name = plugin.name_from_file(plugin_path)
|
||||
version = plugin.version_from_file(plugin_path)
|
||||
|
||||
plugin.downgrade(plugin_path)
|
||||
return cls.register(name, version)
|
||||
|
||||
@classmethod
|
||||
def make_obj_by_name(cls, name, version):
|
||||
"""Finds appropriate plugin class version,
|
||||
by plugin version and name.
|
||||
|
||||
:param str name:
|
||||
:param str version:
|
||||
:returns: plugin class
|
||||
:raises: error.BadDataException unsupported package version
|
||||
"""
|
||||
plugin = cls.get_plugin(name, version)
|
||||
package_version = plugin['package_version']
|
||||
|
||||
if StrictVersion('1.0.0') <= \
|
||||
StrictVersion(package_version) < \
|
||||
StrictVersion('2.0.0'):
|
||||
return PluginV1
|
||||
elif StrictVersion('2.0.0') <= StrictVersion(package_version):
|
||||
return PluginV2
|
||||
|
||||
raise error.BadDataException(
|
||||
'Plugin {0}=={1} has unsupported package version {2}'.format(
|
||||
name, version, package_version))
|
||||
|
||||
@classmethod
|
||||
def make_obj_by_file(cls, file_path):
|
||||
"""Finds appropriate plugin class version,
|
||||
by plugin file.
|
||||
|
||||
:param str file_path: plugin path
|
||||
:returns: plugin class
|
||||
:raises: error.BadDataException unsupported package version
|
||||
"""
|
||||
_, ext = os.path.splitext(file_path)
|
||||
|
||||
if ext == '.fp':
|
||||
return PluginV1
|
||||
elif ext == '.rpm':
|
||||
return PluginV2
|
||||
|
||||
raise error.BadDataException(
|
||||
'Plugin {0} has unsupported format {1}'.format(
|
||||
file_path, ext))
|
||||
|
||||
@classmethod
|
||||
def update_or_create(cls, metadata, force=False):
|
||||
"""Try to update existent plugin or create new one.
|
||||
|
||||
:param dict metadata: plugin information
|
||||
:param bool force: updates existent plugin even if
|
||||
it is not updatable
|
||||
"""
|
||||
# Try to update plugin
|
||||
plugin_for_update = cls.get_plugin_for_update(metadata)
|
||||
if plugin_for_update:
|
||||
url = cls.class_instance_path.format(id=plugin_for_update['id'])
|
||||
resp = cls.connection.put_request(url, metadata)
|
||||
return resp
|
||||
|
||||
# If plugin is not updatable it means that we should
|
||||
# create new instance in Nailgun
|
||||
resp_raw = cls.connection.post_request_raw(
|
||||
cls.class_api_path, metadata)
|
||||
resp = resp_raw.json()
|
||||
|
||||
if resp_raw.status_code == 409 and force:
|
||||
# Replace plugin information
|
||||
message = json.loads(resp['message'])
|
||||
url = cls.class_instance_path.format(id=message['id'])
|
||||
resp = cls.connection.put_request(url, metadata)
|
||||
elif resp_raw.status_code == 409:
|
||||
error.exit_with_error(
|
||||
"Nothing to do: %(title)s, version "
|
||||
"%(package_version)s, does not update "
|
||||
"installed plugin." % metadata)
|
||||
else:
|
||||
resp_raw.raise_for_status()
|
||||
|
||||
return resp
|
||||
|
||||
@classmethod
|
||||
def get_plugin_for_update(cls, metadata):
|
||||
"""Retrieves plugins which can be updated
|
||||
|
||||
:param dict metadata: plugin metadata
|
||||
:returns: dict with plugin which can be updated or None
|
||||
"""
|
||||
if not cls.is_updatable(metadata['package_version']):
|
||||
return
|
||||
|
||||
plugins = [p for p in cls.get_all_data()
|
||||
if (p['name'] == metadata['name'] and
|
||||
cls.is_updatable(p['package_version']) and
|
||||
utils.major_plugin_version(metadata['version']) ==
|
||||
utils.major_plugin_version(p['version']))]
|
||||
|
||||
plugin = None
|
||||
if plugins:
|
||||
# List should contain only one plugin, but just
|
||||
# in case we make sure that we get plugin with
|
||||
# higher version
|
||||
plugin = sorted(
|
||||
plugins,
|
||||
key=lambda p: StrictVersion(p['version']))[0]
|
||||
|
||||
return plugin
|
||||
|
||||
@classmethod
|
||||
def is_updatable(cls, package_version):
|
||||
"""Checks if plugin's package version supports updates.
|
||||
|
||||
:param str package_version: package version of the plugin
|
||||
:returns: True if plugin can be updated
|
||||
False if plugin cannot be updated
|
||||
"""
|
||||
return StrictVersion('2.0.0') <= StrictVersion(package_version)
|
||||
|
||||
@classmethod
|
||||
def get_plugin(cls, name, version):
|
||||
"""Returns plugin fetched by name and version.
|
||||
|
||||
:param str name: plugin name
|
||||
:param str version: plugin version
|
||||
:returns: dictionary with plugin data
|
||||
:raises: error.BadDataException if no plugin was found
|
||||
"""
|
||||
plugins = [p for p in cls.get_all_data()
|
||||
if (p['name'], p['version']) == (name, version)]
|
||||
|
||||
if not plugins:
|
||||
raise error.BadDataException(
|
||||
'Plugin "{name}" with version {version}, does '
|
||||
'not exist'.format(name=name, version=version))
|
||||
|
||||
return plugins[0]
|
@ -1,53 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class Release(BaseObject):
|
||||
|
||||
class_api_path = "releases/"
|
||||
instance_api_path = "releases/{0}/"
|
||||
networks_path = 'releases/{0}/networks'
|
||||
attributes_metadata_path = 'releases/{0}/attributes_metadata'
|
||||
deployment_tasks_path = 'releases/{0}/deployment_tasks'
|
||||
components_path = 'releases/{0}/components'
|
||||
|
||||
def get_networks(self):
|
||||
url = self.networks_path.format(self.id)
|
||||
return self.connection.get_request(url)
|
||||
|
||||
def update_networks(self, data):
|
||||
url = self.networks_path.format(self.id)
|
||||
return self.connection.put_request(url, data)
|
||||
|
||||
def update_attributes_metadata(self, data):
|
||||
url = self.attributes_metadata_path.format(self.id)
|
||||
self.connection.put_request(url, data)
|
||||
|
||||
def get_attributes_metadata(self):
|
||||
url = self.attributes_metadata_path.format(self.id)
|
||||
return self.connection.get_request(url)
|
||||
|
||||
def get_deployment_tasks(self):
|
||||
url = self.deployment_tasks_path.format(self.id)
|
||||
return self.connection.get_request(url)
|
||||
|
||||
def update_deployment_tasks(self, data):
|
||||
url = self.deployment_tasks_path.format(self.id)
|
||||
return self.connection.put_request(url, data)
|
||||
|
||||
def get_components(self):
|
||||
url = self.components_path.format(self.id)
|
||||
return self.connection.get_request(url)
|
@ -1,59 +0,0 @@
|
||||
# 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.
|
||||
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class Role(BaseObject):
|
||||
|
||||
instance_api_path = "{owner_type}/{owner_id}/roles/"
|
||||
class_api_path = "{owner_type}/{owner_id}/roles/{role_name}/"
|
||||
|
||||
def __init__(self, owner_type, owner_id, **kwargs):
|
||||
super(Role, self).__init__(owner_id, **kwargs)
|
||||
self.owner_type = owner_type
|
||||
|
||||
def get_all(self):
|
||||
return self.connection.get_request(
|
||||
self.instance_api_path.format(
|
||||
owner_type=self.owner_type,
|
||||
owner_id=self.id))
|
||||
|
||||
def get_role(self, role_name):
|
||||
return self.connection.get_request(
|
||||
self.class_api_path.format(
|
||||
owner_type=self.owner_type,
|
||||
owner_id=self.id,
|
||||
role_name=role_name))
|
||||
|
||||
def update_role(self, role_name, data):
|
||||
return self.connection.put_request(
|
||||
self.class_api_path.format(
|
||||
owner_type=self.owner_type,
|
||||
owner_id=self.id,
|
||||
role_name=role_name),
|
||||
data)
|
||||
|
||||
def create_role(self, data):
|
||||
return self.connection.post_request(
|
||||
self.instance_api_path.format(
|
||||
owner_type=self.owner_type, owner_id=self.id), data)
|
||||
|
||||
def delete_role(self, role_name):
|
||||
return self.connection.delete_request(
|
||||
self.class_api_path.format(
|
||||
owner_type=self.owner_type,
|
||||
owner_id=self.id,
|
||||
role_name=role_name))
|
@ -1,21 +0,0 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class Sequence(BaseObject):
|
||||
|
||||
class_api_path = "sequences/"
|
||||
instance_api_path = "sequences/{0}/"
|
@ -1,56 +0,0 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class Tag(BaseObject):
|
||||
|
||||
instance_api_path = "{owner_type}/{owner_id}/tags/"
|
||||
class_api_path = "{owner_type}/{owner_id}/tags/{tag_name}/"
|
||||
|
||||
def __init__(self, owner_type, owner_id, **kwargs):
|
||||
super(Tag, self).__init__(owner_id, **kwargs)
|
||||
self.owner_type = owner_type
|
||||
|
||||
def get_all(self):
|
||||
return self.connection.get_request(
|
||||
self.instance_api_path.format(owner_type=self.owner_type,
|
||||
owner_id=self.id))
|
||||
|
||||
def get_tag(self, tag_name):
|
||||
return self.connection.get_request(
|
||||
self.class_api_path.format(owner_type=self.owner_type,
|
||||
owner_id=self.id,
|
||||
tag_name=tag_name))
|
||||
|
||||
def update_tag(self, tag_name, data):
|
||||
return self.connection.put_request(
|
||||
self.class_api_path.format(owner_type=self.owner_type,
|
||||
owner_id=self.id,
|
||||
tag_name=tag_name),
|
||||
data)
|
||||
|
||||
def create_tag(self, data):
|
||||
return self.connection.post_request(
|
||||
self.instance_api_path.format(owner_type=self.owner_type,
|
||||
owner_id=self.id),
|
||||
data)
|
||||
|
||||
def delete_tag(self, tag_name):
|
||||
return self.connection.delete_request(
|
||||
self.class_api_path.format(owner_type=self.owner_type,
|
||||
owner_id=self.id,
|
||||
tag_name=tag_name))
|
@ -1,133 +0,0 @@
|
||||
# Copyright 2014 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.
|
||||
|
||||
from operator import methodcaller
|
||||
from time import sleep
|
||||
|
||||
from fuelclient.cli.error import DeployProgressError
|
||||
from fuelclient.objects.base import BaseObject
|
||||
|
||||
|
||||
class Task(BaseObject):
|
||||
|
||||
class_api_path = "transactions/"
|
||||
instance_api_path = "transactions/{0}/"
|
||||
info_types_url_map = {
|
||||
'deployment_info': 'deployment_info',
|
||||
'cluster_settings': 'settings',
|
||||
'network_configuration': 'network_configuration'}
|
||||
|
||||
def delete(self, force=False):
|
||||
return self.connection.delete_request(
|
||||
"transactions/{0}/?force={1}".format(
|
||||
self.id,
|
||||
int(force),
|
||||
))
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
return self.get_fresh_data()["progress"]
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.get_fresh_data()["status"]
|
||||
|
||||
@property
|
||||
def is_finished(self):
|
||||
return self.status in ("ready", "error")
|
||||
|
||||
def wait(self):
|
||||
while not self.is_finished:
|
||||
sleep(0.5)
|
||||
|
||||
def deployment_info(self):
|
||||
return self.connection.get_request(
|
||||
self._get_additional_info_url('deployment_info'))
|
||||
|
||||
def network_configuration(self):
|
||||
return self.connection.get_request(
|
||||
self._get_additional_info_url('network_configuration'))
|
||||
|
||||
def cluster_settings(self):
|
||||
return self.connection.get_request(
|
||||
self._get_additional_info_url('cluster_settings'))
|
||||
|
||||
def _get_additional_info_url(self, info_type):
|
||||
"""Generate additional info url.
|
||||
|
||||
:param info_type: one of deployment_info, cluster_settings,
|
||||
network_configuration
|
||||
:type info_type: str
|
||||
:return: url
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
return self.instance_api_path.format(self.id) +\
|
||||
self.info_types_url_map[info_type]
|
||||
|
||||
|
||||
class DeployTask(Task):
|
||||
|
||||
def __init__(self, obj_id, env_id):
|
||||
from fuelclient.objects.environment import Environment
|
||||
super(DeployTask, self).__init__(obj_id)
|
||||
self.env = Environment(env_id)
|
||||
self.nodes = self.env.get_all_nodes()
|
||||
|
||||
@classmethod
|
||||
def init_with_data(cls, data):
|
||||
return cls(data["id"], data["cluster"])
|
||||
|
||||
@property
|
||||
def not_finished_nodes(self):
|
||||
return filter(
|
||||
lambda n: not n.is_finished(latest=False),
|
||||
self.nodes
|
||||
)
|
||||
|
||||
@property
|
||||
def is_finished(self):
|
||||
return super(DeployTask, self).is_finished and all(
|
||||
map(
|
||||
methodcaller("is_finished"),
|
||||
self.not_finished_nodes
|
||||
)
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
if not self.is_finished:
|
||||
sleep(1)
|
||||
deploy_task_data = self.get_fresh_data()
|
||||
if deploy_task_data["status"] == "error":
|
||||
raise DeployProgressError(deploy_task_data["message"])
|
||||
for node in self.not_finished_nodes:
|
||||
node.update()
|
||||
return self.progress, self.nodes
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
|
||||
class SnapshotTask(Task):
|
||||
|
||||
@classmethod
|
||||
def start_snapshot_task(cls, conf):
|
||||
dump_task = cls.connection.put_request("logs/package", conf)
|
||||
return cls(dump_task["id"])
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls):
|
||||
return cls.connection.get_request("logs/package/config/default/")
|
@ -1,99 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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 cProfile
|
||||
import os
|
||||
from pstats import Stats
|
||||
import time
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient import fuelclient_settings
|
||||
|
||||
|
||||
def profiling_enabled():
|
||||
settings = fuelclient_settings.get_settings()
|
||||
return bool(settings.PERFORMANCE_PROFILING_TESTS)
|
||||
|
||||
|
||||
class Profiler(object):
|
||||
"""Runs profiler and saves results."""
|
||||
|
||||
def __init__(self, method='', handler_name=''):
|
||||
self.method = method
|
||||
self.handler_name = handler_name
|
||||
settings = fuelclient_settings.get_settings()
|
||||
self.paths = settings.PERF_TESTS_PATHS
|
||||
|
||||
if not os.path.exists(self.paths['last_performance_test']):
|
||||
os.makedirs(self.paths['last_performance_test'])
|
||||
|
||||
self.profiler = cProfile.Profile()
|
||||
self.profiler.enable()
|
||||
self.start = time.time()
|
||||
|
||||
def save_data(self):
|
||||
try:
|
||||
import gprof2dot
|
||||
import pyprof2calltree
|
||||
except ImportError:
|
||||
msg = ('Unable to start profiling.\n Please either '
|
||||
'disable performance profiling in settings.yaml or '
|
||||
'install all modules listed in test-requirements.txt.')
|
||||
raise error.ProfilingError(msg)
|
||||
|
||||
self.profiler.disable()
|
||||
elapsed = time.time() - self.start
|
||||
pref_filename = os.path.join(
|
||||
self.paths['last_performance_test'],
|
||||
'{method:s}.{handler_name:s}.{elapsed_time:.0f}ms.{t_time}.'.
|
||||
format(
|
||||
method=self.method,
|
||||
handler_name=self.handler_name or 'root',
|
||||
elapsed_time=elapsed * 1000.0,
|
||||
t_time=time.time()))
|
||||
tree_file = pref_filename + 'prof'
|
||||
stats_file = pref_filename + 'txt'
|
||||
callgraph_file = pref_filename + 'dot'
|
||||
|
||||
# write pstats
|
||||
with file(stats_file, 'w') as file_o:
|
||||
stats = Stats(self.profiler, stream=file_o)
|
||||
stats.sort_stats('time', 'cumulative').print_stats()
|
||||
|
||||
# write callgraph in dot format
|
||||
parser = gprof2dot.PstatsParser(self.profiler)
|
||||
|
||||
def get_function_name(args):
|
||||
filename, line, name = args
|
||||
module = os.path.splitext(filename)[0]
|
||||
module_pieces = module.split(os.path.sep)
|
||||
return "{module:s}:{line:d}:{name:s}".format(
|
||||
module="/".join(module_pieces[-4:]),
|
||||
line=line,
|
||||
name=name)
|
||||
|
||||
parser.get_function_name = get_function_name
|
||||
gprof = parser.parse()
|
||||
|
||||
with open(callgraph_file, 'w') as file_o:
|
||||
dot = gprof2dot.DotWriter(file_o)
|
||||
theme = gprof2dot.TEMPERATURE_COLORMAP
|
||||
dot.graph(gprof, theme)
|
||||
|
||||
# write calltree
|
||||
call_tree = pyprof2calltree.CalltreeConverter(stats)
|
||||
with file(tree_file, 'wb') as file_o:
|
||||
call_tree.output(file_o)
|
@ -1,218 +0,0 @@
|
||||
# Copyright 2013-2014 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 json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from fuelclient import consts
|
||||
from fuelclient.objects import Release
|
||||
|
||||
from oslotest import base as oslo_base
|
||||
|
||||
logging.basicConfig(stream=sys.stderr)
|
||||
log = logging.getLogger("CliTest.ExecutionLog")
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class CliExecutionResult(object):
|
||||
def __init__(self, process_handle, out, err):
|
||||
self.return_code = process_handle.returncode
|
||||
self.stdout = out
|
||||
self.stderr = err
|
||||
|
||||
@property
|
||||
def has_errors(self):
|
||||
return self.return_code != 0
|
||||
|
||||
@property
|
||||
def is_return_code_zero(self):
|
||||
return self.return_code == 0
|
||||
|
||||
|
||||
class BaseTestCase(oslo_base.BaseTestCase):
|
||||
|
||||
handler = ''
|
||||
nailgun_root = os.environ.get('NAILGUN_ROOT', '/tmp/fuel_web/nailgun')
|
||||
fuel_web_root = os.environ.get('FUEL_WEB_ROOT', '/tmp/fuel_web')
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestCase, self).setUp()
|
||||
|
||||
self.reload_nailgun_server()
|
||||
self.temp_directory = tempfile.mkdtemp()
|
||||
|
||||
self.addCleanup(shutil.rmtree, self.temp_directory)
|
||||
|
||||
@staticmethod
|
||||
def run_command(*args, **kwargs):
|
||||
handle = subprocess.Popen(
|
||||
[" ".join(args)],
|
||||
stdout=kwargs.pop('stdout', subprocess.PIPE),
|
||||
stderr=kwargs.pop('stderr', subprocess.PIPE),
|
||||
shell=kwargs.pop('shell', True),
|
||||
**kwargs
|
||||
)
|
||||
log.debug("Running " + " ".join(args))
|
||||
out, err = handle.communicate()
|
||||
log.debug("Finished command with {0} - {1}".format(out, err))
|
||||
|
||||
def upload_command(self, cmd):
|
||||
return "{0} --upload --dir {1}".format(cmd, self.temp_directory)
|
||||
|
||||
def download_command(self, cmd):
|
||||
return "{0} --download --dir {1}".format(cmd, self.temp_directory)
|
||||
|
||||
@classmethod
|
||||
def reload_nailgun_server(cls):
|
||||
for action in ("dropdb", "syncdb", "loaddefault"):
|
||||
cmd = 'tox -evenv -- {0}/manage.py {1}'.format(
|
||||
cls.nailgun_root, action)
|
||||
cls.run_command(cmd, cwd=cls.fuel_web_root)
|
||||
|
||||
@classmethod
|
||||
def load_data_to_nailgun_server(cls):
|
||||
file_path = os.path.join(cls.nailgun_root,
|
||||
'nailgun/fixtures/sample_environment.json')
|
||||
cmd = 'tox -evenv -- {0}/manage.py loaddata {1}'.format(
|
||||
cls.nailgun_root, file_path)
|
||||
cls.run_command(cmd, cwd=cls.fuel_web_root)
|
||||
|
||||
def run_cli_command(self, command_line, handler=None,
|
||||
check_errors=True, env=os.environ.copy()):
|
||||
|
||||
command_args = [" ".join((handler or self.handler, command_line))]
|
||||
process_handle = subprocess.Popen(
|
||||
command_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True,
|
||||
env=env
|
||||
)
|
||||
out, err = process_handle.communicate()
|
||||
result = CliExecutionResult(process_handle, out, err)
|
||||
log.debug("command_args: '%s',stdout: '%s', stderr: '%s'",
|
||||
command_args[0], out, err)
|
||||
if check_errors:
|
||||
if not result.is_return_code_zero or result.has_errors:
|
||||
self.fail(err)
|
||||
return result
|
||||
|
||||
def get_first_deployable_release_id(self):
|
||||
releases = sorted(Release.get_all_data(),
|
||||
key=lambda x: x['id'])
|
||||
for r in releases:
|
||||
if r['is_deployable']:
|
||||
return r['id']
|
||||
self.fail("There are no deployable releases.")
|
||||
|
||||
def run_cli_commands(self, command_lines, **kwargs):
|
||||
for command in command_lines:
|
||||
self.run_cli_command(command, **kwargs)
|
||||
|
||||
def check_if_required(self, command):
|
||||
call = self.run_cli_command(command, check_errors=False)
|
||||
# should not work without env id
|
||||
self.assertIn("required", call.stderr)
|
||||
|
||||
def check_for_stdout(self, command, msg, check_errors=True):
|
||||
call = self.run_cli_command(command, check_errors=check_errors)
|
||||
self.assertEqual(call.stdout, msg)
|
||||
|
||||
def check_for_stdout_by_regexp(self, command, pattern, check_errors=True):
|
||||
call = self.run_cli_command(command, check_errors=check_errors)
|
||||
result = re.search(pattern, call.stdout)
|
||||
self.assertIsNotNone(result)
|
||||
return result
|
||||
|
||||
def check_for_stderr(self, command, msg, check_errors=True):
|
||||
call = self.run_cli_command(command, check_errors=check_errors)
|
||||
self.assertIn(msg, call.stderr)
|
||||
|
||||
def check_all_in_msg(self, command, substrings, **kwargs):
|
||||
output = self.run_cli_command(command, **kwargs)
|
||||
for substring in substrings:
|
||||
self.assertIn(substring, output.stdout)
|
||||
|
||||
def check_for_rows_in_table(self, command):
|
||||
output = self.run_cli_command(command)
|
||||
message = output.stdout.split("\n")
|
||||
# no env
|
||||
self.assertEqual(message[2], '')
|
||||
|
||||
def check_number_of_rows_in_table(self, command, number_of_rows):
|
||||
output = self.run_cli_command(command)
|
||||
self.assertEqual(len(output.stdout.split("\n")), number_of_rows + 3)
|
||||
|
||||
def _get_task_info(self, task_id):
|
||||
"""Get info about task with given ID.
|
||||
|
||||
:param task_id: Task ID
|
||||
:type task_id: str or int
|
||||
:return: Task info
|
||||
:rtype: dict
|
||||
"""
|
||||
return {}
|
||||
|
||||
def wait_task_ready(self, task_id, timeout=60, interval=3):
|
||||
"""Wait for changing task status to 'ready'.
|
||||
|
||||
:param task_id: Task ID
|
||||
:type task_id: str or int
|
||||
:param timeout: Max time of waiting, in seconds
|
||||
:type timeout: int
|
||||
:param interval: Interval of getting task info, in seconds
|
||||
:type interval: int
|
||||
"""
|
||||
wait_until_in_statuses = (consts.TASK_STATUSES.running,
|
||||
consts.TASK_STATUSES.pending)
|
||||
timer = time.time()
|
||||
while True:
|
||||
task = self._get_task_info(task_id)
|
||||
status = task.get('status', '')
|
||||
if status not in wait_until_in_statuses:
|
||||
self.assertEqual(status, consts.TASK_STATUSES.ready)
|
||||
break
|
||||
|
||||
if time.time() - timer > timeout:
|
||||
raise Exception(
|
||||
"Task '{0}' seems to be hanged".format(task['name'])
|
||||
)
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
class CLIv1TestCase(BaseTestCase):
|
||||
|
||||
handler = 'fuel'
|
||||
|
||||
def _get_task_info(self, task_id):
|
||||
command = "task show -f json {0}".format(str(task_id))
|
||||
call = self.run_cli_command(command, handler='fuel2')
|
||||
return json.loads(call.stdout)
|
||||
|
||||
|
||||
class CLIv2TestCase(BaseTestCase):
|
||||
|
||||
handler = 'fuel2'
|
||||
|
||||
def _get_task_info(self, task_id):
|
||||
command = "task show -f json {0}".format(str(task_id))
|
||||
call = self.run_cli_command(command)
|
||||
return json.loads(call.stdout)
|
@ -1,516 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013-2014 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 os
|
||||
import tempfile
|
||||
|
||||
from fuelclient.tests.functional import base
|
||||
|
||||
|
||||
class TestHandlers(base.CLIv1TestCase):
|
||||
|
||||
def test_env_action(self):
|
||||
# check env help
|
||||
help_msgs = ["usage: fuel environment [-h]",
|
||||
"[--list | --set | --delete | --create]",
|
||||
"optional arguments:", "--help", "--list", "--set",
|
||||
"--delete", "--rel", "--env-create",
|
||||
"--create", "--name", "--env-name", "--nst",
|
||||
"--net-segment-type"]
|
||||
self.check_all_in_msg("env --help", help_msgs)
|
||||
# no clusters
|
||||
self.check_for_rows_in_table("env")
|
||||
|
||||
for action in ("set", "create", "delete"):
|
||||
self.check_if_required("env {0}".format(action))
|
||||
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
|
||||
# list of tuples (<fuel CLI command>, <expected output of a command>)
|
||||
expected_stdout = \
|
||||
[(
|
||||
"env --create --name=TestEnv --release={0}".format(release_id),
|
||||
"Environment 'TestEnv' with id=1 was created!\n"
|
||||
), (
|
||||
"--env-id=1 env set --name=NewEnv",
|
||||
("Following attributes are changed for "
|
||||
"the environment: name=NewEnv\n")
|
||||
)]
|
||||
|
||||
for cmd, msg in expected_stdout:
|
||||
self.check_for_stdout(cmd, msg)
|
||||
|
||||
def test_node_action(self):
|
||||
help_msg = ["fuel node [-h] [--env ENV]",
|
||||
"[--list | --set | --delete | --attributes |"
|
||||
" --network | --disk | --deploy |"
|
||||
" --hostname HOSTNAME | --name NAME |"
|
||||
" --delete-from-db | --provision]", "-h", "--help", " -s",
|
||||
"--default", " -d", "--download", " -u",
|
||||
"--upload", "--dir", "--node", "--node-id", " -r",
|
||||
"--role", "--net", "--hostname", "--name"]
|
||||
self.check_all_in_msg("node --help", help_msg)
|
||||
|
||||
self.check_for_rows_in_table("node")
|
||||
|
||||
for action in ("set", "remove", "--network", "--disk"):
|
||||
self.check_if_required("node {0}".format(action))
|
||||
|
||||
self.load_data_to_nailgun_server()
|
||||
self.check_number_of_rows_in_table("node --node 9f:b6,9d:24,ab:aa", 3)
|
||||
|
||||
def test_selected_node_provision(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 1 --role=controller"
|
||||
))
|
||||
cmd = "--env-id=1 node --provision --node=1"
|
||||
msg = "Started provisioning nodes [1].\n"
|
||||
|
||||
self.check_for_stdout(cmd, msg)
|
||||
|
||||
def test_help_works_without_connection(self):
|
||||
fake_config = 'SERVER_ADDRESS: "333.333.333.333"'
|
||||
|
||||
c_handle, c_path = tempfile.mkstemp(suffix='.json', text=True)
|
||||
with open(c_path, 'w') as f:
|
||||
f.write(fake_config)
|
||||
|
||||
env = os.environ.copy()
|
||||
env['FUELCLIENT_CUSTOM_SETTINGS'] = c_path
|
||||
|
||||
try:
|
||||
result = self.run_cli_command("--help", env=env)
|
||||
self.assertEqual(result.return_code, 0)
|
||||
finally:
|
||||
os.remove(c_path)
|
||||
|
||||
def test_error_when_destroying_online_node(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 1 --role=controller"
|
||||
), check_errors=False)
|
||||
msg = ("Nodes with ids [1] cannot be deleted from cluster because "
|
||||
"they are online. You might want to use the --force option.\n")
|
||||
self.check_for_stderr(
|
||||
"node --node 1 --delete-from-db",
|
||||
msg,
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_force_destroy_online_node(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 1 --role=controller"
|
||||
))
|
||||
msg = ("Nodes with ids [1] have been deleted from Fuel db.\n")
|
||||
self.check_for_stdout(
|
||||
"node --node 1 --delete-from-db --force",
|
||||
msg
|
||||
)
|
||||
|
||||
def test_destroy_offline_node(self):
|
||||
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
node_id = 4
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node {0} --role=controller".format(node_id)
|
||||
))
|
||||
msg = ("Nodes with ids [{0}] have been deleted from Fuel db.\n".format(
|
||||
node_id))
|
||||
self.check_for_stdout(
|
||||
"node --node {0} --delete-from-db".format(node_id),
|
||||
msg
|
||||
)
|
||||
|
||||
def test_node_change_hostname(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 2 --role=controller"
|
||||
))
|
||||
msg = "Hostname for node with id 2 has been changed to test-name.\n"
|
||||
self.check_for_stdout(
|
||||
"node --node 2 --hostname test-name",
|
||||
msg
|
||||
)
|
||||
|
||||
def test_env_create_neutron_tun(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.check_for_stdout(
|
||||
"env create --name=NewEnv --release={0} --nst=tun"
|
||||
.format(release_id),
|
||||
"Environment 'NewEnv' with id=1 was created!\n")
|
||||
|
||||
def test_destroy_multiple_nodes(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 1 2 --role=controller"
|
||||
))
|
||||
msg = ("Nodes with ids [1, 2] have been deleted from Fuel db.\n")
|
||||
self.check_for_stdout(
|
||||
"node --node 1 2 --delete-from-db --force",
|
||||
msg
|
||||
)
|
||||
|
||||
def test_for_examples_in_action_help(self):
|
||||
actions = (
|
||||
"node", "stop", "deployment", "reset", "network",
|
||||
"settings", "provisioning", "environment", "deploy-changes",
|
||||
"role", "release", "snapshot", "health", "vip"
|
||||
)
|
||||
for action in actions:
|
||||
self.check_all_in_msg("{0} -h".format(action), ("Examples",))
|
||||
|
||||
def test_get_release_list_without_errors(self):
|
||||
cmd = 'release --list'
|
||||
self.run_cli_command(cmd)
|
||||
|
||||
def test_reassign_node_group(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0} --nst=gre"
|
||||
.format(release_id),
|
||||
"--env-id=1 node set --node 1 2 --role=controller",
|
||||
"nodegroup --create --env 1 --name 'new group'"
|
||||
))
|
||||
msg = ['PUT http://127.0.0.1',
|
||||
'/api/v1/nodes/ data=',
|
||||
'"id": 1',
|
||||
'"group_id": 2']
|
||||
self.check_all_in_msg(
|
||||
"nodegroup --assign --group 2 --node 1 --debug",
|
||||
msg
|
||||
)
|
||||
|
||||
def test_node_group_creation_prints_warning_w_seg_type_vlan(self):
|
||||
warn_msg = ("WARNING: In VLAN segmentation type, there will be no "
|
||||
"connectivity over private network between instances "
|
||||
"running on hypervisors in different segments and that "
|
||||
"it's a user's responsibility to handle this "
|
||||
"situation.")
|
||||
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0} --nst=vlan"
|
||||
.format(release_id),
|
||||
|
||||
))
|
||||
self.check_for_stderr(
|
||||
"nodegroup create --name tor1 --env 1",
|
||||
warn_msg,
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_create_network_group_fails_w_duplicate_name(self):
|
||||
err_msg = ("(Network with name storage already exists "
|
||||
"in node group default)\n")
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0} --nst=gre"
|
||||
.format(release_id),
|
||||
))
|
||||
self.check_for_stderr(
|
||||
("network-group --create --name storage --node-group 1 "
|
||||
"--vlan 10 --cidr 10.0.0.0/24"),
|
||||
err_msg,
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_create_network_group_fails_w_invalid_group(self):
|
||||
err_msg = "(Node group with ID 997755 does not exist)\n"
|
||||
|
||||
self.check_for_stderr(
|
||||
("network-group --create --name test --node-group 997755 "
|
||||
"--vlan 10 --cidr 10.0.0.0/24"),
|
||||
err_msg,
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
|
||||
class TestCharset(base.CLIv1TestCase):
|
||||
|
||||
def test_charset_problem(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=привет --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 1 --role=controller",
|
||||
"env"
|
||||
))
|
||||
|
||||
|
||||
class TestFiles(base.CLIv1TestCase):
|
||||
|
||||
def test_file_creation(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 1 --role=controller",
|
||||
"--env-id=1 node set --node 2,3 --role=compute"
|
||||
))
|
||||
for action in ("network", "settings"):
|
||||
for format_ in ("yaml", "json"):
|
||||
self.check_if_files_created(
|
||||
"--env 1 {0} --download --{1}".format(action, format_),
|
||||
("{0}_1.{1}".format(action, format_),)
|
||||
)
|
||||
command_to_files_map = (
|
||||
(
|
||||
"--env 1 deployment --default",
|
||||
(
|
||||
"deployment_1",
|
||||
"deployment_1/1.yaml",
|
||||
"deployment_1/2.yaml",
|
||||
"deployment_1/3.yaml"
|
||||
)
|
||||
),
|
||||
(
|
||||
"--env 1 provisioning --default",
|
||||
(
|
||||
"provisioning_1",
|
||||
"provisioning_1/engine.yaml",
|
||||
"provisioning_1/node-1.yaml",
|
||||
"provisioning_1/node-2.yaml",
|
||||
"provisioning_1/node-3.yaml"
|
||||
)
|
||||
),
|
||||
(
|
||||
"--env 1 deployment --default --json",
|
||||
(
|
||||
"deployment_1/1.json",
|
||||
"deployment_1/2.json",
|
||||
"deployment_1/3.json"
|
||||
)
|
||||
),
|
||||
(
|
||||
"--env 1 provisioning --default --json",
|
||||
(
|
||||
"provisioning_1/engine.json",
|
||||
"provisioning_1/node-1.json",
|
||||
"provisioning_1/node-2.json",
|
||||
"provisioning_1/node-3.json"
|
||||
)
|
||||
),
|
||||
(
|
||||
"node --node 1 --disk --default",
|
||||
(
|
||||
"node_1",
|
||||
"node_1/disks.yaml"
|
||||
)
|
||||
),
|
||||
(
|
||||
"node --node 1 --network --default",
|
||||
(
|
||||
"node_1",
|
||||
"node_1/interfaces.yaml"
|
||||
)
|
||||
),
|
||||
(
|
||||
"node --node 1 --disk --default --json",
|
||||
(
|
||||
"node_1/disks.json",
|
||||
)
|
||||
),
|
||||
(
|
||||
"node --node 1 --network --default --json",
|
||||
(
|
||||
"node_1/interfaces.json",
|
||||
)
|
||||
)
|
||||
)
|
||||
for command, files in command_to_files_map:
|
||||
self.check_if_files_created(command, files)
|
||||
|
||||
def check_if_files_created(self, command, paths):
|
||||
command_in_dir = "{0} --dir={1}".format(command, self.temp_directory)
|
||||
self.run_cli_command(command_in_dir)
|
||||
for path in paths:
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(self.temp_directory, path)
|
||||
))
|
||||
|
||||
|
||||
class TestDownloadUploadNodeAttributes(base.CLIv1TestCase):
|
||||
|
||||
def test_upload_download_interfaces(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
env_create = "env create --name=test --release={0}".format(release_id)
|
||||
add_node = "--env-id=1 node set --node 1 --role=controller"
|
||||
|
||||
cmd = "node --node-id 1 --network"
|
||||
self.run_cli_commands((env_create,
|
||||
add_node,
|
||||
self.download_command(cmd),
|
||||
self.upload_command(cmd)))
|
||||
|
||||
def test_upload_download_disks(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
cmd = "node --node-id 1 --disk"
|
||||
self.run_cli_commands((self.download_command(cmd),
|
||||
self.upload_command(cmd)))
|
||||
|
||||
|
||||
class TestDeployChanges(base.CLIv1TestCase):
|
||||
|
||||
cmd_create_env = "env create --name=test --release={0}"
|
||||
cmd_add_node = "--env-id=1 node set --node 1 --role=controller"
|
||||
cmd_deploy_changes = "deploy-changes --env 1"
|
||||
cmd_redeploy_changes = "redeploy-changes --env 1"
|
||||
|
||||
pattern_success = (r"^Deployment task with id (\d{1,}) "
|
||||
r"for the environment 1 has been started.\n$")
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeployChanges, self).setUp()
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.cmd_create_env = self.cmd_create_env.format(release_id)
|
||||
self.run_cli_commands((
|
||||
self.cmd_create_env,
|
||||
self.cmd_add_node
|
||||
))
|
||||
|
||||
def test_deploy_changes(self):
|
||||
self.check_for_stdout_by_regexp(self.cmd_deploy_changes,
|
||||
self.pattern_success)
|
||||
|
||||
def test_redeploy_changes(self):
|
||||
result = self.check_for_stdout_by_regexp(self.cmd_deploy_changes,
|
||||
self.pattern_success)
|
||||
task_id = result.group(1)
|
||||
self.wait_task_ready(task_id)
|
||||
self.check_for_stdout_by_regexp(self.cmd_redeploy_changes,
|
||||
self.pattern_success)
|
||||
|
||||
|
||||
class TestDirectoryDoesntExistErrorMessages(base.CLIv1TestCase):
|
||||
|
||||
def test_settings_upload(self):
|
||||
self.check_for_stderr(
|
||||
"settings --upload --dir /foo/bar/baz --env 1",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_deployment_upload(self):
|
||||
self.check_for_stderr(
|
||||
"deployment --upload --dir /foo/bar/baz --env 1",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_net_upload(self):
|
||||
self.check_for_stderr(
|
||||
"network --upload --dir /foo/bar/baz --env 1",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_env_download(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 2 --role=controller"
|
||||
))
|
||||
self.check_for_stderr(
|
||||
"network --download --dir /foo/bar/baz --env 1",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_download_network_configuration(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 2 --role=controller"
|
||||
))
|
||||
self.check_for_stderr(
|
||||
"--env 1 network --download --dir /foo/bar/baz",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_download_default_settings(self):
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.run_cli_commands((
|
||||
"env create --name=NewEnv --release={0}".format(release_id),
|
||||
"--env-id=1 node set --node 2 --role=controller"
|
||||
))
|
||||
self.check_for_stderr(
|
||||
"--env 1 settings --default --dir /foo/bar/baz",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_upload_network_configuration(self):
|
||||
self.check_for_stderr(
|
||||
"--env 1 network --upload --dir /foo/bar/baz",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
def test_upload_network_template(self):
|
||||
self.check_for_stderr(
|
||||
"--env 1 network-template --upload --dir /foo/bar/baz",
|
||||
"Directory '/foo/bar/baz' doesn't exist.\n",
|
||||
check_errors=False
|
||||
)
|
||||
|
||||
|
||||
class TestUploadSettings(base.CLIv1TestCase):
|
||||
|
||||
create_env = "env create --name=test --release={0}"
|
||||
add_node = "--env-id=1 node set --node 1 --role=controller"
|
||||
deploy_changes = "deploy-changes --env 1"
|
||||
cmd = "settings --env 1"
|
||||
cmd_force = "settings --env 1 --force"
|
||||
|
||||
def setUp(self):
|
||||
super(TestUploadSettings, self).setUp()
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.create_env = self.create_env.format(release_id)
|
||||
self.run_cli_commands((
|
||||
self.create_env,
|
||||
self.add_node,
|
||||
self.download_command(self.cmd)
|
||||
))
|
||||
|
||||
def test_upload_settings(self):
|
||||
msg_success = "Settings configuration uploaded.\n"
|
||||
self.check_for_stdout(self.upload_command(self.cmd),
|
||||
msg_success)
|
@ -1,82 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013-2014 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.
|
||||
|
||||
from fuelclient.tests.functional import base
|
||||
|
||||
|
||||
class TestDeployChanges(base.CLIv2TestCase):
|
||||
|
||||
cmd_create_env = "env create -r {0} cluster-test"
|
||||
cmd_add_node = "env add nodes -e 1 -n 1 -r controller"
|
||||
cmd_deploy_changes = "env deploy 1"
|
||||
cmd_redeploy_changes = "env redeploy 1"
|
||||
|
||||
pattern_success = (r"^Deployment task with id (\d{1,}) "
|
||||
r"for the environment 1 has been started.\n$")
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeployChanges, self).setUp()
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.cmd_create_env = self.cmd_create_env.format(release_id)
|
||||
self.run_cli_commands((
|
||||
self.cmd_create_env,
|
||||
self.cmd_add_node
|
||||
))
|
||||
|
||||
def test_deploy_changes(self):
|
||||
self.check_for_stdout_by_regexp(self.cmd_deploy_changes,
|
||||
self.pattern_success)
|
||||
|
||||
def test_redeploy_changes(self):
|
||||
result = self.check_for_stdout_by_regexp(self.cmd_deploy_changes,
|
||||
self.pattern_success)
|
||||
task_id = result.group(1)
|
||||
self.wait_task_ready(task_id)
|
||||
|
||||
self.check_for_stdout_by_regexp(self.cmd_redeploy_changes,
|
||||
self.pattern_success)
|
||||
|
||||
|
||||
class TestExtensionManagement(base.CLIv2TestCase):
|
||||
|
||||
cmd_create_env = "env create -r {0} cluster-test-extensions-mgmt"
|
||||
cmd_disable_exts = "env extension disable 1 --extensions volume_manager"
|
||||
cmd_enable_exts = "env extension enable 1 --extensions volume_manager"
|
||||
|
||||
pattern_enable_success = (r"^The following extensions: volume_manager "
|
||||
r"have been enabled for the environment with "
|
||||
r"id 1.\n$")
|
||||
pattern_disable_success = (r"^The following extensions: volume_manager "
|
||||
r"have been disabled for the environment with "
|
||||
r"id 1.\n$")
|
||||
|
||||
def setUp(self):
|
||||
super(TestExtensionManagement, self).setUp()
|
||||
self.load_data_to_nailgun_server()
|
||||
release_id = self.get_first_deployable_release_id()
|
||||
self.cmd_create_env = self.cmd_create_env.format(release_id)
|
||||
self.run_cli_commands((
|
||||
self.cmd_create_env,
|
||||
))
|
||||
|
||||
def test_disable_extensions(self):
|
||||
self.check_for_stdout_by_regexp(self.cmd_disable_exts,
|
||||
self.pattern_disable_success)
|
||||
|
||||
def test_enable_extensions(self):
|
||||
self.check_for_stdout_by_regexp(self.cmd_enable_exts,
|
||||
self.pattern_enable_success)
|
@ -1,145 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient import fuelclient_settings
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
|
||||
|
||||
class TestSettings(base.UnitTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSettings, self).setUp()
|
||||
|
||||
self.useFixture(fixtures.MockPatchObject(fuelclient_settings,
|
||||
'_SETTINGS',
|
||||
None))
|
||||
|
||||
@mock.patch('os.makedirs')
|
||||
@mock.patch('shutil.copy')
|
||||
@mock.patch('os.chmod')
|
||||
@mock.patch('os.path.exists')
|
||||
def test_config_generation(self, m_exists, m_chmod, m_copy, m_makedirs):
|
||||
project_dir = os.path.dirname(fuelclient_settings.__file__)
|
||||
|
||||
expected_fmode = 0o600
|
||||
expected_dmode = 0o700
|
||||
expected_default = os.path.join(project_dir,
|
||||
'fuel_client.yaml')
|
||||
expected_path = os.path.expanduser('~/.config/fuel/fuel_client.yaml')
|
||||
conf_home = os.path.expanduser('~/.config/')
|
||||
conf_dir = os.path.dirname(expected_path)
|
||||
|
||||
m_exists.return_value = False
|
||||
f_confdir = fixtures.EnvironmentVariable('XDG_CONFIG_HOME', conf_home)
|
||||
f_settings = fixtures.EnvironmentVariable('FUELCLIENT_CUSTOM_SETTINGS')
|
||||
|
||||
self.useFixture(f_confdir)
|
||||
self.useFixture(f_settings)
|
||||
|
||||
fuelclient_settings.get_settings()
|
||||
|
||||
m_makedirs.assert_called_once_with(conf_dir, expected_dmode)
|
||||
m_copy.assert_called_once_with(expected_default, expected_path)
|
||||
m_chmod.assert_called_once_with(expected_path, expected_fmode)
|
||||
|
||||
@mock.patch('os.makedirs')
|
||||
@mock.patch('os.path.exists')
|
||||
def test_config_generation_write_error(self, m_exists, m_makedirs):
|
||||
m_exists.return_value = False
|
||||
m_makedirs.side_effect = OSError('[Errno 13] Permission denied')
|
||||
|
||||
f_settings = fixtures.EnvironmentVariable('FUELCLIENT_CUSTOM_SETTINGS')
|
||||
self.useFixture(f_settings)
|
||||
|
||||
self.assertRaises(error.SettingsException,
|
||||
fuelclient_settings.get_settings)
|
||||
|
||||
@mock.patch('six.print_')
|
||||
def test_deprecated_option_produces_warning(self, m_print):
|
||||
expected_warings = [mock.call('DEPRECATION WARNING: LISTEN_PORT '
|
||||
'parameter was deprecated and will not '
|
||||
'be supported in the next version of '
|
||||
'python-fuelclient.', end='',
|
||||
file=sys.stderr),
|
||||
mock.call(' Please replace this '
|
||||
'parameter with SERVER_PORT',
|
||||
file=sys.stderr)]
|
||||
|
||||
m = mock.mock_open(read_data='LISTEN_PORT: 9000')
|
||||
with mock.patch('fuelclient.fuelclient_settings.open', m):
|
||||
fuelclient_settings.get_settings()
|
||||
|
||||
m_print.assert_has_calls(expected_warings, any_order=False)
|
||||
|
||||
@mock.patch('six.print_')
|
||||
def test_both_deprecated_and_new_options_produce_warning(self, m_print):
|
||||
expected_waring = ('WARNING: configuration contains both '
|
||||
'LISTEN_PORT and SERVER_PORT options set. Since '
|
||||
'LISTEN_PORT was deprecated, only the value of '
|
||||
'SERVER_PORT will be used.')
|
||||
|
||||
m = mock.mock_open(read_data='LISTEN_PORT: 9000\nSERVER_PORT: 9000')
|
||||
with mock.patch('fuelclient.fuelclient_settings.open', m):
|
||||
fuelclient_settings.get_settings()
|
||||
|
||||
m_print.assert_has_calls([mock.call(expected_waring, file=sys.stderr)])
|
||||
|
||||
@mock.patch('six.print_')
|
||||
def test_set_deprecated_option_overwrites_unset_new_option(self, m_print):
|
||||
m = mock.mock_open(read_data='KEYSTONE_PASS: "admin"\n'
|
||||
'OS_PASSWORD:\n')
|
||||
with mock.patch('fuelclient.fuelclient_settings.open', m):
|
||||
settings = fuelclient_settings.get_settings()
|
||||
|
||||
self.assertEqual('admin', settings.OS_PASSWORD)
|
||||
self.assertNotIn('OS_PASSWORD', settings.config)
|
||||
|
||||
def test_fallback_to_deprecated_option(self):
|
||||
m = mock.mock_open(read_data='LISTEN_PORT: 9000')
|
||||
with mock.patch('fuelclient.fuelclient_settings.open', m):
|
||||
settings = fuelclient_settings.get_settings()
|
||||
|
||||
self.assertEqual(9000, settings.LISTEN_PORT)
|
||||
|
||||
def test_update_from_cli_params(self):
|
||||
test_config_text = ('SERVER_ADDRESS: "127.0.0.1"\n'
|
||||
'SERVER_PORT: "8000"\n'
|
||||
'OS_USERNAME: "admin"\n'
|
||||
'OS_PASSWORD:\n'
|
||||
'OS_TENANT_NAME:\n')
|
||||
|
||||
test_parsed_args = mock.Mock(os_password='test_password',
|
||||
server_port="3000",
|
||||
os_username=None)
|
||||
del test_parsed_args.server_address
|
||||
del test_parsed_args.os_tenant_name
|
||||
|
||||
m = mock.mock_open(read_data=test_config_text)
|
||||
with mock.patch('fuelclient.fuelclient_settings.open', m):
|
||||
settings = fuelclient_settings.get_settings()
|
||||
|
||||
settings.update_from_command_line_options(test_parsed_args)
|
||||
|
||||
self.assertEqual('3000', settings.SERVER_PORT)
|
||||
self.assertEqual('test_password', settings.OS_PASSWORD)
|
||||
self.assertEqual('admin', settings.OS_USERNAME)
|
@ -1,127 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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 mock
|
||||
import os
|
||||
|
||||
from fuelclient import objects
|
||||
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
|
||||
|
||||
class TestEnvironmentObject(base.UnitTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEnvironmentObject, self).setUp()
|
||||
self.env_object = objects.Environment(1)
|
||||
|
||||
def _setup_os_mock(self, os_mock):
|
||||
os_mock.path.exists.return_value = False
|
||||
os_mock.path.join = os.path.join
|
||||
os_mock.path.abspath = lambda x: x
|
||||
|
||||
@mock.patch("fuelclient.objects.environment.os")
|
||||
def test_write_facts_to_dir_for_legacy_envs(self, os_mock):
|
||||
facts = [
|
||||
{
|
||||
"uid": "1",
|
||||
"role": "controller",
|
||||
"data": "data1"
|
||||
},
|
||||
{
|
||||
"uid": "2",
|
||||
"role": "compute",
|
||||
"data": "data2"
|
||||
},
|
||||
]
|
||||
|
||||
self._setup_os_mock(os_mock)
|
||||
serializer = mock.MagicMock()
|
||||
|
||||
self.env_object.write_facts_to_dir(
|
||||
"deployment", facts, serializer=serializer
|
||||
)
|
||||
|
||||
serializer.write_to_path.assert_has_calls(
|
||||
[
|
||||
mock.call("./deployment_1/controller_1", facts[0]),
|
||||
mock.call("./deployment_1/compute_2", facts[1])
|
||||
]
|
||||
)
|
||||
|
||||
@mock.patch("fuelclient.objects.environment.os")
|
||||
def test_write_facts_to_dir_for_new_envs(self, os_mock):
|
||||
facts = [
|
||||
{
|
||||
"uid": "1",
|
||||
"roles": ["controller"],
|
||||
"data": "data1"
|
||||
},
|
||||
{
|
||||
"uid": "2",
|
||||
"roles": ["compute"],
|
||||
"data": "data2"
|
||||
},
|
||||
]
|
||||
|
||||
self._setup_os_mock(os_mock)
|
||||
serializer = mock.MagicMock()
|
||||
|
||||
self.env_object.write_facts_to_dir(
|
||||
"deployment", facts, serializer=serializer
|
||||
)
|
||||
|
||||
serializer.write_to_path.assert_has_calls(
|
||||
[
|
||||
mock.call("./deployment_1/1", facts[0]),
|
||||
mock.call("./deployment_1/2", facts[1])
|
||||
]
|
||||
)
|
||||
|
||||
@mock.patch("fuelclient.objects.environment.os")
|
||||
def test_write_facts_to_dir_if_facts_is_dict(self, os_mock):
|
||||
facts = {
|
||||
"engine": "test_engine",
|
||||
"nodes": [
|
||||
{
|
||||
"uid": "1",
|
||||
"name": "node-1",
|
||||
"roles": ["controller"],
|
||||
"data": "data1"
|
||||
},
|
||||
{
|
||||
"uid": "2",
|
||||
"name": "node-2",
|
||||
"roles": ["compute"],
|
||||
"data": "data2"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
self._setup_os_mock(os_mock)
|
||||
serializer = mock.MagicMock()
|
||||
|
||||
self.env_object.write_facts_to_dir(
|
||||
"deployment", facts, serializer=serializer
|
||||
)
|
||||
|
||||
serializer.write_to_path.assert_has_calls(
|
||||
[
|
||||
mock.call("./deployment_1/engine", facts['engine']),
|
||||
mock.call("./deployment_1/node-1", facts['nodes'][0]),
|
||||
mock.call("./deployment_1/node-2", facts['nodes'][1])
|
||||
]
|
||||
)
|
@ -1,157 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 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 json
|
||||
import mock
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
|
||||
YAML_TEMPLATE = """adv_net_template:
|
||||
default:
|
||||
network_assignments:
|
||||
fuelweb_admin:
|
||||
ep: br-fw-admin
|
||||
management:
|
||||
ep: br-mgmt
|
||||
private:
|
||||
ep: br-prv
|
||||
public:
|
||||
ep: br-ex
|
||||
storage:
|
||||
ep: br-storage
|
||||
nic_mapping:
|
||||
default:
|
||||
if1: eth0
|
||||
templates_for_node_role:
|
||||
ceph-osd:
|
||||
- common
|
||||
- storage
|
||||
compute:
|
||||
- common
|
||||
- private
|
||||
- storage
|
||||
controller:
|
||||
- public
|
||||
- private
|
||||
- storage
|
||||
- common
|
||||
"""
|
||||
|
||||
JSON_TEMPLATE = """{
|
||||
"adv_net_template": {
|
||||
"default": {
|
||||
"nic_mapping": {
|
||||
"default": {
|
||||
"if1": "eth0"
|
||||
}
|
||||
},
|
||||
"templates_for_node_role": {
|
||||
"controller": [
|
||||
"public",
|
||||
"private",
|
||||
"storage",
|
||||
"common"
|
||||
],
|
||||
"compute": [
|
||||
"common",
|
||||
"private",
|
||||
"storage"
|
||||
],
|
||||
"ceph-osd": [
|
||||
"common",
|
||||
"storage"
|
||||
]
|
||||
},
|
||||
"network_assignments": {
|
||||
"storage": {
|
||||
"ep": "br-storage"
|
||||
},
|
||||
"private": {
|
||||
"ep": "br-prv"
|
||||
},
|
||||
"public": {
|
||||
"ep": "br-ex"
|
||||
},
|
||||
"management": {
|
||||
"ep": "br-mgmt"
|
||||
},
|
||||
"fuelweb_admin": {
|
||||
"ep": "br-fw-admin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class TestNetworkTemplate(base.UnitTestCase):
|
||||
def setUp(self):
|
||||
super(TestNetworkTemplate, self).setUp()
|
||||
|
||||
self.env_id = 42
|
||||
self.req_path = ('/api/v1/clusters/{0}/network_configuration/'
|
||||
'template'.format(self.env_id))
|
||||
|
||||
def test_upload_action(self):
|
||||
mput = self.m_request.put(self.req_path, json={})
|
||||
test_command = [
|
||||
'fuel', 'network-template', '--env', str(self.env_id), '--upload']
|
||||
|
||||
m_open = mock.mock_open(read_data=YAML_TEMPLATE)
|
||||
with mock.patch('fuelclient.cli.serializers.open',
|
||||
m_open,
|
||||
create=True):
|
||||
self.execute(test_command)
|
||||
|
||||
self.assertTrue(mput.called)
|
||||
self.assertEqual(mput.last_request.json(), json.loads(JSON_TEMPLATE))
|
||||
m_open().read.assert_called_once_with()
|
||||
|
||||
def test_download_action(self):
|
||||
mget = self.m_request.get(self.req_path, text=JSON_TEMPLATE)
|
||||
|
||||
test_command = [
|
||||
'fuel', 'network-template', '--env', str(self.env_id),
|
||||
'--download']
|
||||
|
||||
m_open = mock.mock_open()
|
||||
with mock.patch('fuelclient.cli.serializers.open', m_open,
|
||||
create=True):
|
||||
self.execute(test_command)
|
||||
|
||||
self.assertTrue(mget.called)
|
||||
|
||||
written_yaml = yaml.safe_load(m_open().write.mock_calls[0][1][0])
|
||||
expected_yaml = yaml.safe_load(YAML_TEMPLATE)
|
||||
self.assertEqual(written_yaml, expected_yaml)
|
||||
|
||||
def test_delete_action(self):
|
||||
mdelete = self.m_request.delete(self.req_path, json={})
|
||||
|
||||
cmd = ['fuel', 'network-template', '--env', str(self.env_id),
|
||||
'--delete']
|
||||
|
||||
with mock.patch('sys.stdout', new=six.StringIO()) as m_out:
|
||||
self.execute(cmd)
|
||||
|
||||
self.assertTrue(mdelete.called)
|
||||
|
||||
msg = ("Network template configuration for environment id={0}"
|
||||
" has been deleted.".format(self.env_id))
|
||||
self.assertIn(msg, m_out.getvalue())
|
@ -1,88 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2016 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.
|
||||
|
||||
from fuelclient.commands.release import ReleaseComponentList
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
|
||||
|
||||
class TestReleaseComponent(base.UnitTestCase):
|
||||
|
||||
def test_retrieve_predicates(self):
|
||||
predicates = ('any_of', 'all_of', 'one_of', 'none_of')
|
||||
items = {
|
||||
"items": ["fake:component:1",
|
||||
"fake:component:2"]
|
||||
}
|
||||
|
||||
for predicate in predicates:
|
||||
test_data = {predicate: items}
|
||||
real_data = ReleaseComponentList.retrieve_predicates(test_data)
|
||||
expected_data = "{} (fake:component:1, fake:component:2)".format(
|
||||
predicate)
|
||||
self.assertEqual(expected_data, real_data)
|
||||
|
||||
def test_retrieve_predicates_w_wrong_predicate(self):
|
||||
test_data = {
|
||||
"bad_predicate": {
|
||||
"items": ["fake:component:1",
|
||||
"fake:component:2"],
|
||||
}
|
||||
}
|
||||
|
||||
self.assertRaisesRegexp(ValueError,
|
||||
"Predicates not found.",
|
||||
ReleaseComponentList.retrieve_predicates,
|
||||
test_data)
|
||||
|
||||
def test_retrieve_data(self):
|
||||
test_data = "fake:component:1"
|
||||
real_data = ReleaseComponentList.retrieve_data(test_data)
|
||||
self.assertEqual("fake:component:1", real_data)
|
||||
|
||||
test_data = [{"name": "fake:component:1"}]
|
||||
real_data = ReleaseComponentList.retrieve_data(test_data)
|
||||
self.assertEqual("fake:component:1", real_data)
|
||||
|
||||
test_data = [
|
||||
{
|
||||
"one_of": {
|
||||
"items": ["fake:component:1"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"any_of": {
|
||||
"items": ["fake:component:1",
|
||||
"fake:component:2"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"all_of": {
|
||||
"items": ["fake:component:1",
|
||||
"fake:component:2"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"none_of": {
|
||||
"items": ["fake:component:1"]
|
||||
}
|
||||
}
|
||||
]
|
||||
real_data = ReleaseComponentList.retrieve_data(test_data)
|
||||
expected_data = ("one_of (fake:component:1), "
|
||||
"any_of (fake:component:1, fake:component:2), "
|
||||
"all_of (fake:component:1, fake:component:2), "
|
||||
"none_of (fake:component:1)")
|
||||
self.assertEqual(expected_data, real_data)
|
@ -1,74 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 json
|
||||
|
||||
import mock
|
||||
import yaml
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.cli.serializers import Serializer
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
|
||||
|
||||
class TestSerializers(base.UnitTestCase):
|
||||
|
||||
DATA = {
|
||||
'a': 1,
|
||||
'b': {
|
||||
'c': [2, 3, 4],
|
||||
'd': 'string',
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_from_params(self):
|
||||
params_to_formats = (
|
||||
('yaml', 'yaml'),
|
||||
('json', 'json'),
|
||||
('xyz', Serializer.default_format),
|
||||
)
|
||||
for param, format in params_to_formats:
|
||||
params = mock.Mock(serialization_format=format)
|
||||
serializer = Serializer.from_params(params)
|
||||
self.assertEqual(serializer.format, format)
|
||||
|
||||
def test_serialize(self):
|
||||
deserializers = {'json': json.loads, 'yaml': yaml.load}
|
||||
for format, deserialize in deserializers.items():
|
||||
serialized = Serializer(format).serialize(self.DATA)
|
||||
self.assertEqual(self.DATA, deserialize(serialized))
|
||||
|
||||
def test_deserialize(self):
|
||||
serializers = {'json': json.dumps, 'yaml': yaml.safe_dump}
|
||||
for format, serialize in serializers.items():
|
||||
serialized = serialize(self.DATA)
|
||||
deserialized = Serializer(format).deserialize(serialized)
|
||||
self.assertEqual(self.DATA, deserialized)
|
||||
|
||||
def test_deserialize_fail(self):
|
||||
|
||||
broken_data = '{foo: bar: buzz:}'
|
||||
for format in ('json', 'yaml'):
|
||||
self.assertRaises(error.BadDataException,
|
||||
Serializer(format).deserialize, broken_data)
|
||||
|
||||
def test_write_to_path_invalid_file_exception(self):
|
||||
serializer = Serializer('json')
|
||||
mo = mock.mock_open()
|
||||
with mock.patch('__main__.open', mo, create=True) as mocked_open:
|
||||
mocked_open.side_effect = IOError()
|
||||
self.assertRaises(error.InvalidFileException,
|
||||
serializer.write_to_path,
|
||||
'/foo/bar/baz', self.DATA)
|
@ -1,97 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 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.
|
||||
|
||||
from mock import mock_open
|
||||
from mock import patch
|
||||
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
|
||||
|
||||
YAML_SETTINGS_DATA = """editable:
|
||||
access:
|
||||
user:
|
||||
value: test_user
|
||||
"""
|
||||
JSON_SETTINGS_DATA = {
|
||||
'editable': {
|
||||
'access': {
|
||||
'user': {
|
||||
'value': 'test_user'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BaseSettings(base.UnitTestCase):
|
||||
|
||||
def check_upload_action(self, test_command, test_url):
|
||||
m = mock_open(read_data=YAML_SETTINGS_DATA)
|
||||
put = self.m_request.put(test_url, json={})
|
||||
|
||||
with patch('six.moves.builtins.open', m, create=True):
|
||||
self.execute(test_command)
|
||||
|
||||
m().read.assert_called_once_with()
|
||||
self.assertTrue(put.called)
|
||||
self.assertDictEqual(put.last_request.json(), JSON_SETTINGS_DATA)
|
||||
|
||||
def check_default_action(self, test_command, test_url):
|
||||
m = mock_open()
|
||||
get = self.m_request.get(test_url, json=JSON_SETTINGS_DATA)
|
||||
|
||||
with patch('six.moves.builtins.open', m, create=True):
|
||||
self.execute(test_command)
|
||||
|
||||
self.assertTrue(get.called)
|
||||
m().write.assert_called_once_with(YAML_SETTINGS_DATA)
|
||||
|
||||
def check_download_action(self, test_command, test_url):
|
||||
m = mock_open()
|
||||
get = self.m_request.get(test_url, json=JSON_SETTINGS_DATA)
|
||||
|
||||
with patch('six.moves.builtins.open', m, create=True):
|
||||
self.execute(test_command)
|
||||
|
||||
m().write.assert_called_once_with(YAML_SETTINGS_DATA)
|
||||
self.assertTrue(get.called)
|
||||
|
||||
|
||||
class TestSettings(BaseSettings):
|
||||
|
||||
def test_upload_action(self):
|
||||
self.check_upload_action(
|
||||
test_command=[
|
||||
'fuel', 'settings', '--env', '1', '--upload'],
|
||||
test_url='/api/v1/clusters/1/attributes')
|
||||
|
||||
def test_upload_force_action(self):
|
||||
self.check_upload_action(
|
||||
test_command=[
|
||||
'fuel', 'settings', '--env', '1', '--upload', '--force'],
|
||||
test_url='/api/v1/clusters/1/attributes?force=1')
|
||||
|
||||
def test_default_action(self):
|
||||
self.check_default_action(
|
||||
test_command=[
|
||||
'fuel', 'settings', '--env', '1', '--default'],
|
||||
test_url='/api/v1/clusters/1/attributes/defaults')
|
||||
|
||||
def test_download_action(self):
|
||||
self.check_download_action(
|
||||
test_command=[
|
||||
'fuel', 'settings', '--env', '1', '--download'],
|
||||
test_url='/api/v1/clusters/1/attributes')
|
@ -1,331 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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 json
|
||||
import os
|
||||
import six
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient import client
|
||||
from fuelclient.common import data_utils
|
||||
from fuelclient.tests.unit.v1 import base
|
||||
from fuelclient import utils
|
||||
|
||||
|
||||
class TestUtils(base.UnitTestCase):
|
||||
|
||||
@mock.patch('fuelclient.utils.os.walk')
|
||||
def test_iterfiles(self, mwalk):
|
||||
mwalk.return_value = [
|
||||
('/some_directory/', [], ['valid.yaml', 'invalid.yml'])]
|
||||
|
||||
pattern = '*.yaml'
|
||||
directory = '/some_directory'
|
||||
|
||||
expected_result = [os.path.join(directory, 'valid.yaml')]
|
||||
files = list(utils.iterfiles(directory, pattern))
|
||||
|
||||
mwalk.assert_called_once_with(directory, followlinks=True)
|
||||
self.assertEqual(expected_result, files)
|
||||
|
||||
def make_process_mock(self, return_code=0):
|
||||
process_mock = mock.Mock()
|
||||
process_mock.stdout = ['Stdout line 1', 'Stdout line 2']
|
||||
process_mock.returncode = return_code
|
||||
|
||||
return process_mock
|
||||
|
||||
def test_exec_cmd(self):
|
||||
cmd = 'some command'
|
||||
|
||||
process_mock = self.make_process_mock()
|
||||
with mock.patch.object(
|
||||
subprocess, 'Popen', return_value=process_mock) as popen_mock:
|
||||
utils.exec_cmd(cmd)
|
||||
|
||||
popen_mock.assert_called_once_with(
|
||||
cmd,
|
||||
stdout=None,
|
||||
stderr=subprocess.STDOUT,
|
||||
shell=True,
|
||||
cwd=None)
|
||||
|
||||
def test_exec_cmd_raises_error(self):
|
||||
cmd = 'some command'
|
||||
return_code = 1
|
||||
|
||||
process_mock = self.make_process_mock(return_code=return_code)
|
||||
|
||||
with mock.patch.object(
|
||||
subprocess, 'Popen', return_value=process_mock) as popen_mock:
|
||||
self.assertRaisesRegexp(
|
||||
error.ExecutedErrorNonZeroExitCode,
|
||||
'Shell command executed with "{0}" '
|
||||
'exit code: {1} '.format(return_code, cmd),
|
||||
utils.exec_cmd, cmd)
|
||||
|
||||
popen_mock.assert_called_once_with(
|
||||
cmd,
|
||||
stdout=None,
|
||||
stderr=subprocess.STDOUT,
|
||||
shell=True,
|
||||
cwd=None)
|
||||
|
||||
def test_exec_cmd_iterator(self):
|
||||
cmd = 'some command'
|
||||
|
||||
process_mock = self.make_process_mock()
|
||||
with mock.patch.object(
|
||||
subprocess, 'Popen', return_value=process_mock) as popen_mock:
|
||||
for line in utils.exec_cmd_iterator(cmd):
|
||||
self.assertTrue(line.startswith('Stdout line '))
|
||||
|
||||
popen_mock.assert_called_once_with(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True)
|
||||
|
||||
def test_exec_cmd_iterator_raises_error(self):
|
||||
cmd = 'some command'
|
||||
return_code = 1
|
||||
|
||||
process_mock = self.make_process_mock(return_code=return_code)
|
||||
with mock.patch.object(subprocess, 'Popen', return_value=process_mock):
|
||||
with self.assertRaisesRegexp(
|
||||
error.ExecutedErrorNonZeroExitCode,
|
||||
'Shell command executed with "{0}" '
|
||||
'exit code: {1} '.format(return_code, cmd)):
|
||||
for line in utils.exec_cmd_iterator(cmd):
|
||||
self.assertTrue(line.startswith('Stdout line '))
|
||||
|
||||
def test_parse_yaml_file(self):
|
||||
mock_open = self.mock_open("key: value")
|
||||
|
||||
with mock.patch('fuelclient.utils.io.open', mock_open):
|
||||
self.assertEqual(
|
||||
utils.parse_yaml_file('some_file_name'),
|
||||
{'key': 'value'})
|
||||
|
||||
@mock.patch('fuelclient.utils.glob.iglob',
|
||||
return_value=['file1', 'file2'])
|
||||
@mock.patch('fuelclient.utils.parse_yaml_file',
|
||||
side_effect=['content_file1', 'content_file2'])
|
||||
def test_glob_and_parse_yaml(self, parse_mock, iglob_mock):
|
||||
path = '/tmp/path/mask*'
|
||||
|
||||
content = []
|
||||
for data in utils.glob_and_parse_yaml(path):
|
||||
content.append(data)
|
||||
|
||||
iglob_mock.assert_called_once_with(path)
|
||||
self.assertEqual(
|
||||
parse_mock.call_args_list,
|
||||
[mock.call('file1'),
|
||||
mock.call('file2')])
|
||||
|
||||
self.assertEqual(content, ['content_file1', 'content_file2'])
|
||||
|
||||
def test_major_plugin_version(self):
|
||||
pairs = [
|
||||
['1.2.3', '1.2'],
|
||||
['123456789.123456789.12121', '123456789.123456789'],
|
||||
['1.2', '1.2']]
|
||||
|
||||
for arg, expected in pairs:
|
||||
self.assertEqual(
|
||||
utils.major_plugin_version(arg),
|
||||
expected)
|
||||
|
||||
@mock.patch('fuelclient.utils.os.path.lexists', side_effect=[True, False])
|
||||
def test_file_exists(self, lexists_mock):
|
||||
self.assertTrue(utils.file_exists('file1'))
|
||||
self.assertFalse(utils.file_exists('file2'))
|
||||
|
||||
self.assertEqual(
|
||||
lexists_mock.call_args_list,
|
||||
[mock.call('file1'), mock.call('file2')])
|
||||
|
||||
def test_get_error_body_get_from_json(self):
|
||||
error_body = 'This is error body'
|
||||
|
||||
body_json = json.dumps({
|
||||
'message': error_body
|
||||
})
|
||||
if isinstance(body_json, six.text_type):
|
||||
body_json = body_json.encode('utf-8')
|
||||
|
||||
resp = requests.Response()
|
||||
resp._content = body_json
|
||||
|
||||
exception = requests.HTTPError()
|
||||
exception.response = resp
|
||||
|
||||
self.assertEqual(error.get_error_body(exception), error_body)
|
||||
|
||||
def test_get_error_body_get_from_plaintext(self):
|
||||
error_body = b'This is error body'
|
||||
|
||||
resp = requests.Response()
|
||||
resp._content = error_body
|
||||
|
||||
exception = requests.HTTPError()
|
||||
exception.response = resp
|
||||
|
||||
self.assertEqual(error.get_error_body(exception),
|
||||
error_body.decode('utf-8'))
|
||||
|
||||
def test_get_display_data_single(self):
|
||||
test_data = {'a': 1, 'b': [], 'c': [1, 2, 3], 'd': 4}
|
||||
fields = ('a', 'b', 'c')
|
||||
|
||||
result = data_utils.get_display_data_single(fields, test_data)
|
||||
self.assertEqual([1, [], [1, 2, 3]], result)
|
||||
|
||||
def test_get_display_data_bad_key(self):
|
||||
test_data = {'a': 1, 'b': 2, 'c': 3}
|
||||
fields = ('b', 'bad_key')
|
||||
self.assertEqual(
|
||||
[2, None],
|
||||
data_utils.get_display_data_single(fields, test_data)
|
||||
)
|
||||
|
||||
def test_get_display_data_multi(self):
|
||||
test_data = [{'a': 1, 'b': 2, 'c': 3}, {'b': 8, 'c': 9}]
|
||||
fields = ('b', 'c')
|
||||
|
||||
result = data_utils.get_display_data_multi(fields, test_data)
|
||||
self.assertEqual([[2, 3], [8, 9]], result)
|
||||
|
||||
@mock.patch('sys.getfilesystemencoding', return_value='utf-8')
|
||||
def test_str_to_unicode(self, _):
|
||||
test_data = 'тест'
|
||||
expected_data = test_data if six.PY3 else u'тест'
|
||||
result = utils.str_to_unicode(test_data)
|
||||
self.assertIsInstance(result, six.text_type)
|
||||
self.assertEqual(result, expected_data)
|
||||
|
||||
@mock.patch('fuelclient.utils.sys')
|
||||
def test_latin_str_to_unicode(self, sys_mock):
|
||||
sys_mock.getfilesystemencoding.return_value = 'iso-8859-16'
|
||||
|
||||
test_data = 'czegoś' if six.PY3 else u'czegoś'.encode('iso-8859-16')
|
||||
expected_data = test_data if six.PY3 else u'czegoś'
|
||||
result = utils.str_to_unicode(test_data)
|
||||
self.assertIsInstance(result, six.text_type)
|
||||
self.assertEqual(result, expected_data)
|
||||
|
||||
def test_HTTP_error_message(self):
|
||||
text = 'message text'
|
||||
|
||||
self.m_request.post('/api/v1/address',
|
||||
json={'message': text},
|
||||
status_code=403)
|
||||
|
||||
with self.assertRaisesRegexp(error.HTTPError,
|
||||
'403.*{}'.format(text)):
|
||||
client.DefaultAPIClient.post_request('address')
|
||||
|
||||
def test_parse_to_list_of_dicts(self):
|
||||
items = utils.parse_to_list_of_dicts([{"id": 1}])
|
||||
self.assertEqual(items, [{"id": 1}])
|
||||
|
||||
items = utils.parse_to_list_of_dicts([{"id": 2}, {"id": 3}])
|
||||
self.assertEqual(items, [{"id": 2}, {"id": 3}])
|
||||
|
||||
items = utils.parse_to_list_of_dicts([[{"id": 4}]])
|
||||
self.assertEqual(items, [{"id": 4}])
|
||||
|
||||
items = utils.parse_to_list_of_dicts(
|
||||
[[{"id": 5}, {"id": 6}], {"id": 7}])
|
||||
self.assertEqual(items, [{"id": 5}, {"id": 6}, {"id": 7}])
|
||||
|
||||
self.assertRaisesRegexp(
|
||||
TypeError, 'A dict or list instance expected',
|
||||
utils.parse_to_list_of_dicts, [42])
|
||||
|
||||
def test_safe_load_json(self):
|
||||
test_data = {'test_key': 'test_val'}
|
||||
|
||||
m_open = mock.mock_open(read_data=json.dumps(test_data))
|
||||
with mock.patch('fuelclient.tests.unit.common.test_utils.open',
|
||||
m_open):
|
||||
stream = open('/a/random/file', 'r')
|
||||
loaded = data_utils.safe_load('json', stream)
|
||||
|
||||
self.assertEqual(test_data, loaded)
|
||||
|
||||
def test_safe_load_yaml(self):
|
||||
test_data = {'test_key': 'test_val'}
|
||||
|
||||
m_open = mock.mock_open(read_data=yaml.dump(test_data))
|
||||
with mock.patch('fuelclient.tests.unit.common.test_utils.open',
|
||||
m_open):
|
||||
stream = open('/a/random/file', 'r')
|
||||
loaded = data_utils.safe_load('yaml', stream)
|
||||
|
||||
self.assertEqual(test_data, loaded)
|
||||
|
||||
@mock.patch('json.dump')
|
||||
def test_safe_dump_json(self, m_dump):
|
||||
test_data = {'test_key': 'test_val'}
|
||||
|
||||
m_open = mock.mock_open()
|
||||
with mock.patch('fuelclient.tests.unit.common.test_utils.open',
|
||||
m_open):
|
||||
stream = open('/a/random/file', 'w')
|
||||
data_utils.safe_dump('json', stream, test_data)
|
||||
|
||||
m_dump.assert_called_once_with(test_data, stream, indent=4)
|
||||
|
||||
@mock.patch('yaml.safe_dump')
|
||||
def test_safe_dump_yaml(self, m_dump):
|
||||
test_data = {'test_key': 'test_val'}
|
||||
|
||||
m_open = mock.mock_open()
|
||||
with mock.patch('fuelclient.tests.unit.common.test_utils.open',
|
||||
m_open):
|
||||
stream = open('/a/random/file', 'w')
|
||||
data_utils.safe_dump('yaml', stream, test_data)
|
||||
|
||||
m_dump.assert_called_once_with(test_data,
|
||||
stream,
|
||||
default_flow_style=False)
|
||||
|
||||
def test_safe_dump_wrong_format(self):
|
||||
test_data = {'test_key': 'test_val'}
|
||||
|
||||
m_open = mock.mock_open()
|
||||
with mock.patch('fuelclient.tests.unit.common.test_utils.open',
|
||||
m_open):
|
||||
stream = open('/a/random/file', 'w')
|
||||
self.assertRaises(ValueError,
|
||||
data_utils.safe_dump,
|
||||
'bad', stream, test_data)
|
||||
|
||||
def test_safe_load_wrong_format(self):
|
||||
m_open = mock.mock_open()
|
||||
with mock.patch('fuelclient.tests.unit.common.test_utils.open',
|
||||
m_open):
|
||||
stream = open('/a/random/file', 'w')
|
||||
self.assertRaises(ValueError,
|
||||
data_utils.safe_load,
|
||||
'bad', stream)
|
@ -1,72 +0,0 @@
|
||||
# Copyright 2013-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 mock
|
||||
from oslotest import base as oslo_base
|
||||
import requests_mock as rm
|
||||
import six
|
||||
|
||||
from fuelclient.cli import parser
|
||||
|
||||
|
||||
class FakeFile(six.StringIO):
|
||||
"""Context manager for a fake file
|
||||
|
||||
NOTE(eli): We cannot use mock_open from mock library
|
||||
here, because it hangs when we use 'with' statement,
|
||||
and when we want to read file by chunks.
|
||||
|
||||
"""
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class UnitTestCase(oslo_base.BaseTestCase):
|
||||
"""Base test class which does not require nailgun server to run."""
|
||||
|
||||
def setUp(self):
|
||||
super(UnitTestCase, self).setUp()
|
||||
|
||||
self.auth_required_patcher = mock.patch('fuelclient.client.'
|
||||
'APIClient.auth_required',
|
||||
new_callable=mock.PropertyMock)
|
||||
|
||||
self.auth_required_mock = self.auth_required_patcher.start()
|
||||
self.auth_required_mock.return_value = False
|
||||
|
||||
self.m_request = rm.Mocker()
|
||||
self.m_request.start()
|
||||
|
||||
self.addCleanup(self.auth_required_patcher.stop)
|
||||
self.addCleanup(self.m_request.stop)
|
||||
|
||||
def execute(self, command):
|
||||
"""Execute old CLI."""
|
||||
|
||||
return parser.main(command)
|
||||
|
||||
def mock_open(self, text, filename='some.file'):
|
||||
"""Mocks builtin open function
|
||||
|
||||
Usage example:
|
||||
|
||||
with mock.patch('__builtin__.open', self.mock_open('file content')):
|
||||
# call mocked code
|
||||
"""
|
||||
fileobj = FakeFile(text)
|
||||
setattr(fileobj, 'name', filename)
|
||||
return mock.MagicMock(return_value=fileobj)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user