Add initial commands
Change-Id: I6a488185ea43b9aaa512c1eca0e5b0a89943d5f2
This commit is contained in:
parent
5cbd08c062
commit
28d62a38d4
304
gyanclient/api_versions.py
Normal file
304
gyanclient/api_versions.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
#
|
||||||
|
# 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 functools
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from oslo_utils import strutils
|
||||||
|
|
||||||
|
from gyanclient import exceptions
|
||||||
|
from gyanclient.i18n import _
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
if not LOG.handlers:
|
||||||
|
LOG.addHandler(logging.StreamHandler())
|
||||||
|
|
||||||
|
|
||||||
|
HEADER_NAME = "OpenStack-API-Version"
|
||||||
|
SERVICE_TYPE = "ml-infra"
|
||||||
|
MIN_API_VERSION = '1.1'
|
||||||
|
MAX_API_VERSION = '1.25'
|
||||||
|
DEFAULT_API_VERSION = MAX_API_VERSION
|
||||||
|
|
||||||
|
_SUBSTITUTIONS = {}
|
||||||
|
|
||||||
|
|
||||||
|
_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'")
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersion(object):
|
||||||
|
"""This class represents an API Version Request.
|
||||||
|
|
||||||
|
This class provides convenience methods for manipulation
|
||||||
|
and comparison of version numbers that we need to do to
|
||||||
|
implement microversions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, version_str=None):
|
||||||
|
"""Create an API version object.
|
||||||
|
|
||||||
|
:param version_str: String representation of APIVersionRequest.
|
||||||
|
Correct format is 'X.Y', where 'X' and 'Y'
|
||||||
|
are int values. None value should be used
|
||||||
|
to create Null APIVersionRequest, which is
|
||||||
|
equal to 0.0
|
||||||
|
"""
|
||||||
|
self.ver_major = 0
|
||||||
|
self.ver_minor = 0
|
||||||
|
|
||||||
|
if version_str is not None:
|
||||||
|
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str)
|
||||||
|
if match:
|
||||||
|
self.ver_major = int(match.group(1))
|
||||||
|
if match.group(2) == "latest":
|
||||||
|
self.ver_minor = float("inf")
|
||||||
|
else:
|
||||||
|
self.ver_minor = int(match.group(2))
|
||||||
|
else:
|
||||||
|
msg = _("Invalid format of client version '%s'. "
|
||||||
|
"Expected format 'X.Y', where X is a major part and Y "
|
||||||
|
"is a minor part of version.") % version_str
|
||||||
|
raise exceptions.UnsupportedVersion(msg)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Debug/Logging representation of object."""
|
||||||
|
if self.is_latest():
|
||||||
|
return "Latest API Version Major: %s" % self.ver_major
|
||||||
|
return ("API Version Major: %s, Minor: %s"
|
||||||
|
% (self.ver_major, self.ver_minor))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.is_null():
|
||||||
|
return "<APIVersion: null>"
|
||||||
|
else:
|
||||||
|
return "<APIVersion: %s>" % self.get_string()
|
||||||
|
|
||||||
|
def is_null(self):
|
||||||
|
return self.ver_major == 0 and self.ver_minor == 0
|
||||||
|
|
||||||
|
def is_latest(self):
|
||||||
|
return self.ver_minor == float("inf")
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if not isinstance(other, APIVersion):
|
||||||
|
raise TypeError(_type_error_msg % {"other": other,
|
||||||
|
"cls": self.__class__})
|
||||||
|
|
||||||
|
return ((self.ver_major, self.ver_minor) <
|
||||||
|
(other.ver_major, other.ver_minor))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, APIVersion):
|
||||||
|
raise TypeError(_type_error_msg % {"other": other,
|
||||||
|
"cls": self.__class__})
|
||||||
|
|
||||||
|
return ((self.ver_major, self.ver_minor) ==
|
||||||
|
(other.ver_major, other.ver_minor))
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
if not isinstance(other, APIVersion):
|
||||||
|
raise TypeError(_type_error_msg % {"other": other,
|
||||||
|
"cls": self.__class__})
|
||||||
|
|
||||||
|
return ((self.ver_major, self.ver_minor) >
|
||||||
|
(other.ver_major, other.ver_minor))
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self < other or self == other
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self > other or self == other
|
||||||
|
|
||||||
|
def matches(self, min_version, max_version):
|
||||||
|
"""Matches the version object.
|
||||||
|
|
||||||
|
Returns whether the version object represents a version
|
||||||
|
greater than or equal to the minimum version and less than
|
||||||
|
or equal to the maximum version.
|
||||||
|
|
||||||
|
:param min_version: Minimum acceptable version.
|
||||||
|
:param max_version: Maximum acceptable version.
|
||||||
|
:returns: boolean
|
||||||
|
|
||||||
|
If min_version is null then there is no minimum limit.
|
||||||
|
If max_version is null then there is no maximum limit.
|
||||||
|
If self is null then raise ValueError
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.is_null():
|
||||||
|
raise ValueError(_("Null APIVersion doesn't support 'matches'."))
|
||||||
|
if max_version.is_null() and min_version.is_null():
|
||||||
|
return True
|
||||||
|
elif max_version.is_null():
|
||||||
|
return min_version <= self
|
||||||
|
elif min_version.is_null():
|
||||||
|
return self <= max_version
|
||||||
|
else:
|
||||||
|
return min_version <= self <= max_version
|
||||||
|
|
||||||
|
def get_string(self):
|
||||||
|
"""Version string representation.
|
||||||
|
|
||||||
|
Converts object to string representation which if used to create
|
||||||
|
an APIVersion object results in the same version.
|
||||||
|
"""
|
||||||
|
if self.is_null():
|
||||||
|
raise ValueError(
|
||||||
|
_("Null APIVersion cannot be converted to string."))
|
||||||
|
elif self.is_latest():
|
||||||
|
return "%s.%s" % (self.ver_major, "latest")
|
||||||
|
return "%s.%s" % (self.ver_major, self.ver_minor)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedMethod(object):
|
||||||
|
|
||||||
|
def __init__(self, name, start_version, end_version, func):
|
||||||
|
"""Versioning information for a single method
|
||||||
|
|
||||||
|
:param name: Name of the method
|
||||||
|
:param start_version: Minimum acceptable version
|
||||||
|
:param end_version: Maximum acceptable_version
|
||||||
|
:param func: Method to call
|
||||||
|
|
||||||
|
Minimum and maximums are inclusive
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.start_version = start_version
|
||||||
|
self.end_version = end_version
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("Version Method %s: min: %s, max: %s"
|
||||||
|
% (self.name, self.start_version, self.end_version))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<VersionedMethod %s>" % self.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_major_versions():
|
||||||
|
matcher = re.compile(r"v[0-9]*$")
|
||||||
|
submodules = pkgutil.iter_modules([os.path.dirname(__file__)])
|
||||||
|
available_versions = [name[1:] for loader, name, ispkg in submodules
|
||||||
|
if matcher.search(name)]
|
||||||
|
|
||||||
|
return available_versions
|
||||||
|
|
||||||
|
|
||||||
|
def check_major_version(api_version):
|
||||||
|
"""Checks major part of ``APIVersion`` obj is supported.
|
||||||
|
|
||||||
|
:raises exceptions.UnsupportedVersion: if major part is not supported
|
||||||
|
"""
|
||||||
|
available_versions = get_available_major_versions()
|
||||||
|
if (not api_version.is_null() and
|
||||||
|
str(api_version.ver_major) not in available_versions):
|
||||||
|
if len(available_versions) == 1:
|
||||||
|
msg = _("Invalid client version '%(version)s'. "
|
||||||
|
"Major part should be '%(major)s'") % {
|
||||||
|
"version": api_version.get_string(),
|
||||||
|
"major": available_versions[0]}
|
||||||
|
else:
|
||||||
|
msg = _("Invalid client version '%(version)s'. "
|
||||||
|
"Major part must be one of: '%(major)s'") % {
|
||||||
|
"version": api_version.get_string(),
|
||||||
|
"major": ", ".join(available_versions)}
|
||||||
|
raise exceptions.UnsupportedVersion(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_version(version_string):
|
||||||
|
"""Returns checked APIVersion object"""
|
||||||
|
version_string = str(version_string)
|
||||||
|
if strutils.is_int_like(version_string):
|
||||||
|
version_string = "%s.0" % version_string
|
||||||
|
|
||||||
|
api_version = APIVersion(version_string)
|
||||||
|
check_major_version(api_version)
|
||||||
|
return api_version
|
||||||
|
|
||||||
|
|
||||||
|
def update_headers(headers, api_version):
|
||||||
|
"""Set microversion headers if api_version is not null"""
|
||||||
|
|
||||||
|
if not api_version.is_null() and api_version.ver_minor != 0:
|
||||||
|
version_string = api_version.get_string()
|
||||||
|
headers[HEADER_NAME] = '%s %s' % (SERVICE_TYPE, version_string)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_substitution(versioned_method):
|
||||||
|
_SUBSTITUTIONS.setdefault(versioned_method.name, [])
|
||||||
|
_SUBSTITUTIONS[versioned_method.name].append(versioned_method)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_function_name(func):
|
||||||
|
filename, _lineno, _name, line = traceback.extract_stack()[-4]
|
||||||
|
module, _file_extension = os.path.splitext(filename)
|
||||||
|
module = module.replace("/", ".")
|
||||||
|
if module.endswith(func.__module__):
|
||||||
|
return "%s.[%s].%s" % (func.__module__, line, func.__name__)
|
||||||
|
else:
|
||||||
|
return "%s.%s" % (func.__module__, func.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_substitutions(func_name, api_version=None):
|
||||||
|
if hasattr(func_name, "__id__"):
|
||||||
|
func_name = func_name.__id__
|
||||||
|
|
||||||
|
substitutions = _SUBSTITUTIONS.get(func_name, [])
|
||||||
|
if api_version and not api_version.is_null():
|
||||||
|
return [m for m in substitutions
|
||||||
|
if api_version.matches(m.start_version, m.end_version)]
|
||||||
|
return sorted(substitutions, key=lambda m: m.start_version)
|
||||||
|
|
||||||
|
|
||||||
|
def wraps(start_version, end_version=None):
|
||||||
|
start_version = APIVersion(start_version)
|
||||||
|
if end_version:
|
||||||
|
end_version = APIVersion(end_version)
|
||||||
|
else:
|
||||||
|
end_version = APIVersion("%s.latest" % start_version.ver_major)
|
||||||
|
|
||||||
|
def decor(func):
|
||||||
|
func.versioned = True
|
||||||
|
name = _get_function_name(func)
|
||||||
|
|
||||||
|
versioned_method = VersionedMethod(name, start_version,
|
||||||
|
end_version, func)
|
||||||
|
_add_substitution(versioned_method)
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def substitution(obj, *args, **kwargs):
|
||||||
|
methods = get_substitutions(name, obj.api_version)
|
||||||
|
|
||||||
|
if not methods:
|
||||||
|
raise exceptions.VersionNotFoundForAPIMethod(
|
||||||
|
obj.api_version.get_string(), name)
|
||||||
|
return methods[-1].func(obj, *args, **kwargs)
|
||||||
|
|
||||||
|
# Let's share "arguments" with original method and substitution to
|
||||||
|
# allow put cliutils.arg and wraps decorators in any order
|
||||||
|
if not hasattr(func, 'arguments'):
|
||||||
|
func.arguments = []
|
||||||
|
substitution.arguments = func.arguments
|
||||||
|
|
||||||
|
substitution.__id__ = name
|
||||||
|
|
||||||
|
return substitution
|
||||||
|
|
||||||
|
return decor
|
93
gyanclient/client.py
Normal file
93
gyanclient/client.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# 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 warnings
|
||||||
|
|
||||||
|
from oslo_utils import importutils
|
||||||
|
|
||||||
|
from gyanclient import api_versions
|
||||||
|
from gyanclient import exceptions
|
||||||
|
from gyanclient.i18n import _
|
||||||
|
|
||||||
|
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client_class_and_version(version):
|
||||||
|
if not isinstance(version, api_versions.APIVersion):
|
||||||
|
version = api_versions.get_api_version(version)
|
||||||
|
else:
|
||||||
|
api_versions.check_major_version(version)
|
||||||
|
if version.is_latest():
|
||||||
|
raise exceptions.UnsupportedVersion(
|
||||||
|
_('The version should be explicit, not latest.'))
|
||||||
|
return version, importutils.import_class(
|
||||||
|
'gyanclient.v%s.client.Client' % version.ver_major)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_arguments(kwargs, release, deprecated_name, right_name=None):
|
||||||
|
"""Process deprecation of arguments.
|
||||||
|
|
||||||
|
Check presence of deprecated argument in kwargs, prints proper warning
|
||||||
|
message, renames key to right one it needed.
|
||||||
|
"""
|
||||||
|
if deprecated_name in kwargs:
|
||||||
|
if right_name:
|
||||||
|
if right_name in kwargs:
|
||||||
|
msg = ('The %(old)s argument is deprecated in %(release)s'
|
||||||
|
'and its use may result in errors in future releases.'
|
||||||
|
'As %(new)s is provided, the %(old)s argument will '
|
||||||
|
'be ignored.') % {'old': deprecated_name,
|
||||||
|
'release': release,
|
||||||
|
'new': right_name}
|
||||||
|
kwargs.pop(deprecated_name)
|
||||||
|
else:
|
||||||
|
msg = ('The %(old)s argument is deprecated in %(release)s '
|
||||||
|
'and its use may result in errors in future releases. '
|
||||||
|
'Use %(new)s instead.') % {'old': deprecated_name,
|
||||||
|
'release': release,
|
||||||
|
'new': right_name}
|
||||||
|
kwargs[right_name] = kwargs.pop(deprecated_name)
|
||||||
|
else:
|
||||||
|
msg = ('The %(old)s argument is deprecated in %(release)s '
|
||||||
|
'and its use may result in errors in future '
|
||||||
|
'releases') % {'old': deprecated_name,
|
||||||
|
'release': release}
|
||||||
|
kwargs.pop(deprecated_name)
|
||||||
|
warnings.warn(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def Client(version='1', username=None, auth_url=None, **kwargs):
|
||||||
|
"""Initialize client objects based on given version"""
|
||||||
|
_check_arguments(kwargs, 'Queens', 'api_key', right_name='password')
|
||||||
|
_check_arguments(kwargs, 'Queens', 'endpoint_type',
|
||||||
|
right_name='interface')
|
||||||
|
_check_arguments(kwargs, 'Queens', 'gyan_url',
|
||||||
|
right_name='endpoint_override')
|
||||||
|
_check_arguments(kwargs, 'Queens', 'tenant_name',
|
||||||
|
right_name='project_name')
|
||||||
|
_check_arguments(kwargs, 'Queens', 'tenant_id', right_name='project_id')
|
||||||
|
|
||||||
|
profile = kwargs.pop('profile', None)
|
||||||
|
if osprofiler_profiler and profile:
|
||||||
|
# Initialize the root of the future trace: the created trace ID
|
||||||
|
# will be used as the very first parent to which all related
|
||||||
|
# traces will be bound to. The given HMAC key must correspond to
|
||||||
|
# the one set in gyan-api gyan.conf, otherwise the latter
|
||||||
|
# will fail to check the request signature and will skip
|
||||||
|
# initialization of osprofiler on the server side.
|
||||||
|
osprofiler_profiler.init(profile)
|
||||||
|
|
||||||
|
api_version, client_class = _get_client_class_and_version(version)
|
||||||
|
return client_class(api_version=api_version,
|
||||||
|
auth_url=auth_url,
|
||||||
|
username=username,
|
||||||
|
**kwargs)
|
0
gyanclient/common/__init__.py
Normal file
0
gyanclient/common/__init__.py
Normal file
0
gyanclient/common/apiclient/__init__.py
Normal file
0
gyanclient/common/apiclient/__init__.py
Normal file
148
gyanclient/common/apiclient/auth.py
Normal file
148
gyanclient/common/apiclient/auth.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# 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 argparse
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
|
||||||
|
from gyanclient.common.apiclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
_discovered_plugins = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_auth_system_opts(parser):
|
||||||
|
"""Load options needed by the available auth-systems into a parser.
|
||||||
|
|
||||||
|
This function will try to populate the parser with options from the
|
||||||
|
available plugins.
|
||||||
|
"""
|
||||||
|
group = parser.add_argument_group("Common auth options")
|
||||||
|
BaseAuthPlugin.add_common_opts(group)
|
||||||
|
for name, auth_plugin in _discovered_plugins.items():
|
||||||
|
group = parser.add_argument_group(
|
||||||
|
"Auth-system '%s' options" % name,
|
||||||
|
conflict_handler="resolve")
|
||||||
|
auth_plugin.add_opts(group)
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin(auth_system):
|
||||||
|
try:
|
||||||
|
plugin_class = _discovered_plugins[auth_system]
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.AuthSystemNotFound(auth_system)
|
||||||
|
return plugin_class(auth_system=auth_system)
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class BaseAuthPlugin(object):
|
||||||
|
"""Base class for authentication plugins.
|
||||||
|
|
||||||
|
An authentication plugin needs to override at least the authenticate
|
||||||
|
method to be a valid plugin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
auth_system = None
|
||||||
|
opt_names = []
|
||||||
|
common_opt_names = [
|
||||||
|
"auth_system",
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"auth_url",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, auth_system=None, **kwargs):
|
||||||
|
self.auth_system = auth_system or self.auth_system
|
||||||
|
self.opts = dict((name, kwargs.get(name))
|
||||||
|
for name in self.opt_names)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parser_add_opt(parser, opt):
|
||||||
|
"""Add an option to parser in two variants.
|
||||||
|
|
||||||
|
:param opt: option name (with underscores)
|
||||||
|
"""
|
||||||
|
dashed_opt = opt.replace("_", "-")
|
||||||
|
env_var = "OS_%s" % opt.upper()
|
||||||
|
arg_default = os.environ.get(env_var, "")
|
||||||
|
arg_help = "Defaults to env[%s]." % env_var
|
||||||
|
parser.add_argument(
|
||||||
|
"--os-%s" % dashed_opt,
|
||||||
|
metavar="<%s>" % dashed_opt,
|
||||||
|
default=arg_default,
|
||||||
|
help=arg_help)
|
||||||
|
parser.add_argument(
|
||||||
|
"--os_%s" % opt,
|
||||||
|
metavar="<%s>" % dashed_opt,
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_opts(cls, parser):
|
||||||
|
"""Populate the parser with the options for this plugin."""
|
||||||
|
for opt in cls.opt_names:
|
||||||
|
# use `BaseAuthPlugin.common_opt_names` since it is never
|
||||||
|
# changed in child classes
|
||||||
|
if opt not in BaseAuthPlugin.common_opt_names:
|
||||||
|
cls._parser_add_opt(parser, opt)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_common_opts(cls, parser):
|
||||||
|
"""Add options that are common for several plugins."""
|
||||||
|
for opt in cls.common_opt_names:
|
||||||
|
cls._parser_add_opt(parser, opt)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_opt(opt_name, args):
|
||||||
|
"""Return option name and value.
|
||||||
|
|
||||||
|
:param opt_name: name of the option, e.g., "username"
|
||||||
|
:param args: parsed arguments
|
||||||
|
"""
|
||||||
|
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
||||||
|
|
||||||
|
def parse_opts(self, args):
|
||||||
|
"""Parse the actual auth-system options if any.
|
||||||
|
|
||||||
|
This method is expected to populate the attribute `self.opts` with a
|
||||||
|
dict containing the options and values needed to make authentication.
|
||||||
|
"""
|
||||||
|
self.opts.update(dict(self.get_opt(opt_name, args)
|
||||||
|
for opt_name in self.opt_names))
|
||||||
|
|
||||||
|
def authenticate(self, http_client):
|
||||||
|
"""Authenticate using plugin defined method.
|
||||||
|
|
||||||
|
The method usually analyses `self.opts` and performs
|
||||||
|
a request to authentication server.
|
||||||
|
|
||||||
|
:param http_client: client object that needs authentication
|
||||||
|
:type http_client: HTTPClient
|
||||||
|
:raises: AuthorizationFailure
|
||||||
|
"""
|
||||||
|
self.sufficient_options()
|
||||||
|
self._do_authenticate(http_client)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _do_authenticate(self, http_client):
|
||||||
|
"""Protected method for authentication."""
|
||||||
|
|
||||||
|
def sufficient_options(self):
|
||||||
|
"""Check if all required options are present.
|
||||||
|
|
||||||
|
:raises: AuthPluginOptionsMissing
|
||||||
|
"""
|
||||||
|
missing = [opt
|
||||||
|
for opt in self.opt_names
|
||||||
|
if not self.opts.get(opt)]
|
||||||
|
if missing:
|
||||||
|
raise exceptions.AuthPluginOptionsMissing(missing)
|
98
gyanclient/common/apiclient/base.py
Normal file
98
gyanclient/common/apiclient/base.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base utilities to build API operation managers and objects on top of.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||||
|
|
||||||
|
This is pretty much just a bag for attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, manager, info, loaded=False):
|
||||||
|
"""Populate and bind to a manager.
|
||||||
|
|
||||||
|
:param manager: BaseManager object
|
||||||
|
:param info: dictionary representing resource attributes
|
||||||
|
:param loaded: prevent lazy-loading if set to True
|
||||||
|
"""
|
||||||
|
self.manager = manager
|
||||||
|
self._info = info
|
||||||
|
self._add_details(info)
|
||||||
|
self._loaded = loaded
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
reprkeys = sorted(k
|
||||||
|
for k in self.__dict__.keys()
|
||||||
|
if k[0] != '_' and k != 'manager')
|
||||||
|
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||||
|
return "<%s %s>" % (self.__class__.__name__, info)
|
||||||
|
|
||||||
|
def _add_details(self, info):
|
||||||
|
for (k, v) in info.items():
|
||||||
|
try:
|
||||||
|
setattr(self, k, v)
|
||||||
|
self._info[k] = v
|
||||||
|
except AttributeError:
|
||||||
|
# In this case we already defined the attribute on the class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getattr__(self, k):
|
||||||
|
if k not in self.__dict__:
|
||||||
|
if not self.is_loaded():
|
||||||
|
self.get()
|
||||||
|
return self.__getattr__(k)
|
||||||
|
|
||||||
|
raise AttributeError(k)
|
||||||
|
else:
|
||||||
|
return self.__dict__[k]
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""Support for lazy loading details.
|
||||||
|
|
||||||
|
Some clients, such as novaclient have the option to lazy load the
|
||||||
|
details, details which can be loaded with this function.
|
||||||
|
"""
|
||||||
|
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||||
|
self.set_loaded(True)
|
||||||
|
if not hasattr(self.manager, 'get'):
|
||||||
|
return
|
||||||
|
|
||||||
|
new = self.manager.get(self.id)
|
||||||
|
if new:
|
||||||
|
self._add_details(new._info)
|
||||||
|
self._add_details(
|
||||||
|
{'x_request_id': self.manager.client.last_request_id})
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Resource):
|
||||||
|
return NotImplemented
|
||||||
|
# two resources of different types are not equal
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return False
|
||||||
|
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||||
|
return self.id == other.id
|
||||||
|
return self._info == other._info
|
||||||
|
|
||||||
|
def is_loaded(self):
|
||||||
|
return self._loaded
|
||||||
|
|
||||||
|
def set_loaded(self, val):
|
||||||
|
self._loaded = val
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return copy.deepcopy(self._info)
|
463
gyanclient/common/apiclient/exceptions.py
Normal file
463
gyanclient/common/apiclient/exceptions.py
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Exception definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from gyanclient.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class VersionNotFoundForAPIMethod(Exception):
|
||||||
|
msg_fmt = "API version '%(vers)s' is not supported on '%(method)s' method."
|
||||||
|
|
||||||
|
def __init__(self, version, method):
|
||||||
|
self.version = version
|
||||||
|
self.method = method
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.msg_fmt % {"vers": self.version, "method": self.method}
|
||||||
|
|
||||||
|
|
||||||
|
class ClientException(Exception):
|
||||||
|
"""The base exception class for all exceptions this library raises."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedVersion(ClientException):
|
||||||
|
"""User is trying to use an unsupported version of the API."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CommandError(ClientException):
|
||||||
|
"""Error in CLI tool."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationFailure(ClientException):
|
||||||
|
"""Cannot authorize API client."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionError(ClientException):
|
||||||
|
"""Cannot connect to API service."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionRefused(ConnectionError):
|
||||||
|
"""Connection refused while trying to connect to API service."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||||
|
"""Auth plugin misses some options."""
|
||||||
|
def __init__(self, opt_names):
|
||||||
|
super(AuthPluginOptionsMissing, self).__init__(
|
||||||
|
_("Authentication failed. Missing options: %s") %
|
||||||
|
", ".join(opt_names))
|
||||||
|
self.opt_names = opt_names
|
||||||
|
|
||||||
|
|
||||||
|
class AuthSystemNotFound(AuthorizationFailure):
|
||||||
|
"""User has specified an AuthSystem that is not installed."""
|
||||||
|
def __init__(self, auth_system):
|
||||||
|
super(AuthSystemNotFound, self).__init__(
|
||||||
|
_("AuthSystemNotFound: %r") % auth_system)
|
||||||
|
self.auth_system = auth_system
|
||||||
|
|
||||||
|
|
||||||
|
class NoUniqueMatch(ClientException):
|
||||||
|
"""Multiple entities found instead of one."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointException(ClientException):
|
||||||
|
"""Something is rotten in Service Catalog."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNotFound(EndpointException):
|
||||||
|
"""Could not find requested endpoint in Service Catalog."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AmbiguousEndpoints(EndpointException):
|
||||||
|
"""Found more than one matching endpoint in Service Catalog."""
|
||||||
|
def __init__(self, endpoints=None):
|
||||||
|
super(AmbiguousEndpoints, self).__init__(
|
||||||
|
_("AmbiguousEndpoints: %r") % endpoints)
|
||||||
|
self.endpoints = endpoints
|
||||||
|
|
||||||
|
|
||||||
|
class HttpError(ClientException):
|
||||||
|
"""The base exception class for all HTTP exceptions."""
|
||||||
|
http_status = 0
|
||||||
|
message = _("HTTP Error")
|
||||||
|
|
||||||
|
def __init__(self, message=None, details=None,
|
||||||
|
response=None, request_id=None,
|
||||||
|
url=None, method=None, http_status=None):
|
||||||
|
self.http_status = http_status or self.http_status
|
||||||
|
self.message = message or self.message
|
||||||
|
self.details = details
|
||||||
|
self.request_id = request_id
|
||||||
|
self.response = response
|
||||||
|
self.url = url
|
||||||
|
self.method = method
|
||||||
|
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||||
|
if request_id:
|
||||||
|
formatted_string += " (Request-ID: %s)" % request_id
|
||||||
|
super(HttpError, self).__init__(formatted_string)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPRedirection(HttpError):
|
||||||
|
"""HTTP Redirection."""
|
||||||
|
message = _("HTTP Redirection")
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClientError(HttpError):
|
||||||
|
"""Client-side HTTP error.
|
||||||
|
|
||||||
|
Exception for cases in which the client seems to have erred.
|
||||||
|
"""
|
||||||
|
message = _("HTTP Client Error")
|
||||||
|
|
||||||
|
|
||||||
|
class HttpServerError(HttpError):
|
||||||
|
"""Server-side HTTP error.
|
||||||
|
|
||||||
|
Exception for cases in which the server is aware that it has
|
||||||
|
erred or is incapable of performing the request.
|
||||||
|
"""
|
||||||
|
message = _("HTTP Server Error")
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleChoices(HTTPRedirection):
|
||||||
|
"""HTTP 300 - Multiple Choices.
|
||||||
|
|
||||||
|
Indicates multiple options for the resource that the client may follow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_status = 300
|
||||||
|
message = _("Multiple Choices")
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequest(HTTPClientError):
|
||||||
|
"""HTTP 400 - Bad Request.
|
||||||
|
|
||||||
|
The request cannot be fulfilled due to bad syntax.
|
||||||
|
"""
|
||||||
|
http_status = 400
|
||||||
|
message = _("Bad Request")
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(HTTPClientError):
|
||||||
|
"""HTTP 401 - Unauthorized.
|
||||||
|
|
||||||
|
Similar to 403 Forbidden, but specifically for use when authentication
|
||||||
|
is required and has failed or has not yet been provided.
|
||||||
|
"""
|
||||||
|
http_status = 401
|
||||||
|
message = _("Unauthorized")
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentRequired(HTTPClientError):
|
||||||
|
"""HTTP 402 - Payment Required.
|
||||||
|
|
||||||
|
Reserved for future use.
|
||||||
|
"""
|
||||||
|
http_status = 402
|
||||||
|
message = _("Payment Required")
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(HTTPClientError):
|
||||||
|
"""HTTP 403 - Forbidden.
|
||||||
|
|
||||||
|
The request was a valid request, but the server is refusing to respond
|
||||||
|
to it.
|
||||||
|
"""
|
||||||
|
http_status = 403
|
||||||
|
message = _("Forbidden")
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(HTTPClientError):
|
||||||
|
"""HTTP 404 - Not Found.
|
||||||
|
|
||||||
|
The requested resource could not be found but may be available again
|
||||||
|
in the future.
|
||||||
|
"""
|
||||||
|
http_status = 404
|
||||||
|
message = _("Not Found")
|
||||||
|
|
||||||
|
|
||||||
|
class MethodNotAllowed(HTTPClientError):
|
||||||
|
"""HTTP 405 - Method Not Allowed.
|
||||||
|
|
||||||
|
A request was made of a resource using a request method not supported
|
||||||
|
by that resource.
|
||||||
|
"""
|
||||||
|
http_status = 405
|
||||||
|
message = _("Method Not Allowed")
|
||||||
|
|
||||||
|
|
||||||
|
class NotAcceptable(HTTPClientError):
|
||||||
|
"""HTTP 406 - Not Acceptable.
|
||||||
|
|
||||||
|
The requested resource is only capable of generating content not
|
||||||
|
acceptable according to the Accept headers sent in the request.
|
||||||
|
"""
|
||||||
|
http_status = 406
|
||||||
|
message = _("Not Acceptable")
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyAuthenticationRequired(HTTPClientError):
|
||||||
|
"""HTTP 407 - Proxy Authentication Required.
|
||||||
|
|
||||||
|
The client must first authenticate itself with the proxy.
|
||||||
|
"""
|
||||||
|
http_status = 407
|
||||||
|
message = _("Proxy Authentication Required")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTimeout(HTTPClientError):
|
||||||
|
"""HTTP 408 - Request Timeout.
|
||||||
|
|
||||||
|
The server timed out waiting for the request.
|
||||||
|
"""
|
||||||
|
http_status = 408
|
||||||
|
message = _("Request Timeout")
|
||||||
|
|
||||||
|
|
||||||
|
class Conflict(HTTPClientError):
|
||||||
|
"""HTTP 409 - Conflict.
|
||||||
|
|
||||||
|
Indicates that the request could not be processed because of conflict
|
||||||
|
in the request, such as an edit conflict.
|
||||||
|
"""
|
||||||
|
http_status = 409
|
||||||
|
message = _("Conflict")
|
||||||
|
|
||||||
|
|
||||||
|
class Gone(HTTPClientError):
|
||||||
|
"""HTTP 410 - Gone.
|
||||||
|
|
||||||
|
Indicates that the resource requested is no longer available and will
|
||||||
|
not be available again.
|
||||||
|
"""
|
||||||
|
http_status = 410
|
||||||
|
message = _("Gone")
|
||||||
|
|
||||||
|
|
||||||
|
class LengthRequired(HTTPClientError):
|
||||||
|
"""HTTP 411 - Length Required.
|
||||||
|
|
||||||
|
The request did not specify the length of its content, which is
|
||||||
|
required by the requested resource.
|
||||||
|
"""
|
||||||
|
http_status = 411
|
||||||
|
message = _("Length Required")
|
||||||
|
|
||||||
|
|
||||||
|
class PreconditionFailed(HTTPClientError):
|
||||||
|
"""HTTP 412 - Precondition Failed.
|
||||||
|
|
||||||
|
The server does not meet one of the preconditions that the requester
|
||||||
|
put on the request.
|
||||||
|
"""
|
||||||
|
http_status = 412
|
||||||
|
message = _("Precondition Failed")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestEntityTooLarge(HTTPClientError):
|
||||||
|
"""HTTP 413 - Request Entity Too Large.
|
||||||
|
|
||||||
|
The request is larger than the server is willing or able to process.
|
||||||
|
"""
|
||||||
|
http_status = 413
|
||||||
|
message = _("Request Entity Too Large")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.retry_after = int(kwargs.pop('retry_after'))
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
self.retry_after = 0
|
||||||
|
|
||||||
|
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestUriTooLong(HTTPClientError):
|
||||||
|
"""HTTP 414 - Request-URI Too Long.
|
||||||
|
|
||||||
|
The URI provided was too long for the server to process.
|
||||||
|
"""
|
||||||
|
http_status = 414
|
||||||
|
message = _("Request-URI Too Long")
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedMediaType(HTTPClientError):
|
||||||
|
"""HTTP 415 - Unsupported Media Type.
|
||||||
|
|
||||||
|
The request entity has a media type which the server or resource does
|
||||||
|
not support.
|
||||||
|
"""
|
||||||
|
http_status = 415
|
||||||
|
message = _("Unsupported Media Type")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||||
|
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||||
|
|
||||||
|
The client has asked for a portion of the file, but the server cannot
|
||||||
|
supply that portion.
|
||||||
|
"""
|
||||||
|
http_status = 416
|
||||||
|
message = _("Requested Range Not Satisfiable")
|
||||||
|
|
||||||
|
|
||||||
|
class ExpectationFailed(HTTPClientError):
|
||||||
|
"""HTTP 417 - Expectation Failed.
|
||||||
|
|
||||||
|
The server cannot meet the requirements of the Expect request-header field.
|
||||||
|
"""
|
||||||
|
http_status = 417
|
||||||
|
message = _("Expectation Failed")
|
||||||
|
|
||||||
|
|
||||||
|
class UnprocessableEntity(HTTPClientError):
|
||||||
|
"""HTTP 422 - Unprocessable Entity.
|
||||||
|
|
||||||
|
The request was well-formed but was unable to be followed due to semantic
|
||||||
|
errors.
|
||||||
|
"""
|
||||||
|
http_status = 422
|
||||||
|
message = _("Unprocessable Entity")
|
||||||
|
|
||||||
|
|
||||||
|
class InternalServerError(HttpServerError):
|
||||||
|
"""HTTP 500 - Internal Server Error.
|
||||||
|
|
||||||
|
A generic error message, given when no more specific message is suitable.
|
||||||
|
"""
|
||||||
|
http_status = 500
|
||||||
|
message = _("Internal Server Error")
|
||||||
|
|
||||||
|
|
||||||
|
# NotImplemented is a python keyword.
|
||||||
|
class HttpNotImplemented(HttpServerError):
|
||||||
|
"""HTTP 501 - Not Implemented.
|
||||||
|
|
||||||
|
The server either does not recognize the request method, or it lacks
|
||||||
|
the ability to fulfill the request.
|
||||||
|
"""
|
||||||
|
http_status = 501
|
||||||
|
message = _("Not Implemented")
|
||||||
|
|
||||||
|
|
||||||
|
class BadGateway(HttpServerError):
|
||||||
|
"""HTTP 502 - Bad Gateway.
|
||||||
|
|
||||||
|
The server was acting as a gateway or proxy and received an invalid
|
||||||
|
response from the upstream server.
|
||||||
|
"""
|
||||||
|
http_status = 502
|
||||||
|
message = _("Bad Gateway")
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailable(HttpServerError):
|
||||||
|
"""HTTP 503 - Service Unavailable.
|
||||||
|
|
||||||
|
The server is currently unavailable.
|
||||||
|
"""
|
||||||
|
http_status = 503
|
||||||
|
message = _("Service Unavailable")
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayTimeout(HttpServerError):
|
||||||
|
"""HTTP 504 - Gateway Timeout.
|
||||||
|
|
||||||
|
The server was acting as a gateway or proxy and did not receive a timely
|
||||||
|
response from the upstream server.
|
||||||
|
"""
|
||||||
|
http_status = 504
|
||||||
|
message = _("Gateway Timeout")
|
||||||
|
|
||||||
|
|
||||||
|
class HttpVersionNotSupported(HttpServerError):
|
||||||
|
"""HTTP 505 - HttpVersion Not Supported.
|
||||||
|
|
||||||
|
The server does not support the HTTP protocol version used in the request.
|
||||||
|
"""
|
||||||
|
http_status = 505
|
||||||
|
message = _("HTTP Version Not Supported")
|
||||||
|
|
||||||
|
|
||||||
|
# _code_map contains all the classes that have http_status attribute.
|
||||||
|
_code_map = dict(
|
||||||
|
(getattr(obj, 'http_status', None), obj)
|
||||||
|
for name, obj in vars(sys.modules[__name__]).items()
|
||||||
|
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def from_response(response, method, url):
|
||||||
|
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||||
|
|
||||||
|
:param response: instance of `requests.Response` class
|
||||||
|
:param method: HTTP method used for request
|
||||||
|
:param url: URL used for request
|
||||||
|
"""
|
||||||
|
|
||||||
|
req_id = response.headers.get("x-openstack-request-id")
|
||||||
|
if not req_id:
|
||||||
|
req_id = response.headers.get("x-compute-request-id")
|
||||||
|
kwargs = {
|
||||||
|
"http_status": response.status_code,
|
||||||
|
"response": response,
|
||||||
|
"method": method,
|
||||||
|
"url": url,
|
||||||
|
"request_id": req_id,
|
||||||
|
}
|
||||||
|
if "retry-after" in response.headers:
|
||||||
|
kwargs["retry_after"] = response.headers["retry-after"]
|
||||||
|
|
||||||
|
content_type = response.headers.get("Content-Type", "")
|
||||||
|
if content_type.startswith("application/json"):
|
||||||
|
try:
|
||||||
|
body = response.json()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(body, dict):
|
||||||
|
error = body.get(list(body)[0])
|
||||||
|
if isinstance(error, dict):
|
||||||
|
kwargs["message"] = (error.get("message") or
|
||||||
|
error.get("faultstring"))
|
||||||
|
kwargs["details"] = (error.get("details") or
|
||||||
|
six.text_type(body))
|
||||||
|
elif content_type.startswith("text/"):
|
||||||
|
kwargs["details"] = getattr(response, 'text', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls = _code_map[response.status_code]
|
||||||
|
except KeyError:
|
||||||
|
if 500 <= response.status_code < 600:
|
||||||
|
cls = HttpServerError
|
||||||
|
elif 400 <= response.status_code < 500:
|
||||||
|
cls = HTTPClientError
|
||||||
|
else:
|
||||||
|
cls = HttpError
|
||||||
|
return cls(**kwargs)
|
160
gyanclient/common/base.py
Normal file
160
gyanclient/common/base.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base utilities to build API operation managers and objects on top of.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
|
from gyanclient.common.apiclient import base
|
||||||
|
|
||||||
|
|
||||||
|
def getid(obj):
|
||||||
|
"""Wrapper to get object's ID.
|
||||||
|
|
||||||
|
Abstracts the common pattern of allowing both an object or an
|
||||||
|
object's ID (UUID) as a parameter when dealing with relationships.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return obj.id
|
||||||
|
except AttributeError:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(object):
|
||||||
|
"""Provides CRUD operations with a particular API."""
|
||||||
|
resource_class = None
|
||||||
|
|
||||||
|
def __init__(self, api):
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_version(self):
|
||||||
|
return self.api.api_version
|
||||||
|
|
||||||
|
def _create(self, url, body):
|
||||||
|
resp, body = self.api.json_request('POST', url, body=body)
|
||||||
|
if body:
|
||||||
|
return self.resource_class(self, body)
|
||||||
|
|
||||||
|
def _format_body_data(self, body, response_key):
|
||||||
|
if response_key:
|
||||||
|
try:
|
||||||
|
data = body[response_key]
|
||||||
|
except KeyError:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
data = body
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
data = [data]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _list_pagination(self, url, response_key=None, obj_class=None,
|
||||||
|
limit=None):
|
||||||
|
"""Retrieve a list of items.
|
||||||
|
|
||||||
|
The Gyan API is configured to return a maximum number of
|
||||||
|
items per request, (FIXME: see Gyan's api.max_limit option). This
|
||||||
|
iterates over the 'next' link (pagination) in the responses,
|
||||||
|
to get the number of items specified by 'limit'. If 'limit'
|
||||||
|
is None this function will continue pagination until there are
|
||||||
|
no more values to be returned.
|
||||||
|
|
||||||
|
:param url: a partial URL, e.g. '/nodes'
|
||||||
|
:param response_key: the key to be looked up in response
|
||||||
|
dictionary, e.g. 'nodes'
|
||||||
|
:param obj_class: class for constructing the returned objects.
|
||||||
|
:param limit: maximum number of items to return. If None returns
|
||||||
|
everything.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if obj_class is None:
|
||||||
|
obj_class = self.resource_class
|
||||||
|
|
||||||
|
if limit is not None:
|
||||||
|
limit = int(limit)
|
||||||
|
|
||||||
|
object_list = []
|
||||||
|
object_count = 0
|
||||||
|
limit_reached = False
|
||||||
|
while url:
|
||||||
|
resp, body = self.api.json_request('GET', url)
|
||||||
|
data = self._format_body_data(body, response_key)
|
||||||
|
for obj in data:
|
||||||
|
object_list.append(obj_class(self, obj, loaded=True))
|
||||||
|
object_count += 1
|
||||||
|
if limit and object_count >= limit:
|
||||||
|
# break the for loop
|
||||||
|
limit_reached = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# break the while loop and return
|
||||||
|
if limit_reached:
|
||||||
|
break
|
||||||
|
|
||||||
|
url = body.get('next')
|
||||||
|
if url:
|
||||||
|
url_parts = list(urlparse.urlparse(url))
|
||||||
|
url_parts[0] = url_parts[1] = ''
|
||||||
|
url = urlparse.urlunparse(url_parts)
|
||||||
|
|
||||||
|
return object_list
|
||||||
|
|
||||||
|
def _list(self, url, response_key=None, obj_class=None, body=None,
|
||||||
|
qparams=None):
|
||||||
|
if qparams:
|
||||||
|
url = "%s?%s" % (url, urlparse.urlencode(qparams))
|
||||||
|
|
||||||
|
resp, body = self.api.json_request('GET', url)
|
||||||
|
|
||||||
|
if obj_class is None:
|
||||||
|
obj_class = self.resource_class
|
||||||
|
|
||||||
|
data = self._format_body_data(body, response_key)
|
||||||
|
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||||
|
|
||||||
|
def _update(self, url, body, method='PATCH', response_key=None):
|
||||||
|
resp, body = self.api.json_request(method, url, body=body)
|
||||||
|
# PATCH/PUT requests may not return a body
|
||||||
|
if body:
|
||||||
|
return self.resource_class(self, body)
|
||||||
|
|
||||||
|
def _delete(self, url, qparams=None):
|
||||||
|
if qparams:
|
||||||
|
url = "%s?%s" % (url, urlparse.urlencode(qparams))
|
||||||
|
self.api.raw_request('DELETE', url)
|
||||||
|
|
||||||
|
def _search(self, url, qparams=None, response_key=None, obj_class=None,
|
||||||
|
body=None):
|
||||||
|
if qparams:
|
||||||
|
url = "%s?%s" % (url, urlparse.urlencode(qparams))
|
||||||
|
|
||||||
|
resp, body = self.api.json_request('GET', url, body=body)
|
||||||
|
data = self._format_body_data(body, response_key)
|
||||||
|
if obj_class is None:
|
||||||
|
obj_class = self.resource_class
|
||||||
|
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(base.Resource):
|
||||||
|
"""Represents a particular instance of an object (tenant, user, etc).
|
||||||
|
|
||||||
|
This is pretty much just a bag for attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return copy.deepcopy(self._info)
|
319
gyanclient/common/cliutils.py
Normal file
319
gyanclient/common/cliutils.py
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
# 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 collections
|
||||||
|
import getpass
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
import decorator
|
||||||
|
from oslo_utils import encodeutils
|
||||||
|
from oslo_utils import strutils
|
||||||
|
import prettytable
|
||||||
|
import six
|
||||||
|
from six import moves
|
||||||
|
|
||||||
|
from gyanclient.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class MissingArgs(Exception):
|
||||||
|
"""Supplied arguments are not sufficient for calling a function."""
|
||||||
|
def __init__(self, missing):
|
||||||
|
self.missing = missing
|
||||||
|
msg = _("Missing arguments: %s") % ", ".join(missing)
|
||||||
|
super(MissingArgs, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_args(fn, *args, **kwargs):
|
||||||
|
"""Check that the supplied args are sufficient for calling a function.
|
||||||
|
|
||||||
|
>>> validate_args(lambda a: None)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
MissingArgs: Missing argument(s): a
|
||||||
|
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
MissingArgs: Missing argument(s): b, d
|
||||||
|
|
||||||
|
:param fn: the function to check
|
||||||
|
:param arg: the positional arguments supplied
|
||||||
|
:param kwargs: the keyword arguments supplied
|
||||||
|
"""
|
||||||
|
argspec = inspect.getargspec(fn)
|
||||||
|
|
||||||
|
num_defaults = len(argspec.defaults or [])
|
||||||
|
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
||||||
|
|
||||||
|
def isbound(method):
|
||||||
|
return getattr(method, '__self__', None) is not None
|
||||||
|
|
||||||
|
if isbound(fn):
|
||||||
|
required_args.pop(0)
|
||||||
|
|
||||||
|
missing = [arg for arg in required_args if arg not in kwargs]
|
||||||
|
missing = missing[len(args):]
|
||||||
|
if missing:
|
||||||
|
raise MissingArgs(missing)
|
||||||
|
|
||||||
|
|
||||||
|
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 exclusive_arg(group_name, *args, **kwargs):
|
||||||
|
"""Decorator for CLI mutually exclusive args."""
|
||||||
|
def _decorator(func):
|
||||||
|
required = kwargs.pop('required', None)
|
||||||
|
add_exclusive_arg(func, group_name, required, *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 = []
|
||||||
|
|
||||||
|
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 add_exclusive_arg(func, group_name, required, *args, **kwargs):
|
||||||
|
"""Bind CLI mutally exclusive arguments to a shell.py `do_foo` function."""
|
||||||
|
|
||||||
|
if not hasattr(func, 'exclusive_args'):
|
||||||
|
func.exclusive_args = collections.defaultdict(list)
|
||||||
|
# Default required to False
|
||||||
|
func.exclusive_args['__required__'] = collections.defaultdict(bool)
|
||||||
|
|
||||||
|
if (args, kwargs) not in func.exclusive_args[group_name]:
|
||||||
|
# Because of the semantics of decorator composition if we just append
|
||||||
|
# to the options list positional options will appear to be backwards.
|
||||||
|
func.exclusive_args[group_name].insert(0, (args, kwargs))
|
||||||
|
if required is not None:
|
||||||
|
func.exclusive_args['__required__'][group_name] = required
|
||||||
|
|
||||||
|
|
||||||
|
def unauthenticated(func):
|
||||||
|
"""Adds 'unauthenticated' attribute to decorated function.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
>>> @unauthenticated
|
||||||
|
... def mymethod(f):
|
||||||
|
... pass
|
||||||
|
"""
|
||||||
|
func.unauthenticated = True
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def isunauthenticated(func):
|
||||||
|
"""Checks if the function does not require authentication.
|
||||||
|
|
||||||
|
Mark such functions with the `@unauthenticated` decorator.
|
||||||
|
|
||||||
|
:returns: bool
|
||||||
|
"""
|
||||||
|
return getattr(func, 'unauthenticated', False)
|
||||||
|
|
||||||
|
|
||||||
|
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||||
|
mixed_case_fields=None, field_labels=None):
|
||||||
|
"""Print a list or 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)
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
|
||||||
|
else:
|
||||||
|
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
|
||||||
|
|
||||||
|
|
||||||
|
def keys_and_vals_to_strs(dictionary):
|
||||||
|
"""Recursively convert a dictionary's keys and values to strings.
|
||||||
|
|
||||||
|
:param dictionary: dictionary whose keys/vals are to be converted to strs
|
||||||
|
"""
|
||||||
|
def to_str(k_or_v):
|
||||||
|
if isinstance(k_or_v, dict):
|
||||||
|
return keys_and_vals_to_strs(k_or_v)
|
||||||
|
elif isinstance(k_or_v, six.text_type):
|
||||||
|
return str(k_or_v)
|
||||||
|
else:
|
||||||
|
return k_or_v
|
||||||
|
return dict((to_str(k), to_str(v)) for k, v in dictionary.items())
|
||||||
|
|
||||||
|
|
||||||
|
def print_dict(dct, dict_property="Property", wrap=0):
|
||||||
|
"""Print a `dict` as a table of two columns.
|
||||||
|
|
||||||
|
:param dct: `dict` to print
|
||||||
|
:param dict_property: name of the first column
|
||||||
|
:param wrap: wrapping for the second column
|
||||||
|
"""
|
||||||
|
pt = prettytable.PrettyTable([dict_property, 'Value'])
|
||||||
|
pt.align = 'l'
|
||||||
|
for k, v in dct.items():
|
||||||
|
# convert dict to str to check length
|
||||||
|
if isinstance(v, dict):
|
||||||
|
v = six.text_type(keys_and_vals_to_strs(v))
|
||||||
|
if wrap > 0:
|
||||||
|
v = textwrap.fill(six.text_type(v), wrap)
|
||||||
|
elif wrap < 0:
|
||||||
|
raise ValueError(_("Wrap argument should be a positive integer"))
|
||||||
|
# if value has a newline, add in multiple rows
|
||||||
|
# e.g. fault with stacktrace
|
||||||
|
if v and isinstance(v, six.string_types) and r'\n' in v:
|
||||||
|
lines = v.strip().split(r'\n')
|
||||||
|
col1 = k
|
||||||
|
for line in lines:
|
||||||
|
pt.add_row([col1, line])
|
||||||
|
col1 = ''
|
||||||
|
elif isinstance(v, list):
|
||||||
|
val = str([str(i) for i in v])
|
||||||
|
pt.add_row([k, val])
|
||||||
|
else:
|
||||||
|
pt.add_row([k, v])
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
print(encodeutils.safe_encode(pt.get_string()).decode())
|
||||||
|
else:
|
||||||
|
print(encodeutils.safe_encode(pt.get_string()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_password(max_password_prompts=3):
|
||||||
|
"""Read password from TTY."""
|
||||||
|
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
||||||
|
pw = None
|
||||||
|
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
||||||
|
# Check for Ctrl-D
|
||||||
|
try:
|
||||||
|
for __ in moves.range(max_password_prompts):
|
||||||
|
pw1 = getpass.getpass("OS Password: ")
|
||||||
|
if verify:
|
||||||
|
pw2 = getpass.getpass("Please verify: ")
|
||||||
|
else:
|
||||||
|
pw2 = pw1
|
||||||
|
if pw1 == pw2 and pw1:
|
||||||
|
pw = pw1
|
||||||
|
break
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
return pw
|
||||||
|
|
||||||
|
|
||||||
|
def service_type(stype):
|
||||||
|
"""Adds 'service_type' attribute to decorated function.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@service_type('volume')
|
||||||
|
def mymethod(f):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
def inner(f):
|
||||||
|
f.service_type = stype
|
||||||
|
return f
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_type(f):
|
||||||
|
"""Retrieves service type from function."""
|
||||||
|
return getattr(f, 'service_type', None)
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_choice_list(l):
|
||||||
|
return ', '.join("'%s'" % i for i in l)
|
||||||
|
|
||||||
|
|
||||||
|
def exit(msg=''):
|
||||||
|
if msg:
|
||||||
|
print(msg, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(message):
|
||||||
|
@decorator.decorator
|
||||||
|
def wrapper(func, *args, **kwargs):
|
||||||
|
print(message)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
421
gyanclient/common/httpclient.py
Normal file
421
gyanclient/common/httpclient.py
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
# 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 copy
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
from keystoneauth1 import adapter
|
||||||
|
from oslo_utils import importutils
|
||||||
|
import six
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
|
from gyanclient import api_versions
|
||||||
|
from gyanclient import exceptions
|
||||||
|
|
||||||
|
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
USER_AGENT = 'python-gyanclient'
|
||||||
|
CHUNKSIZE = 1024 * 64 # 64kB
|
||||||
|
|
||||||
|
API_VERSION = '/v1'
|
||||||
|
DEFAULT_API_VERSION = '1.latest'
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_error_json(body):
|
||||||
|
"""Return error_message from the HTTP response body."""
|
||||||
|
error_json = {}
|
||||||
|
try:
|
||||||
|
body_json = json.loads(body)
|
||||||
|
if 'error_message' in body_json:
|
||||||
|
raw_msg = body_json['error_message']
|
||||||
|
error_json = json.loads(raw_msg)
|
||||||
|
elif 'error' in body_json:
|
||||||
|
error_body = body_json['error']
|
||||||
|
error_json = {'faultstring': error_body['title'],
|
||||||
|
'debuginfo': error_body['message']}
|
||||||
|
else:
|
||||||
|
error_body = body_json['errors'][0]
|
||||||
|
error_json = {'faultstring': error_body['title']}
|
||||||
|
if 'detail' in error_body:
|
||||||
|
error_json['debuginfo'] = error_body['detail']
|
||||||
|
elif 'description' in error_body:
|
||||||
|
error_json['debuginfo'] = error_body['description']
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return error_json
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClient(object):
|
||||||
|
|
||||||
|
def __init__(self, endpoint, api_version=DEFAULT_API_VERSION, **kwargs):
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.auth_token = kwargs.get('token')
|
||||||
|
self.auth_ref = kwargs.get('auth_ref')
|
||||||
|
self.api_version = api_version or api_versions.APIVersion()
|
||||||
|
self.connection_params = self.get_connection_params(endpoint, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_connection_params(endpoint, **kwargs):
|
||||||
|
parts = urlparse.urlparse(endpoint)
|
||||||
|
|
||||||
|
# trim API version and trailing slash from endpoint
|
||||||
|
path = parts.path
|
||||||
|
path = path.rstrip('/').rstrip(API_VERSION)
|
||||||
|
|
||||||
|
_args = (parts.hostname, parts.port, path)
|
||||||
|
_kwargs = {'timeout': (float(kwargs.get('timeout'))
|
||||||
|
if kwargs.get('timeout') else 600)}
|
||||||
|
|
||||||
|
if parts.scheme == 'https':
|
||||||
|
_class = VerifiedHTTPSConnection
|
||||||
|
_kwargs['ca_file'] = kwargs.get('ca_file', None)
|
||||||
|
_kwargs['cert_file'] = kwargs.get('cert_file', None)
|
||||||
|
_kwargs['key_file'] = kwargs.get('key_file', None)
|
||||||
|
_kwargs['insecure'] = kwargs.get('insecure', False)
|
||||||
|
elif parts.scheme == 'http':
|
||||||
|
_class = six.moves.http_client.HTTPConnection
|
||||||
|
else:
|
||||||
|
msg = 'Unsupported scheme: %s' % parts.scheme
|
||||||
|
raise exceptions.EndpointException(msg)
|
||||||
|
|
||||||
|
return (_class, _args, _kwargs)
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
_class = self.connection_params[0]
|
||||||
|
try:
|
||||||
|
return _class(*self.connection_params[1][0:2],
|
||||||
|
**self.connection_params[2])
|
||||||
|
except six.moves.http_client.InvalidURL:
|
||||||
|
raise exceptions.EndpointException()
|
||||||
|
|
||||||
|
def log_curl_request(self, method, url, kwargs):
|
||||||
|
curl = ['curl -i -X %s' % method]
|
||||||
|
|
||||||
|
for (key, value) in kwargs['headers'].items():
|
||||||
|
header = '-H \'%s: %s\'' % (key, value)
|
||||||
|
curl.append(header)
|
||||||
|
|
||||||
|
conn_params_fmt = [
|
||||||
|
('key_file', '--key %s'),
|
||||||
|
('cert_file', '--cert %s'),
|
||||||
|
('ca_file', '--cacert %s'),
|
||||||
|
]
|
||||||
|
for (key, fmt) in conn_params_fmt:
|
||||||
|
value = self.connection_params[2].get(key)
|
||||||
|
if value:
|
||||||
|
curl.append(fmt % value)
|
||||||
|
|
||||||
|
if self.connection_params[2].get('insecure'):
|
||||||
|
curl.append('-k')
|
||||||
|
|
||||||
|
if 'body' in kwargs:
|
||||||
|
curl.append('-d \'%s\'' % kwargs['body'])
|
||||||
|
|
||||||
|
curl.append('%s/%s' % (self.endpoint, url.lstrip(API_VERSION)))
|
||||||
|
LOG.debug(' '.join(curl))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_http_response(resp, body=None):
|
||||||
|
status = (resp.version / 10.0, resp.status, resp.reason)
|
||||||
|
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||||
|
dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()])
|
||||||
|
dump.append('')
|
||||||
|
if body:
|
||||||
|
dump.extend([body, ''])
|
||||||
|
LOG.debug('\n'.join(dump))
|
||||||
|
|
||||||
|
def _make_connection_url(self, url):
|
||||||
|
(_class, _args, _kwargs) = self.connection_params
|
||||||
|
base_url = _args[2]
|
||||||
|
return '%s/%s' % (base_url, url.lstrip('/'))
|
||||||
|
|
||||||
|
def _http_request(self, url, method, **kwargs):
|
||||||
|
"""Send an http request with the specified characteristics.
|
||||||
|
|
||||||
|
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
|
||||||
|
as setting headers and error handling.
|
||||||
|
"""
|
||||||
|
# Copy the kwargs so we can reuse the original in case of redirects
|
||||||
|
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||||
|
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
|
||||||
|
api_versions.update_headers(kwargs["headers"], self.api_version)
|
||||||
|
|
||||||
|
if self.auth_token:
|
||||||
|
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||||
|
|
||||||
|
self.log_curl_request(method, url, kwargs)
|
||||||
|
conn = self.get_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn_url = self._make_connection_url(url)
|
||||||
|
conn.request(method, conn_url, **kwargs)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
except socket.gaierror as e:
|
||||||
|
message = ("Error finding address for %(url)s: %(e)s"
|
||||||
|
% dict(url=url, e=e))
|
||||||
|
raise exceptions.EndpointNotFound(message)
|
||||||
|
except (socket.error, socket.timeout) as e:
|
||||||
|
endpoint = self.endpoint
|
||||||
|
message = ("Error communicating with %(endpoint)s %(e)s"
|
||||||
|
% dict(endpoint=endpoint, e=e))
|
||||||
|
raise exceptions.ConnectionRefused(message)
|
||||||
|
|
||||||
|
body_iter = ResponseBodyIterator(resp)
|
||||||
|
|
||||||
|
# Read body into string if it isn't obviously image data
|
||||||
|
body_str = None
|
||||||
|
if resp.getheader('content-type', None) != 'application/octet-stream':
|
||||||
|
# decoding byte to string is necessary for Python 3.4 compatibility
|
||||||
|
# this issues has not been found with Python 3.4 unit tests
|
||||||
|
# because the test creates a fake http response of type str
|
||||||
|
# the if statement satisfies test (str) and real (bytes) behavior
|
||||||
|
body_list = [
|
||||||
|
chunk.decode("utf-8") if isinstance(chunk, bytes)
|
||||||
|
else chunk for chunk in body_iter
|
||||||
|
]
|
||||||
|
body_str = ''.join(body_list)
|
||||||
|
self.log_http_response(resp, body_str)
|
||||||
|
body_iter = six.StringIO(body_str)
|
||||||
|
else:
|
||||||
|
self.log_http_response(resp)
|
||||||
|
|
||||||
|
if 400 <= resp.status < 600:
|
||||||
|
LOG.warning("Request returned failure status.")
|
||||||
|
error_json = _extract_error_json(body_str)
|
||||||
|
raise exceptions.from_response(
|
||||||
|
resp, error_json.get('faultstring'),
|
||||||
|
error_json.get('debuginfo'), method, url)
|
||||||
|
elif resp.status in (301, 302, 305):
|
||||||
|
# Redirected. Reissue the request to the new location.
|
||||||
|
return self._http_request(resp['location'], method, **kwargs)
|
||||||
|
elif resp.status == 300:
|
||||||
|
raise exceptions.from_response(resp, method=method, url=url)
|
||||||
|
|
||||||
|
return resp, body_iter
|
||||||
|
|
||||||
|
def json_request(self, method, url, **kwargs):
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||||
|
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||||
|
|
||||||
|
if 'body' in kwargs:
|
||||||
|
kwargs['body'] = json.dumps(kwargs['body'])
|
||||||
|
|
||||||
|
resp, body_iter = self._http_request(url, method, **kwargs)
|
||||||
|
content_type = resp.getheader('content-type', None)
|
||||||
|
|
||||||
|
if resp.status == 204 or resp.status == 205 or content_type is None:
|
||||||
|
return resp, list()
|
||||||
|
|
||||||
|
if 'application/json' in content_type:
|
||||||
|
body = ''.join([chunk for chunk in body_iter])
|
||||||
|
try:
|
||||||
|
body = json.loads(body)
|
||||||
|
except ValueError:
|
||||||
|
LOG.error('Could not decode response body as JSON')
|
||||||
|
else:
|
||||||
|
body = None
|
||||||
|
|
||||||
|
return resp, body
|
||||||
|
|
||||||
|
def raw_request(self, method, url, **kwargs):
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers'].setdefault('Content-Type',
|
||||||
|
'application/octet-stream')
|
||||||
|
return self._http_request(url, method, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
|
||||||
|
"""httplib-compatibile connection using client-side SSL authentication
|
||||||
|
|
||||||
|
:see http://code.activestate.com/recipes/
|
||||||
|
577548-https-httplib-client-connection-with-certificate-v/
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, host, port, key_file=None, cert_file=None,
|
||||||
|
ca_file=None, timeout=None, insecure=False):
|
||||||
|
six.moves.http_client.HTTPSConnection.__init__(self, host, port,
|
||||||
|
key_file=key_file,
|
||||||
|
cert_file=cert_file)
|
||||||
|
self.key_file = key_file
|
||||||
|
self.cert_file = cert_file
|
||||||
|
if ca_file is not None:
|
||||||
|
self.ca_file = ca_file
|
||||||
|
else:
|
||||||
|
self.ca_file = self.get_system_ca_file()
|
||||||
|
self.timeout = timeout
|
||||||
|
self.insecure = insecure
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect to a host on a given (SSL) port.
|
||||||
|
|
||||||
|
If ca_file is pointing somewhere, use it to check Server Certificate.
|
||||||
|
|
||||||
|
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
|
||||||
|
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
|
||||||
|
ssl.wrap_socket(), which forces SSL to check server certificate against
|
||||||
|
our client certificate.
|
||||||
|
"""
|
||||||
|
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||||
|
|
||||||
|
if self._tunnel_host:
|
||||||
|
self.sock = sock
|
||||||
|
self._tunnel()
|
||||||
|
|
||||||
|
if self.insecure is True:
|
||||||
|
kwargs = {'cert_reqs': ssl.CERT_NONE}
|
||||||
|
else:
|
||||||
|
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
|
||||||
|
|
||||||
|
if self.cert_file:
|
||||||
|
kwargs['certfile'] = self.cert_file
|
||||||
|
if self.key_file:
|
||||||
|
kwargs['keyfile'] = self.key_file
|
||||||
|
|
||||||
|
self.sock = ssl.wrap_socket(sock, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_system_ca_file():
|
||||||
|
"""Return path to system default CA file."""
|
||||||
|
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
|
||||||
|
# Suse, FreeBSD/OpenBSD
|
||||||
|
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
|
||||||
|
'/etc/pki/tls/certs/ca-bundle.crt',
|
||||||
|
'/etc/ssl/ca-bundle.pem',
|
||||||
|
'/etc/ssl/cert.pem']
|
||||||
|
for ca in ca_path:
|
||||||
|
if os.path.exists(ca):
|
||||||
|
return ca
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class SessionClient(adapter.LegacyJsonAdapter):
|
||||||
|
"""HTTP client based on Keystone client session."""
|
||||||
|
|
||||||
|
def __init__(self, user_agent=USER_AGENT, logger=LOG,
|
||||||
|
api_version=DEFAULT_API_VERSION, *args, **kwargs):
|
||||||
|
self.user_agent = USER_AGENT
|
||||||
|
self.api_version = api_version or api_versions.APIVersion()
|
||||||
|
super(SessionClient, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _http_request(self, url, method, **kwargs):
|
||||||
|
if url.startswith(API_VERSION):
|
||||||
|
url = url[len(API_VERSION):]
|
||||||
|
|
||||||
|
kwargs.setdefault('user_agent', self.user_agent)
|
||||||
|
kwargs.setdefault('auth', self.auth)
|
||||||
|
kwargs.setdefault('endpoint_override', self.endpoint_override)
|
||||||
|
|
||||||
|
# Copy the kwargs so we can reuse the original in case of redirects
|
||||||
|
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||||
|
kwargs['headers'].setdefault('User-Agent', self.user_agent)
|
||||||
|
api_versions.update_headers(kwargs["headers"], self.api_version)
|
||||||
|
|
||||||
|
if osprofiler_web:
|
||||||
|
kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
|
||||||
|
|
||||||
|
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
|
||||||
|
endpoint_filter.setdefault('interface', self.interface)
|
||||||
|
endpoint_filter.setdefault('service_type', self.service_type)
|
||||||
|
endpoint_filter.setdefault('region_name', self.region_name)
|
||||||
|
resp = self.session.request(url, method,
|
||||||
|
raise_exc=False, **kwargs)
|
||||||
|
|
||||||
|
if 400 <= resp.status_code < 600:
|
||||||
|
error_json = _extract_error_json(resp.content)
|
||||||
|
raise exceptions.from_response(
|
||||||
|
resp, error_json.get('faultstring'),
|
||||||
|
error_json.get('debuginfo'), method, url)
|
||||||
|
elif resp.status_code in (301, 302, 305):
|
||||||
|
# Redirected. Reissue the request to the new location.
|
||||||
|
location = resp.headers.get('location')
|
||||||
|
resp = self._http_request(location, method, **kwargs)
|
||||||
|
elif resp.status_code == 300:
|
||||||
|
raise exceptions.from_response(resp, method=method, url=url)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def json_request(self, method, url, **kwargs):
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||||
|
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||||
|
|
||||||
|
if 'body' in kwargs:
|
||||||
|
kwargs['data'] = json.dumps(kwargs.pop('body'))
|
||||||
|
|
||||||
|
resp = self._http_request(url, method, **kwargs)
|
||||||
|
body = resp.content
|
||||||
|
content_type = resp.headers.get('content-type', None)
|
||||||
|
status = resp.status_code
|
||||||
|
if status == 204 or status == 205 or content_type is None:
|
||||||
|
return resp, list()
|
||||||
|
if 'application/json' in content_type:
|
||||||
|
try:
|
||||||
|
body = resp.json()
|
||||||
|
except ValueError:
|
||||||
|
LOG.error('Could not decode response body as JSON')
|
||||||
|
else:
|
||||||
|
body = None
|
||||||
|
|
||||||
|
return resp, body
|
||||||
|
|
||||||
|
def raw_request(self, method, url, **kwargs):
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers'].setdefault('Content-Type',
|
||||||
|
'application/octet-stream')
|
||||||
|
return self._http_request(url, method, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseBodyIterator(object):
|
||||||
|
"""A class that acts as an iterator over an HTTP response."""
|
||||||
|
|
||||||
|
def __init__(self, resp):
|
||||||
|
self.resp = resp
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
yield self.next()
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
chunk = self.resp.read(CHUNKSIZE)
|
||||||
|
if chunk:
|
||||||
|
return chunk
|
||||||
|
else:
|
||||||
|
raise StopIteration()
|
||||||
|
|
||||||
|
|
||||||
|
def _construct_http_client(*args, **kwargs):
|
||||||
|
session = kwargs.pop('session', None)
|
||||||
|
auth = kwargs.pop('auth', None)
|
||||||
|
|
||||||
|
if session:
|
||||||
|
service_type = kwargs.pop('service_type', 'container')
|
||||||
|
interface = kwargs.pop('endpoint_type', None)
|
||||||
|
region_name = kwargs.pop('region_name', None)
|
||||||
|
return SessionClient(session=session,
|
||||||
|
auth=auth,
|
||||||
|
interface=interface,
|
||||||
|
service_type=service_type,
|
||||||
|
region_name=region_name,
|
||||||
|
service_name=None,
|
||||||
|
user_agent='python-gyanclient')
|
||||||
|
else:
|
||||||
|
return HTTPClient(*args, **kwargs)
|
64
gyanclient/common/template_format.py
Normal file
64
gyanclient/common/template_format.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# 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 json
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
if hasattr(yaml, 'CSafeDumper'):
|
||||||
|
yaml_dumper_base = yaml.CSafeDumper
|
||||||
|
else:
|
||||||
|
yaml_dumper_base = yaml.SafeDumper
|
||||||
|
|
||||||
|
|
||||||
|
# We create custom class to not overriden the default yaml behavior
|
||||||
|
class yaml_loader(yaml.SafeLoader):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class yaml_dumper(yaml_dumper_base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _construct_yaml_str(self, node):
|
||||||
|
# Override the default string handling function
|
||||||
|
# to always return unicode objects
|
||||||
|
return self.construct_scalar(node)
|
||||||
|
yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str)
|
||||||
|
# Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type
|
||||||
|
# datetime.data which causes problems in API layer when being processed by
|
||||||
|
# openstack.common.jsonutils. Therefore, make unicode string out of timestamps
|
||||||
|
# until jsonutils can handle dates.
|
||||||
|
yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp',
|
||||||
|
_construct_yaml_str)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(tmpl_str):
|
||||||
|
"""Takes a string and returns a dict containing the parsed structure.
|
||||||
|
|
||||||
|
This includes determination of whether the string is using the
|
||||||
|
JSON or YAML format.
|
||||||
|
"""
|
||||||
|
# strip any whitespace before the check
|
||||||
|
tmpl_str = tmpl_str.strip()
|
||||||
|
if tmpl_str.startswith('{'):
|
||||||
|
tpl = json.loads(tmpl_str)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
tpl = yaml.safe_load(tmpl_str)
|
||||||
|
except yaml.YAMLError as yea:
|
||||||
|
raise ValueError(yea)
|
||||||
|
else:
|
||||||
|
if tpl is None:
|
||||||
|
tpl = {}
|
||||||
|
|
||||||
|
return tpl
|
85
gyanclient/common/template_utils.py
Normal file
85
gyanclient/common/template_utils.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# 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 oslo_serialization import jsonutils
|
||||||
|
import six
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
from six.moves.urllib import request
|
||||||
|
|
||||||
|
from gyanclient.common import template_format
|
||||||
|
from gyanclient.common import utils
|
||||||
|
from gyanclient import exceptions
|
||||||
|
from gyanclient.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_contents(template_file=None, template_url=None,
|
||||||
|
files=None):
|
||||||
|
|
||||||
|
# Transform a bare file path to a file:// URL.
|
||||||
|
if template_file: # nosec
|
||||||
|
template_url = utils.normalise_file_path_to_url(template_file)
|
||||||
|
tpl = request.urlopen(template_url).read()
|
||||||
|
else:
|
||||||
|
raise exceptions.CommandErrorException(_('Need to specify exactly '
|
||||||
|
'one of %(arg1)s, %(arg2)s '
|
||||||
|
'or %(arg3)s') %
|
||||||
|
{'arg1': '--template-file',
|
||||||
|
'arg2': '--template-url'})
|
||||||
|
|
||||||
|
if not tpl:
|
||||||
|
raise exceptions.CommandErrorException(_('Could not fetch '
|
||||||
|
'template from %s') %
|
||||||
|
template_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(tpl, six.binary_type):
|
||||||
|
tpl = tpl.decode('utf-8')
|
||||||
|
template = template_format.parse(tpl)
|
||||||
|
except ValueError as e:
|
||||||
|
raise exceptions.CommandErrorException(_('Error parsing template '
|
||||||
|
'%(url)s %(error)s') %
|
||||||
|
{'url': template_url,
|
||||||
|
'error': e})
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def is_template(file_content):
|
||||||
|
try:
|
||||||
|
if isinstance(file_content, six.binary_type):
|
||||||
|
file_content = file_content.decode('utf-8')
|
||||||
|
template_format.parse(file_content)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_contents(from_data, files, base_url=None,
|
||||||
|
ignore_if=None):
|
||||||
|
|
||||||
|
if isinstance(from_data, dict):
|
||||||
|
for key, value in from_data.items():
|
||||||
|
if ignore_if and ignore_if(key, value):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if base_url and not base_url.endswith('/'):
|
||||||
|
base_url = base_url + '/'
|
||||||
|
|
||||||
|
str_url = parse.urljoin(base_url, value)
|
||||||
|
if str_url not in files:
|
||||||
|
file_content = utils.read_url_content(str_url)
|
||||||
|
if is_template(file_content):
|
||||||
|
template = get_template_contents(
|
||||||
|
template_url=str_url, files=files)[1]
|
||||||
|
file_content = jsonutils.dumps(template)
|
||||||
|
files[str_url] = file_content
|
||||||
|
# replace the data value with the normalised absolute URL
|
||||||
|
from_data[key] = str_url
|
173
gyanclient/common/utils.py
Normal file
173
gyanclient/common/utils.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# 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 binascii
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from oslo_utils import netutils
|
||||||
|
import six
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
from six.moves.urllib import request
|
||||||
|
from gyanclient.common.apiclient import exceptions as apiexec
|
||||||
|
from gyanclient.common import cliutils as utils
|
||||||
|
from gyanclient import exceptions as exc
|
||||||
|
from gyanclient.i18n import _
|
||||||
|
|
||||||
|
VALID_UNITS = (
|
||||||
|
K,
|
||||||
|
M,
|
||||||
|
G,
|
||||||
|
) = (
|
||||||
|
1024,
|
||||||
|
1024 * 1024,
|
||||||
|
1024 * 1024 * 1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def common_filters(marker=None, limit=None, sort_key=None,
|
||||||
|
sort_dir=None, all_projects=False):
|
||||||
|
"""Generate common filters for any list request.
|
||||||
|
|
||||||
|
:param all_projects: list containers in all projects or not
|
||||||
|
: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'.
|
||||||
|
:returns: list of string filters.
|
||||||
|
"""
|
||||||
|
filters = []
|
||||||
|
if all_projects is True:
|
||||||
|
filters.append('all_projects=1')
|
||||||
|
if isinstance(limit, int):
|
||||||
|
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)
|
||||||
|
return filters
|
||||||
|
|
||||||
|
|
||||||
|
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_patch(attributes):
|
||||||
|
patch = []
|
||||||
|
for attr in attributes:
|
||||||
|
path, value = split_and_deserialize(attr)
|
||||||
|
patch.append({path: value})
|
||||||
|
return patch
|
||||||
|
|
||||||
|
|
||||||
|
def format_args(args, parse_comma=True):
|
||||||
|
'''Reformat a list of key-value arguments into a dict.
|
||||||
|
|
||||||
|
Convert arguments into format expected by the API.
|
||||||
|
'''
|
||||||
|
if not args:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if parse_comma:
|
||||||
|
# expect multiple invocations of --label (or other arguments) but fall
|
||||||
|
# back to either , or ; delimited if only one --label is specified
|
||||||
|
if len(args) == 1:
|
||||||
|
args = args[0].replace(';', ',').split(',')
|
||||||
|
|
||||||
|
fmt_args = {}
|
||||||
|
for arg in args:
|
||||||
|
try:
|
||||||
|
(k, v) = arg.split(('='), 1)
|
||||||
|
except ValueError:
|
||||||
|
raise exc.CommandError(_('arguments must be a list of KEY=VALUE '
|
||||||
|
'not %s') % arg)
|
||||||
|
if k not in fmt_args:
|
||||||
|
fmt_args[k] = v
|
||||||
|
else:
|
||||||
|
if not isinstance(fmt_args[k], list):
|
||||||
|
fmt_args[k] = [fmt_args[k]]
|
||||||
|
fmt_args[k].append(v)
|
||||||
|
|
||||||
|
return fmt_args
|
||||||
|
|
||||||
|
|
||||||
|
def print_list_field(field):
|
||||||
|
return lambda obj: ', '.join(getattr(obj, field))
|
||||||
|
|
||||||
|
|
||||||
|
def remove_null_parms(**kwargs):
|
||||||
|
new = {}
|
||||||
|
for (key, value) in kwargs.items():
|
||||||
|
if value is not None:
|
||||||
|
new[key] = value
|
||||||
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
def list_nodes(nodes):
|
||||||
|
columns = ('uuid', 'name', 'type', 'status')
|
||||||
|
utils.print_list(nodes, columns,
|
||||||
|
{'versions': print_list_field('versions')},
|
||||||
|
sortby_index=None)
|
||||||
|
|
||||||
|
|
||||||
|
def list_models(models):
|
||||||
|
columns = ('uuid', 'name', 'type', 'status', 'state', 'deployed_url',
|
||||||
|
'deployed_on')
|
||||||
|
utils.print_list(models, columns,
|
||||||
|
{'versions': print_list_field('versions')},
|
||||||
|
sortby_index=None)
|
||||||
|
|
||||||
|
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 base_url_for_url(url):
|
||||||
|
parsed = parse.urlparse(url)
|
||||||
|
parsed_dir = os.path.dirname(parsed.path)
|
||||||
|
return parse.urljoin(url, parsed_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_file_data(data):
|
||||||
|
if six.PY3 and isinstance(data, str):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
return base64.b64encode(data).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def decode_file_data(data):
|
||||||
|
# Py3 raises binascii.Error instead of TypeError as in Py27
|
||||||
|
try:
|
||||||
|
return base64.b64decode(data)
|
||||||
|
except (TypeError, binascii.Error):
|
||||||
|
raise exc.CommandError(_('Invalid Base 64 file data.'))
|
60
gyanclient/exceptions.py
Normal file
60
gyanclient/exceptions.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# 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 gyanclient.common.apiclient import exceptions
|
||||||
|
from gyanclient.common.apiclient.exceptions import * # noqa
|
||||||
|
|
||||||
|
|
||||||
|
InvalidEndpoint = EndpointException
|
||||||
|
CommunicationError = ConnectionRefused
|
||||||
|
HTTPBadRequest = BadRequest
|
||||||
|
HTTPInternalServerError = InternalServerError
|
||||||
|
HTTPNotFound = NotFound
|
||||||
|
HTTPServiceUnavailable = ServiceUnavailable
|
||||||
|
CommandErrorException = CommandError
|
||||||
|
|
||||||
|
|
||||||
|
class AmbiguousAuthSystem(ClientException):
|
||||||
|
"""Could not obtain token and endpoint using provided credentials."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Alias for backwards compatibility
|
||||||
|
AmbigiousAuthSystem = AmbiguousAuthSystem
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAttribute(ClientException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def from_response(response, message=None, traceback=None, method=None,
|
||||||
|
url=None):
|
||||||
|
"""Return an HttpError instance based on response from httplib/requests."""
|
||||||
|
|
||||||
|
error_body = {}
|
||||||
|
if message:
|
||||||
|
error_body['message'] = message
|
||||||
|
if traceback:
|
||||||
|
error_body['details'] = traceback
|
||||||
|
|
||||||
|
if hasattr(response, 'status') and not hasattr(response, 'status_code'):
|
||||||
|
response.status_code = response.status
|
||||||
|
response.headers = {
|
||||||
|
'Content-Type': response.getheader('content-type', "")}
|
||||||
|
|
||||||
|
if hasattr(response, 'status_code'):
|
||||||
|
response.json = lambda: {'error': error_body}
|
||||||
|
|
||||||
|
if (response.headers.get('Content-Type', '').startswith('text/') and
|
||||||
|
not hasattr(response, 'text')):
|
||||||
|
response.text = ''
|
||||||
|
|
||||||
|
return exceptions.from_response(response, method, url)
|
25
gyanclient/i18n.py
Normal file
25
gyanclient/i18n.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""oslo_i18n integration module for gyanclient.
|
||||||
|
|
||||||
|
See https://docs.openstack.org/oslo.i18n/latest/user/usage.html.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import oslo_i18n
|
||||||
|
|
||||||
|
|
||||||
|
_translators = oslo_i18n.TranslatorFactory(domain='gyanclient')
|
||||||
|
|
||||||
|
# The primary translation function using the well-known name "_"
|
||||||
|
_ = _translators.primary
|
701
gyanclient/shell.py
Normal file
701
gyanclient/shell.py
Normal file
@ -0,0 +1,701 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# This code is taken from python-zunclient. Goal is minimal modification.
|
||||||
|
###
|
||||||
|
|
||||||
|
"""
|
||||||
|
Command-line interface to the OpenStack Gyan API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from oslo_utils import encodeutils
|
||||||
|
from oslo_utils import importutils
|
||||||
|
from oslo_utils import strutils
|
||||||
|
import six
|
||||||
|
|
||||||
|
profiler = importutils.try_import("osprofiler.profiler")
|
||||||
|
|
||||||
|
HAS_KEYRING = False
|
||||||
|
all_errors = ValueError
|
||||||
|
try:
|
||||||
|
import keyring
|
||||||
|
HAS_KEYRING = True
|
||||||
|
try:
|
||||||
|
if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring):
|
||||||
|
import gnomekeyring
|
||||||
|
all_errors = (ValueError,
|
||||||
|
gnomekeyring.IOError,
|
||||||
|
gnomekeyring.NoKeyringDaemonError)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from gyanclient import api_versions
|
||||||
|
from gyanclient import client as base_client
|
||||||
|
from gyanclient.common.apiclient import auth
|
||||||
|
from gyanclient.common import cliutils
|
||||||
|
from gyanclient import exceptions as exc
|
||||||
|
from gyanclient.i18n import _
|
||||||
|
from gyanclient.v1 import shell as shell_v1
|
||||||
|
from gyanclient import version
|
||||||
|
|
||||||
|
DEFAULT_API_VERSION = api_versions.DEFAULT_API_VERSION
|
||||||
|
DEFAULT_ENDPOINT_TYPE = 'publicURL'
|
||||||
|
DEFAULT_SERVICE_TYPE = 'ml-infra'
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def positive_non_zero_float(text):
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
value = float(text)
|
||||||
|
except ValueError:
|
||||||
|
msg = "%s must be a float" % text
|
||||||
|
raise argparse.ArgumentTypeError(msg)
|
||||||
|
if value <= 0:
|
||||||
|
msg = "%s must be greater than 0" % text
|
||||||
|
raise argparse.ArgumentTypeError(msg)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class SecretsHelper(object):
|
||||||
|
def __init__(self, args, client):
|
||||||
|
self.args = args
|
||||||
|
self.client = client
|
||||||
|
self.key = None
|
||||||
|
|
||||||
|
def _validate_string(self, text):
|
||||||
|
if text is None or len(text) == 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _make_key(self):
|
||||||
|
if self.key is not None:
|
||||||
|
return self.key
|
||||||
|
keys = [
|
||||||
|
self.client.auth_url,
|
||||||
|
self.client.projectid,
|
||||||
|
self.client.user,
|
||||||
|
self.client.region_name,
|
||||||
|
self.client.endpoint_type,
|
||||||
|
self.client.service_type,
|
||||||
|
self.client.service_name,
|
||||||
|
self.client.volume_service_name,
|
||||||
|
]
|
||||||
|
for (index, key) in enumerate(keys):
|
||||||
|
if key is None:
|
||||||
|
keys[index] = '?'
|
||||||
|
else:
|
||||||
|
keys[index] = str(keys[index])
|
||||||
|
self.key = "/".join(keys)
|
||||||
|
return self.key
|
||||||
|
|
||||||
|
def _prompt_password(self, verify=True):
|
||||||
|
pw = None
|
||||||
|
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
|
||||||
|
# Check for Ctl-D
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
pw1 = getpass.getpass('OS Password: ')
|
||||||
|
if verify:
|
||||||
|
pw2 = getpass.getpass('Please verify: ')
|
||||||
|
else:
|
||||||
|
pw2 = pw1
|
||||||
|
if pw1 == pw2 and self._validate_string(pw1):
|
||||||
|
pw = pw1
|
||||||
|
break
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
return pw
|
||||||
|
|
||||||
|
def save(self, auth_token, management_url, tenant_id):
|
||||||
|
if not HAS_KEYRING or not self.args.os_cache:
|
||||||
|
return
|
||||||
|
if (auth_token == self.auth_token and
|
||||||
|
management_url == self.management_url):
|
||||||
|
# Nothing changed....
|
||||||
|
return
|
||||||
|
if not all([management_url, auth_token, tenant_id]):
|
||||||
|
raise ValueError("Unable to save empty management url/auth token")
|
||||||
|
value = "|".join([str(auth_token),
|
||||||
|
str(management_url),
|
||||||
|
str(tenant_id)])
|
||||||
|
keyring.set_password("gyanclient_auth", self._make_key(), value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self):
|
||||||
|
if self._validate_string(self.args.os_password):
|
||||||
|
return self.args.os_password
|
||||||
|
verify_pass = (
|
||||||
|
strutils.bool_from_string(cliutils.env("OS_VERIFY_PASSWORD"))
|
||||||
|
)
|
||||||
|
return self._prompt_password(verify_pass)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def management_url(self):
|
||||||
|
if not HAS_KEYRING or not self.args.os_cache:
|
||||||
|
return None
|
||||||
|
management_url = None
|
||||||
|
try:
|
||||||
|
block = keyring.get_password('gyanclient_auth',
|
||||||
|
self._make_key())
|
||||||
|
if block:
|
||||||
|
_token, management_url, _tenant_id = block.split('|', 2)
|
||||||
|
except all_errors:
|
||||||
|
pass
|
||||||
|
return management_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token(self):
|
||||||
|
# Now is where it gets complicated since we
|
||||||
|
# want to look into the keyring module, if it
|
||||||
|
# exists and see if anything was provided in that
|
||||||
|
# file that we can use.
|
||||||
|
if not HAS_KEYRING or not self.args.os_cache:
|
||||||
|
return None
|
||||||
|
token = None
|
||||||
|
try:
|
||||||
|
block = keyring.get_password('gyanclient_auth',
|
||||||
|
self._make_key())
|
||||||
|
if block:
|
||||||
|
token, _management_url, _tenant_id = block.split('|', 2)
|
||||||
|
except all_errors:
|
||||||
|
pass
|
||||||
|
return token
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tenant_id(self):
|
||||||
|
if not HAS_KEYRING or not self.args.os_cache:
|
||||||
|
return None
|
||||||
|
tenant_id = None
|
||||||
|
try:
|
||||||
|
block = keyring.get_password('gyanclient_auth',
|
||||||
|
self._make_key())
|
||||||
|
if block:
|
||||||
|
_token, _management_url, tenant_id = block.split('|', 2)
|
||||||
|
except all_errors:
|
||||||
|
pass
|
||||||
|
return tenant_id
|
||||||
|
|
||||||
|
|
||||||
|
class GyanClientArgumentParser(argparse.ArgumentParser):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(GyanClientArgumentParser, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def error(self, message):
|
||||||
|
"""error(message: string)
|
||||||
|
|
||||||
|
Prints a usage message incorporating the message to stderr and
|
||||||
|
exits.
|
||||||
|
"""
|
||||||
|
self.print_usage(sys.stderr)
|
||||||
|
# FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
|
||||||
|
choose_from = ' (choose from'
|
||||||
|
progparts = self.prog.partition(' ')
|
||||||
|
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
|
||||||
|
" for more information.\n" %
|
||||||
|
{'errmsg': message.split(choose_from)[0],
|
||||||
|
'mainp': progparts[0],
|
||||||
|
'subp': progparts[2]})
|
||||||
|
|
||||||
|
|
||||||
|
class OpenStackGyanShell(object):
|
||||||
|
|
||||||
|
def get_base_parser(self):
|
||||||
|
parser = GyanClientArgumentParser(
|
||||||
|
prog='gyan',
|
||||||
|
description=__doc__.strip(),
|
||||||
|
epilog='See "gyan help COMMAND" '
|
||||||
|
'for help on a specific command.',
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=OpenStackHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Global arguments
|
||||||
|
parser.add_argument('-h', '--help',
|
||||||
|
action='store_true',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--version',
|
||||||
|
action='version',
|
||||||
|
version=version.version_info.version_string())
|
||||||
|
|
||||||
|
parser.add_argument('--debug',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help="Print debugging output.")
|
||||||
|
|
||||||
|
parser.add_argument('--os-cache',
|
||||||
|
default=strutils.bool_from_string(
|
||||||
|
cliutils.env('OS_CACHE', default=False)),
|
||||||
|
action='store_true',
|
||||||
|
help="Use the auth token cache. Defaults to False "
|
||||||
|
"if env[OS_CACHE] is not set.")
|
||||||
|
|
||||||
|
parser.add_argument('--os-region-name',
|
||||||
|
metavar='<region-name>',
|
||||||
|
default=os.environ.get('OS_REGION_NAME'),
|
||||||
|
help='Region name. Default=env[OS_REGION_NAME].')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO(mattf) - add get_timings support to Client
|
||||||
|
# parser.add_argument('--timings',
|
||||||
|
# default=False,
|
||||||
|
# action='store_true',
|
||||||
|
# help="Print call timing info")
|
||||||
|
|
||||||
|
# TODO(mattf) - use timeout
|
||||||
|
# parser.add_argument('--timeout',
|
||||||
|
# default=600,
|
||||||
|
# metavar='<seconds>',
|
||||||
|
# type=positive_non_zero_float,
|
||||||
|
# help="Set HTTP call timeout (in seconds)")
|
||||||
|
|
||||||
|
parser.add_argument('--os-project-id',
|
||||||
|
metavar='<auth-project-id>',
|
||||||
|
default=cliutils.env('OS_PROJECT_ID',
|
||||||
|
default=None),
|
||||||
|
help='Defaults to env[OS_PROJECT_ID].')
|
||||||
|
|
||||||
|
parser.add_argument('--os-project-name',
|
||||||
|
metavar='<auth-project-name>',
|
||||||
|
default=cliutils.env('OS_PROJECT_NAME',
|
||||||
|
default=None),
|
||||||
|
help='Defaults to env[OS_PROJECT_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument('--os-user-domain-id',
|
||||||
|
metavar='<auth-user-domain-id>',
|
||||||
|
default=cliutils.env('OS_USER_DOMAIN_ID'),
|
||||||
|
help='Defaults to env[OS_USER_DOMAIN_ID].')
|
||||||
|
|
||||||
|
parser.add_argument('--os-user-domain-name',
|
||||||
|
metavar='<auth-user-domain-name>',
|
||||||
|
default=cliutils.env('OS_USER_DOMAIN_NAME'),
|
||||||
|
help='Defaults to env[OS_USER_DOMAIN_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument('--os-project-domain-id',
|
||||||
|
metavar='<auth-project-domain-id>',
|
||||||
|
default=cliutils.env('OS_PROJECT_DOMAIN_ID'),
|
||||||
|
help='Defaults to env[OS_PROJECT_DOMAIN_ID].')
|
||||||
|
|
||||||
|
parser.add_argument('--os-project-domain-name',
|
||||||
|
metavar='<auth-project-domain-name>',
|
||||||
|
default=cliutils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||||
|
help='Defaults to env[OS_PROJECT_DOMAIN_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument('--service-type',
|
||||||
|
metavar='<service-type>',
|
||||||
|
help='Defaults to container for all '
|
||||||
|
'actions.')
|
||||||
|
parser.add_argument('--service_type',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--endpoint-type',
|
||||||
|
metavar='<endpoint-type>',
|
||||||
|
default=cliutils.env(
|
||||||
|
'OS_ENDPOINT_TYPE',
|
||||||
|
default=DEFAULT_ENDPOINT_TYPE),
|
||||||
|
help='Defaults to env[OS_ENDPOINT_TYPE] or '
|
||||||
|
+ DEFAULT_ENDPOINT_TYPE + '.')
|
||||||
|
|
||||||
|
parser.add_argument('--gyan-api-version',
|
||||||
|
metavar='<gyan-api-ver>',
|
||||||
|
default=cliutils.env(
|
||||||
|
'GYAN_API_VERSION',
|
||||||
|
default=DEFAULT_API_VERSION),
|
||||||
|
help='Accepts X, X.Y (where X is major, Y is minor'
|
||||||
|
' part), defaults to env[GYAN_API_VERSION].')
|
||||||
|
parser.add_argument('--gyan_api_version',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--os-cacert',
|
||||||
|
metavar='<ca-certificate>',
|
||||||
|
default=cliutils.env('OS_CACERT', default=None),
|
||||||
|
help='Specify a CA bundle file to use in '
|
||||||
|
'verifying a TLS (https) server certificate. '
|
||||||
|
'Defaults to env[OS_CACERT].')
|
||||||
|
|
||||||
|
parser.add_argument('--bypass-url',
|
||||||
|
metavar='<bypass-url>',
|
||||||
|
default=cliutils.env('BYPASS_URL', default=None),
|
||||||
|
dest='bypass_url',
|
||||||
|
help="Use this API endpoint instead of the "
|
||||||
|
"Service Catalog.")
|
||||||
|
parser.add_argument('--bypass_url',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--insecure',
|
||||||
|
default=cliutils.env('GYANCLIENT_INSECURE',
|
||||||
|
default=False),
|
||||||
|
action='store_true',
|
||||||
|
help="Do not verify https connections")
|
||||||
|
|
||||||
|
if profiler:
|
||||||
|
parser.add_argument('--profile',
|
||||||
|
metavar='HMAC_KEY',
|
||||||
|
default=cliutils.env('OS_PROFILE',
|
||||||
|
default=None),
|
||||||
|
help='HMAC key to use for encrypting context '
|
||||||
|
'data for performance profiling of '
|
||||||
|
'operation. This key should be the '
|
||||||
|
'value of the HMAC key configured for '
|
||||||
|
'the OSprofiler middleware in gyan; it '
|
||||||
|
'is specified in the Gyan configuration '
|
||||||
|
'file at "/etc/gyan/gyan.conf". Without '
|
||||||
|
'the key, profiling functions will not '
|
||||||
|
'be triggered even if OSprofiler is '
|
||||||
|
'enabled on the server side.')
|
||||||
|
|
||||||
|
# The auth-system-plugins might require some extra options
|
||||||
|
auth.load_auth_system_opts(parser)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def get_subcommand_parser(self, version, do_help=False):
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
|
||||||
|
self.subcommands = {}
|
||||||
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||||
|
|
||||||
|
actions_modules = shell_v1.COMMAND_MODULES
|
||||||
|
|
||||||
|
for action_modules in actions_modules:
|
||||||
|
self._find_actions(subparsers, action_modules, version, do_help)
|
||||||
|
self._find_actions(subparsers, self, version, do_help)
|
||||||
|
|
||||||
|
self._add_bash_completion_subparser(subparsers)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def _add_bash_completion_subparser(self, subparsers):
|
||||||
|
subparser = (
|
||||||
|
subparsers.add_parser('bash_completion',
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=OpenStackHelpFormatter)
|
||||||
|
)
|
||||||
|
self.subcommands['bash_completion'] = subparser
|
||||||
|
subparser.set_defaults(func=self.do_bash_completion)
|
||||||
|
|
||||||
|
def _find_actions(self, subparsers, actions_module, version, do_help):
|
||||||
|
msg = _(" (Supported by API versions '%(start)s' - '%(end)s')")
|
||||||
|
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||||
|
# I prefer to be hyphen-separated instead of underscores.
|
||||||
|
command = attr[3:].replace('_', '-')
|
||||||
|
callback = getattr(actions_module, attr)
|
||||||
|
desc = callback.__doc__ or ''
|
||||||
|
if hasattr(callback, "versioned"):
|
||||||
|
subs = api_versions.get_substitutions(callback)
|
||||||
|
if do_help:
|
||||||
|
desc += msg % {'start': subs[0].start_version.get_string(),
|
||||||
|
'end': subs[-1].end_version.get_string()}
|
||||||
|
else:
|
||||||
|
for versioned_method in subs:
|
||||||
|
if version.matches(versioned_method.start_version,
|
||||||
|
versioned_method.end_version):
|
||||||
|
callback = versioned_method.func
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
action_help = desc.strip()
|
||||||
|
exclusive_args = getattr(callback, 'exclusive_args', {})
|
||||||
|
arguments = getattr(callback, 'arguments', [])
|
||||||
|
|
||||||
|
subparser = (
|
||||||
|
subparsers.add_parser(command,
|
||||||
|
help=action_help,
|
||||||
|
description=desc,
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=OpenStackHelpFormatter)
|
||||||
|
)
|
||||||
|
subparser.add_argument('-h', '--help',
|
||||||
|
action='help',
|
||||||
|
help=argparse.SUPPRESS,)
|
||||||
|
self.subcommands[command] = subparser
|
||||||
|
|
||||||
|
self._add_subparser_args(subparser, arguments, version, do_help,
|
||||||
|
msg)
|
||||||
|
self._add_subparser_exclusive_args(subparser, exclusive_args,
|
||||||
|
version, do_help, msg)
|
||||||
|
subparser.set_defaults(func=callback)
|
||||||
|
|
||||||
|
def _add_subparser_exclusive_args(self, subparser, exclusive_args,
|
||||||
|
version, do_help, msg):
|
||||||
|
for group_name, arguments in exclusive_args.items():
|
||||||
|
if group_name == '__required__':
|
||||||
|
continue
|
||||||
|
required = exclusive_args['__required__'][group_name]
|
||||||
|
exclusive_group = subparser.add_mutually_exclusive_group(
|
||||||
|
required=required)
|
||||||
|
self._add_subparser_args(exclusive_group, arguments,
|
||||||
|
version, do_help, msg)
|
||||||
|
|
||||||
|
def _add_subparser_args(self, subparser, arguments, version, do_help, msg):
|
||||||
|
for (args, kwargs) in arguments:
|
||||||
|
start_version = kwargs.get("start_version", None)
|
||||||
|
if start_version:
|
||||||
|
start_version = api_versions.APIVersion(start_version)
|
||||||
|
end_version = kwargs.get("end_version", None)
|
||||||
|
if end_version:
|
||||||
|
end_version = api_versions.APIVersion(end_version)
|
||||||
|
else:
|
||||||
|
end_version = api_versions.APIVersion(
|
||||||
|
"%s.latest" % start_version.ver_major)
|
||||||
|
if do_help:
|
||||||
|
kwargs["help"] = kwargs.get("help", "") + (msg % {
|
||||||
|
"start": start_version.get_string(),
|
||||||
|
"end": end_version.get_string()})
|
||||||
|
else:
|
||||||
|
if not version.matches(start_version, end_version):
|
||||||
|
continue
|
||||||
|
kw = kwargs.copy()
|
||||||
|
kw.pop("start_version", None)
|
||||||
|
kw.pop("end_version", None)
|
||||||
|
subparser.add_argument(*args, **kwargs)
|
||||||
|
|
||||||
|
def setup_debugging(self, debug):
|
||||||
|
if debug:
|
||||||
|
streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
|
||||||
|
# Set up the root logger to debug so that the submodules can
|
||||||
|
# print debug messages
|
||||||
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
|
format=streamformat)
|
||||||
|
else:
|
||||||
|
streamformat = "%(levelname)s %(message)s"
|
||||||
|
logging.basicConfig(level=logging.CRITICAL,
|
||||||
|
format=streamformat)
|
||||||
|
|
||||||
|
def main(self, argv):
|
||||||
|
|
||||||
|
argv = list(argv)
|
||||||
|
|
||||||
|
# Parse args once to find version and debug settings
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
(options, args) = parser.parse_known_args(argv)
|
||||||
|
self.setup_debugging(options.debug)
|
||||||
|
|
||||||
|
api_version = api_versions.get_api_version(options.gyan_api_version)
|
||||||
|
|
||||||
|
if '--endpoint_type' in argv:
|
||||||
|
spot = argv.index('--endpoint_type')
|
||||||
|
argv[spot] = '--endpoint-type'
|
||||||
|
|
||||||
|
subcommand_parser = self.get_subcommand_parser(
|
||||||
|
api_version, do_help=("help" in args))
|
||||||
|
|
||||||
|
self.parser = subcommand_parser
|
||||||
|
|
||||||
|
if options.help or not argv:
|
||||||
|
subcommand_parser.print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
args = subcommand_parser.parse_args(argv)
|
||||||
|
|
||||||
|
# Short-circuit and deal with help right away.
|
||||||
|
if not hasattr(args, 'func') or args.func == self.do_help:
|
||||||
|
self.do_help(args)
|
||||||
|
return 0
|
||||||
|
elif args.func == self.do_bash_completion:
|
||||||
|
self.do_bash_completion(args)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
(os_username, os_project_name, os_project_id,
|
||||||
|
os_user_domain_id, os_user_domain_name,
|
||||||
|
os_project_domain_id, os_project_domain_name,
|
||||||
|
os_auth_url, os_auth_system, endpoint_type,
|
||||||
|
service_type, bypass_url, insecure, os_cacert) = (
|
||||||
|
(args.os_username, args.os_project_name, args.os_project_id,
|
||||||
|
args.os_user_domain_id, args.os_user_domain_name,
|
||||||
|
args.os_project_domain_id, args.os_project_domain_name,
|
||||||
|
args.os_auth_url, args.os_auth_system, args.endpoint_type,
|
||||||
|
args.service_type, args.bypass_url, args.insecure,
|
||||||
|
args.os_cacert)
|
||||||
|
)
|
||||||
|
|
||||||
|
if os_auth_system and os_auth_system != "keystone":
|
||||||
|
auth_plugin = auth.load_plugin(os_auth_system)
|
||||||
|
else:
|
||||||
|
auth_plugin = None
|
||||||
|
|
||||||
|
# Fetched and set later as needed
|
||||||
|
os_password = None
|
||||||
|
|
||||||
|
if not endpoint_type:
|
||||||
|
endpoint_type = DEFAULT_ENDPOINT_TYPE
|
||||||
|
|
||||||
|
if not service_type:
|
||||||
|
service_type = DEFAULT_SERVICE_TYPE
|
||||||
|
|
||||||
|
# NA - there is only one service this CLI accesses
|
||||||
|
# service_type = utils.get_service_type(args.func) or service_type
|
||||||
|
|
||||||
|
# FIXME(usrleon): Here should be restrict for project id same as
|
||||||
|
# for os_username or os_password but for compatibility it is not.
|
||||||
|
if not cliutils.isunauthenticated(args.func):
|
||||||
|
if auth_plugin:
|
||||||
|
auth_plugin.parse_opts(args)
|
||||||
|
|
||||||
|
if not auth_plugin or not auth_plugin.opts:
|
||||||
|
if not os_username:
|
||||||
|
raise exc.CommandError("You must provide a username "
|
||||||
|
"via either --os-username or "
|
||||||
|
"env[OS_USERNAME]")
|
||||||
|
|
||||||
|
if not os_project_name and not os_project_id:
|
||||||
|
raise exc.CommandError("You must provide a project name "
|
||||||
|
"or project id via --os-project-name, "
|
||||||
|
"--os-project-id, env[OS_PROJECT_NAME] "
|
||||||
|
"or env[OS_PROJECT_ID]")
|
||||||
|
|
||||||
|
if not os_auth_url:
|
||||||
|
if os_auth_system and os_auth_system != 'keystone':
|
||||||
|
os_auth_url = auth_plugin.get_auth_url()
|
||||||
|
|
||||||
|
if not os_auth_url:
|
||||||
|
raise exc.CommandError("You must provide an auth url "
|
||||||
|
"via either --os-auth-url or "
|
||||||
|
"env[OS_AUTH_URL] or specify an "
|
||||||
|
"auth_system which defines a "
|
||||||
|
"default url with --os-auth-system "
|
||||||
|
"or env[OS_AUTH_SYSTEM]")
|
||||||
|
|
||||||
|
# Now check for the password/token of which pieces of the
|
||||||
|
# identifying keyring key can come from the underlying client
|
||||||
|
if not cliutils.isunauthenticated(args.func):
|
||||||
|
# NA - Client can't be used with SecretsHelper
|
||||||
|
if (auth_plugin and auth_plugin.opts and
|
||||||
|
"os_password" not in auth_plugin.opts):
|
||||||
|
use_pw = False
|
||||||
|
else:
|
||||||
|
use_pw = True
|
||||||
|
|
||||||
|
if use_pw:
|
||||||
|
# Auth using token must have failed or not happened
|
||||||
|
# at all, so now switch to password mode and save
|
||||||
|
# the token when its gotten... using our keyring
|
||||||
|
# saver
|
||||||
|
os_password = args.os_password
|
||||||
|
if not os_password:
|
||||||
|
raise exc.CommandError(
|
||||||
|
'Expecting a password provided via either '
|
||||||
|
'--os-password, env[OS_PASSWORD], or '
|
||||||
|
'prompted response')
|
||||||
|
|
||||||
|
client = base_client
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
if profiler:
|
||||||
|
kwargs["profile"] = args.profile
|
||||||
|
|
||||||
|
self.cs = client.Client(version=api_version,
|
||||||
|
username=os_username,
|
||||||
|
password=os_password,
|
||||||
|
project_id=os_project_id,
|
||||||
|
project_name=os_project_name,
|
||||||
|
user_domain_id=os_user_domain_id,
|
||||||
|
user_domain_name=os_user_domain_name,
|
||||||
|
project_domain_id=os_project_domain_id,
|
||||||
|
project_domain_name=os_project_domain_name,
|
||||||
|
auth_url=os_auth_url,
|
||||||
|
service_type=service_type,
|
||||||
|
region_name=args.os_region_name,
|
||||||
|
endpoint_override=bypass_url,
|
||||||
|
interface=endpoint_type,
|
||||||
|
insecure=insecure,
|
||||||
|
cacert=os_cacert,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
args.func(self.cs, args)
|
||||||
|
|
||||||
|
if profiler and args.profile:
|
||||||
|
trace_id = profiler.get().get_base_id()
|
||||||
|
print("To display trace use the command:\n\n"
|
||||||
|
" osprofiler trace show --html %s " % trace_id)
|
||||||
|
|
||||||
|
def _dump_timings(self, timings):
|
||||||
|
class Tyme(object):
|
||||||
|
def __init__(self, url, seconds):
|
||||||
|
self.url = url
|
||||||
|
self.seconds = seconds
|
||||||
|
results = [Tyme(url, end - start) for url, start, end in timings]
|
||||||
|
total = 0.0
|
||||||
|
for tyme in results:
|
||||||
|
total += tyme.seconds
|
||||||
|
results.append(Tyme("Total", total))
|
||||||
|
cliutils.print_list(results, ["url", "seconds"], sortby_index=None)
|
||||||
|
|
||||||
|
def do_bash_completion(self, _args):
|
||||||
|
"""Prints arguments for bash-completion.
|
||||||
|
|
||||||
|
Prints all of the commands and options to stdout so that the
|
||||||
|
gyan.bash_completion script doesn't have to hard code them.
|
||||||
|
"""
|
||||||
|
commands = set()
|
||||||
|
options = set()
|
||||||
|
for sc_str, sc in self.subcommands.items():
|
||||||
|
commands.add(sc_str)
|
||||||
|
for option in sc._optionals._option_string_actions.keys():
|
||||||
|
options.add(option)
|
||||||
|
|
||||||
|
commands.remove('bash-completion')
|
||||||
|
commands.remove('bash_completion')
|
||||||
|
print(' '.join(commands | options))
|
||||||
|
|
||||||
|
@cliutils.arg('command', metavar='<subcommand>', nargs='?',
|
||||||
|
help='Display help for <subcommand>.')
|
||||||
|
def do_help(self, args):
|
||||||
|
"""Display help about this program or one of its subcommands."""
|
||||||
|
command = getattr(args, 'command', '')
|
||||||
|
if command:
|
||||||
|
if args.command in self.subcommands:
|
||||||
|
self.subcommands[args.command].print_help()
|
||||||
|
else:
|
||||||
|
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||||
|
args.command)
|
||||||
|
else:
|
||||||
|
self.parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
# I'm picky about my shell help.
|
||||||
|
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||||
|
def start_section(self, heading):
|
||||||
|
# Title-case the headings
|
||||||
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||||
|
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
return OpenStackGyanShell().main(
|
||||||
|
map(encodeutils.safe_decode, sys.argv[1:]))
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e, exc_info=1)
|
||||||
|
print("ERROR: %s" % encodeutils.safe_encode(six.text_type(e)),
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
0
gyanclient/v1/__init__.py
Normal file
0
gyanclient/v1/__init__.py
Normal file
125
gyanclient/v1/client.py
Normal file
125
gyanclient/v1/client.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# 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 keystoneauth1 import loading
|
||||||
|
from keystoneauth1 import session as ksa_session
|
||||||
|
|
||||||
|
from gyanclient.common import httpclient
|
||||||
|
from gyanclient.v1 import nodes
|
||||||
|
from gyanclient.v1 import models
|
||||||
|
from gyanclient.v1 import versions
|
||||||
|
|
||||||
|
|
||||||
|
class Client(object):
|
||||||
|
"""Top-level object to access the OpenStack Gyan API."""
|
||||||
|
|
||||||
|
def __init__(self, api_version=None, auth_token=None,
|
||||||
|
auth_type='password', auth_url=None, endpoint_override=None,
|
||||||
|
interface='public', insecure=False, password=None,
|
||||||
|
project_domain_id=None, project_domain_name=None,
|
||||||
|
project_id=None, project_name=None, region_name=None,
|
||||||
|
service_name=None, service_type='container', session=None,
|
||||||
|
user_domain_id=None, user_domain_name=None,
|
||||||
|
username=None, cacert=None, **kwargs):
|
||||||
|
"""Initialization of Client object.
|
||||||
|
|
||||||
|
:param api_version: Gyan API version
|
||||||
|
:type api_version: gyanclient.api_version.APIVersion
|
||||||
|
:param str auth_token: Auth token
|
||||||
|
:param str auth_url: Auth URL
|
||||||
|
:param str auth_type: Auth Type
|
||||||
|
:param str endpoint_override: Bypass URL
|
||||||
|
:param str interface: Interface
|
||||||
|
:param str insecure: Allow insecure
|
||||||
|
:param str password: User password
|
||||||
|
:param str project_domain_id: ID of project domain
|
||||||
|
:param str project_domain_name: Nam of project domain
|
||||||
|
:param str project_id: Project/Tenant ID
|
||||||
|
:param str project_name: Project/Tenant Name
|
||||||
|
:param str region_name: Region Name
|
||||||
|
:param str service_name: Service Name
|
||||||
|
:param str service_type: Service Type
|
||||||
|
:param str session: Session
|
||||||
|
:param str user_domain_id: ID of user domain
|
||||||
|
:param str user_id: User ID
|
||||||
|
:param str username: Username
|
||||||
|
:param str cacert: CA certificate
|
||||||
|
"""
|
||||||
|
if endpoint_override and auth_token:
|
||||||
|
auth_type = 'admin_token'
|
||||||
|
session = None
|
||||||
|
loader_kwargs = {
|
||||||
|
'token': auth_token,
|
||||||
|
'endpoint': endpoint_override
|
||||||
|
}
|
||||||
|
elif auth_token and not session:
|
||||||
|
auth_type = 'token'
|
||||||
|
loader_kwargs = {
|
||||||
|
'token': auth_token,
|
||||||
|
'auth_url': auth_url,
|
||||||
|
'project_domain_id': project_domain_id,
|
||||||
|
'project_domain_name': project_domain_name,
|
||||||
|
'project_id': project_id,
|
||||||
|
'project_name': project_name,
|
||||||
|
'user_domain_id': user_domain_id,
|
||||||
|
'user_domain_name': user_domain_name
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
loader_kwargs = {
|
||||||
|
'auth_url': auth_url,
|
||||||
|
'password': password,
|
||||||
|
'project_domain_id': project_domain_id,
|
||||||
|
'project_domain_name': project_domain_name,
|
||||||
|
'project_id': project_id,
|
||||||
|
'project_name': project_name,
|
||||||
|
'user_domain_id': user_domain_id,
|
||||||
|
'user_domain_name': user_domain_name,
|
||||||
|
'username': username,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backwards compatibility for people not passing in Session
|
||||||
|
if session is None:
|
||||||
|
loader = loading.get_plugin_loader(auth_type)
|
||||||
|
# This should be able to handle v2 and v3 Keystone Auth
|
||||||
|
auth_plugin = loader.load_from_options(**loader_kwargs)
|
||||||
|
session = ksa_session.Session(auth=auth_plugin,
|
||||||
|
verify=(cacert or not insecure))
|
||||||
|
client_kwargs = {}
|
||||||
|
if not endpoint_override:
|
||||||
|
try:
|
||||||
|
# Trigger an auth error so that we can throw the exception
|
||||||
|
# we always have
|
||||||
|
session.get_endpoint(
|
||||||
|
service_name=service_name,
|
||||||
|
service_type=service_type,
|
||||||
|
interface=interface,
|
||||||
|
region_name=region_name
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise RuntimeError('Not authorized')
|
||||||
|
else:
|
||||||
|
client_kwargs = {'endpoint_override': endpoint_override}
|
||||||
|
|
||||||
|
self.http_client = httpclient.SessionClient(service_type=service_type,
|
||||||
|
service_name=service_name,
|
||||||
|
interface=interface,
|
||||||
|
region_name=region_name,
|
||||||
|
session=session,
|
||||||
|
api_version=api_version,
|
||||||
|
**client_kwargs)
|
||||||
|
self.containers = nodes.NodeManager(self.http_client)
|
||||||
|
self.images = models.ModelManager(self.http_client)
|
||||||
|
self.versions = versions.VersionManager(self.http_client)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_version(self):
|
||||||
|
return self.http_client.api_version
|
112
gyanclient/v1/models.py
Normal file
112
gyanclient/v1/models.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# 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 six.moves.urllib import parse
|
||||||
|
|
||||||
|
from gyanclient import api_versions
|
||||||
|
from gyanclient.common import base
|
||||||
|
from gyanclient.common import utils
|
||||||
|
from gyanclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
CREATION_ATTRIBUTES = ['name', 'image', 'command', 'cpu', 'memory',
|
||||||
|
'environment', 'workdir', 'labels', 'image_pull_policy',
|
||||||
|
'restart_policy', 'interactive', 'image_driver',
|
||||||
|
'security_groups', 'hints', 'nets', 'auto_remove',
|
||||||
|
'runtime', 'hostname', 'mounts', 'disk',
|
||||||
|
'availability_zone', 'auto_heal', 'privileged',
|
||||||
|
'exposed_ports', 'healthcheck']
|
||||||
|
|
||||||
|
|
||||||
|
class Model(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Model %s>" % self._info
|
||||||
|
|
||||||
|
|
||||||
|
class ModelManager(base.Manager):
|
||||||
|
resource_class = Model
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _path(id=None):
|
||||||
|
|
||||||
|
if id:
|
||||||
|
return '/v1/ml-models/%s' % id
|
||||||
|
else:
|
||||||
|
return '/v1/ml-models'
|
||||||
|
|
||||||
|
def list_models(self, **kwargs):
|
||||||
|
"""Retrieve a list of Models.
|
||||||
|
|
||||||
|
:returns: A list of models.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._list_pagination(self._path(''),
|
||||||
|
"models")
|
||||||
|
|
||||||
|
def get(self, id):
|
||||||
|
try:
|
||||||
|
return self._list(self._path(id))[0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def model_train(self, **kwargs):
|
||||||
|
new = {}
|
||||||
|
new['name'] = kwargs["name"]
|
||||||
|
new['ml_file'] = kwargs["ml_file"]
|
||||||
|
return self._create(self._path(), new)
|
||||||
|
|
||||||
|
def delete_model(self, id):
|
||||||
|
return self._delete(self._path(id))
|
||||||
|
|
||||||
|
def _action(self, id, action, method='POST', **kwargs):
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers'].setdefault('Content-Length', '0')
|
||||||
|
resp, body = self.api.json_request(method,
|
||||||
|
self._path(id) + action,
|
||||||
|
**kwargs)
|
||||||
|
return resp, body
|
||||||
|
|
||||||
|
def deploy_model(self, id):
|
||||||
|
return self._action(id, '/deploy')
|
||||||
|
|
||||||
|
def undeploy_model(self, id):
|
||||||
|
return self._action(id, '/unstop')
|
||||||
|
|
||||||
|
def rebuild(self, id, **kwargs):
|
||||||
|
return self._action(id, '/rebuild',
|
||||||
|
qparams=kwargs)
|
||||||
|
|
||||||
|
def restart(self, id, timeout):
|
||||||
|
return self._action(id, '/reboot',
|
||||||
|
qparams={'timeout': timeout})
|
||||||
|
|
||||||
|
def pause(self, id):
|
||||||
|
return self._action(id, '/pause')
|
||||||
|
|
||||||
|
def unpause(self, id):
|
||||||
|
return self._action(id, '/unpause')
|
||||||
|
|
||||||
|
def logs(self, id, **kwargs):
|
||||||
|
if kwargs['stdout'] is False and kwargs['stderr'] is False:
|
||||||
|
kwargs['stdout'] = True
|
||||||
|
kwargs['stderr'] = True
|
||||||
|
return self._action(id, '/logs', method='GET',
|
||||||
|
qparams=kwargs)[1]
|
||||||
|
|
||||||
|
def execute(self, id, **kwargs):
|
||||||
|
return self._action(id, '/execute',
|
||||||
|
qparams=kwargs)[1]
|
||||||
|
|
||||||
|
def execute_resize(self, id, exec_id, width, height):
|
||||||
|
self._action(id, '/execute_resize',
|
||||||
|
qparams={'exec_id': exec_id, 'w': width, 'h': height})[1]
|
116
gyanclient/v1/models_shell.py
Normal file
116
gyanclient/v1/models_shell.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# 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 argparse
|
||||||
|
from contextlib import closing
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from gyanclient.common import cliutils as utils
|
||||||
|
from gyanclient.common import utils as gyan_utils
|
||||||
|
from gyanclient import exceptions as exc
|
||||||
|
|
||||||
|
|
||||||
|
def _show_model(model):
|
||||||
|
utils.print_dict(model._info)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('model-id',
|
||||||
|
metavar='<model-id>',
|
||||||
|
nargs='+',
|
||||||
|
help='ID of the model to delete.')
|
||||||
|
def do_model_delete(cs, args):
|
||||||
|
"""Delete specified model."""
|
||||||
|
opts = {}
|
||||||
|
opts['id'] = args.model_id
|
||||||
|
opts = gyan_utils.remove_null_parms(**opts)
|
||||||
|
try:
|
||||||
|
cs.models.delete_model(**opts)
|
||||||
|
print("Request to delete model %s has been accepted." %
|
||||||
|
args.model_id)
|
||||||
|
except Exception as e:
|
||||||
|
print("Delete for model %(model)s failed: %(e)s" %
|
||||||
|
{'model': args.model_id, 'e': e})
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('model-id',
|
||||||
|
metavar='<model-id>',
|
||||||
|
help='ID or name of the model to show.')
|
||||||
|
def do_model_show(cs, args):
|
||||||
|
"""Show details of a container."""
|
||||||
|
opts = {}
|
||||||
|
opts['model_id'] = args.model_id
|
||||||
|
opts = gyan_utils.remove_null_parms(**opts)
|
||||||
|
model = cs.models.get(**opts)
|
||||||
|
_show_model(model)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('model-id',
|
||||||
|
metavar='<model-id>',
|
||||||
|
help='ID of the model to be deployed')
|
||||||
|
def do_undeploy_model(cs, args):
|
||||||
|
"""Undeploy the model."""
|
||||||
|
opts = {}
|
||||||
|
opts['model_id'] = args.model_id
|
||||||
|
opts = gyan_utils.remove_null_parms(**opts)
|
||||||
|
try:
|
||||||
|
model = cs.models.undeploy_model(**opts)
|
||||||
|
_show_model(model)
|
||||||
|
except Exception as e:
|
||||||
|
print("Undeployment of the model %(model)s "
|
||||||
|
"failed: %(e)s" % {'model': args.model_id, 'e': e})
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('model-id',
|
||||||
|
metavar='<model-id>',
|
||||||
|
help='ID of the model to be deployed')
|
||||||
|
def do_deploy_model(cs, args):
|
||||||
|
"""Deploy already created model."""
|
||||||
|
opts = {}
|
||||||
|
opts['model_id'] = args.model_id
|
||||||
|
opts = gyan_utils.remove_null_parms(**opts)
|
||||||
|
try:
|
||||||
|
model = cs.models.deploy_model(**opts)
|
||||||
|
_show_model(model)
|
||||||
|
except Exception as e:
|
||||||
|
print("Deployment of the model %(model)s "
|
||||||
|
"failed: %(e)s" % {'model': args.model_id, 'e': e})
|
||||||
|
|
||||||
|
|
||||||
|
def do_model_list(cs, args):
|
||||||
|
"""List models"""
|
||||||
|
models = cs.models.list_models()
|
||||||
|
gyan_utils.list_models(models)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('name',
|
||||||
|
metavar='<name>',
|
||||||
|
help='ID or name of the model to train')
|
||||||
|
@utils.arg('--ml-file',
|
||||||
|
metavar='<ml_file>',
|
||||||
|
help='The ML model file to be trained')
|
||||||
|
def do_train_model(cs, args):
|
||||||
|
"""Remove security group for specified container."""
|
||||||
|
opts = {}
|
||||||
|
opts['name'] = args.name
|
||||||
|
opts = gyan_utils.remove_null_parms(**opts)
|
||||||
|
try:
|
||||||
|
opts['ml_file'] = yaml.load(open(args.ml_file))
|
||||||
|
models = cs.models.model_train(**opts)
|
||||||
|
gyan_utils.list_models(models)
|
||||||
|
except Exception as e:
|
||||||
|
print("Creation of model %(model)s "
|
||||||
|
"failed: %(e)s" % {'model': args.name, 'e': e})
|
51
gyanclient/v1/nodes.py
Normal file
51
gyanclient/v1/nodes.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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 six.moves.urllib import parse
|
||||||
|
|
||||||
|
from gyanclient import api_versions
|
||||||
|
from gyanclient.common import base
|
||||||
|
from gyanclient.common import utils
|
||||||
|
from gyanclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class Node(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Node %s>" % self._info
|
||||||
|
|
||||||
|
|
||||||
|
class NodeManager(base.Manager):
|
||||||
|
resource_class = Node
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _path(id=None):
|
||||||
|
|
||||||
|
if id:
|
||||||
|
return '/v1/ml-nodes/%s' % id
|
||||||
|
else:
|
||||||
|
return '/v1/ml-nodes'
|
||||||
|
|
||||||
|
def list_nodes(self, **kwargs):
|
||||||
|
"""Retrieve a list of Nodes.
|
||||||
|
|
||||||
|
:returns: A list of nodes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._list_pagination(self._path(''),
|
||||||
|
"nodes")
|
||||||
|
|
||||||
|
def get(self, id):
|
||||||
|
try:
|
||||||
|
return self._list(self._path(id))[0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
46
gyanclient/v1/nodes_shell.py
Normal file
46
gyanclient/v1/nodes_shell.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# 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 argparse
|
||||||
|
from contextlib import closing
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from gyanclient.common import cliutils as utils
|
||||||
|
from gyanclient.common import utils as gyan_utils
|
||||||
|
from gyanclient import exceptions as exc
|
||||||
|
|
||||||
|
|
||||||
|
def _show_node(node):
|
||||||
|
utils.print_dict(node._info)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('node-id',
|
||||||
|
metavar='<node-id>',
|
||||||
|
help='ID or name of the node to show.')
|
||||||
|
def do_node_show(cs, args):
|
||||||
|
"""Show details of a container."""
|
||||||
|
opts = {}
|
||||||
|
opts['node_id'] = args.node_id
|
||||||
|
opts = gyan_utils.remove_null_parms(**opts)
|
||||||
|
node = cs.nodes.get(**opts)
|
||||||
|
_show_node(node)
|
||||||
|
|
||||||
|
|
||||||
|
def do_node_list(cs, args):
|
||||||
|
"""List Nodes"""
|
||||||
|
nodes = cs.nodes.list_nodes()
|
||||||
|
gyan_utils.list_nodes(nodes)
|
21
gyanclient/v1/shell.py
Normal file
21
gyanclient/v1/shell.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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 gyanclient.v1 import nodes_shell
|
||||||
|
from gyanclient.v1 import models_shell
|
||||||
|
from gyanclient.v1 import versions_shell
|
||||||
|
|
||||||
|
COMMAND_MODULES = [
|
||||||
|
nodes_shell,
|
||||||
|
models_shell,
|
||||||
|
versions_shell
|
||||||
|
]
|
27
gyanclient/v1/versions.py
Normal file
27
gyanclient/v1/versions.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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 gyanclient.common import base
|
||||||
|
|
||||||
|
|
||||||
|
class Version(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Version>"
|
||||||
|
|
||||||
|
|
||||||
|
class VersionManager(base.Manager):
|
||||||
|
resource_class = Version
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
url = "%s" % self.api.get_endpoint()
|
||||||
|
url = "%s/" % url.rsplit("/", 1)[0]
|
||||||
|
return self._list(url, "versions")
|
28
gyanclient/v1/versions_shell.py
Normal file
28
gyanclient/v1/versions_shell.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# 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 gyanclient import api_versions
|
||||||
|
from gyanclient.common import cliutils as utils
|
||||||
|
|
||||||
|
|
||||||
|
def do_version_list(cs, args):
|
||||||
|
"""List all API versions."""
|
||||||
|
print("Client supported API versions:")
|
||||||
|
print("Minimum version %(v)s" %
|
||||||
|
{'v': api_versions.MIN_API_VERSION})
|
||||||
|
print("Maximum version %(v)s" %
|
||||||
|
{'v': api_versions.MAX_API_VERSION})
|
||||||
|
|
||||||
|
print("\nServer supported API versions:")
|
||||||
|
result = cs.versions.list()
|
||||||
|
columns = ["Id", "Status", "Min Version", "Max Version"]
|
||||||
|
utils.print_list(result, columns)
|
15
gyanclient/version.py
Normal file
15
gyanclient/version.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# 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 pbr import version
|
||||||
|
|
||||||
|
version_info = version.VersionInfo('python-gyanclient')
|
16
setup.cfg
16
setup.cfg
@ -35,3 +35,19 @@ input_file = gyanclient/locale/gyanclient.pot
|
|||||||
keywords = _ gettext ngettext l_ lazy_gettext
|
keywords = _ gettext ngettext l_ lazy_gettext
|
||||||
mapping_file = babel.cfg
|
mapping_file = babel.cfg
|
||||||
output_file = gyanclient/locale/gyanclient.pot
|
output_file = gyanclient/locale/gyanclient.pot
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
console_scripts =
|
||||||
|
gyan = gyanclient.shell:main
|
||||||
|
|
||||||
|
[build_releasenotes]
|
||||||
|
all_files = 1
|
||||||
|
build-dir = releasenotes/build
|
||||||
|
source-dir = releasenotes/source
|
||||||
|
|
||||||
|
[wheel]
|
||||||
|
universal = 1
|
||||||
|
|
||||||
|
[global]
|
||||||
|
setup-hooks =
|
||||||
|
pbr.hooks.setup_hook
|
||||||
|
2
setup.py
2
setup.py
@ -1,5 +1,3 @@
|
|||||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
|
Loading…
x
Reference in New Issue
Block a user