Use osc-lib and set up deprecation warnings
The initial use of osc-lib is behind the compatibility/deprecation modules that we will leave in place for a time for plugins to catch up. * openstackclient.common.exceptions * openstackclient.common.utils Module-level warnings are emitted directly on stderr since logging has not been configured yet. Change-Id: I79e57ce9523a20366bccaf9b949ab5906792ea0d
This commit is contained in:
parent
df71ae814e
commit
6c269efda8
@ -1,5 +1,3 @@
|
|||||||
# Copyright 2012-2013 OpenStack, LLC.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,105 +11,15 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
"""Exception definitions."""
|
# NOTE(dtroyer): This file is deprecated in Jun 2016, remove after 4.x release
|
||||||
|
# or Jun 2017.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from osc_lib.exceptions import * # noqa
|
||||||
|
|
||||||
|
|
||||||
class CommandError(Exception):
|
sys.stderr.write(
|
||||||
pass
|
"WARNING: %s is deprecated and will be removed after Jun 2017. "
|
||||||
|
"Please use osc_lib.exceptions\n" % __name__
|
||||||
|
)
|
||||||
class AuthorizationFailure(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PluginAttributeError(Exception):
|
|
||||||
"""A plugin threw an AttributeError while being lazily loaded."""
|
|
||||||
# This *must not* inherit from AttributeError;
|
|
||||||
# that would defeat the whole purpose.
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NoTokenLookupException(Exception):
|
|
||||||
"""This does not support looking up endpoints from an existing token."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointNotFound(Exception):
|
|
||||||
"""Could not find Service or Region in Service Catalog."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedVersion(Exception):
|
|
||||||
"""The user is trying to use an unsupported version of the API"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
|
||||||
"""The base exception class for all exceptions this library raises."""
|
|
||||||
|
|
||||||
def __init__(self, code, message=None, details=None):
|
|
||||||
self.code = code
|
|
||||||
self.message = message or self.__class__.message
|
|
||||||
self.details = details
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s (HTTP %s)" % (self.message, self.code)
|
|
||||||
|
|
||||||
|
|
||||||
class BadRequest(ClientException):
|
|
||||||
"""HTTP 400 - Bad request: you sent some malformed data."""
|
|
||||||
http_status = 400
|
|
||||||
message = "Bad request"
|
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(ClientException):
|
|
||||||
"""HTTP 401 - Unauthorized: bad credentials."""
|
|
||||||
http_status = 401
|
|
||||||
message = "Unauthorized"
|
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(ClientException):
|
|
||||||
"""HTTP 403 - Forbidden: not authorized to access to this resource."""
|
|
||||||
http_status = 403
|
|
||||||
message = "Forbidden"
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(ClientException):
|
|
||||||
"""HTTP 404 - Not found"""
|
|
||||||
http_status = 404
|
|
||||||
message = "Not found"
|
|
||||||
|
|
||||||
|
|
||||||
class Conflict(ClientException):
|
|
||||||
"""HTTP 409 - Conflict"""
|
|
||||||
http_status = 409
|
|
||||||
message = "Conflict"
|
|
||||||
|
|
||||||
|
|
||||||
class OverLimit(ClientException):
|
|
||||||
"""HTTP 413 - Over limit: reached the API limits for this time period."""
|
|
||||||
http_status = 413
|
|
||||||
message = "Over limit"
|
|
||||||
|
|
||||||
|
|
||||||
# NotImplemented is a python keyword.
|
|
||||||
class HTTPNotImplemented(ClientException):
|
|
||||||
"""HTTP 501 - Not Implemented: server does not support this operation."""
|
|
||||||
http_status = 501
|
|
||||||
message = "Not Implemented"
|
|
||||||
|
|
||||||
|
|
||||||
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
|
||||||
# so we can do this:
|
|
||||||
# _code_map = dict((c.http_status, c)
|
|
||||||
# for c in ClientException.__subclasses__())
|
|
||||||
#
|
|
||||||
# Instead, we have to hardcode it:
|
|
||||||
_code_map = dict((c.http_status, c) for c in [
|
|
||||||
BadRequest,
|
|
||||||
Unauthorized,
|
|
||||||
Forbidden,
|
|
||||||
NotFound,
|
|
||||||
OverLimit,
|
|
||||||
HTTPNotImplemented
|
|
||||||
])
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# Copyright 2012-2013 OpenStack Foundation
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
# a copy of the License at
|
# a copy of the License at
|
||||||
@ -13,425 +11,15 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
"""Common client utilities"""
|
# NOTE(dtroyer): This file is deprecated in Jun 2016, remove after 4.x release
|
||||||
|
# or Jun 2017.
|
||||||
|
|
||||||
import getpass
|
import sys
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import six
|
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_utils import importutils
|
from osc_lib.utils import * # noqa
|
||||||
|
|
||||||
from openstackclient.common import exceptions
|
|
||||||
from openstackclient.i18n import _
|
|
||||||
|
|
||||||
|
sys.stderr.write(
|
||||||
def find_resource(manager, name_or_id, **kwargs):
|
"WARNING: %s is deprecated and will be removed after Jun 2017. "
|
||||||
"""Helper for the _find_* methods.
|
"Please use osc_lib.utils\n" % __name__
|
||||||
|
)
|
||||||
:param manager: A client manager class
|
|
||||||
:param name_or_id: The resource we are trying to find
|
|
||||||
:param kwargs: To be used in calling .find()
|
|
||||||
:rtype: The found resource
|
|
||||||
|
|
||||||
This method will attempt to find a resource in a variety of ways.
|
|
||||||
Primarily .get() methods will be called with `name_or_id` as an integer
|
|
||||||
value, and tried again as a string value.
|
|
||||||
|
|
||||||
If both fail, then a .find() is attempted, which is essentially calling
|
|
||||||
a .list() function with a 'name' query parameter that is set to
|
|
||||||
`name_or_id`.
|
|
||||||
|
|
||||||
Lastly, if any kwargs are passed in, they will be treated as additional
|
|
||||||
query parameters. This is particularly handy in the case of finding
|
|
||||||
resources in a domain.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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), **kwargs)
|
|
||||||
# FIXME(dtroyer): The exception to catch here is dependent on which
|
|
||||||
# client library the manager passed in belongs to.
|
|
||||||
# Eventually this should be pulled from a common set
|
|
||||||
# of client exceptions.
|
|
||||||
except Exception as ex:
|
|
||||||
if type(ex).__name__ == 'NotFound':
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Try directly using the passed value
|
|
||||||
try:
|
|
||||||
return manager.get(name_or_id, **kwargs)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if len(kwargs) == 0:
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Prepare the kwargs for calling find
|
|
||||||
if 'NAME_ATTR' in manager.resource_class.__dict__:
|
|
||||||
# novaclient does this for oddball resources
|
|
||||||
kwargs[manager.resource_class.NAME_ATTR] = name_or_id
|
|
||||||
else:
|
|
||||||
kwargs['name'] = name_or_id
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# finally try to find entity by name
|
|
||||||
try:
|
|
||||||
return manager.find(**kwargs)
|
|
||||||
# FIXME(dtroyer): The exception to catch here is dependent on which
|
|
||||||
# client library the manager passed in belongs to.
|
|
||||||
# Eventually this should be pulled from a common set
|
|
||||||
# of client exceptions.
|
|
||||||
except Exception as ex:
|
|
||||||
if type(ex).__name__ == 'NotFound':
|
|
||||||
msg = _("No %(resource)s with a name or ID "
|
|
||||||
"of '%(name_or_id)s' exists.")
|
|
||||||
raise exceptions.CommandError(
|
|
||||||
msg % {'resource': manager.resource_class.__name__.lower(),
|
|
||||||
'name_or_id': name_or_id}
|
|
||||||
)
|
|
||||||
if type(ex).__name__ == 'NoUniqueMatch':
|
|
||||||
msg = _("More than one %(resource)s exists with "
|
|
||||||
"the name '%(name_or_id)s'.")
|
|
||||||
raise exceptions.CommandError(
|
|
||||||
msg % {'resource': manager.resource_class.__name__.lower(),
|
|
||||||
'name_or_id': name_or_id}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for resource in manager.list():
|
|
||||||
# short circuit and return the first match
|
|
||||||
if (resource.get('id') == name_or_id or
|
|
||||||
resource.get('name') == name_or_id):
|
|
||||||
return resource
|
|
||||||
else:
|
|
||||||
# we found no match, report back this error:
|
|
||||||
msg = _("Could not find resource %s") % name_or_id
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def format_dict(data):
|
|
||||||
"""Return a formatted string of key value pairs
|
|
||||||
|
|
||||||
:param data: a dict
|
|
||||||
:rtype: a string formatted to key='value'
|
|
||||||
"""
|
|
||||||
|
|
||||||
output = ""
|
|
||||||
for s in sorted(data):
|
|
||||||
output = output + s + "='" + six.text_type(data[s]) + "', "
|
|
||||||
return output[:-2]
|
|
||||||
|
|
||||||
|
|
||||||
def format_list(data, separator=', '):
|
|
||||||
"""Return a formatted strings
|
|
||||||
|
|
||||||
:param data: a list of strings
|
|
||||||
:param separator: the separator to use between strings (default: ', ')
|
|
||||||
:rtype: a string formatted based on separator
|
|
||||||
"""
|
|
||||||
|
|
||||||
return separator.join(sorted(data))
|
|
||||||
|
|
||||||
|
|
||||||
def format_list_of_dicts(data):
|
|
||||||
"""Return a formatted string of key value pairs for each dict
|
|
||||||
|
|
||||||
:param data: a list of dicts
|
|
||||||
:rtype: a string formatted to key='value' with dicts separated by new line
|
|
||||||
"""
|
|
||||||
|
|
||||||
return '\n'.join(format_dict(i) for i in data)
|
|
||||||
|
|
||||||
|
|
||||||
def get_field(item, field):
|
|
||||||
try:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
return item[field]
|
|
||||||
else:
|
|
||||||
return getattr(item, field)
|
|
||||||
except Exception:
|
|
||||||
msg = _("Resource doesn't have field %s") % field
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
|
|
||||||
"""Return a tuple containing the item properties.
|
|
||||||
|
|
||||||
:param item: a single item resource (e.g. Server, Project, etc)
|
|
||||||
:param fields: tuple of strings with the desired field names
|
|
||||||
:param mixed_case_fields: tuple of field names to preserve case
|
|
||||||
:param formatters: dictionary mapping field names to callables
|
|
||||||
to format the values
|
|
||||||
"""
|
|
||||||
if mixed_case_fields is None:
|
|
||||||
mixed_case_fields = []
|
|
||||||
if formatters is None:
|
|
||||||
formatters = {}
|
|
||||||
|
|
||||||
row = []
|
|
||||||
|
|
||||||
for field in fields:
|
|
||||||
if field in mixed_case_fields:
|
|
||||||
field_name = field.replace(' ', '_')
|
|
||||||
else:
|
|
||||||
field_name = field.lower().replace(' ', '_')
|
|
||||||
data = getattr(item, field_name, '')
|
|
||||||
if field in formatters:
|
|
||||||
row.append(formatters[field](data))
|
|
||||||
else:
|
|
||||||
row.append(data)
|
|
||||||
return tuple(row)
|
|
||||||
|
|
||||||
|
|
||||||
def get_dict_properties(item, fields, mixed_case_fields=None, formatters=None):
|
|
||||||
"""Return a tuple containing the item properties.
|
|
||||||
|
|
||||||
:param item: a single dict resource
|
|
||||||
:param fields: tuple of strings with the desired field names
|
|
||||||
:param mixed_case_fields: tuple of field names to preserve case
|
|
||||||
:param formatters: dictionary mapping field names to callables
|
|
||||||
to format the values
|
|
||||||
"""
|
|
||||||
if mixed_case_fields is None:
|
|
||||||
mixed_case_fields = []
|
|
||||||
if formatters is None:
|
|
||||||
formatters = {}
|
|
||||||
|
|
||||||
row = []
|
|
||||||
|
|
||||||
for field in fields:
|
|
||||||
if field in mixed_case_fields:
|
|
||||||
field_name = field.replace(' ', '_')
|
|
||||||
else:
|
|
||||||
field_name = field.lower().replace(' ', '_')
|
|
||||||
data = item[field_name] if field_name in item else ''
|
|
||||||
if field in formatters:
|
|
||||||
row.append(formatters[field](data))
|
|
||||||
else:
|
|
||||||
row.append(data)
|
|
||||||
return tuple(row)
|
|
||||||
|
|
||||||
|
|
||||||
def sort_items(items, sort_str):
|
|
||||||
"""Sort items based on sort keys and sort directions given by sort_str.
|
|
||||||
|
|
||||||
:param items: a list or generator object of items
|
|
||||||
:param sort_str: a string defining the sort rules, the format is
|
|
||||||
'<key1>:[direction1],<key2>:[direction2]...', direction can be 'asc'
|
|
||||||
for ascending or 'desc' for descending, if direction is not given,
|
|
||||||
it's ascending by default
|
|
||||||
:return: sorted items
|
|
||||||
"""
|
|
||||||
if not sort_str:
|
|
||||||
return items
|
|
||||||
# items may be a generator object, transform it to a list
|
|
||||||
items = list(items)
|
|
||||||
sort_keys = sort_str.strip().split(',')
|
|
||||||
for sort_key in reversed(sort_keys):
|
|
||||||
reverse = False
|
|
||||||
if ':' in sort_key:
|
|
||||||
sort_key, direction = sort_key.split(':', 1)
|
|
||||||
if not sort_key:
|
|
||||||
msg = _("empty string is not a valid sort key")
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
if direction not in ['asc', 'desc']:
|
|
||||||
if not direction:
|
|
||||||
direction = "empty string"
|
|
||||||
msg = _("%(direction)s is not a valid sort direction for "
|
|
||||||
"sort key %(sort_key)s, use asc or desc instead")
|
|
||||||
raise exceptions.CommandError(
|
|
||||||
msg % {'direction': direction,
|
|
||||||
'sort_key': sort_key}
|
|
||||||
)
|
|
||||||
if direction == 'desc':
|
|
||||||
reverse = True
|
|
||||||
items.sort(key=lambda item: get_field(item, sort_key),
|
|
||||||
reverse=reverse)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def env(*vars, **kwargs):
|
|
||||||
"""Search for the first defined of possibly many env vars
|
|
||||||
|
|
||||||
Returns the first environment variable defined in vars, or
|
|
||||||
returns the default defined in kwargs.
|
|
||||||
"""
|
|
||||||
for v in vars:
|
|
||||||
value = os.environ.get(v, None)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return kwargs.get('default', '')
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_class(api_name, version, version_map):
|
|
||||||
"""Returns the client class for the requested API version
|
|
||||||
|
|
||||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
|
||||||
:param version: the requested API version
|
|
||||||
:param version_map: a dict of client classes keyed by version
|
|
||||||
:rtype: a client class for the requested API version
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
client_path = version_map[str(version)]
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
msg = _("Invalid %(api_name)s client version '%(version)s'. "
|
|
||||||
"must be one of: %(version_map)s")
|
|
||||||
raise exceptions.UnsupportedVersion(
|
|
||||||
msg % {'api_name': api_name,
|
|
||||||
'version': version,
|
|
||||||
'version_map': ', '.join(list(version_map.keys()))}
|
|
||||||
)
|
|
||||||
|
|
||||||
return importutils.import_class(client_path)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_status(status_f,
|
|
||||||
res_id,
|
|
||||||
status_field='status',
|
|
||||||
success_status=['active'],
|
|
||||||
error_status=['error'],
|
|
||||||
sleep_time=5,
|
|
||||||
callback=None):
|
|
||||||
"""Wait for status change on a resource during a long-running operation
|
|
||||||
|
|
||||||
:param status_f: a status function that takes a single id argument
|
|
||||||
:param res_id: the resource id to watch
|
|
||||||
:param status_field: the status attribute in the returned resource object
|
|
||||||
:param success_status: a list of status strings for successful completion
|
|
||||||
:param error_status: a list of status strings for error
|
|
||||||
:param sleep_time: wait this long (seconds)
|
|
||||||
:param callback: called per sleep cycle, useful to display progress
|
|
||||||
:rtype: True on success
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
res = status_f(res_id)
|
|
||||||
status = getattr(res, status_field, '').lower()
|
|
||||||
if status in success_status:
|
|
||||||
retval = True
|
|
||||||
break
|
|
||||||
elif status in error_status:
|
|
||||||
retval = False
|
|
||||||
break
|
|
||||||
if callback:
|
|
||||||
progress = getattr(res, 'progress', None) or 0
|
|
||||||
callback(progress)
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
return retval
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_delete(manager,
|
|
||||||
res_id,
|
|
||||||
status_field='status',
|
|
||||||
error_status=['error'],
|
|
||||||
exception_name=['NotFound'],
|
|
||||||
sleep_time=5,
|
|
||||||
timeout=300,
|
|
||||||
callback=None):
|
|
||||||
"""Wait for resource deletion
|
|
||||||
|
|
||||||
:param manager: the manager from which we can get the resource
|
|
||||||
:param res_id: the resource id to watch
|
|
||||||
:param status_field: the status attribute in the returned resource object,
|
|
||||||
this is used to check for error states while the resource is being
|
|
||||||
deleted
|
|
||||||
:param error_status: a list of status strings for error
|
|
||||||
:param exception_name: a list of exception strings for deleted case
|
|
||||||
:param sleep_time: wait this long between checks (seconds)
|
|
||||||
:param timeout: check until this long (seconds)
|
|
||||||
:param callback: called per sleep cycle, useful to display progress; this
|
|
||||||
function is passed a progress value during each iteration of the wait
|
|
||||||
loop
|
|
||||||
:rtype: True on success, False if the resource has gone to error state or
|
|
||||||
the timeout has been reached
|
|
||||||
"""
|
|
||||||
total_time = 0
|
|
||||||
while total_time < timeout:
|
|
||||||
try:
|
|
||||||
# might not be a bad idea to re-use find_resource here if it was
|
|
||||||
# a bit more friendly in the exceptions it raised so we could just
|
|
||||||
# handle a NotFound exception here without parsing the message
|
|
||||||
res = manager.get(res_id)
|
|
||||||
except Exception as ex:
|
|
||||||
if type(ex).__name__ in exception_name:
|
|
||||||
return True
|
|
||||||
raise
|
|
||||||
|
|
||||||
status = getattr(res, status_field, '').lower()
|
|
||||||
if status in error_status:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
progress = getattr(res, 'progress', None) or 0
|
|
||||||
callback(progress)
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
total_time += sleep_time
|
|
||||||
|
|
||||||
# if we got this far we've timed out
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_effective_log_level():
|
|
||||||
"""Returns the lowest logging level considered by logging handlers
|
|
||||||
|
|
||||||
Retrieve and return the smallest log level set among the root
|
|
||||||
logger's handlers (in case of multiple handlers).
|
|
||||||
"""
|
|
||||||
root_log = logging.getLogger()
|
|
||||||
min_log_lvl = logging.CRITICAL
|
|
||||||
for handler in root_log.handlers:
|
|
||||||
min_log_lvl = min(min_log_lvl, handler.level)
|
|
||||||
return min_log_lvl
|
|
||||||
|
|
||||||
|
|
||||||
def get_password(stdin, prompt=None, confirm=True):
|
|
||||||
message = prompt or "User Password:"
|
|
||||||
if hasattr(stdin, 'isatty') and stdin.isatty():
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
first_pass = getpass.getpass(message)
|
|
||||||
if not confirm:
|
|
||||||
return first_pass
|
|
||||||
second_pass = getpass.getpass("Repeat " + message)
|
|
||||||
if first_pass == second_pass:
|
|
||||||
return first_pass
|
|
||||||
print("The passwords entered were not the same")
|
|
||||||
except EOFError: # Ctl-D
|
|
||||||
raise exceptions.CommandError(_("Error reading password."))
|
|
||||||
raise exceptions.CommandError(_("There was a request to be prompted "
|
|
||||||
"for a password and a terminal was "
|
|
||||||
"not detected."))
|
|
||||||
|
|
||||||
|
|
||||||
def read_blob_file_contents(blob_file):
|
|
||||||
try:
|
|
||||||
with open(blob_file) as file:
|
|
||||||
blob = file.read().strip()
|
|
||||||
return blob
|
|
||||||
except IOError:
|
|
||||||
msg = _("Error occurred trying to read from file %s")
|
|
||||||
raise exceptions.CommandError(msg % blob_file)
|
|
||||||
|
|
||||||
|
|
||||||
def build_kwargs_dict(arg_name, value):
|
|
||||||
"""Return a dictionary containing `arg_name` if `value` is set."""
|
|
||||||
kwargs = {}
|
|
||||||
if value:
|
|
||||||
kwargs[arg_name] = value
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def is_ascii(string):
|
|
||||||
try:
|
|
||||||
string.decode('ascii')
|
|
||||||
return True
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
return False
|
|
||||||
|
@ -9,6 +9,7 @@ cliff!=1.16.0,!=1.17.0,>=1.15.0 # Apache-2.0
|
|||||||
keystoneauth1>=2.1.0 # Apache-2.0
|
keystoneauth1>=2.1.0 # Apache-2.0
|
||||||
openstacksdk>=0.8.6 # Apache-2.0
|
openstacksdk>=0.8.6 # Apache-2.0
|
||||||
os-client-config>=1.13.1 # Apache-2.0
|
os-client-config>=1.13.1 # Apache-2.0
|
||||||
|
osc-lib>=0.1.0 # Apache-2.0
|
||||||
oslo.config>=3.10.0 # Apache-2.0
|
oslo.config>=3.10.0 # Apache-2.0
|
||||||
oslo.i18n>=2.1.0 # Apache-2.0
|
oslo.i18n>=2.1.0 # Apache-2.0
|
||||||
oslo.utils>=3.11.0 # Apache-2.0
|
oslo.utils>=3.11.0 # Apache-2.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user