Add initial commands
Change-Id: I6a488185ea43b9aaa512c1eca0e5b0a89943d5f2
This commit is contained in:
		
							
								
								
									
										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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 bharath
					bharath