894699d2b4
This patch add a "--fields" parameter to the "port-list", "port-show", "chassis-list", "chassis-show" and "chassis-node-list" commands which the user can specify a subset of fields that will be returned by the server. This patch also enhances the error message for check_for_invalid_fileds() method to show what are the valid fields. Change-Id: Iaf90d615c9fa68f24e54c23d708ff9c5bee7c46c
307 lines
11 KiB
Python
307 lines
11 KiB
Python
# Copyright 2012 OpenStack LLC.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import base64
|
|
import contextlib
|
|
import gzip
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from oslo_utils import importutils
|
|
from oslo_utils import strutils
|
|
|
|
from ironicclient.common.i18n import _
|
|
from ironicclient import exc
|
|
|
|
|
|
class HelpFormatter(argparse.HelpFormatter):
|
|
def start_section(self, heading):
|
|
# Title-case the headings
|
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
|
super(HelpFormatter, self).start_section(heading)
|
|
|
|
|
|
def define_command(subparsers, command, callback, cmd_mapper):
|
|
'''Define a command in the subparsers collection.
|
|
|
|
:param subparsers: subparsers collection where the command will go
|
|
:param command: command name
|
|
:param callback: function that will be used to process the command
|
|
'''
|
|
desc = callback.__doc__ or ''
|
|
help = desc.strip().split('\n')[0]
|
|
arguments = getattr(callback, 'arguments', [])
|
|
|
|
subparser = subparsers.add_parser(command, help=help,
|
|
description=desc,
|
|
add_help=False,
|
|
formatter_class=HelpFormatter)
|
|
subparser.add_argument('-h', '--help', action='help',
|
|
help=argparse.SUPPRESS)
|
|
cmd_mapper[command] = subparser
|
|
for (args, kwargs) in arguments:
|
|
subparser.add_argument(*args, **kwargs)
|
|
subparser.set_defaults(func=callback)
|
|
|
|
|
|
def define_commands_from_module(subparsers, command_module, cmd_mapper):
|
|
"""Add *do_* methods in a module and add as commands into a subparsers."""
|
|
|
|
for method_name in (a for a in dir(command_module) if a.startswith('do_')):
|
|
# Commands should be hypen-separated instead of underscores.
|
|
command = method_name[3:].replace('_', '-')
|
|
callback = getattr(command_module, method_name)
|
|
define_command(subparsers, command, callback, cmd_mapper)
|
|
|
|
|
|
def import_versioned_module(version, submodule=None):
|
|
module = 'ironicclient.v%s' % version
|
|
if submodule:
|
|
module = '.'.join((module, submodule))
|
|
return importutils.import_module(module)
|
|
|
|
|
|
def split_and_deserialize(string):
|
|
"""Split and try to JSON deserialize a string.
|
|
|
|
Gets a string with the KEY=VALUE format, split it (using '=' as the
|
|
separator) and try to JSON deserialize the VALUE.
|
|
|
|
:returns: A tuple of (key, value).
|
|
"""
|
|
try:
|
|
key, value = string.split("=", 1)
|
|
except ValueError:
|
|
raise exc.CommandError(_('Attributes must be a list of '
|
|
'PATH=VALUE not "%s"') % string)
|
|
try:
|
|
value = json.loads(value)
|
|
except ValueError:
|
|
pass
|
|
|
|
return (key, value)
|
|
|
|
|
|
def args_array_to_dict(kwargs, key_to_convert):
|
|
values_to_convert = kwargs.get(key_to_convert)
|
|
if values_to_convert:
|
|
kwargs[key_to_convert] = dict(split_and_deserialize(v)
|
|
for v in values_to_convert)
|
|
return kwargs
|
|
|
|
|
|
def args_array_to_patch(op, attributes):
|
|
patch = []
|
|
for attr in attributes:
|
|
# Sanitize
|
|
if not attr.startswith('/'):
|
|
attr = '/' + attr
|
|
|
|
if op in ['add', 'replace']:
|
|
path, value = split_and_deserialize(attr)
|
|
patch.append({'op': op, 'path': path, 'value': value})
|
|
|
|
elif op == "remove":
|
|
# For remove only the key is needed
|
|
patch.append({'op': op, 'path': attr})
|
|
else:
|
|
raise exc.CommandError(_('Unknown PATCH operation: %s') % op)
|
|
return patch
|
|
|
|
|
|
def common_params_for_list(args, fields, field_labels):
|
|
"""Generate 'params' dict that is common for every 'list' command.
|
|
|
|
:param args: arguments from command line.
|
|
:param fields: possible fields for sorting.
|
|
:param field_labels: possible field labels for sorting.
|
|
:returns: a dict with params to pass to the client method.
|
|
"""
|
|
params = {}
|
|
if args.marker is not None:
|
|
params['marker'] = args.marker
|
|
if args.limit is not None:
|
|
if args.limit < 0:
|
|
raise exc.CommandError(
|
|
_('Expected non-negative --limit, got %s') % args.limit)
|
|
params['limit'] = args.limit
|
|
|
|
if args.sort_key is not None:
|
|
# Support using both heading and field name for sort_key
|
|
fields_map = dict(zip(field_labels, fields))
|
|
fields_map.update(zip(fields, fields))
|
|
try:
|
|
sort_key = fields_map[args.sort_key]
|
|
except KeyError:
|
|
raise exc.CommandError(
|
|
_("%(sort_key)s is an invalid field for sorting, "
|
|
"valid values for --sort-key are: %(valid)s") %
|
|
{'sort_key': args.sort_key,
|
|
'valid': list(fields_map)})
|
|
params['sort_key'] = sort_key
|
|
if args.sort_dir is not None:
|
|
if args.sort_dir not in ('asc', 'desc'):
|
|
raise exc.CommandError(
|
|
_("%s is an invalid value for sort direction, "
|
|
"valid values for --sort-dir are: 'asc', 'desc'") %
|
|
args.sort_dir)
|
|
params['sort_dir'] = args.sort_dir
|
|
|
|
params['detail'] = args.detail
|
|
|
|
requested_fields = args.fields[0] if args.fields else None
|
|
if requested_fields is not None:
|
|
params['fields'] = requested_fields
|
|
|
|
return params
|
|
|
|
|
|
def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None,
|
|
fields=None):
|
|
"""Generate common filters for any list request.
|
|
|
|
:param marker: entity ID from which to start returning entities.
|
|
:param limit: maximum number of entities to return.
|
|
:param sort_key: field to use for sorting.
|
|
:param sort_dir: direction of sorting: 'asc' or 'desc'.
|
|
:param fields: a list with a specified set of fields of the resource
|
|
to be returned.
|
|
:returns: list of string filters.
|
|
"""
|
|
filters = []
|
|
if isinstance(limit, int) and limit > 0:
|
|
filters.append('limit=%s' % limit)
|
|
if marker is not None:
|
|
filters.append('marker=%s' % marker)
|
|
if sort_key is not None:
|
|
filters.append('sort_key=%s' % sort_key)
|
|
if sort_dir is not None:
|
|
filters.append('sort_dir=%s' % sort_dir)
|
|
if fields is not None:
|
|
filters.append('fields=%s' % ','.join(fields))
|
|
return filters
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def tempdir(*args, **kwargs):
|
|
dirname = tempfile.mkdtemp(*args, **kwargs)
|
|
try:
|
|
yield dirname
|
|
finally:
|
|
shutil.rmtree(dirname)
|
|
|
|
|
|
def make_configdrive(path):
|
|
"""Make the config drive file.
|
|
|
|
:param path: The directory containing the config drive files.
|
|
:returns: A gzipped and base64 encoded configdrive string.
|
|
|
|
"""
|
|
# Make sure path it's readable
|
|
if not os.access(path, os.R_OK):
|
|
raise exc.CommandError(_('The directory "%s" is not readable') % path)
|
|
|
|
with tempfile.NamedTemporaryFile() as tmpfile:
|
|
with tempfile.NamedTemporaryFile() as tmpzipfile:
|
|
publisher = 'ironicclient-configdrive 0.1'
|
|
try:
|
|
p = subprocess.Popen(['genisoimage', '-o', tmpfile.name,
|
|
'-ldots', '-allow-lowercase',
|
|
'-allow-multidot', '-l',
|
|
'-publisher', publisher,
|
|
'-quiet', '-J',
|
|
'-r', '-V', 'config-2',
|
|
path],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
except OSError as e:
|
|
raise exc.CommandError(
|
|
_('Error generating the config drive. Make sure the '
|
|
'"genisoimage" tool is installed. Error: %s') % e)
|
|
|
|
stdout, stderr = p.communicate()
|
|
if p.returncode != 0:
|
|
raise exc.CommandError(
|
|
_('Error generating the config drive.'
|
|
'Stdout: "%(stdout)s". Stderr: %(stderr)s') %
|
|
{'stdout': stdout, 'stderr': stderr})
|
|
|
|
# Compress file
|
|
tmpfile.seek(0)
|
|
g = gzip.GzipFile(fileobj=tmpzipfile, mode='wb')
|
|
shutil.copyfileobj(tmpfile, g)
|
|
g.close()
|
|
|
|
tmpzipfile.seek(0)
|
|
return base64.b64encode(tmpzipfile.read())
|
|
|
|
|
|
def check_empty_arg(arg, arg_descriptor):
|
|
if not arg.strip():
|
|
raise exc.CommandError(_('%(arg)s cannot be empty or only have blank'
|
|
' spaces') % {'arg': arg_descriptor})
|
|
|
|
|
|
def bool_argument_value(arg_name, bool_str, strict=True, default=False):
|
|
"""Returns the Boolean represented by bool_str.
|
|
|
|
Returns the Boolean value for the argument named arg_name. The value is
|
|
represented by the string bool_str. If the string is an invalid Boolean
|
|
string: if strict is True, a CommandError exception is raised; otherwise
|
|
the default value is returned.
|
|
|
|
:param arg_name: The name of the argument
|
|
:param bool_str: The string representing a Boolean value
|
|
:param strict: Used if the string is invalid. If True, raises an exception.
|
|
If False, returns the default value.
|
|
:param default: The default value to return if the string is invalid
|
|
and not strict
|
|
:returns: the Boolean value represented by bool_str or the default value
|
|
if bool_str is invalid and strict is False
|
|
:raises CommandError: if bool_str is an invalid Boolean string
|
|
|
|
"""
|
|
try:
|
|
val = strutils.bool_from_string(bool_str, strict, default)
|
|
except ValueError as e:
|
|
raise exc.CommandError(_("argument %(arg)s: %(err)s.")
|
|
% {'arg': arg_name, 'err': e})
|
|
return val
|
|
|
|
|
|
def check_for_invalid_fields(fields, valid_fields):
|
|
"""Check for invalid fields.
|
|
|
|
:param fields: A list of fields specified by the user.
|
|
:param valid_fields: A list of valid fields.
|
|
raises: CommandError: If invalid fields were specified by the user.
|
|
"""
|
|
if not fields:
|
|
return
|
|
|
|
invalid_fields = set(fields) - set(valid_fields)
|
|
if invalid_fields:
|
|
raise exc.CommandError(
|
|
_('Invalid field(s) requested: %(invalid)s. Valid fields '
|
|
'are: %(valid)s.') % {'invalid': ', '.join(invalid_fields),
|
|
'valid': ', '.join(valid_fields)})
|