From 6c51f7d983bf14d2fb956eb8576d0737ac31f0bf Mon Sep 17 00:00:00 2001 From: Timur Nurlygayanov Date: Mon, 18 Feb 2013 00:32:23 -0800 Subject: [PATCH] Added windc API client, sync repo with dev box. --- dashboard/ReadMe.txt | 6 +- dashboard/api/windc.py | 64 ++++ dashboard/windcclient/__init__.py | 0 dashboard/windcclient/common/__init__.py | 0 dashboard/windcclient/common/base.py | 137 +++++++++ dashboard/windcclient/common/client.py | 148 +++++++++ dashboard/windcclient/common/exceptions.py | 140 +++++++++ .../windcclient/common/service_catalog.py | 62 ++++ dashboard/windcclient/common/utils.py | 291 ++++++++++++++++++ dashboard/windcclient/shell.py | 285 +++++++++++++++++ dashboard/windcclient/v1/__init__.py | 0 dashboard/windcclient/v1/client.py | 27 ++ dashboard/windcclient/v1/datacenters.py | 49 +++ 13 files changed, 1207 insertions(+), 2 deletions(-) create mode 100644 dashboard/api/windc.py create mode 100644 dashboard/windcclient/__init__.py create mode 100644 dashboard/windcclient/common/__init__.py create mode 100644 dashboard/windcclient/common/base.py create mode 100644 dashboard/windcclient/common/client.py create mode 100644 dashboard/windcclient/common/exceptions.py create mode 100644 dashboard/windcclient/common/service_catalog.py create mode 100644 dashboard/windcclient/common/utils.py create mode 100644 dashboard/windcclient/shell.py create mode 100644 dashboard/windcclient/v1/__init__.py create mode 100644 dashboard/windcclient/v1/client.py create mode 100644 dashboard/windcclient/v1/datacenters.py diff --git a/dashboard/ReadMe.txt b/dashboard/ReadMe.txt index 8d5cc0da8..45d191b74 100644 --- a/dashboard/ReadMe.txt +++ b/dashboard/ReadMe.txt @@ -6,7 +6,9 @@ This file is described how to install new tab on horizon dashboard. We should do the following: 1. Copy directory 'windc' to directory '/opt/stack/horizon/openstack_dashboard/dashboards/project' - 2. Edit file '/opt/stack/horizon/openstack_dashboard/dashboards/project/dashboard.py' + 2. Copy api/windc.py to directory '/opt/stack/horizon/openstack_dashboard/api' + 3. Copy directory 'windcclient' to directory '/opt/stack/horizon/' + 4. Edit file '/opt/stack/horizon/openstack_dashboard/dashboards/project/dashboard.py' Add line with windc project: ... @@ -24,6 +26,6 @@ class BasePanels(horizon.PanelGroup): ... - 3. Run the test Django server: + 5. Run the test Django server: cd /opt/stack/horizon python manage.py runserver 67.207.197.36:8080 \ No newline at end of file diff --git a/dashboard/api/windc.py b/dashboard/api/windc.py new file mode 100644 index 000000000..1210ae829 --- /dev/null +++ b/dashboard/api/windc.py @@ -0,0 +1,64 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, 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 logging +import urlparse + +from django.utils.decorators import available_attrs + +from windcclient.v1 import client as windc_client + +#from horizon.api import base + + +__all__ = ('datacenter_get','datacenter_list', + 'datacenter_create','datacenter_delete') + + +LOG = logging.getLogger(__name__) + + +def windcclient(request): + o = urlparse.urlparse("http://127.0.0.1:8082") + url = "http://127.0.0.1:8082/foo" + LOG.debug('windcclient connection created using token "%s" and url "%s"' + % (request.user.token, url)) + return windc_client.Client(endpoint=url, token=None) + +def datacenter_create(request, parameters): + name = parameters.get('name') + _type = parameters.get('type') + version = parameters.get('version') + ip = parameters.get('ip') + port = parameters.get('port') + user = parameters.get('user') + password = parameters.get('password') + return windcclient(request).datacenters.create(name, _type, + version, ip, + port, user, password) + +def datacenter_delete(request, datacenter): + return windcclient(request).datacenters.delete(datacenter) + +def datacenter_get(request, lb_id): + return windcclient(request).datacenters.get(lb_id) + +def datacenter_list(request): + return windcclient(request).datacenters.list() diff --git a/dashboard/windcclient/__init__.py b/dashboard/windcclient/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dashboard/windcclient/common/__init__.py b/dashboard/windcclient/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dashboard/windcclient/common/base.py b/dashboard/windcclient/common/base.py new file mode 100644 index 000000000..9f0350447 --- /dev/null +++ b/dashboard/windcclient/common/base.py @@ -0,0 +1,137 @@ +# 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. +""" + + +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 and provide CRUD + operations for them. + """ + resource_class = None + + def __init__(self, api): + self.api = api + + def _list(self, url, response_key, obj_class=None, body=None): + resp, body = self.api.client.json_request('GET', url, body=body) + + if obj_class is None: + obj_class = self.resource_class + + data = body[response_key] + return [obj_class(self, res, loaded=True) for res in data if res] + + def _delete(self, url): + self.api.client.raw_request('DELETE', url) + + def _update(self, url, body, response_key=None): + resp, body = self.api.client.json_request('PUT', url, body=body) + # PUT requests may not return a body + if body: + return self.resource_class(self, body[response_key]) + + def _create(self, url, body, response_key, return_raw=False): + resp, body = self.api.client.json_request('POST', url, body=body) + if return_raw: + return body[response_key] + return self.resource_class(self, body[response_key]) + + def _get(self, url, response_key, return_raw=False): + resp, body = self.api.client.json_request('GET', url) + if return_raw: + return body[response_key] + return self.resource_class(self, body[response_key]) + + +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_info(self): + if not self.is_loaded(): + self.get() + if self._info: + return self._info.copy() + return {} + + 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._info = new._info + 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 diff --git a/dashboard/windcclient/common/client.py b/dashboard/windcclient/common/client.py new file mode 100644 index 000000000..04740cfd8 --- /dev/null +++ b/dashboard/windcclient/common/client.py @@ -0,0 +1,148 @@ +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +""" +OpenStack Client interface. Handles the REST calls and responses. +""" + +import httplib2 +import copy +import logging +import json + +from . import exceptions +from . import utils +from .service_catalog import ServiceCatalog + + +logger = logging.getLogger(__name__) + + +class HTTPClient(httplib2.Http): + + USER_AGENT = 'python-balancerclient' + + def __init__(self, endpoint=None, token=None, username=None, + password=None, tenant_name=None, tenant_id=None, + region_name=None, auth_url=None, auth_tenant_id=None, + timeout=600, insecure=False): + super(HTTPClient, self).__init__(timeout=timeout) + self.endpoint = endpoint + self.auth_token = token + self.auth_url = auth_url + self.auth_tenant_id = auth_tenant_id + self.username = username + self.password = password + self.tenant_name = tenant_name + self.tenant_id = tenant_id + self.region_name = region_name + self.force_exception_to_status_code = True + self.disable_ssl_certificate_validation = insecure + if self.endpoint is None: + self.authenticate() + + def _http_request(self, url, method, **kwargs): + """ Send an http request with the specified characteristics. + """ + + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', self.USER_AGENT) + if self.auth_token: + kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) + + resp, body = super(HTTPClient, self).request(url, method, **kwargs) + + if logger.isEnabledFor(logging.DEBUG): + utils.http_log(logger, (url, method,), kwargs, resp, body) + + if resp.status in (301, 302, 305): + return self._http_request(resp['location'], method, **kwargs) + + return resp, body + + def _json_request(self, method, url, **kwargs): + """ Wrapper around _http_request to handle setting headers, + JSON enconding/decoding and error handling. + """ + + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + + if 'body' in kwargs and kwargs['body'] is not None: + kwargs['body'] = json.dumps(kwargs['body']) + + resp, body = self._http_request(url, method, **kwargs) + + if body: + try: + body = json.loads(body) + except ValueError: + logger.debug("Could not decode JSON from body: %s" % body) + else: + logger.debug("No body was returned.") + body = None + + if 400 <= resp.status < 600: + raise exceptions.from_response(resp, body) + + return resp, body + + def raw_request(self, method, url, **kwargs): + url = self.endpoint + url + + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + + resp, body = self._http_request(url, method, **kwargs) + + if 400 <= resp.status < 600: + raise exceptions.from_response(resp, body) + + return resp, body + + def json_request(self, method, url, **kwargs): + url = self.endpoint + url + resp, body = self._json_request(method, url, **kwargs) + return resp, body + + def authenticate(self): + token_url = self.auth_url + "/tokens" + body = {'auth': {'passwordCredentials': {'username': self.username, + 'password': self.password}}} + if self.tenant_id: + body['auth']['tenantId'] = self.tenant_id + elif self.tenant_name: + body['auth']['tenantName'] = self.tenant_name + + tmp_follow_all_redirects = self.follow_all_redirects + self.follow_all_redirects = True + try: + resp, body = self._json_request('POST', token_url, body=body) + finally: + self.follow_all_redirects = tmp_follow_all_redirects + + try: + self.service_catalog = ServiceCatalog(body['access']) + token = self.service_catalog.get_token() + self.auth_token = token['id'] + self.auth_tenant_id = token['tenant_id'] + except KeyError: + logger.exception("Parse service catalog failed.") + raise exceptions.AuthorizationFailure() + + self.endpoint = self.service_catalog.url_for(attr='region', + filter_value=self.region_name) diff --git a/dashboard/windcclient/common/exceptions.py b/dashboard/windcclient/common/exceptions.py new file mode 100644 index 000000000..4d17b8db2 --- /dev/null +++ b/dashboard/windcclient/common/exceptions.py @@ -0,0 +1,140 @@ +# Copyright 2010 Jacob Kaplan-Moss +""" +Exception definitions. +""" + + +class UnsupportedVersion(Exception): + """Indicates that the user is trying to use an unsupported + version of the API""" + pass + + +class CommandError(Exception): + pass + + +class AuthorizationFailure(Exception): + pass + + +class NoUniqueMatch(Exception): + pass + + +class NoTokenLookupException(Exception): + """This form of authentication does not support looking up + endpoints from an existing token.""" + pass + + +class EndpointNotFound(Exception): + """Could not find Service or Region in Service Catalog.""" + pass + + +class AmbiguousEndpoints(Exception): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + self.endpoints = endpoints + + def __str__(self): + return "AmbiguousEndpoints: %s" % repr(self.endpoints) + + +class ClientException(Exception): + """ + The base exception class for all exceptions this library raises. + """ + def __init__(self, code, message=None, details=None): + self.code = code + self.message = message or self.__class__.message + self.details = details + + def __str__(self): + return "%s (HTTP %s)" % (self.message, self.code) + + +class BadRequest(ClientException): + """ + HTTP 400 - Bad request: you sent some malformed data. + """ + http_status = 400 + message = "Bad request" + + +class Unauthorized(ClientException): + """ + HTTP 401 - Unauthorized: bad credentials. + """ + http_status = 401 + message = "Unauthorized" + + +class Forbidden(ClientException): + """ + HTTP 403 - Forbidden: your credentials don't give you access to this + resource. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(ClientException): + """ + HTTP 404 - Not found + """ + http_status = 404 + message = "Not found" + + +class OverLimit(ClientException): + """ + HTTP 413 - Over limit: you're over the API limits for this time period. + """ + http_status = 413 + message = "Over limit" + + +# NotImplemented is a python keyword. +class HTTPNotImplemented(ClientException): + """ + HTTP 501 - Not Implemented: the server does not support this operation. + """ + http_status = 501 + message = "Not Implemented" + + +# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() +# so we can do this: +# _code_map = dict((c.http_status, c) +# for c in ClientException.__subclasses__()) +# +# Instead, we have to hardcode it: +_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, + Forbidden, NotFound, OverLimit, HTTPNotImplemented]) + + +def from_response(response, body): + """ + Return an instance of an ClientException or subclass + based on an httplib2 response. + + Usage:: + + resp, body = http.request(...) + if resp.status != 200: + raise exception_from_response(resp, body) + """ + cls = _code_map.get(response.status, ClientException) + if body: + if hasattr(body, 'keys'): + error = body[body.keys()[0]] + message = error.get('message', None) + details = error.get('details', None) + else: + message = 'n/a' + details = body + return cls(code=response.status, message=message, details=details) + else: + return cls(code=response.status) diff --git a/dashboard/windcclient/common/service_catalog.py b/dashboard/windcclient/common/service_catalog.py new file mode 100644 index 000000000..d2a91d67e --- /dev/null +++ b/dashboard/windcclient/common/service_catalog.py @@ -0,0 +1,62 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011, Piston Cloud Computing, Inc. +# Copyright 2011 Nebula, Inc. +# +# 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 . import exceptions + + +class ServiceCatalog(object): + """Helper methods for dealing with a Keystone Service Catalog.""" + + def __init__(self, resource_dict): + self.catalog = resource_dict + + def get_token(self): + """Fetch token details fron service catalog""" + token = {'id': self.catalog['token']['id'], + 'expires': self.catalog['token']['expires']} + try: + token['user_id'] = self.catalog['user']['id'] + token['tenant_id'] = self.catalog['token']['tenant']['id'] + except: + # just leave the tenant and user out if it doesn't exist + pass + return token + + def url_for(self, attr=None, filter_value=None, + service_type='loadbalancer', endpoint_type='publicURL'): + """Fetch an endpoint from the service catalog. + + Fetch the specified endpoint from the service catalog for + a particular endpoint attribute. If no attribute is given, return + the first endpoint of the specified type. + + See tests for a sample service catalog. + """ + catalog = self.catalog.get('serviceCatalog', []) + + for service in catalog: + if service['type'] != service_type: + continue + + endpoints = service['endpoints'] + for endpoint in endpoints: + if not filter_value or endpoint.get(attr) == filter_value: + return endpoint[endpoint_type] + + raise exceptions.EndpointNotFound('Endpoint not found.') diff --git a/dashboard/windcclient/common/utils.py b/dashboard/windcclient/common/utils.py new file mode 100644 index 000000000..cabcba88c --- /dev/null +++ b/dashboard/windcclient/common/utils.py @@ -0,0 +1,291 @@ +import os +import re +import sys +import uuid +import logging +import prettytable + +from . import exceptions + + +def arg(*args, **kwargs): + """Decorator for CLI args.""" + def _decorator(func): + add_arg(func, *args, **kwargs) + return func + return _decorator + + +def env(*vars, **kwargs): + """ + returns the first environment variable set + if none are non-empty, defaults to '' or keyword arg default + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +def add_arg(f, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + + if not hasattr(f, 'arguments'): + f.arguments = [] + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in f.arguments: + # Because of the sematics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + f.arguments.insert(0, (args, kwargs)) + + +def add_resource_manager_extra_kwargs_hook(f, hook): + """Adds hook to bind CLI arguments to ResourceManager calls. + + The `do_foo` calls in shell.py will receive CLI args and then in turn pass + them through to the ResourceManager. Before passing through the args, the + hooks registered here will be called, giving us a chance to add extra + kwargs (taken from the command-line) to what's passed to the + ResourceManager. + """ + if not hasattr(f, 'resource_manager_kwargs_hooks'): + f.resource_manager_kwargs_hooks = [] + + names = [h.__name__ for h in f.resource_manager_kwargs_hooks] + if hook.__name__ not in names: + f.resource_manager_kwargs_hooks.append(hook) + + +def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): + """Return extra_kwargs by calling resource manager kwargs hooks.""" + hooks = getattr(f, "resource_manager_kwargs_hooks", []) + extra_kwargs = {} + for hook in hooks: + hook_name = hook.__name__ + hook_kwargs = hook(args) + + conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) + if conflicting_keys and not allow_conflicts: + raise Exception("Hook '%(hook_name)s' is attempting to redefine" + " attributes '%(conflicting_keys)s'" % locals()) + + extra_kwargs.update(hook_kwargs) + + return extra_kwargs + + +def unauthenticated(f): + """ + Adds 'unauthenticated' attribute to decorated function. + Usage: + @unauthenticated + def mymethod(f): + ... + """ + f.unauthenticated = True + return f + + +def isunauthenticated(f): + """ + Checks to see if the function is marked as not requiring authentication + with the @unauthenticated decorator. Returns True if decorator is + set to True, False otherwise. + """ + return getattr(f, 'unauthenticated', False) + + +def service_type(stype): + """ + Adds 'service_type' attribute to decorated function. + Usage: + @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 print_list(objs, fields, formatters={}, sortby_index=0): + if sortby_index == None: + sortby = None + else: + sortby = fields[sortby_index] + + pt = prettytable.PrettyTable([f for f in fields], caching=False) + pt.align = 'l' + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + row.append(data) + pt.add_row(row) + + print pt.get_string(sortby=sortby) + + +def print_flat_list(lst, field): + pt = prettytable.PrettyTable(field) + for el in lst: + pt.add_row([el]) + print pt.get_string() + + +def print_dict(d, property="Property"): + pt = prettytable.PrettyTable([property, 'Value'], caching=False) + pt.align = 'l' + [pt.add_row(list(r)) for r in d.iteritems()] + 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 + + try: + try: + return manager.find(human_id=name_or_id) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + return manager.find(name=name_or_id) + except exceptions.NotFound: + try: + # Volumes does not have name, but display_name + return manager.find(display_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) + except exceptions.NoUniqueMatch: + msg = ("Multiple %s matches found for '%s', use an ID to be more" + " specific." % (manager.resource_class.__name__.lower(), + name_or_id)) + raise exceptions.CommandError(msg) + + +def _format_servers_list_networks(server): + output = [] + for (network, addresses) in server.networks.items(): + if len(addresses) == 0: + continue + addresses_csv = ', '.join(addresses) + group = "%s=%s" % (network, addresses_csv) + output.append(group) + + return '; '.join(output) + + +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +def safe_issubclass(*args): + """Like issubclass, but will just return False if not a class.""" + + try: + if issubclass(*args): + return True + except TypeError: + pass + + return False + + +def import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + __import__(mod_str) + return getattr(sys.modules[mod_str], class_str) + +_slugify_strip_re = re.compile(r'[^\w\s-]') +_slugify_hyphenate_re = re.compile(r'[-\s]+') + + +# http://code.activestate.com/recipes/ +# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ +def slugify(value): + """ + Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. + + From Django's "django/template/defaultfilters.py". + """ + import unicodedata + if not isinstance(value, unicode): + value = unicode(value) + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicode(_slugify_strip_re.sub('', value).strip().lower()) + return _slugify_hyphenate_re.sub('-', value) + + +def http_log(logger, args, kwargs, resp, body): +# if not logger.isEnabledFor(logging.DEBUG): +# return + + string_parts = ['curl -i'] + for element in args: + if element in ('GET', 'POST'): + string_parts.append(' -X %s' % element) + else: + string_parts.append(' %s' % element) + + for element in kwargs['headers']: + header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) + string_parts.append(header) + + logger.debug("REQ: %s\n" % "".join(string_parts)) + if 'body' in kwargs and kwargs['body']: + logger.debug("REQ BODY: %s\n" % (kwargs['body'])) + logger.debug("RESP:%s\n", resp) + logger.debug("RESP BODY:%s\n", body) diff --git a/dashboard/windcclient/shell.py b/dashboard/windcclient/shell.py new file mode 100644 index 000000000..196c7a7b3 --- /dev/null +++ b/dashboard/windcclient/shell.py @@ -0,0 +1,285 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 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. + +""" +Command-line interface to the OpenStack LBaaS API. +""" + +import argparse +import httplib2 +import os +import sys +import logging + +from balancerclient.common import exceptions as exc +from balancerclient.common import utils +from balancerclient.v1 import shell as shell_v1 + + +LOG = logging.getLogger(__name__) + + +class OpenStackBalancerShell(object): + + def get_base_parser(self): + parser = argparse.ArgumentParser( + prog='balancer', + description=__doc__.strip(), + epilog='See "balancer 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('--debug', + default=False, + action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument('--os_username', + metavar='', + default=utils.env('OS_USERNAME'), + help='Defaults to env[OS_USERNAME]') + + parser.add_argument('--os_password', + metavar='', + default=utils.env('OS_PASSWORD'), + help='Defaults to env[OS_PASSWORD]') + + parser.add_argument('--os_tenant_name', + metavar='', + default=utils.env('OS_TENANT_NAME'), + help='Defaults to env[OS_TENANT_NAME]') + + parser.add_argument('--os_tenant_id', + metavar='', + default=utils.env('OS_TENANT_ID'), + help='Defaults to env[OS_TENANT_ID]') + + parser.add_argument('--os_auth_url', + metavar='', + default=utils.env('OS_AUTH_URL'), + help='Defaults to env[OS_AUTH_URL]') + + parser.add_argument('--os_region_name', + metavar='', + default=utils.env('OS_REGION_NAME'), + help='Defaults to env[OS_REGION_NAME]') + + parser.add_argument('--os_balancer_api_version', + metavar='', + default=utils.env('OS_BALANCER_API_VERSION', + 'KEYSTONE_VERSION'), + help='Defaults to env[OS_BALANCER_API_VERSION]' + ' or 2.0') + + parser.add_argument('--token', + metavar='', + default=utils.env('SERVICE_TOKEN'), + help='Defaults to env[SERVICE_TOKEN]') + + parser.add_argument('--endpoint', + metavar='', + default=utils.env('SERVICE_ENDPOINT'), + help='Defaults to env[SERVICE_ENDPOINT]') + + return parser + + def get_subcommand_parser(self, version): + parser = self.get_base_parser() + + self.subcommands = {} + subparsers = parser.add_subparsers(metavar='') + + try: + actions_module = { + '1': shell_v1, + }[version] + except KeyError: + actions_module = shell_v1 + + self._find_actions(subparsers, actions_module) + 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=OpenStackHelpFormatter) + 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 main(self, argv): + # Parse args once to find version + parser = self.get_base_parser() + (options, args) = parser.parse_known_args(argv) + + # build available subcommands based on version + api_version = options.os_balancer_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 not argv or options.help: + self.do_help(options) + return 0 + + # Parse args again and call whatever callback was selected + args = subcommand_parser.parse_args(argv) + + # Deal with global arguments + if args.debug: + httplib2.debuglevel = 1 + + # Short-circuit and deal with help command right away. + if args.func == self.do_help: + self.do_help(args) + return 0 + + #FIXME(usrleon): Here should be restrict for project id same as + # for username or apikey but for compatibility it is not. + + if not utils.isunauthenticated(args.func): + # if the user hasn't provided any auth data + if not (args.token or args.endpoint or args.os_username or + args.os_password or args.os_auth_url): + raise exc.CommandError('Expecting authentication method via \n' + ' either a service token, ' + '--token or env[SERVICE_TOKEN], \n' + ' or credentials, ' + '--os_username or env[OS_USERNAME].') + + # if it looks like the user wants to provide a service token + # but is missing something + if args.token or args.endpoint and not ( + args.token and args.endpoint): + if not args.token: + raise exc.CommandError( + 'Expecting a token provided via either --token or ' + 'env[SERVICE_TOKEN]') + + if not args.endpoint: + raise exc.CommandError( + 'Expecting an endpoint provided via either --endpoint ' + 'or env[SERVICE_ENDPOINT]') + + # if it looks like the user wants to provide a credentials + # but is missing something + if ((args.os_username or args.os_password or args.os_auth_url) + and not (args.os_username and args.os_password and + args.os_auth_url)): + if not args.os_username: + raise exc.CommandError( + 'Expecting a username provided via either ' + '--os_username or env[OS_USERNAME]') + + if not args.os_password: + raise exc.CommandError( + 'Expecting a password provided via either ' + '--os_password or env[OS_PASSWORD]') + + if not args.os_auth_url: + raise exc.CommandError( + 'Expecting an auth URL via either --os_auth_url or ' + 'env[OS_AUTH_URL]') + + if utils.isunauthenticated(args.func): + self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url) + else: + token = None + endpoint = None + if args.token and args.endpoint: + token = args.token + endpoint = args.endpoint + api_version = options.os_balancer_api_version + self.cs = self.get_api_class(api_version)( + username=args.os_username, + tenant_name=args.os_tenant_name, + tenant_id=args.os_tenant_id, + token=token, + endpoint=endpoint, + password=args.os_password, + auth_url=args.os_auth_url, + region_name=args.os_region_name) + + try: + args.func(self.cs, args) + except exc.Unauthorized: + raise exc.CommandError("Invalid OpenStack LBaaS credentials.") + except exc.AuthorizationFailure: + raise exc.CommandError("Unable to authorize user") + + def get_api_class(self, version): + try: + return { + "1": shell_v1.CLIENT_CLASS, + }[version] + except KeyError: + return shell_v1.CLIENT_CLASS + + @utils.arg('command', metavar='', nargs='?', + help='Display help for ') + 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: + 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 OpenStackBalancerShell().main(sys.argv[1:]) + except Exception, err: + LOG.exception("The operation executed with an error %r." % err) + raise diff --git a/dashboard/windcclient/v1/__init__.py b/dashboard/windcclient/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dashboard/windcclient/v1/client.py b/dashboard/windcclient/v1/client.py new file mode 100644 index 000000000..635bd4a3f --- /dev/null +++ b/dashboard/windcclient/v1/client.py @@ -0,0 +1,27 @@ +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from windcclient.common import client +from . import datacenters + + +class Client(object): + """Client for the WinDC v1 API.""" + + def __init__(self, **kwargs): + self.client = client.HTTPClient(**kwargs) + self.datacenters = datacenters.DCManager(self) diff --git a/dashboard/windcclient/v1/datacenters.py b/dashboard/windcclient/v1/datacenters.py new file mode 100644 index 000000000..6805be0aa --- /dev/null +++ b/dashboard/windcclient/v1/datacenters.py @@ -0,0 +1,49 @@ +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +from windcclient.common import base + + +class DC(base.Resource): + """Represent load balancer device instance.""" + + def __repr__(self): + return "" % self._info + + +class DCManager(base.Manager): + resource_class = DC + + def list(self): + return self._list('/datacenters', 'datacenters') + + def create(self, name, type, version, ip, port, user, password, **extra): + body = {'name': name, + 'type': type, + 'version': version, + 'ip': ip, + 'port': port, + 'user': user, + 'password': password} + body.update(extra) + return self._create('/devices', body, 'device') + + def delete(self, datacenter): + self._delete("/datacenters/%s" % base.getid(datacenter)) + + def get(self, datacenter): + return self._get("/datacenters/%s" % base.getid(datacenter), 'datacenter')