Merge topic 'lbaas_python'

* changes:
  Adding a script for building LBaaS package
  Adding a new version of python LBaaS API
This commit is contained in:
Sergey Kraynev 2016-04-18 14:22:58 +04:00 committed by Gerrit Code Review
commit cb3fa8e136
74 changed files with 5027 additions and 587 deletions

View File

@ -0,0 +1 @@
LBaaS_Library.zip

View File

@ -1,587 +0,0 @@
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
cvfduP8DBe+HRgBwAwA=

View File

@ -0,0 +1,9 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_LOG_CAPTURE=${OS_LOG_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-160} \
${PYTHON:-python} -m subunit.run discover -t ./ ./lbaas/tests/unit $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@ -0,0 +1 @@
Nikolay Mahotkin <nmakhotkin@mirantis.com>

View File

@ -0,0 +1,34 @@
CHANGES
=======
0.1
---
* Fixing ssl_info field in REST API
* Add exposing address field to listener
* Move setting default algo to driver
* Change the length of addres field
* Changing structure of ssl_info in listener model
* Adding an 'address' field to listener model
* Adding function to read config again
* Move db operations outside the drivers
* Allowing to use any driver name
* Adding new options for listeners
* Changing warning when creating a member
* Adding API spec to README.md
* Fixing update API
* Fixing update API
* Adding nested members dict into listener API
* Handle haproxy stop when listeners are absent
* Improving sample config
* Improving default_log_levels
* Clear config.sample from unused properties
* Remove unrelated naming
* Make delete API working
* Make update API working
* Get working creating API
* Partially implement haproxy driver
* Add driver mechanism for LBaaS
* Add alembic migrations
* Initial commit to lbaas
* Initial commit

View File

@ -0,0 +1,116 @@
Metadata-Version: 1.0
Name: lbaas
Version: 0.1.0
Summary: LBaaS Project
Home-page: UNKNOWN
Author: Mirantis Inc.
Author-email: nmakhotkin@mirantis.com
License: Apache License, Version 2.0
Description: LBaaS API specification
=======================
Listeners API
-------------
**/v1/listeners** - objects representing a group of machines balancing on LB on specific protocol and port. Each listener contains mapping configuration to members and their listening ports.
Example: listener A listen to HTTP port 80 and maps to 3 machines with their own IPs listening on HTTP port 8080.
---> Member, Machine1_IP (HTTP, 8080)
Listener A (HTTP, 80) ---> Member, Machine2_IP (HTTP, 8080)
---> Member, Machine3_IP (HTTP, 8080)
**POST /v1/listeners**
Creates a new listener object. Returns 201 if succeed.
Parameters:
* **name** - The name of listener. Type string. Required. Should be unique across listener objects.
* **protocol** - The protocol of listener. Type string. Should be one of {“http”, “tcp”}. It is not validated by API! Required.
* **protocol_port** - Protocol TCP port which listener will be listening to. Type integer. Required.
* **algorithm** - Load-balancing algorithm. Type string. If passed, should be compatible with one of possible haproxy algorithm. Optional, default value if not passed - “roundrobin”.
Request body example:
{
“protocol”: “http”,
“protocol_port”: 80,
“name”: “app”,
“algorithm”: “roundrobin”
}
**GET /v1/listeners**
Gets all listeners from LBaaS. Also contains all containing members information. Returns 200 if succeed.
**GET /v1/listeners/<name>**
Gets particular listener from LBaaS. name - the listeners name.
**PUT /v1/listeners/<name>**
Update listener info by its name. Returns 200 code if succeed.
Request body example:
{
“protocol_port”: 8080,
“name”: “app”
}
**DELETE /v1/listeners/<name>**
Deletes the whole listener by its name. Returns 204 if succeed.
Members API
-----------
**/v1/members** - objects representing a machine which is able to receive requests on specific port of specific protocol. Each member belongs to specific listener.
**POST /v1/members**
Creates a new member object. Returns 201 if succeed.
Parameters:
* **name** - The name of member. Type string. Required. Should be unique across member objects.
* **protocol_port** - Protocol TCP port which member is listening to. Type integer. Required.
* **address** - Hostname or IP address of member machine. Type string. Required.
* **listener_name** - The name of listener which adds the current member to. Member will belong to this listener. Each listener may have a number of members. Type string. Required.
Request body example:
{
“address”: “10.0.20.5”,
“protocol_port”: 80,
“name”: “my_server”,
“listener_name”: “app”
}
**GET /v1/members**
Gets all members from LBaaS. Returns 200 if succeed.
**GET /v1/members/<name>**
Gets particular member from LBaaS. name - the members name.
**PUT /v1/members/<name>**
Update listener info by its name. Returns 200 code if succeed.
Request body example:
{
“protocol_port”: 8080,
}
**DELETE /v1/members/<name>**
Deletes the whole member by its name. Returns 204 if succeed.
Platform: UNKNOWN

View File

@ -0,0 +1,105 @@
LBaaS API specification
=======================
Listeners API
-------------
**/v1/listeners** - objects representing a group of machines balancing on LB on specific protocol and port. Each listener contains mapping configuration to members and their listening ports.
Example: listener A listen to HTTP port 80 and maps to 3 machines with their own IPs listening on HTTP port 8080.
---> Member, Machine1_IP (HTTP, 8080)
Listener A (HTTP, 80) ---> Member, Machine2_IP (HTTP, 8080)
---> Member, Machine3_IP (HTTP, 8080)
**POST /v1/listeners**
Creates a new listener object. Returns 201 if succeed.
Parameters:
* **name** - The name of listener. Type string. Required. Should be unique across listener objects.
* **protocol** - The protocol of listener. Type string. Should be one of {“http”, “tcp”}. It is not validated by API! Required.
* **protocol_port** - Protocol TCP port which listener will be listening to. Type integer. Required.
* **algorithm** - Load-balancing algorithm. Type string. If passed, should be compatible with one of possible haproxy algorithm. Optional, default value if not passed - “roundrobin”.
Request body example:
{
“protocol”: “http”,
“protocol_port”: 80,
“name”: “app”,
“algorithm”: “roundrobin”
}
**GET /v1/listeners**
Gets all listeners from LBaaS. Also contains all containing members information. Returns 200 if succeed.
**GET /v1/listeners/<name>**
Gets particular listener from LBaaS. name - the listeners name.
**PUT /v1/listeners/<name>**
Update listener info by its name. Returns 200 code if succeed.
Request body example:
{
“protocol_port”: 8080,
“name”: “app”
}
**DELETE /v1/listeners/<name>**
Deletes the whole listener by its name. Returns 204 if succeed.
Members API
-----------
**/v1/members** - objects representing a machine which is able to receive requests on specific port of specific protocol. Each member belongs to specific listener.
**POST /v1/members**
Creates a new member object. Returns 201 if succeed.
Parameters:
* **name** - The name of member. Type string. Required. Should be unique across member objects.
* **protocol_port** - Protocol TCP port which member is listening to. Type integer. Required.
* **address** - Hostname or IP address of member machine. Type string. Required.
* **listener_name** - The name of listener which adds the current member to. Member will belong to this listener. Each listener may have a number of members. Type string. Required.
Request body example:
{
“address”: “10.0.20.5”,
“protocol_port”: 80,
“name”: “my_server”,
“listener_name”: “app”
}
**GET /v1/members**
Gets all members from LBaaS. Returns 200 if succeed.
**GET /v1/members/<name>**
Gets particular member from LBaaS. name - the members name.
**PUT /v1/members/<name>**
Update listener info by its name. Returns 200 code if succeed.
Request body example:
{
“protocol_port”: 8080,
}
**DELETE /v1/members/<name>**
Deletes the whole member by its name. Returns 204 if succeed.

View File

@ -0,0 +1,232 @@
[DEFAULT]
#
# From oslo.log
#
# Print debugging output (set logging level to DEBUG instead of
# default WARNING level). (boolean value)
#debug = false
# Print more verbose output (set logging level to INFO instead of
# default WARNING level). (boolean value)
#verbose = false
# The name of a logging configuration file. This file is appended to
# any existing logging configuration files. For details about logging
# configuration files, see the Python logging module documentation.
# (string value)
# Deprecated group/name - [DEFAULT]/log_config
#log_config_append = <None>
# DEPRECATED. A logging.Formatter log message format string which may
# use any of the available logging.LogRecord attributes. This option
# is deprecated. Please use logging_context_format_string and
# logging_default_format_string instead. (string value)
#log_format = <None>
# Format string for %%(asctime)s in log records. Default: %(default)s
# . (string value)
#log_date_format = %Y-%m-%d %H:%M:%S
# (Optional) Name of log file to output to. If no default is set,
# logging will go to stdout. (string value)
# Deprecated group/name - [DEFAULT]/logfile
#log_file = <None>
# (Optional) The base directory used for relative --log-file paths.
# (string value)
# Deprecated group/name - [DEFAULT]/logdir
#log_dir = <None>
# Use syslog for logging. Existing syslog format is DEPRECATED and
# will be changed later to honor RFC5424. (boolean value)
#use_syslog = false
# (Optional) Enables or disables syslog rfc5424 format for logging. If
# enabled, prefixes the MSG part of the syslog message with APP-NAME
# (RFC5424). The format without the APP-NAME is deprecated in K, and
# will be removed in M, along with this option. (boolean value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
#use_syslog_rfc_format = true
# Syslog facility to receive log lines. (string value)
#syslog_log_facility = LOG_USER
# Log output to standard error. (boolean value)
#use_stderr = true
# Format string to use for log messages with context. (string value)
#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
# Format string to use for log messages without context. (string
# value)
#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
# Data to append to log format when level is DEBUG. (string value)
#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d
# Prefix each line of exception output with this format. (string
# value)
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
# List of logger=LEVEL pairs. (list value)
#default_log_levels = lbaas.cmd.api=INFO,lbaas.drivers=INFO
# Enables or disables publication of error events. (boolean value)
#publish_errors = false
# The format for an instance that is passed with the log message.
# (string value)
#instance_format = "[instance: %(uuid)s] "
# The format for an instance UUID that is passed with the log message.
# (string value)
#instance_uuid_format = "[instance: %(uuid)s] "
# Enables or disables fatal status of deprecations. (boolean value)
#fatal_deprecations = false
[api]
#
# From lbaas.config
#
# LBaaS API server host (string value)
#host = 0.0.0.0
# LBaaS API server port (port value)
# Minimum value: 1
# Maximum value: 65535
#port = 8993
[database]
#
# From oslo.db
#
# The file name to use with SQLite. (string value)
# Deprecated group/name - [DEFAULT]/sqlite_db
#sqlite_db = oslo.sqlite
# If True, SQLite uses synchronous mode. (boolean value)
# Deprecated group/name - [DEFAULT]/sqlite_synchronous
#sqlite_synchronous = true
# The back end to use for the database. (string value)
# Deprecated group/name - [DEFAULT]/db_backend
#backend = sqlalchemy
# The SQLAlchemy connection string to use to connect to the database.
# (string value)
# Deprecated group/name - [DEFAULT]/sql_connection
# Deprecated group/name - [DATABASE]/sql_connection
# Deprecated group/name - [sql]/connection
#connection = <None>
# The SQLAlchemy connection string to use to connect to the slave
# database. (string value)
#slave_connection = <None>
# The SQL mode to be used for MySQL sessions. This option, including
# the default, overrides any server-set SQL mode. To use whatever SQL
# mode is set by the server configuration, set this to no value.
# Example: mysql_sql_mode= (string value)
#mysql_sql_mode = TRADITIONAL
# Timeout before idle SQL connections are reaped. (integer value)
# Deprecated group/name - [DEFAULT]/sql_idle_timeout
# Deprecated group/name - [DATABASE]/sql_idle_timeout
# Deprecated group/name - [sql]/idle_timeout
#idle_timeout = 3600
# Minimum number of SQL connections to keep open in a pool. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_min_pool_size
# Deprecated group/name - [DATABASE]/sql_min_pool_size
#min_pool_size = 1
# Maximum number of SQL connections to keep open in a pool. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_max_pool_size
# Deprecated group/name - [DATABASE]/sql_max_pool_size
#max_pool_size = <None>
# Maximum number of database connection retries during startup. Set to
# -1 to specify an infinite retry count. (integer value)
# Deprecated group/name - [DEFAULT]/sql_max_retries
# Deprecated group/name - [DATABASE]/sql_max_retries
#max_retries = 10
# Interval between retries of opening a SQL connection. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_retry_interval
# Deprecated group/name - [DATABASE]/reconnect_interval
#retry_interval = 10
# If set, use this value for max_overflow with SQLAlchemy. (integer
# value)
# Deprecated group/name - [DEFAULT]/sql_max_overflow
# Deprecated group/name - [DATABASE]/sqlalchemy_max_overflow
#max_overflow = <None>
# Verbosity of SQL debugging information: 0=None, 100=Everything.
# (integer value)
# Deprecated group/name - [DEFAULT]/sql_connection_debug
#connection_debug = 0
# Add Python stack traces to SQL as comment strings. (boolean value)
# Deprecated group/name - [DEFAULT]/sql_connection_trace
#connection_trace = false
# If set, use this value for pool_timeout with SQLAlchemy. (integer
# value)
# Deprecated group/name - [DATABASE]/sqlalchemy_pool_timeout
#pool_timeout = <None>
# Enable the experimental use of database reconnect on connection
# lost. (boolean value)
#use_db_reconnect = false
# Seconds between retries of a database transaction. (integer value)
#db_retry_interval = 1
# If True, increases the interval between retries of a database
# operation up to db_max_retry_interval. (boolean value)
#db_inc_retry_interval = true
# If db_inc_retry_interval is set, the maximum seconds between retries
# of a database operation. (integer value)
#db_max_retry_interval = 10
# Maximum retries in case of connection error or deadlock error before
# error is raised. Set to -1 to specify an infinite retry count.
# (integer value)
#db_max_retries = 20
[pecan]
#
# From lbaas.config
#
# Pecan root controller (string value)
#root = lbaas.api.controllers.root.RootController
# A list of modules where pecan will search for applications. (list
# value)
#modules = lbaas.api
# Enables the ability to display tracebacks in the browser and
# interactively debug during development. (boolean value)
#debug = false
[lbaas]
#impl = haproxy

