You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
443 lines
14 KiB
Python
443 lines
14 KiB
Python
# Copyright 2012 OpenStack Foundation
|
|
# 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.
|
|
|
|
import base64
|
|
import logging
|
|
import os
|
|
import textwrap
|
|
import uuid
|
|
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import encodeutils
|
|
import prettytable
|
|
from urllib import error
|
|
from urllib import parse
|
|
from urllib import request
|
|
import yaml
|
|
|
|
from heatclient._i18n import _
|
|
from heatclient import exc
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
supported_formats = {
|
|
"json": lambda x: jsonutils.dumps(x, indent=2),
|
|
"yaml": yaml.safe_dump
|
|
}
|
|
|
|
|
|
def arg(*args, **kwargs):
|
|
"""Decorator for CLI args.
|
|
|
|
Example:
|
|
|
|
>>> @arg("name", help="Name of the new entity")
|
|
... def entity_create(args):
|
|
... pass
|
|
"""
|
|
def _decorator(func):
|
|
add_arg(func, *args, **kwargs)
|
|
return func
|
|
return _decorator
|
|
|
|
|
|
def env(*args, **kwargs):
|
|
"""Returns the first environment variable set.
|
|
|
|
If all are empty, defaults to '' or keyword arg `default`.
|
|
"""
|
|
for arg in args:
|
|
value = os.environ.get(arg)
|
|
if value:
|
|
return value
|
|
return kwargs.get('default', '')
|
|
|
|
|
|
def add_arg(func, *args, **kwargs):
|
|
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
|
|
|
if not hasattr(func, 'arguments'):
|
|
func.arguments = []
|
|
|
|
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
|
# tests.
|
|
if (args, kwargs) not in func.arguments:
|
|
# Because of the semantics of decorator composition if we just append
|
|
# to the options list positional options will appear to be backwards.
|
|
func.arguments.insert(0, (args, kwargs))
|
|
|
|
|
|
def print_list(objs, fields, formatters=None, sortby_index=0,
|
|
mixed_case_fields=None, field_labels=None):
|
|
"""Print a list of objects as a table, one row per object.
|
|
|
|
:param objs: iterable of :class:`Resource`
|
|
:param fields: attributes that correspond to columns, in order
|
|
:param formatters: `dict` of callables for field formatting
|
|
:param sortby_index: index of the field for sorting table rows
|
|
:param mixed_case_fields: fields corresponding to object attributes that
|
|
have mixed case names (e.g., 'serverId')
|
|
:param field_labels: Labels to use in the heading of the table, default to
|
|
fields.
|
|
"""
|
|
formatters = formatters or {}
|
|
mixed_case_fields = mixed_case_fields or []
|
|
field_labels = field_labels or fields
|
|
if len(field_labels) != len(fields):
|
|
raise ValueError(_("Field labels list %(labels)s has different number "
|
|
"of elements than fields list %(fields)s"),
|
|
{'labels': field_labels, 'fields': fields})
|
|
|
|
if sortby_index is None:
|
|
kwargs = {}
|
|
else:
|
|
kwargs = {'sortby': field_labels[sortby_index]}
|
|
pt = prettytable.PrettyTable(field_labels)
|
|
pt.align = 'l'
|
|
|
|
for o in objs:
|
|
row = []
|
|
for field in fields:
|
|
if field in formatters:
|
|
row.append(formatters[field](o))
|
|
else:
|
|
if field in mixed_case_fields:
|
|
field_name = field.replace(' ', '_')
|
|
else:
|
|
field_name = field.lower().replace(' ', '_')
|
|
data = getattr(o, field_name, '')
|
|
row.append(data)
|
|
pt.add_row(row)
|
|
|
|
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
|
|
|
|
|
|
def link_formatter(links):
|
|
def format_link(l):
|
|
if 'rel' in l:
|
|
return "%s (%s)" % (l.get('href', ''), l.get('rel', ''))
|
|
else:
|
|
return "%s" % (l.get('href', ''))
|
|
return '\n'.join(format_link(l) for l in links or [])
|
|
|
|
|
|
def resource_nested_identifier(rsrc):
|
|
nested_link = [l for l in rsrc.links or []
|
|
if l.get('rel') == 'nested']
|
|
if nested_link:
|
|
nested_href = nested_link[0].get('href')
|
|
nested_identifier = nested_href.split("/")[-2:]
|
|
return "/".join(nested_identifier)
|
|
|
|
|
|
def json_formatter(js):
|
|
return jsonutils.dumps(js, indent=2, ensure_ascii=False,
|
|
separators=(', ', ': '))
|
|
|
|
|
|
def yaml_formatter(js):
|
|
return yaml.safe_dump(js, default_flow_style=False)
|
|
|
|
|
|
def text_wrap_formatter(d):
|
|
return '\n'.join(textwrap.wrap(d or '', 55))
|
|
|
|
|
|
def newline_list_formatter(r):
|
|
return '\n'.join(r or [])
|
|
|
|
|
|
def print_dict(d, formatters=None):
|
|
formatters = formatters or {}
|
|
pt = prettytable.PrettyTable(['Property', 'Value'],
|
|
caching=False, print_empty=False)
|
|
pt.align = 'l'
|
|
|
|
for field in d:
|
|
if field in formatters:
|
|
pt.add_row([field, formatters[field](d[field])])
|
|
else:
|
|
pt.add_row([field, d[field]])
|
|
print(pt.get_string(sortby='Property'))
|
|
|
|
|
|
class EventLogContext(object):
|
|
|
|
def __init__(self):
|
|
# key is a stack id or the name of the nested stack, value is a tuple
|
|
# of the parent stack id, and the name of the resource in the parent
|
|
# stack
|
|
self.id_to_res_info = {}
|
|
|
|
def prepend_paths(self, resource_path, stack_id):
|
|
if stack_id not in self.id_to_res_info:
|
|
return
|
|
stack_id, res_name = self.id_to_res_info.get(stack_id)
|
|
if res_name in self.id_to_res_info:
|
|
# do a double lookup to skip the ugly stack name that doesn't
|
|
# correspond to an actual resource name
|
|
n_stack_id, res_name = self.id_to_res_info.get(res_name)
|
|
resource_path.insert(0, res_name)
|
|
self.prepend_paths(resource_path, n_stack_id)
|
|
elif res_name:
|
|
resource_path.insert(0, res_name)
|
|
|
|
def build_resource_name(self, event):
|
|
res_name = getattr(event, 'resource_name')
|
|
|
|
# Contribute this event to self.id_to_res_info to assist with
|
|
# future calls to build_resource_name
|
|
|
|
def get_stack_id():
|
|
if getattr(event, 'stack_id', None) is not None:
|
|
return event.stack_id
|
|
for l in getattr(event, 'links', []):
|
|
if l.get('rel') == 'stack':
|
|
if 'href' not in l:
|
|
return None
|
|
stack_link = l['href']
|
|
return stack_link.split('/')[-1]
|
|
|
|
stack_id = get_stack_id()
|
|
if not stack_id:
|
|
return res_name
|
|
phys_id = getattr(event, 'physical_resource_id', None)
|
|
status = getattr(event, 'resource_status', None)
|
|
|
|
is_stack_event = stack_id == phys_id
|
|
if is_stack_event:
|
|
# this is an event for a stack
|
|
self.id_to_res_info[stack_id] = (stack_id, res_name)
|
|
elif phys_id and status == 'CREATE_IN_PROGRESS':
|
|
# this might be an event for a resource which creates a stack
|
|
self.id_to_res_info[phys_id] = (stack_id, res_name)
|
|
|
|
# Now build this resource path based on previous calls to
|
|
# build_resource_name
|
|
resource_path = []
|
|
if res_name and not is_stack_event:
|
|
resource_path.append(res_name)
|
|
self.prepend_paths(resource_path, stack_id)
|
|
|
|
return '.'.join(resource_path)
|
|
|
|
|
|
def event_log_formatter(events, event_log_context=None):
|
|
"""Return the events in log format."""
|
|
event_log = []
|
|
log_format = ("%(event_time)s "
|
|
"[%(rsrc_name)s]: %(rsrc_status)s %(rsrc_status_reason)s")
|
|
|
|
# It is preferable for a context to be passed in, but there might be enough
|
|
# events in this call to build a better resource name, so create a context
|
|
# anyway
|
|
if event_log_context is None:
|
|
event_log_context = EventLogContext()
|
|
|
|
for event in events:
|
|
rsrc_name = event_log_context.build_resource_name(event)
|
|
|
|
event_time = getattr(event, 'event_time', '')
|
|
log = log_format % {
|
|
'event_time': event_time.replace('T', ' '),
|
|
'rsrc_name': rsrc_name,
|
|
'rsrc_status': getattr(event, 'resource_status', ''),
|
|
'rsrc_status_reason': getattr(event, 'resource_status_reason', '')
|
|
}
|
|
event_log.append(log)
|
|
|
|
return "\n".join(event_log)
|
|
|
|
|
|
def print_update_list(lst, fields, formatters=None):
|
|
"""Print the stack-update --dry-run output as a table.
|
|
|
|
This function is necessary to print the stack-update --dry-run
|
|
output, which contains additional information about the update.
|
|
"""
|
|
formatters = formatters or {}
|
|
pt = prettytable.PrettyTable(fields, caching=False, print_empty=False)
|
|
pt.align = 'l'
|
|
|
|
for change in lst:
|
|
row = []
|
|
for field in fields:
|
|
if field in formatters:
|
|
row.append(formatters[field](change.get(field, None)))
|
|
else:
|
|
row.append(change.get(field, None))
|
|
|
|
pt.add_row(row)
|
|
|
|
print(encodeutils.safe_encode(pt.get_string()).decode())
|
|
|
|
|
|
def find_resource(manager, name_or_id):
|
|
"""Helper for the _find_* methods."""
|
|
# first try to get entity as integer id
|
|
try:
|
|
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
|
return manager.get(int(name_or_id))
|
|
except exc.NotFound:
|
|
pass
|
|
|
|
# now try to get entity as uuid
|
|
try:
|
|
uuid.UUID(str(name_or_id))
|
|
return manager.get(name_or_id)
|
|
except (ValueError, exc.NotFound):
|
|
pass
|
|
|
|
# finally try to find entity by name
|
|
try:
|
|
return manager.find(name=name_or_id)
|
|
except exc.NotFound:
|
|
msg = (
|
|
_("No %(name)s with a name or ID of "
|
|
"'%(name_or_id)s' exists.")
|
|
% {
|
|
'name': manager.resource_class.__name__.lower(),
|
|
'name_or_id': name_or_id
|
|
})
|
|
raise exc.CommandError(msg)
|
|
|
|
|
|
def format_parameters(params, parse_semicolon=True):
|
|
'''Reformat parameters into dict of format expected by the API.'''
|
|
|
|
if not params:
|
|
return {}
|
|
|
|
if parse_semicolon:
|
|
# expect multiple invocations of --parameters but fall back
|
|
# to ; delimited if only one --parameters is specified
|
|
if len(params) == 1:
|
|
params = params[0].split(';')
|
|
|
|
parameters = {}
|
|
for p in params:
|
|
try:
|
|
(n, v) = p.split(('='), 1)
|
|
except ValueError:
|
|
msg = _('Malformed parameter(%s). Use the key=value format.') % p
|
|
raise exc.CommandError(msg)
|
|
|
|
if n not in parameters:
|
|
parameters[n] = v
|
|
else:
|
|
if not isinstance(parameters[n], list):
|
|
parameters[n] = [parameters[n]]
|
|
parameters[n].append(v)
|
|
|
|
return parameters
|
|
|
|
|
|
def format_all_parameters(params, param_files,
|
|
template_file=None, template_url=None):
|
|
parameters = {}
|
|
parameters.update(format_parameters(params))
|
|
parameters.update(format_parameter_file(
|
|
param_files,
|
|
template_file,
|
|
template_url))
|
|
return parameters
|
|
|
|
|
|
def format_parameter_file(param_files, template_file=None,
|
|
template_url=None):
|
|
'''Reformat file parameters into dict of format expected by the API.'''
|
|
if not param_files:
|
|
return {}
|
|
params = format_parameters(param_files, False)
|
|
|
|
template_base_url = None
|
|
if template_file or template_url:
|
|
template_base_url = base_url_for_url(get_template_url(
|
|
template_file, template_url))
|
|
|
|
param_file = {}
|
|
for key, value in params.items():
|
|
param_file[key] = resolve_param_get_file(value,
|
|
template_base_url)
|
|
return param_file
|
|
|
|
|
|
def resolve_param_get_file(file, base_url):
|
|
if base_url and not base_url.endswith('/'):
|
|
base_url = base_url + '/'
|
|
str_url = parse.urljoin(base_url, file)
|
|
return read_url_content(str_url)
|
|
|
|
|
|
def format_output(output, format='yaml'):
|
|
"""Format the supplied dict as specified."""
|
|
output_format = format.lower()
|
|
try:
|
|
return supported_formats[output_format](output)
|
|
except KeyError:
|
|
raise exc.HTTPUnsupported(_("The format(%s) is unsupported.")
|
|
% output_format)
|
|
|
|
|
|
def parse_query_url(url):
|
|
base_url, query_params = url.split('?')
|
|
return base_url, parse.parse_qs(query_params)
|
|
|
|
|
|
def get_template_url(template_file=None, template_url=None):
|
|
if template_file:
|
|
template_url = normalise_file_path_to_url(template_file)
|
|
return template_url
|
|
|
|
|
|
def read_url_content(url):
|
|
try:
|
|
content = request.urlopen(url).read()
|
|
except error.URLError:
|
|
raise exc.CommandError(_('Could not fetch contents for %s') % url)
|
|
|
|
if content:
|
|
try:
|
|
content.decode('utf-8')
|
|
except ValueError:
|
|
content = base64.encodebytes(content)
|
|
return content
|
|
|
|
|
|
def base_url_for_url(url):
|
|
parsed = parse.urlparse(url)
|
|
parsed_dir = os.path.dirname(parsed.path)
|
|
return parse.urljoin(url, parsed_dir)
|
|
|
|
|
|
def normalise_file_path_to_url(path):
|
|
if parse.urlparse(path).scheme:
|
|
return path
|
|
path = os.path.abspath(path)
|
|
return parse.urljoin('file:', request.pathname2url(path))
|
|
|
|
|
|
def get_response_body(resp):
|
|
body = resp.content
|
|
if 'application/json' in resp.headers.get('content-type', ''):
|
|
try:
|
|
body = resp.json()
|
|
except ValueError:
|
|
LOG.error('Could not decode response body as JSON')
|
|
else:
|
|
body = None
|
|
return body
|