python-fuelclient/fuelclient/commands/node.py

607 lines
19 KiB
Python

# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import abc
import collections
import json
import operator
import os
from oslo_utils import fileutils
import six
from fuelclient.cli import error
from fuelclient.commands import base
from fuelclient.common import data_utils
from fuelclient import utils
class NodeMixIn(object):
entity_name = 'node'
numa_fields = (
'numa_nodes',
'supported_hugepages',
'distances')
supported_file_formats = ('json', 'yaml')
allowed_attr_types = ('attributes', 'disks', 'interfaces')
@classmethod
def get_numa_topology_info(cls, data):
numa_topology_info = {}
numa_topology = data['meta'].get('numa_topology', {})
for key in cls.numa_fields:
numa_topology_info[key] = numa_topology.get(key)
return numa_topology_info
@six.add_metaclass(abc.ABCMeta)
class BaseUploadCommand(NodeMixIn, base.BaseCommand):
"""Base class for uploading attributes of a node."""
@abc.abstractproperty
def attribute(self):
"""String with the name of the attribute."""
pass
@abc.abstractproperty
def uploader(self):
"""Callable for uploading data."""
pass
def get_parser(self, prog_name):
parser = super(BaseUploadCommand, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of a node.')
parser.add_argument('-f',
'--format',
required=True,
choices=self.supported_file_formats,
help='Format of serialized '
'{} data.'.format(self.attribute))
parser.add_argument('-d',
'--directory',
required=False,
default=os.curdir,
help='Source directory. Defaults to '
'the current directory.')
return parser
def take_action(self, parsed_args):
directory = parsed_args.directory
file_path = self.get_attributes_path(self.attribute,
parsed_args.format,
parsed_args.id,
directory)
try:
with open(file_path, 'r') as stream:
attributes = data_utils.safe_load(parsed_args.format,
stream)
self.uploader(parsed_args.id, attributes)
except (OSError, IOError):
msg = 'Could not read configuration of {} at {}.'
raise error.InvalidFileException(msg.format(self.attribute,
file_path))
msg = ('Configuration of {t} for node with id '
'{node} was loaded from {path}\n')
self.app.stdout.write(msg.format(t=self.attribute,
node=parsed_args.id,
path=file_path))
@six.add_metaclass(abc.ABCMeta)
class BaseDownloadCommand(NodeMixIn, base.BaseCommand):
"""Base class for downloading attributes of a node."""
@abc.abstractproperty
def attribute(self):
"""String with the name of the attribute."""
pass
@abc.abstractproperty
def downloader(self):
"""Callable for downloading data."""
pass
def get_parser(self, prog_name):
parser = super(BaseDownloadCommand, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of a node.')
parser.add_argument('-f',
'--format',
required=True,
choices=self.supported_file_formats,
help='Format of serialized '
'{} data.'.format(self.attribute))
parser.add_argument('-d',
'--directory',
required=False,
default=os.curdir,
help='Destination directory. Defaults to '
'the current directory.')
return parser
def take_action(self, parsed_args):
directory = parsed_args.directory
attributes = self.downloader(parsed_args.id)
file_path = self.get_attributes_path(self.attribute,
parsed_args.format,
parsed_args.id,
directory)
try:
fileutils.ensure_tree(os.path.dirname(file_path))
fileutils.delete_if_exists(file_path)
with open(file_path, 'w') as stream:
data_utils.safe_dump(parsed_args.format, stream, attributes)
except (OSError, IOError):
msg = 'Could not store configuration of {} at {}.'
raise error.InvalidFileException(msg.format(self.attribute,
file_path))
msg = ('Configuration of {t} for node with id '
'{node} was stored in {path}\n')
self.app.stdout.write(msg.format(t=self.attribute,
node=parsed_args.id,
path=file_path))
class NodeList(NodeMixIn, base.BaseListCommand):
"""Show list of all available nodes."""
columns = ('id',
'name',
'status',
'os_platform',
'roles',
'ip',
'mac',
'cluster',
'platform_name',
'online')
def get_parser(self, prog_name):
parser = super(NodeList, self).get_parser(prog_name)
parser.add_argument(
'-e',
'--env',
type=int,
help='Show only nodes that are in the specified environment')
parser.add_argument(
'-l',
'--labels',
type=utils.str_to_unicode,
nargs='+',
help='Show only nodes that have specific labels')
return parser
def take_action(self, parsed_args):
data = self.client.get_all(
environment_id=parsed_args.env, labels=parsed_args.labels)
data = data_utils.get_display_data_multi(self.columns, data)
return (self.columns, data)
class NodeShow(NodeMixIn, base.BaseShowCommand):
"""Show info about node with given id."""
columns = ('id',
'name',
'status',
'os_platform',
'roles',
'kernel_params',
'pending_roles',
'ip',
'mac',
'error_type',
'pending_addition',
'hostname',
'fqdn',
'platform_name',
'cluster',
'online',
'progress',
'pending_deletion',
'group_id',
# TODO(romcheg): network_data mostly never fits the screen
# 'network_data',
'manufacturer')
columns += NodeMixIn.numa_fields
def take_action(self, parsed_args):
data = self.client.get_by_id(parsed_args.id)
numa_topology = self.get_numa_topology_info(data)
data.update(numa_topology)
data = data_utils.get_display_data_single(self.columns, data)
return self.columns, data
class NodeUpdate(NodeMixIn, base.BaseShowCommand):
"""Change given attributes for a node."""
columns = NodeShow.columns
def get_parser(self, prog_name):
parser = super(NodeUpdate, self).get_parser(prog_name)
parser.add_argument(
'-H',
'--hostname',
type=str,
default=None,
help='New hostname for node')
parser.add_argument(
'--name',
type=lambda x: x.decode('utf-8') if six.PY2 else x,
default=None,
help='New name for node')
return parser
def take_action(self, parsed_args):
updates = {}
for attr in self.client._updatable_attributes:
if getattr(parsed_args, attr, None):
updates[attr] = getattr(parsed_args, attr)
updated_node = self.client.update(
parsed_args.id, **updates)
numa_topology = self.get_numa_topology_info(updated_node)
updated_node.update(numa_topology)
updated_node = data_utils.get_display_data_single(
self.columns, updated_node)
return self.columns, updated_node
class NodeVmsList(NodeMixIn, base.BaseShowCommand):
"""Show list vms for node."""
columns = ('vms_conf',)
def take_action(self, parsed_args):
data = self.client.get_node_vms_conf(parsed_args.id)
data = data_utils.get_display_data_single(self.columns, data)
return (self.columns, data)
class NodeCreateVMsConf(NodeMixIn, base.BaseCommand):
"""Create vms config in metadata for selected node."""
def get_parser(self, prog_name):
parser = super(NodeCreateVMsConf, self).get_parser(prog_name)
parser.add_argument('id', type=int,
help='Id of the {0}.'.format(self.entity_name))
parser.add_argument(
'--conf',
type=json.loads,
required=True,
nargs='+',
help='JSONs with VMs configuration',
)
return parser
def take_action(self, parsed_args):
try:
confs = utils.parse_to_list_of_dicts(parsed_args.conf)
except TypeError:
raise error.BadDataException(
'VM configuration should be a dictionary '
'or a list of dictionaries')
data = self.client.node_vms_create(parsed_args.id, confs)
msg = "{0}".format(data)
self.app.stdout.write(msg)
class NodeLabelList(NodeMixIn, base.BaseListCommand):
"""Show list of all labels."""
columns = (
'node_id',
'label_name',
'label_value')
def get_parser(self, prog_name):
parser = super(NodeLabelList, self).get_parser(prog_name)
parser.add_argument(
'-n',
'--nodes',
nargs='+',
help='Show labels for specific nodes')
return parser
def take_action(self, parsed_args):
data = self.client.get_all_labels_for_nodes(
node_ids=parsed_args.nodes)
data = data_utils.get_display_data_multi(self.columns, data)
return (self.columns, data)
class NodeLabelSet(NodeMixIn, base.BaseCommand):
"""Create or update specifc labels on nodes."""
def get_parser(self, prog_name):
parser = super(NodeLabelSet, self).get_parser(prog_name)
parser.add_argument(
'-l',
'--labels',
required=True,
nargs='+',
help='List of labels for create or update')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
'-n',
'--nodes',
nargs='+',
help='Create or update labels only for specific nodes')
group.add_argument(
'--nodes-all',
action='store_true',
help='Create or update labels for all nodes')
return parser
def take_action(self, parsed_args):
nodes_ids = None if parsed_args.nodes_all else parsed_args.nodes
data = self.client.set_labels_for_nodes(
labels=parsed_args.labels, node_ids=nodes_ids)
msg = "Labels have been updated on nodes: {0} \n".format(
','.join(data))
self.app.stdout.write(msg)
class NodeLabelDelete(NodeMixIn, base.BaseCommand):
"""Delete specific labels on nodes."""
def get_parser(self, prog_name):
parser = super(NodeLabelDelete, self).get_parser(prog_name)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
'-l',
'--labels',
nargs='+',
help='List of labels keys for delete')
group.add_argument(
'--labels-all',
action='store_true',
help='Delete all labels for node')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
'-n',
'--nodes',
nargs='+',
help='Delete labels only for specific nodes')
group.add_argument(
'--nodes-all',
action='store_true',
help='Delete labels for all nodes')
return parser
def take_action(self, parsed_args):
nodes_ids = None if parsed_args.nodes_all else parsed_args.nodes
labels = None if parsed_args.labels_all \
else parsed_args.labels
data = self.client.delete_labels_for_nodes(
labels=labels, node_ids=nodes_ids)
msg = "Labels have been deleted on nodes: {0} \n".format(
','.join(data))
self.app.stdout.write(msg)
class NodeAttributesDownload(NodeMixIn, base.BaseCommand):
"""Download node attributes."""
def get_parser(self, prog_name):
parser = super(NodeAttributesDownload, self).get_parser(prog_name)
parser.add_argument(
'id', type=int, help='Node ID')
parser.add_argument(
'--dir', type=str, help='Directory to save attributes')
return parser
def take_action(self, parsed_args):
file_path = self.client.download_attributes(
parsed_args.id, parsed_args.dir)
self.app.stdout.write(
"Attributes for node {0} were written to {1}"
.format(parsed_args.id, file_path) + os.linesep)
class NodeAttributesUpload(NodeMixIn, base.BaseCommand):
"""Upload node attributes."""
def get_parser(self, prog_name):
parser = super(NodeAttributesUpload, self).get_parser(prog_name)
parser.add_argument(
'id', type=int, help='Node ID')
parser.add_argument(
'--dir', type=str, help='Directory to read attributes from')
return parser
def take_action(self, parsed_args):
self.client.upload_attributes(parsed_args.id, parsed_args.dir)
self.app.stdout.write(
"Attributes for node {0} were uploaded."
.format(parsed_args.id) + os.linesep)
class NodeAnsibleInventory(NodeMixIn, base.BaseCommand):
"""Generate ansible inventory file based on the nodes list."""
def get_parser(self, prog_name):
parser = super(NodeAnsibleInventory, self).get_parser(prog_name)
# if this is a required argument, we'll avoid ambiguity of having nodes
# of multiple different clusters in the same inventory file
parser.add_argument(
'-e',
'--env',
type=int,
required=True,
help='Use only nodes that are in the specified environment')
parser.add_argument(
'-l',
'--labels',
type=utils.str_to_unicode,
nargs='+',
help='Use only nodes that have specific labels')
return parser
def take_action(self, parsed_args):
data = self.client.get_all(environment_id=parsed_args.env,
labels=parsed_args.labels)
nodes_by_role = collections.defaultdict(list)
for node in data:
for role in node['roles']:
nodes_by_role[role].append(node)
for role, nodes in sorted(nodes_by_role.items()):
self.app.stdout.write(u'[{role}]\n'.format(role=role))
self.app.stdout.write(
u'\n'.join(
u'{name} ansible_host={ip}'.format(name=node['hostname'],
ip=node['ip'])
for node in sorted(nodes_by_role[role],
key=operator.itemgetter('hostname'))
)
)
self.app.stdout.write(u'\n\n')
class NodeInterfacesDownload(BaseDownloadCommand):
"""Download and store configuration of interfaces for a node to a file."""
attribute = 'interfaces'
@property
def downloader(self):
return self.client.get_interfaces
class NodeInterfacesGetDefault(BaseDownloadCommand):
"""Download default configuration of interfaces for a node to a file."""
attribute = 'interfaces'
@property
def downloader(self):
return self.client.get_default_interfaces
class NodeInterfacesUpload(BaseUploadCommand):
"""Upload stored configuration of interfaces for a node from a file."""
attribute = 'interfaces'
@property
def uploader(self):
return self.client.set_interfaces
class NodeDisksDownload(BaseDownloadCommand):
"""Download and store configuration of disks for a node to a file."""
attribute = 'disks'
@property
def downloader(self):
return self.client.get_disks
class NodeDisksGetDefault(BaseDownloadCommand):
"""Download default configuration of disks for a node to a file."""
attribute = 'disks'
@property
def downloader(self):
return self.client.get_default_disks
class NodeDisksUpload(BaseUploadCommand):
"""Upload stored configuration of disks for a node from a file."""
attribute = 'disks'
@property
def uploader(self):
return self.client.set_disks
class NodeUndiscover(NodeMixIn, base.BaseCommand):
"""Remove nodes from database."""
def get_parser(self, prog_name):
parser = super(NodeUndiscover, self).get_parser(prog_name)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-e',
'--env',
type=int,
help='Id of environment to remove all nodes '
'from database.')
group.add_argument('-n',
'--node',
type=int,
help='Id of the node to remove from database.')
parser.add_argument(
'-f',
'--force',
action='store_true',
help='Forces deletion of nodes from database '
'regardless of their state.')
return parser
def take_action(self, parsed_args):
node_ids = self.client.undiscover_nodes(env_id=parsed_args.env,
node_id=parsed_args.node,
force=parsed_args.force)
self.app.stdout.write(
'Nodes {0} were deleted from the database\n'.format(node_ids)
)