Switch client code towards OSLO

* Uses auth plugins - is still WIP but using a working one for now.
  Will have to re-visit this once it lands in keystone / oslo update.
* Resource and Manager abstractions.
* Python 3 compatability.
* Split the API/Bindings and Shell code
* Use entrypoints for versions
* Mandatory options are now positional
  Example: libra <cmd> --id <id> is now libra <cmd> <id>

Change-Id: I42275cc88be5689f040864e195e48c5ebacb2cea
This commit is contained in:
Endre Karlson
2013-10-17 13:37:50 +02:00
parent fda54cbc5b
commit 209fb0401c
37 changed files with 4449 additions and 992 deletions

View File

@@ -4,20 +4,20 @@ Usage
Synopsis Synopsis
-------- --------
:program:`libra_client` [:ref:`GENERAL OPTIONS <libra_client-options>`] [:ref:`COMMAND <libra_client-commands>`] [*COMMAND_OPTIONS*] :program:`libra` [:ref:`GENERAL OPTIONS <libra-options>`] [:ref:`COMMAND <libra-commands>`] [*COMMAND_OPTIONS*]
Description Description
----------- -----------
:program:`libra_client` is a utility designed to communicate with Atlas API :program:`libra` is a utility designed to communicate with Atlas API
based Load Balancer as a Service systems. based Load Balancer as a Service systems.
.. _libra_client-options: .. _libra-options:
Global Options Global Options
-------------- --------------
.. program:: libra_client .. program:: libra
.. option:: --help, -h .. option:: --help, -h
@@ -66,19 +66,19 @@ Global Options
The region the load balancer is located. Default is ``OS_REGION_NAME`` or The region the load balancer is located. Default is ``OS_REGION_NAME`` or
``LIBRA_REGION_NAME`` environment variables ``LIBRA_REGION_NAME`` environment variables
.. _libra_client-commands: .. _libra-commands:
Client Commands Client Commands
--------------- ---------------
.. program:: libra_client algorithms .. program:: libra algorithms
algorithms algorithms
^^^^^^^^^^ ^^^^^^^^^^
Gets a list of supported algorithms Gets a list of supported algorithms
.. program:: libra_client create .. program:: libra create
create create
^^^^^^ ^^^^^^
@@ -108,25 +108,25 @@ Create a load balancer
The virtual IP ID of an existing load balancer to attach to The virtual IP ID of an existing load balancer to attach to
.. program:: libra_client delete .. program:: libra delete
delete delete
^^^^^^ ^^^^^^
Delete a load balancer Delete a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. program:: libra_client limits .. program:: libra limits
limits limits
^^^^^^ ^^^^^^
Show the API limits for the user Show the API limits for the user
.. program:: libra_client list .. program:: libra list
list list
^^^^ ^^^^
@@ -137,14 +137,14 @@ List all load balancers
Show deleted load balancers Show deleted load balancers
.. program:: libra_client logs .. program:: libra logs
logs logs
^^^^ ^^^^
Send a snapshot of logs to an object store Send a snapshot of logs to an object store
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
@@ -164,14 +164,14 @@ Send a snapshot of logs to an object store
Object store authentication token Object store authentication token
.. program:: libra_client modify .. program:: libra modify
modify modify
^^^^^^ ^^^^^^
Update a load balancer's configuration Update a load balancer's configuration
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
@@ -183,47 +183,47 @@ Update a load balancer's configuration
A new algorithm for the load balancer A new algorithm for the load balancer
.. program:: libra_client monitor-list .. program:: libra monitor-list
monitor-list monitor-list
^^^^^^^^^^^^ ^^^^^^^^^^^^
List the health monitor for a load balancer List the health monitor for a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. program:: libra_client monitor-delete .. program:: libra monitor-delete
monitor-delete monitor-delete
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
Delete the health monitor for a load balancer Delete the health monitor for a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. program:: libra_client monitor-modify .. program:: libra monitor-modify
monitor-modify monitor-modify
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
Modify the health monitor for a load balancer Modify the health monitor for a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. program:: libra_client node-add .. program:: libra node-add
node-add node-add
^^^^^^^^ ^^^^^^^^
Add a node to a load balancer Add a node to a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
@@ -232,44 +232,44 @@ Add a node to a load balancer
The node address in ip:port format (can be used multiple times to add multiple nodes). The node address in ip:port format (can be used multiple times to add multiple nodes).
Additional node options may be specified after the ip:port portion in a option=value format. Additional node options may be specified after the ip:port portion in a option=value format.
.. program:: libra_client node-delete .. program:: libra node-delete
node-delete node-delete
^^^^^^^^^^^ ^^^^^^^^^^^
Delete a node from the load balancer Delete a node from the load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. option:: --nodeid <nodeid> .. option:: <nodeid>
The ID of the node to be removed The ID of the node to be removed
.. program:: libra_client node-list .. program:: libra node-list
node-list node-list
^^^^^^^^^ ^^^^^^^^^
List the nodes in a load balancer List the nodes in a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. program:: libra_client node-modify .. program:: libra node-modify
node-modify node-modify
^^^^^^^^^^^ ^^^^^^^^^^^
Modify a node's state in a load balancer Modify a node's state in a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. option:: --nodeid <nodeid> .. option:: <nodeid>
The ID of the node to be modified The ID of the node to be modified
@@ -277,35 +277,35 @@ Modify a node's state in a load balancer
The new state of the node (either ENABLED or DISABLED) The new state of the node (either ENABLED or DISABLED)
.. program:: libra_client node-status .. program:: libra node-status
node-status node-show
^^^^^^^^^^^ ^^^^^^^^^
Get the status of a node in a load balancer Get the status of a node in a load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
.. option:: --nodeid <nodeid> .. option:: <nodeid>
The ID of the node in the load balancer The ID of the node in the load balancer
.. program:: libra_client protocols .. program:: libra protocols
protocols protocols
^^^^^^^^^ ^^^^^^^^^
Gets a list of supported protocols Gets a list of supported protocols
.. program:: libra_client status .. program:: libra show
status show
^^^^^^ ^^^^
Get the status of a single load balancer Get the status of a single load balancer
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer
@@ -314,6 +314,6 @@ virtualips
Get a list of virtual IPs Get a list of virtual IPs
.. option:: --id <id> .. option:: <id>
The ID of the load balancer The ID of the load balancer

View File

@@ -6,7 +6,7 @@ Create Load Balancer
.. code-block:: bash .. code-block:: bash
libra_client --os_auth_url=https://company.com/openstack/auth/url \ libra --os_auth_url=https://company.com/openstack/auth/url \
--os_username=username --os_password=pasword --os_tenant_name=tenant \ --os_username=username --os_password=pasword --os_tenant_name=tenant \
--os_region_name=region create --name=my_load_balancer \ --os_region_name=region create --name=my_load_balancer \
--node 192.168.1.1:80 --node 192.168.1.2:80 --node 192.168.1.1:80 --node 192.168.1.2:80
@@ -33,7 +33,7 @@ Create a Load Balancer with Node Options
.. code-block:: bash .. code-block:: bash
libra_client --os_auth_url=https://company.com/openstack/auth/url \ libra --os_auth_url=https://company.com/openstack/auth/url \
--os_username=username --os_password=pasword --os_tenant_name=tenant \ --os_username=username --os_password=pasword --os_tenant_name=tenant \
--os_region_name=region create --name=my_load_balancer \ --os_region_name=region create --name=my_load_balancer \
--node 192.168.1.1:80:weight=1 --node 192.168.1.2:80:weight=2 --node 192.168.1.1:80:weight=1 --node 192.168.1.2:80:weight=2
@@ -51,7 +51,7 @@ to the load balancer we created previously:
.. code-block:: bash .. code-block:: bash
libra_client --os_auth_url=https://company.com/openstack/auth/url \ libra --os_auth_url=https://company.com/openstack/auth/url \
--os_username=username --os_password=pasword --os_tenant_name=tenant \ --os_username=username --os_password=pasword --os_tenant_name=tenant \
--os_region_name=region create --name=my_load_balancer \ --os_region_name=region create --name=my_load_balancer \
--node 192.168.1.1:443 --node 192.168.1.2:443 --protocol=TCP --port=443 \ --node 192.168.1.1:443 --node 192.168.1.2:443 --protocol=TCP --port=443 \
@@ -79,9 +79,9 @@ Add a Node
.. code-block:: bash .. code-block:: bash
libra_client --os_auth_url=https://company.com/openstack/auth/url \ libra --os_auth_url=https://company.com/openstack/auth/url \
--os_username=username --os_password=pasword --os_tenant_name=tenant \ --os_username=username --os_password=pasword --os_tenant_name=tenant \
--os_region_name=region node-add --id=1158 --node=192.168.1.3:443 --os_region_name=region node-add 158 --node=192.168.1.3:443
In this example we have take the ID of the load balancer of the previos example In this example we have take the ID of the load balancer of the previos example
to add a web server to. The result should look something like this: to add a web server to. The result should look something like this:

View File

@@ -11,3 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import pbr.version
__version__ = pbr.version.VersionInfo('python-libraclient').version_string()

View File

@@ -12,47 +12,27 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import sys from libraclient.openstack.common.apiclient import exceptions
import os
from libraapi import LibraAPI import pkg_resources
from clientoptions import ClientOptions
from novaclient import exceptions
def main(): def _find_versions():
options = ClientOptions() versions = {}
args = options.run() for e in list(pkg_resources.iter_entry_points('libraclient.versions')):
versions[e.name] = (e.module_name, e.load())
return versions
required_args = [
'os_auth_url', 'os_username', 'os_password', 'os_tenant_name',
'os_region_name'
]
missing_args = 0
for req in required_args:
test_var = getattr(args, req)
if test_var == '':
missing_args += 1
sys.stderr.write(
'{app}: error: argument --{test_var} is required\n'
.format(app=os.path.basename(sys.argv[0]), test_var=req))
if missing_args:
return 2
api = LibraAPI(args)
cmd = args.command.replace('-', '_')
method = getattr(api, '{cmd}_lb'.format(cmd=cmd))
def get_version(version):
versions = _find_versions()
try: try:
method(args) return versions[version][1]
except exceptions.ClientException as exc: except KeyError:
print exc raise exceptions.UnsupportedVersion('%s is not a supported version.' %
if exc.details: version)
print exc.details
except exceptions.EndpointNotFound:
return 2
except Exception as exc:
print exc
return 2
return 0
def VersionedClient(version, *args, **kw):
cls = get_version(version)
return cls(*args, **kw)

View File