View File

@ -0,0 +1,32 @@
[loggers]
keys=root
[handlers]
keys=consoleHandler, fileHandler
[formatters]
keys=verboseFormatter, simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler, fileHandler
[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=simpleFormatter
args=(sys.stdout,)
[handler_fileHandler]
class=FileHandler
level=INFO
formatter=verboseFormatter
args=("/var/log/lbaas.log",)
[formatter_verboseFormatter]
format=%(asctime)s %(thread)s %(levelname)s %(module)s [-] %(message)s
datefmt=
[formatter_simpleFormatter]
format=%(asctime)s %(levelname)s [-] %(message)s
datefmt=

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
import pecan
from lbaas.db.v1 import api as db_api
def get_pecan_config():
# Set up the pecan configuration.
opts = cfg.CONF.pecan
cfg_dict = {
"app": {
"root": opts.root,
"modules": opts.modules,
"debug": opts.debug
}
}
return pecan.configuration.conf_from_dict(cfg_dict)
def setup_app(config=None):
if not config:
config = get_pecan_config()
app_conf = dict(config.app)
db_api.setup_db()
app = pecan.make_app(
app_conf.pop('root'),
logging=getattr(config, 'logging', {}),
**app_conf
)
return app

View File

@ -0,0 +1,156 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from wsme import types as wtypes
class Resource(wtypes.Base):
"""REST API Resource."""
_wsme_attributes = []
def to_dict(self):
d = {}
for attr in self._wsme_attributes:
attr_val = getattr(self, attr.name)
if not isinstance(attr_val, wtypes.UnsetType):
d[attr.name] = attr_val
return d
@classmethod
def from_dict(cls, d):
obj = cls()
for key, val in d.items():
if hasattr(obj, key):
setattr(obj, key, val)
return obj
def __str__(self):
"""WSME based implementation of __str__."""
res = "%s [" % type(self).__name__
first = True
for attr in self._wsme_attributes:
if not first:
res += ', '
else:
first = False
res += "%s='%s'" % (attr.name, getattr(self, attr.name))
return res + "]"
def to_string(self):
return json.dumps(self.to_dict())
@classmethod
def get_fields(cls):
obj = cls()
return [attr.name for attr in obj._wsme_attributes]
class ResourceList(Resource):
"""Resource containing the list of other resources."""
next = wtypes.text
"""A link to retrieve the next subset of the resource list"""
@property
def collection(self):
return getattr(self, self._type)
@classmethod
def convert_with_links(cls, resources, limit, url=None, fields=None,
**kwargs):
resource_collection = cls()
setattr(resource_collection, resource_collection._type, resources)
resource_collection.next = resource_collection.get_next(
limit,
url=url,
fields=fields,
**kwargs
)
return resource_collection
def has_next(self, limit):
"""Return whether resources has more items."""
return len(self.collection) and len(self.collection) == limit
def get_next(self, limit, url=None, fields=None, **kwargs):
"""Return a link to the next subset of the resources."""
if not self.has_next(limit):
return wtypes.Unset
q_args = ''.join(
['%s=%s&' % (key, value) for key, value in kwargs.items()]
)
resource_args = (
'?%(args)slimit=%(limit)d&marker=%(marker)s' %
{
'args': q_args,
'limit': limit,
'marker': self.collection[-1].id
}
)
# Fields is handled specially here, we can move it above when it's
# supported by all resources query.
if fields:
resource_args += '&fields=%s' % fields
next_link = "%(host_url)s/v2/%(resource)s%(args)s" % {
'host_url': url,
'resource': self._type,
'args': resource_args
}
return next_link
def to_dict(self):
d = {}
for attr in self._wsme_attributes:
attr_val = getattr(self, attr.name)
if isinstance(attr_val, list):
if isinstance(attr_val[0], Resource):
d[attr.name] = [v.to_dict() for v in attr_val]
elif not isinstance(attr_val, wtypes.UnsetType):
d[attr.name] = attr_val
return d
class Link(Resource):
"""Web link."""
href = wtypes.text
target = wtypes.text
@classmethod
def sample(cls):
return cls(href='http://example.com/here',
target='here')

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_log import log as logging
import pecan
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from lbaas.api.controllers import resource
from lbaas.api.controllers.v1 import root as v1_root
LOG = logging.getLogger(__name__)
API_STATUS = wtypes.Enum(str, 'SUPPORTED', 'CURRENT', 'DEPRECATED')
class APIVersion(resource.Resource):
"""API Version."""
id = wtypes.text
"The version identifier."
status = API_STATUS
"The status of the API (SUPPORTED, CURRENT or DEPRECATED)."
link = resource.Link
"The link to the versioned API."
class RootController(object):
v1 = v1_root.Controller()
@wsme_pecan.wsexpose([APIVersion])
def index(self):
LOG.debug("Fetching API versions.")
host_url_v1 = '%s/%s' % (pecan.request.host_url, 'v1')
api_v1 = APIVersion(
id='v1.0',
status='CURRENT',
link=resource.Link(href=host_url_v1, target='v1')
)
return [api_v1]

View File

@ -0,0 +1,139 @@
#
# 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 oslo_log import log as logging
from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from lbaas.api.controllers import resource
from lbaas.api.controllers.v1 import member
from lbaas.db.v1 import api as db_api
from lbaas.drivers import driver
from lbaas import exceptions as exceptions
from lbaas.utils import rest_utils
LOG = logging.getLogger(__name__)
class Listener(resource.Resource):
"""Environment resource."""
id = wtypes.text
name = wtypes.text
description = wtypes.text
address = wtypes.text
protocol = wtypes.text
protocol_port = wtypes.IntegerType()
algorithm = wtypes.text
options = wtypes.DictType(wtypes.text, wtypes.text)
ssl_info = wtypes.DictType(wtypes.text, wtypes.text)
members = [member.Member]
created_at = wtypes.text
updated_at = wtypes.text
class Listeners(resource.Resource):
"""A collection of Environment resources."""
listeners = [Listener]
class ListenersController(rest.RestController):
@wsme_pecan.wsexpose(Listeners)
def get_all(self):
"""Return all listeners."""
LOG.info("Fetch listeners.")
listeners = []
for l in db_api.get_listeners():
l_dict = l.to_dict()
l_dict['members'] = [
member.Member.from_dict(m.to_dict()) for m in l.members
]
listeners += [Listener.from_dict(l_dict)]
return Listeners(listeners=listeners)
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(Listener, wtypes.text)
def get(self, name):
"""Return the named listener."""
LOG.info("Fetch listener [name=%s]" % name)
db_model = db_api.get_listener(name)
return Listener.from_dict(db_model.to_dict())
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(Listener, body=Listener, status_code=201)
def post(self, listener):
"""Create a new listener."""
LOG.info("Create listener [listener=%s]" % listener)
if not (listener.name and listener.protocol_port
and listener.protocol):
raise exceptions.InputException(
'You must provide at least name, protocol_port and'
' protocol of the listener.'
)
lb_driver = driver.LB_DRIVER()
with db_api.transaction():
listener = db_api.create_listener(listener.to_dict())
db_model = lb_driver.create_listener(listener)
lb_driver.apply_changes()
return Listener.from_dict(db_model.to_dict())
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(Listener, wtypes.text, body=Listener)
def put(self, name, listener):
"""Update an listener."""
LOG.info(
"Update listener [name=%s, listener=%s]" %
(name, listener)
)
lb_driver = driver.LB_DRIVER()
with db_api.transaction():
listener = db_api.update_listener(name, listener.to_dict())
db_model = lb_driver.update_listener(listener)
lb_driver.apply_changes()
return Listener.from_dict(db_model.to_dict())
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, name):
"""Delete the named listener."""
LOG.info("Delete listener [name=%s]" % name)
lb_driver = driver.LB_DRIVER()
with db_api.transaction():
listener = db_api.get_listener(name)
lb_driver.delete_listener(listener)
db_api.delete_listener(name)
lb_driver.apply_changes()

View File

@ -0,0 +1,142 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_log import log as logging
import pecan
from pecan import hooks
from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from lbaas.api.controllers import resource
from lbaas.db.v1 import api as db_api
from lbaas.drivers import driver
from lbaas import exceptions
from lbaas.utils import rest_utils
LOG = logging.getLogger(__name__)
class Member(resource.Resource):
"""Member resource."""
id = wtypes.text
name = wtypes.text
address = wtypes.text
protocol_port = wtypes.IntegerType()
listener_name = wtypes.text
description = wtypes.text
tags = [wtypes.text]
created_at = wtypes.text
updated_at = wtypes.text
class Members(resource.Resource):
"""A collection of Members."""
members = [Member]
class MembersController(rest.RestController, hooks.HookController):
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(Member, wtypes.text)
def get(self, name):
"""Return the named member."""
LOG.info("Fetch member [name=%s]" % name)
db_model = db_api.get_member(name)
return Member.from_dict(db_model.to_dict())
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(Member, wtypes.text, body=Member)
def put(self, name, member):
"""Update a member."""
LOG.info("Update member [member_name=%s]" % name)
values = member.to_dict()
lb_driver = driver.LB_DRIVER()
with db_api.transaction():
member = db_api.update_member(name, values)
db_model = lb_driver.update_member(member)
lb_driver.apply_changes()
return Member.from_dict(db_model.to_dict())
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(Member, body=Member, status_code=201)
def post(self, member):
"""Create a new member."""
LOG.info("Create member [member_name=%s]" % member.name)
if not (member.name and member.protocol_port
and member.address and member.listener_name):
raise exceptions.InputException(
'You must provide at least name, protocol_port, '
'listener_name and address of the member.'
)
pecan.response.status = 201
values = member.to_dict()
listener_name = values.pop('listener_name')
lb_driver = driver.LB_DRIVER()
with db_api.transaction():
listener = db_api.get_listener(listener_name)
values['listener_id'] = listener.id
member = db_api.create_member(values)
db_model = lb_driver.create_member(member)
lb_driver.apply_changes()
return Member.from_dict(db_model.to_dict())
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, name):
"""Delete the named member."""
LOG.info("Delete member [name=%s]" % name)
lb_driver = driver.LB_DRIVER()
with db_api.transaction():
member = db_api.get_member(name)
db_api.delete_member(name)
lb_driver.delete_member(member)
lb_driver.apply_changes()
@wsme_pecan.wsexpose(Members)
def get_all(self):
"""Return all members."""
LOG.info("Fetch members.")
members_list = [
Member.from_dict(db_model.to_dict())
for db_model in db_api.get_members()
]
return Members(members=members_list)

View File

