Migrate client code from libra codebase

Change-Id: Icd9d758e45c7167c5b8db7aa2c5e15b4bb93c766
This commit is contained in:
Andrew Hutchings 2013-01-03 12:21:53 +00:00
parent 775f861346
commit a861024bed
23 changed files with 1789 additions and 6 deletions

View File

@ -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
View 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
View File

@ -0,0 +1,9 @@
include README
exclude .gitignore
exclude .gitreview
global-exclude *.pyc
graft doc
graft tools

1
README Normal file
View File

@ -0,0 +1 @@
Libra command line client

6
build_pdf.sh Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
Libra Command Line Client
=========================
.. toctree::
:maxdepth: 2
command

7
openstack-common.conf Normal file
View 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
View File

View File

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
python_novaclient

5
tools/test-requires Normal file
View File

@ -0,0 +1,5 @@
pep8
mock
httplib2
testrepository>=0.0.8
testtools>=0.9.22

21
tox.ini Normal file
View 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