@@ -1,220 +0,0 @@
# Copyright 2012 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
from novaclient import utils
LIBRA_DEFAULT_SERVICE_TYPE = 'hpext:lbaas'
class ClientOptions(object):
def __init__(self):
self.options = argparse.ArgumentParser('Libra command line client')
def _generate(self):
self.options.add_argument(
'--os_auth_url',
metavar='<auth-url>',
default=utils.env('OS_AUTH_URL', 'LIBRA_URL'),
help='Authentication URL'
)
self.options.add_argument(
'--os_username',
metavar='<auth-user-name>',
default=utils.env('OS_USERNAME', 'LIBRA_USERNAME'),
help='Authentication username'
)
self.options.add_argument(
'--os_password',
metavar='<auth-password>',
default=utils.env('OS_PASSWORD', 'LIBRA_PASSWORD'),
help='Authentication password'
)
self.options.add_argument(
'--os_tenant_name',
metavar='<auth-tenant-name>',
default=utils.env('OS_TENANT_NAME', 'LIBRA_PROJECT_ID'),
help='Authentication tenant'
)
self.options.add_argument(
'--os_region_name',
metavar='<region-name>',
default=utils.env('OS_REGION_NAME', 'LIBRAL_REGION_NAME'),
help='Authentication region'
)
self.options.add_argument(
'--debug',
action='store_true',
help='Debug network messages'
)
self.options.add_argument(
'--insecure',
action='store_true',
help='Don\'t verify SSL cert'
)
self.options.add_argument(
'--bypass_url',
help='Use this API endpoint instead of the Service Catalog'
)
self.options.add_argument(
'--service_type',
default=LIBRA_DEFAULT_SERVICE_TYPE,
help='Default ' + LIBRA_DEFAULT_SERVICE_TYPE
)
subparsers = self.options.add_subparsers(
metavar='<subcommand>', dest='command'
)
subparsers.add_parser(
'algorithms', help='get a list of supported algorithms'
)
sp = subparsers.add_parser(
'create', help='create a load balancer'
)
sp.add_argument('--name', help='name for the load balancer',
required=True)
sp.add_argument('--port',
help='port for the load balancer, 80 is default')
sp.add_argument('--protocol',
help='protocol for the load balancer, HTTP is default',
choices=['HTTP', 'TCP', 'GALERA'])
sp.add_argument('--algorithm',
help='algorithm for the load balancer,'
' ROUND_ROBIN is default',
choices=['LEAST_CONNECTIONS', 'ROUND_ROBIN'])
sp.add_argument('--node',
help='a node for the load balancer in ip:port format',
action='append', required=True)
sp.add_argument('--vip',
help='the virtual IP to attach the load balancer to')
sp = subparsers.add_parser(
'delete', help='delete a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
subparsers.add_parser(
'limits', help='get account API usage limits'
)
sp = subparsers.add_parser(
'list', help='list load balancers'
)
sp.add_argument(
'--deleted', help='list deleted load balancers',
action='store_true'
)
sp = subparsers.add_parser(
'logs', help='send a snapshot of logs to an object store'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp.add_argument('--storage', help='storage type', choices=['Swift'])
sp.add_argument('--endpoint', help='object store endpoint to use')
sp.add_argument('--basepath', help='object store based directory')
sp.add_argument('--token', help='object store authentication token')
sp = subparsers.add_parser(
'modify', help='modify a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp.add_argument('--name', help='new name for the load balancer')
sp.add_argument('--algorithm',
help='new algorithm for the load balancer',
choices=['LEAST_CONNECTIONS', 'ROUND_ROBIN'])
sp = subparsers.add_parser(
'monitor-list',
help='list health monitor information'
)
sp.add_argument('--id', required=True, help='load balancer ID')
sp = subparsers.add_parser(
'monitor-delete',
help='delete a health monitor'
)
sp.add_argument('--id', required=True, help='load balancer ID')
sp = subparsers.add_parser(
'monitor-modify',
help='modify a health monitor'
)
sp.add_argument('--id', required=True, help='load balancer ID')
sp.add_argument('--type', choices=['CONNECT', 'HTTP'],
default='CONNECT', help='health monitor type')
sp.add_argument('--delay', type=int, default=30, metavar='SECONDS',
help='time between health monitor calls')
sp.add_argument('--timeout', type=int, default=30, metavar='SECONDS',
help='time to wait before monitor times out')
sp.add_argument('--attempts', type=int, default=2, metavar='COUNT',
help='connection attempts before marking node as bad')
sp.add_argument('--path',
help='URI path for health check')
sp = subparsers.add_parser(
'node-add', help='add node to a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp.add_argument('--node', help='node to add in ip:port form',
required=True, action='append')
sp = subparsers.add_parser(
'node-delete', help='delete node from a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp.add_argument('--nodeid',
help='node ID to remove from load balancer',
required=True)
sp = subparsers.add_parser(
'node-list', help='list nodes in a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp = subparsers.add_parser(
'node-modify', help='modify node in a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp.add_argument('--nodeid', help='node ID to modify', required=True)
sp.add_argument('--condition', help='the new state for the node',
choices=['ENABLED', 'DISABLED'])
sp.add_argument('--weight', type=int, default=1, metavar='COUNT',
help='node weight ratio as compared to other nodes')
sp = subparsers.add_parser(
'node-status', help='get status of a node in a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp.add_argument('--nodeid', help='node ID to get status from',
required=True)
subparsers.add_parser(
'protocols', help='get a list of supported protocols and ports'
)
sp = subparsers.add_parser(
'status', help='get status of a load balancer'
)
sp.add_argument('--id', help='load balancer ID', required=True)
sp = subparsers.add_parser(
'virtualips', help='get a list of virtual IPs'
)
sp.add_argument('--id', help='load balancer ID', required=True)
def run(self):
self._generate()
return self.options.parse_args()

View File

@@ -1,319 +0,0 @@
# Copyright 2012 Hewlett-Packard Development Company, L.P.
#
# 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 prettytable
import novaclient
import socket
import logging
from novaclient import client
# NOTE(LinuxJedi): Override novaclient's error handler as we send messages in
# a slightly different format which causes novaclient's to throw an exception
def from_response(response, body, url, method=None):
"""
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 = novaclient.exceptions._code_map.get(
response.status_code, novaclient.exceptions.ClientException
)
if response.headers:
request_id = response.headers.get('x-compute-request-id')
else:
request_id = None
if body:
message = "n/a"
details = "n/a"
if hasattr(body, 'keys'):
message = body.get('faultstring', None)
if not message:
message = body.get('message', None)
details = body.get('details', None)
return cls(code=response.status_code, message=message, details=details,
request_id=request_id, url=url, method=method)
else:
return cls(code=response.status_code, request_id=request_id, url=url,
method=method)
novaclient.exceptions.from_response = from_response
class LibraAPI(object):
def __init__(self, args):
if args.debug:
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
self.nova = client.HTTPClient(
args.os_username,
args.os_password,
args.os_tenant_name,
args.os_auth_url,
region_name=args.os_region_name,
service_type=args.service_type,
http_log_debug=args.debug,
insecure=args.insecure,
bypass_url=args.bypass_url
)
def limits_lb(self, args):
resp, body = self._get('/limits')
# Work around the fact that limits is missing from HP's API server
if 'rate' in body['limits']:
column_names = ['Verb', 'Value', 'Remaining', 'Unit',
'Next Available']
columns = ['verb', 'value', 'remaining', 'unit', 'next-available']
self._render_list(column_names, columns,
body['limits']['rate']['values']['limit'])
column_names = ['Values']
columns = ['values']
self._render_dict(column_names, columns, body['limits']['absolute'])
def protocols_lb(self, args):
resp, body = self._get('/protocols')
column_names = ['Name', 'Port']
columns = ['name', 'port']
self._render_list(column_names, columns, body['protocols'])
def algorithms_lb(self, args):
resp, body = self._get('/algorithms')
column_names = ['Name']
columns = ['name']
self._render_list(column_names, columns, body['algorithms'])
def list_lb(self, args):
if args.deleted:
resp, body = self._get('/loadbalancers?status=DELETED')
else:
resp, body = self._get('/loadbalancers')
column_names = ['Name', 'ID', 'Protocol', 'Port', 'Algorithm',
'Status', 'Created', 'Updated', 'Node Count']
columns = ['name', 'id', 'protocol', 'port', 'algorithm', 'status',
'created', 'updated', 'nodeCount']
self._render_list(column_names, columns, body['loadBalancers'])
def status_lb(self, args):
resp, body = self._get('/loadbalancers/{0}'.format(args.id))
column_names = ['ID', 'Name', 'Protocol', 'Port', 'Algorithm',
'Status', 'Status Description', 'Created', 'Updated',
'IPs', 'Nodes', 'Persistence Type',
'Connection Throttle', 'Node Count']
columns = ['id', 'name', 'protocol', 'port', 'algorithm', 'status',
'statusDescription', 'created', 'updated', 'virtualIps',
'nodes', 'sessionPersistence', 'connectionThrottle',
'nodeCount']
if 'sessionPersistence' not in body:
body['sessionPersistence'] = 'None'
if 'connectionThrottle' not in body:
body['connectionThrottle'] = 'None'
if 'statusDescription' in body:
body['statusDescription'] = body['statusDescription'].rstrip()
else:
body['statusDescription'] = 'None'
self._render_dict(column_names, columns, body)
def virtualips_lb(self, args):
resp, body = self._get('/loadbalancers/{0}/virtualips'.format(args.id))
column_names = ['ID', 'Address', 'Type', 'IP Version']
columns = ['id', 'address', 'type', 'ipVersion']
self._render_list(column_names, columns, body['virtualIps'])
def delete_lb(self, args):
self._delete('/loadbalancers/{0}'.format(args.id))
def create_lb(self, args):
data = {}
data['name'] = args.name
if args.port is not None:
data['port'] = args.port
if args.protocol is not None:
data['protocol'] = args.protocol
if args.algorithm is not None:
data['algorithm'] = args.algorithm
data['nodes'] = self._parse_nodes(args.node)
if args.vip is not None:
data['virtualIps'] = [{'id': args.vip}]
resp, body = self._post('/loadbalancers', body=data)
column_names = ['ID', 'Name', 'Protocol', 'Port', 'Algorithm',
'Status', 'Created', 'Updated', 'IPs', 'Nodes']
columns = ['id', 'name', 'protocol', 'port', 'algorithm', 'status',
'created', 'updated', 'virtualIps', 'nodes']
self._render_dict(column_names, columns, body)
def modify_lb(self, args):
data = {}
if args.name is not None:
data['name'] = args.name
if args.algorithm is not None:
data['algorithm'] = args.algorithm
self._put('/loadbalancers/{0}'.format(args.id), body=data)
def node_list_lb(self, args):
resp, body = self._get('/loadbalancers/{0}/nodes'.format(args.id))
column_names = ['ID', 'Address', 'Port', 'Condition', 'Weight',
'Status']
columns = ['id', 'address', 'port', 'condition', 'weight', 'status']
self._render_list(column_names, columns, body['nodes'])
def node_delete_lb(self, args):
self._delete('/loadbalancers/{0}/nodes/{1}'
.format(args.id, args.nodeid))
def node_add_lb(self, args):
data = {}
data['nodes'] = self._parse_nodes(args.node)
resp, body = self._post('/loadbalancers/{0}/nodes'
.format(args.id), body=data)
column_names = ['ID', 'Address', 'Port', 'Condition', 'Weight',
'Status']
columns = ['id', 'address', 'port', 'condition', 'weight', 'status']
self._render_list(column_names, columns, body['nodes'])
def node_modify_lb(self, args):
data = {'condition': args.condition, 'weight': args.weight}
self._put('/loadbalancers/{0}/nodes/{1}'
.format(args.id, args.nodeid), body=data)
def node_status_lb(self, args):
resp, body = self._get('/loadbalancers/{0}/nodes/{1}'
.format(args.id, args.nodeid))
column_names = ['ID', 'Address', 'Port', 'Condition',
'Weight', 'Status']
columns = ['id', 'address', 'port', 'condition', 'weight', 'status']
self._render_dict(column_names, columns, body)
def logs_lb(self, args):
data = {}
if args.storage:
data['objectStoreType'] = args.storage
if args.endpoint:
data['objectStoreEndpoint'] = args.endpoint
if args.basepath:
data['objectStoreBasePath'] = args.basepath
if args.token:
data['authToken'] = args.token
resp, body = self._post('/loadbalancers/{0}/logs'.format(args.id),
body=data)
def monitor_delete_lb(self, args):
resp, body = self._delete('/loadbalancers/{0}/healthmonitor'
.format(args.id))
def monitor_list_lb(self, args):
resp, body = self._get('/loadbalancers/{0}/healthmonitor'
.format(args.id))
column_names = ['Type', 'Delay', 'Timeout', 'Attempts', 'Path']
columns = ['type', 'delay', 'timeout', 'attemptsBeforeDeactivation',
'path']
self._render_dict(column_names, columns, body or {})
def monitor_modify_lb(self, args):
data = {}
data['type'] = args.type
data['delay'] = args.delay
data['timeout'] = args.timeout
data['attemptsBeforeDeactivation'] = args.attempts
if args.type.upper() != "CONNECT":
data['path'] = args.path
resp, body = self._put('/loadbalancers/{0}/healthmonitor'
.format(args.id), body=data)
def _render_list(self, column_names, columns, data):
table = prettytable.PrettyTable(column_names)
for item in data:
row = []
for column in columns:
if column in item:
rdata = item[column]
else:
rdata = ''
row.append(rdata)
table.add_row(row)
print table
def _render_dict(self, column_names, columns, data):
table = prettytable.PrettyTable(column_names)
row = []
for column in columns:
if column in data:
rdata = data[column]
else:
rdata = ''
row.append(rdata)
table.add_row(row)
print table
def _get(self, url, **kwargs):
return self.nova.get(url, **kwargs)
def _post(self, url, **kwargs):
return self.nova.post(url, **kwargs)
def _put(self, url, **kwargs):
return self.nova.put(url, **kwargs)
def _delete(self, url, **kwargs):
return self.nova.delete(url, **kwargs)
def _parse_nodes(self, nodes):
out_nodes = []
try:
for node in nodes:
nodeopts = node.split(':')
ipaddr = nodeopts[0]
port = nodeopts[1]
weight, backup = None, None
# Test IP valid
# TODO: change to pton when we want to support IPv6
socket.inet_aton(ipaddr)
# Test port valid
if int(port) < 0 or int(port) > 65535:
raise Exception('Port out of range')
# Process the rest of the node options as key=value
for kv in nodeopts[2:]:
key, value = kv.split('=')
key = key.lower()
value = value.upper()
if key == 'weight':
weight = int(value)
elif key == 'backup':
backup = value # 'TRUE' or 'FALSE'
else:
raise Exception("Unknown node option '%s'" % key)
node_def = {'address': ipaddr, 'port': port}
if weight:
node_def['weight'] = weight
if backup:
node_def['backup'] = backup
out_nodes.append(node_def)
except Exception as e:
raise Exception("Invalid value specified for --node: %s" % e)
return out_nodes

View File

@@ -0,0 +1,13 @@
# Copyright 2012 Hewlett-Packard Development Company, L.P.
#
# 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.

View File

@@ -0,0 +1,13 @@
# Copyright 2012 Hewlett-Packard Development Company, L.P.
#
# 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.

View File

@@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation
# 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.

View File

@@ -0,0 +1,228 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Spanish National Research Council.
# 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.
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import abc
import argparse
import logging
import os
import six
from stevedore import extension
from libraclient.openstack.common.apiclient import exceptions
logger = logging.getLogger(__name__)
_discovered_plugins = {}
def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
global _discovered_plugins
_discovered_plugins = {}
def add_plugin(ext):
_discovered_plugins[ext.name] = ext.plugin
ep_namespace = "openstack.common.apiclient.auth"
mgr = extension.ExtensionManager(ep_namespace)
if mgr.extensions:
mgr.map(add_plugin)
def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
group = parser.add_argument_group("Common auth options")
BaseAuthPlugin.add_common_opts(group)
for name, auth_plugin in _discovered_plugins.iteritems():
group = parser.add_argument_group(
"Auth-system '%s' options" % name,
conflict_handler="resolve")
auth_plugin.add_opts(group)
def load_plugin(auth_system):
try:
plugin_class = _discovered_plugins[auth_system]
except KeyError:
raise exceptions.AuthSystemNotFound(auth_system)
return plugin_class(auth_system=auth_system)
def load_plugin_from_args(args):
"""Load requred plugin and populate it with options.
Try to guess auth system if it is not specified. Systems are tried in
alphabetical order.
:type args: argparse.Namespace
:raises: AuthorizationFailure
"""
auth_system = args.os_auth_system
if auth_system:
plugin = load_plugin(auth_system)
plugin.parse_opts(args)
plugin.sufficient_options()
return plugin
for plugin_auth_system in sorted(_discovered_plugins.iterkeys()):
plugin_class = _discovered_plugins[plugin_auth_system]
plugin = plugin_class()
plugin.parse_opts(args)
try:
plugin.sufficient_options()
except exceptions.AuthPluginOptionsMissing:
continue
return plugin
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
@six.add_metaclass(abc.ABCMeta)
class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
auth_system = None
opt_names = []
common_opt_names = [
"auth_system",
"username",
"password",
"tenant_name",
"token",
"auth_url",
]
def __init__(self, auth_system=None, **kwargs):
self.auth_system = auth_system or self.auth_system
self.opts = dict((name, kwargs.get(name))
for name in self.opt_names)
@staticmethod
def _parser_add_opt(parser, opt):
"""Add an option to parser in two variants.
:param opt: option name (with underscores)
"""
dashed_opt = opt.replace("_", "-")
env_var = "OS_%s" % opt.upper()
arg_default = os.environ.get(env_var, "")
arg_help = "Defaults to env[%s]." % env_var
parser.add_argument(
"--os-%s" % dashed_opt,
metavar="<%s>" % dashed_opt,
default=arg_default,
help=arg_help)
parser.add_argument(
"--os_%s" % opt,
metavar="<%s>" % dashed_opt,
help=argparse.SUPPRESS)
@classmethod
def add_opts(cls, parser):
"""Populate the parser with the options for this plugin.
"""
for opt in cls.opt_names:
# use `BaseAuthPlugin.common_opt_names` since it is never
# changed in child classes
if opt not in BaseAuthPlugin.common_opt_names:
cls._parser_add_opt(parser, opt)
@classmethod
def add_common_opts(cls, parser):
"""Add options that are common for several plugins.
"""
for opt in cls.common_opt_names:
cls._parser_add_opt(parser, opt)
@staticmethod
def get_opt(opt_name, args):
"""Return option name and value.
:param opt_name: name of the option, e.g., "username"
:param args: parsed arguments
"""
return (opt_name, getattr(args, "os_%s" % opt_name, None))
def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute `self.opts` with a
dict containing the options and values needed to make authentication.
"""
self.opts.update(dict(self.get_opt(opt_name, args)
for opt_name in self.opt_names))
def authenticate(self, http_client):
"""Authenticate using plugin defined method.
The method usually analyses `self.opts` and performs
a request to authentication server.
:param http_client: client object that needs authentication
:type http_client: HTTPClient
:raises: AuthorizationFailure
"""
self.sufficient_options()
self._do_authenticate(http_client)
@abc.abstractmethod
def _do_authenticate(self, http_client):
"""Protected method for authentication.
"""
def sufficient_options(self):
"""Check if all required options are present.
:raises: AuthPluginOptionsMissing
"""
missing = [opt
for opt in self.opt_names
if not self.opts.get(opt)]
if missing:
raise exceptions.AuthPluginOptionsMissing(missing)
@abc.abstractmethod
def token_and_endpoint(self, endpoint_type, service_type):
"""Return token and endpoint.
:param service_type: Service type of the endpoint
:type service_type: string
:param endpoint_type: Type of endpoint.
Possible values: public or publicURL,
internal or internalURL,
admin or adminURL
:type endpoint_type: string
:returns: tuple of token and endpoint strings
:raises: EndpointException
"""

View File

@@ -0,0 +1,544 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# 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.
"""
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import six
import urllib
from libraclient.openstack.common.apiclient import exceptions
from libraclient.openstack.common import strutils
def getid(obj):
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
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):
"""Run all hooks of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param **args: args to be passed to every hook function
:param **kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
class BaseManager(HookableMixin):
"""Basic manager type providing common operations.
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, client):
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client
def _extract_data(self, data, response_key=None):
"""
Extract the data within response_key in data.
:param data: The data to handle.
:param response_key: The response key to extract.
"""
return data[response_key] if response_key else data
def _make_obj(self, data, obj_class=None):
"""
Make a object out of obj_class.
:param data: The data to handle.
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
"""
cls = obj_class or self.resource_class
return cls(self, data, loaded=True)
def _list(self, url, response_key=None, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key=None, return_raw=False, obj_class=None):
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
# NOTE(ekarlso): If the response key is None, then we just return the body.
body = self.client.get(url).json()
data = self._extract_data(body, response_key=response_key)
if return_raw:
return data
else:
return self._make_obj(data, obj_class=obj_class)
def _head(self, url):
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key=None, return_raw=False,
obj_class=None):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
data = self._extract_data(body, response_key=response_key)
if return_raw:
return data
else:
return self._make_obj(data, obj_class=obj_class)
def _put(self, url, json=None, response_key=None, return_raw=False,
obj_class=None):
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
data = self._extract_data(body, response_key=response_key)
if return_raw:
return data
else:
return self._make_obj(data, obj_class=obj_class)
def _patch(self, url, json=None, response_key=None, return_raw=False,
obj_class=None):
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.patch(url, json=json).json()
data = self._extract_data(body, response_key=response_key)
if return_raw:
return data
else:
return self._make_obj(data, obj_class=obj_class)
def _delete(self, url):
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(BaseManager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in kwargs.copy().iteritems():
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(404, msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]
class Extension(HookableMixin):
"""Extension descriptor."""
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None
def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass
def __repr__(self):
return "<Extension '%s'>" % self.name
class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
"""
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion.
"""
if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
return strutils.to_slug(getattr(self, self.NAME_ATTR))
return None
def _add_details(self, info):
for (k, v) in info.iteritems():
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
#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 get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val

View File

@@ -0,0 +1,366 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2011 Piston Cloud Computing, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# 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.
"""
OpenStack Client interface. Handles the REST calls and responses.
"""
# E0202: An attribute inherited from %s hide this method
# pylint: disable=E0202
import logging
import time
try:
import simplejson as json
except ImportError:
import json
import requests
from libraclient.openstack.common.apiclient import exceptions
from libraclient.openstack.common import importutils
_logger = logging.getLogger(__name__)
class HTTPClient(object):
"""
This client handles sending HTTP requests to OpenStack servers.
Features:
- share authentication information between several clients to different
services (e.g., for compute and image clients);
- reissue authentication request for expired tokens;
- encode/decode JSON bodies;
- raise exeptions on HTTP errors;
- pluggable authentication;
- store authentication information in a keyring;
- store time spent for requests;
- register clients for particular services, so one can use
`http_client.identity` or `http_client.compute`;
- log requests and responses in a format that is easy to copy-and-paste
into terminal and send the same request with curl.
"""
user_agent = "libraclient.openstack.common.apiclient"
def __init__(self,
auth_plugin,
region_name=None,
endpoint_type="publicURL",
original_ip=None,
verify=True,
cert=None,
timeout=None,
timings=False,
keyring_saver=None,
debug=False,
user_agent=None,
http=None):
self.auth_plugin = auth_plugin
self.endpoint_type = endpoint_type
self.region_name = region_name
self.original_ip = original_ip
self.timeout = timeout
self.verify = verify
self.cert = cert
self.keyring_saver = keyring_saver
self.debug = debug
self.user_agent = user_agent or self.user_agent
self.times = [] # [("item", starttime, endtime), ...]
self.timings = timings
# requests within the same session can reuse TCP connections from pool
self.http = http or requests.Session()
self.cached_token = None
def _http_log_req(self, method, url, kwargs):
if not self.debug:
return
string_parts = [
"curl -i",
"-X '%s'" % method,
"'%s'" % url,
]
for element in kwargs['headers']:
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
string_parts.append(header)
_logger.debug("REQ: %s" % " ".join(string_parts))
if 'data' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
def _http_log_resp(self, resp):
if not self.debug:
return
_logger.debug(
"RESP: [%s] %s\n",
resp.status_code,
resp.headers)
if resp._content_consumed:
_logger.debug(
"RESP BODY: %s\n",
resp.text)
def serialize(self, kwargs):
if kwargs.get('json') is not None:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['data'] = json.dumps(kwargs['json'])
try:
del kwargs['json']
except KeyError:
pass
def get_timings(self):
return self.times
def reset_timings(self):
self.times = []
def request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
setting headers, JSON encoding/decoding, and error handling.
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
"""
kwargs.setdefault("headers", kwargs.get("headers", {}))
kwargs["headers"]["User-Agent"] = self.user_agent
if self.original_ip:
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
self.original_ip, self.user_agent)
if self.timeout is not None:
kwargs.setdefault("timeout", self.timeout)
kwargs.setdefault("verify", self.verify)
if self.cert is not None:
kwargs.setdefault("cert", self.cert)
self.serialize(kwargs)
self._http_log_req(method, url, kwargs)
if self.timings:
start_time = time.time()
resp = self.http.request(method, url, **kwargs)
if self.timings:
self.times.append(("%s %s" % (method, url),
start_time, time.time()))
self._http_log_resp(resp)
if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
resp.status_code)
raise exceptions.from_response(resp, method, url)
return resp
@staticmethod
def concat_url(endpoint, url):
"""Concatenate endpoint and final URL.
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
"http://keystone/v2.0/tokens".
:param endpoint: the base URL
:param url: the final URL
"""
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
def client_request(self, client, method, url, **kwargs):
"""Send an http request using `client`'s endpoint and specified `url`.
If request was rejected as unauthorized (possibly because the token is
expired), issue one authorization attempt and send the request once
again.
:param client: instance of BaseClient descendant
:param method: method of HTTP request
:param url: URL of HTTP request
:param kwargs: any other parameter that can be passed to
' `HTTPClient.request`
"""
filter_args = {
"endpoint_type": client.endpoint_type or self.endpoint_type,
"service_type": client.service_type,
}
token, endpoint = (self.cached_token, client.cached_endpoint)
just_authenticated = False
if not (token and endpoint):
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
pass
if not (token and endpoint):
self.authenticate()
just_authenticated = True
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
if not (token and endpoint):
raise exceptions.AuthorizationFailure(
"Cannot find endpoint or token for request")
old_token_endpoint = (token, endpoint)
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
self.cached_token = token
client.cached_endpoint = endpoint
# Perform the request once. If we get Unauthorized, then it
# might be because the auth token expired, so try to
# re-authenticate and try again. If it still fails, bail.
try:
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
except exceptions.Unauthorized as unauth_ex:
if just_authenticated:
raise
self.cached_token = None
client.cached_endpoint = None
self.authenticate()
try:
token, endpoint = self.auth_plugin.token_and_endpoint(
**filter_args)
except exceptions.EndpointException:
raise unauth_ex
if (not (token and endpoint) or
old_token_endpoint == (token, endpoint)):
raise unauth_ex
self.cached_token = token
client.cached_endpoint = endpoint
kwargs["headers"]["X-Auth-Token"] = token
return self.request(
method, self.concat_url(endpoint, url), **kwargs)
def add_client(self, base_client_instance):
"""Add a new instance of :class:`BaseClient` descendant.
`self` will store a reference to `base_client_instance`.
Example:
>>> def test_clients():
... from keystoneclient.auth import keystone
... from openstack.common.apiclient import client
... auth = keystone.KeystoneAuthPlugin(
... username="user", password="pass", tenant_name="tenant",
... auth_url="http://auth:5000/v2.0")
... openstack_client = client.HTTPClient(auth)
... # create nova client
... from novaclient.v1_1 import client
... client.Client(openstack_client)
... # create keystone client
... from keystoneclient.v2_0 import client
... client.Client(openstack_client)
... # use them
... openstack_client.identity.tenants.list()
... openstack_client.compute.servers.list()
"""
service_type = base_client_instance.service_type
if service_type and not hasattr(self, service_type):
setattr(self, service_type, base_client_instance)
def authenticate(self):
self.auth_plugin.authenticate(self)
# Store the authentication results in the keyring for later requests
if self.keyring_saver:
self.keyring_saver.save(self)
class BaseClient(object):
"""Top-level object to access the OpenStack API.
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
will handle a bunch of issues such as authentication.
"""
service_type = None
endpoint_type = None # "publicURL" will be used
cached_endpoint = None
def __init__(self, http_client, service_type=None, endpoint_type=None,
extensions=None):
self.service_type = service_type
self.endpoint_type = endpoint_type
self.http_client = http_client
http_client.add_client(self)
# Add in any extensions...
if extensions:
for extension in extensions:
if extension.manager_class:
setattr(self, extension.name,
extension.manager_class(self))
def client_request(self, method, url, **kwargs):
return self.http_client.client_request(
self, method, url, **kwargs)
def head(self, url, **kwargs):
return self.client_request("HEAD", url, **kwargs)
def get(self, url, **kwargs):
return self.client_request("GET", url, **kwargs)
def post(self, url, **kwargs):
return self.client_request("POST", url, **kwargs)
def put(self, url, **kwargs):
return self.client_request("PUT", url, **kwargs)
def delete(self, url, **kwargs):
return self.client_request("DELETE", url, **kwargs)
def patch(self, url, **kwargs):
return self.client_request("PATCH", url, **kwargs)
@staticmethod
def get_class(api_name, version, version_map):
"""Returns the client class for the requested API version
:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = "Invalid %s client version '%s'. must be one of: %s" % (
(api_name, version, ', '.join(version_map.keys())))
raise exceptions.UnsupportedVersion(msg)
return importutils.import_class(client_path)

View File

@@ -0,0 +1,446 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 OpenStack Foundation
# 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.
"""
Exception definitions.
"""
import sys
class ClientException(Exception):
"""The base exception class for all exceptions this library raises.
"""
pass
class MissingArgs(ClientException):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = "Missing argument(s): %s" % ", ".join(missing)
super(MissingArgs, self).__init__(msg)
class ValidationError(ClientException):
"""Error in validation on API client side."""
pass
class UnsupportedVersion(ClientException):
"""User is trying to use an unsupported version of the API."""
pass
class CommandError(ClientException):
"""Error in CLI tool."""
pass
class AuthorizationFailure(ClientException):
"""Cannot authorize API client."""
pass
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
super(AuthPluginOptionsMissing, self).__init__(
"Authentication failed. Missing options: %s" %
", ".join(opt_names))
self.opt_names = opt_names
class AuthSystemNotFound(AuthorizationFailure):
"""User has specified a AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
"AuthSystemNotFound: %s" % repr(auth_system))
self.auth_system = auth_system
class NoUniqueMatch(ClientException):
"""Multiple entities found instead of one."""
pass
class EndpointException(ClientException):
"""Something is rotten in Service Catalog."""
pass
class EndpointNotFound(EndpointException):
"""Could not find requested endpoint in Service Catalog."""
pass
class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
super(AmbiguousEndpoints, self).__init__(
"AmbiguousEndpoints: %s" % repr(endpoints))
self.endpoints = endpoints
class HttpError(ClientException):
"""The base exception class for all HTTP exceptions.
"""
http_status = 0
message = "HTTP Error"
def __init__(self, message=None, details=None,
response=None, request_id=None,
url=None, method=None, http_status=None):
self.http_status = http_status or self.http_status
self.message = message or self.message
self.details = details
self.request_id = request_id
self.response = response
self.url = url
self.method = method
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
if request_id:
formatted_string += " (Request-ID: %s)" % request_id
super(HttpError, self).__init__(formatted_string)
class HTTPClientError(HttpError):
"""Client-side HTTP error.
Exception for cases in which the client seems to have erred.
"""
message = "HTTP Client Error"
class HttpServerError(HttpError):
"""Server-side HTTP error.
Exception for cases in which the server is aware that it has
erred or is incapable of performing the request.
"""
message = "HTTP Server Error"
class BadRequest(HTTPClientError):
"""HTTP 400 - Bad Request.
The request cannot be fulfilled due to bad syntax.
"""
http_status = 400
message = "Bad Request"
class Unauthorized(HTTPClientError):
"""HTTP 401 - Unauthorized.
Similar to 403 Forbidden, but specifically for use when authentication
is required and has failed or has not yet been provided.
"""
http_status = 401
message = "Unauthorized"
class PaymentRequired(HTTPClientError):
"""HTTP 402 - Payment Required.
Reserved for future use.
"""
http_status = 402
message = "Payment Required"
class Forbidden(HTTPClientError):
"""HTTP 403 - Forbidden.
The request was a valid request, but the server is refusing to respond
to it.
"""
http_status = 403
message = "Forbidden"
class NotFound(HTTPClientError):
"""HTTP 404 - Not Found.
The requested resource could not be found but may be available again
in the future.
"""
http_status = 404
message = "Not Found"
class MethodNotAllowed(HTTPClientError):
"""HTTP 405 - Method Not Allowed.
A request was made of a resource using a request method not supported
by that resource.
"""
http_status = 405
message = "Method Not Allowed"
class NotAcceptable(HTTPClientError):
"""HTTP 406 - Not Acceptable.
The requested resource is only capable of generating content not
acceptable according to the Accept headers sent in the request.
"""
http_status = 406
message = "Not Acceptable"
class ProxyAuthenticationRequired(HTTPClientError):
"""HTTP 407 - Proxy Authentication Required.
The client must first authenticate itself with the proxy.
"""
http_status = 407
message = "Proxy Authentication Required"
class RequestTimeout(HTTPClientError):
"""HTTP 408 - Request Timeout.
The server timed out waiting for the request.
"""
http_status = 408
message = "Request Timeout"
class Conflict(HTTPClientError):
"""HTTP 409 - Conflict.
Indicates that the request could not be processed because of conflict
in the request, such as an edit conflict.
"""
http_status = 409
message = "Conflict"
class Gone(HTTPClientError):
"""HTTP 410 - Gone.
Indicates that the resource requested is no longer available and will
not be available again.
"""
http_status = 410
message = "Gone"
class LengthRequired(HTTPClientError):
"""HTTP 411 - Length Required.
The request did not specify the length of its content, which is
required by the requested resource.
"""
http_status = 411
message = "Length Required"
class PreconditionFailed(HTTPClientError):
"""HTTP 412 - Precondition Failed.
The server does not meet one of the preconditions that the requester
put on the request.
"""
http_status = 412
message = "Precondition Failed"
class RequestEntityTooLarge(HTTPClientError):
"""HTTP 413 - Request Entity Too Large.
The request is larger than the server is willing or able to process.
"""
http_status = 413
message = "Request Entity Too Large"
def __init__(self, *args, **kwargs):
try:
self.retry_after = int(kwargs.pop('retry_after'))
except (KeyError, ValueError):
self.retry_after = 0
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
class RequestUriTooLong(HTTPClientError):
"""HTTP 414 - Request-URI Too Long.
The URI provided was too long for the server to process.
"""
http_status = 414
message = "Request-URI Too Long"
class UnsupportedMediaType(HTTPClientError):
"""HTTP 415 - Unsupported Media Type.
The request entity has a media type which the server or resource does
not support.
"""
http_status = 415
message = "Unsupported Media Type"
class RequestedRangeNotSatisfiable(HTTPClientError):
"""HTTP 416 - Requested Range Not Satisfiable.
The client has asked for a portion of the file, but the server cannot
supply that portion.
"""
http_status = 416
message = "Requested Range Not Satisfiable"
class ExpectationFailed(HTTPClientError):
"""HTTP 417 - Expectation Failed.
The server cannot meet the requirements of the Expect request-header field.
"""
http_status = 417
message = "Expectation Failed"
class UnprocessableEntity(HTTPClientError):
"""HTTP 422 - Unprocessable Entity.
The request was well-formed but was unable to be followed due to semantic
errors.
"""
http_status = 422
message = "Unprocessable Entity"
class InternalServerError(HttpServerError):
"""HTTP 500 - Internal Server Error.
A generic error message, given when no more specific message is suitable.
"""
http_status = 500
message = "Internal Server Error"
# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
"""HTTP 501 - Not Implemented.
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
http_status = 501
message = "Not Implemented"
class BadGateway(HttpServerError):
"""HTTP 502 - Bad Gateway.
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
http_status = 502
message = "Bad Gateway"
class ServiceUnavailable(HttpServerError):
"""HTTP 503 - Service Unavailable.
The server is currently unavailable.
"""
http_status = 503
message = "Service Unavailable"
class GatewayTimeout(HttpServerError):
"""HTTP 504 - Gateway Timeout.
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
http_status = 504
message = "Gateway Timeout"
class HttpVersionNotSupported(HttpServerError):
"""HTTP 505 - HttpVersion Not Supported.
The server does not support the HTTP protocol version used in the request.
"""
http_status = 505
message = "HTTP Version Not Supported"
# 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 HttpError.__subclasses__())
_code_map = {}
for obj in sys.modules[__name__].__dict__.values():
if isinstance(obj, type):
try:
http_status = obj.http_status
except AttributeError:
pass
else:
if http_status:
_code_map[http_status] = obj
def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": response.headers.get("x-compute-request-id"),
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]
content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if hasattr(body, "keys"):
error = body[body.keys()[0]]
kwargs["message"] = error.get("message", None)
kwargs["details"] = error.get("details", None)
elif content_type.startswith("text/"):
kwargs["details"] = response.text
try:
cls = _code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = HttpServerError
elif 400 <= response.status_code < 500:
cls = HTTPClientError
else:
cls = HttpError
return cls(**kwargs)

View File

@@ -0,0 +1,172 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation
# 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.
"""
A fake server that "responds" to API methods with pre-canned responses.
All of these responses come from the spec, so if for some reason the spec's
wrong the tests might raise AssertionError. I've indicated in comments the
places where actual behavior differs from the spec.
"""
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import urlparse
import requests
from libraclient.openstack.common.apiclient import client
def assert_has_keys(dct, required=[], optional=[]):
for k in required:
try:
assert k in dct
except AssertionError:
extra_keys = set(dct.keys()).difference(set(required + optional))
raise AssertionError("found unexpected keys: %s" %
list(extra_keys))
class TestResponse(requests.Response):
"""Wrap requests.Response and provide a convenient initialization.
"""
def __init__(self, data):
super(TestResponse, self).__init__()
self._content_consumed = True
if isinstance(data, dict):
self.status_code = data.get('status_code', 200)
# Fake the text attribute to streamline Response creation
text = data.get('text', "")
if isinstance(text, (dict, list)):
self._content = json.dumps(text)
default_headers = {
"Content-Type": "application/json",
}
else:
self._content = text
default_headers = {}
self.headers = data.get('headers') or default_headers
else:
self.status_code = data
def __eq__(self, other):
return (self.status_code == other.status_code and
self.headers == other.headers and
self._content == other._content)
class FakeHTTPClient(client.HTTPClient):
def __init__(self, *args, **kwargs):
self.callstack = []
self.fixtures = kwargs.pop("fixtures", None) or {}
if not args and not "auth_plugin" in kwargs:
args = (None, )
super(FakeHTTPClient, self).__init__(*args, **kwargs)
def assert_called(self, method, url, body=None, pos=-1):
"""Assert than an API method was just called.
"""
expected = (method, url)
called = self.callstack[pos][0:2]
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
assert expected == called, 'Expected %s %s; got %s %s' % \
(expected + called)
if body is not None:
if self.callstack[pos][3] != body:
raise AssertionError('%r != %r' %
(self.callstack[pos][3], body))
def assert_called_anytime(self, method, url, body=None):
"""Assert than an API method was called anytime in the test.
"""
expected = (method, url)
assert self.callstack, \
"Expected %s %s but no calls were made." % expected
found = False
entry = None
for entry in self.callstack:
if expected == entry[0:2]:
found = True
break
assert found, 'Expected %s %s; got %s' % \
(method, url, self.callstack)
if body is not None:
assert entry[3] == body, "%s != %s" % (entry[3], body)
self.callstack = []
def clear_callstack(self):
self.callstack = []
def authenticate(self):
pass
def client_request(self, client, method, url, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(method,
url,
kwargs.get("headers") or {},
kwargs.get("json") or kwargs.get("data")))
try:
fixture = self.fixtures[url][method]
except KeyError:
pass
else:
return TestResponse({"headers": fixture[0],
"text": fixture[1]})
# Call the method
args = urlparse.parse_qsl(urlparse.urlparse(url)[4])
kwargs.update(args)
munged_url = url.rsplit('?', 1)[0]
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
munged_url = munged_url.replace('-', '_')
callback = "%s_%s" % (method.lower(), munged_url)
if not hasattr(self, callback):
raise AssertionError('Called unknown API method: %s %s, '
'expected fakes method name: %s' %
(method, url, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})

View File

@@ -0,0 +1,186 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation
# 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 abc
import logging
from libraclient.openstack.common.apiclient import auth
from libraclient.openstack.common.apiclient import exceptions
from keystoneclient import access
logger = logging.getLogger(__name__)
class KeystoneBaseAuthPlugin(auth.BaseAuthPlugin):
access_info = None
def _do_authenticate(self, http_client):
resp = self._get_auth_response(http_client)
try:
body = resp.json()
except ValueError as ex:
raise exceptions.AuthorizationFailure(ex)
self.access_info = access.AccessInfo.factory(
resp, body, region_name=http_client.region_name)
class KeystoneAuthPluginV2(KeystoneBaseAuthPlugin):
auth_system = "keystone2"
opt_names = [
"auth_url",
"user_id",
"username",
"password",
"token",
"tenant_id",
"tenant_name",
]
def sufficient_options(self):
have_identity = (
self.opts.get("token") or
(self.opts.get("username") and self.opts.get("password")))
if not (self.opts.get("auth_url") and have_identity):
req_opts = ("auth_url", "username", "password", "token")
raise exceptions.AuthPluginOptionsMissing(
[opt for opt in req_opts if not self.opts.get(opt)])
def _get_auth_response(self, http_client):
headers = {}
token = self.opts.get("token")
if token:
params = {"auth": {"token": {"id": token}}}
headers["X-Auth-Token"] = token
else:
params = {
"auth": {
"passwordCredentials": {
"username": self.opts.get("username"),
"password": self.opts.get("password"),
}
}
}
if self.opts.get("tenant_id"):
params["auth"]["tenantId"] = self.opts.get("tenant_id")
elif self.opts.get("tenant_name"):
params["auth"]["tenantName"] = self.opts.get("tenant_name")
return http_client.request(
"POST",
http_client.concat_url(self.opts.get("auth_url"), "/tokens"),
allow_redirects=True,
headers=headers,
json=params)
def token_and_endpoint(self, endpoint_type, service_type):
url = self.access_info.service_catalog.url_for(
service_type=service_type,
endpoint_type=endpoint_type)
return self.access_info.auth_token, url
class KeystoneAuthPluginV3(KeystoneBaseAuthPlugin):
auth_system = "keystone3"
opt_names = [
"auth_url",
"user_id",
"username",
"user_domain_id",
"user_domain_name",
"password",
"domain_id",
"domain_name",
"project_id",
"project_name",
"project_domain_id",
"project_domain_name",
"token",
]
def sufficient_options(self):
have_identity = (
self.opts.get("token") or
((self.opts.get("username") or self.opts.get("user_id"))
and self.opts.get("password")))
if not (self.opts.get("auth_url") and have_identity):
req_opts = ("auth_url", "username", "user_id", "password", "token")
raise exceptions.AuthPluginOptionsMissing(
[opt for opt in req_opts if not self.opts.get(opt)])
def _set_id_or_name(self, dct, key, id_key, name_key):
value = self.opts.get(id_key)
if value:
dct[key] = {"id": value}
return
value = self.opts.get(name_key)
if value:
dct[key] = {"name": value}
def _get_auth_response(self, http_client):
domain_scoped = (self.opts.get("domain_id") or
self.opts.get("domain_name"))
project_scoped = (self.opts.get("project_id") or
self.opts.get("project_name"))
if domain_scoped and project_scoped:
raise ValueError("Authentication cannot be scoped to both domain"
" and project.")
headers = {}
body = {"auth": {"identity": {}}}
ident = body["auth"]["identity"]
token = self.opts.get("token")
if token:
headers["X-Auth-Token"] = token
ident["methods"] = ["token"]
ident["token"] = {}
ident["token"]["id"] = token
else:
# password authentication
ident["methods"] = ["password"]
ident["password"] = {}
self._set_id_or_name(ident["password"], "user",
"user_id", "username")
user = ident["password"]["user"]
user["password"] = self.opts.get("password")
if "name" in user:
self._set_id_or_name(user, "domain",
"user_domain_id", "user_domain_name")
if domain_scoped or project_scoped:
body["auth"]["scope"] = {}
scope = body["auth"]["scope"]
self._set_id_or_name(scope, "domain", "domain_id", "domain_name")
if "domain" not in scope:
# use project_id or project_name
self._set_id_or_name(scope, "project",
"project_id", "project_name")
if "name" in scope["project"]:
self._set_id_or_name(
scope["project"], "domain",
"project_domain_id", "project_domain_name")
return http_client.request(
"POST",
http_client.concat_url(self.opts.get("auth_url"), "/auth/tokens"),
allow_redirects=True,
headers=headers,
json=body)

View File

@@ -0,0 +1,213 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Red Hat, 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.
# W0603: Using the global statement
# W0621: Redefining name %s from outer scope
# pylint: disable=W0603,W0621
import getpass
import inspect
import os
import sys
import textwrap
import prettytable
from libraclient.openstack.common.apiclient import exceptions
from libraclient.openstack.common import strutils
def validate_args(fn, *args, **kwargs):
"""Check that the supplied args are sufficient for calling a function.
>>> validate_args(lambda a: None)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): a
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): b, d
:param fn: the function to check
:param arg: the positional arguments supplied
:param kwargs: the keyword arguments supplied
"""
argspec = inspect.getargspec(fn)
num_defaults = len(argspec.defaults or [])
required_args = argspec.args[:len(argspec.args) - num_defaults]
def isbound(method):
return getattr(method, 'im_self', None) is not None
if isbound(fn):
required_args.pop(0)
missing = [arg for arg in required_args if arg not in kwargs]
missing = missing[len(args):]
if missing:
raise exceptions.MissingArgs(missing)
def arg(*args, **kwargs):
"""Decorator for CLI args.
Example:
>>> @arg("name", help="Name of the new entity")
... def entity_create(args):
... pass
"""
def _decorator(func):
add_arg(func, *args, **kwargs)
return func
return _decorator
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg, None)
if value:
return value
return kwargs.get('default', '')
def add_arg(func, *args, **kwargs):
"""Bind CLI arguments to a shell.py `do_foo` function."""
if not hasattr(func, 'arguments'):
func.arguments = []
# NOTE(sirp): avoid dups that can occur when the module is shared across
# tests.
if (args, kwargs) not in func.arguments:
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.arguments.insert(0, (args, kwargs))
def unauthenticated(func):
"""Adds 'unauthenticated' attribute to decorated function.
Usage:
>>> @unauthenticated
... def mymethod(f):
... pass
"""
func.unauthenticated = True
return func
def isunauthenticated(func):
"""Checks if the function does not require authentication.
Mark such functions with the `@unauthenticated` decorator.
:returns: bool
"""
return getattr(func, 'unauthenticated', False)
def print_list(objs, fields, formatters=None, sortby_index=0,
mixed_case_fields=None):
"""Print a list or objects as a table, one row per object.
:param objs: iterable of :class:`Resource`
:param fields: attributes that correspond to columns, in order
:param formatters: `dict` of callables for field formatting
:param sortby_index: index of the field for sorting table rows
:param mixed_case_fields: fields corresponding to object attributes that
have mixed case names (e.g., 'serverId')
"""
formatters = formatters or {}
mixed_case_fields = mixed_case_fields or []
if sortby_index is None:
sortby = None
else:
sortby = fields[sortby_index]
pt = prettytable.PrettyTable(fields, caching=False)
pt.align = 'l'
for o in objs:
row = []
for field in fields:
if formatters and field in formatters:
row.append(formatters[field](o))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, '')
row.append(data)
pt.add_row(row)
print(strutils.safe_encode(pt.get_string(sortby=sortby)))
def print_dict(dct, dict_property="Property", wrap=0):
"""Print a `dict` as a table of two columns.
:param dct: `dict` to print
:param dict_property: name of the first column
:param wrap: wrapping for the second column
"""
pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False)
pt.align = 'l'
for k, v in dct.iteritems():
# convert dict to str to check length
if isinstance(v, dict):
v = str(v)
if wrap > 0:
v = textwrap.fill(str(v), wrap)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, basestring) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
pt.add_row([col1, line])
col1 = ''
else:
pt.add_row([k, v])
print(strutils.safe_encode(pt.get_string()))
def get_password(max_password_prompts=3):
"""Read password from TTY."""
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
pw = None
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
# Check for Ctrl-D
try:
for _ in xrange(max_password_prompts):
pw1 = getpass.getpass("OS Password: ")
if verify:
pw2 = getpass.getpass("Please verify: ")
else:
pw2 = pw1
if pw1 == pw2 and pw1:
pw = pw1
break
except EOFError:
pass
return pw

View File

@@ -0,0 +1,373 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Red Hat, Inc.
# Copyright 2013 IBM Corp.
# 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.
"""
gettext for openstack-common modules.
Usual usage in an openstack.common module:
from libraclient.openstack.common.gettextutils import _
"""
import copy
import gettext
import logging
import os
import re
try:
import UserString as _userString
except ImportError:
import collections as _userString
from babel import localedata
import six
_localedir = os.environ.get('libraclient'.upper() + '_LOCALEDIR')
_t = gettext.translation('libraclient', localedir=_localedir, fallback=True)
_AVAILABLE_LANGUAGES = {}
USE_LAZY = False
def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext
Call this at the start of execution to enable the gettextutils._
function to use lazy gettext functionality. This is useful if
your project is importing _ directly instead of using the
gettextutils.install() way of importing the _ function.
"""
global USE_LAZY
USE_LAZY = True
def _(msg):
if USE_LAZY:
return Message(msg, 'libraclient')
else:
if six.PY3:
return _t.gettext(msg)
return _t.ugettext(msg)
def install(domain, lazy=False):
"""Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's
install() function.
The main difference from gettext.install() is that we allow
overriding the default localedir (e.g. /usr/share/locale) using
a translation-domain-specific environment variable (e.g.
NOVA_LOCALEDIR).
:param domain: the translation domain
:param lazy: indicates whether or not to install the lazy _() function.
The lazy _() introduces a way to do deferred translation
of messages by installing a _ that builds Message objects,
instead of strings, which can then be lazily translated into
any available locale.
"""
if lazy:
# NOTE(mrodden): Lazy gettext functionality.
#
# The following introduces a deferred way to do translations on
# messages in OpenStack. We override the standard _() function
# and % (format string) operation to build Message objects that can
# later be translated when we have more information.
#
# Also included below is an example LocaleHandler that translates
# Messages to an associated locale, effectively allowing many logs,
# each with their own locale.
def _lazy_gettext(msg):
"""Create and return a Message object.
Lazy gettext function for a given domain, it is a factory method
for a project/module to get a lazy gettext function for its own
translation domain (i.e. nova, glance, cinder, etc.)
Message encapsulates a string so that we can translate
it later when needed.
"""
return Message(msg, domain)
from six import moves
moves.builtins.__dict__['_'] = _lazy_gettext
else:
localedir = '%s_LOCALEDIR' % domain.upper()
if six.PY3:
gettext.install(domain,
localedir=os.environ.get(localedir))
else:
gettext.install(domain,
localedir=os.environ.get(localedir),
unicode=True)
class Message(_userString.UserString, object):
"""Class used to encapsulate translatable messages."""
def __init__(self, msg, domain):
# _msg is the gettext msgid and should never change
self._msg = msg
self._left_extra_msg = ''
self._right_extra_msg = ''
self._locale = None
self.params = None
self.domain = domain
@property
def data(self):
# NOTE(mrodden): this should always resolve to a unicode string
# that best represents the state of the message currently
localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
if self.locale:
lang = gettext.translation(self.domain,
localedir=localedir,
languages=[self.locale],
fallback=True)
else:
# use system locale for translations
lang = gettext.translation(self.domain,
localedir=localedir,
fallback=True)
if six.PY3:
ugettext = lang.gettext
else:
ugettext = lang.ugettext
full_msg = (self._left_extra_msg +
ugettext(self._msg) +
self._right_extra_msg)
if self.params is not None:
full_msg = full_msg % self.params
return six.text_type(full_msg)
@property
def locale(self):
return self._locale
@locale.setter
def locale(self, value):
self._locale = value
if not self.params:
return
# This Message object may have been constructed with one or more
# Message objects as substitution parameters, given as a single
# Message, or a tuple or Map containing some, so when setting the
# locale for this Message we need to set it for those Messages too.
if isinstance(self.params, Message):
self.params.locale = value
return
if isinstance(self.params, tuple):
for param in self.params:
if isinstance(param, Message):
param.locale = value
return
if isinstance(self.params, dict):
for param in self.params.values():
if isinstance(param, Message):
param.locale = value
def _save_dictionary_parameter(self, dict_param):
full_msg = self.data
# look for %(blah) fields in string;
# ignore %% and deal with the
# case where % is first character on the line
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg)
# if we don't find any %(blah) blocks but have a %s
if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg):
# apparently the full dictionary is the parameter
params = copy.deepcopy(dict_param)
else:
params = {}
for key in keys:
try:
params[key] = copy.deepcopy(dict_param[key])
except TypeError:
# cast uncopyable thing to unicode string
params[key] = six.text_type(dict_param[key])
return params
def _save_parameters(self, other):
# we check for None later to see if
# we actually have parameters to inject,
# so encapsulate if our parameter is actually None
if other is None:
self.params = (other, )
elif isinstance(other, dict):
self.params = self._save_dictionary_parameter(other)
else:
# fallback to casting to unicode,
# this will handle the problematic python code-like
# objects that cannot be deep-copied
try:
self.params = copy.deepcopy(other)
except TypeError:
self.params = six.text_type(other)
return self
# overrides to be more string-like
def __unicode__(self):
return self.data
def __str__(self):
if six.PY3:
return self.__unicode__()
return self.data.encode('utf-8')
def __getstate__(self):
to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
'domain', 'params', '_locale']
new_dict = self.__dict__.fromkeys(to_copy)
for attr in to_copy:
new_dict[attr] = copy.deepcopy(self.__dict__[attr])
return new_dict
def __setstate__(self, state):
for (k, v) in state.items():
setattr(self, k, v)
# operator overloads
def __add__(self, other):
copied = copy.deepcopy(self)
copied._right_extra_msg += other.__str__()
return copied
def __radd__(self, other):
copied = copy.deepcopy(self)
copied._left_extra_msg += other.__str__()
return copied
def __mod__(self, other):
# do a format string to catch and raise
# any possible KeyErrors from missing parameters
self.data % other
copied = copy.deepcopy(self)
return copied._save_parameters(other)
def __mul__(self, other):
return self.data * other
def __rmul__(self, other):
return other * self.data
def __getitem__(self, key):
return self.data[key]
def __getslice__(self, start, end):
return self.data.__getslice__(start, end)
def __getattribute__(self, name):
# NOTE(mrodden): handle lossy operations that we can't deal with yet
# These override the UserString implementation, since UserString
# uses our __class__ attribute to try and build a new message
# after running the inner data string through the operation.
# At that point, we have lost the gettext message id and can just
# safely resolve to a string instead.
ops = ['capitalize', 'center', 'decode', 'encode',
'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
if name in ops:
return getattr(self.data, name)
else:
return _userString.UserString.__getattribute__(self, name)
def get_available_languages(domain):
"""Lists the available languages for the given translation domain.
:param domain: the domain to get languages for
"""
if domain in _AVAILABLE_LANGUAGES:
return copy.copy(_AVAILABLE_LANGUAGES[domain])
localedir = '%s_LOCALEDIR' % domain.upper()
find = lambda x: gettext.find(domain,
localedir=os.environ.get(localedir),
languages=[x])
# NOTE(mrodden): en_US should always be available (and first in case
# order matters) since our in-line message strings are en_US
language_list = ['en_US']
# NOTE(luisg): Babel <1.0 used a function called list(), which was
# renamed to locale_identifiers() in >=1.0, the requirements master list
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
# this check when the master list updates to >=1.0, and all projects udpate
list_identifiers = (getattr(localedata, 'list', None) or
getattr(localedata, 'locale_identifiers'))
locale_identifiers = list_identifiers()
for i in locale_identifiers:
if find(i) is not None:
language_list.append(i)
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)
def get_localized_message(message, user_locale):
"""Gets a localized version of the given message in the given locale.
If the message is not a Message object the message is returned as-is.
If the locale is None the message is translated to the default locale.
:returns: the translated message in unicode, or the original message if
it could not be translated
"""
translated = message
if isinstance(message, Message):
original_locale = message.locale
message.locale = user_locale
translated = six.text_type(message)
message.locale = original_locale
return translated
class LocaleHandler(logging.Handler):
"""Handler that can have a locale associated to translate Messages.
A quick example of how to utilize the Message class above.
LocaleHandler takes a locale and a target logging.Handler object
to forward LogRecord objects to after translating the internal Message.
"""
def __init__(self, locale, target):
"""Initialize a LocaleHandler
:param locale: locale to use for translating messages
:param target: logging.Handler object to forward
LogRecord objects to after translation
"""
logging.Handler.__init__(self)
self.locale = locale
self.target = target
def emit(self, record):
if isinstance(record.msg, Message):
# set the locale and resolve to a string
record.msg.locale = self.locale
self.target.emit(record)

