Fuelclient major refactoring

* package structure
 * some implementation ideas
 * a lot of working functions
 * fixed Fuelclient tests
 * fixed action validation
 * fixed formatting and pep8
 * fixed copyright
 * fixed unnecessary request during deployment

 Implements: blueprint fuelclient-refactoring

Change-Id: Ice7f96273afb5d96e7970acfc45c61bab620aaed
This commit is contained in:
Alexandr Notchenko 2014-03-28 13:08:49 +04:00
parent 3bffdedf04
commit d125ae1384
32 changed files with 3156 additions and 2083 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
# 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.
try:
import pkg_resources
try:
__version__ = pkg_resources.get_distribution(
"fuelclient").version
except pkg_resources.DistributionNotFound:
__version__ = ""
except ImportError:
__version__ = ""

View File

@ -1,4 +1,4 @@
# Copyright 2013 Mirantis, Inc.
# 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

View File

@ -0,0 +1,50 @@
# 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.deploy import DeployChangesAction
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.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.node import NodeAction
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.task import TaskAction
actions_tuple = (
ReleaseAction,
RoleAction,
EnvironmentAction,
DeployChangesAction,
NodeAction,
DeploymentAction,
ProvisioningAction,
StopAction,
ResetAction,
SettingsAction,
NetworkAction,
TaskAction,
SnapshotAction,
HealthCheckAction
)
actions = dict(
(action.action_name, action())
for action in actions_tuple
)

View File

@ -0,0 +1,86 @@
# 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
from itertools import imap
from fuelclient.cli.error import ArgumentException
from fuelclient.cli.formatting import quote_and_join
from fuelclient.cli.serializers import Serializer
from fuelclient.client import APIClient
class Action(object):
"""Action class generalizes logic of action execution
method action_func - entry point of parser
"""
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
"""
APIClient.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):
methods_with_docs = set(
method
for _, method in self.flag_func_map
)
return "Examples:\n\n" + \
"\n".join(
imap(
lambda method: (
"\t" + method.__doc__.replace("\n ", "\n")
),
methods_with_docs
)
).format(
action_name=self.action_name
)
def wrap(method, args, f):
@wraps(f)
def wrapped_f(self, params):
if method(getattr(params, _arg) for _arg in args):
return f(self, params)
else:
raise ArgumentException(
"{0} required!".format(
quote_and_join(
"--" + arg for arg in args
)
)
)
return wrapped_f
def check_all(*args):
return partial(wrap, all, args)
def check_any(*args):
return partial(wrap, any, args)

View File

@ -0,0 +1,46 @@
# 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.formatting import print_deploy_progress
class DeployChangesAction(Action):
"""Deploy changes to environments
"""
action_name = "deploy-changes"
def __init__(self):
super(DeployChangesAction, self).__init__()
self.args = (
Args.get_env_arg(required=True),
)
self.flag_func_map = (
(None, self.deploy_changes),
)
def deploy_changes(self, params):
"""To deploy all applied changes to some environment:
fuel --env 1 deploy-changes
"""
from fuelclient.objects.environment import Environment
env = Environment(params.env)
deploy_task = env.deploy_changes()
self.serializer.print_to_output(
deploy_task.data,
deploy_task,
print_method=print_deploy_progress)

View File

@ -0,0 +1,151 @@
# 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.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 name, deployment mode)"
),
Args.get_delete_arg(
"Delete environment with specific env or name"
),
Args.get_create_arg(
"Create a new environment with "
"specific release id and name."
)
),
Args.get_release_arg(
"Release id"
),
Args.get_name_arg(
"environment name"
),
Args.get_mode_arg(
"Set deployment mode for specific environment."
),
Args.get_net_arg(
"Set network mode for specific environment."
),
Args.get_nst_arg(
"Set network segment type"
)
]
self.flag_func_map = (
("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 in multinode mode, and nova
network mode, to specify other modes add optional arguments:
fuel env create --name MyEnv --rel 1 \\
--mode ha --network-mode neutron
"""
env = Environment.create(
params.name,
params.release,
params.net,
net_segment_type=params.nst
)
if params.mode:
data = env.set(mode=params.mode)
else:
data = env.get_fresh_data()
self.serializer.print_to_output(
data,
"Environment '{name}' with id={id}, mode={mode}"
" and network-mode={net_provider} was created!"
.format(**data)
)
@check_all("env")
@check_any("name", "mode")
def set(self, params):
"""For changing environments name, mode
or network mode exists set action:
fuel --env 1 env set --name NewEmvName --mode ha_compact
"""
env = Environment(params.env, params=params)
data = env.set(name=params.name, mode=params.mode)
msg_templates = []
if params.name:
msg_templates.append(
"Environment with id={id} was renamed to '{name}'.")
if params.mode:
msg_templates.append(
"Mode of environment with id={id} was set to '{mode}'.")
self.serializer.print_to_output(
data,
"\n".join(msg_templates).format(**data)
)
@check_all("env")
def delete(self, params):
"""To delete the environment:
fuel --env 1 env delete
"""
env = Environment(params.env, params=params)
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", "mode",
"release_id", "changes")
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,
subdict_keys=[("release_id", u"id")]
)
)

