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:
commit
cb3fa8e136
1
murano-apps/LBaaS-interface/.gitignore
vendored
Normal file
1
murano-apps/LBaaS-interface/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
LBaaS_Library.zip
|
@ -1,587 +0,0 @@
|
||||
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
|
||||
cvfduP8DBe+HRgBwAwA=
|
@ -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
|
@ -0,0 +1 @@
|
||||
Nikolay Mahotkin <nmakhotkin@mirantis.com>
|
@ -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
|
@ -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 listener’s 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 member’s 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
|
@ -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 listener’s 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 member’s 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.
|
@ -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
|
||||
|
@ -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=
|
@ -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
|
@ -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')
|
@ -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]
|
@ -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()
|
@ -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)
|
@ -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'))
|
105
murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/launch.py
Executable file
105
murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/launch.py
Executable 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()
|
@ -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')
|
@ -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)
|
@ -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
|
@ -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
|
@ -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()
|
@ -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"}
|
@ -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')
|
||||
)
|
@ -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)
|
||||
)
|
@ -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)
|
||||
)
|
@ -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()
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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'
|
||||
)
|
@ -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
|
@ -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)
|
@ -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))
|
@ -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"
|
@ -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')
|
@ -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)
|
@ -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']))
|
@ -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']
|
||||
)
|
@ -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
|
@ -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())
|
@ -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
|
||||
)
|
@ -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
|
@ -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)
|
@ -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)
|
||||
)
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
)
|
@ -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
|
@ -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
|
@ -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 && "$@"
|
@ -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
|
||||
|
34
murano-apps/LBaaS-interface/build_package.sh
Executable file
34
murano-apps/LBaaS-interface/build_package.sh
Executable 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
|
Loading…
x
Reference in New Issue
Block a user