View File

@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC. # Copyright 2011 OpenStack Foundation.
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -24,12 +24,12 @@ import traceback
def import_class(import_str): def import_class(import_str):
"""Returns a class from a string including module and class""" """Returns a class from a string including module and class."""
mod_str, _sep, class_str = import_str.rpartition('.') mod_str, _sep, class_str = import_str.rpartition('.')
try: try:
__import__(mod_str) __import__(mod_str)
return getattr(sys.modules[mod_str], class_str) return getattr(sys.modules[mod_str], class_str)
except (ValueError, AttributeError), exc: except (ValueError, AttributeError):
raise ImportError('Class %s cannot be found (%s)' % raise ImportError('Class %s cannot be found (%s)' %
(class_str, (class_str,
traceback.format_exception(*sys.exc_info()))) traceback.format_exception(*sys.exc_info())))
@@ -41,8 +41,9 @@ def import_object(import_str, *args, **kwargs):
def import_object_ns(name_space, import_str, *args, **kwargs): def import_object_ns(name_space, import_str, *args, **kwargs):
""" """Tries to import object from default namespace.
Import a class and return an instance of it, first by trying
Imports a class and return an instance of it, first by trying
to find the class in a default namespace, then failing back to to find the class in a default namespace, then failing back to
a full path if not found in the default namespace. a full path if not found in the default namespace.
""" """
@@ -57,3 +58,11 @@ def import_module(import_str):
"""Import a module.""" """Import a module."""
__import__(import_str) __import__(import_str)
return sys.modules[import_str] return sys.modules[import_str]
def try_import(import_str, default=None):
"""Try to import a module and if it fails return default."""
try:
return import_module(import_str)
except ImportError:
return default