View File

@ -0,0 +1,123 @@
# 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):
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."
),
]
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)
env.write_facts_to_dir(
self.action_name,
env.get_default_facts(self.action_name, nodes=params.node),
directory=params.dir
)
def upload(self, params):
"""To upload {action_name} information for some environment:
fuel --env 1 {action_name} --upload
"""
env = Environment(params.env)
facts = getattr(env, self.read_method_name)(
self.action_name,
directory=params.dir
)
env.upload_facts(self.action_name, facts)
self.serializer.print_to_output(
facts,
"{0} facts 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)
self.serializer.print_to_output(
{},
"{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)
env.write_facts_to_dir(
self.action_name,
env.get_facts(self.action_name, nodes=params.node),
directory=params.dir
)
@property
def read_method_name(self):
return "read_{0}_info".format(self.action_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"

View File

@ -0,0 +1,75 @@
# 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.error import exit_with_error
from fuelclient.cli.formatting import format_table
from fuelclient.cli.formatting import print_health_check
from fuelclient.objects.environment import Environment
class HealthCheckAction(Action):
"""Run health check on environment
"""
action_name = "health"
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.")
)
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.is_customized and not params.force:
exit_with_error(
"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())
env.run_test_sets(test_sets_to_check)
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 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)
)

View File

@ -0,0 +1,58 @@
# 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 -t {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"

View File

@ -0,0 +1,88 @@
# 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/derectory
"""
env = Environment(params.env)
network_data = env.read_network_data(directory=params.dir)
response = env.set_network_data(network_data)
self.serializer.print_to_output(
response,
"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/derectory
"""
env = Environment(params.env)
response = env.verify_network()
self.serializer.print_to_output(
response,
"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)
self.serializer.print_to_output(
network_data,
"Network configuration for environment with id={0}"
" downloaded to {1}"
.format(env.id, network_file_path)
)

View File

@ -0,0 +1,239 @@
# 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.error import ActionException
from fuelclient.cli.error import ArgumentException
from fuelclient.cli.formatting import format_table
from fuelclient.cli.formatting import quote_and_join
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")
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_network_arg("Node network configuration."),
Args.get_disk_arg("Node disk configuration."),
Args.get_deploy_arg("Deploy specific nodes."),
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_all_arg("Select all nodes."),
Args.get_role_arg("Role to assign for node.")
]
self.flag_func_map = (
("set", self.set),
("delete", self.delete),
("network", self.attributes),
("disk", self.attributes),
("deploy", self.start),
("provision", self.start),
(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 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 = list(_nodes)
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)
)
@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
)
files.append(file_path)
attributes.append(default_attribute)
message = "Default node attributes for {0} were written" \
" to {1}".format(attribute_type, quote_and_join(files))
elif params.upload:
for node in nodes:
attribute = node.read_attribute(attribute_type, params.dir)
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
)
attributes.append(downloaded_attribute)
files.append(file_path)
message = "Node attributes for {0} were written" \
" to {1}".format(attribute_type, quote_and_join(files))
self.serializer.print_to_output(
attributes,
message
)
@check_all("env", "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_ids = set(n.env_id for n in node_collection)
if len(env_ids) != 1:
raise ActionException(
"Inputed nodes assigned to multiple environments!")
else:
env_id_to_start = env_ids.pop()
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))
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
)
)

View File

