From bd12b4fd1edfdbd9a5f79c1e3a19d76f7f63245f Mon Sep 17 00:00:00 2001 From: Nikolay Mahotkin Date: Tue, 5 Apr 2016 16:33:20 +0300 Subject: [PATCH 1/2] Adding a new version of python LBaaS API * For review, it is added as source code instead of archive * Note that it should be packed into .tar.gz, then encoded using base64 and renamed to 'lbaas.tar.gz.bs64' before uploading the package to murano Change-Id: I2b6d49aea9c383b877cebbd603cf3a23f65a66d1 --- .../Resources/scripts/lbaas.tar.gz.bs64 | 587 ------------------ .../scripts/lbaas_api-0.1/.testr.conf | 9 + .../Resources/scripts/lbaas_api-0.1/AUTHORS | 1 + .../Resources/scripts/lbaas_api-0.1/ChangeLog | 34 + .../Resources/scripts/lbaas_api-0.1/PKG-INFO | 116 ++++ .../Resources/scripts/lbaas_api-0.1/README.md | 105 ++++ .../lbaas_api-0.1/etc/lbaas.conf.sample | 232 +++++++ .../lbaas_api-0.1/etc/logging.conf.sample | 32 + .../scripts/lbaas_api-0.1/lbaas/__init__.py | 0 .../lbaas_api-0.1/lbaas/api/__init__.py | 0 .../scripts/lbaas_api-0.1/lbaas/api/app.py | 52 ++ .../lbaas/api/controllers/__init__.py | 0 .../lbaas/api/controllers/resource.py | 156 +++++ .../lbaas/api/controllers/root.py | 57 ++ .../lbaas/api/controllers/v1/__init__.py | 0 .../lbaas/api/controllers/v1/listener.py | 139 +++++ .../lbaas/api/controllers/v1/member.py | 142 +++++ .../lbaas/api/controllers/v1/root.py | 41 ++ .../lbaas_api-0.1/lbaas/cmd/__init__.py | 0 .../scripts/lbaas_api-0.1/lbaas/cmd/launch.py | 105 ++++ .../scripts/lbaas_api-0.1/lbaas/config.py | 101 +++ .../lbaas_api-0.1/lbaas/db/__init__.py | 0 .../lbaas/db/sqlalchemy/__init__.py | 0 .../lbaas_api-0.1/lbaas/db/sqlalchemy/base.py | 192 ++++++ .../lbaas/db/sqlalchemy/migration/__init__.py | 0 .../lbaas/db/sqlalchemy/migration/alembic.ini | 58 ++ .../migration/alembic_migrations/README.md | 66 ++ .../migration/alembic_migrations/__init__.py | 0 .../migration/alembic_migrations/env.py | 86 +++ .../alembic_migrations/script.py.mako | 34 + .../versions/001_initial_lbaas_scheme.py | 63 ++ .../versions/002_add_options_to_listeners.py | 42 ++ .../003_added_address_field_to_listener.py | 37 ++ .../alembic_migrations/versions/__init__.py | 0 .../lbaas/db/sqlalchemy/migration/cli.py | 121 ++++ .../lbaas/db/sqlalchemy/model_base.py | 107 ++++ .../lbaas/db/sqlalchemy/types.py | 96 +++ .../lbaas_api-0.1/lbaas/db/v1/__init__.py | 0 .../scripts/lbaas_api-0.1/lbaas/db/v1/api.py | 128 ++++ .../lbaas/db/v1/sqlalchemy/__init__.py | 0 .../lbaas/db/v1/sqlalchemy/api.py | 281 +++++++++ .../lbaas/db/v1/sqlalchemy/models.py | 83 +++ .../lbaas_api-0.1/lbaas/drivers/__init__.py | 0 .../lbaas_api-0.1/lbaas/drivers/base.py | 45 ++ .../lbaas_api-0.1/lbaas/drivers/driver.py | 34 + .../lbaas_api-0.1/lbaas/drivers/haproxy.py | 159 +++++ .../scripts/lbaas_api-0.1/lbaas/exceptions.py | 76 +++ .../lbaas_api-0.1/lbaas/tests/__init__.py | 0 .../lbaas/tests/unit/__init__.py | 0 .../lbaas/tests/unit/api/__init__.py | 0 .../lbaas/tests/unit/api/base.py | 53 ++ .../lbaas/tests/unit/api/v1/__init__.py | 0 .../lbaas/tests/unit/api/v1/test_listeners.py | 179 ++++++ .../lbaas/tests/unit/api/v1/test_members.py | 171 +++++ .../lbaas/tests/unit/api/v1/test_root.py | 46 ++ .../lbaas_api-0.1/lbaas/tests/unit/base.py | 183 ++++++ .../lbaas/tests/unit/db/__init__.py | 0 .../lbaas/tests/unit/db/v1/__init__.py | 0 .../unit/db/v1/test_sqlalchemy_db_api.py | 384 ++++++++++++ .../lbaas/tests/unit/drivers/__init__.py | 0 .../lbaas/tests/unit/drivers/test_haproxy.py | 294 +++++++++ .../lbaas_api-0.1/lbaas/utils/__init__.py | 331 ++++++++++ .../lbaas_api-0.1/lbaas/utils/file_utils.py | 33 + .../lbaas_api-0.1/lbaas/utils/rest_utils.py | 98 +++ .../scripts/lbaas_api-0.1/lbaas/version.py | 20 + .../scripts/lbaas_api-0.1/requirements.txt | 21 + .../Resources/scripts/lbaas_api-0.1/setup.cfg | 36 ++ .../Resources/scripts/lbaas_api-0.1/setup.py | 30 + .../lbaas_api-0.1/test-requirements.txt | 18 + .../lbaas_api-0.1/tools/update_env_deps | 16 + .../scripts/lbaas_api-0.1/tools/with_venv.sh | 7 + .../Resources/scripts/lbaas_api-0.1/tox.ini | 42 ++ 72 files changed, 4992 insertions(+), 587 deletions(-) delete mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas.tar.gz.bs64 create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/.testr.conf create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/AUTHORS create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/ChangeLog create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/PKG-INFO create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/README.md create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/lbaas.conf.sample create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/logging.conf.sample create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/app.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/resource.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/root.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/listener.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/member.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/root.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/__init__.py create mode 100755 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/launch.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/config.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/base.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic.ini create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/README.md create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/env.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/script.py.mako create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/001_initial_lbaas_scheme.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/002_add_options_to_listeners.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/003_added_address_field_to_listener.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/cli.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/model_base.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/types.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/api.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/api.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/models.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/base.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/driver.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/haproxy.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/exceptions.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/base.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_listeners.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_members.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_root.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/base.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/v1/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/v1/test_sqlalchemy_db_api.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/drivers/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/drivers/test_haproxy.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/__init__.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/file_utils.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/rest_utils.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/version.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/requirements.txt create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.cfg create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.py create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/test-requirements.txt create mode 100755 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/update_env_deps create mode 100755 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/with_venv.sh create mode 100644 murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tox.ini diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas.tar.gz.bs64 b/murano-apps/LBaaS-interface/Resources/scripts/lbaas.tar.gz.bs64 deleted file mode 100644 index 693604d..0000000 --- a/murano-apps/LBaaS-interface/Resources/scripts/lbaas.tar.gz.bs64 +++ /dev/null @@ -1,587 +0,0 @@ -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 -cvfduP8DBe+HRgBwAwA= diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/.testr.conf b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/.testr.conf new file mode 100644 index 0000000..411405a --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/.testr.conf @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/AUTHORS b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/AUTHORS new file mode 100644 index 0000000..d3a46a0 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/AUTHORS @@ -0,0 +1 @@ +Nikolay Mahotkin diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/ChangeLog b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/ChangeLog new file mode 100644 index 0000000..00070e1 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/ChangeLog @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/PKG-INFO b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/PKG-INFO new file mode 100644 index 0000000..c1efc7e --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/PKG-INFO @@ -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/** + + Gets particular listener from LBaaS. name - the listener’s name. + + **PUT /v1/listeners/** + + Update listener info by its name. Returns 200 code if succeed. + Request body example: + + { + “protocol_port”: 8080, + “name”: “app” + } + + + **DELETE /v1/listeners/** + + 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/** + + Gets particular member from LBaaS. name - the member’s name. + + + **PUT /v1/members/** + + Update listener info by its name. Returns 200 code if succeed. + + Request body example: + + { + “protocol_port”: 8080, + } + + + **DELETE /v1/members/** + + Deletes the whole member by its name. Returns 204 if succeed. + + +Platform: UNKNOWN diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/README.md b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/README.md new file mode 100644 index 0000000..cd369a0 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/README.md @@ -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/** + +Gets particular listener from LBaaS. name - the listener’s name. + +**PUT /v1/listeners/** + +Update listener info by its name. Returns 200 code if succeed. +Request body example: + + { + “protocol_port”: 8080, + “name”: “app” + } + + +**DELETE /v1/listeners/** + +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/** + +Gets particular member from LBaaS. name - the member’s name. + + +**PUT /v1/members/** + +Update listener info by its name. Returns 200 code if succeed. + +Request body example: + + { + “protocol_port”: 8080, + } + + +**DELETE /v1/members/** + +Deletes the whole member by its name. Returns 204 if succeed. diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/lbaas.conf.sample b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/lbaas.conf.sample new file mode 100644 index 0000000..580321c --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/lbaas.conf.sample @@ -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 = + +# 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 = + +# 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 = + +# (Optional) The base directory used for relative --log-file paths. +# (string value) +# Deprecated group/name - [DEFAULT]/logdir +#log_dir = + +# 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 = + +# The SQLAlchemy connection string to use to connect to the slave +# database. (string value) +#slave_connection = + +# 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 = + +# 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 = + +# 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 = + +# 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 + diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/logging.conf.sample b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/logging.conf.sample new file mode 100644 index 0000000..4d76688 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/etc/logging.conf.sample @@ -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= diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/app.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/app.py new file mode 100644 index 0000000..0687753 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/app.py @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/resource.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/resource.py new file mode 100644 index 0000000..09549d9 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/resource.py @@ -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') diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/root.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/root.py new file mode 100644 index 0000000..88a428d --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/root.py @@ -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] diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/listener.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/listener.py new file mode 100644 index 0000000..d9ac4ff --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/listener.py @@ -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() diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/member.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/member.py new file mode 100644 index 0000000..f7f127a --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/member.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/root.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/root.py new file mode 100644 index 0000000..396ed5f --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/api/controllers/v1/root.py @@ -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')) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/launch.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/launch.py new file mode 100755 index 0000000..17b4f60 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/cmd/launch.py @@ -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() diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/config.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/config.py new file mode 100644 index 0000000..463e726 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/config.py @@ -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') diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/base.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/base.py new file mode 100644 index 0000000..90c58ba --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/base.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic.ini b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic.ini new file mode 100644 index 0000000..2c138fe --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic.ini @@ -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 \ No newline at end of file diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/README.md b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/README.md new file mode 100644 index 0000000..bde89d5 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/README.md @@ -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 : --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 +``` + +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 \ No newline at end of file diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/env.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/env.py new file mode 100644 index 0000000..a45e626 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/env.py @@ -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() diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/script.py.mako b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/script.py.mako new file mode 100644 index 0000000..7b7f184 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/script.py.mako @@ -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"} \ No newline at end of file diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/001_initial_lbaas_scheme.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/001_initial_lbaas_scheme.py new file mode 100644 index 0000000..ff0b689 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/001_initial_lbaas_scheme.py @@ -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') + ) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/002_add_options_to_listeners.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/002_add_options_to_listeners.py new file mode 100644 index 0000000..b0ba760 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/002_add_options_to_listeners.py @@ -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) + ) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/003_added_address_field_to_listener.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/003_added_address_field_to_listener.py new file mode 100644 index 0000000..aa37c70 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/003_added_address_field_to_listener.py @@ -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) + ) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/alembic_migrations/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/cli.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/cli.py new file mode 100644 index 0000000..3bfdec1 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/migration/cli.py @@ -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() diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/model_base.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/model_base.py new file mode 100644 index 0000000..01aaabc --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/model_base.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/types.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/types.py new file mode 100644 index 0000000..2a87f65 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/sqlalchemy/types.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/api.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/api.py new file mode 100644 index 0000000..0712aee --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/api.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/api.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/api.py new file mode 100644 index 0000000..9a6c29d --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/api.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/models.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/models.py new file mode 100644 index 0000000..1b71a62 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/db/v1/sqlalchemy/models.py @@ -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' +) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/base.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/base.py new file mode 100644 index 0000000..507a210 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/base.py @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/driver.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/driver.py new file mode 100644 index 0000000..8e49c18 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/driver.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/haproxy.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/haproxy.py new file mode 100644 index 0000000..66012bd --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/drivers/haproxy.py @@ -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)) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/exceptions.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/exceptions.py new file mode 100644 index 0000000..72ddcb9 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/exceptions.py @@ -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" diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/base.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/base.py new file mode 100644 index 0000000..a2d001f --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/base.py @@ -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') diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_listeners.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_listeners.py new file mode 100644 index 0000000..2790a38 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_listeners.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_members.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_members.py new file mode 100644 index 0000000..8665f4a --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_members.py @@ -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'])) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_root.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_root.py new file mode 100644 index 0000000..9ff5089 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/api/v1/test_root.py @@ -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'] + ) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/base.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/base.py new file mode 100644 index 0000000..5af1478 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/base.py @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/v1/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/v1/test_sqlalchemy_db_api.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/v1/test_sqlalchemy_db_api.py new file mode 100644 index 0000000..fe58566 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/db/v1/test_sqlalchemy_db_api.py @@ -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()) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/drivers/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/drivers/test_haproxy.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/drivers/test_haproxy.py new file mode 100644 index 0000000..29ffba3 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/tests/unit/drivers/test_haproxy.py @@ -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 + ) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/__init__.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/__init__.py new file mode 100644 index 0000000..4f3cf31 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/__init__.py @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/file_utils.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/file_utils.py new file mode 100644 index 0000000..6a366ab --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/file_utils.py @@ -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) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/rest_utils.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/rest_utils.py new file mode 100644 index 0000000..be7df83 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/utils/rest_utils.py @@ -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) + ) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/version.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/version.py new file mode 100644 index 0000000..caf4526 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/lbaas/version.py @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/requirements.txt b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/requirements.txt new file mode 100644 index 0000000..b993fcf --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/requirements.txt @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.cfg b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.cfg new file mode 100644 index 0000000..b384a68 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.cfg @@ -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 + diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.py b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.py new file mode 100644 index 0000000..0f35626 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/setup.py @@ -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 +) diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/test-requirements.txt b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/test-requirements.txt new file mode 100644 index 0000000..c429a22 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/test-requirements.txt @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/update_env_deps b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/update_env_deps new file mode 100755 index 0000000..5551daf --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/update_env_deps @@ -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 diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/with_venv.sh b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/with_venv.sh new file mode 100755 index 0000000..94e05c1 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tools/with_venv.sh @@ -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 && "$@" diff --git a/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tox.ini b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tox.ini new file mode 100644 index 0000000..717c877 --- /dev/null +++ b/murano-apps/LBaaS-interface/Resources/scripts/lbaas_api-0.1/tox.ini @@ -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 + From 155a3971a09f0415a382b855b05d53057e9e6875 Mon Sep 17 00:00:00 2001 From: Nikolay Mahotkin Date: Wed, 6 Apr 2016 11:57:13 +0300 Subject: [PATCH 2/2] Adding a script for building LBaaS package * LBaaS_Library.zip (the reslt of the script) is added to gitignore in order to prevent adding it to git. Change-Id: I13295661c52da8d0387545c2f3c444ec49e85034 --- murano-apps/LBaaS-interface/.gitignore | 1 + murano-apps/LBaaS-interface/build_package.sh | 34 ++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 murano-apps/LBaaS-interface/.gitignore create mode 100755 murano-apps/LBaaS-interface/build_package.sh diff --git a/murano-apps/LBaaS-interface/.gitignore b/murano-apps/LBaaS-interface/.gitignore new file mode 100644 index 0000000..055d62b --- /dev/null +++ b/murano-apps/LBaaS-interface/.gitignore @@ -0,0 +1 @@ +LBaaS_Library.zip diff --git a/murano-apps/LBaaS-interface/build_package.sh b/murano-apps/LBaaS-interface/build_package.sh new file mode 100755 index 0000000..f7ad721 --- /dev/null +++ b/murano-apps/LBaaS-interface/build_package.sh @@ -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