View File

@@ -0,0 +1,17 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 Canonical Ltd.
# 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.
#

View File

@@ -0,0 +1,51 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 Canonical Ltd.
# 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.
#
"""
Python2/Python3 compatibility layer for OpenStack
"""
import six
if six.PY3:
# python3
import urllib.parse
urlencode = urllib.parse.urlencode
urljoin = urllib.parse.urljoin
quote = urllib.parse.quote
parse_qsl = urllib.parse.parse_qsl
unquote = urllib.parse.unquote
urlparse = urllib.parse.urlparse
urlsplit = urllib.parse.urlsplit
urlunsplit = urllib.parse.urlunsplit
else:
# python2
import urllib
import urlparse
urlencode = urllib.urlencode
quote = urllib.quote
unquote = urllib.unquote
parse = urlparse
parse_qsl = parse.parse_qsl
urljoin = parse.urljoin
urlparse = parse.urlparse
urlsplit = parse.urlsplit
urlunsplit = parse.urlunsplit

View File

@@ -0,0 +1,218 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack Foundation.
# 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.
"""
System-level utilities and helper functions.
"""
import re
import sys
import unicodedata
import six
from libraclient.openstack.common.gettextutils import _ # noqa
# Used for looking up extensions of text
# to their 'multiplied' byte amount
BYTE_MULTIPLIERS = {
'': 1,
't': 1024 ** 4,
'g': 1024 ** 3,
'm': 1024 ** 2,
'k': 1024,
}
BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)')
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
def int_from_bool_as_string(subject):
"""Interpret a string as a boolean and return either 1 or 0.
Any string value in:
('True', 'true', 'On', 'on', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
return bool_from_string(subject) and 1 or 0
def bool_from_string(subject, strict=False):
"""Interpret a string as a boolean.
A case-insensitive match is performed such that strings matching 't',
'true', 'on', 'y', 'yes', or '1' are considered True and, when
`strict=False`, anything else is considered False.
Useful for JSON-decoded stuff and config file parsing.
If `strict=True`, unrecognized values, including None, will raise a
ValueError which is useful when parsing values passed in from an API call.
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
"""
if not isinstance(subject, six.string_types):
subject = str(subject)
lowered = subject.strip().lower()
if lowered in TRUE_STRINGS:
return True
elif lowered in FALSE_STRINGS:
return False
elif strict:
acceptable = ', '.join(
"'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
msg = _("Unrecognized value '%(val)s', acceptable values are:"
" %(acceptable)s") % {'val': subject,
'acceptable': acceptable}
raise ValueError(msg)
else:
return False
def safe_decode(text, incoming=None, errors='strict'):
"""Decodes incoming str using `incoming` if they're not already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a unicode `incoming` encoded
representation of it.
:raises TypeError: If text is not an isntance of str
"""
if not isinstance(text, six.string_types):
raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, six.text_type):
return text
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
try:
return text.decode(incoming, errors)
except UnicodeDecodeError:
# Note(flaper87) If we get here, it means that
# sys.stdin.encoding / sys.getdefaultencoding
# didn't return a suitable encoding to decode
# text. This happens mostly when global LANG
# var is not set correctly and there's no
# default encoding. In this case, most likely
# python will use ASCII or ANSI encoders as
# default encodings but they won't be capable
# of decoding non-ASCII characters.
#
# Also, UTF-8 is being used since it's an ASCII
# extension.
return text.decode('utf-8', errors)
def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'):
"""Encodes incoming str/unicode using `encoding`.
If incoming is not specified, text is expected to be encoded with
current python's default encoding. (`sys.getdefaultencoding`)
:param incoming: Text's current encoding
:param encoding: Expected encoding for text (Default UTF-8)
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a bytestring `encoding` encoded
representation of it.
:raises TypeError: If text is not an isntance of str
"""
if not isinstance(text, six.string_types):
raise TypeError("%s can't be encoded" % type(text))
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
if isinstance(text, six.text_type):
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors)
return text.encode(encoding, errors)
return text
def to_bytes(text, default=0):
"""Converts a string into an integer of bytes.
Looks at the last characters of the text to determine
what conversion is needed to turn the input text into a byte number.
Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive)
:param text: String input for bytes size conversion.
:param default: Default return value when text is blank.
"""
match = BYTE_REGEX.search(text)
if match:
magnitude = int(match.group(1))
mult_key_org = match.group(2)
if not mult_key_org:
return magnitude
elif text:
msg = _('Invalid string format: %s') % text
raise TypeError(msg)
else:
return default
mult_key = mult_key_org.lower().replace('b', '', 1)
multiplier = BYTE_MULTIPLIERS.get(mult_key)
if multiplier is None:
msg = _('Unknown byte multiplier: %s') % mult_key_org
raise TypeError(msg)
return magnitude * multiplier
def to_slug(value, incoming=None, errors="strict"):
"""Normalize string.
Convert to lowercase, remove non-word characters, and convert spaces
to hyphens.
Inspired by Django's `slugify` filter.
:param value: Text to slugify
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: slugified unicode representation of `value`
:raises TypeError: If text is not an instance of str
"""
value = safe_decode(value, incoming, errors)
# NOTE(aababilov): no need to use safe_(encode|decode) here:
# encodings are always "ascii", error handling is always "ignore"
# and types are always known (first: unicode; second: str)
value = unicodedata.normalize("NFKD", value).encode(
"ascii", "ignore").decode("ascii")
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
return SLUGIFY_HYPHENATE_RE.sub("-", value)

703
libraclient/shell.py Normal file
View File

@@ -0,0 +1,703 @@
"""
CLI (Command Line Interface) for Libra LBaaS tools
"""
from __future__ import print_function
import argparse
import glob
import imp
import itertools
import os
import pkgutil
import sys
import logging
import pkg_resources
import six
HAS_KEYRING = False
all_errors = ValueError
try:
import keyring
HAS_KEYRING = True
try:
if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring):
import gnomekeyring
all_errors = (ValueError,
gnomekeyring.IOError,
gnomekeyring.NoKeyringDaemonError)
except Exception:
pass
except ImportError:
pass
import libraclient
from libraclient.client import VersionedClient
from libraclient.openstack.common.apiclient import auth
from libraclient.openstack.common.apiclient import base
from libraclient.openstack.common.apiclient import client
from libraclient.openstack.common.apiclient import exceptions as exc
from libraclient.openstack.common import cliutils
from libraclient.openstack.common import strutils
from libraclient.v1_1 import shell as shell_v1
DEFAULT_API_VERSION = "1.1"
DEFAULT_ENDPOINT_TYPE = 'publicURL'
DEFAULT_SERVICE_TYPE = 'hpext:lbaas'
DEFAULT_SERVICE_NAME = 'libra'
logger = logging.getLogger(__name__)
def positive_non_zero_float(text):
if text is None:
return None
try:
value = float(text)
except ValueError:
msg = "%s must be a float" % text
raise argparse.ArgumentTypeError(msg)
if value <= 0:
msg = "%s must be greater than 0" % text
raise argparse.ArgumentTypeError(msg)
return value
class SecretsHelper(object):
def __init__(self, args, client):
self.args = args
self.client = client
self.key = None
def _validate_string(self, text):
if text is None or len(text) == 0:
return False
return True
def _make_key(self):
if self.key is not None:
return self.key
keys = [
self.client.auth_url,
self.client.projectid,
self.client.user,
self.client.region_name,
self.client.endpoint_type,
self.client.service_type,
self.client.service_name,
]
for (index, key) in enumerate(keys):
if key is None:
keys[index] = '?'
else:
keys[index] = str(keys[index])
self.key = "/".join(keys)
return self.key
def _prompt_password(self, verify=True):
pw = None
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
# Check for Ctl-D
try:
while True:
pw1 = getpass.getpass('OS Password: ')
if verify:
pw2 = getpass.getpass('Please verify: ')
else:
pw2 = pw1
if pw1 == pw2 and self._validate_string(pw1):
pw = pw1
break
except EOFError:
pass
return pw
def save(self, auth_token, management_url, tenant_id):
if not HAS_KEYRING or not self.args.os_cache:
return
if (auth_token == self.auth_token and
management_url == self.management_url):
# Nothing changed....
return
if not all([management_url, auth_token, tenant_id]):
raise ValueError("Unable to save empty management url/auth token")
value = "|".join([str(auth_token),
str(management_url),
str(tenant_id)])
keyring.set_password("libraclient_auth", self._make_key(), value)
@property
def password(self):
if self._validate_string(self.args.os_password):
return self.args.os_password
verify_pass = utils.bool_from_str(utils.env("OS_VERIFY_PASSWORD"))
return self._prompt_password(verify_pass)
@property
def management_url(self):
if not HAS_KEYRING or not self.args.os_cache:
return None
management_url = None
try:
block = keyring.get_password('libraclient_auth', self._make_key())
if block:
_token, management_url, _tenant_id = block.split('|', 2)
except all_errors:
pass
return management_url
@property
def auth_token(self):
# Now is where it gets complicated since we
# want to look into the keyring module, if it
# exists and see if anything was provided in that
# file that we can use.
if not HAS_KEYRING or not self.args.os_cache:
return None
token = None
try:
block = keyring.get_password('libraclient_auth', self._make_key())
if block:
token, _management_url, _tenant_id = block.split('|', 2)
except all_errors:
pass
return token
@property
def tenant_id(self):
if not HAS_KEYRING or not self.args.os_cache:
return None
tenant_id = None
try:
block = keyring.get_password('libraclient_auth', self._make_key())
if block:
_token, _management_url, tenant_id = block.split('|', 2)
except all_errors:
pass
return tenant_id
class LibraClientArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super(LibraClientArgumentParser, self).__init__(*args, **kwargs)
def error(self, message):
"""error(message: string)
Prints a usage message incorporating the message to stderr and
exits.
"""
self.print_usage(sys.stderr)
#FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value
choose_from = ' (choose from'
progparts = self.prog.partition(' ')
self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'"
" for more information.\n" %
{'errmsg': message.split(choose_from)[0],
'mainp': progparts[0],
'subp': progparts[2]})
# 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)
class LibraShell(object):
def get_base_parser(self):
parser = LibraClientArgumentParser(
prog='libra',
description=__doc__.strip(),
epilog='See "libraclient help COMMAND" '
'for help on a specific command.',
add_help=False,
formatter_class=OpenStackHelpFormatter,
)
# Global arguments
parser.add_argument(
'-h', '--help',
action='store_true',
help=argparse.SUPPRESS,
)
parser.add_argument(
'--version',
action='version',
version=libraclient.__version__)
parser.add_argument(
'--debug',
default=False,
action='store_true',
help="Print debugging output")
parser.add_argument(
'--no-cache',
default=not strutils.bool_from_string(
cliutils.env('OS_NO_CACHE', default='true')),
action='store_false',
dest='os_cache',
help=argparse.SUPPRESS)
parser.add_argument(
'--no_cache',
action='store_false',
dest='os_cache',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-cache',
default=cliutils.env('OS_CACHE', default=False),
action='store_true',
help="Use the auth token cache.")
parser.add_argument(
'--timings',
default=False,
action='store_true',
help="Print call timing info")
parser.add_argument(
'--api-timeout',
default=600,
metavar='<seconds>',
type=positive_non_zero_float,
help="Set HTTP call timeout (in seconds)")
parser.add_argument(
'--os-tenant-id',
metavar='<auth-tenant-id>',
default=cliutils.env('OS_TENANT_ID'),
help='Defaults to env[OS_TENANT_ID].')
parser.add_argument(
'--os-region-name',
metavar='<region-name>',
default=cliutils.env('OS_REGION_NAME', 'LIBRA_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].')
parser.add_argument(
'--os_region_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--service-type',
metavar='<service-type>',
default=cliutils.env('LIBRA_SERVICE_TYPE',
default=DEFAULT_SERVICE_TYPE),
help='Defaults to libra for most actions')
parser.add_argument(
'--service_type',
help=argparse.SUPPRESS)
parser.add_argument(
'--service-name',
metavar='<service-name>',
default=cliutils.env('LIBRA_SERVICE_NAME',
default=DEFAULT_SERVICE_NAME),
help='Defaults to env[LIBRA_SERVICE_NAME]')
parser.add_argument(
'--service_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--endpoint-type',
metavar='<endpoint-type>',
default=cliutils.env('LIBRA_ENDPOINT_TYPE',
default=DEFAULT_ENDPOINT_TYPE),
help='Defaults to env[LIBRA_ENDPOINT_TYPE] or '
+ DEFAULT_ENDPOINT_TYPE + '.')
parser.add_argument(
'--libra-api-version',
metavar='<compute-api-ver>',
default=cliutils.env('LIBRA_API_VERSION',
default=DEFAULT_API_VERSION),
help='Accepts 1.1'
'defaults to env[LIBRA_API_VERSION].')
parser.add_argument(
'--os_compute_api_version',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-cacert',
metavar='<ca-certificate>',
default=cliutils.env('OS_CACERT', default=None),
help='Specify a CA bundle file to use in '
'verifying a TLS (https) server certificate. '
'Defaults to env[OS_CACERT]')
parser.add_argument(
'--insecure',
default=cliutils.env('LIBRA_INSECURE', default=False),
action='store_true',
help="Explicitly allow libraclient to perform \"insecure\" "
"SSL (https) requests. The server's certificate will "
"not be verified against any certificate authorities. "
"This option should be used with caution.")
parser.add_argument(
'--bypass-url',
metavar='<bypass-url>',
dest='bypass_url',
help="Use this API endpoint instead of the Service Catalog")
parser.add_argument(
'--bypass_url',
help=argparse.SUPPRESS)
# The auth-system-plugins might require some extra options
auth.load_auth_system_opts(parser)
return parser
def get_subcommand_parser(self, version):
parser = self.get_base_parser()
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
actions_module = shell_v1
self._find_actions(subparsers, actions_module)
self._find_actions(subparsers, self)
for extension in self.extensions:
self._find_actions(subparsers, extension.module)
self._add_bash_completion_subparser(subparsers)
return parser
def _discover_extensions(self, version):
extensions = []
for name, module in itertools.chain(
self._discover_via_python_path(),
self._discover_via_contrib_path(version),
self._discover_via_entry_points()):
extension = base.Extension(name, module)
extensions.append(extension)
return extensions
def _discover_via_python_path(self):
for (module_loader, name, _ispkg) in pkgutil.iter_modules():
if name.endswith('_python_libraclient_ext'):
if not hasattr(module_loader, 'load_module'):
# Python 2.6 compat: actually get an ImpImporter obj
module_loader = module_loader.find_module(name)
module = module_loader.load_module(name)
if hasattr(module, 'extension_name'):
name = module.extension_name
yield name, module
def _discover_via_contrib_path(self, version):
module_path = os.path.dirname(os.path.abspath(__file__))
version_str = "v%s" % version.replace('.', '_')
ext_path = os.path.join(module_path, version_str, 'contrib')
ext_glob = os.path.join(ext_path, "*.py")
for ext_path in glob.iglob(ext_glob):
name = os.path.basename(ext_path)[:-3]
if name == "__init__":
continue
module = imp.load_source(name, ext_path)
yield name, module
def _discover_via_entry_points(self):
for ep in pkg_resources.iter_entry_points('libraclient.extension'):
name = ep.name
module = ep.load()
yield name, module
def _add_bash_completion_subparser(self, subparsers):
subparser = subparsers.add_parser(
'bash_completion',
add_help=False,
formatter_class=OpenStackHelpFormatter
)
self.subcommands['bash_completion'] = subparser
subparser.set_defaults(func=self.do_bash_completion)
def _find_actions(self, subparsers, actions_module):
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 ''
action_help = desc.strip()
arguments = getattr(callback, 'arguments', [])
subparser = subparsers.add_parser(
command,
help=action_help,
description=desc,
add_help=False,
formatter_class=OpenStackHelpFormatter
)
subparser.add_argument(
'-h', '--help',
action='help',
help=argparse.SUPPRESS,
)
self.subcommands[command] = subparser
for (args, kwargs) in arguments:
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
def setup_debugging(self, debug):
if not debug:
return
streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s"
# Set up the root logger to debug so that the submodules can
# print debug messages
logging.basicConfig(level=logging.DEBUG,
format=streamformat)
def main(self, argv):
# Parse args once to find version and debug settings
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.setup_debugging(options.debug)
# Discover available auth plugins
auth.discover_auth_systems()
# build available subcommands based on version
self.extensions = self._discover_extensions(
options.libra_api_version)
self._run_extension_hooks('__pre_parse_args__')
if '--endpoint_type' in argv:
spot = argv.index('--endpoint_type')
argv[spot] = '--endpoint-type'
subcommand_parser = self.get_subcommand_parser(
options.os_compute_api_version)
self.parser = subcommand_parser
if options.help or not argv:
subcommand_parser.print_help()
return 0
args = subcommand_parser.parse_args(argv)
self._run_extension_hooks('__post_parse_args__', args)
# Short-circuit and deal with help right away.
if args.func == self.do_help:
self.do_help(args)
return 0
elif args.func == self.do_bash_completion:
self.do_bash_completion(args)
return 0
os_username = args.os_username
os_password = args.os_password
os_tenant_name = args.os_tenant_name
os_tenant_id = args.os_tenant_id
os_auth_url = args.os_auth_url
os_region_name = args.os_region_name
os_auth_system = args.os_auth_system
endpoint_type = args.endpoint_type
insecure = args.insecure
service_type = args.service_type
service_name = args.service_name
bypass_url = args.bypass_url
os_cache = args.os_cache
cacert = args.os_cacert
timeout = args.api_timeout
if not os_auth_system:
os_auth_system = 'keystone2'
auth_plugin = auth.load_plugin(os_auth_system)
os_password = None
#FIXME(usrleon): Here should be restrict for project id same as
# for os_username or os_password but for compatibility it is not.
if not cliutils.isunauthenticated(args.func):
if auth_plugin:
auth_plugin.parse_opts(args)
if not auth_plugin or not auth_plugin.opts:
if not os_username:
raise exc.CommandError(
"You must provide a username"
" via either --os-username or env[OS_USERNAME]")
if not os_tenant_name and not os_tenant_id:
raise exc.CommandError(
"You must provide a tenant name "
"or tenant id via --os-tenant-name, "
"--os-tenant-id, env[OS_TENANT_NAME] "
"or env[OS_TENANT_ID]")
if not os_auth_url:
if os_auth_system and os_auth_system != 'keystone':
os_auth_url = auth_plugin.get_auth_url()
if not os_auth_url:
raise exc.CommandError(
"You must provide an auth url "
"via either --os-auth-url or env[OS_AUTH_URL] "
"or specify an auth_system which defines a "
"default url with --os-auth-system "
"or env[OS_AUTH_SYSTEM]")
if not (os_tenant_name or os_tenant_id):
raise exc.CommandError(
"You must provide a tenant_id "
"via either --os-tenant-id or env[OS_TENANT_ID]")
if not os_auth_url:
raise exc.CommandError(
"You must provide an auth url "
"via either --os-auth-url or env[OS_AUTH_URL]")
http_client = client.HTTPClient(
auth_plugin,
region_name=os_region_name,
endpoint_type=endpoint_type,
debug=args.debug)
self.cs = VersionedClient(
options.libra_api_version,
http_client,
endpoint_type=endpoint_type,
service_type=service_type)
# Now check for the password/token of which pieces of the
# identifying keyring key can come from the underlying client
if not cliutils.isunauthenticated(args.func):
helper = SecretsHelper(args, self.cs.http_client)
if (auth_plugin and auth_plugin.opts and
"os_password" not in auth_plugin.opts):
use_pw = False
else:
use_pw = True
tenant_id, auth_token, management_url = (helper.tenant_id,
helper.auth_token,
helper.management_url)
if tenant_id and auth_token and management_url:
self.cs.client.tenant_id = tenant_id
self.cs.client.auth_token = auth_token
self.cs.client.management_url = management_url
# Try to auth with the given info, if it fails
# go into password mode...
try:
self.cs.http_client.authenticate()
use_pw = False
except (exc.Unauthorized, exc.AuthorizationFailure):
# Likely it expired or just didn't work...
self.cs.client.auth_token = None
self.cs.client.management_url = None
if use_pw:
# Auth using token must have failed or not happened
# at all, so now switch to password mode and save
# the token when its gotten... using our keyring
# saver
os_password = helper.password
if not os_password:
raise exc.CommandError(
'Expecting a password provided via either '
'--os-password, env[OS_PASSWORD], or '
'prompted response')
self.cs.client.password = os_password
self.cs.client.keyring_saver = helper
try:
if not cliutils.isunauthenticated(args.func):
self.cs.http_client.authenticate()
except exc.Unauthorized:
raise exc.CommandError("Invalid OpenStack libra credentials.")
except exc.AuthorizationFailure, e:
import ipdb
ipdb.set_trace()
raise exc.CommandError("Unable to authorize user")
args.func(self.cs, args)
if args.timings:
self._dump_timings(self.cs.get_timings())
def _dump_timings(self, timings):
class Tyme(object):
def __init__(self, url, seconds):
self.url = url
self.seconds = seconds
results = [Tyme(url, end - start) for url, start, end in timings]
total = 0.0
for tyme in results:
total += tyme.seconds
results.append(Tyme("Total", total))
cliutils.print_list(results, ["url", "seconds"], sortby_index=None)
def _run_extension_hooks(self, hook_type, *args, **kwargs):
"""Run hooks for all registered extensions."""
for extension in self.extensions:
extension.run_hooks(hook_type, *args, **kwargs)
def do_bash_completion(self, _args):
"""
Prints all of the commands and options to stdout so that the
libra.bash_completion script doesn't have to hard code them.
"""
commands = set()
options = set()
for sc_str, sc in self.subcommands.items():
commands.add(sc_str)
for option in sc._optionals._option_string_actions.keys():
options.add(option)
commands.remove('bash-completion')
commands.remove('bash_completion')
print(' '.join(commands | options))
@cliutils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
def do_help(self, args):
"""
Display help about this program or one of its subcommands.
"""
if args.command:
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
raise exc.CommandError("'%s' is not a valid subcommand" %
args.command)
else:
self.parser.print_help()
def main():
try:
if sys.version_info >= (3, 0):
LibraShell().main(sys.argv[1:])
else:
LibraShell().main(map(strutils.safe_decode,
sys.argv[1:]))
except KeyboardInterrupt:
print("... terminating libra client", file=sys.stderr)
sys.exit(130)
except Exception as e:
logger.debug(e, exc_info=1)
message = e.message
if not isinstance(message, six.string_types):
message = str(message)
print("ERROR: %s" % strutils.safe_encode(message), file=sys.stderr)
sys.exit(1)

88
libraclient/utils.py Normal file
View File

@@ -0,0 +1,88 @@
import prettytable
from libraclient.openstack.common import strutils
def _field(orig_field, titled=True):
"""
Allow for writin short-hand field stuff like n='server', dn='Server'
"""
field = {}
aliases = {
'n': 'name',
'dn': 'display',
'f': 'formatter'
}
if isinstance(orig_field, dict):
for alias, name in aliases.items():
if alias in orig_field:
field[name] = orig_field[alias]
elif name in orig_field:
field[name] = orig_field[name]
elif isinstance(orig_field, basestring):
field['name'] = orig_field
elif isinstance(orig_field, tuple):
field['name'], field['display'] = orig_field
if 'display' not in field:
dn = field['name']
if titled:
dn = dn.title()
field['display'] = dn
return field
def _get_fields(obj):
# NOTE: Resource class
try:
i = obj._info
except AttributeError:
pass
# NOTE: dict
if isinstance(obj, dict):
i = obj
return [{'name': n} for n in i.keys()]
def create_row(obj, fields=None, titled=False):
"""
:param obj: a :class:`dict` or :class:`Resource`
:param fields: A :class:`list` of :class:`dicts` describing fields to do.
Default: obj.keys() if dict
:param formatters: Field formatters.
"""
fields = [_field(f, titled=titled) for f in fields or _get_fields(obj)]
row = []
for field in fields:
if 'formatter' in field:
row.append(field['formatter'](obj[field['name']]))
else:
if isinstance(obj, dict):
fv = obj[field['name']]
elif hasattr(obj, '_info'):
fv = getattr(obj, field['name'])
row.append(fv)
return row
def print_list(objs, fields=None, sort_by=None, titled=False):
# If no fields are given use objs[0]
fields = [_field(f, titled=titled) for f in fields or _get_fields(objs[0])]
# Set the display names for headers.
field_names = [f['display'] for f in fields]
# Sort by column
if sort_by is None:
sortby = None
else:
sortby = fields[sortby_index]
pt = prettytable.PrettyTable(field_names, caching=False)
pt.align = 'l'
for o in objs:
row = create_row(o, fields=fields, titled=False)
pt.add_row(row)
if objs:
print(strutils.safe_encode(pt.get_string(sortby=sortby)))

View File

@@ -0,0 +1,13 @@
# Copyright 2012 Hewlett-Packard Development Company, L.P.
#
# 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.

View File

@@ -0,0 +1,14 @@
from libraclient.openstack.common.apiclient import base
from libraclient.v1_1.base import Manager
class Algorithm(base.Resource):
def __repr__(self):
return '<Algorithm: %s>' % self.name
class AlgorithmManager(Manager):
resource_class = Algorithm
def list(self):
return self._list('/algorithms', 'algorithms')

11
libraclient/v1_1/base.py Normal file
View File

@@ -0,0 +1,11 @@
from urllib import urlencode
from libraclient.openstack.common.apiclient.base import ManagerWithFind
class Manager(ManagerWithFind):
def build_url(self, url, params):
q = urlencode(params) if params else ''
return '%(url)s%(params)s' % {
'url': url,
'params': '?%s' % q
}

View File

@@ -0,0 +1,55 @@
from libraclient.openstack.common.apiclient import client
from libraclient.openstack.common.apiclient import exceptions
from libraclient.v1_1.algorithms import AlgorithmManager
from libraclient.v1_1.loadbalancer import LoadBalancerManager
from libraclient.v1_1.limits import LimitManager
from libraclient.v1_1.protocols import ProtocolManager
# NOTE(LinuxJedi): Override novaclient's error handler as we send messages in
# a slightly different format which causes novaclient's to throw an exception
def from_response(response, body, url, method=None):
"""
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 = exceptions._code_map.get(
response.status_code, exceptions.ClientException
)
if response.headers:
request_id = response.headers.get('x-compute-request-id')
else:
request_id = None
if body:
message = "n/a"
details = "n/a"
if hasattr(body, 'keys'):
message = body.get('faultstring', None)
if not message:
message = body.get('message', None)
details = body.get('details', None)
return cls(http_status=response.status_code, message=message,
details=details, request_id=request_id, url=url,
method=method)
else:
return cls(request_id=request_id, url=url,
method=method)
exceptions.from_response = from_response
class Client(client.BaseClient):
def __init__(self, *args, **kw):
super(Client, self).__init__(*args, **kw)
self.algorithms = AlgorithmManager(self)
self.loadbalancers = LoadBalancerManager(self)
self.limits = LimitManager(self)
self.protocols = ProtocolManager(self)