@ -0,0 +1,96 @@
# 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
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
class ReleaseAction(Action):
"""List and modify currently available releases
"""
action_name = "release"
def __init__(self):
super(ReleaseAction, self).__init__()
self.args = [
group(
Args.get_list_arg("List all available releases."),
Args.get_config_arg("Configure release with --release"),
),
Args.get_release_arg("Specify release id to configure"),
Args.get_username_arg("Username for release credentials"),
Args.get_password_arg("Password for release credentials"),
Args.get_satellite_arg("Satellite server hostname"),
Args.get_activation_key_arg("activation key")
]
self.flag_func_map = (
("config", self.configure_release),
(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", "username", "password")
def configure_release(self, params):
"""To configure RedHat release:
fuel rel --rel <id of RedHat release> \\
-c -U <username> -P <password>
To configure RedHat release with satellite server:
fuel rel --rel <...> -c -U <...> -P <...> \\
--satellite-server-hostname <hostname> --activation-key <key>
"""
release = Release(params.release)
release_response = release.configure(
params.username,
params.password,
satellite_server_hostname=None,
activation_key=None
)
self.serializer.print_to_output(
release_response,
"Credentials for release with id={0}"
" were modified."
.format(release.id)
)

View File

@ -0,0 +1,58 @@
# 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.formatting import format_table
from fuelclient.objects.release import Release
class RoleAction(Action):
"""List all roles for specific release
"""
action_name = "role"
def __init__(self):
super(RoleAction, self).__init__()
self.args = [
Args.get_list_arg("List all roles for specific release"),
Args.get_release_arg("Release id", required=True)
]
self.flag_func_map = (
(None, self.list),
)
def list(self, params):
"""Print all available roles and their
conflicts for some release with id=1:
fuel role --rel 1
"""
release = Release(params.release, params=params)
data = release.get_fresh_data()
acceptable_keys = ("name", "conflicts")
roles = [
{
"name": role_name,
"conflicts": ", ".join(
metadata.get("conflicts", ["-"])
)
} for role_name, metadata in data["roles_metadata"].iteritems()]
self.serializer.print_to_output(
roles,
format_table(
roles,
acceptable_keys=acceptable_keys
)
)

View File

@ -0,0 +1,84 @@
# 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.")
),
Args.get_dir_arg("Directory with configuration data.")
)
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/derectory
"""
env = Environment(params.env)
network_data = env.read_settings_data(directory=params.dir)
response = env.set_settings_data(network_data)
self.serializer.print_to_output(
response,
"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/derectory
"""
env = Environment(params.env)
default_data = env.get_default_settings_data()
settings_file_path = env.write_settings_data(
default_data,
directory=params.dir)
self.serializer.print_to_output(
default_data,
"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)
self.serializer.print_to_output(
settings_data,
"Settings configuration for environment with id={0}"
" downloaded to {1}"
.format(env.id, settings_file_path)
)

View File

@ -0,0 +1,51 @@
# 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.formatting import download_snapshot_with_progress_bar
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_dir_arg("Directory to which download snapshot."),
)
self.flag_func_map = (
(None, self.get_snapshot),
)
def get_snapshot(self, params):
"""To download diagnostic snapshot:
fuel snapshot
To download diagnostic snapshot to specific directory:
fuel snapshot --dir path/to/directory
"""
snapshot_task = SnapshotTask.start_snapshot_task()
self.serializer.print_to_output(
snapshot_task.data,
"Generating dump..."
)
snapshot_task.wait()
download_snapshot_with_progress_bar(
snapshot_task.connection.api_root + snapshot_task.data["message"],
directory=params.dir
)

View File

@ -0,0 +1,81 @@
# 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
import fuelclient.cli.arguments as Args
from fuelclient.cli.arguments import group
from fuelclient.cli.formatting import format_table
from fuelclient.objects.task import Task
class TaskAction(Action):
"""Show tasks
"""
action_name = "task"
def __init__(self):
super(TaskAction, self).__init__()
self.args = [
group(
Args.get_list_arg("List all tasks"),
Args.get_delete_arg("Delete task with some task-id.")
),
Args.get_force_arg("Force deletion"),
Args.get_task_arg("Task id.")
]
self.flag_func_map = (
("delete", self.delete),
(None, self.list)
)
@check_all("task")
def delete(self, params):
"""To delete some tasks:
fuel task delete -t 1,2,3
To delete some tasks forcefully (without considering their state):
fuel task delete -f -t 1,6
"""
tasks = Task.get_by_ids(params.task)
delete_response = map(
lambda task: task.delete(force=params.force),
tasks
)
self.serializer.print_to_output(
delete_response,
"Tasks with id's {0} deleted."
.format(', '.join(map(str, params.task)))
)
def list(self, params):
"""To display all tasks:
fuel task
To display tasks with some ids:
fuel task -t 1,2,3
"""
acceptable_keys = ("id", "status", "name",
"cluster", "progress", "uuid")
if params.task:
tasks_data = map(
Task.get_fresh_data,
Task.get_by_ids(params.task)
)
else:
tasks_data = Task.get_all_data()
self.serializer.print_to_output(
tasks_data,
format_table(tasks_data, acceptable_keys=acceptable_keys)
)

View File

