419 lines
16 KiB
Python
419 lines
16 KiB
Python
# 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.")
|