View File

@@ -0,0 +1,19 @@
from libraclient.openstack.common.apiclient import base
class Limit(base.Resource):
def __repr__(self):
return '<Limit: %s>' % self.name
class LimitManager(base.BaseManager):
resource_class = Limit
def list_limits(self):
limits = []
json = self.client.get('/limits').json()
for lname, lvalues in json['limits'].items():
values = lvalues['values']
values['name'] = lname
limits.append(Limit(self, values))
return limits

View File

@@ -0,0 +1,288 @@
from libraclient.openstack.common.apiclient.base import getid, Resource
from libraclient.v1_1.base import Manager
import socket
class Node(Resource):
def __repr__(self):
return '<Node: %s' % self.manager.format_node(self._info)
class Monitor(Resource):
def __repr__(self):
return '<Monitor: %s>' % self.name
class LoadBalancer(Resource):
def __repr__(self):
return '<LoadBalancer: %s>' % self.name
def delete(self):
self.manager.delete(self)
def update(self, **kw):
self.manager.update(self, **kw)
def create_node(self, node):
return self.manager.create_node(node)
def list_nodes(self):
return self.manager.list_nodes(self)
def get_node(self, node):
return self.manager.get_node(self, node)
def update_node(self, node, condition=None, weight=None):
return self.manager.update_node(self, node,
condition=condition, weight=weight)
def delete_node(self, node):
return self.manager.delete_node(self, node)
def update_monitor(self, type_='CONNECT', delay=30, timeout=30,
attempts=2, path=None):
return self.manager.update_monitor(
self, type_=type_, delay=delay, timeout=timeout, attempts=attempts,
path=path)
def delete_monitor(self, lb):
self.manager.delete_monitor(lb)
def list_vip(self):
return self.manager.list_vip(self)
class LoadBalancerManager(Manager):
resource_class = LoadBalancer
@staticmethod
def format_node(node):
return '{status}/{condition} - {id} - {address}:{port}'.format(**node)
def _parse_nodes(self, nodes):
out_nodes = []
try:
for node in nodes:
nodeopts = node.split(':')
ipaddr = nodeopts[0]
port = nodeopts[1]
weight, backup = None, None
# Test IP valid
# TODO: change to pton when we want to support IPv6
socket.inet_aton(ipaddr)
# Test port valid
if int(port) < 0 or int(port) > 65535:
raise Exception('Port out of range')
# Process the rest of the node options as key=value
for kv in nodeopts[2:]:
key, value = kv.split('=')
key = key.lower()
value = value.upper()
if key == 'weight':
weight = int(value)
elif key == 'backup':
backup = value # 'TRUE' or 'FALSE'
else:
raise Exception("Unknown node option '%s'" % key)
node_def = {'address': ipaddr, 'port': port}
if weight:
node_def['weight'] = weight
if backup:
node_def['backup'] = backup
out_nodes.append(node_def)
except Exception as e:
raise Exception("Invalid value specified for --node: %s" % e)
return out_nodes
def create(self, name, nodes, port=None, protocol=None, algorithm=None,
virtual_ip=None):
"""
Create a LoadBalancer from given values.
:param name: The name / display name.
:param nodes: Nodes.
:param port: Numeric port (80, 443 for example.)
:param protocol: Protocol to use (TCP / HTTP for example.)
:param algorithm: Algorithm (ROUND_ROBIN for example.)
:param virtual_ip: VIP ID to set if Shared LB.
"""
parsed_nodes = self._parse_nodes(nodes)
body = {
'name': name,
'nodes': parsed_nodes,
}
if port is not None:
body['port'] = port
if protocol is not None:
body['protocol'] = protocol
if algorithm is not None:
body['algorithm'] = algorithm
if virtual_ip is not None:
body['virtualIps'] = [{'id': virtual_ip}]
return self._post('/loadbalancers', body)
def get(self, lb):
"""
Get a LoadBalancer.
:param lb: The :class:`LoadBalancer` (or its ID) to update.
"""
lb = self._get('/loadbalancers/%s' % getid(lb))
return lb
def list(self, deleted=False):
"""
List loadBalancers.
:param deleted: Show deleted LoadBalancers.
"""
params = {}
if deleted:
params['status'] = 'DELETED'
url = self.build_url('/loadbalancers', params)
lbs = self._list(url, 'loadBalancers')
return lbs
def update(self, lb, name=None, algorithm=None):
"""
Update a LoadBalancer
:param lb: The :class:`LoadBalancer` (or its ID).
:param name: Set the name of the LoadBalancer.
:param algorithm: Algorithm (ROUND_ROBIN for example.)
"""
data = {}
if name is not None:
data['name'] = name
if algorithm is not None:
data['algorithm'] = algorithm
return self._put('/loadbalancers/%s' % getid(lb), data)
def delete(self, lb):
"""
Delete a LoadBalancer
:param lb: The :class:`LoadBalancer` (or its ID).
"""
self._delete('/loadbalancers/%s' % getid(lb))
def create_node(self, lb, node):
data = {}
data['nodes'] = self._parse_nodes(node)
return self._post('/loadbalancers/%s/nodes' % getid(lb),
data, obj_class=Node)
def get_node(self, lb, node):
"""
Get a Node belonging to a LoadBalancer
:param lb: The :class:`LoadBalancer` (or its ID).
:param node: The :class:`Node` (or its ID).
"""
url = '/loadbalancers/%s/nodes/%s' % (getid(lb), getid(node))
return self._get(url, obj_class=Node)
def list_nodes(self, lb):
"""
List Nodes belonging to a LoadBalancer.
:param lb: The :class:`LoadBalancer` (or its ID)..
"""
url = '/loadbalancers/%s/nodes' % getid(lb)
return self._list(url, 'nodes', obj_class=Node)
def update_node(self, lb, node, condition=None, weight=None):
"""
Update a node
:param lb: The :class:`LoadBalancer` (or its ID).
:param node: The :class:`Node` (or its ID).
:param condition: Set the conditioon.
:param weight: Set the weight.
"""
data = {}
if condition is not None:
data['condition'] = condition
if weight is not None:
data['weight'] = weight
url = '/loadbalancers/%s/nodes/%s' % (getid(lb), getid(node))
return self._put(url, data, obj_class=Node)
def delete_node(self, lb, node):
"""
Delete a node from a LoadBalancer.
:param lb: The :class:`LoadBalancer` (or its ID).
:param node: The :class:`Node` (or its ID).
"""
url = '/loadbalancers/%s/nodes/%s' % (getid(lb), getid(node))
self._delete(url)
def get_monitor(self, lb):
"""
Get a Monitor for a LoadBalancer
:param lb: The :class:`LoadBalancer` (or its ID).
"""
url = '/loadbalancers/%s/healthmonitor' % getid(lb)
return self._get(url, obj_class=Monitor)
def update_monitor(self, lb, type_='CONNECT', delay=30, timeout=30,
attempts=2, path=None):
"""
Update a Monitor in a LoadBalancer
:param lb: The :class:`LoadBalancer` (or its ID).
:param type_: Monitor type.
:param delay: Numeric delay, must be less then timeout.
:param timeout: Numeric timeout, must be geater then delay.
:param attempts: Max attempts before deactivation.
:param path: URI path when using HTTP type.
"""
data = {}
data['type'] = type_
if timeout > delay:
raise ValueError('Timeout can\'t be greater then Delay')
data['delay'] = delay
data['timeout'] = timeout
data['attemptsBeforeDeactivation'] = attempts
if type_.upper() != 'CONNECT':
data['path'] = path
url = '/loadbalancers/%s/healthmonitor' % getid(lb)
return self._put(url, data, obj_class=Monitor)
def delete_monitor(self, lb):
"""
Delete monitor from a LoadBalancer
:param lb: The :class:`LoadBalancer` (or its ID).
"""
url = '/loadbalancers/%s/healthmonitor' % getid(lb)
self._delete(url)
def list_vip(self, lb):
"""
List Virtual IPs for a LoadBalancer
:param lb: The :class:`LoadBalancer` (or its ID) to update.
"""
res = self.client.get('loadbalancers/%s/virtualips' % getid(lb))
return res.json()['virtualIps']
def send_logs(self, lb, **kw):
"""
Send a snapshot of logs somewhere.
:param lb: The :class:`LoadBalancer` (or its ID).
:param storage: Storage type.
:param kw: The values to send it with, pass as kw.
"""
self._post('/loadbalancers/%s/logs' % getid(lb), kw)

