Added windc API client, sync repo with dev box.
This commit is contained in:
parent
1571d0775a
commit
6c51f7d983
@ -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
|
64
dashboard/api/windc.py
Normal file
64
dashboard/api/windc.py
Normal file
@ -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()
|
0
dashboard/windcclient/__init__.py
Normal file
0
dashboard/windcclient/__init__.py
Normal file
0
dashboard/windcclient/common/__init__.py
Normal file
0
dashboard/windcclient/common/__init__.py
Normal file
137
dashboard/windcclient/common/base.py
Normal file
137
dashboard/windcclient/common/base.py
Normal file
@ -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
|
148
dashboard/windcclient/common/client.py
Normal file
148
dashboard/windcclient/common/client.py
Normal file
@ -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)
|
140
dashboard/windcclient/common/exceptions.py
Normal file
140
dashboard/windcclient/common/exceptions.py
Normal file
@ -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)
|
62
dashboard/windcclient/common/service_catalog.py
Normal file
62
dashboard/windcclient/common/service_catalog.py
Normal file
@ -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.')
|
291
dashboard/windcclient/common/utils.py
Normal file
291
dashboard/windcclient/common/utils.py
Normal file
@ -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)
|
285
dashboard/windcclient/shell.py
Normal file
285
dashboard/windcclient/shell.py
Normal file
@ -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='<auth-user-name>',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help='Defaults to env[OS_USERNAME]')
|
||||
|
||||
parser.add_argument('--os_password',
|
||||
metavar='<auth-password>',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help='Defaults to env[OS_PASSWORD]')
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
metavar='<auth-tenant-name>',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help='Defaults to env[OS_TENANT_NAME]')
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
metavar='<tenant-id>',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help='Defaults to env[OS_TENANT_ID]')
|
||||
|
||||
parser.add_argument('--os_auth_url',
|
||||
metavar='<auth-url>',
|
||||
default=utils.env('OS_AUTH_URL'),
|
||||
help='Defaults to env[OS_AUTH_URL]')
|
||||
|
||||
parser.add_argument('--os_region_name',
|
||||
metavar='<region-name>',
|
||||
default=utils.env('OS_REGION_NAME'),
|
||||
help='Defaults to env[OS_REGION_NAME]')
|
||||
|
||||
parser.add_argument('--os_balancer_api_version',
|
||||
metavar='<balancer-api-version>',
|
||||
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='<service-token>',
|
||||
default=utils.env('SERVICE_TOKEN'),
|
||||
help='Defaults to env[SERVICE_TOKEN]')
|
||||
|
||||
parser.add_argument('--endpoint',
|
||||
metavar='<service-endpoint>',
|
||||
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='<subcommand>')
|
||||
|
||||
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='<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:
|
||||
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
|
0
dashboard/windcclient/v1/__init__.py
Normal file
0
dashboard/windcclient/v1/__init__.py
Normal file
27
dashboard/windcclient/v1/client.py
Normal file
27
dashboard/windcclient/v1/client.py
Normal file
@ -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)
|
49
dashboard/windcclient/v1/datacenters.py
Normal file
49
dashboard/windcclient/v1/datacenters.py
Normal file
@ -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 "<DC(%s)>" % 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')
|
Loading…
Reference in New Issue
Block a user