Add forgotten common folder and other files
This commit is contained in:
parent
e68f0c28e8
commit
4110e728e1
0
metadataclient/__init__.py
Normal file
0
metadataclient/__init__.py
Normal file
21
metadataclient/client.py
Normal file
21
metadataclient/client.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# 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 metadataclient.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
def Client(version, *args, **kwargs):
|
||||||
|
module = utils.import_versioned_module(version, 'client')
|
||||||
|
client_class = getattr(module, 'Client')
|
||||||
|
return client_class(*args, **kwargs)
|
0
metadataclient/common/__init__.py
Normal file
0
metadataclient/common/__init__.py
Normal file
167
metadataclient/common/base.py
Normal file
167
metadataclient/common/base.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Copyright 2012 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base utilities to build API operation managers and objects on top of.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
# Python 2.4 compat
|
||||||
|
try:
|
||||||
|
all
|
||||||
|
except NameError:
|
||||||
|
def all(iterable):
|
||||||
|
return True not in (not x for x in iterable)
|
||||||
|
|
||||||
|
|
||||||
|
def getid(obj):
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Managers interact with a particular type of API (servers, flavors, images,
|
||||||
|
etc.) and provide CRUD operations for them.
|
||||||
|
"""
|
||||||
|
resource_class = None
|
||||||
|
|
||||||
|
def __init__(self, api):
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
def _list(self, url, response_key=None, obj_class=None,
|
||||||
|
body=None, headers={}):
|
||||||
|
|
||||||
|
resp, body = self.api.json_request('GET', url, headers=headers)
|
||||||
|
|
||||||
|
if obj_class is None:
|
||||||
|
obj_class = self.resource_class
|
||||||
|
|
||||||
|
if response_key:
|
||||||
|
if not response_key in body:
|
||||||
|
body[response_key] = []
|
||||||
|
data = body[response_key]
|
||||||
|
else:
|
||||||
|
data = body
|
||||||
|
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||||
|
|
||||||
|
def _delete(self, url, headers={}):
|
||||||
|
self.api.raw_request('DELETE', url, headers=headers)
|
||||||
|
|
||||||
|
def _update(self, url, body, response_key=None, headers={}):
|
||||||
|
resp, body = self.api.json_request('PUT', url, body=body,
|
||||||
|
headers=headers)
|
||||||
|
# PUT requests may not return a body
|
||||||
|
if body:
|
||||||
|
if response_key:
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
return self.resource_class(self, body)
|
||||||
|
|
||||||
|
def _create(self, url, body=None, response_key=None,
|
||||||
|
return_raw=False, headers={}):
|
||||||
|
|
||||||
|
if body:
|
||||||
|
resp, body = self.api.json_request('POST', url,
|
||||||
|
body=body, headers=headers)
|
||||||
|
else:
|
||||||
|
resp, body = self.api.json_request('POST', url, headers=headers)
|
||||||
|
if return_raw:
|
||||||
|
if response_key:
|
||||||
|
return body[response_key]
|
||||||
|
return body
|
||||||
|
if response_key:
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
return self.resource_class(self, body)
|
||||||
|
|
||||||
|
def _get(self, url, response_key=None, return_raw=False, headers={}):
|
||||||
|
resp, body = self.api.json_request('GET', url, headers=headers)
|
||||||
|
if return_raw:
|
||||||
|
if response_key:
|
||||||
|
return body[response_key]
|
||||||
|
return body
|
||||||
|
if response_key:
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
return self.resource_class(self, body)
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""
|
||||||
|
A resource represents a particular instance of an object (tenant, user,
|
||||||
|
etc). This is pretty much just a bag for attributes.
|
||||||
|
|
||||||
|
:param manager: Manager object
|
||||||
|
:param info: dictionary representing resource attributes
|
||||||
|
:param loaded: prevent lazy-loading if set to True
|
||||||
|
"""
|
||||||
|
def __init__(self, manager, info, loaded=False):
|
||||||
|
self.manager = manager
|
||||||
|
self._info = info
|
||||||
|
self._add_details(info)
|
||||||
|
self._loaded = loaded
|
||||||
|
|
||||||
|
def _add_details(self, info):
|
||||||
|
for (k, v) in info.iteritems():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
def __getattr__(self, k):
|
||||||
|
if k not in self.__dict__:
|
||||||
|
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||||
|
if not self.is_loaded():
|
||||||
|
self.get()
|
||||||
|
return self.__getattr__(k)
|
||||||
|
|
||||||
|
raise AttributeError(k)
|
||||||
|
else:
|
||||||
|
return self.__dict__[k]
|
||||||
|
|
||||||
|
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 get(self):
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
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)
|
178
metadataclient/common/exceptions.py
Normal file
178
metadataclient/common/exceptions.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Copyright 2012 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class BaseException(Exception):
|
||||||
|
"""An error occurred."""
|
||||||
|
def __init__(self, message=None):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message or self.__class__.__doc__
|
||||||
|
|
||||||
|
|
||||||
|
class CommandError(BaseException):
|
||||||
|
"""Invalid usage of CLI."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidEndpoint(BaseException):
|
||||||
|
"""The provided endpoint is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class CommunicationError(BaseException):
|
||||||
|
"""Unable to communicate with server."""
|
||||||
|
|
||||||
|
|
||||||
|
class ClientException(Exception):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPException(ClientException):
|
||||||
|
"""Base exception for all HTTP-derived exceptions."""
|
||||||
|
code = 'N/A'
|
||||||
|
|
||||||
|
def __init__(self, details=None):
|
||||||
|
self.details = details or self.__class__.__name__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s (HTTP %s)" % (self.details, self.code)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMultipleChoices(HTTPException):
|
||||||
|
code = 300
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
self.details = ("Requested version of OpenStack Images API is not"
|
||||||
|
"available.")
|
||||||
|
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
|
||||||
|
self.details)
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequest(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPBadRequest(BadRequest):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 401
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPUnauthorized(Unauthorized):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPForbidden(Forbidden):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotFound(NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMethodNotAllowed(HTTPException):
|
||||||
|
code = 405
|
||||||
|
|
||||||
|
|
||||||
|
class Conflict(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPConflict(Conflict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OverLimit(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 413
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPOverLimit(OverLimit):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPInternalServerError(HTTPException):
|
||||||
|
code = 500
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotImplemented(HTTPException):
|
||||||
|
code = 501
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPBadGateway(HTTPException):
|
||||||
|
code = 502
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailable(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 503
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPServiceUnavailable(ServiceUnavailable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
#NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
|
||||||
|
# classes
|
||||||
|
_code_map = {}
|
||||||
|
for obj_name in dir(sys.modules[__name__]):
|
||||||
|
if obj_name.startswith('HTTP'):
|
||||||
|
obj = getattr(sys.modules[__name__], obj_name)
|
||||||
|
_code_map[obj.code] = obj
|
||||||
|
|
||||||
|
|
||||||
|
def from_response(response, body=None):
|
||||||
|
"""Return an instance of an HTTPException based on httplib response."""
|
||||||
|
cls = _code_map.get(response.status, HTTPException)
|
||||||
|
if body:
|
||||||
|
details = body.replace('\n\n', '\n')
|
||||||
|
return cls(details=details)
|
||||||
|
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
class NoTokenLookupException(Exception):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNotFound(Exception):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SSLConfigurationError(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SSLCertificateError(BaseException):
|
||||||
|
pass
|
498
metadataclient/common/http.py
Normal file
498
metadataclient/common/http.py
Normal file
@ -0,0 +1,498 @@
|
|||||||
|
# Copyright 2012 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import errno
|
||||||
|
import hashlib
|
||||||
|
import httplib
|
||||||
|
import logging
|
||||||
|
import posixpath
|
||||||
|
import socket
|
||||||
|
import StringIO
|
||||||
|
import struct
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
# Python 2.5 compat fix
|
||||||
|
if not hasattr(urlparse, 'parse_qsl'):
|
||||||
|
import cgi
|
||||||
|
urlparse.parse_qsl = cgi.parse_qsl
|
||||||
|
|
||||||
|
import OpenSSL
|
||||||
|
|
||||||
|
from metadataclient.common import exceptions as exc
|
||||||
|
from metadataclient.common import utils
|
||||||
|
from metadataclient.openstack.common import strutils
|
||||||
|
|
||||||
|
try:
|
||||||
|
from eventlet import patcher
|
||||||
|
# Handle case where we are running in a monkey patched environment
|
||||||
|
if patcher.is_monkey_patched('socket'):
|
||||||
|
from eventlet.green.httplib import HTTPSConnection
|
||||||
|
from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
|
||||||
|
from eventlet.greenio import GreenSocket
|
||||||
|
# TODO(mclaren): A getsockopt workaround: see 'getsockopt' doc string
|
||||||
|
GreenSocket.getsockopt = utils.getsockopt
|
||||||
|
else:
|
||||||
|
raise ImportError
|
||||||
|
except ImportError:
|
||||||
|
from httplib import HTTPSConnection
|
||||||
|
from OpenSSL.SSL import Connection as Connection
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
USER_AGENT = 'python-metadatalient'
|
||||||
|
CHUNKSIZE = 1024 * 64 # 64kB
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClient(object):
|
||||||
|
|
||||||
|
def __init__(self, endpoint, **kwargs):
|
||||||
|
self.endpoint = endpoint
|
||||||
|
endpoint_parts = self.parse_endpoint(self.endpoint)
|
||||||
|
self.endpoint_scheme = endpoint_parts.scheme
|
||||||
|
self.endpoint_hostname = endpoint_parts.hostname
|
||||||
|
self.endpoint_port = endpoint_parts.port
|
||||||
|
self.endpoint_path = endpoint_parts.path
|
||||||
|
|
||||||
|
self.connection_class = self.get_connection_class(self.endpoint_scheme)
|
||||||
|
self.connection_kwargs = self.get_connection_kwargs(
|
||||||
|
self.endpoint_scheme, **kwargs)
|
||||||
|
|
||||||
|
self.identity_headers = kwargs.get('identity_headers')
|
||||||
|
self.auth_token = kwargs.get('token')
|
||||||
|
if self.identity_headers:
|
||||||
|
if self.identity_headers.get('X-Auth-Token'):
|
||||||
|
self.auth_token = self.identity_headers.get('X-Auth-Token')
|
||||||
|
del self.identity_headers['X-Auth-Token']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_endpoint(endpoint):
|
||||||
|
return urlparse.urlparse(endpoint)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_connection_class(scheme):
|
||||||
|
if scheme == 'https':
|
||||||
|
return VerifiedHTTPSConnection
|
||||||
|
else:
|
||||||
|
return httplib.HTTPConnection
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_connection_kwargs(scheme, **kwargs):
|
||||||
|
_kwargs = {'timeout': float(kwargs.get('timeout', 600))}
|
||||||
|
|
||||||
|
if scheme == 'https':
|
||||||
|
_kwargs['cacert'] = kwargs.get('cacert', None)
|
||||||
|
_kwargs['cert_file'] = kwargs.get('cert_file', None)
|
||||||
|
_kwargs['key_file'] = kwargs.get('key_file', None)
|
||||||
|
_kwargs['insecure'] = kwargs.get('insecure', False)
|
||||||
|
_kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
|
||||||
|
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
_class = self.connection_class
|
||||||
|
try:
|
||||||
|
return _class(self.endpoint_hostname, self.endpoint_port,
|
||||||
|
**self.connection_kwargs)
|
||||||
|
except httplib.InvalidURL:
|
||||||
|
raise exc.InvalidEndpoint()
|
||||||
|
|
||||||
|
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'),
|
||||||
|
('cacert', '--cacert %s'),
|
||||||
|
]
|
||||||
|
for (key, fmt) in conn_params_fmt:
|
||||||
|
value = self.connection_kwargs.get(key)
|
||||||
|
if value:
|
||||||
|
curl.append(fmt % value)
|
||||||
|
|
||||||
|
if self.connection_kwargs.get('insecure'):
|
||||||
|
curl.append('-k')
|
||||||
|
|
||||||
|
if kwargs.get('body') is not None:
|
||||||
|
curl.append('-d \'%s\'' % kwargs['body'])
|
||||||
|
|
||||||
|
curl.append('%s%s' % (self.endpoint, url))
|
||||||
|
LOG.debug(strutils.safe_encode(' '.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(strutils.safe_encode('\n'.join(dump)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_headers(headers):
|
||||||
|
"""Encodes headers.
|
||||||
|
|
||||||
|
Note: This should be used right before
|
||||||
|
sending anything out.
|
||||||
|
|
||||||
|
:param headers: Headers to encode
|
||||||
|
:returns: Dictionary with encoded headers'
|
||||||
|
names and values
|
||||||
|
"""
|
||||||
|
to_str = strutils.safe_encode
|
||||||
|
return dict([(to_str(h), to_str(v)) for h, v in headers.iteritems()])
|
||||||
|
|
||||||
|
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)
|
||||||
|
if self.auth_token:
|
||||||
|
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||||
|
|
||||||
|
if self.identity_headers:
|
||||||
|
for k, v in self.identity_headers.iteritems():
|
||||||
|
kwargs['headers'].setdefault(k, v)
|
||||||
|
|
||||||
|
self.log_curl_request(method, url, kwargs)
|
||||||
|
conn = self.get_connection()
|
||||||
|
|
||||||
|
# Note(flaper87): Before letting headers / url fly,
|
||||||
|
# they should be encoded otherwise httplib will
|
||||||
|
# complain. If we decide to rely on python-request
|
||||||
|
# this wont be necessary anymore.
|
||||||
|
kwargs['headers'] = self.encode_headers(kwargs['headers'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.endpoint_path:
|
||||||
|
url = '%s/%s' % (self.endpoint_path, url)
|
||||||
|
conn_url = posixpath.normpath(url)
|
||||||
|
# Note(flaper87): Ditto, headers / url
|
||||||
|
# encoding to make httplib happy.
|
||||||
|
conn_url = strutils.safe_encode(conn_url)
|
||||||
|
if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
|
||||||
|
conn.putrequest(method, conn_url)
|
||||||
|
for header, value in kwargs['headers'].items():
|
||||||
|
conn.putheader(header, value)
|
||||||
|
conn.endheaders()
|
||||||
|
chunk = kwargs['body'].read(CHUNKSIZE)
|
||||||
|
# Chunk it, baby...
|
||||||
|
while chunk:
|
||||||
|
conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
|
||||||
|
chunk = kwargs['body'].read(CHUNKSIZE)
|
||||||
|
conn.send('0\r\n\r\n')
|
||||||
|
else:
|
||||||
|
conn.request(method, conn_url, **kwargs)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
except socket.gaierror as e:
|
||||||
|
message = "Error finding address for %s: %s" % (
|
||||||
|
self.endpoint_hostname, e)
|
||||||
|
raise exc.InvalidEndpoint(message=message)
|
||||||
|
except (socket.error, socket.timeout) as e:
|
||||||
|
endpoint = self.endpoint
|
||||||
|
message = "Error communicating with %(endpoint)s %(e)s" % locals()
|
||||||
|
raise exc.CommunicationError(message=message)
|
||||||
|
|
||||||
|
body_iter = ResponseBodyIterator(resp)
|
||||||
|
|
||||||
|
# Read body into string if it isn't obviously image data
|
||||||
|
if resp.getheader('content-type', None) != 'application/octet-stream':
|
||||||
|
body_str = ''.join([chunk for chunk in body_iter])
|
||||||
|
self.log_http_response(resp, body_str)
|
||||||
|
body_iter = StringIO.StringIO(body_str)
|
||||||
|
else:
|
||||||
|
self.log_http_response(resp)
|
||||||
|
|
||||||
|
if 400 <= resp.status < 600:
|
||||||
|
LOG.error("Request returned failure status.")
|
||||||
|
raise exc.from_response(resp, body_str)
|
||||||
|
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 exc.from_response(resp)
|
||||||
|
|
||||||
|
return resp, body_iter
|
||||||
|
|
||||||
|
def json_request(self, method, url, **kwargs):
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||||
|
|
||||||
|
if 'body' in kwargs:
|
||||||
|
kwargs['body'] = json.dumps(kwargs['body'])
|
||||||
|
|
||||||
|
resp, body_iter = self._http_request(url, method, **kwargs)
|
||||||
|
|
||||||
|
if 'application/json' in resp.getheader('content-type', None):
|
||||||
|
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')
|
||||||
|
if 'body' in kwargs:
|
||||||
|
if (hasattr(kwargs['body'], 'read')
|
||||||
|
and method.lower() in ('post', 'put')):
|
||||||
|
# We use 'Transfer-Encoding: chunked' because
|
||||||
|
# body size may not always be known in advance.
|
||||||
|
kwargs['headers']['Transfer-Encoding'] = 'chunked'
|
||||||
|
return self._http_request(url, method, **kwargs)
|
||||||
|
|
||||||
|
def archive_request(self, method, url, **kwargs):
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs['headers'].setdefault('Content-Type',
|
||||||
|
'application/x-tar')
|
||||||
|
return self._http_request(url, method, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenSSLConnectionDelegator(object):
|
||||||
|
"""
|
||||||
|
An OpenSSL.SSL.Connection delegator.
|
||||||
|
|
||||||
|
Supplies an additional 'makefile' method which httplib requires
|
||||||
|
and is not present in OpenSSL.SSL.Connection.
|
||||||
|
|
||||||
|
Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
|
||||||
|
a delegator must be used.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.connection = Connection(*args, **kwargs)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.connection, name)
|
||||||
|
|
||||||
|
def makefile(self, *args, **kwargs):
|
||||||
|
# Making sure socket is closed when this file is closed
|
||||||
|
# since we now avoid closing socket on connection close
|
||||||
|
# see new close method under VerifiedHTTPSConnection
|
||||||
|
kwargs['close'] = True
|
||||||
|
|
||||||
|
return socket._fileobject(self.connection, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class VerifiedHTTPSConnection(HTTPSConnection):
|
||||||
|
"""
|
||||||
|
Extended HTTPSConnection which uses the OpenSSL library
|
||||||
|
for enhanced SSL support.
|
||||||
|
Note: Much of this functionality can eventually be replaced
|
||||||
|
with native Python 3.3 code.
|
||||||
|
"""
|
||||||
|
def __init__(self, host, port=None, key_file=None, cert_file=None,
|
||||||
|
cacert=None, timeout=None, insecure=False,
|
||||||
|
ssl_compression=True):
|
||||||
|
HTTPSConnection.__init__(self, host, port,
|
||||||
|
key_file=key_file,
|
||||||
|
cert_file=cert_file)
|
||||||
|
self.key_file = key_file
|
||||||
|
self.cert_file = cert_file
|
||||||
|
self.timeout = timeout
|
||||||
|
self.insecure = insecure
|
||||||
|
self.ssl_compression = ssl_compression
|
||||||
|
self.cacert = cacert
|
||||||
|
self.setcontext()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def host_matches_cert(host, x509):
|
||||||
|
"""
|
||||||
|
Verify that the the x509 certificate we have received
|
||||||
|
from 'host' correctly identifies the server we are
|
||||||
|
connecting to, ie that the certificate's Common Name
|
||||||
|
or a Subject Alternative Name matches 'host'.
|
||||||
|
"""
|
||||||
|
# First see if we can match the CN
|
||||||
|
if x509.get_subject().commonName == host:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Also try Subject Alternative Names for a match
|
||||||
|
san_list = None
|
||||||
|
for i in xrange(x509.get_extension_count()):
|
||||||
|
ext = x509.get_extension(i)
|
||||||
|
if ext.get_short_name() == 'subjectAltName':
|
||||||
|
san_list = str(ext)
|
||||||
|
for san in ''.join(san_list.split()).split(','):
|
||||||
|
if san == "DNS:%s" % host:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Server certificate does not match host
|
||||||
|
msg = ('Host "%s" does not match x509 certificate contents: '
|
||||||
|
'CommonName "%s"' % (host, x509.get_subject().commonName))
|
||||||
|
if san_list is not None:
|
||||||
|
msg = msg + ', subjectAltName "%s"' % san_list
|
||||||
|
raise exc.SSLCertificateError(msg)
|
||||||
|
|
||||||
|
def verify_callback(self, connection, x509, errnum,
|
||||||
|
depth, preverify_ok):
|
||||||
|
# NOTE(leaman): preverify_ok may be a non-boolean type
|
||||||
|
preverify_ok = bool(preverify_ok)
|
||||||
|
if x509.has_expired():
|
||||||
|
msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
|
||||||
|
raise exc.SSLCertificateError(msg)
|
||||||
|
|
||||||
|
if depth == 0 and preverify_ok:
|
||||||
|
# We verify that the host matches against the last
|
||||||
|
# certificate in the chain
|
||||||
|
return self.host_matches_cert(self.host, x509)
|
||||||
|
else:
|
||||||
|
# Pass through OpenSSL's default result
|
||||||
|
return preverify_ok
|
||||||
|
|
||||||
|
def setcontext(self):
|
||||||
|
"""
|
||||||
|
Set up the OpenSSL context.
|
||||||
|
"""
|
||||||
|
self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
||||||
|
|
||||||
|
if self.ssl_compression is False:
|
||||||
|
self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
|
||||||
|
|
||||||
|
if self.insecure is not True:
|
||||||
|
self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
|
||||||
|
self.verify_callback)
|
||||||
|
else:
|
||||||
|
self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
|
||||||
|
lambda *args: True)
|
||||||
|
|
||||||
|
if self.cert_file:
|
||||||
|
try:
|
||||||
|
self.context.use_certificate_file(self.cert_file)
|
||||||
|
except Exception as e:
|
||||||
|
msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
|
||||||
|
raise exc.SSLConfigurationError(msg)
|
||||||
|
if self.key_file is None:
|
||||||
|
# We support having key and cert in same file
|
||||||
|
try:
|
||||||
|
self.context.use_privatekey_file(self.cert_file)
|
||||||
|
except Exception as e:
|
||||||
|
msg = ('No key file specified and unable to load key '
|
||||||
|
'from "%s" %s' % (self.cert_file, e))
|
||||||
|
raise exc.SSLConfigurationError(msg)
|
||||||
|
|
||||||
|
if self.key_file:
|
||||||
|
try:
|
||||||
|
self.context.use_privatekey_file(self.key_file)
|
||||||
|
except Exception as e:
|
||||||
|
msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
|
||||||
|
raise exc.SSLConfigurationError(msg)
|
||||||
|
|
||||||
|
if self.cacert:
|
||||||
|
try:
|
||||||
|
self.context.load_verify_locations(self.cacert)
|
||||||
|
except Exception as e:
|
||||||
|
msg = 'Unable to load CA from "%s"' % (self.cacert, e)
|
||||||
|
raise exc.SSLConfigurationError(msg)
|
||||||
|
else:
|
||||||
|
self.context.set_default_verify_paths()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""
|
||||||
|
Connect to an SSL port using the OpenSSL library and apply
|
||||||
|
per-connection parameters.
|
||||||
|
"""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
if self.timeout is not None:
|
||||||
|
# '0' microseconds
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
|
||||||
|
struct.pack('fL', self.timeout, 0))
|
||||||
|
self.sock = OpenSSLConnectionDelegator(self.context, sock)
|
||||||
|
self.sock.connect((self.host, self.port))
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.sock:
|
||||||
|
# Removing reference to socket but don't close it yet.
|
||||||
|
# Response close will close both socket and associated
|
||||||
|
# file. Closing socket too soon will cause response
|
||||||
|
# reads to fail with socket IO error 'Bad file descriptor'.
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
# Calling close on HTTPConnection to continue doing that cleanup.
|
||||||
|
HTTPSConnection.close(self)
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseBodyIterator(object):
|
||||||
|
"""
|
||||||
|
A class that acts as an iterator over an HTTP response.
|
||||||
|
|
||||||
|
This class will also check response body integrity when iterating over
|
||||||
|
the instance and if a checksum was supplied using `set_checksum` method,
|
||||||
|
else by default the class will not do any integrity check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, resp):
|
||||||
|
self._resp = resp
|
||||||
|
self._checksum = None
|
||||||
|
self._size = int(resp.getheader('content-length', 0))
|
||||||
|
self._end_reached = False
|
||||||
|
|
||||||
|
def set_checksum(self, checksum):
|
||||||
|
"""
|
||||||
|
Set checksum to check against when iterating over this instance.
|
||||||
|
|
||||||
|
:raise: AttributeError if iterator is already consumed.
|
||||||
|
"""
|
||||||
|
if self._end_reached:
|
||||||
|
raise AttributeError("Can't set checksum for an already consumed"
|
||||||
|
" iterator")
|
||||||
|
self._checksum = checksum
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return int(self._size)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
md5sum = hashlib.md5()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = self.next()
|
||||||
|
except StopIteration:
|
||||||
|
self._end_reached = True
|
||||||
|
# NOTE(mouad): Check image integrity when the end of response
|
||||||
|
# body is reached.
|
||||||
|
md5sum = md5sum.hexdigest()
|
||||||
|
if self._checksum is not None and md5sum != self._checksum:
|
||||||
|
raise IOError(errno.EPIPE,
|
||||||
|
'Corrupted image. Checksum was %s '
|
||||||
|
'expected %s' % (md5sum, self._checksum))
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
yield chunk
|
||||||
|
md5sum.update(chunk)
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
chunk = self._resp.read(CHUNKSIZE)
|
||||||
|
if chunk:
|
||||||
|
return chunk
|
||||||
|
else:
|
||||||
|
raise StopIteration()
|
119
metadataclient/common/utils.py
Normal file
119
metadataclient/common/utils.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# Copyright 2012 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import os
|
||||||
|
from muranoclient.common import exceptions
|
||||||
|
import prettytable
|
||||||
|
from muranoclient.openstack.common import importutils
|
||||||
|
|
||||||
|
|
||||||
|
# Decorator for cli-args
|
||||||
|
def arg(*args, **kwargs):
|
||||||
|
def _decorator(func):
|
||||||
|
# Because of the sematics of decorator composition if we just append
|
||||||
|
# to the options list positional options will appear to be backwards.
|
||||||
|
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||||
|
return func
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_choice_list(l):
|
||||||
|
return ', '.join("'%s'" % i for i in l)
|
||||||
|
|
||||||
|
|
||||||
|
def print_list(objs, fields, field_labels, formatters={}, sortby=0):
|
||||||
|
pt = prettytable.PrettyTable([f for f in field_labels], caching=False)
|
||||||
|
pt.align = 'l'
|
||||||
|
|
||||||
|
for o in objs:
|
||||||
|
row = []
|
||||||
|
for field in fields:
|
||||||
|
if field in formatters:
|
||||||
|
row.append(formatters[field](o))
|
||||||
|
else:
|
||||||
|
data = getattr(o, field, None) or ''
|
||||||
|
row.append(data)
|
||||||
|
pt.add_row(row)
|
||||||
|
print pt.get_string(sortby=field_labels[sortby])
|
||||||
|
|
||||||
|
|
||||||
|
def print_dict(d, formatters={}):
|
||||||
|
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
||||||
|
pt.align = 'l'
|
||||||
|
|
||||||
|
for field in d.keys():
|
||||||
|
if field in formatters:
|
||||||
|
pt.add_row([field, formatters[field](d[field])])
|
||||||
|
else:
|
||||||
|
pt.add_row([field, d[field]])
|
||||||
|
print pt.get_string(sortby='Property')
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(manager, name_or_id):
|
||||||
|
"""Helper for the _find_* methods."""
|
||||||
|
# first try to get entity as integer id
|
||||||
|
try:
|
||||||
|
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||||
|
return manager.get(int(name_or_id))
|
||||||
|
except exceptions.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# now try to get entity as uuid
|
||||||
|
try:
|
||||||
|
uuid.UUID(str(name_or_id))
|
||||||
|
return manager.get(name_or_id)
|
||||||
|
except (ValueError, exceptions.NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# finally try to find entity by name
|
||||||
|
try:
|
||||||
|
return manager.find(name=name_or_id)
|
||||||
|
except exceptions.NotFound:
|
||||||
|
msg = "No %s with a name or ID of '%s' exists." % \
|
||||||
|
(manager.resource_class.__name__.lower(), name_or_id)
|
||||||
|
raise exceptions.CommandError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def string_to_bool(arg):
|
||||||
|
return arg.strip().lower() in ('t', 'true', 'yes', '1')
|
||||||
|
|
||||||
|
|
||||||
|
def env(*vars, **kwargs):
|
||||||
|
"""Search for the first defined of possibly many env vars
|
||||||
|
|
||||||
|
Returns the first environment variable defined in vars, or
|
||||||
|
returns the default defined in kwargs.
|
||||||
|
"""
|
||||||
|
for v in vars:
|
||||||
|
value = os.environ.get(v, None)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return kwargs.get('default', '')
|
||||||
|
|
||||||
|
|
||||||
|
def import_versioned_module(version, submodule=None):
|
||||||
|
module = 'muranoclient.v%s' % version
|
||||||
|
if submodule:
|
||||||
|
module = '.'.join((module, submodule))
|
||||||
|
return importutils.import_module(module)
|
||||||
|
|
||||||
|
|
||||||
|
def exit(msg=''):
|
||||||
|
if msg:
|
||||||
|
print >> sys.stderr, msg
|
||||||
|
sys.exit(1)
|
178
metadataclient/exc.py
Normal file
178
metadataclient/exc.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Copyright 2012 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class BaseException(Exception):
|
||||||
|
"""An error occurred."""
|
||||||
|
def __init__(self, message=None):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message or self.__class__.__doc__
|
||||||
|
|
||||||
|
|
||||||
|
class CommandError(BaseException):
|
||||||
|
"""Invalid usage of CLI."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidEndpoint(BaseException):
|
||||||
|
"""The provided endpoint is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class CommunicationError(BaseException):
|
||||||
|
"""Unable to communicate with server."""
|
||||||
|
|
||||||
|
|
||||||
|
class ClientException(Exception):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPException(ClientException):
|
||||||
|
"""Base exception for all HTTP-derived exceptions."""
|
||||||
|
code = 'N/A'
|
||||||
|
|
||||||
|
def __init__(self, details=None):
|
||||||
|
self.details = details or self.__class__.__name__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s (HTTP %s)" % (self.details, self.code)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMultipleChoices(HTTPException):
|
||||||
|
code = 300
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
self.details = ("Requested version of OpenStack Images API is not"
|
||||||
|
"available.")
|
||||||
|
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
|
||||||
|
self.details)
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequest(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPBadRequest(BadRequest):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 401
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPUnauthorized(Unauthorized):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPForbidden(Forbidden):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotFound(NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMethodNotAllowed(HTTPException):
|
||||||
|
code = 405
|
||||||
|
|
||||||
|
|
||||||
|
class Conflict(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPConflict(Conflict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OverLimit(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 413
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPOverLimit(OverLimit):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPInternalServerError(HTTPException):
|
||||||
|
code = 500
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotImplemented(HTTPException):
|
||||||
|
code = 501
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPBadGateway(HTTPException):
|
||||||
|
code = 502
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailable(HTTPException):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
code = 503
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPServiceUnavailable(ServiceUnavailable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
#NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
|
||||||
|
# classes
|
||||||
|
_code_map = {}
|
||||||
|
for obj_name in dir(sys.modules[__name__]):
|
||||||
|
if obj_name.startswith('HTTP'):
|
||||||
|
obj = getattr(sys.modules[__name__], obj_name)
|
||||||
|
_code_map[obj.code] = obj
|
||||||
|
|
||||||
|
|
||||||
|
def from_response(response, body=None):
|
||||||
|
"""Return an instance of an HTTPException based on httplib response."""
|
||||||
|
cls = _code_map.get(response.status, HTTPException)
|
||||||
|
if body:
|
||||||
|
details = body.replace('\n\n', '\n')
|
||||||
|
return cls(details=details)
|
||||||
|
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
class NoTokenLookupException(Exception):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNotFound(Exception):
|
||||||
|
"""DEPRECATED!"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SSLConfigurationError(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SSLCertificateError(BaseException):
|
||||||
|
pass
|
322
metadataclient/shell.py
Normal file
322
metadataclient/shell.py
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Command-line interface to the Murano Metadata Repository API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import httplib2
|
||||||
|
from keystoneclient.v2_0 import client as ksclient
|
||||||
|
|
||||||
|
import metadataclient
|
||||||
|
from metadataclient.common import exceptions
|
||||||
|
from metadataclient.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
class MuranoRepositoryShell(object):
|
||||||
|
|
||||||
|
def get_base_parser(self):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='muranorepository',
|
||||||
|
description=__doc__.strip(),
|
||||||
|
epilog='See "metadata help COMMAND" '
|
||||||
|
'for help on a specific command.',
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=HelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Global arguments
|
||||||
|
parser.add_argument('-h', '--help',
|
||||||
|
action='store_true',
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('-d', '--debug',
|
||||||
|
default=bool(utils.env('METADATACLIENT_DEBUG')),
|
||||||
|
action='store_true',
|
||||||
|
help='Defaults to env[METADATACLIENT_DEBUG]')
|
||||||
|
|
||||||
|
parser.add_argument('-v', '--verbose',
|
||||||
|
default=False, action="store_true",
|
||||||
|
help="Print more verbose output")
|
||||||
|
|
||||||
|
parser.add_argument('--get-schema',
|
||||||
|
default=False, action="store_true",
|
||||||
|
dest='get_schema',
|
||||||
|
help='Force retrieving the schema used to generate'
|
||||||
|
' portions of the help text rather than using'
|
||||||
|
' a cached copy. Ignored with api version 1')
|
||||||
|
|
||||||
|
parser.add_argument('-k', '--insecure',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Explicitly allow metadataclient to perform '
|
||||||
|
'\"insecure SSL\" (https) requests. The server\'s '
|
||||||
|
'certificate will not be verified against any '
|
||||||
|
'certificate authorities. This option should '
|
||||||
|
'be used with caution.')
|
||||||
|
|
||||||
|
parser.add_argument('--cert-file',
|
||||||
|
help='Path of certificate file to use in SSL '
|
||||||
|
'connection. This file can optionally be '
|
||||||
|
'prepended with the private key.')
|
||||||
|
|
||||||
|
parser.add_argument('--key-file',
|
||||||
|
help='Path of client key to use in SSL '
|
||||||
|
'connection. This option is not necessary '
|
||||||
|
'if your key is prepended to your cert file.')
|
||||||
|
|
||||||
|
parser.add_argument('--os-cacert',
|
||||||
|
metavar='<ca-certificate-file>',
|
||||||
|
dest='os_cacert',
|
||||||
|
default=utils.env('OS_CACERT'),
|
||||||
|
help='Path of CA TLS certificate(s) used to '
|
||||||
|
'verify the remote server\'s certificate. '
|
||||||
|
'Without this option metadata looks for the '
|
||||||
|
'default system CA certificates.')
|
||||||
|
|
||||||
|
parser.add_argument('--ca-file',
|
||||||
|
dest='os_cacert',
|
||||||
|
help='DEPRECATED! Use --os-cacert.')
|
||||||
|
|
||||||
|
parser.add_argument('--timeout',
|
||||||
|
default=600,
|
||||||
|
help='Number of seconds to wait for a response')
|
||||||
|
|
||||||
|
parser.add_argument('--os-tenant-id',
|
||||||
|
default=utils.env('OS_TENANT_ID'),
|
||||||
|
help='Defaults to env[OS_TENANT_ID]')
|
||||||
|
|
||||||
|
parser.add_argument('--os-tenant-name',
|
||||||
|
default=utils.env('OS_TENANT_NAME'),
|
||||||
|
help='Defaults to env[OS_TENANT_NAME]')
|
||||||
|
|
||||||
|
parser.add_argument('--os-auth-url',
|
||||||
|
default=utils.env('OS_AUTH_URL'),
|
||||||
|
help='Defaults to env[OS_AUTH_URL]')
|
||||||
|
|
||||||
|
parser.add_argument('--os-region-name',
|
||||||
|
default=utils.env('OS_REGION_NAME'),
|
||||||
|
help='Defaults to env[OS_REGION_NAME]')
|
||||||
|
|
||||||
|
parser.add_argument('--os-auth-token',
|
||||||
|
default=utils.env('OS_AUTH_TOKEN'),
|
||||||
|
help='Defaults to env[OS_AUTH_TOKEN]')
|
||||||
|
|
||||||
|
parser.add_argument('--os-service-type',
|
||||||
|
default=utils.env('OS_SERVICE_TYPE'),
|
||||||
|
help='Defaults to env[OS_SERVICE_TYPE]')
|
||||||
|
|
||||||
|
parser.add_argument('--os-endpoint-type',
|
||||||
|
default=utils.env('OS_ENDPOINT_TYPE'),
|
||||||
|
help='Defaults to env[OS_ENDPOINT_TYPE]')
|
||||||
|
|
||||||
|
parser.add_argument('--murano-metadata-url',
|
||||||
|
default=utils.env('MURANO_METADATA_URL'),
|
||||||
|
help='Defaults to env[MURANO_METADATA_URL]')
|
||||||
|
|
||||||
|
parser.add_argument('--murano_metadata-api-version',
|
||||||
|
default=utils.env(
|
||||||
|
'MURANO_METADATA_API_VERSION', default='1'),
|
||||||
|
help='Defaults to env[MURANO_METADATA_API_VERSION] '
|
||||||
|
'or 1')
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def get_subcommand_parser(self, version):
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
|
||||||
|
self.subcommands = {}
|
||||||
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||||
|
submodule = utils.import_versioned_module(version, 'shell')
|
||||||
|
self._find_actions(subparsers, submodule)
|
||||||
|
self._find_actions(subparsers, self)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def _find_actions(self, subparsers, actions_module):
|
||||||
|
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||||
|
# I prefer to be hypen-separated instead of underscores.
|
||||||
|
command = attr[3:].replace('_', '-')
|
||||||
|
callback = getattr(actions_module, attr)
|
||||||
|
desc = callback.__doc__ or ''
|
||||||
|
help = desc.strip().split('\n')[0]
|
||||||
|
arguments = getattr(callback, 'arguments', [])
|
||||||
|
|
||||||
|
subparser = subparsers.add_parser(command,
|
||||||
|
help=help,
|
||||||
|
description=desc,
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=HelpFormatter
|
||||||
|
)
|
||||||
|
subparser.add_argument('-h', '--help',
|
||||||
|
action='help',
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
self.subcommands[command] = subparser
|
||||||
|
for (args, kwargs) in arguments:
|
||||||
|
subparser.add_argument(*args, **kwargs)
|
||||||
|
subparser.set_defaults(func=callback)
|
||||||
|
|
||||||
|
def _get_ksclient(self, **kwargs):
|
||||||
|
"""Get an endpoint and auth token from Keystone.
|
||||||
|
|
||||||
|
:param username: name of user
|
||||||
|
:param password: user's password
|
||||||
|
:param tenant_id: unique identifier of tenant
|
||||||
|
:param tenant_name: name of tenant
|
||||||
|
:param auth_url: endpoint to authenticate against
|
||||||
|
"""
|
||||||
|
return ksclient.Client(username=kwargs.get('username'),
|
||||||
|
password=kwargs.get('password'),
|
||||||
|
tenant_id=kwargs.get('tenant_id'),
|
||||||
|
tenant_name=kwargs.get('tenant_name'),
|
||||||
|
auth_url=kwargs.get('auth_url'),
|
||||||
|
insecure=kwargs.get('insecure'))
|
||||||
|
|
||||||
|
def _get_endpoint(self, client, **kwargs):
|
||||||
|
"""Get an endpoint using the provided keystone client."""
|
||||||
|
return client.service_catalog.url_for(
|
||||||
|
service_type=kwargs.get('service_type') or 'metering',
|
||||||
|
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
|
||||||
|
|
||||||
|
def _setup_debugging(self, debug):
|
||||||
|
if debug:
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(levelname)s (%(module)s:%(lineno)d) %(message)s",
|
||||||
|
level=logging.DEBUG)
|
||||||
|
|
||||||
|
httplib2.debuglevel = 1
|
||||||
|
else:
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(levelname)s %(message)s",
|
||||||
|
level=logging.INFO)
|
||||||
|
|
||||||
|
def main(self, argv):
|
||||||
|
# Parse args once to find version
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
(options, args) = parser.parse_known_args(argv)
|
||||||
|
self._setup_debugging(options.debug)
|
||||||
|
|
||||||
|
# build available subcommands based on version
|
||||||
|
api_version = options.murano_metadata_api_version
|
||||||
|
subcommand_parser = self.get_subcommand_parser(api_version)
|
||||||
|
self.parser = subcommand_parser
|
||||||
|
|
||||||
|
# Handle top-level --help/-h before attempting to parse
|
||||||
|
# a command off the command line
|
||||||
|
if options.help or not argv:
|
||||||
|
self.do_help(options)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Parse args again and call whatever callback was selected
|
||||||
|
args = subcommand_parser.parse_args(argv)
|
||||||
|
|
||||||
|
# Short-circuit and deal with help command right away.
|
||||||
|
if args.func == self.do_help:
|
||||||
|
self.do_help(args)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.os_auth_token and args.murano_metadata_url:
|
||||||
|
token = args.os_auth_token
|
||||||
|
endpoint = args.murano_metadata_url
|
||||||
|
else:
|
||||||
|
if not args.os_username:
|
||||||
|
raise exceptions.CommandError("You must provide a username "
|
||||||
|
"via either --os-username "
|
||||||
|
"or via env[OS_USERNAME]")
|
||||||
|
|
||||||
|
if not args.os_password:
|
||||||
|
raise exceptions.CommandError("You must provide a password "
|
||||||
|
"via either --os-password "
|
||||||
|
"or via env[OS_PASSWORD]")
|
||||||
|
|
||||||
|
if not (args.os_tenant_id or args.os_tenant_name):
|
||||||
|
raise exceptions.CommandError("You must provide a tenant_id "
|
||||||
|
"via either --os-tenant-id "
|
||||||
|
"or via env[OS_TENANT_ID]")
|
||||||
|
|
||||||
|
if not args.os_auth_url:
|
||||||
|
raise exceptions.CommandError("You must provide an auth url "
|
||||||
|
"via either --os-auth-url or "
|
||||||
|
"via env[OS_AUTH_URL]")
|
||||||
|
kwargs = {
|
||||||
|
'username': args.os_username,
|
||||||
|
'password': args.os_password,
|
||||||
|
'tenant_id': args.os_tenant_id,
|
||||||
|
'tenant_name': args.os_tenant_name,
|
||||||
|
'auth_url': args.os_auth_url,
|
||||||
|
'service_type': args.os_service_type,
|
||||||
|
'endpoint_type': args.os_endpoint_type,
|
||||||
|
'insecure': args.insecure
|
||||||
|
}
|
||||||
|
_ksclient = self._get_ksclient(**kwargs)
|
||||||
|
token = args.os_auth_token or _ksclient.auth_token
|
||||||
|
|
||||||
|
url = args.murano_metadata_url
|
||||||
|
endpoint = url or self._get_endpoint(_ksclient, **kwargs)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'token': token,
|
||||||
|
'insecure': args.insecure,
|
||||||
|
'timeout': args.timeout,
|
||||||
|
'ca_file': args.ca_file,
|
||||||
|
'cert_file': args.cert_file,
|
||||||
|
'key_file': args.key_file,
|
||||||
|
}
|
||||||
|
|
||||||
|
client = metadataclient.Client(api_version, endpoint, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
args.func(client, args)
|
||||||
|
except exceptions.Unauthorized:
|
||||||
|
msg = "Invalid OpenStack Identity credentials."
|
||||||
|
raise exceptions.CommandError(msg)
|
||||||
|
|
||||||
|
@utils.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.
|
||||||
|
"""
|
||||||
|
if getattr(args, 'command', None):
|
||||||
|
if args.command in self.subcommands:
|
||||||
|
self.subcommands[args.command].print_help()
|
||||||
|
else:
|
||||||
|
msg = "'%s' is not a valid subcommand"
|
||||||
|
raise exceptions.CommandError(msg % args.command)
|
||||||
|
else:
|
||||||
|
self.parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
class HelpFormatter(argparse.HelpFormatter):
|
||||||
|
def start_section(self, heading):
|
||||||
|
# Title-case the headings
|
||||||
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||||
|
super(HelpFormatter, self).start_section(heading)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
MuranoRepositoryShell().main(sys.argv[1:])
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
print >> sys.stderr, e
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
34
test.py
Normal file
34
test.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from metadataclient.v1.client import Client
|
||||||
|
|
||||||
|
|
||||||
|
def metadataclient():
|
||||||
|
endpoint = 'http://localhost:5000'
|
||||||
|
insecure = False
|
||||||
|
token_id = '12casc2'
|
||||||
|
|
||||||
|
return Client(endpoint=endpoint, token=token_id, insecure=insecure)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# metadataclient().metadata_client.get_ui_data()
|
||||||
|
# metadataclient().metadata_client.get_conductor_data()
|
||||||
|
admin = metadataclient().metadata_admin
|
||||||
|
# print admin.list_ui()
|
||||||
|
# print admin.list_ui('Murano')
|
||||||
|
# print admin.list_agent()
|
||||||
|
# workflows = admin.list_workflows()
|
||||||
|
# for key, value in workflows.iteritems():
|
||||||
|
# for i in value:
|
||||||
|
# print i
|
||||||
|
# heat = metadataclient().metadata_admin.list_heat()
|
||||||
|
# for key, value in heat.iteritems():
|
||||||
|
# for i in value:
|
||||||
|
# print i
|
||||||
|
|
||||||
|
# admin.get_file('ui', 'WebServer.yaml')
|
||||||
|
# print admin.get_file('ui', 'Murano/Demo.yaml')
|
||||||
|
# admin.create_directory('ui', 'test')
|
||||||
|
# admin.delete_dir('ui', 'Murano/README.rst')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
x
Reference in New Issue
Block a user