@ -0,0 +1,388 @@
# 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 APIClient
substitutions = {
#replace from: to
"env": "environment",
"nodes": "node",
"net": "network",
"rel": "release",
"list": "--list",
"set": "--set",
"delete": "--delete",
"download": "--download",
"upload": "--upload",
"default": "--default",
"create": "--create",
"remove": "--delete",
"config": "--config",
"--roles": "--role"
}
def group(*args, **kwargs):
required = kwargs.get("required", False)
return (required, ) + args
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 APIClient.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)
)
setattr(namespace, self.dest, map(int, only_ids))
class FuelVersionAction(argparse._VersionAction):
"""Custom argparse._VersionAction subclass to compute fuel server version
:returns: prints fuel server version
"""
def __call__(self, parser, namespace, values, option_string=None):
parser.exit(message=APIClient.get_fuel_version())
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 parse_ids(x):
"""Parse arguments with commas and spaces
:returns: list of lists with numbers
"""
filtered = [y for y in x.split(",") if y.strip() != '']
if len(filtered) > 1:
return map(int, filtered)
elif len(filtered) == 1:
return [int(filtered[0])]
else:
return None
def get_serializer_arg(serialization_method):
return {
"args": ["--{0}".format(serialization_method)],
"params": {
"dest": serialization_method,
"action": "store_true",
"help": "prints only {0} to stdout".format(serialization_method),
"default": False
}
}
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_fuel_version_arg():
return {
"args": ["--fuel-version"],
"params": {
"action": FuelVersionAction,
"help": "show Fuel server's version number and exit"
}
}
def get_arg(name, flags=None, aliases=None, help_=None, **kwargs):
if "_" in name:
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_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_multinum_arg(name, **kwargs):
default_kwargs = {
"action": "store",
"type": parse_ids,
"nargs": '+',
"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_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_check_arg(help_msg):
return get_set_type_arg("check", help=help_msg)
def get_name_arg(help_msg):
return get_str_arg("name", flags=("--env-name",), help=help_msg)
def get_mode_arg(help_msg):
return get_arg("mode",
action="store",
choices=("multinode", "ha"),
default=False,
flags=("-m", "--deployment-mode"),
help_=help_msg)
def get_net_arg(help_msg):
return get_arg("net",
flags=("-n", "--network-mode"),
action="store",
choices=("nova", "neutron"),
help_=help_msg,
default="nova")
def get_nst_arg(help_msg):
return get_arg("nst",
flags=("--net-segment-type",),
action="store",
choices=("gre", "vlan"),
help_=help_msg,
default=False)
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_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_release_arg(help_msg, required=False):
return get_int_arg(
"release",
flags=("--rel",),
required=required,
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_task_arg(help_msg):
return get_multinum_arg(
"task",
flags=("--tid", "--task-id"),
help=help_msg)
def get_config_arg(help_msg):
return get_boolean_arg("config", flags=("-c",), help=help_msg)
def get_username_arg(help_msg):
return get_str_arg("username", flags=("-U", "--user"), help=help_msg)
def get_password_arg(help_msg):
return get_str_arg("password", flags=("-P", "--pass"), help=help_msg)
def get_satellite_arg(help_msg):
return get_str_arg("satellite_server_hostname", help=help_msg)
def get_activation_key_arg(help_msg):
return get_str_arg("activation_key", help=help_msg)

View File

@ -0,0 +1,67 @@
# 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 sys
import urllib2
def exit_with_error(message):
sys.stderr.write(message + "\n")
exit(1)
class FuelClientException(Exception):
pass
class DeployProgressError(FuelClientException):
pass
class ArgumentException(FuelClientException):
pass
class ActionException(FuelClientException):
pass
class ParserException(FuelClientException):
pass
def handle_exceptions(exc):
if isinstance(exc, urllib2.HTTPError):
error_body = exc.read()
exit_with_error("{0} {1}".format(
exc,
"({0})".format(error_body or "")
))
elif isinstance(exc, urllib2.URLError):
exit_with_error("Can't connect to Nailgun server!")
elif isinstance(exc, FuelClientException):
exit_with_error(exc.message)
else:
raise
def exceptions_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as exc:
handle_exceptions(exc)
return wrapper

View File

@ -0,0 +1,256 @@
# 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 curses
from functools import partial
from itertools import chain
import math
from operator import itemgetter
import os
import sys
from time import sleep
import urllib2
from fuelclient.cli.error import DeployProgressError
from fuelclient.cli.error import exit_with_error
def recur_get(multi_level_dict, key_chain):
"""Method accesses some field in nested dictionaries
:returns: value for last key in key_chain in last dictionary
"""
if not isinstance(multi_level_dict[key_chain[0]], dict):
return multi_level_dict[key_chain[0]]
else:
return recur_get(multi_level_dict[key_chain[0]], key_chain[1:])
def format_table(data, acceptable_keys=None, subdict_keys=None):
"""Format list of dicts to ascii table
:acceptable_keys list(str): list of keys for which to create table
also specifies their order
:subdict_keys list(tuple(str)): list of key chains (tuples of key strings)
which are applied to dictionaries
to extract values
"""
if subdict_keys:
for key_chain in subdict_keys:
for data_dict in data:
data_dict[key_chain[0]] = recur_get(data_dict, key_chain)
if acceptable_keys:
rows = [tuple([value[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)
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], len(str(element))))
for index, element in enumerate(row)
)
row_template = ' | '.join(
"{{{0}:{1}}}".format(idx, width)
for idx, width in column_widths.iteritems()
)
return '\n'.join(
(row_template.format(*header),
'-|-'.join(column_widths[column_index] * '-'
for column_index in range(number_of_columns)),
'\n'.join(row_template.format(*x) for x in rows))
)
def quote_and_join(words):
words = list(words)
if len(words) > 1:
return '{0} and "{1}"'.format(
", ".join(
map(
lambda x: '"{0}"'.format(x),
words
)[0:-1]
),
words[-1]
)
else:
return '"{0}"'.format(words[0])
def get_bar_for_progress(full_width, progress):
number_of_equal_signs = int(
math.ceil(progress * float(full_width - 2) / 100)
)
return "[{0}{1}{2}]".format(
"=" * number_of_equal_signs,
">" if number_of_equal_signs < full_width - 2 else "",
" " * (full_width - 3 - number_of_equal_signs)
)
def download_snapshot_with_progress_bar(url, directory=os.path.curdir):
if not os.path.exists(directory):
exit_with_error("Folder {0} doesn't exist.".format(directory))
file_name = os.path.join(
os.path.abspath(directory),
url.split('/')[-1]
)
download_handle = urllib2.urlopen(url)
with open(file_name, 'wb') as file_handle:
meta = download_handle.info()
file_size = int(meta.getheaders("Content-Length")[0])
print("Downloading: {0} Bytes: {1}".format(url, file_size))
file_size_dl = 0
block_size = 8192
bar = partial(get_bar_for_progress, 80)
while True:
data_buffer = download_handle.read(block_size)
if not data_buffer:
break
file_size_dl += len(data_buffer)
file_handle.write(data_buffer)
progress = int(100 * float(file_size_dl) / file_size)
sys.stdout.write("\r{0}".format(
bar(progress)
))
sys.stdout.flush()
sleep(1 / 10)
print()
def print_deploy_progress(deploy_task):
try:
terminal_screen = curses.initscr()
print_deploy_progress_with_terminal(deploy_task, terminal_screen)
except curses.error:
print_deploy_progress_without_terminal(deploy_task)
def print_deploy_progress_without_terminal(deploy_task):
print("Deploying changes to environment with id={0}".format(
deploy_task.env.id
))
message_len = 0
try:
for progress, nodes in deploy_task:
sys.stdout.write("\r" * message_len)
message_len = 0
deployment_message = "[Deployment: {0:3}%]".format(progress)
sys.stdout.write(deployment_message)
message_len += len(deployment_message)
for index, node in enumerate(nodes):
node_message = "[Node{id:2} {progress:3}%]".format(
**node.data
)
message_len += len(node_message)
sys.stdout.write(node_message)
print("\nFinished deployment!")
except DeployProgressError as de:
print(de.message)
def print_deploy_progress_with_terminal(deploy_task, terminal_screen):
scr_width = terminal_screen.getmaxyx()[1]
curses.noecho()
curses.cbreak()
total_progress_bar = partial(get_bar_for_progress, scr_width - 17)
node_bar = partial(get_bar_for_progress, scr_width - 28)
env_id = deploy_task.env.id
try:
for progress, nodes in deploy_task:
terminal_screen.refresh()
terminal_screen.addstr(
0, 0,
"Deploying changes to environment with id={0}".format(
env_id
)
)
terminal_screen.addstr(
1, 0,
"Deployment: {0} {1:3}%".format(
total_progress_bar(progress),
progress
)
)
for index, node in enumerate(nodes):
terminal_screen.addstr(
index + 2, 0,
"Node{id:3} {status:13}: {bar} {progress:3}%"
.format(bar=node_bar(node.progress), **node.data)
)
except DeployProgressError as de:
close_curses()
print(de.message)
finally:
close_curses()
def close_curses():
curses.echo()
curses.nocbreak()
curses.endwin()
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(
"[{0:2} of {1}] [{status}] '{name}' "
"({taken:.4} s) {message}".format(
test_counter,
total_tests_count,
**test
)
)
test_counter += 1
sleep(1)

View File

@ -0,0 +1,144 @@
# 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_fuel_version_arg
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
class Parser:
def __init__(self):
self.args = sys.argv
self.parser = argparse.ArgumentParser(
usage="fuel [optional args] <namespace> [action] [flags]"
)
self.universal_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()
def generate_actions(self):
for action, action_object in actions.iteritems():
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()
parsed_params, _ = self.parser.parse_known_args(self.args[1:])
if parsed_params.action not in actions:
self.parser.print_help()
sys.exit(0)
actions[parsed_params.action].action_func(parsed_params)
def add_serializers_args(self):
for format_name in Serializer.serializers.keys():
serialization_flag = "--{0}".format(format_name)
self.universal_flags.append(serialization_flag)
self.parser.add_argument(
serialization_flag,
dest=format_name,
action="store_true",
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):
for args in (get_version_arg(), get_fuel_version_arg()):
self.parser.add_argument(*args["args"], **args["params"])
def prepare_args(self):
# replace some args from dict substitutions
self.args = map(
lambda x: substitutions.get(x, x),
self.args
)
# move --json and --debug flags before any action
for flag in self.universal_flags:
if flag in self.args:
self.args.remove(flag)
self.args.insert(1, flag)
self.move_argument_before_action("--env", )
def move_argument_before_action(self, argument):
for arg in self.args:
if argument in arg:
# if declaration with '=' sign (e.g. --env-id=1)
if "=" in arg:
index_of_env = self.args.index(arg)
env = self.args.pop(index_of_env)
self.args.append(env)
else:
try:
index_of_env = self.args.index(arg)
self.args.pop(index_of_env)
env = self.args.pop(index_of_env)
self.args.append(arg)
self.args.append(env)
except IndexError:
raise ParserException(
'Corresponding value must follow "{0}" flag'
.format(arg)
)
break
@exceptions_decorator
def main():
parser = Parser()
parser.parse()

View File

@ -0,0 +1,90 @@
# 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
from itertools import ifilter
from itertools import imap
import json
import os
import yaml
class Serializer(object):
serializers = {
"json": {
"w": lambda d: json.dumps(d, indent=4),
"r": lambda d: json.loads(d)
},
"yaml": {
"w": lambda d: yaml.safe_dump(d, default_flow_style=False),
"r": lambda d: yaml.load(d)
}
}
format_flags = False
default_format = "yaml"
format = default_format
def __init__(self, **kwargs):
for f in self.serializers:
if kwargs.get(f, False):
self.format = f
self.format_flags = True
break
@property
def serializer(self):
return self.serializers[self.format]
@classmethod
def from_params(cls, params):
kwargs = dict((key, getattr(params, key)) for key in cls.serializers)
return cls(**kwargs)
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:
print_method(arg)
def prepare_path(self, path):
return "{0}.{1}".format(
path, self.format
)
def write_to_file(self, path, data):
full_path = self.prepare_path(path)
with open(full_path, "w+") as file_to_write:
file_to_write.write(self.serializer["w"](data))
return full_path
def read_from_file(self, path):
full_path = self.prepare_path(path)
with open(full_path, "r") as file_to_read:
return self.serializer["r"](file_to_read.read())
def listdir_without_extensions(dir_path):
return ifilter(
lambda f: f != "",
imap(
lambda f: f.split(".")[0],
os.listdir(dir_path)
)
)

View File

@ -0,0 +1,127 @@
# 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 urllib2
import yaml
from fuelclient.cli.error import exceptions_decorator
class Client(object):
"""This class handles API requests
"""
def __init__(self):
self.debug = False
path_to_config = "/etc/fuel-client.yaml"
defaults = {
"LISTEN_ADDRESS": "127.0.0.1",
"LISTEN_PORT": "8000"
}
if os.path.exists(path_to_config):
with open(path_to_config, "r") as fh:
config = yaml.load(fh.read())
defaults.update(config)
else:
defaults.update(os.environ)
self.root = "http://{LISTEN_ADDRESS}:{LISTEN_PORT}".format(**defaults)
self.api_root = self.root + "/api/v1/"
self.ostf_root = self.root + "/ostf/"
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
"""
self.print_debug(
"DELETE {0}".format(self.api_root + api)
)
opener = urllib2.build_opener(urllib2.HTTPHandler)
request = urllib2.Request(self.api_root + api)
request.add_header('Content-Type', 'application/json')
request.get_method = lambda: 'DELETE'
opener.open(request)
return {}
def put_request(self, api, data):
"""Make PUT request to specific API with some data
"""
data_json = json.dumps(data)
self.print_debug(
"PUT {0} data={1}"
.format(self.api_root + api, data_json)
)
opener = urllib2.build_opener(urllib2.HTTPHandler)
request = urllib2.Request(self.api_root + api, data=data_json)
request.add_header('Content-Type', 'application/json')
request.get_method = lambda: 'PUT'
return json.loads(
opener.open(request).read()
)
def get_request(self, api, ostf=False):
"""Make GET request to specific API
"""
url = (self.ostf_root if ostf else self.api_root) + api
self.print_debug(
"GET {0}"
.format(url)
)
request = urllib2.urlopen(url)
return json.loads(
request.read()
)
def post_request(self, api, data, ostf=False):
"""Make POST request to specific API with some data
"""
url = (self.ostf_root if ostf else self.api_root) + api
data_json = json.dumps(data)
self.print_debug(
"POST {0} data={1}"
.format(url, data_json)
)
request = urllib2.Request(
url=url,
data=data_json,
headers={
'Content-Type': 'application/json'
}
)
try:
response = json.loads(
urllib2.urlopen(request)
.read()
)
except ValueError:
response = {}
return response
@exceptions_decorator
def get_fuel_version(self):
return yaml.safe_dump(
self.get_request("version"),
default_flow_style=False
)
APIClient = Client()

View File

@ -0,0 +1,23 @@
# 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
from fuelclient.objects.environment import Environment
from fuelclient.objects.node import Node
from fuelclient.objects.node import NodeCollection
from fuelclient.objects.release import Release
from fuelclient.objects.task import DeployTask
from fuelclient.objects.task import SnapshotTask
from fuelclient.objects.task import Task

View File

@ -0,0 +1,62 @@
# 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 APIClient
class BaseObject(object):
class_api_path = None
instance_api_path = None
connection = APIClient
def __init__(self, obj_id, **kwargs):
self.connection = APIClient
self.serializer = Serializer(**kwargs)
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):
return cls.connection.get_request(cls.class_api_path)
@classmethod
def get_all(cls):
return map(cls.init_with_data, cls.get_all_data())

View File

@ -0,0 +1,346 @@
# 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.error import ActionException
from fuelclient.cli.error import ArgumentException
from fuelclient.cli.serializers import listdir_without_extensions
from fuelclient.objects.base import BaseObject
from fuelclient.objects.task import DeployTask
from functools import wraps
def memorize_one(func):
func.cache = None
@wraps(func)
def nested(*args, **kwargs):
if func.cache is None:
func.cache = func(*args, **kwargs)
return func.cache
return nested
class Environment(BaseObject):
class_api_path = "clusters/"
instance_api_path = "clusters/{0}/"
@classmethod
def create(cls, name, release_id, net, net_segment_type=None):
data = {
"nodes": [],
"tasks": [],
"name": name,
"release_id": release_id
}
if net.lower() == "nova":
data["net_provider"] = "nova_network"
else:
data["net_provider"] = "neutron"
if net_segment_type is not None:
data["net_segment_type"] = net_segment_type
else:
raise ArgumentException(
'"--net-segment-type" must be specified!')
data = cls.connection.post_request("clusters/", data)
return cls.init_with_data(data)
def set(self, name=None, mode=None):
data = {}
if mode:
data["mode"] = "ha_compact" \
if mode.lower() == "ha" else "multinode"
if name:
data["name"] = name
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)
def unassign_all(self):
nodes = self.get_all_nodes()
if not nodes:
raise 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):
deploy_data = self.connection.put_request(
"clusters/{0}/changes".format(self.id),
{}
)
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 write_network_data(self, network_data, directory=os.curdir):
return self.serializer.write_to_file(
self.get_network_data_path(directory),
network_data
)
def write_settings_data(self, settings_data, directory=os.curdir):
return self.serializer.write_to_file(
self.get_settings_data_path(directory),
settings_data
)
def read_network_data(self, directory=os.curdir):
network_file_path = self.get_network_data_path(directory)
return self.serializer.read_from_file(network_file_path)
def read_settings_data(self, directory=os.curdir):
settings_file_path = self.get_settings_data_path(directory)
return self.serializer.read_from_file(settings_file_path)
@property
def settings_url(self):
return "clusters/{0}/attributes".format(self.id)
@property
def default_settings_url(self):
return self.settings_url + "/defaults"
@property
@memorize_one
def network_url(self):
return "clusters/{id}/network_configuration/{net_provider}".format(
**self.data
)
@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 set_network_data(self, data):
return self.connection.put_request(
self.network_url, data)
def set_settings_data(self, data):
return self.connection.put_request(
self.settings_url, data)
def verify_network(self):
return self.connection.put_request(
self.network_verification_url, self.get_network_data())
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_default_url(self, fact_type, nodes=None):
default_url = "clusters/{0}/orchestrator/{1}/defaults".format(
self.id,
fact_type
)
if nodes is not None:
default_url += "/?nodes=" + ",".join(map(str, nodes))
return default_url
def _get_fact_url(self, fact_type, nodes=None):
fact_url = "clusters/{0}/orchestrator/{1}/".format(
self.id,
fact_type
)
if nodes is not None:
fact_url += "/?nodes=" + ",".join(map(str, nodes))
return fact_url
def get_default_facts(self, fact_type, nodes=None):
return self.connection.get_request(
self._get_fact_default_url(fact_type, nodes=nodes))
def get_facts(self, fact_type, nodes=None):
return self.connection.get_request(
self._get_fact_url(fact_type, nodes=nodes))
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):
if fact_type == "deployment":
return self.read_deployment_info(fact_type)
elif fact_type == "provisioning":
return self.read_provisioning_info(fact_type)
def write_facts_to_dir(self, fact_type, facts, directory=os.path.curdir):
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")
self.serializer.write_to_file(engine_file_path, facts["engine"])
facts = facts["nodes"]
name_template = "{name}"
else:
name_template = "{role}_{uid}"
for _fact in facts:
fact_path = os.path.join(
dir_name,
name_template.format(**_fact)
)
self.serializer.write_to_file(fact_path, _fact)
def read_deployment_info(self, fact_type, directory=os.path.curdir):
dir_name = self._get_fact_dir_name(fact_type, directory=directory)
return map(
lambda f: 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):
dir_name = self._get_fact_dir_name(fact_type, directory=directory)
node_facts = map(
lambda f: 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 = self.serializer.read_from_file(
os.path.join(dir_name, "engine"))
return {
"engine": engine,
"nodes": node_facts
}
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"]
def is_in_running_test_sets(self, test_set):
return test_set["testset"] in self._test_sets_to_run,
def run_test_sets(self, test_sets_to_run):
self._test_sets_to_run = test_sets_to_run
tests_data = map(
lambda testset: {
"testset": testset,
"metadata": {
"config": {},
"cluster_id": self.id
}
},
test_sets_to_run
)
return self.connection.post_request(
"testruns",
tests_data,
ostf=True
)
def get_state_of_tests(self):
return self.connection.get_request(
"testruns/last/{0}".format(self.id),
ostf=True
)
def stop(self):
from fuelclient.objects.task import Task
return Task.init_with_data(
self.connection.put_request(
"clusters/{0}/stop_deployment/".format(self.id),
{}
)
)
def reset(self):
from fuelclient.objects.task import Task
return Task.init_with_data(
self.connection.put_request(
"clusters/{0}/reset/".format(self.id),
{}
)
)
def _get_method_url(self, method_type, nodes):
return "clusters/{0}/{1}/?nodes={2}".format(
self.id,
method_type,
','.join(map(lambda n: str(n.id), nodes)))
def install_selected_nodes(self, method_type, nodes):
from fuelclient.objects.task import Task
return Task.init_with_data(
self.connection.put_request(
self._get_method_url(method_type, nodes),
{}
)
)

View File

@ -0,0 +1,159 @@
# 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 exit_with_error
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_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"]
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_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)
if attributes_type == "interfaces":
attributes = [{
"interfaces": attributes,
"id": self.id
}]
return self.connection.put_request(
url,
attributes
)
def write_attribute(self, attribute_type, attributes, directory):
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)
self.serializer.write_to_file(
attribute_path,
attributes
)
def read_attribute(self, attributes_type, directory):
attributes_directory = self.get_attributes_path(directory)
if not os.path.exists(attributes_directory):
exit_with_error(
"Folder {0} doesn't contain node folder '{1}'"
.format(directory, "node_{0}".format(self.id))
)
return 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,))
class NodeCollection(object):
def __init__(self, nodes):
self.collection = nodes
@classmethod
def init_with_ids(cls, ids):
return cls(map(Node, ids))
@classmethod
def init_with_data(cls, data):
return cls(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())
def filter_by_env_id(self, env_id):
self.collection = filter(
lambda node: node.env_id == env_id,
self.collection
)

View File

@ -0,0 +1,60 @@
# 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.error import ArgumentException
from fuelclient.objects.base import BaseObject
class Release(BaseObject):
class_api_path = "releases/"
instance_api_path = "releases/{0}/"
def configure(self, username, password,
satellite_server_hostname=None, activation_key=None):
data = {
"release_id": self.id,
"username": username,
"password": password
}
satellite_flags = [satellite_server_hostname,
activation_key]
if not any(satellite_flags):
data.update({
"license_type": "rhsm",
"satellite": "",
"activation_key": ""
})
elif all(satellite_flags):
data.update({
"license_type": "rhn",
"satellite": satellite_server_hostname,
"activation_key": activation_key
})
else:
raise ArgumentException(
'RedHat satellite settings requires both a'
' "--satellite-server-hostname" and '
'a "--activation-key" flags.'
)
release_response = self.connection.post_request(
"redhat/setup/",
data
)
return release_response
@classmethod
def get_all(cls):
map(cls.init_with_data, cls.get_all_data())

View File

@ -0,0 +1,100 @@
# 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 = "tasks/"
instance_api_path = "tasks/{0}/"
def delete(self, force=False):
return self.connection.delete_request(
"tasks/{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)
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):
dump_task = cls.connection.put_request("logs/package", {})
return cls(dump_task["id"])

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python
# Copyright 2013 Mirantis, Inc.
# 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
@ -13,16 +12,22 @@
# License for the specific language governing permissions and limitations
# under the License.
from setuptools import find_packages
from setuptools import setup
setup(
name='python-fuelclient',
version='0.1',
name='fuelclient',
version='0.2',
description='Command line interface for Nailgun',
long_description="""Command line interface for Nailgun""",
author='Mirantis Inc.',
author_email='product@mirantis.com',
url='http://mirantis.com',
install_requires=['PyYAML==3.10'],
scripts=['fuel']
install_requires=['PyYAML==3.10', "argparse==1.2.1"],
packages=find_packages(),
entry_points={
'console_scripts': [
'fuel = fuelclient.cli.parser:main',
],
}
)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
# 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

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
# 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
@ -20,7 +18,7 @@ import os
from shutil import rmtree
from tempfile import mkdtemp
from fuelclient.tests.base import BaseTestCase
from tests.base import BaseTestCase
class TestHandlers(BaseTestCase):
@ -28,9 +26,9 @@ class TestHandlers(BaseTestCase):
def test_env_action(self):
#check env help
help_msgs = ["usage: fuel environment [-h]",
"[-h] [--env ENV] [-l] [-s]",
"[--list | --set | --delete | --create]",
"optional arguments:", "--help", "--list", "--set",
"--delete", "--rel", "--release", "--env-create,",
"--delete", "--rel", "--env-create",
"--create", "--name", "--env-name", "--mode", "--net",
"--network-mode", "--nst", "--net-segment-type",
"--deployment-mode"]
@ -59,12 +57,11 @@ class TestHandlers(BaseTestCase):
self.check_for_stdout(cmd, msg)
def test_node_action(self):
help_msg = ["fuel node [-h] [--env ENV] [-l]",
"[-l] [-s] [--delete] [--default]", "-h", "--help", "-l",
"--list", "-s", "--set", "--delete", "--default", "-d",
"--download", "-u", "--upload", "--dir", "--node",
"--node-id", "-r", "--role", "--net", "--network",
"--disk", "--deploy", "--provision"]
help_msg = ["fuel node [-h] [--env ENV]",
"[--list | --set | --delete | --network | --disk |"
" --deploy | --provision]", "-h", "--help", " -s",
"--default", " -d", "--download", " -u", "--upload",
"--dir", "--node", "--node-id", " -r", "--role", "--net"]
self.check_all_in_msg("node --help", help_msg)
self.check_for_rows_in_table("node")