diff --git a/magnumclient/api/bays.py b/magnumclient/api/bays.py new file mode 100644 index 00000000..3932695f --- /dev/null +++ b/magnumclient/api/bays.py @@ -0,0 +1,99 @@ +# Copyright 2014 NEC Corporation. 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. + +from magnumclient.common import base +from magnumclient.common import utils +from magnumclient import exceptions + + +# FIXME: Modify correct attributes. +CREATION_ATTRIBUTES = ['description'] + + +class Bay(base.Resource): + def __repr__(self): + return "" % self._info + + +class BayManager(base.Manager): + resource_class = Bay + + @staticmethod + def _path(id=None): + return '/v1/bays/%s' % id if id else '/v1/bays' + + def list(self, marker=None, limit=None, sort_key=None, + sort_dir=None, detail=False): + """Retrieve a list of bays. + + :param marker: Optional, the UUID of a bays, eg the last + bays from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of bays to return. + 2) limit == 0, return the entire list of bays. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Magnum API + (see Magnum's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about bays. + + :returns: A list of bays. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "bays") + else: + return self._list_pagination(self._path(path), "bays", + limit=limit) + + def get(self, bay_id): + try: + return self._list(self._path(bay_id))[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute() + return self._create(self._path(), new) + + def delete(self, bay_id): + return self._delete(self._path(bay_id)) + + def update(self, bay_id, patch): + return self._update(self._path(bay_id), patch) diff --git a/magnumclient/api/client.py b/magnumclient/api/client.py index 31e5f871..9cbd6fb4 100644 --- a/magnumclient/api/client.py +++ b/magnumclient/api/client.py @@ -16,7 +16,11 @@ from keystoneclient.v2_0 import client as keystone_client_v2 from keystoneclient.v3 import client as keystone_client_v3 -from magnumclient.api import httpclient +from magnumclient.api import bays +from magnumclient.api import containers +from magnumclient.api import pods +from magnumclient.api import services +from magnumclient.common import httpclient class Client(object): @@ -52,8 +56,26 @@ class Client(object): if not magnum_catalog_url: raise RuntimeError("Could not find Magnum endpoint in catalog") - self.client = httpclient.HTTPClient(magnum_catalog_url, - input_auth_token) + http_cli_kwargs = { + 'token': input_auth_token, + # TODO(yuanying): - use insecure + # 'insecure': kwargs.get('insecure'), + # TODO(yuanying): - use timeout + # 'timeout': kwargs.get('timeout'), + # TODO(yuanying): - use ca_file + # 'ca_file': kwargs.get('ca_file'), + # TODO(yuanying): - use cert_file + # 'cert_file': kwargs.get('cert_file'), + # TODO(yuanying): - use key_file + # 'key_file': kwargs.get('key_file'), + 'auth_ref': None, + } + self.http_client = httpclient.HTTPClient(magnum_catalog_url, + **http_cli_kwargs) + self.bays = bays.BayManager(self.http_client) + self.pods = pods.PodManager(self.http_client) + self.services = services.ServiceManager(self.http_client) + self.containers = containers.ContainerManager(self.http_client) def get_keystone_client(self, username=None, api_key=None, auth_url=None, token=None, project_id=None, project_name=None): diff --git a/magnumclient/api/containers.py b/magnumclient/api/containers.py new file mode 100644 index 00000000..2230cc99 --- /dev/null +++ b/magnumclient/api/containers.py @@ -0,0 +1,99 @@ +# Copyright 2014 NEC Corporation. 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. + +from magnumclient.common import base +from magnumclient.common import utils +from magnumclient import exceptions + + +# FIXME: Modify correct attributes. +CREATION_ATTRIBUTES = ['description'] + + +class Container(base.Resource): + def __repr__(self): + return "" % self._info + + +class ContainerManager(base.Manager): + resource_class = Container + + @staticmethod + def _path(id=None): + return '/v1/containers/%s' % id if id else '/v1/containers' + + def list(self, marker=None, limit=None, sort_key=None, + sort_dir=None, detail=False): + """Retrieve a list of containers. + + :param marker: Optional, the UUID of a containers, eg the last + containers from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of containers to return. + 2) limit == 0, return the entire list of containers. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Magnum API + (see Magnum's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about containers. + + :returns: A list of containers. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "containers") + else: + return self._list_pagination(self._path(path), "containers", + limit=limit) + + def get(self, container_id): + try: + return self._list(self._path(container_id))[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute() + return self._create(self._path(), new) + + def delete(self, container_id): + return self._delete(self._path(container_id)) + + def update(self, container_id, patch): + return self._update(self._path(container_id), patch) diff --git a/magnumclient/api/httpclient.py b/magnumclient/api/httpclient.py deleted file mode 100644 index e97ca569..00000000 --- a/magnumclient/api/httpclient.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2014 -# The Cloudscaling Group, 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. - -import requests - - -class HTTPClient(object): - def __init__(self, base_url, token): - self.base_url = base_url - self.token = token - - def get(self, url): - return requests.get(self.base_url + url, - headers={'x-auth-token': self.token}) - - def post(self, url, body, json=True): - headers = {'x-auth-token': self.token} - if json: - headers['content-type'] = 'application/json' - return requests.post(self.base_url + url, body, headers=headers) - - def put(self, url, body, json=True): - headers = {'x-auth-token': self.token} - if json: - headers['content-type'] = 'application/json' - return requests.put(self.base_url + url, body, headers=headers) - - def delete(self, url): - return requests.delete(self.base_url + url, - headers={'x-auth-token': self.token}) diff --git a/magnumclient/api/pods.py b/magnumclient/api/pods.py new file mode 100644 index 00000000..4ed2ced3 --- /dev/null +++ b/magnumclient/api/pods.py @@ -0,0 +1,99 @@ +# Copyright 2014 NEC Corporation. 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. + +from magnumclient.common import base +from magnumclient.common import utils +from magnumclient import exceptions + + +# FIXME: Modify correct attributes. +CREATION_ATTRIBUTES = ['description'] + + +class Pod(base.Resource): + def __repr__(self): + return "" % self._info + + +class PodManager(base.Manager): + resource_class = Pod + + @staticmethod + def _path(id=None): + return '/v1/pods/%s' % id if id else '/v1/pods' + + def list(self, marker=None, limit=None, sort_key=None, + sort_dir=None, detail=False): + """Retrieve a list of pods. + + :param marker: Optional, the UUID of a pods, eg the last + pods from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of pods to return. + 2) limit == 0, return the entire list of pods. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Magnum API + (see Magnum's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about pods. + + :returns: A list of pods. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "pods") + else: + return self._list_pagination(self._path(path), "pods", + limit=limit) + + def get(self, pod_id): + try: + return self._list(self._path(pod_id))[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute() + return self._create(self._path(), new) + + def delete(self, pod_id): + return self._delete(self._path(pod_id)) + + def update(self, pod_id, patch): + return self._update(self._path(pod_id), patch) \ No newline at end of file diff --git a/magnumclient/api/services.py b/magnumclient/api/services.py new file mode 100644 index 00000000..97c266d9 --- /dev/null +++ b/magnumclient/api/services.py @@ -0,0 +1,98 @@ +# Copyright 2014 NEC Corporation. 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. + +from magnumclient.common import base +from magnumclient.common import utils +from magnumclient import exceptions + +# FIXME: Modify correct attributes. +CREATION_ATTRIBUTES = ['description'] + + +class Service(base.Resource): + def __repr__(self): + return "" % self._info + + +class ServiceManager(base.Manager): + resource_class = Service + + @staticmethod + def _path(id=None): + return '/v1/services/%s' % id if id else '/v1/services' + + def list(self, marker=None, limit=None, sort_key=None, + sort_dir=None, detail=False): + """Retrieve a list of services. + + :param marker: Optional, the UUID of a services, eg the last + services from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of services to return. + 2) limit == 0, return the entire list of services. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Magnum API + (see Magnum's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about services. + + :returns: A list of services. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), "services") + else: + return self._list_pagination(self._path(path), "services", + limit=limit) + + def get(self, service_id): + try: + return self._list(self._path(service_id))[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute() + return self._create(self._path(), new) + + def delete(self, service_id): + return self._delete(self._path(service_id)) + + def update(self, service_id, patch): + return self._update(self._path(service_id), patch) \ No newline at end of file diff --git a/magnumclient/common/__init__.py b/magnumclient/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/magnumclient/common/base.py b/magnumclient/common/base.py new file mode 100644 index 00000000..7d0237e9 --- /dev/null +++ b/magnumclient/common/base.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# +# 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 + +import six.moves.urllib.parse as urlparse + +from magnumclient.openstack.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 + + 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 Magnum API is configured to return a maximum number of + items per request, (FIXME: see Magnum'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: + # NOTE(lucasagomes): We need to edit the URL to remove + # the scheme and netloc + 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): + 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): + self.api.raw_request('DELETE', url) + + +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) diff --git a/magnumclient/common/httpclient.py b/magnumclient/common/httpclient.py new file mode 100644 index 00000000..bc206f8d --- /dev/null +++ b/magnumclient/common/httpclient.py @@ -0,0 +1,374 @@ +# -*- coding: utf-8 -*- +# +# 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 json +import logging +import os +import socket +import ssl + +from keystoneclient import adapter +import six +import six.moves.urllib.parse as urlparse + +from magnumclient import exceptions + + +LOG = logging.getLogger(__name__) +USER_AGENT = 'python-magnumclient' +CHUNKSIZE = 1024 * 64 # 64kB + +API_VERSION = '/v1' + + +class HTTPClient(object): + + def __init__(self, endpoint, **kwargs): + self.endpoint = endpoint + self.auth_token = kwargs.get('token') + self.auth_ref = kwargs.get('auth_ref') + 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)) + 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 _extract_error_json(self, 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) + except ValueError: + return {} + + return error_json + + 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) + + 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': + body_str = ''.join([chunk for chunk in body_iter]) + 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.warn("Request returned failure status.") + error_json = self._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 _http_request(self, url, method, **kwargs): + kwargs.setdefault('user_agent', USER_AGENT) + kwargs.setdefault('auth', self.auth) + + 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: + raise exceptions.from_response(resp) + 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: + yield self.next() + + 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', 'baremetal') + 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-magnumclient') + else: + return HTTPClient(*args, **kwargs) diff --git a/magnumclient/common/utils.py b/magnumclient/common/utils.py new file mode 100644 index 00000000..8df52872 --- /dev/null +++ b/magnumclient/common/utils.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# 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. + + +def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None): + """Generate common filters for any list request. + + :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 isinstance(limit, int) and limit > 0: + 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 \ No newline at end of file diff --git a/magnumclient/exceptions.py b/magnumclient/exceptions.py new file mode 100644 index 00000000..3538cba9 --- /dev/null +++ b/magnumclient/exceptions.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# 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 magnumclient.openstack.common.apiclient import exceptions +from magnumclient.openstack.common.apiclient.exceptions import * # noqa + + +# NOTE(akurilin): This alias is left here since v.0.1.3 to support backwards +# compatibility. +InvalidEndpoint = EndpointException +CommunicationError = ConnectionRefused +HTTPBadRequest = BadRequest +HTTPInternalServerError = InternalServerError +HTTPNotFound = NotFound +HTTPServiceUnavailable = ServiceUnavailable + + +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'): + # NOTE(akurilin): These modifications around response object give + # ability to get all necessary information in method `from_response` + # from common code, which expecting response object from `requests` + # library instead of object from `httplib/httplib2` library. + response.status_code = response.status + response.headers = { + 'Content-Type': response.getheader('content-type', "")} + response.json = lambda: {'error': error_body} + + return exceptions.from_response(response, message, url) diff --git a/magnumclient/shell.py b/magnumclient/shell.py index e5d5ab7e..3d78b39f 100644 --- a/magnumclient/shell.py +++ b/magnumclient/shell.py @@ -588,6 +588,7 @@ class OpenStackMagnumShell(object): project_id=os_tenant_id, project_name=os_tenant_name, auth_url=os_auth_url, + service_type=service_type, magnum_url=bypass_url) args.func(self.cs, args)