@ -0,0 +1,41 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pecan
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from lbaas.api.controllers import resource
from lbaas.api.controllers.v1 import listener
from lbaas.api.controllers.v1 import member
class RootResource(resource.Resource):
"""Root resource for API version 1.
It references all other resources belonging to the API.
"""
uri = wtypes.text
class Controller(object):
"""API root controller for version 1."""
members = member.MembersController()
listeners = listener.ListenersController()
@wsme_pecan.wsexpose(RootResource)
def index(self):
return RootResource(uri='%s/%s' % (pecan.request.host_url, 'v1'))

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 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 eventlet
eventlet.monkey_patch(
os=True,
select=True,
socket=True,
thread=False if '--use-debugger' in sys.argv else True,
time=True)
import os
# If ../lbaas/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'lbaas', '__init__.py')):
sys.path.insert(0, POSSIBLE_TOPDIR)
from oslo_config import cfg
from oslo_log import log as logging
from wsgiref import simple_server
from lbaas.api import app
from lbaas import config
from lbaas.drivers import driver
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def launch_api():
host = cfg.CONF.api.host
port = cfg.CONF.api.port
server = simple_server.make_server(
host,
port,
app.setup_app()
)
LOG.info("LBaaS API is serving on http://%s:%s (PID=%s)" %
(host, port, os.getpid()))
server.serve_forever()
def get_properly_ordered_parameters():
"""Orders launch parameters in the right order.
In oslo it's important the order of the launch parameters.
if --config-file came after the command line parameters the command
line parameters are ignored.
So to make user command line parameters are never ignored this method
moves --config-file to be always first.
"""
args = sys.argv[1:]
for arg in sys.argv[1:]:
if arg == '--config-file' or arg.startswith('--config-file='):
conf_file_value = args[args.index(arg) + 1]
args.remove(conf_file_value)
args.remove(arg)
args.insert(0, arg)
args.insert(1, conf_file_value)
return args
def main():
try:
config.parse_args(get_properly_ordered_parameters())
logging.setup(CONF, 'Lbaas')
driver.load_lb_drivers()
launch_api()
except RuntimeError as excp:
sys.stderr.write("ERROR: %s\n" % excp)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Configuration options registration and useful routines.
"""
from oslo_config import cfg
from oslo_log import log
from lbaas import version
api_opts = [
cfg.StrOpt('host', default='0.0.0.0', help='LBaaS API server host'),
cfg.PortOpt('port', default=8993, help='LBaaS API server port'),
]
pecan_opts = [
cfg.StrOpt(
'root',
default='lbaas.api.controllers.root.RootController',
help='Pecan root controller'
),
cfg.ListOpt(
'modules',
default=["lbaas.api"],
help='A list of modules where pecan will search for '
'applications.'
),
cfg.BoolOpt(
'debug',
default=False,
help='Enables the ability to display tracebacks in the '
'browser and interactively debug during development.'
),
]
lbaas_opts = [
cfg.StrOpt(
'impl',
default='haproxy',
help='Implementation driver for LBaaS'
),
]
CONF = cfg.CONF
API_GROUP = 'api'
LBAAS_GROUP = 'lbaas'
PECAN_GROUP = 'pecan'
CONF.register_opts(api_opts, group=API_GROUP)
CONF.register_opts(lbaas_opts, group=LBAAS_GROUP)
CONF.register_opts(pecan_opts, group=PECAN_GROUP)
_DEFAULT_LOG_LEVELS = [
'sqlalchemy=WARN',
'eventlet.wsgi.server=WARN',
'stevedore=INFO',
]
def list_opts():
return [
(API_GROUP, api_opts),
(LBAAS_GROUP, lbaas_opts),
(PECAN_GROUP, pecan_opts),
]
def parse_args(args=None, usage=None, default_config_files=None):
log.set_defaults(default_log_levels=_DEFAULT_LOG_LEVELS)
log.register_options(CONF)
CONF(
args=args,
project='lbaas',
version=version,
usage=usage,
default_config_files=default_config_files
)
def read_config():
CONF(project='lbaas')

View File

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import six
from oslo_config import cfg
from oslo_db import options
from oslo_db.sqlalchemy import session as db_session
from oslo_log import log as logging
from lbaas import exceptions as exc
from lbaas import utils
LOG = logging.getLogger(__name__)
# Note(dzimine): sqlite only works for basic testing.
options.set_defaults(cfg.CONF, connection="sqlite:///lbaas.sqlite")
_DB_SESSION_THREAD_LOCAL_NAME = "db_sql_alchemy_session"
_facade = None
def _get_facade():
global _facade
if not _facade:
_facade = db_session.EngineFacade(
cfg.CONF.database.connection,
sqlite_fk=True,
autocommit=False,
**dict(six.iteritems(cfg.CONF.database))
)
return _facade
def get_engine():
return _get_facade().get_engine()
def _get_session():
return _get_facade().get_session()
def _get_thread_local_session():
return utils.get_thread_local(_DB_SESSION_THREAD_LOCAL_NAME)
def _get_or_create_thread_local_session():
ses = _get_thread_local_session()
if ses:
return ses, False
ses = _get_session()
_set_thread_local_session(ses)
return ses, True
def _set_thread_local_session(session):
utils.set_thread_local(_DB_SESSION_THREAD_LOCAL_NAME, session)
def session_aware(param_name="session"):
"""Decorator for methods working within db session."""
def _decorator(func):
def _within_session(*args, **kw):
# If 'created' flag is True it means that the transaction is
# demarcated explicitly outside this module.
ses, created = _get_or_create_thread_local_session()
try:
kw[param_name] = ses
result = func(*args, **kw)
if created:
ses.commit()
return result
except Exception:
if created:
ses.rollback()
raise
finally:
if created:
_set_thread_local_session(None)
ses.close()
_within_session.__doc__ = func.__doc__
return _within_session
return _decorator
# Transaction management.
def start_tx():
"""Starts transaction.
Opens new database session and starts new transaction assuming
there wasn't any opened sessions within the same thread.
"""
if _get_thread_local_session():
raise exc.DataAccessException(
"Database transaction has already been started."
)
_set_thread_local_session(_get_session())
def commit_tx():
"""Commits previously started database transaction."""
ses = _get_thread_local_session()
if not ses:
raise exc.DataAccessException(
"Nothing to commit. Database transaction"
" has not been previously started."
)
ses.commit()
def rollback_tx():
"""Rolls back previously started database transaction."""
ses = _get_thread_local_session()
if not ses:
raise exc.DataAccessException(
"Nothing to roll back. Database transaction has not been started."
)
ses.rollback()
def end_tx():
"""Ends transaction.
Ends current database transaction.
It rolls back all uncommitted changes and closes database session.
"""
ses = _get_thread_local_session()
if not ses:
raise exc.DataAccessException(
"Database transaction has not been started."
)
if ses.dirty:
rollback_tx()
ses.close()
_set_thread_local_session(None)
@session_aware()
def get_driver_name(session=None):
return session.bind.url.drivername
@session_aware()
def model_query(model, columns=(), session=None):
"""Query helper.
:param model: Base model to query.
:param columns: Optional. Which columns to be queried.
"""
if columns:
return session.query(*columns)
return session.query(model)

View File

@ -0,0 +1,58 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = lbaas/db/sqlalchemy/migration/alembic_migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
sqlalchemy.url =
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,66 @@
The migrations in `alembic_migrations/versions` contain the changes needed to migrate
between Mistral database revisions. A migration occurs by executing a script that
details the changes needed to upgrade the database. The migration scripts
are ordered so that multiple scripts can run sequentially. The scripts are executed by
Mistral's migration wrapper which uses the Alembic library to manage the migration. Mistral
supports migration from Kilo or later.
You can upgrade to the latest database version via:
```
lbaas-db-manage --config-file /path/to/lbaas.conf upgrade head
```
You can populate the database with standard actions and workflows:
```
lbaas-db-manage --config-file /path/to/lbaas.conf populate
```
To check the current database version:
```
lbaas-db-manage --config-file /path/to/lbaas.conf current
```
To create a script to run the migration offline:
```
lbaas-db-manage --config-file /path/to/lbaas.conf upgrade head --sql
```
To run the offline migration between specific migration versions:
```
lbaas-db-manage --config-file /path/to/lbaas.conf upgrade <start version>:<end version> --sql
```
Upgrade the database incrementally:
```
lbaas-db-manage --config-file /path/to/lbaas.conf upgrade --delta <# of revs>
```
Or, upgrade the database to one newer revision:
```
lbaas-db-manage --config-file /path/to/lbaas.conf upgrade +1
```
Create new revision:
```
lbaas-db-manage --config-file /path/to/lbaas.conf revision -m "description of revision" --autogenerate
```
Create a blank file:
```
lbaas-db-manage --config-file /path/to/lbaas.conf revision -m "description of revision"
```
This command does not perform any migrations, it only sets the revision.
Revision may be any existing revision. Use this command carefully.
```
lbaas-db-manage --config-file /path/to/lbaas.conf stamp <revision>
```
To verify that the timeline does branch, you can run this command:
```
lbaas-db-manage --config-file /path/to/lbaas.conf check_migration
```
If the migration path has branch, you can find the branch point via:
```
lbaas-db-manage --config-file /path/to/lbaas.conf history

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
#
# 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 __future__ import with_statement
from alembic import context
from logging import config as c
from oslo_utils import importutils
from sqlalchemy import create_engine
from sqlalchemy import pool
from lbaas.db.sqlalchemy import model_base
importutils.try_import('lbaas.db.v1.sqlalchemy.models')
# This is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
lbaas_config = config.lbaas_config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
c.fileConfig(config.config_file_name)
# Add your model's MetaData object here for 'autogenerate' support.
target_metadata = model_base.LbaasModelBase.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure(url=lbaas_config.database.connection)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = create_engine(
lbaas_config.database.connection,
poolclass=pool.NullPool
)
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,34 @@
# Copyright ${create_date.year} OpenStack Foundation.
#
# 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.
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}

View File

@ -0,0 +1,63 @@
# Copyright 2015 OpenStack Foundation.
#
# 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.
"""Initial LBaaS scheme
Revision ID: 001
Revises: None
Create Date: 2015-12-11 12:58:30.775597
"""
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
from alembic import op
import sqlalchemy as sa
from lbaas.db.sqlalchemy import types
def upgrade():
op.create_table(
'listeners_v1',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('protocol', sa.String(length=10), nullable=True),
sa.Column('protocol_port', sa.Integer(), nullable=True),
sa.Column('algorithm', sa.String(length=30), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table(
'members_v1',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('address', sa.String(length=200), nullable=True),
sa.Column('protocol', sa.String(length=10), nullable=True),
sa.Column('protocol_port', sa.Integer(), nullable=True),
sa.Column('tags', types.JsonEncoded(), nullable=True),
sa.Column('listener_id', sa.String(length=36), nullable=True),
sa.ForeignKeyConstraint(['listener_id'], [u'listeners_v1.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)

View File

@ -0,0 +1,42 @@
# Copyright 2016 OpenStack Foundation.
#
# 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.
"""Add options to listeners
Revision ID: 002
Revises: 001
Create Date: 2016-01-14 17:15:03.867571
"""
# revision identifiers, used by Alembic.
revision = '002'
down_revision = '001'
from alembic import op
import sqlalchemy as sa
from lbaas.db.sqlalchemy import types
def upgrade():
op.add_column(
'listeners_v1',
sa.Column('options', types.JsonEncoded(), nullable=True)
)
op.add_column(
'listeners_v1',
sa.Column('ssl_info', types.JsonEncoded(), nullable=True)
)

View File

@ -0,0 +1,37 @@
# Copyright 2016 OpenStack Foundation.
#
# 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.
"""Added address field to listener
Revision ID: 003
Revises: 002
Create Date: 2016-04-01 11:35:09.048572
"""
# revision identifiers, used by Alembic.
revision = '003'
down_revision = '002'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column(
'listeners_v1',
sa.Column('address', sa.String(length=200), nullable=True)
)

View File

@ -0,0 +1,121 @@
#
# 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.
"""Starter script for lbaas-db-manage."""
import os
from alembic import command as alembic_cmd
from alembic import config as alembic_cfg
from alembic import util as alembic_u
from oslo_config import cfg
from oslo_utils import importutils
import six
# We need to import lbaas.api.app to
# make sure we register all needed options.
importutils.try_import('lbaas.api.app')
CONF = cfg.CONF
def do_alembic_command(config, cmd, *args, **kwargs):
try:
getattr(alembic_cmd, cmd)(config, *args, **kwargs)
except alembic_u.CommandError as e:
alembic_u.err(six.text_type(e))
def do_check_migration(config, _cmd):
do_alembic_command(config, 'branches')
def do_upgrade(config, cmd):
if not CONF.command.revision and not CONF.command.delta:
raise SystemExit('You must provide a revision or relative delta')
revision = CONF.command.revision
if CONF.command.delta:
sign = '+' if CONF.command.name == 'upgrade' else '-'
revision = sign + str(CONF.command.delta)
do_alembic_command(config, cmd, revision, sql=CONF.command.sql)
def do_stamp(config, cmd):
do_alembic_command(
config, cmd,
CONF.command.revision,
sql=CONF.command.sql
)
def do_revision(config, cmd):
do_alembic_command(
config, cmd,
message=CONF.command.message,
autogenerate=CONF.command.autogenerate,
sql=CONF.command.sql
)
def add_command_parsers(subparsers):
for name in ['current', 'history', 'branches']:
parser = subparsers.add_parser(name)
parser.set_defaults(func=do_alembic_command)
parser = subparsers.add_parser('upgrade')
parser.add_argument('--delta', type=int)
parser.add_argument('--sql', action='store_true')
parser.add_argument('revision', nargs='?')
parser.set_defaults(func=do_upgrade)
parser = subparsers.add_parser('stamp')
parser.add_argument('--sql', action='store_true')
parser.add_argument('revision', nargs='?')
parser.set_defaults(func=do_stamp)
parser = subparsers.add_parser('revision')
parser.add_argument('-m', '--message')
parser.add_argument('--autogenerate', action='store_true')
parser.add_argument('--sql', action='store_true')
parser.set_defaults(func=do_revision)
command_opt = cfg.SubCommandOpt('command',
title='Command',
help='Available commands',
handler=add_command_parsers)
CONF.register_cli_opt(command_opt)
def main():
config = alembic_cfg.Config(
os.path.join(os.path.dirname(__file__), 'alembic.ini')
)
config.set_main_option(
'script_location',
'lbaas.db.sqlalchemy.migration:alembic_migrations'
)
# attach the Mistral conf to the Alembic conf
config.lbaas_config = CONF
CONF(project='lbaas')
CONF.command.func(config, CONF.command.name)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import six
from oslo_db.sqlalchemy import models as oslo_models
import sqlalchemy as sa
from sqlalchemy.ext import declarative
from sqlalchemy.orm import attributes
from lbaas import utils
def id_column():
return sa.Column(
sa.String(36),
primary_key=True,
default=utils.generate_unicode_uuid
)
class _LbaasModelBase(oslo_models.ModelBase, oslo_models.TimestampMixin):
"""Base class for all LBaaS SQLAlchemy DB Models."""
__table__ = None
__hash__ = object.__hash__
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def __eq__(self, other):
if type(self) is not type(other):
return False
for col in self.__table__.columns:
# In case of single table inheritance a class attribute
# corresponding to a table column may not exist so we need
# to skip these attributes.
if (hasattr(self, col.name)
and hasattr(other, col.name)
and getattr(self, col.name) != getattr(other, col.name)):
return False
return True
def to_dict(self):
"""sqlalchemy based automatic to_dict method."""
d = {}
# If a column is unloaded at this point, it is
# probably deferred. We do not want to access it
# here and thereby cause it to load.
unloaded = attributes.instance_state(self).unloaded
for col in self.__table__.columns:
if col.name not in unloaded and hasattr(self, col.name):
d[col.name] = getattr(self, col.name)
datetime_to_str(d, 'created_at')
datetime_to_str(d, 'updated_at')
return d
def get_clone(self):
"""Clones current object, loads all fields and returns the result."""
m = self.__class__()
for col in self.__table__.columns:
if hasattr(self, col.name):
setattr(m, col.name, getattr(self, col.name))
setattr(m, 'created_at', getattr(self, 'created_at').isoformat(' '))
updated_at = getattr(self, 'updated_at')
# NOTE(nmakhotkin): 'updated_at' field is empty for just created
# object since it has not updated yet.
if updated_at:
setattr(m, 'updated_at', updated_at.isoformat(' '))
return m
def __repr__(self):
return '%s %s' % (type(self).__name__, self.to_dict().__repr__())
def datetime_to_str(dct, attr_name):
if (dct.get(attr_name) is not None
and not isinstance(dct.get(attr_name), six.string_types)):
dct[attr_name] = dct[attr_name].isoformat(' ')
LbaasModelBase = declarative.declarative_base(cls=_LbaasModelBase)

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This module implements SQLAlchemy-based types for dict and list
# expressed by json-strings
#
from oslo_serialization import jsonutils
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from sqlalchemy.ext import mutable
class JsonEncoded(sa.TypeDecorator):
"""Represents an immutable structure as a json-encoded string."""
impl = sa.Text
def process_bind_param(self, value, dialect):
if value is not None:
value = jsonutils.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = jsonutils.loads(value)
return value
class MutableList(mutable.Mutable, list):
@classmethod
def coerce(cls, key, value):
"""Convert plain lists to MutableList."""
if not isinstance(value, MutableList):
if isinstance(value, list):
return MutableList(value)
# this call will raise ValueError
return mutable.Mutable.coerce(key, value)
return value
def __add__(self, value):
"""Detect list add events and emit change events."""
list.__add__(self, value)
self.changed()
def append(self, value):
"""Detect list add events and emit change events."""
list.append(self, value)
self.changed()
def __setitem__(self, key, value):
"""Detect list set events and emit change events."""
list.__setitem__(self, key, value)
self.changed()
def __delitem__(self, i):
"""Detect list del events and emit change events."""
list.__delitem__(self, i)
self.changed()
def JsonDictType():
"""Returns an SQLAlchemy Column Type suitable to store a Json dict."""
return mutable.MutableDict.as_mutable(JsonEncoded)
def JsonListType():
"""Returns an SQLAlchemy Column Type suitable to store a Json array."""
return MutableList.as_mutable(JsonEncoded)
def LongText():
# TODO(rakhmerov): Need to do for postgres.
return sa.Text().with_variant(mysql.LONGTEXT(), 'mysql')
class JsonEncodedLongText(JsonEncoded):
impl = LongText()
def JsonLongDictType():
return mutable.MutableDict.as_mutable(JsonEncodedLongText)

View File

@ -0,0 +1,128 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
from oslo_db import api as db_api
from oslo_log import log as logging
_BACKEND_MAPPING = {
'sqlalchemy': 'lbaas.db.v1.sqlalchemy.api',
}
IMPL = db_api.DBAPI('sqlalchemy', backend_mapping=_BACKEND_MAPPING)
LOG = logging.getLogger(__name__)
def setup_db():
IMPL.setup_db()
def drop_db():
IMPL.drop_db()
# Transaction control.
def start_tx():
IMPL.start_tx()
def commit_tx():
IMPL.commit_tx()
def rollback_tx():
IMPL.rollback_tx()
def end_tx():
IMPL.end_tx()
@contextlib.contextmanager
def transaction():
with IMPL.transaction():
yield
# Members.
def get_member(name):
return IMPL.get_member(name)
def load_member(name):
"""Unlike get_member this method is allowed to return None."""
return IMPL.load_member(name)
def get_members():
return IMPL.get_members()
def create_member(values):
return IMPL.create_member(values)
def update_member(name, values):
return IMPL.update_member(name, values)
def create_or_update_member(name, values):
return IMPL.create_or_update_member(name, values)
def delete_member(name):
IMPL.delete_member(name)
def delete_members(**kwargs):
IMPL.delete_members(**kwargs)
# Listeners.
def get_listener(name):
return IMPL.get_listener(name)
def load_listener(name):
"""Unlike get_listener this method is allowed to return None."""
return IMPL.load_listener(name)
def get_listeners():
return IMPL.get_listeners()
def create_listener(values):
return IMPL.create_listener(values)
def update_listener(name, values):
return IMPL.update_listener(name, values)
def create_or_update_listener(name, values):
return IMPL.create_or_update_listener(name, values)
def delete_listener(name):
IMPL.delete_listener(name)
def delete_listeners(**kwargs):
IMPL.delete_listeners(**kwargs)

View File

@ -0,0 +1,281 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import sys
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import utils as db_utils
from oslo_log import log as logging
import sqlalchemy as sa
from lbaas.db.sqlalchemy import base as b
from lbaas.db.v1.sqlalchemy import models
from lbaas import exceptions as exc
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def get_backend():
"""Consumed by openstack common code.
The backend is this module itself.
:return Name of db backend.
"""
return sys.modules[__name__]
def setup_db():
try:
models.Listener.metadata.create_all(b.get_engine())
except sa.exc.OperationalError as e:
raise exc.DBException("Failed to setup database: %s" % e)
def drop_db():
global _facade
try:
models.Listener.metadata.drop_all(b.get_engine())
_facade = None
except Exception as e:
raise exc.DBException("Failed to drop database: %s" % e)
# Transaction management.
def start_tx():
b.start_tx()
def commit_tx():
b.commit_tx()
def rollback_tx():
b.rollback_tx()
def end_tx():
b.end_tx()
@contextlib.contextmanager
def transaction():
try:
start_tx()
yield
commit_tx()
finally:
end_tx()
def _secure_query(model, *columns):
query = b.model_query(model, columns)
return query
def _paginate_query(model, limit=None, marker=None, sort_keys=None,
sort_dirs=None, query=None):
if not query:
query = _secure_query(model)
query = db_utils.paginate_query(
query,
model,
limit,
sort_keys if sort_keys else {},
marker=marker,
sort_dirs=sort_dirs
)
return query.all()
def _delete_all(model, session=None, **kwargs):
_secure_query(model).filter_by(**kwargs).delete()
def _get_collection_sorted_by_name(model, **kwargs):
return _secure_query(model).filter_by(**kwargs).order_by(model.name).all()
def _get_collection_sorted_by_time(model, **kwargs):
query = _secure_query(model)
return query.filter_by(**kwargs).order_by(model.created_at).all()
def _get_db_object_by_name(model, name):
return _secure_query(model).filter_by(name=name).first()
def _get_db_object_by_id(model, id):
return _secure_query(model).filter_by(id=id).first()
# Member definitions.
def get_member(name):
member = _get_member(name)
if not member:
raise exc.NotFoundException(
"Member not found [member_name=%s]" % name)
return member
def load_member(name):
return _get_member(name)
def get_members(**kwargs):
return _get_collection_sorted_by_name(models.Member, **kwargs)
@b.session_aware()
def create_member(values, session=None):
member = models.Member()
member.update(values.copy())
try:
member.save(session=session)
except db_exc.DBDuplicateEntry as e:
raise exc.DBDuplicateEntryException(
"Duplicate entry for MemberDefinition: %s" % e.columns
)
return member
@b.session_aware()
def update_member(name, values, session=None):
member = _get_member(name)
if not member:
raise exc.NotFoundException(
"Member not found [member_name=%s]" % name)
member.update(values.copy())
return member
@b.session_aware()
def create_or_update_member(name, values, session=None):
if not _get_member(name):
return create_member(values)
else:
return update_member(name, values)
@b.session_aware()
def delete_member(name, session=None):
member = _get_member(name)
if not member:
raise exc.NotFoundException(
"Member not found [member_name=%s]" % name)
session.delete(member)
def _get_member(name):
return _get_db_object_by_name(models.Member, name)
@b.session_aware()
def delete_members(**kwargs):
return _delete_all(models.Member, **kwargs)
# Listeners.
def get_listener(name):
listener = _get_listener(name)
if not listener:
raise exc.NotFoundException("Listener not found [name=%s]" % name)
return listener
def load_listener(name):
return _get_listener(name)
def get_listeners(**kwargs):
return _get_collection_sorted_by_name(models.Listener, **kwargs)
@b.session_aware()
def create_listener(values, session=None):
listener = models.Listener()
listener.update(values)
try:
listener.save(session=session)
except db_exc.DBDuplicateEntry as e:
raise exc.DBDuplicateEntryException(
"Duplicate entry for Listener: %s" % e.columns
)
return listener
@b.session_aware()
def update_listener(name, values, session=None):
listener = _get_listener(name)
if not listener:
raise exc.NotFoundException("Listener not found [name=%s]" % name)
listener.update(values)
return listener
@b.session_aware()
def create_or_update_listener(name, values, session=None):
listener = _get_listener(name)
if not listener:
return create_listener(values)
else:
return update_listener(name, values)
@b.session_aware()
def delete_listener(name, session=None):
listener = _get_listener(name)
if not listener:
raise exc.NotFoundException("Listener not found [name=%s]" % name)
session.delete(listener)
def _get_listener(name):
return _get_db_object_by_name(models.Listener, name)
@b.session_aware()
def delete_listeners(**kwargs):
return _delete_all(models.Listener, **kwargs)

View File

@ -0,0 +1,83 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sqlalchemy as sa
from sqlalchemy.orm import backref
from sqlalchemy.orm import relationship
from oslo_log import log as logging
from lbaas.db.sqlalchemy import model_base as mb
from lbaas.db.sqlalchemy import types as st
# Definition objects.
LOG = logging.getLogger(__name__)
class Listener(mb.LbaasModelBase):
"""Listener object"""
__tablename__ = 'listeners_v1'
__table_args__ = (
sa.UniqueConstraint('name'),
)
id = mb.id_column()
address = sa.Column(sa.String(200))
name = sa.Column(sa.String(80))
description = sa.Column(sa.Text(), nullable=True)
protocol = sa.Column(sa.String(10))
protocol_port = sa.Column(sa.Integer())
algorithm = sa.Column(sa.String(30))
options = sa.Column(st.JsonDictType(), default={})
ssl_info = sa.Column(st.JsonDictType(), default={})
class Member(mb.LbaasModelBase):
"""Member object."""
__tablename__ = 'members_v1'
__table_args__ = (
sa.UniqueConstraint('name'),
)
# Main properties.
id = mb.id_column()
name = sa.Column(sa.String(80))
description = sa.Column(sa.String(255), nullable=True)
address = sa.Column(sa.String(200))
protocol = sa.Column(sa.String(10))
protocol_port = sa.Column(sa.Integer())
tags = sa.Column(st.JsonListType())
# Many-to-one for 'Member' and 'Listener'.
Member.listener_id = sa.Column(
sa.String(36),
sa.ForeignKey(Listener.id)
)
Listener.members = relationship(
Member,
backref=backref('listener', remote_side=[Listener.id]),
cascade='all, delete-orphan',
foreign_keys=Member.listener_id,
lazy='select'
)

View File

@ -0,0 +1,45 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
class LoadBalancerDriver(object):
@abc.abstractmethod
def create_listener(self, listener):
pass
@abc.abstractmethod
def update_listener(self, listener):
pass
@abc.abstractmethod
def delete_listener(self, listener):
pass
@abc.abstractmethod
def create_member(self, member):
pass
@abc.abstractmethod
def update_member(self, member):
pass
@abc.abstractmethod
def delete_member(self, member):
pass
@abc.abstractmethod
def apply_changes(self):
pass

View File

@ -0,0 +1,34 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
from oslo_log import log as logging
from stevedore import driver
LOG = logging.getLogger(__name__)
LB_DRIVER = None
def load_lb_drivers():
lbaas_impl = cfg.CONF.lbaas.impl
global LB_DRIVER
LB_DRIVER = driver.DriverManager(
'lbaas.drivers',
"%s" % lbaas_impl
).driver
LOG.info("Load Balancer driver for %s is loaded." % lbaas_impl)

View File

@ -0,0 +1,159 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import itertools
from oslo_concurrency import processutils
from lbaas.db.v1 import api as db_api
from lbaas.drivers import base
from lbaas.utils import file_utils
class HAProxyDriver(base.LoadBalancerDriver):
config_file = "/etc/haproxy/haproxy.cfg"
config = []
def __init__(self):
self._sync_configuration()
def _sync_configuration(self):
pass
def create_listener(self, listener):
# For HAProxy, default listener address is 0.0.0.0.
if not listener.address:
listener.address = '0.0.0.0'
if not listener.algorithm:
listener.algorithm = 'roundrobin'
self._save_config()
return listener
def update_listener(self, listener):
self._save_config()
return listener
def delete_listener(self, listener):
self._save_config()
def delete_member(self, member):
self._save_config()
def update_member(self, member):
self._save_config()
return member
def create_member(self, member):
self._save_config()
return member
def _save_config(self):
conf = []
conf.extend(_build_global())
conf.extend(_build_defaults())
for l in db_api.get_listeners():
conf.extend(_build_frontend(l))
conf.extend(_build_backend(l))
file_utils.replace_file(self.config_file, '\n'.join(conf))
def apply_changes(self):
if db_api.get_listeners():
cmd = 'sudo service haproxy restart'.split()
else:
# There is no listeners at all.
cmd = 'sudo service haproxy stop'.split()
return processutils.execute(*cmd)
def _build_global(user_group='nogroup'):
opts = [
'log 127.0.0.1 syslog info',
'daemon',
'user nobody',
'group %s' % user_group,
]
return itertools.chain(['global'], ('\t' + o for o in opts))
def _build_defaults():
opts = [
'log global',
'retries 3',
'option redispatch',
'maxconn 64000',
'timeout connect 30000ms',
'timeout client 50000',
'timeout server 50000',
]
return itertools.chain(['defaults'], ('\t' + o for o in opts))
def _build_frontend(listener):
bind_str = 'bind %s:%s' % (
listener.address,
listener.protocol_port
)
if listener.ssl_info:
path = listener.ssl_info['path']
options = listener.ssl_info.get('options', '')
ciphers = listener.ssl_info.get('ciphers', '')
bind_str = "%s ssl crt %s" % (
bind_str,
' '.join([path, ' '.join(options), ciphers])
)
opts = [
'mode %s' % listener.protocol,
'default_backend %s' % listener.name,
bind_str
]
listener_options = ['%s %s' % (k, v) for k, v in listener.options.items()]
frontend_line = 'frontend %s' % listener.name
return itertools.chain(
[frontend_line],
('\t' + o for o in opts),
('\t' + o for o in listener_options),
)
def _build_backend(listener):
opts = [
'mode %s' % listener.protocol,
'balance %s' % listener.algorithm,
]
for mem in listener.members:
opts += [
'server %s %s:%s'
% (mem.name, mem.address, mem.protocol_port)
]
listener_line = 'backend %s' % listener.name
return itertools.chain([listener_line], ('\t' + o for o in opts))

View File

@ -0,0 +1,76 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class Error(Exception):
def __init__(self, message=None):
super(Error, self).__init__(message)
class LBaaSException(Error):
"""Base Exception for the project
To correctly use this class, inherit from it and define
a 'message' and 'http_code' properties.
"""
message = "An unknown exception occurred"
http_code = 500
@property
def code(self):
"""This is here for webob to read.
https://github.com/Pylons/webob/blob/master/webob/exc.py
"""
return self.http_code
def __str__(self):
return self.message
def __init__(self, message=None):
if message is not None:
self.message = message
super(LBaaSException, self).__init__(
'%d: %s' % (self.http_code, self.message))
class DBException(LBaaSException):
http_code = 400
class DataAccessException(LBaaSException):
http_code = 400
class NotFoundException(LBaaSException):
http_code = 404
message = "Object not found"
class DBDuplicateEntryException(LBaaSException):
http_code = 409
message = "Database object already exists"
class DBQueryEntryException(LBaaSException):
http_code = 400
class InputException(LBaaSException):
http_code = 400
class NotAllowedException(LBaaSException):
http_code = 403
message = "Operation not allowed"

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_config import cfg
import pecan
import pecan.testing
from webtest import app as webtest_app
from lbaas.tests.unit import base
__all__ = ['FunctionalTest']
class FunctionalTest(base.DbTestCase):
def setUp(self):
super(FunctionalTest, self).setUp()
pecan_opts = cfg.CONF.pecan
self.app = pecan.testing.load_test_app({
'app': {
'root': pecan_opts.root,
'modules': pecan_opts.modules,
'debug': pecan_opts.debug,
'auth_enable': False
}
})
self.addCleanup(pecan.set_config, {}, overwrite=True)
def assertNotFound(self, url):
try:
self.app.get(url, headers={'Accept': 'application/json'})
except webtest_app.AppError as error:
self.assertIn('Bad response: 404 Not Found', str(error))
return
self.fail('Expected 404 Not found but got OK')

View File

@ -0,0 +1,179 @@
#
# 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 copy
import datetime
import uuid
import mock
from lbaas.db.v1 import api as db_api
from lbaas.db.v1.sqlalchemy import models as db
from lbaas.drivers import driver
from lbaas import exceptions as exc
from lbaas.tests.unit.api import base
LISTENER_FOR_UPDATE = {
'name': 'test',
'description': 'my test settings2',
}
LISTENER = {
'id': str(uuid.uuid4()),
'name': 'test',
'description': 'my test settings',
'protocol': 'HTTP',
'protocol_port': 80,
'algorithm': 'ROUND_ROBIN',
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00'
}
LISTENER_DB = db.Listener(
id=LISTENER['id'],
name=LISTENER['name'],
description=LISTENER['description'],
protocol=LISTENER['protocol'],
protocol_port=LISTENER['protocol_port'],
algorithm=LISTENER['algorithm'],
created_at=datetime.datetime(1970, 1, 1),
updated_at=datetime.datetime(1970, 1, 1)
)
LISTENER_DB_DICT = LISTENER_DB.to_dict()
FOR_UPDATED_LISTENER = copy.deepcopy(LISTENER_FOR_UPDATE)
UPDATED_LISTENER = copy.deepcopy(LISTENER)
UPDATED_LISTENER_DB = db.Listener(**LISTENER_DB_DICT)
MOCK_LISTENER = mock.MagicMock(return_value=LISTENER_DB)
MOCK_LISTENERS = mock.MagicMock(return_value=[LISTENER_DB])
MOCK_UPDATED_LISTENER = mock.MagicMock(return_value=UPDATED_LISTENER_DB)
MOCK_EMPTY = mock.MagicMock(return_value=[])
MOCK_NOT_FOUND = mock.MagicMock(side_effect=exc.NotFoundException())
MOCK_DUPLICATE = mock.MagicMock(side_effect=exc.DBDuplicateEntryException())
MOCK_DELETE = mock.MagicMock(return_value=None)
class TestListenerController(base.FunctionalTest):
def setUp(self):
super(TestListenerController, self).setUp()
self.driver_origin = driver.LB_DRIVER
driver.LB_DRIVER = mock.Mock()
def tearDown(self):
driver.LB_DRIVER = self.driver_origin
super(TestListenerController, self).tearDown()
@mock.patch.object(db_api, 'get_listeners', MOCK_LISTENERS)
def test_get_all(self):
resp = self.app.get('/v1/listeners')
self.assertEqual(200, resp.status_int)
self.assertEqual(1, len(resp.json['listeners']))
def test_get_all_empty(self):
resp = self.app.get('/v1/listeners')
self.assertEqual(200, resp.status_int)
self.assertEqual(0, len(resp.json['listeners']))
@mock.patch.object(db_api, 'get_listener', MOCK_LISTENER)
def test_get(self):
resp = self.app.get('/v1/listeners/123')
self.assertEqual(200, resp.status_int)
self.assertDictEqual(LISTENER, resp.json)
@mock.patch.object(db_api, "get_listener", MOCK_NOT_FOUND)
def test_get_not_found(self):
resp = self.app.get('/v1/listeners/123', expect_errors=True)
self.assertEqual(404, resp.status_int)
@mock.patch.object(db_api, "create_listener", MOCK_LISTENER)
def test_post(self):
driver.LB_DRIVER().create_listener = MOCK_LISTENER
resp = self.app.post_json(
'/v1/listeners',
LISTENER
)
self.assertEqual(201, resp.status_int)
self.assertDictEqual(LISTENER, resp.json)
@mock.patch.object(db_api, "create_listener", MOCK_DUPLICATE)
def test_post_dup(self):
driver.LB_DRIVER().create_listener = MOCK_DUPLICATE
resp = self.app.post_json(
'/v1/listeners',
LISTENER,
expect_errors=True
)
self.assertEqual(409, resp.status_int)
@mock.patch.object(db_api, "update_listener", MOCK_UPDATED_LISTENER)
def test_put(self):
driver.LB_DRIVER().update_listener = MOCK_UPDATED_LISTENER
resp = self.app.put_json(
'/v1/listeners/test',
FOR_UPDATED_LISTENER
)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(UPDATED_LISTENER, resp.json)
@mock.patch.object(db_api, "update_listener", MOCK_NOT_FOUND)
def test_put_not_found(self):
driver.LB_DRIVER().update_listener = MOCK_NOT_FOUND
listener = FOR_UPDATED_LISTENER
resp = self.app.put_json(
'/v1/listeners/test',
listener,
expect_errors=True
)
self.assertEqual(404, resp.status_int)
@mock.patch.object(db_api, "get_listener", MOCK_LISTENER)
@mock.patch.object(
db_api,
"delete_listener",
mock.Mock(return_value=None)
)
def test_delete(self):
driver.LB_DRIVER().delete_listener = MOCK_DELETE
resp = self.app.delete('/v1/listeners/123')
self.assertEqual(204, resp.status_int)
@mock.patch.object(db_api, "delete_listener", MOCK_NOT_FOUND)
def test_delete_not_found(self):
driver.LB_DRIVER().delete_listener = MOCK_NOT_FOUND
resp = self.app.delete('/v1/listeners/123', expect_errors=True)
self.assertEqual(404, resp.status_int)

View File

@ -0,0 +1,171 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import datetime
import mock
from lbaas.db.v1 import api as db_api
from lbaas.db.v1.sqlalchemy import models
from lbaas.drivers import driver
from lbaas import exceptions as exc
from lbaas.tests.unit.api import base
MEMBER_DB = models.Member(
id='123',
name='member',
tags=['deployment', 'demo'],
address='10.0.0.1',
protocol_port=80,
created_at=datetime.datetime(1970, 1, 1),
updated_at=datetime.datetime(1970, 1, 1)
)
MEMBER = {
'id': '123',
'name': 'member',
'tags': ['deployment', 'demo'],
'address': '10.0.0.1',
'protocol_port': 80,
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00'
}
UPDATED_MEMBER_DB = copy.copy(MEMBER_DB)
UPDATED_MEMBER = copy.deepcopy(MEMBER)
MOCK_MEMBER = mock.MagicMock(return_value=MEMBER_DB)
MOCK_MEMBERS = mock.MagicMock(return_value=[MEMBER_DB])
MOCK_UPDATED_MEMBER = mock.MagicMock(return_value=UPDATED_MEMBER_DB)
MOCK_DELETE = mock.MagicMock(return_value=None)
MOCK_EMPTY = mock.MagicMock(return_value=[])
MOCK_NOT_FOUND = mock.MagicMock(side_effect=exc.NotFoundException())
MOCK_DUPLICATE = mock.MagicMock(side_effect=exc.DBDuplicateEntryException())
class TestMembersController(base.FunctionalTest):
def setUp(self):
super(TestMembersController, self).setUp()
self.driver_origin = driver.LB_DRIVER
driver.LB_DRIVER = mock.Mock()
def tearDown(self):
driver.LB_DRIVER = self.driver_origin
super(TestMembersController, self).tearDown()
@mock.patch.object(db_api, "get_member", MOCK_MEMBER)
def test_get(self):
resp = self.app.get('/v1/members/123')
self.assertEqual(200, resp.status_int)
self.assertDictEqual(MEMBER, resp.json)
@mock.patch.object(db_api, "get_member", MOCK_NOT_FOUND)
def test_get_not_found(self):
resp = self.app.get('/v1/members/123', expect_errors=True)
self.assertEqual(404, resp.status_int)
@mock.patch.object(db_api, "update_member", MOCK_UPDATED_MEMBER)
def test_put(self):
driver.LB_DRIVER().update_member = MOCK_UPDATED_MEMBER
resp = self.app.put_json(
'/v1/members/123',
UPDATED_MEMBER,
)
self.assertEqual(200, resp.status_int)
self.assertEqual(UPDATED_MEMBER, resp.json)
@mock.patch.object(db_api, "update_member", MOCK_NOT_FOUND)
def test_put_not_found(self):
driver.LB_DRIVER().update_member = MOCK_NOT_FOUND
resp = self.app.put_json(
'/v1/members/123',
UPDATED_MEMBER,
expect_errors=True
)
self.assertEqual(404, resp.status_int)
@mock.patch.object(db_api, "create_member", MOCK_MEMBER)
@mock.patch.object(db_api, "get_listener", MOCK_MEMBER)
def test_post(self):
driver.LB_DRIVER().create_member = MOCK_MEMBER
member = copy.deepcopy(MEMBER)
member['listener_name'] = 'listener_name'
resp = self.app.post_json(
'/v1/members',
member,
)
self.assertEqual(201, resp.status_int)
self.assertEqual(MEMBER, resp.json)
@mock.patch.object(db_api, "create_member", MOCK_DUPLICATE)
@mock.patch.object(db_api, "get_listener", MOCK_MEMBER)
def test_post_dup(self):
driver.LB_DRIVER().create_member = MOCK_DUPLICATE
member = copy.deepcopy(MEMBER)
member['listener_name'] = 'listener_name'
resp = self.app.post_json(
'/v1/members',
member,
expect_errors=True
)
self.assertEqual(409, resp.status_int)
@mock.patch.object(db_api, "get_member", MOCK_MEMBER)
@mock.patch.object(db_api, "delete_member", mock.Mock(return_value=None))
def test_delete(self):
driver.LB_DRIVER().delete_member = MOCK_DELETE
resp = self.app.delete('/v1/members/123')
self.assertEqual(204, resp.status_int)
@mock.patch.object(db_api, "get_member", MOCK_NOT_FOUND)
def test_delete_not_found(self):
driver.LB_DRIVER().delete_member = MOCK_NOT_FOUND
resp = self.app.delete('/v1/members/123', expect_errors=True)
self.assertEqual(404, resp.status_int)
@mock.patch.object(db_api, "get_members", MOCK_MEMBERS)
def test_get_all(self):
resp = self.app.get('/v1/members')
self.assertEqual(200, resp.status_int)
self.assertEqual(1, len(resp.json['members']))
self.assertDictEqual(MEMBER, resp.json['members'][0])
@mock.patch.object(db_api, "get_members", MOCK_EMPTY)
def test_get_all_empty(self):
resp = self.app.get('/v1/members')
self.assertEqual(200, resp.status_int)
self.assertEqual(0, len(resp.json['members']))

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_serialization import jsonutils
from lbaas.tests.unit.api import base
class TestRootController(base.FunctionalTest):
def test_index(self):
resp = self.app.get('/', headers={'Accept': 'application/json'})
self.assertEqual(200, resp.status_int)
data = jsonutils.loads(resp.body.decode())
self.assertEqual('v1.0', data[0]['id'])
self.assertEqual('CURRENT', data[0]['status'])
self.assertEqual(
{'href': 'http://localhost/v1', 'target': 'v1'},
data[0]['link']
)
def test_v2_root(self):
resp = self.app.get('/v1/', headers={'Accept': 'application/json'})
self.assertEqual(200, resp.status_int)
data = jsonutils.loads(resp.body.decode())
self.assertEqual(
'http://localhost/v1',
data['uri']
)

View File

@ -0,0 +1,183 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import time
from oslo_config import cfg
from oslo_log import log as logging
from oslotest import base
import six
import testtools.matchers as ttm
from lbaas import config
from lbaas.db.sqlalchemy import base as db_sa_base
from lbaas.db.v1 import api as db_api_v2
LOG = logging.getLogger(__name__)
CONF = config.CONF
class BaseTest(base.BaseTestCase):
def assertListEqual(self, l1, l2):
if tuple(sys.version_info)[0:2] < (2, 7):
# for python 2.6 compatibility
self.assertEqual(l1, l2)
else:
super(BaseTest, self).assertListEqual(l1, l2)
def assertDictEqual(self, cmp1, cmp2):
if tuple(sys.version_info)[0:2] < (2, 7):
# for python 2.6 compatibility
self.assertThat(cmp1, ttm.Equals(cmp2))
else:
super(BaseTest, self).assertDictEqual(cmp1, cmp2)
def _assert_single_item(self, items, **props):
return self._assert_multiple_items(items, 1, **props)[0]
def _assert_multiple_items(self, items, count, **props):
def _matches(item, **props):
for prop_name, prop_val in six.iteritems(props):
v = item[prop_name] if isinstance(
item, dict) else getattr(item, prop_name)
if v != prop_val:
return False
return True
filtered_items = list(
filter(lambda item: _matches(item, **props), items)
)
found = len(filtered_items)
if found != count:
LOG.info("[failed test ctx] items=%s, expected_props=%s" % (str(
items), props))
self.fail("Wrong number of items found [props=%s, "
"expected=%s, found=%s]" % (props, count, found))
return filtered_items
def _assert_dict_contains_subset(self, expected, actual, msg=None):
"""Checks whether actual is a superset of expected.
Note: This is almost the exact copy of the standard method
assertDictContainsSubset() that appeared in Python 2.7, it was
added to use it with Python 2.6.
"""
missing = []
mismatched = []
for key, value in six.iteritems(expected):
if key not in actual:
missing.append(key)
elif value != actual[key]:
mismatched.append('%s, expected: %s, actual: %s' %
(key, value,
actual[key]))
if not (missing or mismatched):
return
standardMsg = ''
if missing:
standardMsg = 'Missing: %s' % ','.join(m for m in missing)
if mismatched:
if standardMsg:
standardMsg += '; '
standardMsg += 'Mismatched values: %s' % ','.join(mismatched)
self.fail(self._formatMessage(msg, standardMsg))
def _await(self, predicate, delay=1, timeout=60):
"""Awaits for predicate function to evaluate to True.
If within a configured timeout predicate function hasn't evaluated
to True then an exception is raised.
:param predicate: Predication function.
:param delay: Delay in seconds between predicate function calls.
:param timeout: Maximum amount of time to wait for predication
function to evaluate to True.
:return:
"""
end_time = time.time() + timeout
while True:
if predicate():
break
if time.time() + delay > end_time:
raise AssertionError("Failed to wait for expected result.")
time.sleep(delay)
def _sleep(self, seconds):
time.sleep(seconds)
class DbTestCase(BaseTest):
is_heavy_init_called = False
@classmethod
def __heavy_init(cls):
"""Method that runs heavy_init().
Make this method private to prevent extending this one.
It runs heavy_init() only once.
Note: setUpClass() can be used, but it magically is not invoked
from child class in another module.
"""
if not cls.is_heavy_init_called:
cls.heavy_init()
cls.is_heavy_init_called = True
@classmethod
def heavy_init(cls):
"""Runs a long initialization.
This method runs long initialization once by class
and can be extended by child classes.
"""
# If using sqlite, change to memory. The default is file based.
if cfg.CONF.database.connection.startswith('sqlite'):
cfg.CONF.set_default('connection', 'sqlite://', group='database')
cfg.CONF.set_default('max_overflow', -1, group='database')
cfg.CONF.set_default('max_pool_size', 1000, group='database')
db_api_v2.setup_db()
def _clean_db(self):
with db_api_v2.transaction():
db_api_v2.delete_members()
db_api_v2.delete_listeners()
if not cfg.CONF.database.connection.startswith('sqlite'):
db_sa_base.get_engine().dispose()
def setUp(self):
super(DbTestCase, self).setUp()
self.__heavy_init()
self.addCleanup(self._clean_db)
def is_db_session_open(self):
return db_sa_base._get_thread_local_session() is not None

View File

@ -0,0 +1,384 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from lbaas.db.v1.sqlalchemy import api as db_api
from lbaas import exceptions as exc
from lbaas.tests.unit import base as test_base
MEMBERS = [
{
'name': 'my_member1',
'description': 'empty',
'tags': ['mc'],
'updated_at': None,
'address': '10.0.0.1',
'protocol_port': 80,
},
{
'name': 'my_member2',
'description': 'my description',
'tags': ['mc'],
'updated_at': None,
'address': '10.0.0.2',
'protocol_port': 80,
},
]
class MemberTest(test_base.DbTestCase):
def test_create_and_get_and_load_member(self):
created = db_api.create_member(MEMBERS[0])
fetched = db_api.get_member(created['name'])
self.assertEqual(created, fetched)
fetched = db_api.load_member(created.name)
self.assertEqual(created, fetched)
self.assertIsNone(db_api.load_member("not-existing-wb"))
def test_update_member(self):
created = db_api.create_member(MEMBERS[0])
self.assertIsNone(created.updated_at)
updated = db_api.update_member(
created.name,
{'description': 'my new description'}
)
self.assertEqual('my new description', updated.description)
fetched = db_api.get_member(created['name'])
self.assertEqual(updated, fetched)
self.assertIsNotNone(fetched.updated_at)
def test_create_or_update_member(self):
name = MEMBERS[0]['name']
self.assertIsNone(db_api.load_member(name))
created = db_api.create_or_update_member(
name,
MEMBERS[0]
)
self.assertIsNotNone(created)
self.assertIsNotNone(created.name)
updated = db_api.create_or_update_member(
created.name,
{'description': 'my new description'}
)
self.assertEqual('my new description', updated.description)
self.assertEqual(
'my new description',
db_api.load_member(updated.name).description
)
fetched = db_api.get_member(created.name)
self.assertEqual(updated, fetched)
def test_get_members(self):
created0 = db_api.create_member(MEMBERS[0])
created1 = db_api.create_member(MEMBERS[1])
fetched = db_api.get_members()
self.assertEqual(2, len(fetched))
self.assertEqual(created0, fetched[0])
self.assertEqual(created1, fetched[1])
def test_delete_member(self):
created = db_api.create_member(MEMBERS[0])
fetched = db_api.get_member(created.name)
self.assertEqual(created, fetched)
db_api.delete_member(created.name)
self.assertRaises(
exc.NotFoundException,
db_api.get_member,
created.name
)
def test_member_repr(self):
s = db_api.create_member(MEMBERS[0]).__repr__()
self.assertIn('Member ', s)
self.assertIn("'name': 'my_member1'", s)
LISTENERS = [
{
'name': 'listener1',
'description': 'Test Listener #1',
'protocol': 'HTTP',
'protocol_port': 80,
'algorithm': 'ROUND_ROBIN',
},
{
'name': 'listener2',
'description': 'Test Listener #2',
'protocol': 'HTTP',
'protocol_port': 80,
'algorithm': 'SOURCE',
}
]
class ListenerTest(test_base.DbTestCase):
def setUp(self):
super(ListenerTest, self).setUp()
db_api.delete_listeners()
def test_create_and_get_and_load_listener(self):
created = db_api.create_listener(LISTENERS[0])
fetched = db_api.get_listener(created.name)
self.assertEqual(created, fetched)
fetched = db_api.load_listener(created.name)
self.assertEqual(created, fetched)
self.assertIsNone(db_api.load_listener("not-existing-id"))
def test_update_listener(self):
created = db_api.create_listener(LISTENERS[0])
self.assertIsNone(created.updated_at)
updated = db_api.update_listener(
created.name,
{'description': 'my new desc'}
)
self.assertEqual('my new desc', updated.description)
fetched = db_api.get_listener(created.name)
self.assertEqual(updated, fetched)
self.assertIsNotNone(fetched.updated_at)
def test_create_or_update_listener(self):
name = 'not-existing-id'
self.assertIsNone(db_api.load_listener(name))
created = db_api.create_or_update_listener(name, LISTENERS[0])
self.assertIsNotNone(created)
self.assertIsNotNone(created.name)
updated = db_api.create_or_update_listener(
created.name,
{'description': 'my new desc'}
)
self.assertEqual('my new desc', updated.description)
self.assertEqual(
'my new desc',
db_api.load_listener(updated.name).description
)
fetched = db_api.get_listener(created.name)
self.assertEqual(updated, fetched)
def test_get_listeners(self):
created0 = db_api.create_listener(LISTENERS[0])
created1 = db_api.create_listener(LISTENERS[1])
fetched = db_api.get_listeners()
self.assertEqual(2, len(fetched))
self.assertEqual(created0, fetched[0])
self.assertEqual(created1, fetched[1])
def test_delete_listener(self):
created = db_api.create_listener(LISTENERS[0])
fetched = db_api.get_listener(created.name)
self.assertEqual(created, fetched)
db_api.delete_listener(created.name)
self.assertRaises(
exc.NotFoundException,
db_api.get_listener,
created.name
)
def test_listener_repr(self):
s = db_api.create_listener(LISTENERS[0]).__repr__()
self.assertIn('Listener ', s)
self.assertIn("'description': 'Test Listener #1'", s)
self.assertIn("'name': 'listener1'", s)
class TXTest(test_base.DbTestCase):
def test_rollback(self):
db_api.start_tx()
try:
created = db_api.create_member(MEMBERS[0])
fetched = db_api.get_member(created.name)
self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open())
db_api.rollback_tx()
finally:
db_api.end_tx()
self.assertFalse(self.is_db_session_open())
self.assertRaises(
exc.NotFoundException,
db_api.get_member,
created['id']
)
self.assertFalse(self.is_db_session_open())
def test_commit(self):
db_api.start_tx()
try:
created = db_api.create_member(MEMBERS[0])
fetched = db_api.get_member(created.name)
self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open())
db_api.commit_tx()
finally:
db_api.end_tx()
self.assertFalse(self.is_db_session_open())
fetched = db_api.get_member(created.name)
self.assertEqual(created, fetched)
self.assertFalse(self.is_db_session_open())
def test_commit_transaction(self):
with db_api.transaction():
created = db_api.create_member(MEMBERS[0])
fetched = db_api.get_member(created.name)
self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open())
self.assertFalse(self.is_db_session_open())
fetched = db_api.get_member(created.name)
self.assertEqual(created, fetched)
self.assertFalse(self.is_db_session_open())
def test_rollback_multiple_objects(self):
db_api.start_tx()
try:
created = db_api.create_listener(LISTENERS[0])
fetched = db_api.get_listener(created['name'])
self.assertEqual(created, fetched)
created_wb = db_api.create_member(MEMBERS[0])
fetched_wb = db_api.get_member(created_wb.name)
self.assertEqual(created_wb, fetched_wb)
self.assertTrue(self.is_db_session_open())
db_api.rollback_tx()
finally:
db_api.end_tx()
self.assertFalse(self.is_db_session_open())
self.assertRaises(
exc.NotFoundException,
db_api.get_listener,
created.name
)
self.assertRaises(
exc.NotFoundException,
db_api.get_member,
created_wb.name
)
self.assertFalse(self.is_db_session_open())
def test_rollback_transaction(self):
try:
with db_api.transaction():
created = db_api.create_member(MEMBERS[0])
fetched = db_api.get_member(created.name)
self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open())
db_api.create_member(MEMBERS[0])
except exc.DBDuplicateEntryException:
pass
self.assertFalse(self.is_db_session_open())
self.assertRaises(
exc.NotFoundException,
db_api.get_member,
created.name
)
def test_commit_multiple_objects(self):
db_api.start_tx()
try:
created = db_api.create_listener(LISTENERS[0])
fetched = db_api.get_listener(created.name)
self.assertEqual(created, fetched)
created_wb = db_api.create_member(MEMBERS[0])
fetched_wb = db_api.get_member(created_wb.name)
self.assertEqual(created_wb, fetched_wb)
self.assertTrue(self.is_db_session_open())
db_api.commit_tx()
finally:
db_api.end_tx()
self.assertFalse(self.is_db_session_open())
fetched = db_api.get_listener(created.name)
self.assertEqual(created, fetched)
fetched_wb = db_api.get_member(created_wb.name)
self.assertEqual(created_wb, fetched_wb)
self.assertFalse(self.is_db_session_open())

View File

@ -0,0 +1,294 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
from lbaas.db.v1.sqlalchemy import api as db_api
from lbaas.drivers import haproxy as driver
from lbaas import exceptions as exc
from lbaas.tests.unit import base as test_base
from lbaas.utils import file_utils
class HAProxyDriverTest(test_base.DbTestCase):
def setUp(self):
super(HAProxyDriverTest, self).setUp()
self.haproxy = driver.HAProxyDriver()
@mock.patch.object(file_utils, 'replace_file')
def test_create_listener(self, replace_file):
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin'
})
self.haproxy.create_listener(listener)
listener = db_api.get_listener('test_listener')
self.assertEqual('roundrobin', listener.algorithm)
config_data = replace_file.call_args[0][1]
self.assertIn(
'frontend %s' % listener.name,
config_data
)
@mock.patch.object(file_utils, 'replace_file')
def test_create_listener_with_options(self, replace_file):
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin',
'options': {
'option': 'forwardfor',
'reqadd': 'X-Forwarded-Proto:\ https'
}
})
self.haproxy.create_listener(listener)
config_data = replace_file.call_args[0][1]
self.assertIn(
'\toption forwardfor',
config_data
)
self.assertIn(
'\treqadd X-Forwarded-Proto:\ https',
config_data
)
@mock.patch.object(file_utils, 'replace_file')
def test_create_listener_with_ssl(self, replace_file):
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'address': '',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin',
'ssl_info': {'path': '/config/cert.pem', 'options': ['no-sslv3']}
})
self.haproxy.create_listener(listener)
config_data = replace_file.call_args[0][1]
self.assertIn(
'\tbind :80 ssl crt /config/cert.pem no-sslv3',
config_data
)
@mock.patch.object(file_utils, 'replace_file')
def test_create_member(self, replace_file):
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin'
})
# Create a listener first.
self.haproxy.create_listener(listener)
listener = db_api.get_listener('test_listener')
member = db_api.create_member({
'listener_id': listener.id,
'name': 'member1',
'address': '10.0.0.1',
'protocol_port': 80,
})
self.haproxy.create_member(member)
member = db_api.get_member('member1')
self.assertEqual('10.0.0.1', member.address)
self.assertEqual(2, replace_file.call_count)
config_data = replace_file.call_args[0][1]
self.assertIn(
'\tserver %s %s:%s' %
(member.name, member.address, member.protocol_port),
config_data
)
@mock.patch.object(file_utils, 'replace_file')
def test_update_listener(self, replace_file):
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin',
'address': ''
})
self.haproxy.create_listener(listener)
listener = db_api.get_listener('test_listener')
db_api.update_listener(listener.name, {'protocol_port': 8080})
self.haproxy.update_listener(listener)
config_data = replace_file.call_args[0][1]
self.assertIn(
'frontend %s' % listener.name,
config_data
)
self.assertIn(
'bind :%s' % 8080,
config_data
)
@mock.patch.object(file_utils, 'replace_file')
def test_update_member(self, replace_file):
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin'
})
# Create a listener first.
self.haproxy.create_listener(listener)
listener = db_api.get_listener('test_listener')
member = db_api.create_member({
'listener_id': listener.id,
'name': 'member1',
'address': '10.0.0.1',
'protocol_port': 80,
})
member = self.haproxy.create_member(member)
self.assertEqual(80, member.protocol_port)
member = db_api.update_member(member.name, {'protocol_port': 8080})
self.haproxy.update_member(member)
config_data = replace_file.call_args[0][1]
self.assertIn(
'\tserver %s %s:%s' % (member.name, member.address, 8080),
config_data
)
@mock.patch.object(file_utils, 'replace_file')
def test_delete_listener(self, replace_file):
# Create a listener first.
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin'
})
self.haproxy.create_listener(listener)
listener = db_api.get_listener('test_listener')
config_data = replace_file.call_args[0][1]
self.assertIn(
'frontend %s' % listener.name,
config_data
)
db_api.delete_listener(listener.name)
self.haproxy.delete_listener(listener.name)
config_data = replace_file.call_args[0][1]
self.assertNotIn(
'frontend %s' % listener.name,
config_data
)
self.assertRaises(
exc.NotFoundException,
db_api.get_listener,
listener.name
)
@mock.patch.object(file_utils, 'replace_file')
def test_delete_member(self, replace_file):
listener = db_api.create_listener({
'name': 'test_listener',
'description': 'my test settings',
'protocol': 'http',
'protocol_port': 80,
'algorithm': 'roundrobin'
})
# Create a listener first.
self.haproxy.create_listener(listener)
listener = db_api.get_listener('test_listener')
member = db_api.create_member({
'listener_id': listener.id,
'name': 'member1',
'address': '10.0.0.1',
'protocol_port': 80,
})
self.haproxy.create_member(member)
member = db_api.get_member('member1')
config_data = replace_file.call_args[0][1]
self.assertIn(
'\tserver %s %s:%s' %
(member.name, member.address, member.protocol_port),
config_data
)
db_api.delete_member(member.name)
self.haproxy.delete_member(member)
config_data = replace_file.call_args[0][1]
self.assertNotIn(
'\tserver %s %s:%s' %
(member.name, member.address, member.protocol_port),
config_data
)
self.assertRaises(
exc.NotFoundException,
db_api.get_member,
member.name
)

View File

@ -0,0 +1,331 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
# Copyright 2015 - Huawei Technologies Co. Ltd
#
# 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 contextlib
import json
import logging
import os
from os import path
import shutil
import six
import socket
import tempfile
import threading
import uuid
import eventlet
from eventlet import corolocal
from oslo_concurrency import processutils
import pkg_resources as pkg
import random
from lbaas import exceptions as exc
from lbaas import version
# Thread local storage.
_th_loc_storage = threading.local()
def generate_unicode_uuid():
return six.text_type(str(uuid.uuid4()))
def _get_greenlet_local_storage():
greenlet_id = corolocal.get_ident()
greenlet_locals = getattr(_th_loc_storage, "greenlet_locals", None)
if not greenlet_locals:
greenlet_locals = {}
_th_loc_storage.greenlet_locals = greenlet_locals
if greenlet_id in greenlet_locals:
return greenlet_locals[greenlet_id]
else:
return None
def has_thread_local(var_name):
gl_storage = _get_greenlet_local_storage()
return gl_storage and var_name in gl_storage
def get_thread_local(var_name):
if not has_thread_local(var_name):
return None
return _get_greenlet_local_storage()[var_name]
def set_thread_local(var_name, val):
if not val and has_thread_local(var_name):
gl_storage = _get_greenlet_local_storage()
# Delete variable from greenlet local storage.
if gl_storage:
del gl_storage[var_name]
# Delete the entire greenlet local storage from thread local storage.
if gl_storage and len(gl_storage) == 0:
del _th_loc_storage.greenlet_locals[corolocal.get_ident()]
if val:
gl_storage = _get_greenlet_local_storage()
if not gl_storage:
gl_storage = _th_loc_storage.greenlet_locals[
corolocal.get_ident()] = {}
gl_storage[var_name] = val
def log_exec(logger, level=logging.DEBUG):
"""Decorator for logging function execution.
By default, target function execution is logged with DEBUG level.
"""
def _decorator(func):
def _logged(*args, **kw):
params_repr = ("[args=%s, kw=%s]" % (str(args), str(kw))
if args or kw else "")
func_repr = ("Called method [name=%s, doc='%s', params=%s]" %
(func.__name__, func.__doc__, params_repr))
logger.log(level, func_repr)
return func(*args, **kw)
_logged.__doc__ = func.__doc__
return _logged
return _decorator
def merge_dicts(left, right, overwrite=True):
"""Merges two dictionaries.
Values of right dictionary recursively get merged into left dictionary.
:param left: Left dictionary.
:param right: Right dictionary.
:param overwrite: If False, left value will not be overwritten if exists.
"""
if left is None:
return right
if right is None:
return left
for k, v in six.iteritems(right):
if k not in left:
left[k] = v
else:
left_v = left[k]
if isinstance(left_v, dict) and isinstance(v, dict):
merge_dicts(left_v, v, overwrite=overwrite)
elif overwrite:
left[k] = v
return left
def get_file_list(directory):
base_path = pkg.resource_filename(
version.version_info.package,
directory
)
return [path.join(base_path, f) for f in os.listdir(base_path)
if path.isfile(path.join(base_path, f))]
def cut(data, length=100):
if not data:
return data
string = str(data)
if len(string) > length:
return "%s..." % string[:length]
else:
return string
def iter_subclasses(cls, _seen=None):
"""Generator over all subclasses of a given class in depth first order."""
if not isinstance(cls, type):
raise TypeError('iter_subclasses must be called with new-style class'
', not %.100r' % cls)
_seen = _seen or set()
try:
subs = cls.__subclasses__()
except TypeError: # fails only when cls is type
subs = cls.__subclasses__(cls)
for sub in subs:
if sub not in _seen:
_seen.add(sub)
yield sub
for _sub in iter_subclasses(sub, _seen):
yield _sub
def random_sleep(limit=1):
"""Sleeps for a random period of time not exceeding the given limit.
Mostly intended to be used by tests to emulate race conditions.
:param limit: Float number of seconds that a sleep period must not exceed.
"""
seconds = random.Random().randint(0, limit * 1000) * 0.001
print("Sleep: %s sec..." % seconds)
eventlet.sleep(seconds)
class NotDefined(object):
"""This class is just a marker of input params without value."""
pass
def get_dict_from_string(input_string, delimiter=','):
if not input_string:
return {}
raw_inputs = input_string.split(delimiter)
inputs = []
for raw in raw_inputs:
input = raw.strip()
name_value = input.split('=')
if len(name_value) > 1:
try:
value = json.loads(name_value[1])
except ValueError:
value = name_value[1]
inputs += [{name_value[0]: value}]
else:
inputs += [name_value[0]]
return get_input_dict(inputs)
def get_input_dict(inputs):
"""Transform input list to dictionary.
Ensure every input param has a default value(it will be a NotDefined
object if it's not provided).
"""
input_dict = {}
for x in inputs:
if isinstance(x, dict):
input_dict.update(x)
else:
# NOTE(xylan): we put a NotDefined class here as the value of
# param without value specified, to distinguish from the valid
# values such as None, ''(empty string), etc.
input_dict[x] = NotDefined
return input_dict
def get_process_identifier():
"""Gets current running process identifier."""
return "%s_%s" % (socket.gethostname(), os.getpid())
@contextlib.contextmanager
def tempdir(**kwargs):
argdict = kwargs.copy()
if 'dir' not in argdict:
argdict['dir'] = '/tmp/'
tmpdir = tempfile.mkdtemp(**argdict)
try:
yield tmpdir
finally:
try:
shutil.rmtree(tmpdir)
except OSError as e:
raise exc.DataAccessException(
"Failed to delete temp dir %(dir)s (reason: %(reason)s)" %
{'dir': tmpdir, 'reason': e}
)
def save_text_to(text, file_path, overwrite=False):
if os.path.exists(file_path) and not overwrite:
raise exc.DataAccessException(
"Cannot save data to file. File %s already exists."
)
with open(file_path, 'w') as f:
f.write(text)
def generate_key_pair(key_length=2048):
"""Create RSA key pair with specified number of bits in key.
Returns tuple of private and public keys.
"""
with tempdir() as tmpdir:
keyfile = os.path.join(tmpdir, 'tempkey')
args = [
'ssh-keygen',
'-q', # quiet
'-N', '', # w/o passphrase
'-t', 'rsa', # create key of rsa type
'-f', keyfile, # filename of the key file
'-C', 'Generated-by-Mistral' # key comment
]
if key_length is not None:
args.extend(['-b', key_length])
processutils.execute(*args)
if not os.path.exists(keyfile):
raise exc.DataAccessException(
"Private key file hasn't been created"
)
private_key = open(keyfile).read()
public_key_path = keyfile + '.pub'
if not os.path.exists(public_key_path):
raise exc.DataAccessException(
"Public key file hasn't been created"
)
public_key = open(public_key_path).read()
return private_key, public_key

View File

@ -0,0 +1,33 @@
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import tempfile
def replace_file(file_name, data, file_mode=0o644):
"""Replaces the contents of file_name with data in a safe manner.
First write to a temp file and then rename. Since POSIX renames are
atomic, the file is unlikely to be corrupted by competing writes.
We create the tempfile on the same device to ensure that it can be renamed.
"""
base_dir = os.path.dirname(os.path.abspath(file_name))
with tempfile.NamedTemporaryFile('w+',
dir=base_dir,
delete=False) as tmp_file:
tmp_file.write(data)
os.chmod(tmp_file.name, file_mode)
os.rename(tmp_file.name, file_name)

View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
import json
import pecan
import six
from webob import Response
from wsme import exc
from lbaas import exceptions as ex
def wrap_wsme_controller_exception(func):
"""Decorator for controllers method.
This decorator wraps controllers method to manage wsme exceptions:
In case of expected error it aborts the request with specific status code.
"""
@functools.wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except ex.LBaaSException as excp:
pecan.response.translatable_error = excp
raise exc.ClientSideError(msg=six.text_type(excp),
status_code=excp.http_code)
return wrapped
def wrap_pecan_controller_exception(func):
"""Decorator for controllers method.
This decorator wraps controllers method to manage pecan exceptions:
In case of expected error it aborts the request with specific status code.
"""
@functools.wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except ex.LBaaSException as excp:
return Response(
status=excp.http_code,
content_type='application/json',
body=json.dumps(dict(
faultstring=six.text_type(excp))))
return wrapped
def validate_query_params(limit, sort_keys, sort_dirs):
if limit is not None and limit <= 0:
raise exc.ClientSideError("Limit must be positive.")
if len(sort_keys) < len(sort_dirs):
raise exc.ClientSideError("Length of sort_keys must be equal or "
"greater than sort_dirs.")
if len(sort_keys) > len(sort_dirs):
sort_dirs.extend(['asc'] * (len(sort_keys) - len(sort_dirs)))
for sort_dir in sort_dirs:
if sort_dir not in ['asc', 'desc']:
raise exc.ClientSideError("Unknown sort direction, must be 'desc' "
"or 'asc'.")
def validate_fields(fields, object_fields):
"""Check for requested non-existent fields.
Check if the user requested non-existent fields.
:param fields: A list of fields requested by the user.
:param object_fields: A list of fields supported by the object.
"""
if not fields:
return
invalid_fields = set(fields) - set(object_fields)
if invalid_fields:
raise exc.ClientSideError(
'Field(s) %s are invalid.' % ', '.join(invalid_fields)
)

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 - Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pbr import version
version_info = version.VersionInfo('lbaas')
version_string = version_info.version_string

View File

@ -0,0 +1,21 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
alembic>=0.8.0
argparse
eventlet>=0.17.4
jsonschema!=2.5.0,<3.0.0,>=2.0.0
mock>=1.2
oslo.concurrency>=2.3.0 # Apache-2.0
oslo.config>=2.7.0 # Apache-2.0
oslo.db>=3.2.0 # Apache-2.0
oslo.utils>=2.8.0 # Apache-2.0
oslo.log>=1.12.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
pbr>=1.6
pecan>=1.0.0
requests>=2.8.1
six>=1.9.0
SQLAlchemy<1.1.0,>=0.9.9
stevedore>=1.5.0 # Apache-2.0
WSME>=0.8

View File

@ -0,0 +1,36 @@
[metadata]
name = lbaas
summary = LBaaS Project
description-file =
README.md
license = Apache License, Version 2.0
classifiers =
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
#License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
author = Mirantis Inc.
author-email = nmakhotkin@mirantis.com
[files]
packages =
lbaas
[entry_points]
console_scripts =
lbaas-server = lbaas.cmd.launch:main
lbaas-db-manage = lbaas.db.sqlalchemy.migration.cli:main
oslo.config.opts =
lbaas.config = lbaas.config:list_opts
lbaas.drivers =
haproxy = lbaas.drivers.haproxy:HAProxyDriver
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

View File

@ -0,0 +1,30 @@
# Copyright (c) 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.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=1.8'],
pbr=True
)

View File

@ -0,0 +1,18 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
coverage>=3.6
fixtures>=1.3.1
hacking<0.11,>=0.10.0
nose
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
pyflakes==0.8.1
pylint==1.4.4 # GNU GPL v2
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
sphinxcontrib-httpdomain
sphinxcontrib-pecanwsme>=0.8
testrepository>=0.0.18
testtools>=1.4.0
unittest2
reno>=0.1.1 # Apache2

View File

@ -0,0 +1,16 @@
TOX_ENVLIST=`grep envlist tox.ini | cut -d '=' -f 2 | tr ',' ' '`
TESTENVS=`grep testenv tox.ini | awk -F ':' '{print $2}' | tr '[]' ' '`
UNFILTERED_ENVLIST=`echo "$TOX_ENVLIST $TESTENVS"`
ENVLIST=$( awk 'BEGIN{RS=ORS=" "}!a[$0]++' <<<$UNFILTERED_ENVLIST );
for env in $ENVLIST
do
ENV_PATH=.tox/$env
PIP_PATH=$ENV_PATH/bin/pip
echo -e "\nUpdate environment ${env}...\n"
if [ ! -d $ENV_PATH -o ! -f $PIP_PATH ]
then
tox --notest -e$env
else
$PIP_PATH install -r requirements.txt -r test-requirements.txt
fi
done

View File

@ -0,0 +1,7 @@
#!/bin/bash
tools_path=${tools_path:-$(dirname $0)}
venv_path=${venv_path:-${tools_path}}
venv_dir=${venv_name:-/../.venv}
TOOLS=${tools_path}
VENV=${venv:-${venv_path}/${venv_dir}}
source ${VENV}/bin/activate && "$@"

View File

@ -0,0 +1,42 @@
[tox]
envlist = py27,py33,py34,pep8
minversion = 1.6
skipsdist = True
[testenv]
sitepackages = True
usedevelop = True
install_command = pip install -U --force-reinstall {opts} {packages}
setenv = VIRTUAL_ENV={envdir}
PYTHONDONTWRITEBYTECODE = 1
deps = -r{toxinidir}/test-requirements.txt
commands =
python setup.py testr --slowest --testr-args='{posargs}'
whitelist_externals = rm
[testenv:pep8]
commands = flake8 {posargs}
[testenv:cover]
# Also do not run test_coverage_ext tests while gathering coverage as those
# tests conflict with coverage.
setenv = VIRTUAL_ENV={envdir}
commands =
python setup.py testr --coverage \
--testr-args='^(?!.*test.*coverage).*$'
[testenv:venv]
commands = {posargs}
[testenv:docs]
commands = python setup.py build_sphinx
[testenv:pylint]
setenv = VIRTUAL_ENV={envdir}
commands = bash tools/lintstack.sh
[flake8]
show-source = true
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools,scripts

View File

@ -0,0 +1,34 @@
# Stop the script if an error occurs.
set -e
function cleanup {
cd $SCRIPTPATH
rm -rf tmp
}
# In case if script is running not where it is located.
cd $(dirname $0)
SCRIPTPATH=`pwd`
# Cleanup tmp dir on script exit.
trap 'cleanup' EXIT
mkdir tmp
cp -v -r Classes Resources manifest.yaml tmp/
archive_name=lbaas.tar.gz
lbaas_directory_name=lbaas_api-0.1
# Pack python tarball.
pushd tmp/Resources/scripts
tar -czvf $archive_name $lbaas_directory_name/*
base64 $archive_name > $archive_name.bs64
rm -rf $lbaas_directory_name
rm -rf $archive_name
popd
# Make murano package.
pushd tmp
zip -r ../LBaaS_Library.zip .
popd