Migrate client code from libra codebase
Change-Id: Icd9d758e45c7167c5b8db7aa2c5e15b4bb93c766
This commit is contained in:
parent
775f861346
commit
a861024bed
11
.gitreview
11
.gitreview
@ -1,6 +1,5 @@
|
||||
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=stackforge/python-libraclient.git
|
||||
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=stackforge/python-libraclient.git
|
||||
|
||||
|
4
.testr.conf
Normal file
4
.testr.conf
Normal file
@ -0,0 +1,4 @@
|
||||
[DEFAULT]
|
||||
test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
9
MANIFEST.in
Normal file
9
MANIFEST.in
Normal file
@ -0,0 +1,9 @@
|
||||
include README
|
||||
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
||||
global-exclude *.pyc
|
||||
|
||||
graft doc
|
||||
graft tools
|
6
build_pdf.sh
Executable file
6
build_pdf.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
python setup.py build_sphinx_latex
|
||||
# Fix option double dashes in latex output
|
||||
perl -i -pe 's/\\bfcode\{--(.*)\}/\\bfcode\{-\{\}-\1\}/g' build/sphinx/latex/*.tex
|
||||
perl -i -pe 's/\\index\{(.*?)--(.*?)\}/\\index\{\1-\{\}-\2\}/g' build/sphinx/latex/*.tex
|
||||
make -C build/sphinx/latex all-pdf
|
15
client/__init__.py
Normal file
15
client/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
|
||||
__version__ = "1.0"
|
36
client/client.py
Normal file
36
client/client.py
Normal file
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
|
||||
from libraapi import LibraAPI
|
||||
from clientoptions import ClientOptions
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
def main():
|
||||
options = ClientOptions()
|
||||
args = options.run()
|
||||
|
||||
api = LibraAPI(args.os_username, args.os_password, args.os_tenant_name,
|
||||
args.os_auth_url, args.os_region_name, args.insecure,
|
||||
args.debug, args.bypass_url)
|
||||
|
||||
cmd = args.command.replace('-', '_')
|
||||
method = getattr(api, '{cmd}_lb'.format(cmd=cmd))
|
||||
|
||||
try:
|
||||
method(args)
|
||||
except exceptions.ClientException as exc:
|
||||
print exc
|
||||
|
||||
return 0
|
154
client/clientoptions.py
Normal file
154
client/clientoptions.py
Normal file
@ -0,0 +1,154 @@
|
||||
# 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
|
||||
|
||||
|
||||
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>',
|
||||
required=True,
|
||||
help='Authentication URL'
|
||||
)
|
||||
self.options.add_argument(
|
||||
'--os_username',
|
||||
metavar='<auth-user-name>',
|
||||
required=True,
|
||||
help='Authentication username'
|
||||
)
|
||||
self.options.add_argument(
|
||||
'--os_password',
|
||||
metavar='<auth-password>',
|
||||
required=True,
|
||||
help='Authentication password'
|
||||
)
|
||||
self.options.add_argument(
|
||||
'--os_tenant_name',
|
||||
metavar='<auth-tenant-name>',
|
||||
required=True,
|
||||
help='Authentication tenant'
|
||||
)
|
||||
self.options.add_argument(
|
||||
'--os_region_name',
|
||||
metavar='<region-name>',
|
||||
required=True,
|
||||
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'
|
||||
)
|
||||
subparsers = self.options.add_subparsers(
|
||||
metavar='<subcommand>', dest='command'
|
||||
)
|
||||
subparsers.add_parser(
|
||||
'limits', help='get account API usage limits'
|
||||
)
|
||||
subparsers.add_parser(
|
||||
'algorithms', help='get a list of supported algorithms'
|
||||
)
|
||||
subparsers.add_parser(
|
||||
'protocols', help='get a list of supported protocols and ports'
|
||||
)
|
||||
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(
|
||||
'delete', help='delete a load balancer'
|
||||
)
|
||||
sp.add_argument('--id', help='load balancer ID', required=True)
|
||||
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'])
|
||||
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(
|
||||
'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(
|
||||
'status', help='get status of a load balancer'
|
||||
)
|
||||
sp.add_argument('--id', help='load balancer ID', 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-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-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-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'], required=True)
|
||||
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)
|
||||
|
||||
def run(self):
|
||||
self._generate()
|
||||
return self.options.parse_args()
|
217
client/libraapi.py
Normal file
217
client/libraapi.py
Normal file
@ -0,0 +1,217 @@
|
||||
# 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
|
||||
|
||||
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):
|
||||
"""
|
||||
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, novaclient.exceptions.ClientException
|
||||
)
|
||||
request_id = response.get('x-compute-request-id')
|
||||
if body:
|
||||
message = "n/a"
|
||||
details = "n/a"
|
||||
if hasattr(body, 'keys'):
|
||||
message = body.get('message', None)
|
||||
details = body.get('details', None)
|
||||
return cls(code=response.status, message=message, details=details,
|
||||
request_id=request_id)
|
||||
else:
|
||||
return cls(code=response.status, request_id=request_id)
|
||||
|
||||
novaclient.exceptions.from_response = from_response
|
||||
|
||||
|
||||
class LibraAPI(object):
|
||||
def __init__(self, username, password, tenant, auth_url, region,
|
||||
insecure, debug, bypass_url):
|
||||
self.nova = client.HTTPClient(
|
||||
username,
|
||||
password,
|
||||
tenant,
|
||||
auth_url,
|
||||
region_name=region,
|
||||
service_type='compute',
|
||||
http_log_debug=debug,
|
||||
insecure=insecure,
|
||||
bypass_url=bypass_url
|
||||
)
|
||||
|
||||
def limits_lb(self, args):
|
||||
resp, body = self._get('/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']
|
||||
columns = ['name', 'id', 'protocol', 'port', 'algorithm', 'status',
|
||||
'created', 'updated']
|
||||
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', 'Created', 'Updated', 'IPs', 'Nodes',
|
||||
'Persistence Type', 'Connection Throttle']
|
||||
columns = ['id', 'name', 'protocol', 'port', 'algorithm', 'status',
|
||||
'created', 'updated', 'virtualIps', 'nodes',
|
||||
'sessionPersistence', 'connectionThrottle']
|
||||
if 'sessionPersistence' not in body:
|
||||
body['sessionPersistence'] = 'None'
|
||||
if 'connectionThrottle' not in body:
|
||||
body['connectionThrottle'] = 'None'
|
||||
self._render_dict(column_names, columns, body)
|
||||
|
||||
def delete_lb(self, args):
|
||||
self._delete('/loadbalancers/{0}'.format(args.id))
|
||||
|
||||
def create_lb(self, args):
|
||||
data = {}
|
||||
nodes = []
|
||||
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
|
||||
for node in args.node:
|
||||
addr = node.split(':')
|
||||
nodes.append({'address': addr[0], 'port': addr[1],
|
||||
'condition': 'ENABLED'})
|
||||
data['nodes'] = nodes
|
||||
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', 'Status']
|
||||
columns = ['id', 'address', 'port', 'condition', '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 = {}
|
||||
nodes = []
|
||||
|
||||
for node in args.node:
|
||||
addr = node.split(':')
|
||||
nodes.append({'address': addr[0], 'port': addr[1],
|
||||
'condition': 'ENABLED'})
|
||||
data['nodes'] = nodes
|
||||
resp, body = self._post('/loadbalancers/{0}/nodes'
|
||||
.format(args.id), body=data)
|
||||
column_names = ['ID', 'Address', 'Port', 'Condition', 'Status']
|
||||
columns = ['id', 'address', 'port', 'condition', 'status']
|
||||
self._render_list(column_names, columns, body['nodes'])
|
||||
|
||||
def node_modify_lb(self, args):
|
||||
data = {'condition': args.condition}
|
||||
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', 'Status']
|
||||
columns = ['id', 'address', 'port', 'condition', 'status']
|
||||
self._render_dict(column_names, columns, body)
|
||||
|
||||
def _render_list(self, column_names, columns, data):
|
||||
table = prettytable.PrettyTable(column_names)
|
||||
for item in data:
|
||||
row = []
|
||||
for column in columns:
|
||||
rdata = item[column]
|
||||
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:
|
||||
rdata = data[column]
|
||||
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)
|
237
doc/command.rst
Normal file
237
doc/command.rst
Normal file
@ -0,0 +1,237 @@
|
||||
Libra Client
|
||||
============
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
|
||||
:program:`libra_client` [:ref:`GENERAL OPTIONS <libra_client-options>`] [:ref:`COMMAND <libra_client-commands>`] [*COMMAND_OPTIONS*]
|
||||
|
||||
Description
|
||||
-----------
|
||||
|
||||
:program:`libra_client` is a utility designed to communicate with Atlas API
|
||||
based Load Balancer as a Service systems.
|
||||
|
||||
.. _libra_client-options:
|
||||
|
||||
Global Options
|
||||
--------------
|
||||
|
||||
.. program:: libra_client
|
||||
|
||||
.. option:: --help, -h
|
||||
|
||||
Show help message and exit
|
||||
|
||||
.. option:: --debug
|
||||
|
||||
Turn on HTTP debugging for requests
|
||||
|
||||
.. option:: --insecure
|
||||
|
||||
Don't validate SSL certs
|
||||
|
||||
.. option:: --bypass_url <bypass-url>
|
||||
|
||||
URL to use as an endpoint instead of the one specified by the Service
|
||||
Catalog
|
||||
|
||||
.. option:: --os_auth_url <auth-url>
|
||||
|
||||
The OpenStack authentication URL
|
||||
|
||||
.. option:: --os_username <auth-user-name>
|
||||
|
||||
The user name to use for authentication
|
||||
|
||||
.. option:: --os_password <auth-password>
|
||||
|
||||
The password to use for authentication
|
||||
|
||||
.. option:: --os_tenant_name <auth-tenant-name>
|
||||
|
||||
The tenant to authenticate to
|
||||
|
||||
.. option:: --os_region_name <region-name>
|
||||
|
||||
The region the load balancer is located
|
||||
|
||||
.. _libra_client-commands:
|
||||
|
||||
Client Commands
|
||||
---------------
|
||||
|
||||
.. program:: libra_client create
|
||||
|
||||
create
|
||||
^^^^^^
|
||||
|
||||
Create a load balancer
|
||||
|
||||
.. option:: --name <name>
|
||||
|
||||
The name of the node to be created
|
||||
|
||||
.. option:: --port <port>
|
||||
|
||||
The port the load balancer will listen on
|
||||
|
||||
.. option:: --protocol <protocol>
|
||||
|
||||
The protocol type for the load balancer (HTTP or TCP)
|
||||
|
||||
.. option:: --node <ip:port>
|
||||
|
||||
The IP and port for a load balancer node (can be used multiple times to add multiple nodes)
|
||||
|
||||
.. option:: --vip <vip>
|
||||
|
||||
The virtual IP ID of an existing load balancer to attach to
|
||||
|
||||
.. program:: libra_client modify
|
||||
|
||||
modify
|
||||
^^^^^^
|
||||
|
||||
Update a load balancer's configuration
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. option:: --name <name>
|
||||
|
||||
A new name for the load balancer
|
||||
|
||||
.. option:: --algorithm <algorithm>
|
||||
|
||||
A new algorithm for the load balancer
|
||||
|
||||
.. program:: libra_client list
|
||||
|
||||
list
|
||||
^^^^
|
||||
|
||||
List all load balancers
|
||||
|
||||
.. option:: --deleted
|
||||
|
||||
Show deleted load balancers
|
||||
|
||||
.. program:: libra_client limits
|
||||
|
||||
limits
|
||||
^^^^^^
|
||||
|
||||
Show the API limits for the user
|
||||
|
||||
.. program:: libra_client algorithms
|
||||
|
||||
algorithms
|
||||
^^^^^^^^^^
|
||||
|
||||
Gets a list of supported algorithms
|
||||
|
||||
.. program:: libra_client protocols
|
||||
|
||||
protocols
|
||||
^^^^^^^^^
|
||||
|
||||
Gets a list of supported protocols
|
||||
|
||||
.. program:: libra_client status
|
||||
|
||||
status
|
||||
^^^^^^
|
||||
|
||||
Get the status of a single load balancer
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. program:: libra_client delete
|
||||
|
||||
delete
|
||||
^^^^^^
|
||||
|
||||
Delete a load balancer
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. program:: libra_client node-list
|
||||
|
||||
node-list
|
||||
^^^^^^^^^
|
||||
|
||||
List the nodes in a load balancer
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. program:: libra_client node-delete
|
||||
|
||||
node-delete
|
||||
^^^^^^^^^^^
|
||||
|
||||
Delete a node from the load balancer
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. option:: --nodeid <nodeid>
|
||||
|
||||
The ID of the node to be removed
|
||||
|
||||
.. program:: libra_client node-add
|
||||
|
||||
node-add
|
||||
^^^^^^^^
|
||||
|
||||
Add a node to a load balancer
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. option:: --node <ip:port>
|
||||
|
||||
The node address in ip:port format (can be used multiple times to add multiple nodes)
|
||||
|
||||
.. program:: libra_client node-modify
|
||||
|
||||
node-modify
|
||||
^^^^^^^^^^^
|
||||
|
||||
Modify a node's state in a load balancer
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. option:: --nodeid <nodeid>
|
||||
|
||||
The ID of the node to be modified
|
||||
|
||||
.. option:: --condition <condition>
|
||||
|
||||
The new state of the node (either ENABLED or DISABLED)
|
||||
|
||||
.. program:: libra_client node-status
|
||||
|
||||
node-status
|
||||
^^^^^^^^^^^
|
||||
|
||||
Get the status of a node in a load balancer
|
||||
|
||||
.. option:: --id <id>
|
||||
|
||||
The ID of the load balancer
|
||||
|
||||
.. option:: --nodeid <nodeid>
|
||||
|
||||
The ID of the node in the load balancer
|
234
doc/conf.py
Normal file
234
doc/conf.py
Normal file
@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# OpenStack CI documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Jul 18 13:42:23 2011.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing
|
||||
# dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
#extensions = ['rst2pdf.pdfbuilder']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'LBaaS Command Line Client'
|
||||
copyright = u'2012, Andrew Hutchings'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "%d-%02d-%02d-alpha1" % (
|
||||
datetime.datetime.now().year,
|
||||
datetime.datetime.now().month,
|
||||
datetime.datetime.now().day
|
||||
)
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "%d-%02d-%02d-alpha1" % (
|
||||
datetime.datetime.now().year,
|
||||
datetime.datetime.now().month,
|
||||
datetime.datetime.now().day
|
||||
)
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'LibraClent'
|
||||
|
||||
|
||||
# -- Options for LaTeX output -------------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
#latex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#latex_font_size = '10pt'
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'pthon-libraclient-{0}.tex'.format(version), u'Libra Client Documentation',
|
||||
u'Andrew Hutchings', 'manual'),
|
||||
]
|
||||
|
||||
#pdf_documents = [('index', 'Libra-{0}'.format(version), u'Libra Client, Worker and Pool Manager Documentation', u'Andrew Hutchings and David Shrewsbury')]
|
||||
|
||||
#pdf_break_level = 1
|
||||
|
||||
#pdf_stylesheets = ['sphinx', 'libra']
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#latex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output -------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'lbaas', u'LBaaS Client',
|
||||
[u'Andrew Hutchings'], 1)
|
||||
]
|
7
doc/index.rst
Normal file
7
doc/index.rst
Normal file
@ -0,0 +1,7 @@
|
||||
Libra Command Line Client
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
command
|
7
openstack-common.conf
Normal file
7
openstack-common.conf
Normal file
@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from openstack-common
|
||||
modules=importutils,setup
|
||||
|
||||
# The base module to hold the copy of openstack.common
|
||||
base=
|
0
openstack/__init__.py
Normal file
0
openstack/__init__.py
Normal file
0
openstack/common/__init__.py
Normal file
0
openstack/common/__init__.py
Normal file
59
openstack/common/importutils.py
Normal file
59
openstack/common/importutils.py
Normal file
@ -0,0 +1,59 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Import related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class"""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
try:
|
||||
__import__(mod_str)
|
||||
return getattr(sys.modules[mod_str], class_str)
|
||||
except (ValueError, AttributeError), exc:
|
||||
raise ImportError('Class %s cannot be found (%s)' %
|
||||
(class_str,
|
||||
traceback.format_exception(*sys.exc_info())))
|
||||
|
||||
|
||||
def import_object(import_str, *args, **kwargs):
|
||||
"""Import a class and return an instance of it."""
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_object_ns(name_space, import_str, *args, **kwargs):
|
||||
"""
|
||||
Import a class and return an instance of it, first by trying
|
||||
to find the class in a default namespace, then failing back to
|
||||
a full path if not found in the default namespace.
|
||||
"""
|
||||
import_value = "%s.%s" % (name_space, import_str)
|
||||
try:
|
||||
return import_class(import_value)(*args, **kwargs)
|
||||
except ImportError:
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_module(import_str):
|
||||
"""Import a module."""
|
||||
__import__(import_str)
|
||||
return sys.modules[import_str]
|
360
openstack/common/setup.py
Normal file
360
openstack/common/setup.py
Normal file
@ -0,0 +1,360 @@
|
||||
# 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)
|
68
setup.py
Normal file
68
setup.py
Normal file
@ -0,0 +1,68 @@
|
||||
# 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 sys
|
||||
import setuptools
|
||||
from openstack.common import setup
|
||||
|
||||
requires = setup.parse_requirements()
|
||||
tests_requires = setup.parse_requirements(['tools/test-requires'])
|
||||
|
||||
ci_cmdclass = {}
|
||||
|
||||
try:
|
||||
from sphinx.setup_command import BuildDoc
|
||||
|
||||
class local_BuildDoc(BuildDoc):
|
||||
def run(self):
|
||||
builders = ['html', 'man']
|
||||
for builder in builders:
|
||||
self.builder = builder
|
||||
self.finalize_options()
|
||||
BuildDoc.run(self)
|
||||
|
||||
class local_BuildDoc_latex(BuildDoc):
|
||||
def run(self):
|
||||
builders = ['latex']
|
||||
for builder in builders:
|
||||
self.builder = builder
|
||||
self.finalize_options()
|
||||
BuildDoc.run(self)
|
||||
|
||||
ci_cmdclass['build_sphinx'] = local_BuildDoc
|
||||
ci_cmdclass['build_sphinx_latex'] = local_BuildDoc_latex
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
setup_reqs = ['Sphinx']
|
||||
|
||||
execfile('client/__init__.py')
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name="python-libraclient",
|
||||
version=__version__,
|
||||
description="Python client for libra LBaaS solution",
|
||||
author="Andrew Hutchings <andrew@linuxjedi.co.uk>",
|
||||
packages=setuptools.find_packages(exclude=["*.tests"]),
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'libra_client = client.client:main',
|
||||
]
|
||||
},
|
||||
cmdclass=ci_cmdclass,
|
||||
tests_require=tests_requires,
|
||||
install_requires=requires,
|
||||
setup_requires=setup_reqs
|
||||
)
|
13
tests/__init__.py
Normal file
13
tests/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright 2013 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.
|
330
tests/test_lbaas_client.py
Normal file
330
tests/test_lbaas_client.py
Normal file
@ -0,0 +1,330 @@
|
||||
import json
|
||||
import mock
|
||||
import httplib2
|
||||
import sys
|
||||
import novaclient
|
||||
import testtools
|
||||
from StringIO import StringIO
|
||||
from client.libraapi import LibraAPI
|
||||
|
||||
class DummyArgs(object):
|
||||
""" Fake argparse response """
|
||||
def __init__(self):
|
||||
self.id = 2000
|
||||
self.deleted = False
|
||||
|
||||
class DummyCreateArgs(object):
|
||||
""" Fake argparse response for Create function """
|
||||
def __init__(self):
|
||||
self.name = 'a-new-loadbalancer'
|
||||
self.node = ['10.1.1.1:80', '10.1.1.2:81']
|
||||
self.port = None
|
||||
self.protocol = None
|
||||
self.algorithm = None
|
||||
self.vip = None
|
||||
|
||||
class DummyModifyArgs(object):
|
||||
""" Fake argparse response for Modify function """
|
||||
def __init__(self):
|
||||
self.id = 2012
|
||||
self.name = 'a-modified-loadbalancer'
|
||||
self.algorithm = 'LEAST_CONNECTIONS'
|
||||
|
||||
class MockLibraAPI(LibraAPI):
|
||||
""" Used to capture data that would be sent to the API server """
|
||||
def __init__(self, username, password, tenant, auth_url, region):
|
||||
self.postdata = None
|
||||
self.putdata = None
|
||||
return super(MockLibraAPI, self).__init__(username, password, tenant, auth_url, region, False, False, None)
|
||||
def _post(self, url, **kwargs):
|
||||
""" Store the post data and execute as normal """
|
||||
self.postdata = kwargs['body']
|
||||
return super(MockLibraAPI, self)._post(url, **kwargs)
|
||||
def _put(self, url, **kwargs):
|
||||
""" Store the put data, no need to execute the httplib """
|
||||
self.putdata = kwargs['body']
|
||||
|
||||
class TestLBaaSClientLibraAPI(testtools.TestCase):
|
||||
def setUp(self):
|
||||
""" Fake a login with token """
|
||||
super(TestLBaaSClientLibraAPI, self).setUp()
|
||||
self.api = MockLibraAPI('username', 'password', 'tenant', 'auth_test', 'region')
|
||||
self.api.nova.management_url = "http://example.com"
|
||||
self.api.nova.auth_token = "token"
|
||||
|
||||
def testListLb(self):
|
||||
""" Test the table generated from the LIST function """
|
||||
fake_response = httplib2.Response({"status": '200'})
|
||||
fake_body = json.dumps({
|
||||
"loadBalancers":[
|
||||
{
|
||||
"name":"lb-site1",
|
||||
"id":"71",
|
||||
"protocol":"HTTP",
|
||||
"port":"80",
|
||||
"algorithm":"LEAST_CONNECTIONS",
|
||||
"status":"ACTIVE",
|
||||
"created":"2010-11-30T03:23:42Z",
|
||||
"updated":"2010-11-30T03:23:44Z"
|
||||
},
|
||||
{
|
||||
"name":"lb-site2",
|
||||
"id":"166",
|
||||
"protocol":"TCP",
|
||||
"port":"9123",
|
||||
"algorithm":"ROUND_ROBIN",
|
||||
"status":"ACTIVE",
|
||||
"created":"2010-11-30T03:23:42Z",
|
||||
"updated":"2010-11-30T03:23:44Z"
|
||||
}
|
||||
]
|
||||
})
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
|
||||
with mock.patch.object(httplib2.Http, "request", mock_request):
|
||||
with mock.patch('time.time', mock.Mock(return_value=1234)):
|
||||
orig = sys.stdout
|
||||
try:
|
||||
out = StringIO()
|
||||
sys.stdout = out
|
||||
args = DummyArgs()
|
||||
self.api.list_lb(args)
|
||||
output = out.getvalue().strip()
|
||||
self.assertRegexpMatches(output, 'lb-site1')
|
||||
self.assertRegexpMatches(output, '71')
|
||||
self.assertRegexpMatches(output, 'HTTP')
|
||||
self.assertRegexpMatches(output, '80')
|
||||
self.assertRegexpMatches(output, 'LEAST_CONNECTIONS')
|
||||
self.assertRegexpMatches(output, 'ACTIVE')
|
||||
self.assertRegexpMatches(output, '2010-11-30T03:23:42Z')
|
||||
self.assertRegexpMatches(output, '2010-11-30T03:23:44Z')
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
|
||||
def testGetLb(self):
|
||||
""" Test the table generated from the STATUS function """
|
||||
fake_response = httplib2.Response({"status": '200'})
|
||||
fake_body = json.dumps({
|
||||
"id": "2000",
|
||||
"name":"sample-loadbalancer",
|
||||
"protocol":"HTTP",
|
||||
"port": "80",
|
||||
"algorithm":"ROUND_ROBIN",
|
||||
"status":"ACTIVE",
|
||||
"created":"2010-11-30T03:23:42Z",
|
||||
"updated":"2010-11-30T03:23:44Z",
|
||||
"virtualIps":[
|
||||
{
|
||||
"id": "1000",
|
||||
"address":"2001:cdba:0000:0000:0000:0000:3257:9652",
|
||||
"type":"PUBLIC",
|
||||
"ipVersion":"IPV6"
|
||||
}],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "1041",
|
||||
"address":"10.1.1.1",
|
||||
"port": "80",
|
||||
"condition":"ENABLED",
|
||||
"status":"ONLINE"
|
||||
},
|
||||
{
|
||||
"id": "1411",
|
||||
"address":"10.1.1.2",
|
||||
"port": "80",
|
||||
"condition":"ENABLED",
|
||||
"status":"ONLINE"
|
||||
}],
|
||||
"sessionPersistence":{
|
||||
"persistenceType":"HTTP_COOKIE"
|
||||
},
|
||||
"connectionThrottle":{
|
||||
"maxRequestRate": "50",
|
||||
"rateInterval": "60"
|
||||
}
|
||||
})
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
with mock.patch.object(httplib2.Http, "request", mock_request):
|
||||
with mock.patch('time.time', mock.Mock(return_value=1234)):
|
||||
orig = sys.stdout
|
||||
try:
|
||||
out = StringIO()
|
||||
sys.stdout = out
|
||||
args = DummyArgs()
|
||||
self.api.status_lb(args)
|
||||
output = out.getvalue().strip()
|
||||
self.assertRegexpMatches(output, 'HTTP_COOKIE')
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
|
||||
def testDeleteFailLb(self):
|
||||
"""
|
||||
Test a failure of a DELETE function. We don't test a succeed yet
|
||||
since that has no response so nothing to assert on
|
||||
"""
|
||||
fake_response = httplib2.Response({"status": '500'})
|
||||
fake_body = ''
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
with mock.patch.object(httplib2.Http, "request", mock_request):
|
||||
with mock.patch('time.time', mock.Mock(return_value=1234)):
|
||||
args = DummyArgs()
|
||||
self.assertRaises(novaclient.exceptions.ClientException,
|
||||
self.api.delete_lb, args)
|
||||
|
||||
def testCreateLb(self):
|
||||
"""
|
||||
Tests the CREATE function, tests that:
|
||||
1. We send the correct POST data
|
||||
2. We create a table from the response correctly
|
||||
"""
|
||||
fake_response = httplib2.Response({"status": '202'})
|
||||
fake_body = json.dumps({
|
||||
'name': 'a-new-loadbalancer',
|
||||
'id': '144',
|
||||
'protocol': 'HTTP',
|
||||
'port': '83',
|
||||
'algorithm': 'ROUND_ROBIN',
|
||||
'status': 'BUILD',
|
||||
'created': '2011-04-13T14:18:07Z',
|
||||
'updated': '2011-04-13T14:18:07Z',
|
||||
'virtualIps': [
|
||||
{
|
||||
'address': '15.0.0.1',
|
||||
'id': '39',
|
||||
'type': 'PUBLIC',
|
||||
'ipVersion': 'IPV4',
|
||||
}
|
||||
],
|
||||
'nodes': [
|
||||
{
|
||||
'address': '10.1.1.1',
|
||||
'id': '653',
|
||||
'port': '80',
|
||||
'status': 'ONLINE',
|
||||
'condition': 'ENABLED'
|
||||
}
|
||||
]
|
||||
})
|
||||
# This is what the POST data should look like based on the args passed
|
||||
post_compare = {
|
||||
"name": "a-new-loadbalancer",
|
||||
"nodes": [
|
||||
{
|
||||
"address": "10.1.1.1",
|
||||
"condition": "ENABLED",
|
||||
"port": "80"
|
||||
},
|
||||
{
|
||||
"address": "10.1.1.2",
|
||||
"condition": "ENABLED",
|
||||
"port": "81"
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
with mock.patch.object(httplib2.Http, "request", mock_request):
|
||||
with mock.patch('time.time', mock.Mock(return_value=1234)):
|
||||
orig = sys.stdout
|
||||
try:
|
||||
out = StringIO()
|
||||
sys.stdout = out
|
||||
args = DummyCreateArgs()
|
||||
self.api.create_lb(args)
|
||||
self.assertEquals(post_compare, self.api.postdata)
|
||||
output = out.getvalue().strip()
|
||||
# At some point we should possibly compare the complete
|
||||
# table rendering somehow instead of basic field data
|
||||
self.assertRegexpMatches(output, 'ROUND_ROBIN')
|
||||
self.assertRegexpMatches(output, 'BUILD')
|
||||
self.assertRegexpMatches(output, '144')
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
|
||||
def testCreateAddLb(self):
|
||||
"""
|
||||
Tests the CREATE function as above but adding a load balancer to a
|
||||
virtual IP
|
||||
"""
|
||||
fake_response = httplib2.Response({"status": '202'})
|
||||
fake_body = json.dumps({
|
||||
'name': 'a-new-loadbalancer',
|
||||
'id': '144',
|
||||
'protocol': 'HTTP',
|
||||
'port': '83',
|
||||
'algorithm': 'ROUND_ROBIN',
|
||||
'status': 'BUILD',
|
||||
'created': '2011-04-13T14:18:07Z',
|
||||
'updated': '2011-04-13T14:18:07Z',
|
||||
'virtualIps': [
|
||||
{
|
||||
'address': '15.0.0.1',
|
||||
'id': '39',
|
||||
'type': 'PUBLIC',
|
||||
'ipVersion': 'IPV4',
|
||||
}
|
||||
],
|
||||
'nodes': [
|
||||
{
|
||||
'address': '10.1.1.1',
|
||||
'id': '653',
|
||||
'port': '80',
|
||||
'status': 'ONLINE',
|
||||
'condition': 'ENABLED'
|
||||
}
|
||||
]
|
||||
})
|
||||
# This is what the POST data should look like based on the args passed
|
||||
post_compare = {
|
||||
"name": "a-new-loadbalancer",
|
||||
"port": "83",
|
||||
"protocol": "HTTP",
|
||||
"virtualIps": [
|
||||
{
|
||||
"id": "39"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"address": "10.1.1.1",
|
||||
"condition": "ENABLED",
|
||||
"port": "80"
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||
with mock.patch.object(httplib2.Http, "request", mock_request):
|
||||
with mock.patch('time.time', mock.Mock(return_value=1234)):
|
||||
orig = sys.stdout
|
||||
try:
|
||||
out = StringIO()
|
||||
sys.stdout = out
|
||||
# Add args to add a LB to a VIP
|
||||
args = DummyCreateArgs()
|
||||
args.port = '83'
|
||||
args.protocol = 'HTTP'
|
||||
args.vip = '39'
|
||||
args.node = ['10.1.1.1:80']
|
||||
self.api.create_lb(args)
|
||||
self.assertEquals(post_compare, self.api.postdata)
|
||||
output = out.getvalue().strip()
|
||||
# At some point we should possibly compare the complete
|
||||
# table rendering somehow instead of basic field data
|
||||
self.assertRegexpMatches(output, 'ROUND_ROBIN')
|
||||
self.assertRegexpMatches(output, 'BUILD')
|
||||
self.assertRegexpMatches(output, '144')
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
|
||||
|
||||
def testModifyLb(self):
|
||||
"""
|
||||
Tests the MODIFY function, no repsonse so we only test the PUT data
|
||||
"""
|
||||
# This is what the PUT data should look like based on the args passed
|
||||
put_compare = {
|
||||
"name": "a-modified-loadbalancer",
|
||||
"algorithm": "LEAST_CONNECTIONS"
|
||||
}
|
||||
args = DummyModifyArgs()
|
||||
self.api.modify_lb(args)
|
||||
self.assertEquals(put_compare, self.api.putdata)
|
1
tools/pip-requires
Normal file
1
tools/pip-requires
Normal file
@ -0,0 +1 @@
|
||||
python_novaclient
|
5
tools/test-requires
Normal file
5
tools/test-requires
Normal file
@ -0,0 +1,5 @@
|
||||
pep8
|
||||
mock
|
||||
httplib2
|
||||
testrepository>=0.0.8
|
||||
testtools>=0.9.22
|
21
tox.ini
Normal file
21
tox.ini
Normal file
@ -0,0 +1,21 @@
|
||||
[tox]
|
||||
envlist = py27,pep8
|
||||
|
||||
[testenv]
|
||||
deps = -r{toxinidir}/tools/pip-requires
|
||||
-r{toxinidir}/tools/test-requires
|
||||
commands = bash -c 'if [ ! -d ./.testrepository ] ; then testr init ; fi'
|
||||
bash -c 'testr run --parallel {posargs} ; RET=$? ; echo "Slowest Tests" ; testr slowest && exit $RET'
|
||||
|
||||
[tox:jenkins]
|
||||
downloadcache = ~/cache/pip
|
||||
|
||||
[testenv:py27]
|
||||
|
||||
[testenv:pep8]
|
||||
deps = pep8
|
||||
commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc,*openstack/common*,*lib/python*,*egg client setup.py
|
||||
|
||||
[testenv:pyflakes]
|
||||
deps = pyflakes
|
||||
commands = pyflakes client
|
Loading…
x
Reference in New Issue
Block a user