View File

@@ -0,0 +1,14 @@
from libraclient.openstack.common.apiclient import base
from libraclient.v1_1.base import Manager
class Protocol(base.Resource):
def __repr__(self):
return '<Protocol: %s>' % self.name
class ProtocolManager(Manager):
resource_class = Protocol
def list(self):
return self._list('/protocols', 'protocols')

289
libraclient/v1_1/shell.py Normal file
View File

@@ -0,0 +1,289 @@
from libraclient.openstack.common import cliutils
from libraclient import utils
NODE_FIELDS = ["id", "address", "port", "condition", "status"]
def _format_nodes(nodes):
return "\n".join(map(cs.loadbalancers.format_node, lb._info['nodes']))
@cliutils.arg(
'--name',
type=str,
help='Name of the new LoadBalancer.',
required=True)
@cliutils.arg(
'--port',
help='port for the load balancer, 80 is default')
@cliutils.arg(
'--protocol',
help='protocol for the load balancer, HTTP is default',
choices=['HTTP', 'TCP', 'GALERA'])
@cliutils.arg(
'--algorithm',
help='algorithm for the load balancer ROUND_ROBIN is default',
choices=['LEAST_CONNECTIONS', 'ROUND_ROBIN'])
@cliutils.arg(
'--node',
help='a node for the load balancer in ip:port format',
action='append', required=True)
@cliutils.arg(
'--vip',
help='the virtual IP to attach the load balancer to')
def do_create(cs, args):
data = {}
data['name'] = args.name
lb = cs.loadbalancers.create(
name=args.name,
port=args.port,
protocol=args.protocol,
algorithm=args.algorithm,
nodes=args.node,
virtual_ip=args.vip)
cliutils.print_dict(lb._info)
@cliutils.arg(
'id',
type=str,
help='ID to get')
def do_show(cs, args):
lb = cs.loadbalancers.get(args.id)
info = {}
info.update(lb._info)
info['nodes'] = _format_nodes(info['nodes'])
cliutils.print_dict(info, 'Load Balancer')
@cliutils.arg(
'--deleted',
default=False,
action='store_true',
help='Display deleted LBs.')
def do_list(cs, args):
lbs = cs.loadbalancers.list(deleted=args.deleted)
fields = [
('id', 'ID'),
'name',
'protocol',
'port',
'status',
'algorithm',
'created',
'updated',
('nodeCount', 'Node Count')]
utils.print_list(lbs, fields=fields, titled=True)
@cliutils.arg('id', help='load balancer ID')
@cliutils.arg('--name', help='new name for the load balancer')
@cliutils.arg('--algorithm',
help='new algorithm for the load balancer',
choices=['LEAST_CONNECTIONS', 'ROUND_ROBIN'])
def do_update(cs, args):
cs.loadbalancers.update(
args.id,
name=args.name,
algorithm=args.algorithm)
do_modify = do_update
@cliutils.arg(
'id',
type=int,
help='ID to delete')
def do_delete(cs, args):
cs.loadbalancers.delete(args.id)
@cliutils.arg('id', help='load balancer ID')
@cliutils.arg('--node', help='node to add in ip:port form',
required=True, action='append')
def do_node_add(cs, args):
nodes = cs.loadbalancers.create_node(args.id, args.node)
cs.print_list(nodes, fields=NODE_FIELDS)
@cliutils.arg(
'lb_id',
type=str,
help='ID of the LoadBalancer the Nodes belongs to.')
def do_node_list(cs, args):
"""
List LoadBalancer Nodes.
"""
nodes = cs.loadbalancers.list_nodes(args.lb_id)
fields = ["id", "address", "port", "condition", "status"]
utils.print_list(nodes, fields=fields, titled=True)
@cliutils.arg(
'lb_id',
type=str,
help='ID of the LoadBalancer that the Node belongs to.')
@cliutils.arg(
'node_id',
type=str,
help='ID of the Node to show.')
def do_node_show(cs, args):
"""
Show a Node belonging to a LoadBalancer.
"""
node = cs.loadbalancers.get_node(args.lb_id, args.node_id)
cliutils.print_dict(node._info)
@cliutils.arg(
'lb_id',
type=str,
help='ID of the LoadBalancer the Nodes belongs to.')
@cliutils.arg(
'node_id',
help='node ID to modify')
@cliutils.arg(
'--condition',
help='the new state for the node',
choices=['ENABLED', 'DISABLED'])
@cliutils.arg(
'--weight',
type=int,
default=1,
metavar='COUNT',
help='node weight ratio as compared to other nodes')
def do_node_update(cs, args):
cs.loadbalancers.update_node(
args.lb_id, args.node_id, condition=args.condition, weight=args.weight)
@cliutils.arg(
'lb_id',
type=str,
help='ID of the LoadBalancer that the Node belongs to.')
@cliutils.arg(
'node_id',
type=str,
help='ID of the Node to show.')
def do_node_delete(cs, args):
"""
Delete a Node belonging to a LoadBalancer
"""
cs.loadbalancers.delete_node(args.lb_id, args.node_id)
@cliutils.arg(
'lb_id',
type=str,
help='ID of the LoadBalancer that the Node belongs to.')
def do_momitor_show(cs, args):
monitor = cs.loadbalancers.get_monitor(args.lb_id)
cliutils.print_dict(monitor._info)
@cliutils.arg(
'lb_id',
type=str,
help='ID of the LoadBalancer that the Node belongs to.')
@cliutils.arg(
'--type',
choices=['CONNECT', 'HTTP'],
default='CONNECT',
help='health monitor type')
@cliutils.arg(
'--delay',
type=int,
default=30,
metavar='SECONDS',
help='time between health monitor calls')
@cliutils.arg(
'--timeout',
type=int,
default=30,
metavar='SECONDS',
help='time to wait before monitor times out')
@cliutils.arg(
'--attempts',
type=int,
default=2,
metavar='COUNT',
help='connection attempts before marking node as bad')
@cliutils.arg(
'--path',
help='URI path for health check')
def do_monitor_update(cs, args):
monitor = cs.loadbalancers.update_monitor(
args.lb_id, type_=args.type, delay=args.delay, timeout=args.timeout,
attempts=args.attempts, path=args.path)
cliutils.print_dict(monitor._info)
@cliutils.arg(
'lb_id',
type=str,
help='ID of the LoadBalancer that the Node belongs to.')
def do_monitor_delete(cs, args):
cs.loadbalancers.delete_monitor(args.lb_id)
@cliutils.arg(
'id',
type=str,
help='ID to show Virtual IPs for.')
def do_virtualips(cs, args):
vips = cs.loadbalancers.list_vip(args.id)
fields = [
'id',
'type',
('ipVersion', 'IP Version'),
'address'
]
utils.print_list(vips, fields=fields, titled=True)
# Non LB specific commands
def do_algorithm_list(cs, args):
algs = cs.algorithms.list()
fields = [('name', 'Algorithm Name')]
utils.print_list(algs, fields=fields)
# TODO: Figure out the printing of this one
def do_limit_list(cs, args):
limits = cs.limits.list_limits()
out = []
for l in limits:
info = l._info
del info['name']
info = "\n".join(['%s: %s' % (k, info[k])
for k in sorted(info.keys())])
out.append({'name': l.name, 'info': info})
fields = ['name', 'info']
utils.print_list(out, fields=fields)
def do_protocol_list(cs, args):
protocols = cs.protocols.list()
utils.print_list(protocols, titled=True)
@cliutils.arg('id', help='load balancer ID')
@cliutils.arg('--storage', help='storage type', choices=['Swift'])
@cliutils.arg('--endpoint', help='object store endpoint to use')
@cliutils.arg('--basepath', help='object store based directory')
@cliutils.arg('--token', help='object store authentication token')
def do_logs(cs, args):
data = {}
if args.storage:
data['objectStoreType'] = args.storage
if args.endpoint:
data['objectStoreEndpoint'] = args.endpoint
if args.basepath:
data['objectStoreBasePath'] = args.basepath
if args.token:
data['authToken'] = args.token
cs.loadbalancers.send_logs(args.id, data)

View File

@@ -1,7 +1,9 @@
[DEFAULT] [DEFAULT]
# The list of modules to copy from openstack-common # The list of modules to copy from openstack-common
modules=importutils,setup module=apiclient
module=cliutils
module=strutils
# The base module to hold the copy of openstack.common # The base module to hold the copy of openstack.common
base= base=libraclient

View File

View File

@@ -1,360 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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.
"""
Utilities with minimum-depends for use in setup.py
"""
import datetime
import os
import re
import subprocess
import sys
from setuptools.command import sdist
def parse_mailmap(mailmap='.mailmap'):
mapping = {}
if os.path.exists(mailmap):
with open(mailmap, 'r') as fp:
for l in fp:
l = l.strip()
if not l.startswith('#') and ' ' in l:
canonical_email, alias = [x for x in l.split(' ')
if x.startswith('<')]
mapping[alias] = canonical_email
return mapping
def canonicalize_emails(changelog, mapping):
"""Takes in a string and an email alias mapping and replaces all
instances of the aliases in the string with their real email.
"""
for alias, email in mapping.iteritems():
changelog = changelog.replace(alias, email)
return changelog
# Get requirements from the first file that exists
def get_reqs_from_files(requirements_files):
for requirements_file in requirements_files:
if os.path.exists(requirements_file):
with open(requirements_file, 'r') as fil:
return fil.read().split('\n')
return []
def parse_requirements(requirements_files=['requirements.txt',
'tools/pip-requires']):
requirements = []
for line in get_reqs_from_files(requirements_files):
# For the requirements list, we need to inject only the portion
# after egg= so that distutils knows the package it's looking for
# such as:
# -e git://github.com/openstack/nova/master#egg=nova
if re.match(r'\s*-e\s+', line):
requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
line))
# such as:
# http://github.com/openstack/nova/zipball/master#egg=nova
elif re.match(r'\s*https?:', line):
requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
line))
# -f lines are for index locations, and don't get used here
elif re.match(r'\s*-f\s+', line):
pass
# argparse is part of the standard library starting with 2.7
# adding it to the requirements list screws distro installs
elif line == 'argparse' and sys.version_info >= (2, 7):
pass
else:
requirements.append(line)
return requirements
def parse_dependency_links(requirements_files=['requirements.txt',
'tools/pip-requires']):
dependency_links = []
# dependency_links inject alternate locations to find packages listed
# in requirements
for line in get_reqs_from_files(requirements_files):
# skip comments and blank lines
if re.match(r'(\s*#)|(\s*$)', line):
continue
# lines with -e or -f need the whole line, minus the flag
if re.match(r'\s*-[ef]\s+', line):
dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
# lines that are only urls can go in unmolested
elif re.match(r'\s*https?:', line):
dependency_links.append(line)
return dependency_links
def write_requirements():
venv = os.environ.get('VIRTUAL_ENV', None)
if venv is not None:
with open("requirements.txt", "w") as req_file:
output = subprocess.Popen(["pip", "-E", venv, "freeze", "-l"],
stdout=subprocess.PIPE)
requirements = output.communicate()[0].strip()
req_file.write(requirements)
def _run_shell_command(cmd):
output = subprocess.Popen(["/bin/sh", "-c", cmd],
stdout=subprocess.PIPE)
out = output.communicate()
if len(out) == 0:
return None
if len(out[0].strip()) == 0:
return None
return out[0].strip()
def _get_git_next_version_suffix(branch_name):
datestamp = datetime.datetime.now().strftime('%Y%m%d')
if branch_name == 'milestone-proposed':
revno_prefix = "r"
else:
revno_prefix = ""
_run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*")
milestone_cmd = "git show meta/openstack/release:%s" % branch_name
milestonever = _run_shell_command(milestone_cmd)
if not milestonever:
milestonever = ""
post_version = _get_git_post_version()
# post version should look like:
# 0.1.1.4.gcc9e28a
# where the bit after the last . is the short sha, and the bit between
# the last and second to last is the revno count
(revno, sha) = post_version.split(".")[-2:]
first_half = "%s~%s" % (milestonever, datestamp)
second_half = "%s%s.%s" % (revno_prefix, revno, sha)
return ".".join((first_half, second_half))
def _get_git_current_tag():
return _run_shell_command("git tag --contains HEAD")
def _get_git_tag_info():
return _run_shell_command("git describe --tags")
def _get_git_post_version():
current_tag = _get_git_current_tag()
if current_tag is not None:
return current_tag
else:
tag_info = _get_git_tag_info()
if tag_info is None:
base_version = "0.0"
cmd = "git --no-pager log --oneline"
out = _run_shell_command(cmd)
revno = len(out.split("\n"))
sha = _run_shell_command("git describe --always")
else:
tag_infos = tag_info.split("-")
base_version = "-".join(tag_infos[:-2])
(revno, sha) = tag_infos[-2:]
return "%s.%s.%s" % (base_version, revno, sha)
def write_git_changelog():
"""Write a changelog based on the git changelog."""
new_changelog = 'ChangeLog'
if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
if os.path.isdir('.git'):
git_log_cmd = 'git log --stat'
changelog = _run_shell_command(git_log_cmd)
mailmap = parse_mailmap()
with open(new_changelog, "w") as changelog_file:
changelog_file.write(canonicalize_emails(changelog, mailmap))
else:
open(new_changelog, 'w').close()
def generate_authors():
"""Create AUTHORS file using git commits."""
jenkins_email = 'jenkins@review.(openstack|stackforge).org'
old_authors = 'AUTHORS.in'
new_authors = 'AUTHORS'
if not os.getenv('SKIP_GENERATE_AUTHORS'):
if os.path.isdir('.git'):
# don't include jenkins email address in AUTHORS file
git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | "
"egrep -v '" + jenkins_email + "'")
changelog = _run_shell_command(git_log_cmd)
mailmap = parse_mailmap()
with open(new_authors, 'w') as new_authors_fh:
new_authors_fh.write(canonicalize_emails(changelog, mailmap))
if os.path.exists(old_authors):
with open(old_authors, "r") as old_authors_fh:
new_authors_fh.write('\n' + old_authors_fh.read())
else:
open(new_authors, 'w').close()
_rst_template = """%(heading)s
%(underline)s
.. automodule:: %(module)s
:members:
:undoc-members:
:show-inheritance:
"""
def read_versioninfo(project):
"""Read the versioninfo file. If it doesn't exist, we're in a github
zipball, and there's really no way to know what version we really
are, but that should be ok, because the utility of that should be
just about nil if this code path is in use in the first place."""
versioninfo_path = os.path.join(project, 'versioninfo')
if os.path.exists(versioninfo_path):
with open(versioninfo_path, 'r') as vinfo:
version = vinfo.read().strip()
else:
version = "0.0.0"
return version
def write_versioninfo(project, version):
"""Write a simple file containing the version of the package."""
with open(os.path.join(project, 'versioninfo'), 'w') as fil:
fil.write("%s\n" % version)
def get_cmdclass():
"""Return dict of commands to run from setup.py."""
cmdclass = dict()
def _find_modules(arg, dirname, files):
for filename in files:
if filename.endswith('.py') and filename != '__init__.py':
arg["%s.%s" % (dirname.replace('/', '.'),
filename[:-3])] = True
class LocalSDist(sdist.sdist):
"""Builds the ChangeLog and Authors files from VC first."""
def run(self):
write_git_changelog()
generate_authors()
# sdist.sdist is an old style class, can't use super()
sdist.sdist.run(self)
cmdclass['sdist'] = LocalSDist
# If Sphinx is installed on the box running setup.py,
# enable setup.py to build the documentation, otherwise,
# just ignore it
try:
from sphinx.setup_command import BuildDoc
class LocalBuildDoc(BuildDoc):
def generate_autoindex(self):
print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
modules = {}
option_dict = self.distribution.get_option_dict('build_sphinx')
source_dir = os.path.join(option_dict['source_dir'][1], 'api')
if not os.path.exists(source_dir):
os.makedirs(source_dir)
for pkg in self.distribution.packages:
if '.' not in pkg:
os.path.walk(pkg, _find_modules, modules)
module_list = modules.keys()
module_list.sort()
autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
with open(autoindex_filename, 'w') as autoindex:
autoindex.write(""".. toctree::
:maxdepth: 1
""")
for module in module_list:
output_filename = os.path.join(source_dir,
"%s.rst" % module)
heading = "The :mod:`%s` Module" % module
underline = "=" * len(heading)
values = dict(module=module, heading=heading,
underline=underline)
print "Generating %s" % output_filename
with open(output_filename, 'w') as output_file:
output_file.write(_rst_template % values)
autoindex.write(" %s.rst\n" % module)
def run(self):
if not os.getenv('SPHINX_DEBUG'):
self.generate_autoindex()
for builder in ['html', 'man']:
self.builder = builder
self.finalize_options()
self.project = self.distribution.get_name()
self.version = self.distribution.get_version()
self.release = self.distribution.get_version()
BuildDoc.run(self)
cmdclass['build_sphinx'] = LocalBuildDoc
except ImportError:
pass
return cmdclass
def get_git_branchname():
for branch in _run_shell_command("git branch --color=never").split("\n"):
if branch.startswith('*'):
_branch_name = branch.split()[1].strip()
if _branch_name == "(no":
_branch_name = "no-branch"
return _branch_name
def get_pre_version(projectname, base_version):
"""Return a version which is leading up to a version that will
be released in the future."""
if os.path.isdir('.git'):
current_tag = _get_git_current_tag()
if current_tag is not None:
version = current_tag
else:
branch_name = os.getenv('BRANCHNAME',
os.getenv('GERRIT_REFNAME',
get_git_branchname()))
version_suffix = _get_git_next_version_suffix(branch_name)
version = "%s~%s" % (base_version, version_suffix)
write_versioninfo(projectname, version)
return version
else:
version = read_versioninfo(projectname)
return version
def get_post_version(projectname):
"""Return a version which is equal to the tag that's on the current
revision if there is one, or tag plus number of additional revisions
if the current revision has no tag."""
if os.path.isdir('.git'):
version = _get_git_post_version()
write_versioninfo(projectname, version)
return version
return read_versioninfo(projectname)

View File

@@ -1 +1,3 @@
python_novaclient>=2.14.1,<2.14.2 python_novaclient>=2.14.1,<2.14.2
babel
stevedore

View File

@@ -24,7 +24,16 @@ packages =
[entry_points] [entry_points]
console_scripts = console_scripts =
libra_client = libraclient.client:main libra = libraclient.shell:main
libraclient.versions =
1.1 = libraclient.v1_1.client:Client
libraclient.extension =
openstack.common.apiclient.auth =
keystone2 = libraclient.openstack.common.apiclient.keystone:KeystoneAuthPluginV2
keystone3 = libraclient.openstack.common.apiclient.keystone:KeystoneAuthPluginV3
[build_sphinx] [build_sphinx]
source-dir = doc source-dir = doc