From 3de38762aa830eb16d554232b0d456acff3c41b0 Mon Sep 17 00:00:00 2001 From: hosingh000 Date: Sat, 3 Jun 2017 17:28:58 -0500 Subject: [PATCH] initial commit Change-Id: Ie417df96258c2daa852d14e588de4ad2d54bb819 --- .gitignore | 58 + MANIFEST.in | 1 + README.rst | 13 + babel.cfg | 2 + config.py | 54 + doc/source/admin/index.rst | 5 + doc/source/cli/index.rst | 5 + doc/source/conf.py | 81 + doc/source/configuration/index.rst | 5 + doc/source/contributor/contributing.rst | 4 + doc/source/contributor/index.rst | 9 + doc/source/index.rst | 30 + doc/source/install/common_configure.rst | 10 + doc/source/install/common_prerequisites.rst | 75 + doc/source/install/get_started.rst | 9 + doc/source/install/index.rst | 17 + doc/source/install/install-obs.rst | 34 + doc/source/install/install-rdo.rst | 33 + doc/source/install/install-ubuntu.rst | 31 + doc/source/install/install.rst | 20 + doc/source/install/next-steps.rst | 9 + doc/source/install/verify.rst | 24 + doc/source/library/index.rst | 7 + doc/source/readme.rst | 1 + doc/source/reference/index.rst | 5 + doc/source/user/index.rst | 5 + etc/orm.conf | 0 orm/__init__.py | 0 orm/app.py | 14 + orm/cmd/__init__.py | 0 orm/cmd/audit.py | 19 + orm/cmd/cms.py | 19 + orm/cmd/fms.py | 19 + orm/cmd/ims.py | 19 + orm/cmd/keystone.py | 19 + orm/cmd/rds.py | 19 + orm/cmd/rms.py | 19 + orm/cmd/uuidgen.py | 19 + orm/common/__init__.py | 0 orm/common/client/__init__.py | 0 orm/common/client/audit/__init__.py | 0 orm/common/client/keystone/CONTRIBUTING.rst | 17 + orm/common/client/keystone/HACKING.rst | 4 + orm/common/client/keystone/LICENSE | 176 + orm/common/client/keystone/MANIFEST.in | 6 + orm/common/client/keystone/README.rst | 19 + orm/common/client/keystone/__init__.py | 0 orm/common/client/keystone/babel.cfg | 2 + .../keystone/debian/aic-orm-keystone.install | 1 + orm/common/client/keystone/debian/changelog | 5 + orm/common/client/keystone/debian/compat | 1 + orm/common/client/keystone/debian/control | 13 + orm/common/client/keystone/debian/copyright | 34 + orm/common/client/keystone/debian/docs | 0 orm/common/client/keystone/debian/postrm | 4 + orm/common/client/keystone/debian/rules | 8 + .../client/keystone/debian/source/format | 1 + .../keystone/doc/source/contributing.rst | 4 + .../client/keystone/doc/source/index.rst | 25 + .../keystone/doc/source/installation.rst | 12 + .../client/keystone/doc/source/readme.rst | 1 + .../client/keystone/doc/source/usage.rst | 7 + .../keystone/keystone_utils.egg-info/PKG-INFO | 21 + .../keystone_utils.egg-info/SOURCES.txt | 25 + .../dependency_links.txt | 1 + .../keystone_utils.egg-info/not-zip-safe | 1 + .../keystone/keystone_utils.egg-info/pbr.json | 1 + .../keystone_utils.egg-info/top_level.txt | 2 + .../keystone/keystone_utils/__init__.py | 13 + .../keystone/keystone_utils/tests/__init__.py | 0 .../keystone_utils/tests/unit/__init__.py | 0 .../keystone_utils/tests/unit/test_tokens.py | 219 + .../client/keystone/keystone_utils/tokens.py | 278 + .../client/keystone/mock_keystone/__init__.py | 0 .../mock_keystone/keystoneclient/__init__.py | 0 .../keystoneclient/exceptions.py | 2 + .../keystoneclient/v2_0/__init__.py | 0 .../keystoneclient/v2_0/client.py | 6 + .../keystoneclient/v3/__init__.py | 0 .../mock_keystone/keystoneclient/v3/client.py | 6 + .../mock_keystone/orm_common/__init__.py | 0 .../orm_common/utils/__init__.py | 0 .../orm_common/utils/dictator.py | 10 + orm/common/client/keystone/requirements.txt | 5 + orm/common/client/keystone/setup.cfg | 46 + orm/common/client/keystone/setup.py | 14 + .../client/keystone/test-requirements.txt | 15 + orm/common/client/keystone/tox.ini | 24 + orm/common/orm_common/__init__.py | 0 .../extenal_mock/audit_client/__init__.py | 0 .../extenal_mock/audit_client/api/__init__.py | 0 .../extenal_mock/audit_client/api/audit.py | 6 + .../extenal_mock/keystone_utils/__init__.py | 0 .../extenal_mock/keystone_utils/tokens.py | 2 + orm/common/orm_common/hooks/__init__.py | 0 orm/common/orm_common/hooks/api_error_hook.py | 70 + .../orm_common/hooks/security_headers_hook.py | 18 + .../orm_common/hooks/transaction_id_hook.py | 17 + orm/common/orm_common/injector/__init__.py | 0 .../orm_common/injector/fang/__init__.py | 7 + .../injector/fang/dependency_register.py | 77 + orm/common/orm_common/injector/fang/di.py | 16 + orm/common/orm_common/injector/fang/errors.py | 47 + .../orm_common/injector/fang/resolver.py | 44 + .../fang/resource_provider_register.py | 68 + orm/common/orm_common/injector/injector.py | 59 + orm/common/orm_common/policy/__init__.py | 0 orm/common/orm_common/policy/_checks.py | 308 + orm/common/orm_common/policy/_parser.py | 354 + orm/common/orm_common/policy/policy.py | 184 + orm/common/orm_common/policy/qolicy.py | 536 + orm/common/orm_common/tests/__init__.py | 0 orm/common/orm_common/tests/hooks/__init__.py | 0 .../tests/hooks/test_api_error_hook.py | 68 + .../tests/hooks/test_security_headers_hook.py | 31 + .../tests/hooks/test_transaction_id_hook.py | 17 + .../orm_common/tests/injector/__init__.py | 0 .../tests/injector/test_injector.py | 58 + .../orm_common/tests/policy/__init__.py | 0 .../orm_common/tests/policy/test_checks.py | 100 + .../orm_common/tests/policy/test_policy.py | 130 + orm/common/orm_common/tests/utils/__init__.py | 0 .../tests/utils/test_api_error_utils.py | 14 + .../tests/utils/test_cross_api_utils.py | 79 + .../orm_common/tests/utils/test_utils.py | 175 + orm/common/orm_common/utils/__init__.py | 0 .../orm_common/utils/api_error_utils.py | 45 + .../orm_common/utils/cross_api_utils.py | 86 + orm/common/orm_common/utils/dictator.py | 23 + orm/common/orm_common/utils/sanitize.py | 7 + orm/common/orm_common/utils/utils.py | 225 + orm/common/tox.ini | 18 + orm/orm_client/__init__.py | 0 orm/services/__init__.py | 0 orm/services/audit_trail_manager/__init__.py | 0 orm/services/customer_manager/MANIFEST.in | 1 + orm/services/customer_manager/__init__.py | 0 orm/services/customer_manager/cms_rest.conf | 26 + orm/services/customer_manager/cms_rest.wsgi | 2 + .../customer_manager/cms_rest/__init__.py | 0 orm/services/customer_manager/cms_rest/app.py | 36 + .../cms_rest/controllers/__init__.py | 0 .../cms_rest/controllers/root.py | 34 + .../cms_rest/controllers/v1/__init__.py | 0 .../cms_rest/controllers/v1/base.py | 48 + .../cms_rest/controllers/v1/orm/__init__.py | 0 .../controllers/v1/orm/configuration.py | 29 + .../controllers/v1/orm/customer/__init__.py | 0 .../controllers/v1/orm/customer/enabled.py | 55 + .../controllers/v1/orm/customer/metadata.py | 75 + .../controllers/v1/orm/customer/regions.py | 133 + .../controllers/v1/orm/customer/root.py | 184 + .../controllers/v1/orm/customer/users.py | 246 + .../cms_rest/controllers/v1/orm/logs.py | 65 + .../cms_rest/controllers/v1/orm/root.py | 10 + .../cms_rest/controllers/v1/root.py | 6 + .../cms_rest/data/__init__.py | 0 .../cms_rest/data/data_manager.py | 279 + .../cms_rest/data/sql_alchemy/__init__.py | 0 .../cms_rest/data/sql_alchemy/base.py | 2 + .../data/sql_alchemy/cms_user_record.py | 44 + .../data/sql_alchemy/customer_record.py | 173 + .../sql_alchemy/customer_region_record.py | 92 + .../cms_rest/data/sql_alchemy/models.py | 406 + .../data/sql_alchemy/region_record.py | 44 + .../data/sql_alchemy/user_role_record.py | 83 + .../customer_manager/cms_rest/etc/policy.json | 32 + .../extenal_mock/audit_client/__init__.py | 0 .../extenal_mock/audit_client/api/__init__.py | 0 .../extenal_mock/audit_client/api/audit.py | 6 + .../extenal_mock/keystone_utils/__init__.py | 0 .../extenal_mock/keystone_utils/tokens.py | 6 + .../extenal_mock/orm_common/__init__.py | 0 .../extenal_mock/orm_common/logger.py | 2 + .../orm_common/policy/__init__.py | 0 .../extenal_mock/orm_common/policy/policy.py | 6 + .../cms_rest/extenal_mock/orm_common/utils.py | 23 + .../extenal_mock/orm_common/utils/__init__.py | 0 .../orm_common/utils/api_error_utils.py | 8 + .../orm_common/utils/cross_api_utils.py | 6 + .../extenal_mock/orm_common/utils/utils.py | 10 + .../cms_rest/logger/__init__.py | 10 + .../cms_rest/logic/__init__.py | 0 .../cms_rest/logic/customer_logic.py | 721 ++ .../cms_rest/logic/error_base.py | 20 + .../cms_rest/logic/metadata_logic.py | 109 + .../customer_manager/cms_rest/model/Model.py | 38 + .../customer_manager/cms_rest/model/Models.py | 444 + .../cms_rest/model/__init__.py | 15 + .../customer_manager/cms_rest/rds_proxy.py | 106 + .../cms_rest/templates/error.html | 12 + .../cms_rest/templates/index.html | 34 + .../cms_rest/templates/layout.html | 22 + .../cms_rest/tests/__init__.py | 22 + .../customer_manager/cms_rest/tests/config.py | 130 + .../cms_rest/tests/logic/__init__.py | 0 .../tests/logic/test_customer_logic.py | 563 + .../cms_rest/tests/rest/__init__.py | 0 .../cms_rest/tests/rest/test_customer.py | 475 + .../cms_rest/tests/rest/test_enable.py | 101 + .../cms_rest/tests/rest/test_metadata.py | 243 + .../cms_rest/tests/rest/test_regions.py | 240 + .../cms_rest/tests/rest/test_users.py | 363 + .../cms_rest/tests/simple_hook_mock.py | 6 + .../cms_rest/tests/test_authentication.py | 53 + .../cms_rest/tests/test_configuration.py | 14 + .../cms_rest/tests/test_models.py | 49 + .../cms_rest/tests/test_rds_proxy.py | 55 + .../cms_rest/tests/test_utils.py | 14 + .../cms_rest/utils/__init__.py | 0 .../cms_rest/utils/authentication.py | 58 + orm/services/customer_manager/config.py | 125 + orm/services/customer_manager/gulpfile.js | 21 + .../htmlcov/cms_rest___init___py.html | 91 + .../htmlcov/cms_rest_app_py.html | 161 + .../cms_rest_controllers___init___py.html | 91 + .../htmlcov/cms_rest_controllers_root_py.html | 159 + .../cms_rest_controllers_v1___init___py.html | 91 + .../cms_rest_controllers_v1_base_py.html | 187 + ...s_rest_controllers_v1_orm___init___py.html | 91 + ...t_controllers_v1_orm_configuration_py.html | 149 + ...ntrollers_v1_orm_customer___init___py.html | 91 + ...ontrollers_v1_orm_customer_enabled_py.html | 201 + ...ntrollers_v1_orm_customer_metadata_py.html | 241 + ...ontrollers_v1_orm_customer_regions_py.html | 357 + ...t_controllers_v1_orm_customer_root_py.html | 459 + ..._controllers_v1_orm_customer_users_py.html | 583 + .../cms_rest_controllers_v1_orm_logs_py.html | 221 + .../cms_rest_controllers_v1_orm_root_py.html | 111 + .../cms_rest_controllers_v1_root_py.html | 103 + .../htmlcov/cms_rest_logger___init___py.html | 111 + .../htmlcov/cms_rest_logic___init___py.html | 91 + .../cms_rest_logic_customer_logic_py.html | 1535 +++ .../htmlcov/cms_rest_logic_error_base_py.html | 131 + .../cms_rest_logic_metadata_logic_py.html | 309 + .../htmlcov/cms_rest_model_Model_py.html | 167 + .../htmlcov/cms_rest_model_Models_py.html | 979 ++ .../htmlcov/cms_rest_model___init___py.html | 121 + .../htmlcov/cms_rest_rds_proxy_py.html | 303 + .../htmlcov/cms_rest_utils___init___py.html | 91 + .../cms_rest_utils_authentication_py.html | 207 + .../customer_manager/htmlcov/coverage_html.js | 584 + .../customer_manager/htmlcov/index.html | 440 + .../jquery.ba-throttle-debounce.min.js | 9 + .../htmlcov/jquery.hotkeys.js | 100 + .../htmlcov/jquery.isonscreen.js | 53 + .../customer_manager/htmlcov/jquery.min.js | 9404 +++++++++++++++++ .../htmlcov/jquery.tablesorter.min.js | 1 + .../customer_manager/htmlcov/keybd_closed.png | Bin 0 -> 112 bytes .../customer_manager/htmlcov/keybd_open.png | Bin 0 -> 112 bytes .../customer_manager/htmlcov/status.json | 1 + .../customer_manager/htmlcov/style.css | 375 + orm/services/customer_manager/package.json | 33 + .../customer_manager/public/css/style.css | 43 + .../customer_manager/public/images/logo.png | Bin 0 -> 20596 bytes orm/services/customer_manager/pycharm_init.py | 3 + orm/services/customer_manager/rds_mock/app.js | 63 + .../rds_mock/public/stylesheets/style.css | 8 + .../customer_manager/rds_mock/routes/audit.js | 8 + .../customer_manager/rds_mock/routes/index.js | 9 + .../customer_manager/rds_mock/routes/rds.js | 60 + .../customer_manager/rds_mock/routes/users.js | 9 + .../customer_manager/rds_mock/routes/uuid.js | 10 + .../rds_mock/views/error.jade | 6 + .../rds_mock/views/index.jade | 5 + .../rds_mock/views/layout.jade | 7 + orm/services/customer_manager/run_pecan.py | 6 + .../db_scripts/aic_orm_cms_create_db.sql | 90 + .../db_scripts/aic_orm_cms_update_db.sql | 102 + .../scripts/shell_scripts/create_db.sh | 7 + .../customer_manager/swagger/swagger.yaml | 1072 ++ orm/services/customer_manager/tox.ini | 19 + orm/services/flavor_manager/MANIFEST.in | 1 + orm/services/flavor_manager/__init__.py | 0 orm/services/flavor_manager/config.py | 163 + .../flavor_manager/fms_mocks/__init__.py | 0 .../flavor_manager/fms_mocks/audit_mock.py | 15 + .../flavor_manager/fms_mocks/requests_mock.py | 106 + orm/services/flavor_manager/fms_rest.conf | 27 + orm/services/flavor_manager/fms_rest.wsgi | 2 + .../flavor_manager/fms_rest/__init__.py | 0 orm/services/flavor_manager/fms_rest/app.py | 31 + .../fms_rest/controllers/__init__.py | 9 + .../fms_rest/controllers/root.py | 53 + .../fms_rest/controllers/v1/__init__.py | 0 .../fms_rest/controllers/v1/orm/__init__.py | 0 .../controllers/v1/orm/configuration.py | 29 + .../controllers/v1/orm/flavors/__init__.py | 0 .../controllers/v1/orm/flavors/base.py | 64 + .../controllers/v1/orm/flavors/flavors.py | 160 + .../v1/orm/flavors/os_extra_specs.py | 172 + .../controllers/v1/orm/flavors/regions.py | 80 + .../controllers/v1/orm/flavors/tags.py | 136 + .../controllers/v1/orm/flavors/tenants.py | 92 + .../fms_rest/controllers/v1/orm/logs.py | 65 + .../fms_rest/controllers/v1/orm/orm.py | 11 + .../fms_rest/controllers/v1/v1.py | 7 + .../flavor_manager/fms_rest/data/__init__.py | 0 .../fms_rest/data/sql_alchemy/__init__.py | 0 .../fms_rest/data/sql_alchemy/data_manager.py | 102 + .../fms_rest/data/sql_alchemy/db_models.py | 557 + .../data/sql_alchemy/flavor/__init__.py | 0 .../data/sql_alchemy/flavor/flavor_record.py | 194 + .../sql_alchemy/flavor_extra_spec/__init__.py | 0 .../flavor_extra_spec_record.py | 66 + .../sql_alchemy/flavor_options/__init__.py | 0 .../flavor_options/flavor_option_record.py | 65 + .../sql_alchemy/flavor_region/__init__.py | 0 .../flavor_region/flavor_region_record.py | 61 + .../data/sql_alchemy/flavor_tags/__init__.py | 0 .../flavor_tags/flavor_tag_record.py | 65 + .../sql_alchemy/flavor_tenant/__init__.py | 0 .../flavor_tenant/flavor_tenant_record.py | 61 + .../fms_rest/data/wsme/__init__.py | 14 + .../fms_rest/data/wsme/model.py | 13 + .../fms_rest/data/wsme/models.py | 556 + .../fms_rest/di_providers/__init__.py | 0 .../fms_rest/di_providers/mock_providers.py | 16 + .../fms_rest/di_providers/prod_providers.py | 17 + .../flavor_manager/fms_rest/etc/policy.json | 31 + .../external_mock/audit_client/__init__.py | 0 .../audit_client/api/__init__.py | 0 .../external_mock/audit_client/api/audit.py | 6 + .../external_mock/keystone_utils/__init__.py | 0 .../external_mock/keystone_utils/tokens.py | 6 + .../external_mock/orm_common/__init__.py | 0 .../orm_common/hooks/__init__.py | 0 .../orm_common/hooks/transaction_id_hook.py | 7 + .../orm_common/injector/__init__.py | 0 .../orm_common/injector/fang/__init__.py | 7 + .../injector/fang/dependency_register.py | 77 + .../orm_common/injector/fang/di.py | 16 + .../orm_common/injector/fang/errors.py | 47 + .../orm_common/injector/fang/resolver.py | 44 + .../fang/resource_provider_register.py | 68 + .../orm_common/injector/injector.py | 58 + .../orm_common/logger/__init__.py | 10 + .../orm_common/policy/__init__.py | 0 .../external_mock/orm_common/policy/policy.py | 6 + .../external_mock/orm_common/utils.py | 8 + .../orm_common/utils/__init__.py | 10 + .../orm_common/utils/api_error_utils.py | 2 + .../orm_common/utils/cross_api_utils.py | 109 + .../external_mock/orm_common/utils/utils.py | 116 + .../flavor_manager/fms_rest/hooks/__init__.py | 0 .../fms_rest/hooks/service_hooks.py | 17 + .../fms_rest/logger/__init__.py | 10 + .../flavor_manager/fms_rest/logic/__init__.py | 0 .../fms_rest/logic/error_base.py | 31 + .../fms_rest/logic/flavor_logic.py | 850 ++ .../fms_rest/proxies/__init__.py | 0 .../fms_rest/proxies/rds_proxy.py | 104 + .../fms_rest/templates/error.html | 12 + .../fms_rest/templates/index.html | 34 + .../fms_rest/templates/layout.html | 22 + .../flavor_manager/fms_rest/tests/__init__.py | 22 + .../flavor_manager/fms_rest/tests/config.py | 137 + .../fms_rest/tests/data/__init__.py | 0 .../fms_rest/tests/data/test_wsme_models.py | 79 + .../fms_rest/tests/logic/__init__.py | 0 .../fms_rest/tests/logic/test_flavor_logic.py | 768 ++ .../fms_rest/tests/mocks/__init__.py | 0 .../fms_rest/tests/rest/__init__.py | 0 .../fms_rest/tests/rest/test_flavors.py | 423 + .../fms_rest/tests/rest/test_logs.py | 25 + .../tests/rest/test_os_extra_specs.py | 195 + .../fms_rest/tests/rest/test_regions.py | 150 + .../fms_rest/tests/rest/test_tags.py | 354 + .../fms_rest/tests/rest/test_tenants.py | 145 + .../fms_rest/tests/simple_hook_mock.py | 6 + .../fms_rest/tests/test_authentication.py | 53 + .../fms_rest/tests/test_configuration.py | 15 + .../fms_rest/tests/test_models.py | 41 + .../fms_rest/tests/test_rds_proxy.py | 45 + .../fms_rest/tests/test_utils.py | 14 + .../flavor_manager/fms_rest/utils/__init__.py | 0 .../fms_rest/utils/authentication.py | 55 + .../flavor_manager/fms_rest/utils/utils.py | 130 + orm/services/flavor_manager/pycharm_init.py | 3 + orm/services/flavor_manager/run_pecan.py | 6 + .../db_scripts/aic_orm_fms_create_db.sql | 102 + .../db_scripts/aic_orm_fms_update_db.sql | 26 + .../scripts/shell_scripts/create_db.sh | 8 + .../flavor_manager/swagger/swagger.yaml | 748 ++ orm/services/flavor_manager/tox.ini | 23 + orm/services/id_generator/__init__.py | 0 orm/services/image_manager/MANIFEST.in | 1 + orm/services/image_manager/__init__.py | 0 orm/services/image_manager/config.py | 122 + .../image_manager/data_manager_test.py | 229 + orm/services/image_manager/ims.conf | 26 + orm/services/image_manager/ims.wsgi | 2 + orm/services/image_manager/ims/__init__.py | 0 orm/services/image_manager/ims/app.py | 36 + .../image_manager/ims/controllers/__init__.py | 10 + .../image_manager/ims/controllers/root.py | 45 + .../ims/controllers/v1/__init__.py | 9 + .../ims/controllers/v1/orm/__init__.py | 1 + .../ims/controllers/v1/orm/configuration.py | 29 + .../ims/controllers/v1/orm/images/__init__.py | 1 + .../ims/controllers/v1/orm/images/base.py | 49 + .../controllers/v1/orm/images/customers.py | 107 + .../ims/controllers/v1/orm/images/enabled.py | 74 + .../ims/controllers/v1/orm/images/images.py | 201 + .../ims/controllers/v1/orm/images/metadata.py | 43 + .../ims/controllers/v1/orm/images/regions.py | 122 + .../ims/controllers/v1/orm/logs.py | 72 + .../ims/controllers/v1/orm/orm.py | 11 + .../ims/controllers/v1/orm/root.py | 8 + .../image_manager/ims/controllers/v1/root.py | 8 + .../image_manager/ims/controllers/v1/v1.py | 7 + .../ims/di_providers/__init__.py | 0 .../ims/di_providers/mock_providers.py | 18 + .../ims/di_providers/prod_providers.py | 19 + .../image_manager/ims/etc/policy.json | 25 + .../external_mock/audit_client/__init__.py | 0 .../audit_client/api/__init__.py | 0 .../external_mock/audit_client/api/audit.py | 6 + .../external_mock/keystone_utils/__init__.py | 0 .../external_mock/keystone_utils/tokens.py | 10 + .../ims/external_mock/orm_common/__init__.py | 0 .../orm_common/hooks/__init__.py | 0 .../orm_common/hooks/transaction_id_hook.py | 7 + .../orm_common/injector/__init__.py | 0 .../orm_common/injector/fang/__init__.py | 7 + .../injector/fang/dependency_register.py | 77 + .../orm_common/injector/fang/di.py | 16 + .../orm_common/injector/fang/errors.py | 47 + .../orm_common/injector/fang/resolver.py | 44 + .../fang/resource_provider_register.py | 68 + .../orm_common/injector/injector.py | 55 + .../orm_common/logger/__init__.py | 10 + .../orm_common/policy/__init__.py | 0 .../external_mock/orm_common/policy/policy.py | 6 + .../ims/external_mock/orm_common/utils.py | 8 + .../orm_common/utils/__init__.py | 10 + .../orm_common/utils/api_error_utils.py | 2 + .../orm_common/utils/cross_api_utils.py | 109 + .../external_mock/orm_common/utils/utils.py | 116 + .../image_manager/ims/hooks/__init__.py | 0 .../image_manager/ims/hooks/service_hooks.py | 12 + .../image_manager/ims/ims_mocks/__init__.py | 0 .../image_manager/ims/ims_mocks/audit_mock.py | 8 + .../ims/ims_mocks/requests_mock.py | 110 + .../image_manager/ims/logger/__init__.py | 10 + .../image_manager/ims/logic/__init__.py | 0 .../image_manager/ims/logic/error_base.py | 38 + .../image_manager/ims/logic/image_logic.py | 551 + .../image_manager/ims/logic/metadata_logic.py | 33 + .../image_manager/ims/persistency/__init__.py | 0 .../ims/persistency/sql_alchemy/__init__.py | 0 .../persistency/sql_alchemy/data_manager.py | 89 + .../ims/persistency/sql_alchemy/db_models.py | 408 + .../persistency/sql_alchemy/image/__init__.py | 0 .../sql_alchemy/image/image_record.py | 181 + .../persistency/sql_alchemy/infra/__init__.py | 0 .../persistency/sql_alchemy/infra/record.py | 28 + .../ims/persistency/wsme/__init__.py | 0 .../ims/persistency/wsme/base.py | 15 + .../ims/persistency/wsme/models.py | 463 + .../image_manager/ims/proxies/__init__.py | 0 .../image_manager/ims/proxies/rds_proxy.py | 104 + .../image_manager/ims/tests/__init__.py | 22 + .../image_manager/ims/tests/config.py | 107 + .../ims/tests/controllers/__init__.py | 0 .../ims/tests/controllers/v1/__init__.py | 0 .../ims/tests/controllers/v1/orm/__init__.py | 0 .../controllers/v1/orm/images/__init__.py | 0 .../v1/orm/images/test_customers.py | 186 + .../controllers/v1/orm/images/test_enabled.py | 110 + .../controllers/v1/orm/images/test_images.py | 332 + .../v1/orm/images/test_metadata.py | 77 + .../controllers/v1/orm/images/test_regions.py | 188 + .../ims/tests/controllers/v1/orm/test_logs.py | 42 + .../image_manager/ims/tests/logic/__init__.py | 22 + .../ims/tests/logic/test_image_logic.py | 704 ++ .../ims/tests/logic/test_meta_data.py | 54 + .../ims/tests/persistency/__init__.py | 0 .../tests/persistency/sql_alchemy/__init__.py | 0 .../sql_alchemy/images/__init__.py | 0 .../sql_alchemy/images/test_image_record.py | 166 + .../ims/tests/proxies/__init__.py | 0 .../ims/tests/proxies/rds_proxy.py | 86 + .../ims/tests/simple_hook_mock.py | 6 + .../image_manager/ims/tests/test_models.py | 41 + .../image_manager/ims/utils/__init__.py | 0 .../image_manager/ims/utils/authentication.py | 61 + orm/services/image_manager/ims/utils/utils.py | 16 + orm/services/image_manager/pycharm_init.py | 3 + .../scripts/db_scripts/create_db.sql | 86 + .../scripts/db_scripts/update_db.sql | 29 + .../scripts/shell_scripts/create_db.sh | 7 + .../scripts/shell_scripts/update_db.sh | 7 + .../image_manager/swagger/swagger.yaml | 751 ++ orm/services/image_manager/tox.ini | 29 + orm/services/region_manager/MANIFEST.in | 1 + orm/services/region_manager/__init__.py | 0 orm/services/region_manager/config.py | 119 + .../region_manager/cover/coverage_html.js | 584 + orm/services/region_manager/cover/index.html | 455 + .../cover/jquery.ba-throttle-debounce.min.js | 9 + .../region_manager/cover/jquery.hotkeys.js | 99 + .../region_manager/cover/jquery.isonscreen.js | 53 + .../region_manager/cover/jquery.min.js | 9404 +++++++++++++++++ .../cover/jquery.tablesorter.min.js | 1 + .../region_manager/cover/keybd_closed.png | Bin 0 -> 112 bytes .../region_manager/cover/keybd_open.png | Bin 0 -> 112 bytes .../region_manager/cover/rms___init___py.html | 89 + .../cover/rms_controllers___init___py.html | 89 + .../rms_controllers_configuration_py.html | 157 + .../rms_controllers_lcp_controller_py.html | 329 + .../cover/rms_controllers_logs_py.html | 237 + .../cover/rms_controllers_root_py.html | 159 + .../cover/rms_controllers_v2___init___py.html | 91 + .../rms_controllers_v2_orm___init___py.html | 91 + ...trollers_v2_orm_resources___init___py.html | 91 + ...ontrollers_v2_orm_resources_groups_py.html | 597 ++ ...trollers_v2_orm_resources_metadata_py.html | 437 + ...ntrollers_v2_orm_resources_regions_py.html | 819 ++ ...ontrollers_v2_orm_resources_status_py.html | 299 + .../cover/rms_controllers_v2_orm_root_py.html | 109 + .../cover/rms_controllers_v2_root_py.html | 105 + .../cover/rms_external_mock___init___py.html | 89 + ...xternal_mock_audit_client___init___py.html | 89 + ...nal_mock_audit_client_api___init___py.html | 89 + ...ternal_mock_audit_client_api_audit_py.html | 101 + ...ernal_mock_keystone_utils___init___py.html | 89 + ...xternal_mock_keystone_utils_tokens_py.html | 103 + ..._external_mock_orm_common___init___py.html | 89 + ...al_mock_orm_common_policy___init___py.html | 89 + ...rnal_mock_orm_common_policy_policy_py.html | 109 + ...nal_mock_orm_common_utils___init___py.html | 89 + ...k_orm_common_utils_api_error_utils_py.html | 93 + ...ternal_mock_orm_common_utils_utils_py.html | 119 + .../cover/rms_model___init___py.html | 119 + .../cover/rms_model_model_py.html | 475 + .../cover/rms_model_url_parm_py.html | 307 + .../cover/rms_services___init___py.html | 91 + .../cover/rms_services_error_base_py.html | 155 + .../cover/rms_services_services_py.html | 661 ++ .../cover/rms_storage___init___py.html | 89 + .../rms_storage_base_data_manager_py.html | 331 + .../rms_storage_data_manager_factory_py.html | 129 + .../cover/rms_storage_my_sql___init___py.html | 89 + .../rms_storage_my_sql_data_manager_py.html | 1147 ++ .../cover/rms_utils___init___py.html | 89 + .../cover/rms_utils_authentication_py.html | 195 + orm/services/region_manager/cover/status.json | 1 + orm/services/region_manager/cover/style.css | 375 + orm/services/region_manager/csv2db.py | 57 + .../region_manager/htmlcov/coverage_html.js | 584 + .../region_manager/htmlcov/index.html | 455 + .../jquery.ba-throttle-debounce.min.js | 9 + .../region_manager/htmlcov/jquery.hotkeys.js | 99 + .../htmlcov/jquery.isonscreen.js | 53 + .../region_manager/htmlcov/jquery.min.js | 9404 +++++++++++++++++ .../htmlcov/jquery.tablesorter.min.js | 1 + .../region_manager/htmlcov/keybd_closed.png | Bin 0 -> 112 bytes .../region_manager/htmlcov/keybd_open.png | Bin 0 -> 112 bytes .../htmlcov/rms___init___py.html | 89 + .../htmlcov/rms_controllers___init___py.html | 89 + .../rms_controllers_configuration_py.html | 157 + .../rms_controllers_lcp_controller_py.html | 329 + .../htmlcov/rms_controllers_logs_py.html | 237 + .../htmlcov/rms_controllers_root_py.html | 159 + .../rms_controllers_v2___init___py.html | 91 + .../rms_controllers_v2_orm___init___py.html | 91 + ...trollers_v2_orm_resources___init___py.html | 91 + ...ontrollers_v2_orm_resources_groups_py.html | 597 ++ ...trollers_v2_orm_resources_metadata_py.html | 437 + ...ntrollers_v2_orm_resources_regions_py.html | 819 ++ ...ontrollers_v2_orm_resources_status_py.html | 299 + .../rms_controllers_v2_orm_root_py.html | 109 + .../htmlcov/rms_controllers_v2_root_py.html | 105 + .../rms_external_mock___init___py.html | 89 + ...xternal_mock_audit_client___init___py.html | 89 + ...nal_mock_audit_client_api___init___py.html | 89 + ...ternal_mock_audit_client_api_audit_py.html | 101 + ...ernal_mock_keystone_utils___init___py.html | 89 + ...xternal_mock_keystone_utils_tokens_py.html | 103 + ..._external_mock_orm_common___init___py.html | 89 + ...al_mock_orm_common_policy___init___py.html | 89 + ...rnal_mock_orm_common_policy_policy_py.html | 109 + ...nal_mock_orm_common_utils___init___py.html | 89 + ...k_orm_common_utils_api_error_utils_py.html | 93 + ...ternal_mock_orm_common_utils_utils_py.html | 119 + .../htmlcov/rms_model___init___py.html | 119 + .../htmlcov/rms_model_model_py.html | 475 + .../htmlcov/rms_model_url_parm_py.html | 307 + .../htmlcov/rms_services___init___py.html | 91 + .../htmlcov/rms_services_error_base_py.html | 155 + .../htmlcov/rms_services_services_py.html | 661 ++ .../htmlcov/rms_storage___init___py.html | 89 + .../rms_storage_base_data_manager_py.html | 331 + .../rms_storage_data_manager_factory_py.html | 129 + .../rms_storage_my_sql___init___py.html | 89 + .../rms_storage_my_sql_data_manager_py.html | 1147 ++ .../htmlcov/rms_utils___init___py.html | 89 + .../htmlcov/rms_utils_authentication_py.html | 195 + .../region_manager/htmlcov/status.json | 1 + orm/services/region_manager/htmlcov/style.css | 375 + .../region_manager/public/css/style.css | 43 + .../region_manager/public/images/logo.png | Bin 0 -> 20596 bytes orm/services/region_manager/readme.md | 35 + orm/services/region_manager/revert_csv2db.py | 32 + orm/services/region_manager/rms.conf | 26 + orm/services/region_manager/rms.wsgi | 2 + orm/services/region_manager/rms/__init__.py | 0 orm/services/region_manager/rms/app.py | 40 + .../rms/controllers/__init__.py | 0 .../rms/controllers/configuration.py | 34 + .../rms/controllers/lcp_controller.py | 120 + .../region_manager/rms/controllers/logs.py | 74 + .../region_manager/rms/controllers/root.py | 35 + .../rms/controllers/v2/__init__.py | 1 + .../rms/controllers/v2/orm/__init__.py | 1 + .../controllers/v2/orm/resources/__init__.py | 1 + .../controllers/v2/orm/resources/groups.py | 254 + .../controllers/v2/orm/resources/metadata.py | 174 + .../controllers/v2/orm/resources/regions.py | 344 + .../controllers/v2/orm/resources/status.py | 105 + .../rms/controllers/v2/orm/root.py | 10 + .../region_manager/rms/controllers/v2/root.py | 8 + .../region_manager/rms/etc/policy.json | 24 + .../rms/external_mock/__init__.py | 0 .../external_mock/audit_client/__init__.py | 0 .../audit_client/api/__init__.py | 0 .../external_mock/audit_client/api/audit.py | 6 + .../external_mock/keystone_utils/__init__.py | 0 .../external_mock/keystone_utils/tokens.py | 7 + .../rms/external_mock/orm_common/__init__.py | 0 .../orm_common/policy/__init__.py | 0 .../external_mock/orm_common/policy/policy.py | 10 + .../orm_common/utils/__init__.py | 0 .../orm_common/utils/api_error_utils.py | 2 + .../external_mock/orm_common/utils/utils.py | 15 + .../region_manager/rms/logger/__init__.py | 10 + .../region_manager/rms/model/__init__.py | 15 + .../region_manager/rms/model/model.py | 183 + .../region_manager/rms/model/url_parm.py | 102 + .../region_manager/rms/resources/regions.csv | 4 + .../region_manager/rms/services/__init__.py | 1 + .../region_manager/rms/services/error_base.py | 33 + .../region_manager/rms/services/services.py | 286 + .../region_manager/rms/storage/__init__.py | 0 .../rms/storage/base_data_manager.py | 118 + .../rms/storage/data_manager_factory.py | 20 + .../rms/storage/my_sql/__init__.py | 0 .../rms/storage/my_sql/data_manager.py | 517 + .../rms/storage/my_sql/data_models.py | 141 + .../region_manager/rms/tests/__init__.py | 22 + .../region_manager/rms/tests/config.py | 46 + .../rms/tests/controllers/__init__.py | 0 .../rms/tests/controllers/v1/__init__.py | 0 .../rms/tests/controllers/v1/orm/__init__.py | 0 .../controllers/v1/orm/resources/__init__.py | 0 .../v1/orm/resources/test_groups.py | 213 + .../v1/orm/resources/test_metadata.py | 336 + .../v1/orm/resources/test_region.py | 414 + .../v1/orm/resources/test_status.py | 81 + .../region_manager/rms/tests/db_testing.py | 90 + .../rms/tests/model/__init__.py | 0 .../rms/tests/model/test_url_parms.py | 66 + .../rms/tests/services/__init__.py | 0 .../rms/tests/services/test_services.py | 327 + .../rms/tests/storage/__init__.py | 0 .../rms/tests/storage/my_sql/__init__.py | 0 .../tests/storage/my_sql/test_data_manager.py | 293 + .../tests/storage/test_base_data_manager.py | 44 + .../storage/test_data_manager_factory.py | 17 + .../rms/tests/test_configuration.py | 15 + .../region_manager/rms/tests/test_logs.py | 47 + .../rms/tests/tests_lcp_controller.py | 195 + .../rms/tests/utils/__init__.py | 0 .../rms/tests/utils/test_authentication.py | 80 + .../region_manager/rms/utils/__init__.py | 0 .../rms/utils/authentication.py | 53 + .../region_manager/rms_mock/MANIFEST.in | 1 + .../region_manager/rms_mock/__init__.py | 0 .../region_manager/rms_mock/config.py | 56 + .../region_manager/rms_mock/data/zones.json | 4002 +++++++ .../rms_mock/public/css/style.css | 43 + .../rms_mock/public/images/logo.png | Bin 0 -> 20596 bytes .../rms_mock/rms_mock/__init__.py | 0 .../region_manager/rms_mock/rms_mock/app.py | 26 + .../rms_mock/rms_mock/controllers/__init__.py | 0 .../rms_mock/controllers/lcp_controller.py | 26 + .../rms_mock/rms_mock/controllers/root.py | 19 + .../rms_mock/rms_mock/model/__init__.py | 15 + .../rms_mock/rms_mock/templates/error.html | 12 + .../rms_mock/rms_mock/templates/index.html | 34 + .../rms_mock/rms_mock/templates/layout.html | 22 + .../region_manager/rms_mock/setup.cfg | 6 + orm/services/region_manager/rms_mock/setup.py | 22 + .../scripts/db_scripts/create_db.sql | 63 + .../scripts/db_scripts/insert_test_values.sql | 59 + .../scripts/db_scripts/update_db.sql | 80 + .../scripts/shell_scripts/create_db.sh | 15 + .../scripts/shell_scripts/csv_2_db_loader.sh | 15 + .../scripts/shell_scripts/update_db.sh | 5 + .../region_manager/swagger/swagger.yaml | 713 ++ orm/services/region_manager/tox.ini | 27 + orm/services/resource_distributor/_-init__.py | 0 orm/services/resource_distributor/config.py | 176 + .../doc/RDS_Status_Service_Design.docx | Bin 0 -> 466037 bytes .../resource_distributor/doc/source/conf.py | 75 + .../doc/source/contributing.rst | 4 + .../resource_distributor/doc/source/index.rst | 25 + .../doc/source/installation.rst | 12 + .../doc/source/readme.rst | 1 + .../resource_distributor/doc/source/usage.rst | 7 + .../ordmockserver/MANIFEST.in | 1 + .../ordmockserver/config.py | 114 + .../ordmockserver/ordmockserver/__init__.py | 0 .../ordmockserver/ordmockserver/app.py | 14 + .../controllers/OrdNotifier/__init__.py | 0 .../OrdNotifier/models/__init__.py | 0 .../controllers/OrdNotifier/root.py | 90 + .../ordmockserver/controllers/__init__.py | 0 .../ordmockserver/controllers/root.py | 48 + .../ordmockserver/model/__init__.py | 15 + .../ordmockserver/templates/error.html | 12 + .../ordmockserver/templates/index.html | 34 + .../ordmockserver/templates/layout.html | 22 + .../ordmockserver/public/css/style.css | 43 + .../ordmockserver/public/images/logo.png | Bin 0 -> 20596 bytes .../ordmockserver/setup.cfg | 6 + .../ordmockserver/setup.py | 22 + orm/services/resource_distributor/rds.conf | 26 + orm/services/resource_distributor/rds.wsgi | 2 + .../resource_distributor/rds/__init__.py | 0 orm/services/resource_distributor/rds/app.py | 75 + .../rds/controllers/__init__.py | 1 + .../rds/controllers/root.py | 8 + .../rds/controllers/v1/__init__.py | 1 + .../rds/controllers/v1/base.py | 100 + .../controllers/v1/configuration/__init__.py | 1 + .../rds/controllers/v1/configuration/root.py | 28 + .../rds/controllers/v1/logs.py | 65 + .../rds/controllers/v1/resources/__init__.py | 1 + .../rds/controllers/v1/resources/root.py | 283 + .../rds/controllers/v1/root.py | 21 + .../rds/controllers/v1/status/__init__.py | 1 + .../rds/controllers/v1/status/get_resource.py | 111 + .../controllers/v1/status/resource_status.py | 155 + .../rds/ordupdate/__init__.py | 0 .../rds/ordupdate/ord_notifier.py | 287 + .../rds/proxies/__init__.py | 0 .../rds/proxies/ims_proxy.py | 61 + .../rds/proxies/rms_proxy.py | 31 + .../rds/resources/ord.crt | 22 + .../rds/services/__init__.py | 0 .../resource_distributor/rds/services/base.py | 22 + .../rds/services/etc/audit.conf | 4 + .../rds/services/model/__init__.py | 0 .../model/region_resource_id_status.py | 69 + .../rds/services/model/resource_input.py | 13 + .../rds/services/region_resource_id_status.py | 96 + .../rds/services/resource.py | 183 + .../rds/services/yaml_customer_builder.py | 171 + .../rds/services/yaml_flavor_bulder.py | 78 + .../rds/services/yaml_image_builder.py | 56 + .../resource_distributor/rds/sot/__init__.py | 0 .../resource_distributor/rds/sot/base_sot.py | 18 + .../rds/sot/git_sot/__init__.py | 0 .../rds/sot/git_sot/git_base.py | 35 + .../rds/sot/git_sot/git_factory.py | 15 + .../rds/sot/git_sot/git_gittle.py | 63 + .../rds/sot/git_sot/git_native.py | 271 + .../rds/sot/git_sot/git_sot.py | 233 + .../rds/sot/sot_factory.py | 29 + .../resource_distributor/rds/sot/sot_utils.py | 43 + .../rds/storage/__init__.py | 0 .../rds/storage/factory.py | 10 + .../rds/storage/mysql/__init__.py | 0 .../mysql/region_resource_id_status.py | 210 + .../rds/storage/region_resource_id_status.py | 24 + .../rds/tests/__init__.py | 0 .../resource_distributor/rds/tests/base.py | 23 + .../resource_distributor/rds/tests/config.py | 170 + .../rds/tests/controllers/__init__.py | 0 .../rds/tests/controllers/v1/__init__.py | 0 .../controllers/v1/configuration/__init__.py | 0 .../configuration/test_get_configuration.py | 21 + .../tests/controllers/v1/functional_test.py | 5 + .../controllers/v1/resources/__init__.py | 0 .../v1/resources/test_create_resource.py | 242 + .../tests/controllers/v1/status/__init__.py | 0 .../tests/controllers/v1/status/test_base.py | 13 + .../v1/status/test_get_resource_status.py | 45 + .../v1/status/test_resource_status.py | 64 + .../rds/tests/controllers/v1/test_logs.py | 26 + .../rds/tests/functional_test.py | 140 + .../rds/tests/ordupdate/__init__.py | 0 .../rds/tests/ordupdate/test_ord_notifier.py | 234 + .../rds/tests/services/__init__.py | 0 .../rds/tests/services/model/__init__.py | 0 .../model/test_region_resource_id_status.py | 44 + .../tests/services/test_create_resource.py | 671 ++ .../rds/tests/services/test_customer_yaml.py | 293 + .../rds/tests/services/test_flavor_yaml.py | 87 + .../rds/tests/services/test_image_yaml.py | 52 + .../test_region_resource_id_status.py | 170 + .../rds/tests/sot/__init__.py | 0 .../rds/tests/sot/git_sot/__init__.py | 0 .../rds/tests/sot/git_sot/test_git_base.py | 62 + .../rds/tests/sot/git_sot/test_git_factory.py | 25 + .../rds/tests/sot/git_sot/test_git_gittle.py | 56 + .../rds/tests/sot/git_sot/test_git_native.py | 92 + .../rds/tests/sot/git_sot/test_git_sot.py | 294 + .../rds/tests/sot/test_base_sot.py | 20 + .../rds/tests/sot/test_sot_factory.py | 54 + .../rds/tests/storage/__init__.py | 0 .../rds/tests/storage/mysql/__init__.py | 0 .../mysql/test_region_resource_id_status.py | 215 + .../storage/test_region_resource_id_status.py | 21 + .../rds/tests/utils/__init__.py | 0 .../rds/tests/utils/test_uuid_utils.py | 30 + .../rds/utils/__init__.py | 0 .../rds/utils/authentication.py | 111 + .../rds/utils/module_mocks/README.txt | 3 + .../rds/utils/module_mocks/__init__.py | 0 .../module_mocks/audit_client/__init__.py | 0 .../module_mocks/audit_client/api/__init__.py | 0 .../module_mocks/audit_client/api/audit.py | 6 + .../module_mocks/keystone_utils/__init__.py | 0 .../module_mocks/keystone_utils/tokens.py | 6 + .../utils/module_mocks/orm_common/__init__.py | 0 .../module_mocks/orm_common/utils/__init__.py | 0 .../module_mocks/orm_common/utils/utils.py | 14 + .../resource_distributor/rds/utils/utils.py | 73 + .../rds/utils/uuid_utils.py | 8 + .../rds_docs/nodejs_rds_docs/README.md | 24 + .../rds_docs/nodejs_rds_docs/api/swagger.yaml | 296 + .../nodejs_rds_docs/controllers/Resources.js | 15 + .../controllers/ResourcesService.js | 53 + .../nodejs_rds_docs/controllers/Status.js | 15 + .../controllers/StatusService.js | 46 + .../rds_docs/nodejs_rds_docs/index.js | 40 + .../rds_docs/nodejs_rds_docs/package.json | 16 + .../rds_docs/rds_swagger.yaml | 295 + .../scripts/db_scripts/create_db.sql | 37 + .../scripts/db_scripts/update_db.sql | 41 + .../scripts/shell_scripts/create_db.sh | 15 + .../swagger/nodejs_rds_docs/README.md | 24 + .../swagger/nodejs_rds_docs/api/swagger.yaml | 296 + .../nodejs_rds_docs/controllers/Resources.js | 15 + .../controllers/ResourcesService.js | 53 + .../nodejs_rds_docs/controllers/Status.js | 15 + .../controllers/StatusService.js | 46 + .../swagger/nodejs_rds_docs/index.js | 40 + .../swagger/nodejs_rds_docs/package.json | 16 + .../resource_distributor/swagger/swagger.yaml | 295 + orm/services/resource_distributor/tox.ini | 21 + orm/swagger/__init__.py | 0 orm/templates/error.html | 12 + orm/templates/index.html | 34 + orm/templates/layout.html | 22 + orm/tests/__init__.py | 22 + orm/tests/config.py | 137 + orm/tests/functional/__init__.py | 0 orm/tests/test_functional.py | 22 + orm/tests/test_units.py | 7 + orm/tests/unit/__init__.py | 0 public/css/style.css | 43 + public/images/logo.png | Bin 0 -> 20596 bytes requirements.txt | 15 + setup.cfg | 77 + setup.py | 23 + test-requirements.txt | 16 + tox.ini | 39 + 871 files changed, 105408 insertions(+) create mode 100644 .gitignore create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 babel.cfg create mode 100644 config.py create mode 100644 doc/source/admin/index.rst create mode 100644 doc/source/cli/index.rst create mode 100755 doc/source/conf.py create mode 100644 doc/source/configuration/index.rst create mode 100644 doc/source/contributor/contributing.rst create mode 100644 doc/source/contributor/index.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/install/common_configure.rst create mode 100644 doc/source/install/common_prerequisites.rst create mode 100644 doc/source/install/get_started.rst create mode 100644 doc/source/install/index.rst create mode 100644 doc/source/install/install-obs.rst create mode 100644 doc/source/install/install-rdo.rst create mode 100644 doc/source/install/install-ubuntu.rst create mode 100644 doc/source/install/install.rst create mode 100644 doc/source/install/next-steps.rst create mode 100644 doc/source/install/verify.rst create mode 100644 doc/source/library/index.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/reference/index.rst create mode 100644 doc/source/user/index.rst create mode 100644 etc/orm.conf create mode 100644 orm/__init__.py create mode 100644 orm/app.py create mode 100644 orm/cmd/__init__.py create mode 100644 orm/cmd/audit.py create mode 100644 orm/cmd/cms.py create mode 100644 orm/cmd/fms.py create mode 100644 orm/cmd/ims.py create mode 100644 orm/cmd/keystone.py create mode 100644 orm/cmd/rds.py create mode 100644 orm/cmd/rms.py create mode 100644 orm/cmd/uuidgen.py create mode 100644 orm/common/__init__.py create mode 100644 orm/common/client/__init__.py create mode 100644 orm/common/client/audit/__init__.py create mode 100644 orm/common/client/keystone/CONTRIBUTING.rst create mode 100644 orm/common/client/keystone/HACKING.rst create mode 100644 orm/common/client/keystone/LICENSE create mode 100644 orm/common/client/keystone/MANIFEST.in create mode 100644 orm/common/client/keystone/README.rst create mode 100644 orm/common/client/keystone/__init__.py create mode 100644 orm/common/client/keystone/babel.cfg create mode 100644 orm/common/client/keystone/debian/aic-orm-keystone.install create mode 100755 orm/common/client/keystone/debian/changelog create mode 100755 orm/common/client/keystone/debian/compat create mode 100755 orm/common/client/keystone/debian/control create mode 100755 orm/common/client/keystone/debian/copyright create mode 100755 orm/common/client/keystone/debian/docs create mode 100755 orm/common/client/keystone/debian/postrm create mode 100755 orm/common/client/keystone/debian/rules create mode 100755 orm/common/client/keystone/debian/source/format create mode 100644 orm/common/client/keystone/doc/source/contributing.rst create mode 100644 orm/common/client/keystone/doc/source/index.rst create mode 100644 orm/common/client/keystone/doc/source/installation.rst create mode 100644 orm/common/client/keystone/doc/source/readme.rst create mode 100644 orm/common/client/keystone/doc/source/usage.rst create mode 100644 orm/common/client/keystone/keystone_utils.egg-info/PKG-INFO create mode 100644 orm/common/client/keystone/keystone_utils.egg-info/SOURCES.txt create mode 100644 orm/common/client/keystone/keystone_utils.egg-info/dependency_links.txt create mode 100644 orm/common/client/keystone/keystone_utils.egg-info/not-zip-safe create mode 100644 orm/common/client/keystone/keystone_utils.egg-info/pbr.json create mode 100644 orm/common/client/keystone/keystone_utils.egg-info/top_level.txt create mode 100644 orm/common/client/keystone/keystone_utils/__init__.py create mode 100644 orm/common/client/keystone/keystone_utils/tests/__init__.py create mode 100644 orm/common/client/keystone/keystone_utils/tests/unit/__init__.py create mode 100755 orm/common/client/keystone/keystone_utils/tests/unit/test_tokens.py create mode 100755 orm/common/client/keystone/keystone_utils/tokens.py create mode 100644 orm/common/client/keystone/mock_keystone/__init__.py create mode 100644 orm/common/client/keystone/mock_keystone/keystoneclient/__init__.py create mode 100644 orm/common/client/keystone/mock_keystone/keystoneclient/exceptions.py create mode 100644 orm/common/client/keystone/mock_keystone/keystoneclient/v2_0/__init__.py create mode 100644 orm/common/client/keystone/mock_keystone/keystoneclient/v2_0/client.py create mode 100644 orm/common/client/keystone/mock_keystone/keystoneclient/v3/__init__.py create mode 100644 orm/common/client/keystone/mock_keystone/keystoneclient/v3/client.py create mode 100644 orm/common/client/keystone/mock_keystone/orm_common/__init__.py create mode 100644 orm/common/client/keystone/mock_keystone/orm_common/utils/__init__.py create mode 100644 orm/common/client/keystone/mock_keystone/orm_common/utils/dictator.py create mode 100644 orm/common/client/keystone/requirements.txt create mode 100644 orm/common/client/keystone/setup.cfg create mode 100644 orm/common/client/keystone/setup.py create mode 100644 orm/common/client/keystone/test-requirements.txt create mode 100644 orm/common/client/keystone/tox.ini create mode 100644 orm/common/orm_common/__init__.py create mode 100644 orm/common/orm_common/extenal_mock/audit_client/__init__.py create mode 100644 orm/common/orm_common/extenal_mock/audit_client/api/__init__.py create mode 100644 orm/common/orm_common/extenal_mock/audit_client/api/audit.py create mode 100644 orm/common/orm_common/extenal_mock/keystone_utils/__init__.py create mode 100644 orm/common/orm_common/extenal_mock/keystone_utils/tokens.py create mode 100644 orm/common/orm_common/hooks/__init__.py create mode 100755 orm/common/orm_common/hooks/api_error_hook.py create mode 100755 orm/common/orm_common/hooks/security_headers_hook.py create mode 100755 orm/common/orm_common/hooks/transaction_id_hook.py create mode 100644 orm/common/orm_common/injector/__init__.py create mode 100755 orm/common/orm_common/injector/fang/__init__.py create mode 100755 orm/common/orm_common/injector/fang/dependency_register.py create mode 100755 orm/common/orm_common/injector/fang/di.py create mode 100755 orm/common/orm_common/injector/fang/errors.py create mode 100755 orm/common/orm_common/injector/fang/resolver.py create mode 100755 orm/common/orm_common/injector/fang/resource_provider_register.py create mode 100755 orm/common/orm_common/injector/injector.py create mode 100644 orm/common/orm_common/policy/__init__.py create mode 100755 orm/common/orm_common/policy/_checks.py create mode 100755 orm/common/orm_common/policy/_parser.py create mode 100755 orm/common/orm_common/policy/policy.py create mode 100755 orm/common/orm_common/policy/qolicy.py create mode 100644 orm/common/orm_common/tests/__init__.py create mode 100644 orm/common/orm_common/tests/hooks/__init__.py create mode 100755 orm/common/orm_common/tests/hooks/test_api_error_hook.py create mode 100755 orm/common/orm_common/tests/hooks/test_security_headers_hook.py create mode 100755 orm/common/orm_common/tests/hooks/test_transaction_id_hook.py create mode 100644 orm/common/orm_common/tests/injector/__init__.py create mode 100755 orm/common/orm_common/tests/injector/test_injector.py create mode 100644 orm/common/orm_common/tests/policy/__init__.py create mode 100755 orm/common/orm_common/tests/policy/test_checks.py create mode 100755 orm/common/orm_common/tests/policy/test_policy.py create mode 100644 orm/common/orm_common/tests/utils/__init__.py create mode 100755 orm/common/orm_common/tests/utils/test_api_error_utils.py create mode 100755 orm/common/orm_common/tests/utils/test_cross_api_utils.py create mode 100755 orm/common/orm_common/tests/utils/test_utils.py create mode 100644 orm/common/orm_common/utils/__init__.py create mode 100755 orm/common/orm_common/utils/api_error_utils.py create mode 100755 orm/common/orm_common/utils/cross_api_utils.py create mode 100755 orm/common/orm_common/utils/dictator.py create mode 100644 orm/common/orm_common/utils/sanitize.py create mode 100755 orm/common/orm_common/utils/utils.py create mode 100755 orm/common/tox.ini create mode 100644 orm/orm_client/__init__.py create mode 100644 orm/services/__init__.py create mode 100644 orm/services/audit_trail_manager/__init__.py create mode 100755 orm/services/customer_manager/MANIFEST.in create mode 100644 orm/services/customer_manager/__init__.py create mode 100644 orm/services/customer_manager/cms_rest.conf create mode 100644 orm/services/customer_manager/cms_rest.wsgi create mode 100644 orm/services/customer_manager/cms_rest/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/app.py create mode 100644 orm/services/customer_manager/cms_rest/controllers/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/root.py create mode 100644 orm/services/customer_manager/cms_rest/controllers/v1/__init__.py create mode 100644 orm/services/customer_manager/cms_rest/controllers/v1/base.py create mode 100644 orm/services/customer_manager/cms_rest/controllers/v1/orm/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/v1/orm/configuration.py create mode 100644 orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/enabled.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/metadata.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/regions.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/root.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/users.py create mode 100644 orm/services/customer_manager/cms_rest/controllers/v1/orm/logs.py create mode 100755 orm/services/customer_manager/cms_rest/controllers/v1/orm/root.py create mode 100644 orm/services/customer_manager/cms_rest/controllers/v1/root.py create mode 100644 orm/services/customer_manager/cms_rest/data/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/data/data_manager.py create mode 100644 orm/services/customer_manager/cms_rest/data/sql_alchemy/__init__.py create mode 100644 orm/services/customer_manager/cms_rest/data/sql_alchemy/base.py create mode 100755 orm/services/customer_manager/cms_rest/data/sql_alchemy/cms_user_record.py create mode 100755 orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_record.py create mode 100755 orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_region_record.py create mode 100755 orm/services/customer_manager/cms_rest/data/sql_alchemy/models.py create mode 100755 orm/services/customer_manager/cms_rest/data/sql_alchemy/region_record.py create mode 100755 orm/services/customer_manager/cms_rest/data/sql_alchemy/user_role_record.py create mode 100755 orm/services/customer_manager/cms_rest/etc/policy.json create mode 100644 orm/services/customer_manager/cms_rest/extenal_mock/audit_client/__init__.py create mode 100644 orm/services/customer_manager/cms_rest/extenal_mock/audit_client/api/__init__.py create mode 100644 orm/services/customer_manager/cms_rest/extenal_mock/audit_client/api/audit.py create mode 100644 orm/services/customer_manager/cms_rest/extenal_mock/keystone_utils/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/keystone_utils/tokens.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/__init__.py create mode 100644 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/logger.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/policy/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/policy/policy.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/api_error_utils.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/cross_api_utils.py create mode 100755 orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/utils.py create mode 100644 orm/services/customer_manager/cms_rest/logger/__init__.py create mode 100644 orm/services/customer_manager/cms_rest/logic/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/logic/customer_logic.py create mode 100755 orm/services/customer_manager/cms_rest/logic/error_base.py create mode 100755 orm/services/customer_manager/cms_rest/logic/metadata_logic.py create mode 100644 orm/services/customer_manager/cms_rest/model/Model.py create mode 100755 orm/services/customer_manager/cms_rest/model/Models.py create mode 100644 orm/services/customer_manager/cms_rest/model/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/rds_proxy.py create mode 100644 orm/services/customer_manager/cms_rest/templates/error.html create mode 100644 orm/services/customer_manager/cms_rest/templates/index.html create mode 100644 orm/services/customer_manager/cms_rest/templates/layout.html create mode 100644 orm/services/customer_manager/cms_rest/tests/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/tests/config.py create mode 100644 orm/services/customer_manager/cms_rest/tests/logic/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/tests/logic/test_customer_logic.py create mode 100644 orm/services/customer_manager/cms_rest/tests/rest/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/tests/rest/test_customer.py create mode 100755 orm/services/customer_manager/cms_rest/tests/rest/test_enable.py create mode 100755 orm/services/customer_manager/cms_rest/tests/rest/test_metadata.py create mode 100755 orm/services/customer_manager/cms_rest/tests/rest/test_regions.py create mode 100755 orm/services/customer_manager/cms_rest/tests/rest/test_users.py create mode 100644 orm/services/customer_manager/cms_rest/tests/simple_hook_mock.py create mode 100644 orm/services/customer_manager/cms_rest/tests/test_authentication.py create mode 100755 orm/services/customer_manager/cms_rest/tests/test_configuration.py create mode 100755 orm/services/customer_manager/cms_rest/tests/test_models.py create mode 100755 orm/services/customer_manager/cms_rest/tests/test_rds_proxy.py create mode 100755 orm/services/customer_manager/cms_rest/tests/test_utils.py create mode 100644 orm/services/customer_manager/cms_rest/utils/__init__.py create mode 100755 orm/services/customer_manager/cms_rest/utils/authentication.py create mode 100755 orm/services/customer_manager/config.py create mode 100755 orm/services/customer_manager/gulpfile.js create mode 100644 orm/services/customer_manager/htmlcov/cms_rest___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_app_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_root_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_base_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_configuration_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_enabled_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_metadata_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_regions_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_root_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_users_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_logs_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_root_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_root_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_logger___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_logic___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_logic_customer_logic_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_logic_error_base_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_logic_metadata_logic_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_model_Model_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_model_Models_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_model___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_rds_proxy_py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_utils___init___py.html create mode 100644 orm/services/customer_manager/htmlcov/cms_rest_utils_authentication_py.html create mode 100644 orm/services/customer_manager/htmlcov/coverage_html.js create mode 100644 orm/services/customer_manager/htmlcov/index.html create mode 100644 orm/services/customer_manager/htmlcov/jquery.ba-throttle-debounce.min.js create mode 100644 orm/services/customer_manager/htmlcov/jquery.hotkeys.js create mode 100644 orm/services/customer_manager/htmlcov/jquery.isonscreen.js create mode 100644 orm/services/customer_manager/htmlcov/jquery.min.js create mode 100644 orm/services/customer_manager/htmlcov/jquery.tablesorter.min.js create mode 100644 orm/services/customer_manager/htmlcov/keybd_closed.png create mode 100644 orm/services/customer_manager/htmlcov/keybd_open.png create mode 100644 orm/services/customer_manager/htmlcov/status.json create mode 100644 orm/services/customer_manager/htmlcov/style.css create mode 100755 orm/services/customer_manager/package.json create mode 100644 orm/services/customer_manager/public/css/style.css create mode 100644 orm/services/customer_manager/public/images/logo.png create mode 100755 orm/services/customer_manager/pycharm_init.py create mode 100644 orm/services/customer_manager/rds_mock/app.js create mode 100644 orm/services/customer_manager/rds_mock/public/stylesheets/style.css create mode 100644 orm/services/customer_manager/rds_mock/routes/audit.js create mode 100644 orm/services/customer_manager/rds_mock/routes/index.js create mode 100644 orm/services/customer_manager/rds_mock/routes/rds.js create mode 100644 orm/services/customer_manager/rds_mock/routes/users.js create mode 100644 orm/services/customer_manager/rds_mock/routes/uuid.js create mode 100644 orm/services/customer_manager/rds_mock/views/error.jade create mode 100644 orm/services/customer_manager/rds_mock/views/index.jade create mode 100644 orm/services/customer_manager/rds_mock/views/layout.jade create mode 100644 orm/services/customer_manager/run_pecan.py create mode 100755 orm/services/customer_manager/scripts/db_scripts/aic_orm_cms_create_db.sql create mode 100644 orm/services/customer_manager/scripts/db_scripts/aic_orm_cms_update_db.sql create mode 100755 orm/services/customer_manager/scripts/shell_scripts/create_db.sh create mode 100644 orm/services/customer_manager/swagger/swagger.yaml create mode 100755 orm/services/customer_manager/tox.ini create mode 100755 orm/services/flavor_manager/MANIFEST.in create mode 100644 orm/services/flavor_manager/__init__.py create mode 100755 orm/services/flavor_manager/config.py create mode 100644 orm/services/flavor_manager/fms_mocks/__init__.py create mode 100644 orm/services/flavor_manager/fms_mocks/audit_mock.py create mode 100644 orm/services/flavor_manager/fms_mocks/requests_mock.py create mode 100644 orm/services/flavor_manager/fms_rest.conf create mode 100644 orm/services/flavor_manager/fms_rest.wsgi create mode 100644 orm/services/flavor_manager/fms_rest/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/app.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/root.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/v1/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/v1/orm/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/controllers/v1/orm/configuration.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/v1/orm/flavors/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/controllers/v1/orm/flavors/base.py create mode 100755 orm/services/flavor_manager/fms_rest/controllers/v1/orm/flavors/flavors.py create mode 100755 orm/services/flavor_manager/fms_rest/controllers/v1/orm/flavors/os_extra_specs.py create mode 100755 orm/services/flavor_manager/fms_rest/controllers/v1/orm/flavors/regions.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/v1/orm/flavors/tags.py create mode 100755 orm/services/flavor_manager/fms_rest/controllers/v1/orm/flavors/tenants.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/v1/orm/logs.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/v1/orm/orm.py create mode 100644 orm/services/flavor_manager/fms_rest/controllers/v1/v1.py create mode 100644 orm/services/flavor_manager/fms_rest/data/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/data/sql_alchemy/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/data_manager.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/db_models.py create mode 100644 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor/flavor_record.py create mode 100644 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_extra_spec/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_extra_spec/flavor_extra_spec_record.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_options/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_options/flavor_option_record.py create mode 100644 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_region/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_region/flavor_region_record.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_tags/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_tags/flavor_tag_record.py create mode 100644 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_tenant/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/data/sql_alchemy/flavor_tenant/flavor_tenant_record.py create mode 100644 orm/services/flavor_manager/fms_rest/data/wsme/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/data/wsme/model.py create mode 100755 orm/services/flavor_manager/fms_rest/data/wsme/models.py create mode 100644 orm/services/flavor_manager/fms_rest/di_providers/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/di_providers/mock_providers.py create mode 100644 orm/services/flavor_manager/fms_rest/di_providers/prod_providers.py create mode 100755 orm/services/flavor_manager/fms_rest/etc/policy.json create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/audit_client/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/audit_client/api/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/audit_client/api/audit.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/keystone_utils/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/keystone_utils/tokens.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/orm_common/hooks/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/orm_common/hooks/transaction_id_hook.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/fang/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/fang/dependency_register.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/fang/di.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/fang/errors.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/fang/resolver.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/fang/resource_provider_register.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/injector/injector.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/logger/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/orm_common/policy/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/orm_common/policy/policy.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/utils.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/utils/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/external_mock/orm_common/utils/api_error_utils.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/utils/cross_api_utils.py create mode 100755 orm/services/flavor_manager/fms_rest/external_mock/orm_common/utils/utils.py create mode 100644 orm/services/flavor_manager/fms_rest/hooks/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/hooks/service_hooks.py create mode 100644 orm/services/flavor_manager/fms_rest/logger/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/logic/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/logic/error_base.py create mode 100755 orm/services/flavor_manager/fms_rest/logic/flavor_logic.py create mode 100644 orm/services/flavor_manager/fms_rest/proxies/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/proxies/rds_proxy.py create mode 100644 orm/services/flavor_manager/fms_rest/templates/error.html create mode 100644 orm/services/flavor_manager/fms_rest/templates/index.html create mode 100644 orm/services/flavor_manager/fms_rest/templates/layout.html create mode 100644 orm/services/flavor_manager/fms_rest/tests/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/config.py create mode 100644 orm/services/flavor_manager/fms_rest/tests/data/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/data/test_wsme_models.py create mode 100644 orm/services/flavor_manager/fms_rest/tests/logic/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/logic/test_flavor_logic.py create mode 100644 orm/services/flavor_manager/fms_rest/tests/mocks/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/rest/__init__.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/rest/test_flavors.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/rest/test_logs.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/rest/test_os_extra_specs.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/rest/test_regions.py create mode 100644 orm/services/flavor_manager/fms_rest/tests/rest/test_tags.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/rest/test_tenants.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/simple_hook_mock.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/test_authentication.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/test_configuration.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/test_models.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/test_rds_proxy.py create mode 100755 orm/services/flavor_manager/fms_rest/tests/test_utils.py create mode 100644 orm/services/flavor_manager/fms_rest/utils/__init__.py create mode 100644 orm/services/flavor_manager/fms_rest/utils/authentication.py create mode 100755 orm/services/flavor_manager/fms_rest/utils/utils.py create mode 100755 orm/services/flavor_manager/pycharm_init.py create mode 100644 orm/services/flavor_manager/run_pecan.py create mode 100755 orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_create_db.sql create mode 100755 orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_update_db.sql create mode 100644 orm/services/flavor_manager/scripts/shell_scripts/create_db.sh create mode 100644 orm/services/flavor_manager/swagger/swagger.yaml create mode 100755 orm/services/flavor_manager/tox.ini create mode 100644 orm/services/id_generator/__init__.py create mode 100644 orm/services/image_manager/MANIFEST.in create mode 100644 orm/services/image_manager/__init__.py create mode 100755 orm/services/image_manager/config.py create mode 100755 orm/services/image_manager/data_manager_test.py create mode 100644 orm/services/image_manager/ims.conf create mode 100644 orm/services/image_manager/ims.wsgi create mode 100644 orm/services/image_manager/ims/__init__.py create mode 100755 orm/services/image_manager/ims/app.py create mode 100755 orm/services/image_manager/ims/controllers/__init__.py create mode 100755 orm/services/image_manager/ims/controllers/root.py create mode 100755 orm/services/image_manager/ims/controllers/v1/__init__.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/__init__.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/configuration.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/images/__init__.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/images/base.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/images/customers.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/images/enabled.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/images/images.py create mode 100644 orm/services/image_manager/ims/controllers/v1/orm/images/metadata.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/images/regions.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/logs.py create mode 100644 orm/services/image_manager/ims/controllers/v1/orm/orm.py create mode 100755 orm/services/image_manager/ims/controllers/v1/orm/root.py create mode 100755 orm/services/image_manager/ims/controllers/v1/root.py create mode 100644 orm/services/image_manager/ims/controllers/v1/v1.py create mode 100644 orm/services/image_manager/ims/di_providers/__init__.py create mode 100755 orm/services/image_manager/ims/di_providers/mock_providers.py create mode 100644 orm/services/image_manager/ims/di_providers/prod_providers.py create mode 100755 orm/services/image_manager/ims/etc/policy.json create mode 100644 orm/services/image_manager/ims/external_mock/audit_client/__init__.py create mode 100644 orm/services/image_manager/ims/external_mock/audit_client/api/__init__.py create mode 100644 orm/services/image_manager/ims/external_mock/audit_client/api/audit.py create mode 100644 orm/services/image_manager/ims/external_mock/keystone_utils/__init__.py create mode 100755 orm/services/image_manager/ims/external_mock/keystone_utils/tokens.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/__init__.py create mode 100644 orm/services/image_manager/ims/external_mock/orm_common/hooks/__init__.py create mode 100644 orm/services/image_manager/ims/external_mock/orm_common/hooks/transaction_id_hook.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/__init__.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/fang/__init__.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/fang/dependency_register.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/fang/di.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/fang/errors.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resolver.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resource_provider_register.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/injector/injector.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/logger/__init__.py create mode 100644 orm/services/image_manager/ims/external_mock/orm_common/policy/__init__.py create mode 100644 orm/services/image_manager/ims/external_mock/orm_common/policy/policy.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/utils.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/utils/__init__.py create mode 100644 orm/services/image_manager/ims/external_mock/orm_common/utils/api_error_utils.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/utils/cross_api_utils.py create mode 100755 orm/services/image_manager/ims/external_mock/orm_common/utils/utils.py create mode 100644 orm/services/image_manager/ims/hooks/__init__.py create mode 100755 orm/services/image_manager/ims/hooks/service_hooks.py create mode 100644 orm/services/image_manager/ims/ims_mocks/__init__.py create mode 100755 orm/services/image_manager/ims/ims_mocks/audit_mock.py create mode 100755 orm/services/image_manager/ims/ims_mocks/requests_mock.py create mode 100755 orm/services/image_manager/ims/logger/__init__.py create mode 100644 orm/services/image_manager/ims/logic/__init__.py create mode 100755 orm/services/image_manager/ims/logic/error_base.py create mode 100755 orm/services/image_manager/ims/logic/image_logic.py create mode 100644 orm/services/image_manager/ims/logic/metadata_logic.py create mode 100644 orm/services/image_manager/ims/persistency/__init__.py create mode 100644 orm/services/image_manager/ims/persistency/sql_alchemy/__init__.py create mode 100755 orm/services/image_manager/ims/persistency/sql_alchemy/data_manager.py create mode 100755 orm/services/image_manager/ims/persistency/sql_alchemy/db_models.py create mode 100644 orm/services/image_manager/ims/persistency/sql_alchemy/image/__init__.py create mode 100755 orm/services/image_manager/ims/persistency/sql_alchemy/image/image_record.py create mode 100644 orm/services/image_manager/ims/persistency/sql_alchemy/infra/__init__.py create mode 100755 orm/services/image_manager/ims/persistency/sql_alchemy/infra/record.py create mode 100644 orm/services/image_manager/ims/persistency/wsme/__init__.py create mode 100755 orm/services/image_manager/ims/persistency/wsme/base.py create mode 100755 orm/services/image_manager/ims/persistency/wsme/models.py create mode 100644 orm/services/image_manager/ims/proxies/__init__.py create mode 100755 orm/services/image_manager/ims/proxies/rds_proxy.py create mode 100755 orm/services/image_manager/ims/tests/__init__.py create mode 100755 orm/services/image_manager/ims/tests/config.py create mode 100644 orm/services/image_manager/ims/tests/controllers/__init__.py create mode 100644 orm/services/image_manager/ims/tests/controllers/v1/__init__.py create mode 100644 orm/services/image_manager/ims/tests/controllers/v1/orm/__init__.py create mode 100644 orm/services/image_manager/ims/tests/controllers/v1/orm/images/__init__.py create mode 100755 orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_customers.py create mode 100755 orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_enabled.py create mode 100755 orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_images.py create mode 100755 orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_metadata.py create mode 100755 orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_regions.py create mode 100755 orm/services/image_manager/ims/tests/controllers/v1/orm/test_logs.py create mode 100755 orm/services/image_manager/ims/tests/logic/__init__.py create mode 100755 orm/services/image_manager/ims/tests/logic/test_image_logic.py create mode 100755 orm/services/image_manager/ims/tests/logic/test_meta_data.py create mode 100755 orm/services/image_manager/ims/tests/persistency/__init__.py create mode 100755 orm/services/image_manager/ims/tests/persistency/sql_alchemy/__init__.py create mode 100755 orm/services/image_manager/ims/tests/persistency/sql_alchemy/images/__init__.py create mode 100755 orm/services/image_manager/ims/tests/persistency/sql_alchemy/images/test_image_record.py create mode 100755 orm/services/image_manager/ims/tests/proxies/__init__.py create mode 100755 orm/services/image_manager/ims/tests/proxies/rds_proxy.py create mode 100755 orm/services/image_manager/ims/tests/simple_hook_mock.py create mode 100644 orm/services/image_manager/ims/tests/test_models.py create mode 100644 orm/services/image_manager/ims/utils/__init__.py create mode 100755 orm/services/image_manager/ims/utils/authentication.py create mode 100755 orm/services/image_manager/ims/utils/utils.py create mode 100644 orm/services/image_manager/pycharm_init.py create mode 100755 orm/services/image_manager/scripts/db_scripts/create_db.sql create mode 100755 orm/services/image_manager/scripts/db_scripts/update_db.sql create mode 100644 orm/services/image_manager/scripts/shell_scripts/create_db.sh create mode 100755 orm/services/image_manager/scripts/shell_scripts/update_db.sh create mode 100755 orm/services/image_manager/swagger/swagger.yaml create mode 100755 orm/services/image_manager/tox.ini create mode 100755 orm/services/region_manager/MANIFEST.in create mode 100644 orm/services/region_manager/__init__.py create mode 100755 orm/services/region_manager/config.py create mode 100644 orm/services/region_manager/cover/coverage_html.js create mode 100644 orm/services/region_manager/cover/index.html create mode 100644 orm/services/region_manager/cover/jquery.ba-throttle-debounce.min.js create mode 100644 orm/services/region_manager/cover/jquery.hotkeys.js create mode 100644 orm/services/region_manager/cover/jquery.isonscreen.js create mode 100644 orm/services/region_manager/cover/jquery.min.js create mode 100644 orm/services/region_manager/cover/jquery.tablesorter.min.js create mode 100644 orm/services/region_manager/cover/keybd_closed.png create mode 100644 orm/services/region_manager/cover/keybd_open.png create mode 100644 orm/services/region_manager/cover/rms___init___py.html create mode 100644 orm/services/region_manager/cover/rms_controllers___init___py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_configuration_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_lcp_controller_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_logs_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_root_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2___init___py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_orm___init___py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_orm_resources___init___py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_orm_resources_groups_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_orm_resources_metadata_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_orm_resources_regions_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_orm_resources_status_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_orm_root_py.html create mode 100644 orm/services/region_manager/cover/rms_controllers_v2_root_py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock___init___py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_audit_client___init___py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_audit_client_api___init___py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_audit_client_api_audit_py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_keystone_utils___init___py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_keystone_utils_tokens_py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_orm_common___init___py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_orm_common_policy___init___py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_orm_common_policy_policy_py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_orm_common_utils___init___py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_orm_common_utils_api_error_utils_py.html create mode 100644 orm/services/region_manager/cover/rms_external_mock_orm_common_utils_utils_py.html create mode 100644 orm/services/region_manager/cover/rms_model___init___py.html create mode 100644 orm/services/region_manager/cover/rms_model_model_py.html create mode 100644 orm/services/region_manager/cover/rms_model_url_parm_py.html create mode 100644 orm/services/region_manager/cover/rms_services___init___py.html create mode 100644 orm/services/region_manager/cover/rms_services_error_base_py.html create mode 100644 orm/services/region_manager/cover/rms_services_services_py.html create mode 100644 orm/services/region_manager/cover/rms_storage___init___py.html create mode 100644 orm/services/region_manager/cover/rms_storage_base_data_manager_py.html create mode 100644 orm/services/region_manager/cover/rms_storage_data_manager_factory_py.html create mode 100644 orm/services/region_manager/cover/rms_storage_my_sql___init___py.html create mode 100644 orm/services/region_manager/cover/rms_storage_my_sql_data_manager_py.html create mode 100644 orm/services/region_manager/cover/rms_utils___init___py.html create mode 100644 orm/services/region_manager/cover/rms_utils_authentication_py.html create mode 100644 orm/services/region_manager/cover/status.json create mode 100644 orm/services/region_manager/cover/style.css create mode 100755 orm/services/region_manager/csv2db.py create mode 100644 orm/services/region_manager/htmlcov/coverage_html.js create mode 100644 orm/services/region_manager/htmlcov/index.html create mode 100644 orm/services/region_manager/htmlcov/jquery.ba-throttle-debounce.min.js create mode 100644 orm/services/region_manager/htmlcov/jquery.hotkeys.js create mode 100644 orm/services/region_manager/htmlcov/jquery.isonscreen.js create mode 100644 orm/services/region_manager/htmlcov/jquery.min.js create mode 100644 orm/services/region_manager/htmlcov/jquery.tablesorter.min.js create mode 100644 orm/services/region_manager/htmlcov/keybd_closed.png create mode 100644 orm/services/region_manager/htmlcov/keybd_open.png create mode 100644 orm/services/region_manager/htmlcov/rms___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_configuration_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_lcp_controller_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_logs_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_root_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_orm___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_groups_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_metadata_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_regions_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_status_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_orm_root_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_controllers_v2_root_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_audit_client___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api_audit_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils_tokens_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_orm_common___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy_policy_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_api_error_utils_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_utils_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_model___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_model_model_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_model_url_parm_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_services___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_services_error_base_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_services_services_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_storage___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_storage_base_data_manager_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_storage_data_manager_factory_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_storage_my_sql___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_storage_my_sql_data_manager_py.html create mode 100644 orm/services/region_manager/htmlcov/rms_utils___init___py.html create mode 100644 orm/services/region_manager/htmlcov/rms_utils_authentication_py.html create mode 100644 orm/services/region_manager/htmlcov/status.json create mode 100644 orm/services/region_manager/htmlcov/style.css create mode 100755 orm/services/region_manager/public/css/style.css create mode 100755 orm/services/region_manager/public/images/logo.png create mode 100644 orm/services/region_manager/readme.md create mode 100644 orm/services/region_manager/revert_csv2db.py create mode 100644 orm/services/region_manager/rms.conf create mode 100644 orm/services/region_manager/rms.wsgi create mode 100644 orm/services/region_manager/rms/__init__.py create mode 100755 orm/services/region_manager/rms/app.py create mode 100644 orm/services/region_manager/rms/controllers/__init__.py create mode 100755 orm/services/region_manager/rms/controllers/configuration.py create mode 100755 orm/services/region_manager/rms/controllers/lcp_controller.py create mode 100755 orm/services/region_manager/rms/controllers/logs.py create mode 100755 orm/services/region_manager/rms/controllers/root.py create mode 100644 orm/services/region_manager/rms/controllers/v2/__init__.py create mode 100644 orm/services/region_manager/rms/controllers/v2/orm/__init__.py create mode 100644 orm/services/region_manager/rms/controllers/v2/orm/resources/__init__.py create mode 100755 orm/services/region_manager/rms/controllers/v2/orm/resources/groups.py create mode 100755 orm/services/region_manager/rms/controllers/v2/orm/resources/metadata.py create mode 100755 orm/services/region_manager/rms/controllers/v2/orm/resources/regions.py create mode 100755 orm/services/region_manager/rms/controllers/v2/orm/resources/status.py create mode 100755 orm/services/region_manager/rms/controllers/v2/orm/root.py create mode 100755 orm/services/region_manager/rms/controllers/v2/root.py create mode 100755 orm/services/region_manager/rms/etc/policy.json create mode 100644 orm/services/region_manager/rms/external_mock/__init__.py create mode 100644 orm/services/region_manager/rms/external_mock/audit_client/__init__.py create mode 100644 orm/services/region_manager/rms/external_mock/audit_client/api/__init__.py create mode 100644 orm/services/region_manager/rms/external_mock/audit_client/api/audit.py create mode 100644 orm/services/region_manager/rms/external_mock/keystone_utils/__init__.py create mode 100644 orm/services/region_manager/rms/external_mock/keystone_utils/tokens.py create mode 100644 orm/services/region_manager/rms/external_mock/orm_common/__init__.py create mode 100644 orm/services/region_manager/rms/external_mock/orm_common/policy/__init__.py create mode 100644 orm/services/region_manager/rms/external_mock/orm_common/policy/policy.py create mode 100644 orm/services/region_manager/rms/external_mock/orm_common/utils/__init__.py create mode 100644 orm/services/region_manager/rms/external_mock/orm_common/utils/api_error_utils.py create mode 100755 orm/services/region_manager/rms/external_mock/orm_common/utils/utils.py create mode 100644 orm/services/region_manager/rms/logger/__init__.py create mode 100644 orm/services/region_manager/rms/model/__init__.py create mode 100755 orm/services/region_manager/rms/model/model.py create mode 100755 orm/services/region_manager/rms/model/url_parm.py create mode 100644 orm/services/region_manager/rms/resources/regions.csv create mode 100644 orm/services/region_manager/rms/services/__init__.py create mode 100755 orm/services/region_manager/rms/services/error_base.py create mode 100755 orm/services/region_manager/rms/services/services.py create mode 100644 orm/services/region_manager/rms/storage/__init__.py create mode 100755 orm/services/region_manager/rms/storage/base_data_manager.py create mode 100644 orm/services/region_manager/rms/storage/data_manager_factory.py create mode 100644 orm/services/region_manager/rms/storage/my_sql/__init__.py create mode 100755 orm/services/region_manager/rms/storage/my_sql/data_manager.py create mode 100755 orm/services/region_manager/rms/storage/my_sql/data_models.py create mode 100644 orm/services/region_manager/rms/tests/__init__.py create mode 100755 orm/services/region_manager/rms/tests/config.py create mode 100644 orm/services/region_manager/rms/tests/controllers/__init__.py create mode 100644 orm/services/region_manager/rms/tests/controllers/v1/__init__.py create mode 100644 orm/services/region_manager/rms/tests/controllers/v1/orm/__init__.py create mode 100644 orm/services/region_manager/rms/tests/controllers/v1/orm/resources/__init__.py create mode 100755 orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_groups.py create mode 100755 orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_metadata.py create mode 100755 orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_region.py create mode 100755 orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_status.py create mode 100644 orm/services/region_manager/rms/tests/db_testing.py create mode 100644 orm/services/region_manager/rms/tests/model/__init__.py create mode 100755 orm/services/region_manager/rms/tests/model/test_url_parms.py create mode 100644 orm/services/region_manager/rms/tests/services/__init__.py create mode 100755 orm/services/region_manager/rms/tests/services/test_services.py create mode 100644 orm/services/region_manager/rms/tests/storage/__init__.py create mode 100644 orm/services/region_manager/rms/tests/storage/my_sql/__init__.py create mode 100755 orm/services/region_manager/rms/tests/storage/my_sql/test_data_manager.py create mode 100644 orm/services/region_manager/rms/tests/storage/test_base_data_manager.py create mode 100644 orm/services/region_manager/rms/tests/storage/test_data_manager_factory.py create mode 100755 orm/services/region_manager/rms/tests/test_configuration.py create mode 100755 orm/services/region_manager/rms/tests/test_logs.py create mode 100755 orm/services/region_manager/rms/tests/tests_lcp_controller.py create mode 100644 orm/services/region_manager/rms/tests/utils/__init__.py create mode 100755 orm/services/region_manager/rms/tests/utils/test_authentication.py create mode 100644 orm/services/region_manager/rms/utils/__init__.py create mode 100755 orm/services/region_manager/rms/utils/authentication.py create mode 100644 orm/services/region_manager/rms_mock/MANIFEST.in create mode 100644 orm/services/region_manager/rms_mock/__init__.py create mode 100644 orm/services/region_manager/rms_mock/config.py create mode 100644 orm/services/region_manager/rms_mock/data/zones.json create mode 100644 orm/services/region_manager/rms_mock/public/css/style.css create mode 100644 orm/services/region_manager/rms_mock/public/images/logo.png create mode 100644 orm/services/region_manager/rms_mock/rms_mock/__init__.py create mode 100644 orm/services/region_manager/rms_mock/rms_mock/app.py create mode 100644 orm/services/region_manager/rms_mock/rms_mock/controllers/__init__.py create mode 100644 orm/services/region_manager/rms_mock/rms_mock/controllers/lcp_controller.py create mode 100644 orm/services/region_manager/rms_mock/rms_mock/controllers/root.py create mode 100644 orm/services/region_manager/rms_mock/rms_mock/model/__init__.py create mode 100644 orm/services/region_manager/rms_mock/rms_mock/templates/error.html create mode 100644 orm/services/region_manager/rms_mock/rms_mock/templates/index.html create mode 100644 orm/services/region_manager/rms_mock/rms_mock/templates/layout.html create mode 100644 orm/services/region_manager/rms_mock/setup.cfg create mode 100644 orm/services/region_manager/rms_mock/setup.py create mode 100644 orm/services/region_manager/scripts/db_scripts/create_db.sql create mode 100644 orm/services/region_manager/scripts/db_scripts/insert_test_values.sql create mode 100644 orm/services/region_manager/scripts/db_scripts/update_db.sql create mode 100644 orm/services/region_manager/scripts/shell_scripts/create_db.sh create mode 100644 orm/services/region_manager/scripts/shell_scripts/csv_2_db_loader.sh create mode 100644 orm/services/region_manager/scripts/shell_scripts/update_db.sh create mode 100644 orm/services/region_manager/swagger/swagger.yaml create mode 100755 orm/services/region_manager/tox.ini create mode 100644 orm/services/resource_distributor/_-init__.py create mode 100755 orm/services/resource_distributor/config.py create mode 100644 orm/services/resource_distributor/doc/RDS_Status_Service_Design.docx create mode 100644 orm/services/resource_distributor/doc/source/conf.py create mode 100644 orm/services/resource_distributor/doc/source/contributing.rst create mode 100644 orm/services/resource_distributor/doc/source/index.rst create mode 100644 orm/services/resource_distributor/doc/source/installation.rst create mode 100644 orm/services/resource_distributor/doc/source/readme.rst create mode 100644 orm/services/resource_distributor/doc/source/usage.rst create mode 100644 orm/services/resource_distributor/ordmockserver/MANIFEST.in create mode 100755 orm/services/resource_distributor/ordmockserver/config.py create mode 100644 orm/services/resource_distributor/ordmockserver/ordmockserver/__init__.py create mode 100644 orm/services/resource_distributor/ordmockserver/ordmockserver/app.py create mode 100755 orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/__init__.py create mode 100755 orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/models/__init__.py create mode 100755 orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/root.py create mode 100644 orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/__init__.py create mode 100755 orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/root.py create mode 100644 orm/services/resource_distributor/ordmockserver/ordmockserver/model/__init__.py create mode 100644 orm/services/resource_distributor/ordmockserver/ordmockserver/templates/error.html create mode 100644 orm/services/resource_distributor/ordmockserver/ordmockserver/templates/index.html create mode 100644 orm/services/resource_distributor/ordmockserver/ordmockserver/templates/layout.html create mode 100644 orm/services/resource_distributor/ordmockserver/public/css/style.css create mode 100644 orm/services/resource_distributor/ordmockserver/public/images/logo.png create mode 100644 orm/services/resource_distributor/ordmockserver/setup.cfg create mode 100644 orm/services/resource_distributor/ordmockserver/setup.py create mode 100644 orm/services/resource_distributor/rds.conf create mode 100644 orm/services/resource_distributor/rds.wsgi create mode 100644 orm/services/resource_distributor/rds/__init__.py create mode 100755 orm/services/resource_distributor/rds/app.py create mode 100644 orm/services/resource_distributor/rds/controllers/__init__.py create mode 100644 orm/services/resource_distributor/rds/controllers/root.py create mode 100644 orm/services/resource_distributor/rds/controllers/v1/__init__.py create mode 100644 orm/services/resource_distributor/rds/controllers/v1/base.py create mode 100644 orm/services/resource_distributor/rds/controllers/v1/configuration/__init__.py create mode 100644 orm/services/resource_distributor/rds/controllers/v1/configuration/root.py create mode 100644 orm/services/resource_distributor/rds/controllers/v1/logs.py create mode 100644 orm/services/resource_distributor/rds/controllers/v1/resources/__init__.py create mode 100755 orm/services/resource_distributor/rds/controllers/v1/resources/root.py create mode 100755 orm/services/resource_distributor/rds/controllers/v1/root.py create mode 100644 orm/services/resource_distributor/rds/controllers/v1/status/__init__.py create mode 100755 orm/services/resource_distributor/rds/controllers/v1/status/get_resource.py create mode 100755 orm/services/resource_distributor/rds/controllers/v1/status/resource_status.py create mode 100644 orm/services/resource_distributor/rds/ordupdate/__init__.py create mode 100755 orm/services/resource_distributor/rds/ordupdate/ord_notifier.py create mode 100755 orm/services/resource_distributor/rds/proxies/__init__.py create mode 100755 orm/services/resource_distributor/rds/proxies/ims_proxy.py create mode 100755 orm/services/resource_distributor/rds/proxies/rms_proxy.py create mode 100644 orm/services/resource_distributor/rds/resources/ord.crt create mode 100644 orm/services/resource_distributor/rds/services/__init__.py create mode 100644 orm/services/resource_distributor/rds/services/base.py create mode 100644 orm/services/resource_distributor/rds/services/etc/audit.conf create mode 100644 orm/services/resource_distributor/rds/services/model/__init__.py create mode 100755 orm/services/resource_distributor/rds/services/model/region_resource_id_status.py create mode 100644 orm/services/resource_distributor/rds/services/model/resource_input.py create mode 100755 orm/services/resource_distributor/rds/services/region_resource_id_status.py create mode 100755 orm/services/resource_distributor/rds/services/resource.py create mode 100755 orm/services/resource_distributor/rds/services/yaml_customer_builder.py create mode 100755 orm/services/resource_distributor/rds/services/yaml_flavor_bulder.py create mode 100755 orm/services/resource_distributor/rds/services/yaml_image_builder.py create mode 100644 orm/services/resource_distributor/rds/sot/__init__.py create mode 100644 orm/services/resource_distributor/rds/sot/base_sot.py create mode 100644 orm/services/resource_distributor/rds/sot/git_sot/__init__.py create mode 100644 orm/services/resource_distributor/rds/sot/git_sot/git_base.py create mode 100644 orm/services/resource_distributor/rds/sot/git_sot/git_factory.py create mode 100755 orm/services/resource_distributor/rds/sot/git_sot/git_gittle.py create mode 100644 orm/services/resource_distributor/rds/sot/git_sot/git_native.py create mode 100755 orm/services/resource_distributor/rds/sot/git_sot/git_sot.py create mode 100644 orm/services/resource_distributor/rds/sot/sot_factory.py create mode 100644 orm/services/resource_distributor/rds/sot/sot_utils.py create mode 100644 orm/services/resource_distributor/rds/storage/__init__.py create mode 100644 orm/services/resource_distributor/rds/storage/factory.py create mode 100644 orm/services/resource_distributor/rds/storage/mysql/__init__.py create mode 100755 orm/services/resource_distributor/rds/storage/mysql/region_resource_id_status.py create mode 100644 orm/services/resource_distributor/rds/storage/region_resource_id_status.py create mode 100644 orm/services/resource_distributor/rds/tests/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/base.py create mode 100755 orm/services/resource_distributor/rds/tests/config.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/v1/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/v1/configuration/__init__.py create mode 100755 orm/services/resource_distributor/rds/tests/controllers/v1/configuration/test_get_configuration.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/v1/functional_test.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/v1/resources/__init__.py create mode 100755 orm/services/resource_distributor/rds/tests/controllers/v1/resources/test_create_resource.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/v1/status/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/v1/status/test_base.py create mode 100755 orm/services/resource_distributor/rds/tests/controllers/v1/status/test_get_resource_status.py create mode 100644 orm/services/resource_distributor/rds/tests/controllers/v1/status/test_resource_status.py create mode 100755 orm/services/resource_distributor/rds/tests/controllers/v1/test_logs.py create mode 100644 orm/services/resource_distributor/rds/tests/functional_test.py create mode 100644 orm/services/resource_distributor/rds/tests/ordupdate/__init__.py create mode 100755 orm/services/resource_distributor/rds/tests/ordupdate/test_ord_notifier.py create mode 100644 orm/services/resource_distributor/rds/tests/services/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/services/model/__init__.py create mode 100755 orm/services/resource_distributor/rds/tests/services/model/test_region_resource_id_status.py create mode 100755 orm/services/resource_distributor/rds/tests/services/test_create_resource.py create mode 100755 orm/services/resource_distributor/rds/tests/services/test_customer_yaml.py create mode 100755 orm/services/resource_distributor/rds/tests/services/test_flavor_yaml.py create mode 100755 orm/services/resource_distributor/rds/tests/services/test_image_yaml.py create mode 100755 orm/services/resource_distributor/rds/tests/services/test_region_resource_id_status.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/git_sot/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_base.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_factory.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_gittle.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_native.py create mode 100755 orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_sot.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/test_base_sot.py create mode 100644 orm/services/resource_distributor/rds/tests/sot/test_sot_factory.py create mode 100644 orm/services/resource_distributor/rds/tests/storage/__init__.py create mode 100644 orm/services/resource_distributor/rds/tests/storage/mysql/__init__.py create mode 100755 orm/services/resource_distributor/rds/tests/storage/mysql/test_region_resource_id_status.py create mode 100644 orm/services/resource_distributor/rds/tests/storage/test_region_resource_id_status.py create mode 100644 orm/services/resource_distributor/rds/tests/utils/__init__.py create mode 100755 orm/services/resource_distributor/rds/tests/utils/test_uuid_utils.py create mode 100644 orm/services/resource_distributor/rds/utils/__init__.py create mode 100755 orm/services/resource_distributor/rds/utils/authentication.py create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/README.txt create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/__init__.py create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/audit_client/__init__.py create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/audit_client/api/__init__.py create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/audit_client/api/audit.py create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/keystone_utils/__init__.py create mode 100755 orm/services/resource_distributor/rds/utils/module_mocks/keystone_utils/tokens.py create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/orm_common/__init__.py create mode 100644 orm/services/resource_distributor/rds/utils/module_mocks/orm_common/utils/__init__.py create mode 100755 orm/services/resource_distributor/rds/utils/module_mocks/orm_common/utils/utils.py create mode 100755 orm/services/resource_distributor/rds/utils/utils.py create mode 100755 orm/services/resource_distributor/rds/utils/uuid_utils.py create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/README.md create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/api/swagger.yaml create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Resources.js create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/ResourcesService.js create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Status.js create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/StatusService.js create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/index.js create mode 100644 orm/services/resource_distributor/rds_docs/nodejs_rds_docs/package.json create mode 100755 orm/services/resource_distributor/rds_docs/rds_swagger.yaml create mode 100755 orm/services/resource_distributor/scripts/db_scripts/create_db.sql create mode 100755 orm/services/resource_distributor/scripts/db_scripts/update_db.sql create mode 100755 orm/services/resource_distributor/scripts/shell_scripts/create_db.sh create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/README.md create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/api/swagger.yaml create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Resources.js create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/ResourcesService.js create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Status.js create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/StatusService.js create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/index.js create mode 100644 orm/services/resource_distributor/swagger/nodejs_rds_docs/package.json create mode 100755 orm/services/resource_distributor/swagger/swagger.yaml create mode 100755 orm/services/resource_distributor/tox.ini create mode 100644 orm/swagger/__init__.py create mode 100644 orm/templates/error.html create mode 100644 orm/templates/index.html create mode 100644 orm/templates/layout.html create mode 100644 orm/tests/__init__.py create mode 100644 orm/tests/config.py create mode 100644 orm/tests/functional/__init__.py create mode 100644 orm/tests/test_functional.py create mode 100644 orm/tests/test_units.py create mode 100644 orm/tests/unit/__init__.py create mode 100644 public/css/style.css create mode 100644 public/images/logo.png create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..963e589a --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg* +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +cover/ +.coverage* +!.coveragerc +.tox +nosetests.xml +.testrepository +.venv + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +doc/build + +# pbr generates these +AUTHORS +ChangeLog + +# Editors +*~ +.*.swp +.*sw? + +# Files created by releasenotes build +releasenotes/build \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..c922f11a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include public * diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..c8a88e05 --- /dev/null +++ b/README.rst @@ -0,0 +1,13 @@ +=============================== +orm +=============================== + +Openstack Resource Management + +* TODO + +Features +-------- + +* TODO + diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 00000000..15cd6cb7 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/config.py b/config.py new file mode 100644 index 00000000..76a61999 --- /dev/null +++ b/config.py @@ -0,0 +1,54 @@ +# Server Specific Configurations +server = { + 'port': '9866', + 'host': '0.0.0.0' +} + +# Pecan Application Configurations +app = { + 'root': 'orm.controllers.root.RootController', + 'modules': ['orm'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/orm/templates', + 'debug': True, + 'errors': { + 404: '/error/404', + '__force_dict__': True + } +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'orm': {'level': 'DEBUG', 'handlers': ['console']}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console']}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +# Custom Configurations must be in Python dictionary format:: +# +# foo = {'bar':'baz'} +# +# All configurations are accessible at:: +# pecan.conf diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst new file mode 100644 index 00000000..2199771d --- /dev/null +++ b/doc/source/admin/index.rst @@ -0,0 +1,5 @@ +==================== +Administrators guide +==================== + +Administrators guide of ranger. diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst new file mode 100644 index 00000000..a24b26cd --- /dev/null +++ b/doc/source/cli/index.rst @@ -0,0 +1,5 @@ +================================ +Command line interface reference +================================ + +CLI reference of ranger. diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100755 index 00000000..8ae92f59 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,81 @@ +# -*- 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 os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + 'openstackdocstheme', + #'sphinx.ext.intersphinx', +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'ranger' +copyright = u'2017, OpenStack Developers' + +# openstackdocstheme options +repository_name = 'openstack/ranger' +bug_project = 'ranger' +bug_tag = '' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] +html_theme = 'openstackdocs' + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Developers', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/configuration/index.rst b/doc/source/configuration/index.rst new file mode 100644 index 00000000..11524609 --- /dev/null +++ b/doc/source/configuration/index.rst @@ -0,0 +1,5 @@ +============= +Configuration +============= + +Configuration of ranger. diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst new file mode 100644 index 00000000..2aa07077 --- /dev/null +++ b/doc/source/contributor/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../../CONTRIBUTING.rst diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 00000000..036e4494 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,9 @@ +=========================== + Contributor Documentation +=========================== + +.. toctree:: + :maxdepth: 2 + + contributing + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 00000000..954bd235 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,30 @@ +.. ranger documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +====================================== +Welcome to the documentation of ranger +====================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + install/index + library/index + contributor/index + configuration/index + cli/index + user/index + admin/index + reference/index + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/install/common_configure.rst b/doc/source/install/common_configure.rst new file mode 100644 index 00000000..d68e82cc --- /dev/null +++ b/doc/source/install/common_configure.rst @@ -0,0 +1,10 @@ +2. Edit the ``/etc/ranger/ranger.conf`` file and complete the following + actions: + + * In the ``[database]`` section, configure database access: + + .. code-block:: ini + + [database] + ... + connection = mysql+pymysql://ranger:RANGER_DBPASS@controller/ranger diff --git a/doc/source/install/common_prerequisites.rst b/doc/source/install/common_prerequisites.rst new file mode 100644 index 00000000..a53c5786 --- /dev/null +++ b/doc/source/install/common_prerequisites.rst @@ -0,0 +1,75 @@ +Prerequisites +------------- + +Before you install and configure the ranger service, +you must create a database, service credentials, and API endpoints. + +#. To create the database, complete these steps: + + * Use the database access client to connect to the database + server as the ``root`` user: + + .. code-block:: console + + $ mysql -u root -p + + * Create the ``ranger`` database: + + .. code-block:: none + + CREATE DATABASE ranger; + + * Grant proper access to the ``ranger`` database: + + .. code-block:: none + + GRANT ALL PRIVILEGES ON ranger.* TO 'ranger'@'localhost' \ + IDENTIFIED BY 'RANGER_DBPASS'; + GRANT ALL PRIVILEGES ON ranger.* TO 'ranger'@'%' \ + IDENTIFIED BY 'RANGER_DBPASS'; + + Replace ``RANGER_DBPASS`` with a suitable password. + + * Exit the database access client. + + .. code-block:: none + + exit; + +#. Source the ``admin`` credentials to gain access to + admin-only CLI commands: + + .. code-block:: console + + $ . admin-openrc + +#. To create the service credentials, complete these steps: + + * Create the ``ranger`` user: + + .. code-block:: console + + $ openstack user create --domain default --password-prompt ranger + + * Add the ``admin`` role to the ``ranger`` user: + + .. code-block:: console + + $ openstack role add --project service --user ranger admin + + * Create the ranger service entities: + + .. code-block:: console + + $ openstack service create --name ranger --description "ranger" ranger + +#. Create the ranger service API endpoints: + + .. code-block:: console + + $ openstack endpoint create --region RegionOne \ + ranger public http://controller:XXXX/vY/%\(tenant_id\)s + $ openstack endpoint create --region RegionOne \ + ranger internal http://controller:XXXX/vY/%\(tenant_id\)s + $ openstack endpoint create --region RegionOne \ + ranger admin http://controller:XXXX/vY/%\(tenant_id\)s diff --git a/doc/source/install/get_started.rst b/doc/source/install/get_started.rst new file mode 100644 index 00000000..9eb16cb4 --- /dev/null +++ b/doc/source/install/get_started.rst @@ -0,0 +1,9 @@ +======================= +ranger service overview +======================= +The ranger service provides... + +The ranger service consists of the following components: + +``ranger-api`` service + Accepts and responds to end user compute API calls... diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst new file mode 100644 index 00000000..19d2c65c --- /dev/null +++ b/doc/source/install/index.rst @@ -0,0 +1,17 @@ +================================= +ranger service installation guide +================================= + +.. toctree:: + :maxdepth: 2 + + get_started.rst + install.rst + verify.rst + next-steps.rst + +The ranger service (ranger) provides... + +This chapter assumes a working setup of OpenStack following the +`OpenStack Installation Tutorial +`_. diff --git a/doc/source/install/install-obs.rst b/doc/source/install/install-obs.rst new file mode 100644 index 00000000..eabf68f9 --- /dev/null +++ b/doc/source/install/install-obs.rst @@ -0,0 +1,34 @@ +.. _install-obs: + + +Install and configure for openSUSE and SUSE Linux Enterprise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section describes how to install and configure the ranger service +for openSUSE Leap 42.1 and SUSE Linux Enterprise Server 12 SP1. + +.. include:: common_prerequisites.rst + +Install and configure components +-------------------------------- + +#. Install the packages: + + .. code-block:: console + + # zypper --quiet --non-interactive install + +.. include:: common_configure.rst + + +Finalize installation +--------------------- + +Start the ranger services and configure them to start when +the system boots: + +.. code-block:: console + + # systemctl enable openstack-ranger-api.service + + # systemctl start openstack-ranger-api.service diff --git a/doc/source/install/install-rdo.rst b/doc/source/install/install-rdo.rst new file mode 100644 index 00000000..5c71c9ce --- /dev/null +++ b/doc/source/install/install-rdo.rst @@ -0,0 +1,33 @@ +.. _install-rdo: + +Install and configure for Red Hat Enterprise Linux and CentOS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +This section describes how to install and configure the ranger service +for Red Hat Enterprise Linux 7 and CentOS 7. + +.. include:: common_prerequisites.rst + +Install and configure components +-------------------------------- + +#. Install the packages: + + .. code-block:: console + + # yum install + +.. include:: common_configure.rst + +Finalize installation +--------------------- + +Start the ranger services and configure them to start when +the system boots: + +.. code-block:: console + + # systemctl enable openstack-ranger-api.service + + # systemctl start openstack-ranger-api.service diff --git a/doc/source/install/install-ubuntu.rst b/doc/source/install/install-ubuntu.rst new file mode 100644 index 00000000..58b8e300 --- /dev/null +++ b/doc/source/install/install-ubuntu.rst @@ -0,0 +1,31 @@ +.. _install-ubuntu: + +Install and configure for Ubuntu +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section describes how to install and configure the ranger +service for Ubuntu 14.04 (LTS). + +.. include:: common_prerequisites.rst + +Install and configure components +-------------------------------- + +#. Install the packages: + + .. code-block:: console + + # apt-get update + + # apt-get install + +.. include:: common_configure.rst + +Finalize installation +--------------------- + +Restart the ranger services: + +.. code-block:: console + + # service openstack-ranger-api restart diff --git a/doc/source/install/install.rst b/doc/source/install/install.rst new file mode 100644 index 00000000..bf0e3739 --- /dev/null +++ b/doc/source/install/install.rst @@ -0,0 +1,20 @@ +.. _install: + +Install and configure +~~~~~~~~~~~~~~~~~~~~~ + +This section describes how to install and configure the +ranger service, code-named ranger, on the controller node. + +This section assumes that you already have a working OpenStack +environment with at least the following components installed: +.. (add the appropriate services here and further notes) + +Note that installation and configuration vary by distribution. + +.. toctree:: + :maxdepth: 2 + + install-obs.rst + install-rdo.rst + install-ubuntu.rst diff --git a/doc/source/install/next-steps.rst b/doc/source/install/next-steps.rst new file mode 100644 index 00000000..5b26aa42 --- /dev/null +++ b/doc/source/install/next-steps.rst @@ -0,0 +1,9 @@ +.. _next-steps: + +Next steps +~~~~~~~~~~ + +Your OpenStack environment now includes the ranger service. + +To add additional services, see +https://docs.openstack.org/project-install-guide/ocata/. diff --git a/doc/source/install/verify.rst b/doc/source/install/verify.rst new file mode 100644 index 00000000..9f7e7536 --- /dev/null +++ b/doc/source/install/verify.rst @@ -0,0 +1,24 @@ +.. _verify: + +Verify operation +~~~~~~~~~~~~~~~~ + +Verify operation of the ranger service. + +.. note:: + + Perform these commands on the controller node. + +#. Source the ``admin`` project credentials to gain access to + admin-only CLI commands: + + .. code-block:: console + + $ . admin-openrc + +#. List service components to verify successful launch and registration + of each process: + + .. code-block:: console + + $ openstack ranger service list diff --git a/doc/source/library/index.rst b/doc/source/library/index.rst new file mode 100644 index 00000000..50d25348 --- /dev/null +++ b/doc/source/library/index.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use ranger in a project:: + + import ranger diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 00000000..a6210d3d --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 00000000..a1c5ce57 --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,5 @@ +========== +References +========== + +References of ranger. diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst new file mode 100644 index 00000000..4d58bdf8 --- /dev/null +++ b/doc/source/user/index.rst @@ -0,0 +1,5 @@ +=========== +Users guide +=========== + +Users guide of ranger. diff --git a/etc/orm.conf b/etc/orm.conf new file mode 100644 index 00000000..e69de29b diff --git a/orm/__init__.py b/orm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/app.py b/orm/app.py new file mode 100644 index 00000000..aa55a875 --- /dev/null +++ b/orm/app.py @@ -0,0 +1,14 @@ +from pecan import make_app +from orm import model + + +def setup_app(config): + + model.init_model() + app_conf = dict(config.app) + + return make_app( + app_conf.pop('root'), + logging=getattr(config, 'logging', {}), + **app_conf + ) diff --git a/orm/cmd/__init__.py b/orm/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/cmd/audit.py b/orm/cmd/audit.py new file mode 100644 index 00000000..eb2230fc --- /dev/null +++ b/orm/cmd/audit.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.audit_trail_manager.audit_server import app + + +def main(): + app.main() diff --git a/orm/cmd/cms.py b/orm/cmd/cms.py new file mode 100644 index 00000000..01c14741 --- /dev/null +++ b/orm/cmd/cms.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.customer_manager.cms_rest import app + + +def main(): + app.main() diff --git a/orm/cmd/fms.py b/orm/cmd/fms.py new file mode 100644 index 00000000..1abb35f7 --- /dev/null +++ b/orm/cmd/fms.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.flavor_manager.fms_rest import app + + +def main(): + app.main() diff --git a/orm/cmd/ims.py b/orm/cmd/ims.py new file mode 100644 index 00000000..45b9fac0 --- /dev/null +++ b/orm/cmd/ims.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.image_manager.ims import app + + +def main(): + app.main() diff --git a/orm/cmd/keystone.py b/orm/cmd/keystone.py new file mode 100644 index 00000000..6d7bada3 --- /dev/null +++ b/orm/cmd/keystone.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.resource_distributor.rds import app + + +def main(): + app.main() diff --git a/orm/cmd/rds.py b/orm/cmd/rds.py new file mode 100644 index 00000000..6d7bada3 --- /dev/null +++ b/orm/cmd/rds.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.resource_distributor.rds import app + + +def main(): + app.main() diff --git a/orm/cmd/rms.py b/orm/cmd/rms.py new file mode 100644 index 00000000..b6e6cffe --- /dev/null +++ b/orm/cmd/rms.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.region_manager.rms import app + + +def main(): + app.main() diff --git a/orm/cmd/uuidgen.py b/orm/cmd/uuidgen.py new file mode 100644 index 00000000..dbf8844f --- /dev/null +++ b/orm/cmd/uuidgen.py @@ -0,0 +1,19 @@ +# Copyright 2016 ATT +# +# 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 orm.services.id_generator.uuidgen import app + + +def main(): + app.main() diff --git a/orm/common/__init__.py b/orm/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/__init__.py b/orm/common/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/audit/__init__.py b/orm/common/client/audit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/CONTRIBUTING.rst b/orm/common/client/keystone/CONTRIBUTING.rst new file mode 100644 index 00000000..1271f07d --- /dev/null +++ b/orm/common/client/keystone/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/keystone_utils diff --git a/orm/common/client/keystone/HACKING.rst b/orm/common/client/keystone/HACKING.rst new file mode 100644 index 00000000..923c0d23 --- /dev/null +++ b/orm/common/client/keystone/HACKING.rst @@ -0,0 +1,4 @@ +keystone_utils Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/orm/common/client/keystone/LICENSE b/orm/common/client/keystone/LICENSE new file mode 100644 index 00000000..68c771a0 --- /dev/null +++ b/orm/common/client/keystone/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/orm/common/client/keystone/MANIFEST.in b/orm/common/client/keystone/MANIFEST.in new file mode 100644 index 00000000..c978a52d --- /dev/null +++ b/orm/common/client/keystone/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc diff --git a/orm/common/client/keystone/README.rst b/orm/common/client/keystone/README.rst new file mode 100644 index 00000000..40da6123 --- /dev/null +++ b/orm/common/client/keystone/README.rst @@ -0,0 +1,19 @@ +=============================== +keystone_utils +=============================== + +keyKeystone utils + +Please feel here a long description which must be at least 3 lines wrapped on +80 cols, so that distribution package maintainers can use it in their packages. +Note that this is a hard requirement. + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/keystone_utils +* Source: http://git.openstack.org/cgit/keystone_utils/keystone_utils +* Bugs: http://bugs.launchpad.net/keystone_utils + +Features +-------- + +* TODO diff --git a/orm/common/client/keystone/__init__.py b/orm/common/client/keystone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/babel.cfg b/orm/common/client/keystone/babel.cfg new file mode 100644 index 00000000..15cd6cb7 --- /dev/null +++ b/orm/common/client/keystone/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/orm/common/client/keystone/debian/aic-orm-keystone.install b/orm/common/client/keystone/debian/aic-orm-keystone.install new file mode 100644 index 00000000..a98784d5 --- /dev/null +++ b/orm/common/client/keystone/debian/aic-orm-keystone.install @@ -0,0 +1 @@ +aic-orm-keystone/* opt/app/orm/keystone_utils diff --git a/orm/common/client/keystone/debian/changelog b/orm/common/client/keystone/debian/changelog new file mode 100755 index 00000000..86458417 --- /dev/null +++ b/orm/common/client/keystone/debian/changelog @@ -0,0 +1,5 @@ +aic-orm-keystone (3.5.0) stable; urgency=low + + * this is release 3.5.0 + + -- Jenkins job Thu, 24 Jun 2016 14:48:02 +0000 diff --git a/orm/common/client/keystone/debian/compat b/orm/common/client/keystone/debian/compat new file mode 100755 index 00000000..ec635144 --- /dev/null +++ b/orm/common/client/keystone/debian/compat @@ -0,0 +1 @@ +9 diff --git a/orm/common/client/keystone/debian/control b/orm/common/client/keystone/debian/control new file mode 100755 index 00000000..72cab6d3 --- /dev/null +++ b/orm/common/client/keystone/debian/control @@ -0,0 +1,13 @@ +Source: aic-orm-keystone +Section: unknown +Priority: optional +Maintainer: orm +Build-Depends: debhelper (>= 8.0.0) +Standards-Version: 3.9.4 +XS-Python-Version: >= 2.7 +Homepage: + +Package: aic-orm-keystone +Architecture: any +Depends: python-keystoneclient (>= 1.2.0), python-requests (>= 2.2.0) +Description: aic-orm-keystone application for ORM diff --git a/orm/common/client/keystone/debian/copyright b/orm/common/client/keystone/debian/copyright new file mode 100755 index 00000000..60ee39f4 --- /dev/null +++ b/orm/common/client/keystone/debian/copyright @@ -0,0 +1,34 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: aic-orm-keystone +Source: + +Files: * +Copyright: + +License: GPL-3.0+ + +Files: debian/* +Copyright: 2016 root +License: GPL-3.0+ + +License: GPL-3.0+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". + +# Please also look if there are files or directories which have a +# different copyright/license attached and list them here. +# Please avoid to pick license terms that are more restrictive than the +# packaged work, as it may make Debian's contributions unacceptable upstream. diff --git a/orm/common/client/keystone/debian/docs b/orm/common/client/keystone/debian/docs new file mode 100755 index 00000000..e69de29b diff --git a/orm/common/client/keystone/debian/postrm b/orm/common/client/keystone/debian/postrm new file mode 100755 index 00000000..dc3dad32 --- /dev/null +++ b/orm/common/client/keystone/debian/postrm @@ -0,0 +1,4 @@ +#!/bin/bash + +#rm -rf /opt/app/orm/keystone_utils +#echo "Deleting /opt/app/orm/keystone_utils directory." diff --git a/orm/common/client/keystone/debian/rules b/orm/common/client/keystone/debian/rules new file mode 100755 index 00000000..db692436 --- /dev/null +++ b/orm/common/client/keystone/debian/rules @@ -0,0 +1,8 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +%: + dh $@ --buildsystem=python_distutils -D aic-orm-keystone diff --git a/orm/common/client/keystone/debian/source/format b/orm/common/client/keystone/debian/source/format new file mode 100755 index 00000000..89ae9db8 --- /dev/null +++ b/orm/common/client/keystone/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/orm/common/client/keystone/doc/source/contributing.rst b/orm/common/client/keystone/doc/source/contributing.rst new file mode 100644 index 00000000..1728a61c --- /dev/null +++ b/orm/common/client/keystone/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst diff --git a/orm/common/client/keystone/doc/source/index.rst b/orm/common/client/keystone/doc/source/index.rst new file mode 100644 index 00000000..e98a5d29 --- /dev/null +++ b/orm/common/client/keystone/doc/source/index.rst @@ -0,0 +1,25 @@ +.. keystone_utils documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to keystone_utils's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/orm/common/client/keystone/doc/source/installation.rst b/orm/common/client/keystone/doc/source/installation.rst new file mode 100644 index 00000000..645e0aa1 --- /dev/null +++ b/orm/common/client/keystone/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install keystone_utils + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv keystone_utils + $ pip install keystone_utils diff --git a/orm/common/client/keystone/doc/source/readme.rst b/orm/common/client/keystone/doc/source/readme.rst new file mode 100644 index 00000000..a6210d3d --- /dev/null +++ b/orm/common/client/keystone/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst diff --git a/orm/common/client/keystone/doc/source/usage.rst b/orm/common/client/keystone/doc/source/usage.rst new file mode 100644 index 00000000..b669d988 --- /dev/null +++ b/orm/common/client/keystone/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use keystone_utils in a project:: + + import keystone_utils diff --git a/orm/common/client/keystone/keystone_utils.egg-info/PKG-INFO b/orm/common/client/keystone/keystone_utils.egg-info/PKG-INFO new file mode 100644 index 00000000..d8d62d58 --- /dev/null +++ b/orm/common/client/keystone/keystone_utils.egg-info/PKG-INFO @@ -0,0 +1,21 @@ +Metadata-Version: 1.1 +Name: keystone-utils +Version: 0.1 +Summary: keyKeystone utils +Home-page: http://www.openstack.org/ +Author: OpenStack +Author-email: openstack-dev@lists.openstack.org +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Environment :: OpenStack +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 diff --git a/orm/common/client/keystone/keystone_utils.egg-info/SOURCES.txt b/orm/common/client/keystone/keystone_utils.egg-info/SOURCES.txt new file mode 100644 index 00000000..9522de07 --- /dev/null +++ b/orm/common/client/keystone/keystone_utils.egg-info/SOURCES.txt @@ -0,0 +1,25 @@ +MANIFEST.in +README.rst +setup.cfg +setup.py +keystone_utils/__init__.py +keystone_utils/tokens.py +keystone_utils.egg-info/PKG-INFO +keystone_utils.egg-info/SOURCES.txt +keystone_utils.egg-info/dependency_links.txt +keystone_utils.egg-info/not-zip-safe +keystone_utils.egg-info/pbr.json +keystone_utils.egg-info/top_level.txt +keystone_utils/tests/__init__.py +keystone_utils/tests/unit/__init__.py +keystone_utils/tests/unit/test_tokens.py +mock_keystone/__init__.py +mock_keystone/keystoneclient/__init__.py +mock_keystone/keystoneclient/exceptions.py +mock_keystone/keystoneclient/v2_0/__init__.py +mock_keystone/keystoneclient/v2_0/client.py +mock_keystone/keystoneclient/v3/__init__.py +mock_keystone/keystoneclient/v3/client.py +mock_keystone/orm_common/__init__.py +mock_keystone/orm_common/utils/__init__.py +mock_keystone/orm_common/utils/dictator.py \ No newline at end of file diff --git a/orm/common/client/keystone/keystone_utils.egg-info/dependency_links.txt b/orm/common/client/keystone/keystone_utils.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/orm/common/client/keystone/keystone_utils.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/orm/common/client/keystone/keystone_utils.egg-info/not-zip-safe b/orm/common/client/keystone/keystone_utils.egg-info/not-zip-safe new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/orm/common/client/keystone/keystone_utils.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/orm/common/client/keystone/keystone_utils.egg-info/pbr.json b/orm/common/client/keystone/keystone_utils.egg-info/pbr.json new file mode 100644 index 00000000..0b09e18b --- /dev/null +++ b/orm/common/client/keystone/keystone_utils.egg-info/pbr.json @@ -0,0 +1 @@ +{"is_release": false, "git_version": ""} \ No newline at end of file diff --git a/orm/common/client/keystone/keystone_utils.egg-info/top_level.txt b/orm/common/client/keystone/keystone_utils.egg-info/top_level.txt new file mode 100644 index 00000000..b6d50ed1 --- /dev/null +++ b/orm/common/client/keystone/keystone_utils.egg-info/top_level.txt @@ -0,0 +1,2 @@ +keystone_utils +mock_keystone diff --git a/orm/common/client/keystone/keystone_utils/__init__.py b/orm/common/client/keystone/keystone_utils/__init__.py new file mode 100644 index 00000000..19f5e722 --- /dev/null +++ b/orm/common/client/keystone/keystone_utils/__init__.py @@ -0,0 +1,13 @@ +# -*- 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. diff --git a/orm/common/client/keystone/keystone_utils/tests/__init__.py b/orm/common/client/keystone/keystone_utils/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/keystone_utils/tests/unit/__init__.py b/orm/common/client/keystone/keystone_utils/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/keystone_utils/tests/unit/test_tokens.py b/orm/common/client/keystone/keystone_utils/tests/unit/test_tokens.py new file mode 100755 index 00000000..be38f34d --- /dev/null +++ b/orm/common/client/keystone/keystone_utils/tests/unit/test_tokens.py @@ -0,0 +1,219 @@ +"""keystone_utils token validator unittests.""" +import mock +import unittest + +from keystone_utils import tokens + + +class MyResponse(object): + def __init__(self, status, json_result): + self.status_code = status + self._json_result = json_result + + def json(self): + return self._json_result + + +class MyKeystone(object): + def validate(self, a): + raise tokens.v3_client.exceptions.NotFound('test') + + def find(self, **kwargs): + raise tokens.v3_client.exceptions.NotFound('test') + + +class MyClient(object): + def __init__(self, set_tokens=True): + if set_tokens: + self.tokens = MyKeystone() + else: + self.tokens = mock.MagicMock() + + self.roles = MyKeystone() + + +class TokensTest(unittest.TestCase): + def setUp(self): + tokens._KEYSTONES = {} + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + def test_find_keystone_ep_sanity(self, mock_get): + result = tokens._find_keystone_ep('a', 'b') + self.assertEqual(result, 'test') + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE + 1, {'regions': [{'endpoints': [ + {'publicURL': 'test', 'type': 'identity'}]}]})) + def test_find_keystone_ep_bad_return_code(self, mock_get): + result = tokens._find_keystone_ep('a', 'b') + self.assertIsNone(result) + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {})) + def test_find_keystone_ep_no_keystone_ep_in_response(self, mock_get): + result = tokens._find_keystone_ep('a', 'b') + self.assertIsNone(result) + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'test'}]}]})) + def test_find_keystone_ep_no_identity_in_response(self, mock_get): + result = tokens._find_keystone_ep('a', 'b') + self.assertIsNone(result) + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + @mock.patch.object(tokens.v3_client, 'Client') + def test_is_token_valid_sanity(self, mock_get, mock_client): + self.assertTrue(tokens.is_token_valid('a', 'b', tokens.TokenConf( + 'a', 'b', 'c', 'd', '3'))) + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + @mock.patch.object(tokens.v3_client, 'Client') + def test_is_token_valid_sanity_role_required(self, mock_get, mock_client): + user = {'user': {'id': 'test_id', 'domain': {'id': 'test'}}} + mock_client.tokens.validate = mock.MagicMock(return_value=user) + self.assertTrue(tokens.is_token_valid('a', 'b', tokens.TokenConf( + 'a', 'b', 'c', 'd', '3'), 'test', {'domain': 'test'})) + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + def test_is_token_valid_token_not_found(self, mock_get): + client_backup = tokens.v3_client.Client + tokens.v3_client.Client = mock.MagicMock(return_value=MyClient()) + self.assertFalse(tokens.is_token_valid('a', 'b', tokens.TokenConf( + 'a', 'b', 'c', 'd', '3'))) + tokens.v3_client.Client = client_backup + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + def test_is_token_valid_invalid_version(self, mock_get): + client_backup = tokens.v3_client.Client + tokens.v3_client.Client = mock.MagicMock(return_value=MyClient()) + self.assertRaises(ValueError, tokens.is_token_valid, 'a', 'b', + tokens.TokenConf('a', 'b', 'c', 'd', '4')) + tokens.v3_client.Client = client_backup + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + def test_is_token_valid_keystone_v2(self, mock_get): + client_backup = tokens.v2_client.Client + tokens.v2_client.Client = mock.MagicMock() + self.assertFalse(tokens.is_token_valid('a', 'b', + tokens.TokenConf('a', 'b', 'c', + 'd', '2.0'), + 'test', + {'tenant': 'test'})) + tokens.v2_client.Client = client_backup + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + def test_is_token_valid_keystone_v2_invalid_location(self, mock_get): + client_backup = tokens.v2_client.Client + tokens.v2_client.Client = mock.MagicMock() + self.assertRaises(ValueError, tokens.is_token_valid, 'a', 'b', + tokens.TokenConf('a', 'b', 'c', 'd', '2.0'), 'test', + {'domain': 'test'}) + tokens.v2_client.Client = client_backup + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE + 1, {'regions': [{'endpoints': [ + {'publicURL': 'test', 'type': 'identity'}]}]})) + def test_is_token_valid_keystone_ep_not_found(self, mock_get): + self.assertRaises(tokens.KeystoneNotFoundError, tokens.is_token_valid, + 'a', 'b', tokens.TokenConf('a', 'b', 'c', 'd', '3')) + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + def test_is_token_valid_no_role_location(self, mock_get): + tokens.v3_client.Client = mock.MagicMock() + self.assertRaises(ValueError, tokens.is_token_valid, 'a', 'b', + tokens.TokenConf('a', 'b', 'c', 'd', '3'), 'test') + + @mock.patch.object(tokens.v3_client, 'Client') + def test_does_user_have_role_sanity_true(self, mock_client): + user = {'user': {'id': 'test_id', 'domain': {'id': 'test'}}} + self.assertTrue(tokens._does_user_have_role(mock_client, '3', user, + 'admin', + {'domain': 'test'})) + + @mock.patch.object(tokens.v3_client, 'Client') + def test_does_user_have_role_sanity_false(self, mock_client): + user = {'user': {'id': 'test_id', 'domain': {'id': 'test'}}} + mock_client.roles.check = mock.MagicMock( + side_effect=tokens.v3_client.exceptions.NotFound('test')) + self.assertFalse(tokens._does_user_have_role(mock_client, '3', user, + 'admin', + {'domain': 'test'})) + + @mock.patch.object(tokens.v3_client, 'Client') + def test_does_user_have_role_invalid_user(self, mock_client): + user = {} + self.assertFalse(tokens._does_user_have_role(mock_client, '3', user, + 'admin', + {'domain': 'test'})) + + @mock.patch.object(tokens.v3_client, 'Client') + def test_does_user_have_role_role_does_not_exist(self, mock_client): + user = {'user': {'id': 'test_id', 'domain': {'id': 'test'}}} + mock_client.roles.find = mock.MagicMock( + side_effect=tokens.v3_client.exceptions.NotFound('test')) + self.assertRaises(tokens.v3_client.exceptions.NotFound, + tokens._does_user_have_role, mock_client, '3', + user, 'test', {'domain': 'default'}) + + @mock.patch.object(tokens.requests, 'get', return_value=MyResponse( + tokens.OK_CODE, {'regions': [{'endpoints': [{'publicURL': 'test', + 'type': 'identity'}]}]})) + def test_is_token_valid_role_does_not_exist(self, mock_get): + tokens.v3_client.Client = mock.MagicMock(return_value=MyClient(False)) + self.assertRaises(ValueError, tokens.is_token_valid, 'a', 'b', + tokens.TokenConf('a', 'b', 'c', 'd', '3'), 'test', + {'domain': 'test'}) + + def test_get_token_user_invalid_arguments(self): + self.assertRaises(ValueError, tokens.get_token_user, 'a', 'b') + + @mock.patch.object(tokens, '_find_keystone_ep', return_value=None) + def test_get_token_user_keystone_ep_not_found(self, + mock_find_keystone_ep): + self.assertRaises(tokens.KeystoneNotFoundError, + tokens.get_token_user, 'a', mock.MagicMock(), 'c') + + def test_get_token_user_invalid_keystone_version(self): + conf = tokens.TokenConf(*(None,)*5) + self.assertRaises(ValueError, tokens.get_token_user, 'a', conf, 'c', + 'd') + + @mock.patch.object(tokens, '_get_keystone_client') + def test_get_token_user_token_not_found(self, mock_get_keystone_client): + ks = mock.MagicMock() + ks.tokens.validate.side_effect = tokens.v3_client.exceptions.NotFound() + mock_get_keystone_client.return_value = ks + conf = tokens.TokenConf(*('3',)*5) + self.assertIsNone(tokens.get_token_user('a', conf, 'c', 'd')) + + @mock.patch.object(tokens, '_get_keystone_client') + def test_get_token_user_success(self, mock_get_keystone_client): + token_info = mock.MagicMock() + token_info.token = 'a' + token_info.user = 'test_user' + ks = mock.MagicMock() + ks.tokens.validate.return_value = token_info + mock_get_keystone_client.return_value = ks + + conf = tokens.TokenConf(*('2.0',)*5) + result = tokens.get_token_user('a', conf, 'c', 'd') + + self.assertEqual(result.token, 'a') + self.assertEqual(result.user, 'test_user') diff --git a/orm/common/client/keystone/keystone_utils/tokens.py b/orm/common/client/keystone/keystone_utils/tokens.py new file mode 100755 index 00000000..7a64153f --- /dev/null +++ b/orm/common/client/keystone/keystone_utils/tokens.py @@ -0,0 +1,278 @@ +"""Token utility module.""" +import logging +import requests + +from keystoneclient.v2_0 import client as v2_client +from keystoneclient.v3 import client as v3_client + +from orm_common.utils import dictator + +_verify = False + +OK_CODE = 200 +_KEYSTONES = {} +logger = logging.getLogger(__name__) + + +class KeystoneNotFoundError(Exception): + """Indicates that the Keystone EP of a certain LCP was not found.""" + + pass + + +class TokenConf(object): + """The Token Validator configuration class.""" + + def __init__(self, mech_id, password, rms_url, tenant_name, version): + """Initialize the Token Validator configuration. + + :param mech_id: Username for Keystone + :param password: Password for Keystone + :param rms_url: The entire RMS URL, e.g. 'http://1.3.3.7:8080' + :param tenant_name: The ORM tenant name + :param version: Keystone version to use (a string: '3' or '2.0') + """ + self.mech_id = mech_id + self.password = password + self.rms_url = rms_url + self.tenant_name = tenant_name + self.version = version + + +class TokenUser(object): + """Class with details about the token user.""" + + def __init__(self, token): + """Initialize the Token User. + + :param token: The token object (returned by tokens.validate) + """ + self.token = token.token + self.user = token.user + self.tenant = getattr(token, 'tenant', None) + self.domain = getattr(token, 'domain', None) + + +def get_token_user(token, conf, lcp_id=None, keystone_ep=None): + """Get a token user. + + :param token: The token to validate + :param conf: A TokenConf object + :param lcp_id: The ID of the LCP associated with the Keystone instance + with which the token was created. Ignored if keystone_ep is not None + :param keystone_ep: The Keystone endpoint, in case we already have it + :return: False if one of the tokens received (or more) is invalid, + True otherwise. + """ + # Not using logger.error/exception because in some cases, these flows + # can be completely valid + if keystone_ep is None: + if lcp_id is None: + message = 'Received None for both keystone_ep and lcp_id!' + logger.debug(message) + raise ValueError(message) + keystone_ep = _find_keystone_ep(conf.rms_url, lcp_id) + if keystone_ep is None: + message = 'Keystone EP of LCP %s not found in RMS' % (lcp_id,) + logger.debug(message) + logger.critical( + 'CRITICAL|CON{}KEYSTONE002|X-Auth-Region: {} is not ' + 'reachable (not found in RMS)'.format( + dictator.get('service_name', 'ORM'), lcp_id)) + raise KeystoneNotFoundError(message) + + if conf.version == '3': + client = v3_client + elif conf.version == '2.0': + client = v2_client + else: + message = 'Invalid Keystone version: %s' % (conf.version,) + logger.debug(message) + raise ValueError(message) + + keystone = _get_keystone_client(client, conf, keystone_ep, lcp_id) + + try: + token_info = keystone.tokens.validate(token) + logger.debug('User token found in Keystone') + return TokenUser(token_info) + # Other exceptions raised by validate() are critical errors, + # so instead of returning False, we'll just let them propagate + except client.exceptions.NotFound: + logger.debug('User token not found in Keystone! Make sure that it is ' + 'correct and that it has not expired yet') + return None + + +def _find_keystone_ep(rms_url, lcp_name): + """Get the Keystone EP from RMS. + + :param rms_url: RMS server URL + :param lcp_name: The LCP name + :return: Keystone EP (string), None if it was not found + """ + if not rms_url: + message = 'Invalid RMS URL: %s' % (rms_url,) + logger.debug(message) + raise ValueError(message) + + logger.debug( + 'Looking for Keystone EP of LCP {} using RMS URL {}'.format( + lcp_name, rms_url)) + + response = requests.get('%s/v2/orm/regions?regionname=%s' % ( + rms_url, lcp_name, ), verify=_verify) + if response.status_code != OK_CODE: + # The LCP was not found in RMS + logger.debug('Received bad response code from RMS: {}'.format( + response.status_code)) + return None + + lcp = response.json() + try: + for endpoint in lcp['regions'][0]['endpoints']: + if endpoint['type'] == 'identity': + return endpoint['publicURL'] + except KeyError: + logger.debug('Response from RMS came in an unsupported format. ' + 'Make sure that you are using RMS 3.5') + return None + + # Keystone EP not found in the response + logger.debug('No identity endpoint was found in the response from RMS') + return None + + +def _does_user_have_role(keystone, version, user, role, location): + """Check whether a user has a role. + + :param keystone: The Keystone client to use + :param version: Keystone version + :param user: A dict that represents the user in question + :param role: The role to check whether the user has + :param location: Keystone role location + :return: True if the user has the requested role, False otherwise. + :raise: client.exceptions.NotFound when the requested role does not exist, + ValueError when the version is 2.0 but the location is not 'tenant' + """ + location = dict(location) + if version == '3': + role = keystone.roles.find(name=role) + try: + return keystone.roles.check(role, user=user['user']['id'], + **location) + except v3_client.exceptions.NotFound: + return False + except KeyError: + # Shouldn't be raised when using Keystone's v3/v2.0 API, but let's + # play on the safe side + logger.debug('The user parameter came in a wrong format!') + return False + elif version == '2.0': + # v2.0 supports tenants only + if location.keys()[0] != 'tenant': + raise ValueError( + 'Using Keystone v2.0, expected "tenant", received: "%s"' % ( + location.keys()[0],)) + + tenant = keystone.tenants.find(name=location['tenant']) + # v2.0 does not enable us to check for a specific role (unlike v3) + role_list = keystone.roles.roles_for_user(user.user['id'], + tenant=tenant) + return any([user_role.name == role for user_role in role_list]) + + +def _get_keystone_client(client, conf, keystone_ep, lcp_id): + """Get the Keystone client. + + :param client: keystoneclient package to use + :param conf: Token conf + :param keystone_ep: The Keystone endpoint that RMS returned + :param lcp_id: The region ID + + :return: The instance of Keystone client to use + """ + global _KEYSTONES + try: + if keystone_ep not in _KEYSTONES: + # Instantiate the Keystone client according to the configuration + _KEYSTONES[keystone_ep] = client.Client( + username=conf.mech_id, + password=conf.password, + tenant_name=conf.tenant_name, + auth_url=keystone_ep + '/v' + conf.version) + + return _KEYSTONES[keystone_ep] + except Exception: + logger.critical( + 'CRITICAL|CON{}KEYSTONE001|Cannot reach Keystone EP: {} of ' + 'region {}. Please contact Keystone team.'.format( + dictator.get('service_name', 'ORM'), keystone_ep, lcp_id)) + raise + + +def is_token_valid(token_to_validate, lcp_id, conf, required_role=None, + role_location=None): + """Validate a token. + + :param token_to_validate: The token to validate + :param lcp_id: The ID of the LCP associated with the Keystone instance + with which the token was created + :param conf: A TokenConf object + :param required_role: The required role for privileged actions, + e.g. 'admin' (optional). + :param role_location: The Keystone role location (a dict whose single + key is either 'domain' or 'tenant', whose value is the location name) + :return: False if one of the tokens received (or more) is invalid, + True otherwise. + :raise: KeystoneNotFoundError when the Keystone EP for the required LCP + was not found in RMS output, + client.exceptions.AuthorizationFailure when the connection with the + Keystone EP could not be established, + client.exceptions.EndpointNotFound when _our_ authentication + (as an admin) with Keystone failed, + ValueError when an invalid Keystone version was specified, + ValueError when a role or a tenant was not found, + ValueError when a role is required but role_location is None. + """ + keystone_ep = _find_keystone_ep(conf.rms_url, lcp_id) + if keystone_ep is None: + raise KeystoneNotFoundError('Keystone EP of LCP %s not found in RMS' % + (lcp_id,)) + + if conf.version == '3': + client = v3_client + elif conf.version == '2.0': + client = v2_client + else: + raise ValueError('Invalid Keystone version: %s' % (conf.version,)) + + keystone = _get_keystone_client(client, conf, keystone_ep, lcp_id) + + try: + user = keystone.tokens.validate(token_to_validate) + logger.debug('User token found in Keystone') + # Other exceptions raised by validate() are critical errors, + # so instead of returning False, we'll just let them propagate + except client.exceptions.NotFound: + logger.debug('User token not found in Keystone! Make sure that it is' + 'correct and that it has not expired yet') + return False + + if required_role is not None: + if role_location is None: + raise ValueError( + 'A role is required but no role location was specified!') + + try: + logger.debug('Checking role...') + return _does_user_have_role(keystone, conf.version, user, + required_role, role_location) + except client.exceptions.NotFound: + raise ValueError('Role %s or tenant %s not found!' % ( + required_role, role_location,)) + else: + # We know that the token is valid and there's no need to enforce a + # policy on this operation, so we can let the user pass + logger.debug('No role to check, authentication finished successfully') + return True diff --git a/orm/common/client/keystone/mock_keystone/__init__.py b/orm/common/client/keystone/mock_keystone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/mock_keystone/keystoneclient/__init__.py b/orm/common/client/keystone/mock_keystone/keystoneclient/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/mock_keystone/keystoneclient/exceptions.py b/orm/common/client/keystone/mock_keystone/keystoneclient/exceptions.py new file mode 100644 index 00000000..f88096f8 --- /dev/null +++ b/orm/common/client/keystone/mock_keystone/keystoneclient/exceptions.py @@ -0,0 +1,2 @@ +class NotFound(Exception): + pass diff --git a/orm/common/client/keystone/mock_keystone/keystoneclient/v2_0/__init__.py b/orm/common/client/keystone/mock_keystone/keystoneclient/v2_0/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/mock_keystone/keystoneclient/v2_0/client.py b/orm/common/client/keystone/mock_keystone/keystoneclient/v2_0/client.py new file mode 100644 index 00000000..b9600200 --- /dev/null +++ b/orm/common/client/keystone/mock_keystone/keystoneclient/v2_0/client.py @@ -0,0 +1,6 @@ +from keystoneclient import exceptions + + +class Client(object): + def __init__(*args, **kwargs): + pass diff --git a/orm/common/client/keystone/mock_keystone/keystoneclient/v3/__init__.py b/orm/common/client/keystone/mock_keystone/keystoneclient/v3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/mock_keystone/keystoneclient/v3/client.py b/orm/common/client/keystone/mock_keystone/keystoneclient/v3/client.py new file mode 100644 index 00000000..b9600200 --- /dev/null +++ b/orm/common/client/keystone/mock_keystone/keystoneclient/v3/client.py @@ -0,0 +1,6 @@ +from keystoneclient import exceptions + + +class Client(object): + def __init__(*args, **kwargs): + pass diff --git a/orm/common/client/keystone/mock_keystone/orm_common/__init__.py b/orm/common/client/keystone/mock_keystone/orm_common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/mock_keystone/orm_common/utils/__init__.py b/orm/common/client/keystone/mock_keystone/orm_common/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/client/keystone/mock_keystone/orm_common/utils/dictator.py b/orm/common/client/keystone/mock_keystone/orm_common/utils/dictator.py new file mode 100644 index 00000000..0a299155 --- /dev/null +++ b/orm/common/client/keystone/mock_keystone/orm_common/utils/dictator.py @@ -0,0 +1,10 @@ +def get(*args, **kwargs): + pass + + +def set(*args, **kwargs): + pass + + +def soft_set(*args, **kwargs): + pass diff --git a/orm/common/client/keystone/requirements.txt b/orm/common/client/keystone/requirements.txt new file mode 100644 index 00000000..1719ed89 --- /dev/null +++ b/orm/common/client/keystone/requirements.txt @@ -0,0 +1,5 @@ +# 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. +requests==2.2.1 +python-keystoneclient==1.3.1 diff --git a/orm/common/client/keystone/setup.cfg b/orm/common/client/keystone/setup.cfg new file mode 100644 index 00000000..1fa083ad --- /dev/null +++ b/orm/common/client/keystone/setup.cfg @@ -0,0 +1,46 @@ +[metadata] +name = keystone_utils +summary = keyKeystone utils +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + keystone_utils + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = keystone_utils/locale +domain = keystone_utils + +[update_catalog] +domain = keystone_utils +output_dir = keystone_utils/locale +input_file = keystone_utils/locale/keystone_utils.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = keystone_utils/locale/keystone_utils.pot diff --git a/orm/common/client/keystone/setup.py b/orm/common/client/keystone/setup.py new file mode 100644 index 00000000..2ccca3c1 --- /dev/null +++ b/orm/common/client/keystone/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup, find_packages + +setup( + name='keystone_utils', + version='0.1', + description='', + author='', + author_email='', + zip_safe=False, + include_package_data=True, + packages=find_packages(), + test_suite='keystone_utils/tests' + +) diff --git a/orm/common/client/keystone/test-requirements.txt b/orm/common/client/keystone/test-requirements.txt new file mode 100644 index 00000000..ee50a20b --- /dev/null +++ b/orm/common/client/keystone/test-requirements.txt @@ -0,0 +1,15 @@ +# 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. + +oslo.i18n==3.9.0 +oslo.serialization==2.13.0 +oslo.utils==3.16.0 +hacking<0.11,>=0.10.0 +mock<1.1.0,>=1.0 +coverage>=3.6 +python-subunit>=0.0.18 +sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 +testrepository>=0.0.18 +testscenarios==0.4 +testtools==1.4.0 diff --git a/orm/common/client/keystone/tox.ini b/orm/common/client/keystone/tox.ini new file mode 100644 index 00000000..8bdf31df --- /dev/null +++ b/orm/common/client/keystone/tox.ini @@ -0,0 +1,24 @@ +[tox] +envlist = py27,cover +skipsdist = True + +[testenv] +install_command = +# constraints: {[testenv:common-constraints]install_command} + pip install -U --force-reinstall {opts} {packages} +setenv = VIRTUAL_ENV={envdir} + OS_TEST_PATH=./keystone_utils/tests/unit + PYTHONPATH = {toxinidir}/mock_keystone/:/usr/local/lib/python2.7/dist-packages/ +deps = -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt + +[testenv:cover] +commands = + coverage erase + python setup.py testr --coverage + coverage report --omit="keystone_utils/tests/*" + coverage html --omit="keystone_utils/tests/*" + +[testenv:pep8] +commands= + py.test --pep8 -m pep8 diff --git a/orm/common/orm_common/__init__.py b/orm/common/orm_common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/extenal_mock/audit_client/__init__.py b/orm/common/orm_common/extenal_mock/audit_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/extenal_mock/audit_client/api/__init__.py b/orm/common/orm_common/extenal_mock/audit_client/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/extenal_mock/audit_client/api/audit.py b/orm/common/orm_common/extenal_mock/audit_client/api/audit.py new file mode 100644 index 00000000..ec483bdd --- /dev/null +++ b/orm/common/orm_common/extenal_mock/audit_client/api/audit.py @@ -0,0 +1,6 @@ +def audit(*args, **kwargs): + pass + + +def init(*args, **kwargs): + pass diff --git a/orm/common/orm_common/extenal_mock/keystone_utils/__init__.py b/orm/common/orm_common/extenal_mock/keystone_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/extenal_mock/keystone_utils/tokens.py b/orm/common/orm_common/extenal_mock/keystone_utils/tokens.py new file mode 100644 index 00000000..2cc046ea --- /dev/null +++ b/orm/common/orm_common/extenal_mock/keystone_utils/tokens.py @@ -0,0 +1,2 @@ +def get_token_user(*a, **k): + pass diff --git a/orm/common/orm_common/hooks/__init__.py b/orm/common/orm_common/hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/hooks/api_error_hook.py b/orm/common/orm_common/hooks/api_error_hook.py new file mode 100755 index 00000000..9cddff0c --- /dev/null +++ b/orm/common/orm_common/hooks/api_error_hook.py @@ -0,0 +1,70 @@ +import json +import logging +from pecan.hooks import PecanHook + +from orm_common.utils import api_error_utils as err_utils + +logger = logging.getLogger(__name__) + + +class APIErrorHook(PecanHook): + + def after(self, state): + # Handle http errors. reformat returned body and header. + status_code = state.response.status_code + + transaction_id = str(getattr(state.request, + 'transaction_id', + 'N/A')) + tracking_id = str(getattr(state.request, + 'tracking_id', + 'N/A')) + + result_json = {} + if 400 <= status_code <= 500: + + if status_code == 401: + result_json = err_utils.get_error_dict(401, + transaction_id, + None) + + else: + dict_body = None + try: + logger.debug('error: {}'.format(state.response)) + dict_body = json.loads(state.response.body) + if 'line' in str(state.response.body) and 'column' in str( + state.response.body): + result_json = dict_body + status_code = 400 + if 'faultstring' in dict_body: + result_json = err_utils.get_error_dict(status_code, + transaction_id, + dict_body['faultstring'], + "") + else: + result_json = json.loads(dict_body['faultstring']) + logger.debug('Received faultstring: {}'.format(result_json)) + # make sure status code in header and in body are the same + if 'code' in result_json: + status_code = result_json['code'] + + logger.info('Received status code: {}, transaction_id: {}, tracking_id: {}'. + format(status_code, transaction_id, tracking_id)) + + except ValueError: + msg = 'Could not read faultstring from response body!' + logger.error('{} {}'.format(msg, state.response.body)) + if 'faultstring' in state.response.headers: + msg = state.response.headers['faultstring'] + elif dict_body and 'faultstring' in dict_body: + msg = dict_body['faultstring'] + + result_json = err_utils.get_error_dict(status_code, + transaction_id, + msg, + "") + + setattr(state.response, 'body', json.dumps(result_json)) + state.response.status_code = status_code + state.response.headers.add('X-RANGER-Request-Id', tracking_id) diff --git a/orm/common/orm_common/hooks/security_headers_hook.py b/orm/common/orm_common/hooks/security_headers_hook.py new file mode 100755 index 00000000..75b36e00 --- /dev/null +++ b/orm/common/orm_common/hooks/security_headers_hook.py @@ -0,0 +1,18 @@ +import logging +from pecan.hooks import PecanHook + +logger = logging.getLogger(__name__) + + +class SecurityHeadersHook(PecanHook): + def after(self, state): + security_headers = {'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', + 'Content-Security-Policy': 'default-src \'self\'', + 'X-Permitted-Cross-Domain-Policies': 'none', + 'X-XSS-Protection': '1; mode=block'} + + # Add all the security headers + for header, value in security_headers.items(): + state.response.headers.add(header, value) diff --git a/orm/common/orm_common/hooks/transaction_id_hook.py b/orm/common/orm_common/hooks/transaction_id_hook.py new file mode 100755 index 00000000..a5780b2b --- /dev/null +++ b/orm/common/orm_common/hooks/transaction_id_hook.py @@ -0,0 +1,17 @@ +from pecan import abort +from pecan.hooks import PecanHook +from orm_common.utils import utils + + +class TransactionIdHook(PecanHook): + + def before(self, state): + try: + transaction_id = utils.make_transid() + except Exception as exc: + abort(500, headers={'faultstring': exc.message}) + + tracking_id = state.request.headers['X-RANGER-Tracking-Id'] \ + if 'X-RANGER-Tracking-Id' in state.request.headers else transaction_id + setattr(state.request, 'transaction_id', transaction_id) + setattr(state.request, 'tracking_id', tracking_id) diff --git a/orm/common/orm_common/injector/__init__.py b/orm/common/orm_common/injector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/injector/fang/__init__.py b/orm/common/orm_common/injector/fang/__init__.py new file mode 100755 index 00000000..18404b50 --- /dev/null +++ b/orm/common/orm_common/injector/fang/__init__.py @@ -0,0 +1,7 @@ +''' +''' + +from .di import Di +from .dependency_register import DependencyRegister +from .resource_provider_register import ResourceProviderRegister +from .resolver import DependencyResolver diff --git a/orm/common/orm_common/injector/fang/dependency_register.py b/orm/common/orm_common/injector/fang/dependency_register.py new file mode 100755 index 00000000..7b4d7093 --- /dev/null +++ b/orm/common/orm_common/injector/fang/dependency_register.py @@ -0,0 +1,77 @@ +from functools import partial +import inspect + +from .errors import DependentNotFoundError + +try: + import click +except ImportError: + click = None + + +class DependencyRegister: + def __init__(self): + # Maps dependents to names of resources they require + self.dependents = {} + # Maps names of resources to their dependents + self.resources = {} + + @classmethod + def _unwrap_func(cls, decorated_func): + ''' + This unwraps a decorated func, returning the inner wrapped func. + + This may become unnecessary with Python 3.4's inspect.unwrap(). + ''' + if click is not None: + # Workaround for click.command() decorator not setting + # __wrapped__ + if isinstance(decorated_func, click.Command): + return cls._unwrap_func(decorated_func.callback) + + if hasattr(decorated_func, '__wrapped__'): + # Recursion: unwrap more if needed + return cls._unwrap_func(decorated_func.__wrapped__) + else: + # decorated_func isn't actually decorated, no more + # unwrapping to do + return decorated_func + + @classmethod + def _unwrap_dependent(cls, dependent): + # Dependent is effectively a class. Classes are registered as is. + if inspect.isclass(dependent): + return dependent + # dependent is some other kind of callable, eg a function + else: + return cls._unwrap_func(dependent) + + def _register_dependent(self, dependent, resource_name): + if dependent not in self.dependents: + self.dependents[dependent] = [] + self.dependents[dependent].insert(0, resource_name) + + def _register_resource_dependency(self, resource_name, dependent): + if resource_name not in self.resources: + self.resources[resource_name] = set() + self.resources[resource_name].add(dependent) + + def register(self, resource_name, dependent=None): + if dependent is None: + # Give a partial usable as a decorator + return partial(self.register, resource_name) + + dependent = self._unwrap_dependent(dependent) + self._register_dependent(dependent, resource_name) + self._register_resource_dependency(resource_name, dependent) + + # Return dependent to ease use as decorator + return dependent + + def query_resources(self, dependent): + dependent = self._unwrap_dependent(dependent) + + if dependent not in self.dependents: + raise DependentNotFoundError(dependent=dependent) + + return self.dependents[dependent] diff --git a/orm/common/orm_common/injector/fang/di.py b/orm/common/orm_common/injector/fang/di.py new file mode 100755 index 00000000..6a436354 --- /dev/null +++ b/orm/common/orm_common/injector/fang/di.py @@ -0,0 +1,16 @@ +from .dependency_register import DependencyRegister +from .resource_provider_register import ResourceProviderRegister +from .resolver import DependencyResolver + + +class Di: + def __init__(self, namespace=None): + self.namespace = namespace + self.dependencies = DependencyRegister() + self.providers = ResourceProviderRegister() + self.resolver = DependencyResolver( + dependency_register=self.dependencies, + resource_provider_register=self.providers) + + # For use as a decorator + self.dependsOn = self.dependencies.register diff --git a/orm/common/orm_common/injector/fang/errors.py b/orm/common/orm_common/injector/fang/errors.py new file mode 100755 index 00000000..607cfb98 --- /dev/null +++ b/orm/common/orm_common/injector/fang/errors.py @@ -0,0 +1,47 @@ +class FangError(Exception): + pass + + +class DependentNotFoundError(FangError): + def __init__(self, dependent=None): + self.dependent = dependent + if dependent: + message = ( + "Couldn't find dependencies registered for {!r}" + "".format(dependent)) + else: + message = ( + "Couldn't find dependencies registered for the given " + "dependent") + super(DependentNotFoundError, self).__init__(message) + + +class ProviderAlreadyRegisteredError(FangError): + def __init__(self, resource_name=None, existing_provider=None): + self.resource_name = resource_name + self.existing_provider = existing_provider + if resource_name and existing_provider: + message = ( + 'A provider ({provider!r}) has already been ' + 'registered for resource {resource_name!r}'.format( + provider=existing_provider, + resource_name=resource_name)) + else: + message = ( + 'A provider has already been registered for the ' + 'resource') + super(ProviderAlreadyRegisteredError, self).__init__(message) + + +class ProviderNotFoundError(FangError): + def __init__(self, resource_name=None): + self.resource_name = resource_name + if resource_name: + message = ( + "A provider could not be found for resource {!r}" + "".format(resource_name)) + else: + message = ( + "A provider could not be found for the requested " + "resource") + super(ProviderNotFoundError, self).__init__(message) diff --git a/orm/common/orm_common/injector/fang/resolver.py b/orm/common/orm_common/injector/fang/resolver.py new file mode 100755 index 00000000..5a58f8d6 --- /dev/null +++ b/orm/common/orm_common/injector/fang/resolver.py @@ -0,0 +1,44 @@ +from .errors import ProviderNotFoundError + + +# This is effectively what is sometimes termed a "dependency injection +# container". +class DependencyResolver: + def __init__( + self, + dependency_register=None, + resource_provider_register=None): + self.dependency_register = dependency_register + self.resource_provider_register = resource_provider_register + + # Methods delegated to other objects + self.query_dependents_resources = \ + self.dependency_register.query_resources + self.resolve = self.resource_provider_register.resolve + + def resolve_all_dependencies(self, dependent): + return [ + self.resolve(resource_name) + for resource_name in + self.query_dependents_resources(dependent)] + + def unpack(self, dependent): + resources = self.resolve_all_dependencies(dependent) + + # Never return a length-1 list/tuple, to allow easier unpacking + # eg, avoid need for comma in: + # my_one_dep, = my_resolver.unpack_dependencies(my_func) + if len(resources) == 1: + return resources[0] + else: + return resources + + def are_all_dependencies_met_for(self, dependent): + for resource_name in self.query_dependents_resources(dependent): + try: + self.resolve(resource_name) + except ProviderNotFoundError as e: + # TODO: Add error logging here + return False + else: + return True diff --git a/orm/common/orm_common/injector/fang/resource_provider_register.py b/orm/common/orm_common/injector/fang/resource_provider_register.py new file mode 100755 index 00000000..a9d6dbdf --- /dev/null +++ b/orm/common/orm_common/injector/fang/resource_provider_register.py @@ -0,0 +1,68 @@ +from functools import partial + +from .errors import ( + FangError, + ProviderAlreadyRegisteredError, + ProviderNotFoundError) + + +class ResourceProviderRegister: + def __init__(self, namespace=None): + self.namespace = namespace + # Maps resource names to a provider + self.resource_providers = {} + + def register(self, resource_name, provider=None, allow_override=False): + if provider is None: + # Give a partial usable as a decorator + return partial( + self.register, + resource_name, allow_override=allow_override) + + if (not allow_override) and resource_name in self.resource_providers: + raise ProviderAlreadyRegisteredError( + resource_name=resource_name, + existing_provider=self.resource_providers[resource_name]) + + self.resource_providers[resource_name] = provider + + # Return provider to ease use as decorator + return provider + + register_callable = register + + # For registering providers which always return the same instance + def register_instance(self, resource_name, instance=None, **kwargs): + if instance is None: + # Give a partial usable as a decorator + return partial(self.register_instance, resource_name, **kwargs) + + self.register(resource_name, provider=(lambda: instance), **kwargs) + return instance + + def mass_register(self, resource_names_to_providers, **kwargs): + for resource_name, provider in resource_names_to_providers.items(): + self.register_instance(resource_name, provider, **kwargs) + + def load(self, other_register, allow_overrides=False): + if not allow_overrides: + own_keys = self.resource_providers.keys() + other_keys = other_register.resource_providers.keys() + common_keys = own_keys & other_keys + if common_keys: + # TODO Add new FangError sub-class? + raise FangError( + 'This register already has providers for keys: ' + '{!r}'.format(common_keys)) + + self.resource_providers.update( + other_register.resource_providers) + + def clear(self): + self.resource_providers.clear() + + def resolve(self, resource_name): + if resource_name not in self.resource_providers: + raise ProviderNotFoundError(resource_name=resource_name) + + return self.resource_providers[resource_name]() diff --git a/orm/common/orm_common/injector/injector.py b/orm/common/orm_common/injector/injector.py new file mode 100755 index 00000000..8babfdb4 --- /dev/null +++ b/orm/common/orm_common/injector/injector.py @@ -0,0 +1,59 @@ +from orm_common.injector import fang +from orm_common.utils.sanitize import sanitize_symbol_name + +import os +import imp + +_di = fang.Di() +logger = None + + +def register_providers(env_variable, providers_dir_path, _logger): + global logger + logger = _logger + + # TODO: change all prints to logger + logger.info('Initializing dependency injector') + logger.info('Checking {0} variable'.format(env_variable)) + + env = None + if not (env_variable in os.environ): + logger.warn('No {0} variable found using `prod` injector'.format(env_variable)) + env = 'prod' + elif os.environ[env_variable] == '__TEST__': + logger.info('__TEST__ variable found, explicitly skipping provider registration!!!') + return + else: + env = os.environ[env_variable] + log_message = '{0} found setting injector to {1} environment'.format(sanitize_symbol_name(env_variable), env) + log_message = log_message.replace('\n', '_').replace('\r', '_') + logger.info(log_message) + + logger.info('Setting injector providers') + + module = _import_file_by_name(env, providers_dir_path) + + for provider in module.providers: + logger.info('Setting provider `{0}` to {1}'.format(provider[0], provider[1])) + _di.providers.register_instance(provider[0], provider[1]) + + +def get_di(): + return _di + + +def override_injected_dependency(dep_tuple): + _di.providers.register_instance(dep_tuple[0], dep_tuple[1], allow_override=True) + + +def _import_file_by_name(env, providers_dir_path): + file_path = os.path.join(providers_dir_path, '{0}_providers.py'.format(env)) + try: + module = imp.load_source('fms_providers', file_path) + except IOError as ex: + logger.log_exception( + 'File with providers for the {0} environment, path: {1} wasnt found! Crushing!!!'.format(env, file_path), + ex) + raise ex + + return module diff --git a/orm/common/orm_common/policy/__init__.py b/orm/common/orm_common/policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/policy/_checks.py b/orm/common/orm_common/policy/_checks.py new file mode 100755 index 00000000..22fef170 --- /dev/null +++ b/orm/common/orm_common/policy/_checks.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2015 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import logging + +import six +from orm_common.utils import api_error_utils as err_utils +from orm_common.utils import dictator + + +logger = logging.getLogger(__name__) + +registered_checks = {} + + +@six.add_metaclass(abc.ABCMeta) +class BaseCheck(object): + """Abstract base class for Check classes.""" + + @abc.abstractmethod + def __str__(self): + """String representation of the Check tree rooted at this node.""" + + pass + + @abc.abstractmethod + def __call__(self, target, cred, enforcer): + """Triggers if instance of the class is called. + + Performs the check. Returns False to reject the access or a + true value (not necessary True) to accept the access. + """ + + pass + + +class FalseCheck(BaseCheck): + """A policy check that always returns ``False`` (disallow).""" + + def __str__(self): + """Return a string representation of this check.""" + + return '!' + + def __call__(self, target, cred, enforcer): + """Check the policy.""" + + logger.debug('False check, never passing') + return False + + +class TrueCheck(BaseCheck): + """A policy check that always returns ``True`` (allow).""" + + def __str__(self): + """Return a string representation of this check.""" + + return '@' + + def __call__(self, target, cred, enforcer): + """Check the policy.""" + + logger.debug('True check, always passing') + return True + + +class Check(BaseCheck): + def __init__(self, kind, match): + self.kind = kind + self.match = match + + def __str__(self): + """Return a string representation of this check.""" + + return '%s:%s' % (self.kind, self.match) + + +class NotCheck(BaseCheck): + def __init__(self, rule): + self.rule = rule + + def __str__(self): + """Return a string representation of this check.""" + + return 'not %s' % self.rule + + def __call__(self, target, cred, enforcer): + """Check the policy. + + Returns the logical inverse of the wrapped check. + """ + + return not self.rule(target, cred, enforcer) + + +class AndCheck(BaseCheck): + def __init__(self, rules): + self.rules = rules + + def __str__(self): + """Return a string representation of this check.""" + + return '(%s)' % ' and '.join(str(r) for r in self.rules) + + def __call__(self, target, cred, enforcer): + """Check the policy. + + Requires that all rules accept in order to return True. + """ + + for rule in self.rules: + if not rule(target, cred, enforcer): + return False + + return True + + def add_check(self, rule): + """Adds rule to be tested. + + Allows addition of another rule to the list of rules that will + be tested. + + :returns: self + :rtype: :class:`.AndCheck` + """ + + self.rules.append(rule) + return self + + +class OrCheck(BaseCheck): + def __init__(self, rules): + self.rules = rules + + def __str__(self): + """Return a string representation of this check.""" + + return '(%s)' % ' or '.join(str(r) for r in self.rules) + + def __call__(self, target, cred, enforcer): + """Check the policy. + + Requires that at least one rule accept in order to return True. + """ + + for rule in self.rules: + if rule(target, cred, enforcer): + return True + return False + + def add_check(self, rule): + """Adds rule to be tested. + + Allows addition of another rule to the list of rules that will + be tested. Returns the OrCheck object for convenience. + """ + + self.rules.append(rule) + return self + + def pop_check(self): + """Pops the last check from the list and returns them + + :returns: self, the popped check + :rtype: :class:`.OrCheck`, class:`.Check` + """ + + check = self.rules.pop() + return self, check + + +def register(name, func=None): + # Perform the actual decoration by registering the function or + # class. Returns the function or class for compliance with the + # decorator interface. + def decorator(func): + registered_checks[name] = func + return func + + # If the function or class is given, do the registration + if func: + return decorator(func) + + return decorator + + +@register('rule') +class RuleCheck(Check): + def __call__(self, target, creds, enforcer): + try: + return enforcer.rules[self.match](target, creds, enforcer) + except KeyError: + # We don't have any matching rule; fail closed + return False + + +@register('role') +class RoleCheck(Check): + """Check that there is a matching role in the ``user`` object.""" + + def __call__(self, target, user, enforcer): + try: + logger.debug('Checking role:{}'.format(self.match)) + result = any( + [role['name'] == self.match for role in user.user['roles']]) + logger.debug('Role check result: {}'.format(result)) + if not result: + logger.info( + 'INFO|CON{}AUTH001|Not allowed to perform this operation,' + ' user:{} does not have role:{}'.format( + dictator.get('service_name', 'ORM'), + user.user['name'], self.match)) + raise err_utils.get_error('N/A', status_code=403) + return result + except Exception: + logger.debug('Invalid user, failing role check') + raise + + +@register('user') +class UserCheck(Check): + """Check that the user matches.""" + + def __call__(self, target, user, enforcer): + try: + logger.debug('Checking user:{}'.format(self.match)) + result = user.user['name'] == self.match + logger.debug('User check result: {}'.format(result)) + if not result: + logger.info( + 'INFO|CON{}AUTH002|Not allowed to perform this operation,' + ' user:{} is not the user:{}'.format( + dictator.get('service_name', 'ORM'), + user.user['name'], self.match)) + raise err_utils.get_error('N/A', status_code=403) + return result + except Exception: + logger.debug('Invalid user, failing user check') + raise + + +@register('tenant') +class TenantCheck(Check): + """Check that the user's tenant matches.""" + + def __call__(self, target, user, enforcer): + try: + logger.debug('Checking tenant:{}'.format(self.match)) + result = user.tenant['name'] == self.match + logger.debug('Tenant check result: {}'.format(result)) + if not result: + logger.info( + 'INFO|CON{}AUTH003|Not allowed to perform this operation,' + ' user:{} is not in tenant:{}'.format( + dictator.get('service_name', 'ORM'), + user.user['name'], self.match)) + return result + except Exception: + logger.debug('Invalid user, failing tenant check') + return False + + +@register('domain') +class DomainCheck(Check): + """Check that the user's domain matches.""" + + def __call__(self, target, user, enforcer): + try: + logger.debug('Checking domain:{}'.format(self.match)) + result = user.domain['name'] == self.match + logger.debug('Domain check result: {}'.format(result)) + return result + except Exception: + logger.debug('Invalid user, failing domain check') + return False + + +@register(None) +class GenericCheck(Check): + """Check an individual match. + + Matches look like: + + - tenant:%(tenant_id)s + - role:compute:admin + - True:%(user.enabled)s + - 'Member':%(role.name)s + """ + + def __call__(self, target, creds, enforcer): + # We do not want anything besides role, tenant and domain + logger.debug('Received an unknown check!') + return False diff --git a/orm/common/orm_common/policy/_parser.py b/orm/common/orm_common/policy/_parser.py new file mode 100755 index 00000000..5ef04d26 --- /dev/null +++ b/orm/common/orm_common/policy/_parser.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2015 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import re + +import six + +import _checks + +from oslo_policy._i18n import _LE + + +LOG = logging.getLogger(__name__) + + +def reducer(*tokens): + """Decorator for reduction methods. + + Arguments are a sequence of tokens, in order, which should trigger running + this reduction method. + """ + + def decorator(func): + # Make sure we have a list of reducer sequences + if not hasattr(func, 'reducers'): + func.reducers = [] + + # Add the tokens to the list of reducer sequences + func.reducers.append(list(tokens)) + + return func + + return decorator + + +class ParseStateMeta(type): + """Metaclass for the :class:`.ParseState` class. + + Facilitates identifying reduction methods. + """ + + def __new__(mcs, name, bases, cls_dict): + """Create the class. + + Injects the 'reducers' list, a list of tuples matching token sequences + to the names of the corresponding reduction methods. + """ + + reducers = [] + + for key, value in cls_dict.items(): + if not hasattr(value, 'reducers'): + continue + for reduction in value.reducers: + reducers.append((reduction, key)) + + cls_dict['reducers'] = reducers + + return super(ParseStateMeta, mcs).__new__(mcs, name, bases, cls_dict) + + +@six.add_metaclass(ParseStateMeta) +class ParseState(object): + """Implement the core of parsing the policy language. + + Uses a greedy reduction algorithm to reduce a sequence of tokens into + a single terminal, the value of which will be the root of the + :class:`Check` tree. + + .. note:: + + Error reporting is rather lacking. The best we can get with this + parser formulation is an overall "parse failed" error. Fortunately, the + policy language is simple enough that this shouldn't be that big a + problem. + """ + + def __init__(self): + """Initialize the ParseState.""" + + self.tokens = [] + self.values = [] + + def reduce(self): + """Perform a greedy reduction of the token stream. + + If a reducer method matches, it will be executed, then the + :meth:`reduce` method will be called recursively to search for any more + possible reductions. + """ + + for reduction, methname in self.reducers: + if (len(self.tokens) >= len(reduction) and + self.tokens[-len(reduction):] == reduction): + # Get the reduction method + meth = getattr(self, methname) + + # Reduce the token stream + results = meth(*self.values[-len(reduction):]) + + # Update the tokens and values + self.tokens[-len(reduction):] = [r[0] for r in results] + self.values[-len(reduction):] = [r[1] for r in results] + + # Check for any more reductions + return self.reduce() + + def shift(self, tok, value): + """Adds one more token to the state. + + Calls :meth:`reduce`. + """ + + self.tokens.append(tok) + self.values.append(value) + + # Do a greedy reduce... + self.reduce() + + @property + def result(self): + """Obtain the final result of the parse. + + :raises ValueError: If the parse failed to reduce to a single result. + """ + + if len(self.values) != 1: + raise ValueError('Could not parse rule') + return self.values[0] + + @reducer('(', 'check', ')') + @reducer('(', 'and_expr', ')') + @reducer('(', 'or_expr', ')') + def _wrap_check(self, _p1, check, _p2): + """Turn parenthesized expressions into a 'check' token.""" + + return [('check', check)] + + @reducer('check', 'and', 'check') + def _make_and_expr(self, check1, _and, check2): + """Create an 'and_expr'. + + Join two checks by the 'and' operator. + """ + + return [('and_expr', _checks.AndCheck([check1, check2]))] + + @reducer('or_expr', 'and', 'check') + def _mix_or_and_expr(self, or_expr, _and, check): + """Modify the case 'A or B and C'""" + + or_expr, check1 = or_expr.pop_check() + if isinstance(check1, _checks.AndCheck): + and_expr = check1 + and_expr.add_check(check) + else: + and_expr = _checks.AndCheck([check1, check]) + return [('or_expr', or_expr.add_check(and_expr))] + + @reducer('and_expr', 'and', 'check') + def _extend_and_expr(self, and_expr, _and, check): + """Extend an 'and_expr' by adding one more check.""" + + return [('and_expr', and_expr.add_check(check))] + + @reducer('check', 'or', 'check') + @reducer('and_expr', 'or', 'check') + def _make_or_expr(self, check1, _or, check2): + """Create an 'or_expr'. + + Join two checks by the 'or' operator. + """ + + return [('or_expr', _checks.OrCheck([check1, check2]))] + + @reducer('or_expr', 'or', 'check') + def _extend_or_expr(self, or_expr, _or, check): + """Extend an 'or_expr' by adding one more check.""" + + return [('or_expr', or_expr.add_check(check))] + + @reducer('not', 'check') + def _make_not_expr(self, _not, check): + """Invert the result of another check.""" + + return [('check', _checks.NotCheck(check))] + + +def _parse_check(rule): + """Parse a single base check rule into an appropriate Check object.""" + + # Handle the special checks + if rule == '!': + return _checks.FalseCheck() + elif rule == '@': + return _checks.TrueCheck() + + try: + kind, match = rule.split(':', 1) + except Exception: + LOG.exception(_LE('Failed to understand rule %s'), rule) + # If the rule is invalid, we'll fail closed + return _checks.FalseCheck() + + # Find what implements the check + if kind in _checks.registered_checks: + return _checks.registered_checks[kind](kind, match) + elif None in _checks.registered_checks: + return _checks.registered_checks[None](kind, match) + else: + LOG.error(_LE('No handler for matches of kind %s'), kind) + return _checks.FalseCheck() + + +def _parse_list_rule(rule): + """Translates the old list-of-lists syntax into a tree of Check objects. + + Provided for backwards compatibility. + """ + + # Empty rule defaults to True + if not rule: + return _checks.TrueCheck() + + # Outer list is joined by "or"; inner list by "and" + or_list = [] + for inner_rule in rule: + # Skip empty inner lists + if not inner_rule: + continue + + # Handle bare strings + if isinstance(inner_rule, six.string_types): + inner_rule = [inner_rule] + + # Parse the inner rules into Check objects + and_list = [_parse_check(r) for r in inner_rule] + + # Append the appropriate check to the or_list + if len(and_list) == 1: + or_list.append(and_list[0]) + else: + or_list.append(_checks.AndCheck(and_list)) + + # If we have only one check, omit the "or" + if not or_list: + return _checks.FalseCheck() + elif len(or_list) == 1: + return or_list[0] + + return _checks.OrCheck(or_list) + + +# Used for tokenizing the policy language +_tokenize_re = re.compile(r'\s+') + + +def _parse_tokenize(rule): + """Tokenizer for the policy language. + + Most of the single-character tokens are specified in the + _tokenize_re; however, parentheses need to be handled specially, + because they can appear inside a check string. Thankfully, those + parentheses that appear inside a check string can never occur at + the very beginning or end ("%(variable)s" is the correct syntax). + """ + + for tok in _tokenize_re.split(rule): + # Skip empty tokens + if not tok or tok.isspace(): + continue + + # Handle leading parens on the token + clean = tok.lstrip('(') + for i in range(len(tok) - len(clean)): + yield '(', '(' + + # If it was only parentheses, continue + if not clean: + continue + else: + tok = clean + + # Handle trailing parens on the token + clean = tok.rstrip(')') + trail = len(tok) - len(clean) + + # Yield the cleaned token + lowered = clean.lower() + if lowered in ('and', 'or', 'not'): + # Special tokens + yield lowered, clean + elif clean: + # Not a special token, but not composed solely of ')' + if len(tok) >= 2 and ((tok[0], tok[-1]) in + [('"', '"'), ("'", "'")]): + # It's a quoted string + yield 'string', tok[1:-1] + else: + yield 'check', _parse_check(clean) + + # Yield the trailing parens + for i in range(trail): + yield ')', ')' + + +def _parse_text_rule(rule): + """Parses policy to the tree. + + Translates a policy written in the policy language into a tree of + Check objects. + """ + + # Empty rule means always accept + if not rule: + return _checks.TrueCheck() + + # Parse the token stream + state = ParseState() + for tok, value in _parse_tokenize(rule): + state.shift(tok, value) + + try: + return state.result + except ValueError: + # Couldn't parse the rule + LOG.exception(_LE('Failed to understand rule %s'), rule) + + # Fail closed + return _checks.FalseCheck() + + +def parse_rule(rule): + """Parses a policy rule into a tree of :class:`.Check` objects.""" + + # If the rule is a string, it's in the policy language + if isinstance(rule, six.string_types): + return _parse_text_rule(rule) + return _parse_list_rule(rule) diff --git a/orm/common/orm_common/policy/policy.py b/orm/common/orm_common/policy/policy.py new file mode 100755 index 00000000..72c48200 --- /dev/null +++ b/orm/common/orm_common/policy/policy.py @@ -0,0 +1,184 @@ +"""Policy Engine For ORM.""" + +import logging + +from keystone_utils import tokens +from orm_common.utils import api_error_utils as err_utils +from orm_common.utils import dictator +from wsme.exc import ClientSideError + + +import qolicy + +logger = logging.getLogger(__name__) +_ENFORCER = None +_POLICY_FILE = None +_TOKEN_CONF = None + + +def reset(): + global _ENFORCER + if _ENFORCER: + _ENFORCER.clear() + _ENFORCER = None + + global _POLICY_FILE + if _POLICY_FILE: + _POLICY_FILE = None + + +class EnforcerError(Exception): + """An exception that receives *args and **kwargs, necessary for + Enforcer.enforce(). + """ + def __init__(self, *args, **kwargs): + super(EnforcerError, self).__init__() + + +def _get_rules_from_file(path): + logger.debug('Reading policy file: {}'.format(path)) + + return qolicy.Rules.load_json(open(path, 'r').read(), 'default') + + +def init(policy_file, token_conf, default_rule=None): + """Init an Enforcer class. + + :param policy_file: Custom policy file to use. + :param default_rule: Default rule to use + :param token_conf: The Keystone utils token configuration + """ + logger.info('Initializing policy enforcer...') + + global _ENFORCER + global _POLICY_FILE + global _TOKEN_CONF + if not _ENFORCER: + loaded_rules = _get_rules_from_file(policy_file) + _POLICY_FILE = policy_file + _TOKEN_CONF = token_conf + _ENFORCER = qolicy.Enforcer(None, + policy_file=None, + rules=loaded_rules, + default_rule=default_rule, + use_conf=False) + + +def reset_rules(overwrite=True, use_conf=False): + """Reset rules based on the provided dict of rules. + + :param rules: New rules to use. It should be an instance of dict. + :param overwrite: Whether to overwrite current rules or update them + with the new rules. + :param use_conf: Whether to reload rules from config file. + """ + if not _POLICY_FILE: + message = 'Policy file not set (did you call policy.init?)' + logger.error(message) + raise ValueError(message) + _ENFORCER.set_rules(_get_rules_from_file(_POLICY_FILE), + overwrite, use_conf) + + +def enforce(action, token, user, lcp_id=None, keystone_ep=None, + do_raise=True): + """Verifies that the action is valid on the target in this context. + + :param action: string representing the action to be checked + this should be colon separated for clarity. + i.e. ``compute:create_instance``, + ``compute:attach_volume``, + ``volume:attach_volume`` + :param token: The token to validate + :param lcp_id: The ID of the LCP associated with the Keystone instance + with which the token was created + :param keystone_ep: The Keystone endpoint, in case we already have it + :param do_raise: if True (the default), raises Unauthorized (401); + if False, returns False + + :raises EnforcerError if verification fails and do_raise is True. + + :return: returns a non-False value (not necessarily "True") if + authorized, and the exact value False if not authorized and + do_raise is False. + """ + logger.debug('Enforcing policy - action: {}, token: {}, lcp_id: {}, ' + 'keystone_ep: {}'.format(action, token, lcp_id, keystone_ep)) + # Re-read the rules, in case the policy file has changed + reset_rules() + + # May raise EnforcerError, we'll let it propagate + result = _ENFORCER.enforce(action, {}, user, do_raise=do_raise, + exc=EnforcerError, action=action) + + return result + + +def _is_authorization_enabled(app_conf): + return app_conf.authentication.enabled + + +def authorize(action, request, app_conf, keystone_ep=None): + """Authorize a request. + + :param action: The requested action, in the policy.json syntax + :param request: Pecan request object + :param app_conf: Application configuration + :param keystone_ep: Keystone endpoint, in case we already have it + + :raises Unauthorized (401) in case anything fails in the authorization + process + """ + logger.info('Authorize...start') + + token_to_validate = request.headers.get('X-Auth-Token') + lcp_id = request.headers.get('X-Auth-Region') + try: + if _is_authorization_enabled(app_conf): + try: + # Set the service name for Nagios codes + dictator.soft_set('service_name', app_conf.server.name.upper()) + + user = tokens.get_token_user(token_to_validate, _TOKEN_CONF, + lcp_id, keystone_ep) + request.headers['X-RANGER-Client'] = user.user['name'] + request.headers['X-RANGER-Owner'] = user.tenant['id'] + except Exception: + user = None + request.headers['X-RANGER-Client'] = 'NA' + logger.exception( + "policy - Failed to get_token_user, using user={}".format( + user)) + + if token_to_validate is not None and lcp_id is not None and str(token_to_validate).strip() != '' and str(lcp_id).strip() != '': + logger.debug('Authorization: enforcing policy on token=[{}], lcp_id=[{}]'.format(token_to_validate, lcp_id)) + enforce(action, token_to_validate, user, lcp_id, keystone_ep) + is_permitted = True + logger.debug('Authorization: policy check passed') + else: + logger.debug('Token=[{}] and/or Region=[{}] are empty/none.'.format(token_to_validate, lcp_id)) + logger.info( + 'INFO|CON{}AUTH004|One or more of the authentication headers are missing'.format( + dictator.get('service_name', 'ORM'))) + # Enforce anyway, in case the policy for this is to always + # allow any user to perform this operation + enforce(action, token_to_validate, user) + is_permitted = True + else: + logger.debug('The authentication service is disabled. No authentication is needed.') + is_permitted = True + except ClientSideError as e: + logger.error('Fail to validate request. due to {}.'.format(e.message)) + raise err_utils.get_error('N/A', status_code=e.code) + except EnforcerError: + logger.error('The token is unauthorized according to the policy') + is_permitted = False + except Exception as e: + msg = 'Fail to validate request. due to {}.'.format(e.message) + logger.error(msg) + logger.exception(e) + is_permitted = False + + logger.info('Authorize...end') + if not is_permitted: + raise err_utils.get_error('N/A', status_code=401) diff --git a/orm/common/orm_common/policy/qolicy.py b/orm/common/orm_common/policy/qolicy.py new file mode 100755 index 00000000..5afc9700 --- /dev/null +++ b/orm/common/orm_common/policy/qolicy.py @@ -0,0 +1,536 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2012 OpenStack Foundation. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Common Policy Engine Implementation + +Policies are expressed as a target and an associated rule:: + + "": + +The `target` is specific to the service that is conducting policy +enforcement. Typically, the target refers to an API call. + +For the `` part, see `Policy Rule Expressions`. + +Policy Rule Expressions +~~~~~~~~~~~~~~~~~~~~~~~ + +Policy rules can be expressed in one of two forms: a string written in the new +policy language or a list of lists. The string format is preferred since it's +easier for most people to understand. + +In the policy language, each check is specified as a simple "a:b" pair that is +matched to the correct class to perform that check: + + +--------------------------------+------------------------------------------+ + | TYPE | SYNTAX | + +================================+==========================================+ + |User's Role | role:admin | + +--------------------------------+------------------------------------------+ + |Rules already defined on policy | rule:admin_required | + +--------------------------------+------------------------------------------+ + |Against URLs¹ | http://my-url.org/check | + +--------------------------------+------------------------------------------+ + |User attributes² | project_id:%(target.project.id)s | + +--------------------------------+------------------------------------------+ + |Strings | - :'xpto2035abc' | + | | - 'myproject': | + +--------------------------------+------------------------------------------+ + | | - project_id:xpto2035abc | + |Literals | - domain_id:20 | + | | - True:%(user.enabled)s | + +--------------------------------+------------------------------------------+ + +¹URL checking must return ``True`` to be valid + +²User attributes (obtained through the token): user_id, domain_id or project_id + +Conjunction operators ``and`` and ``or`` are available, allowing for more +expressiveness in crafting policies. For example:: + + "role:admin or (project_id:%(project_id)s and role:projectadmin)" + +The policy language also has the ``not`` operator, allowing a richer +policy rule:: + + "project_id:%(project_id)s and not role:dunce" + +Operator precedence is below: + + +------------+-------------+-------------+ + | PRECEDENCE | TYPE | EXPRESSION | + +============+=============+=============+ + | 4 | Grouping | (...) | + +------------+-------------+-------------+ + | 3 | Logical NOT | not ... | + +------------+-------------+-------------+ + | 2 | Logical AND | ... and ... | + +------------+-------------+-------------+ + | 1 | Logical OR | ... or ... | + +------------+-------------+-------------+ + +Operator with larger precedence number precedes others with smaller numbers. + +In the list-of-lists representation, each check inside the innermost +list is combined as with an "and" conjunction -- for that check to pass, +all the specified checks must pass. These innermost lists are then +combined as with an "or" conjunction. As an example, take the following +rule, expressed in the list-of-lists representation:: + + [["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]] + +Finally, two special policy checks should be mentioned; the policy +check "@" will always accept an access, and the policy check "!" will +always reject an access. (Note that if a rule is either the empty +list (``[]``) or the empty string (``""``), this is equivalent to the "@" +policy check.) Of these, the "!" policy check is probably the most useful, +as it allows particular rules to be explicitly disabled. + +Generic Checks +~~~~~~~~~~~~~~ + +A `generic` check is used to perform matching against attributes that are sent +along with the API calls. These attributes can be used by the policy engine +(on the right side of the expression), by using the following syntax:: + + :%(user.id)s + +The value on the right-hand side is either a string or resolves to a +string using regular Python string substitution. The available attributes +and values are dependent on the program that is using the common policy +engine. + +All of these attributes (related to users, API calls, and context) can be +checked against each other or against constants. It is important to note +that these attributes are specific to the service that is conducting +policy enforcement. + +Generic checks can be used to perform policy checks on the following user +attributes obtained through a token: + + - user_id + - domain_id or project_id (depending on the token scope) + - list of roles held for the given token scope + +For example, a check on the user_id would be defined as:: + + user_id: + +Together with the previously shown example, a complete generic check +would be:: + + user_id:%(user.id)s + +It is also possible to perform checks against other attributes that +represent the credentials. This is done by adding additional values to +the ``creds`` dict that is passed to the +:meth:`~oslo_policy.policy.Enforcer.enforce` method. + +Special Checks +~~~~~~~~~~~~~~ + +Special checks allow for more flexibility than is possible using generic +checks. The built-in special check types are ``role``, ``rule``, and ``http`` +checks. + +Role Check +^^^^^^^^^^ + +A ``role`` check is used to check if a specific role is present in the supplied +credentials. A role check is expressed as:: + + "role:" + +Rule Check +^^^^^^^^^^ + +A :class:`rule check ` is used to +reference another defined rule by its name. This allows for common +checks to be defined once as a reusable rule, which is then referenced +within other rules. It also allows one to define a set of checks as a +more descriptive name to aid in readability of policy. A rule check is +expressed as:: + + "rule:" + +The following example shows a role check that is defined as a rule, +which is then used via a rule check:: + + "admin_required": "role:admin" + "": "rule:admin_required" + +HTTP Check +^^^^^^^^^^ + +An ``http`` check is used to make an HTTP request to a remote server to +determine the results of the check. The target and credentials are passed to +the remote server for evaluation. The action is authorized if the remote +server returns a response of ``True``. An http check is expressed as:: + + "http:" + +It is expected that the target URI contains a string formatting keyword, +where the keyword is a key from the target dictionary. An example of an +http check where the `name` key from the target is used to construct the +URL is would be defined as:: + + "http://server.test/%(name)s" + +Registering New Special Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is also possible for additional special check types to be registered +using the :func:`~oslo_policy.policy.register` function. + +The following classes can be used as parents for custom special check types: + + * :class:`~oslo_policy.policy.AndCheck` + * :class:`~oslo_policy.policy.NotCheck` + * :class:`~oslo_policy.policy.OrCheck` + * :class:`~oslo_policy.policy.RuleCheck` + +Default Rule +~~~~~~~~~~~~ + +A default rule can be defined, which will be enforced when a rule does +not exist for the target that is being checked. By default, the rule +associated with the rule name of ``default`` will be used as the default +rule. It is possible to use a different rule name as the default rule +by setting the ``policy_default_rule`` configuration setting to the +desired rule name. +""" + +import logging +import os + +from oslo_config import cfg +from oslo_serialization import jsonutils +import six + +from oslo_policy import _checks +from oslo_policy._i18n import _ +from oslo_policy import opts + +import _parser + + +LOG = logging.getLogger(__name__) + + +register = _checks.register +"""Register a function or :class:`.Check` class as a policy check. + +:param name: Gives the name of the check type, e.g., "rule", + "role", etc. If name is ``None``, a default check type + will be registered. +:param func: If given, provides the function or class to register. + If not given, returns a function taking one argument + to specify the function or class to register, + allowing use as a decorator. +""" + +Check = _checks.Check +"""A base class to allow for user-defined policy checks. + +:param kind: The kind of the check, i.e., the field before the ``:``. +:param match: The match of the check, i.e., the field after the ``:``. + +""" + +AndCheck = _checks.AndCheck +"""Implements the "and" logical operator. + +A policy check that requires that a list of other checks all return True. + +:param list rules: rules that will be tested. + +""" + +NotCheck = _checks.NotCheck +"""Implements the "not" logical operator. + +A policy check that inverts the result of another policy check. + +:param rule: The rule to negate. +:type rule: oslo_policy.policy.Check + +""" + +OrCheck = _checks.OrCheck +"""Implements the "or" operator. + +A policy check that requires that at least one of a list of other +checks returns ``True``. + +:param rules: A list of rules that will be tested. + +""" + +RuleCheck = _checks.RuleCheck +"""Recursively checks credentials based on the defined rules.""" + + +class PolicyNotAuthorized(Exception): + """Default exception raised for policy enforcement failure.""" + + def __init__(self, rule, target, creds): + msg = (_('%(rule)s on %(target)s by %(creds)s disallowed by policy') % + {'rule': rule, 'target': target, 'creds': creds}) + super(PolicyNotAuthorized, self).__init__(msg) + + +class Rules(dict): + """A store for rules. Handles the default_rule setting directly.""" + + @classmethod + def load_json(cls, data, default_rule=None): + """Allow loading of JSON rule data.""" + + # Suck in the JSON data and parse the rules + rules = {k: _parser.parse_rule(v) + for k, v in jsonutils.loads(data).items()} + + return cls(rules, default_rule) + + @classmethod + def from_dict(cls, rules_dict, default_rule=None): + """Allow loading of rule data from a dictionary.""" + + # Parse the rules stored in the dictionary + rules = {k: _parser.parse_rule(v) for k, v in rules_dict.items()} + + return cls(rules, default_rule) + + def __init__(self, rules=None, default_rule=None): + """Initialize the Rules store.""" + + super(Rules, self).__init__(rules or {}) + self.default_rule = default_rule + + def __missing__(self, key): + """Implements the default rule handling.""" + + if isinstance(self.default_rule, dict): + raise KeyError(key) + + # If the default rule isn't actually defined, do something + # reasonably intelligent + if not self.default_rule: + raise KeyError(key) + + if isinstance(self.default_rule, _checks.BaseCheck): + return self.default_rule + + # We need to check this or we can get infinite recursion + if self.default_rule not in self: + raise KeyError(key) + + elif isinstance(self.default_rule, six.string_types): + return self[self.default_rule] + + def __str__(self): + """Dumps a string representation of the rules.""" + + # Start by building the canonical strings for the rules + out_rules = {} + for key, value in self.items(): + # Use empty string for singleton TrueCheck instances + if isinstance(value, _checks.TrueCheck): + out_rules[key] = '' + else: + out_rules[key] = str(value) + + # Dump a pretty-printed JSON representation + return jsonutils.dumps(out_rules, indent=4) + + +class Enforcer(object): + """Responsible for loading and enforcing rules. + + :param conf: A configuration object. + :param policy_file: Custom policy file to use, if none is + specified, ``conf.oslo_policy.policy_file`` will be + used. + :param rules: Default dictionary / Rules to use. It will be + considered just in the first instantiation. If + :meth:`load_rules` with ``force_reload=True``, + :meth:`clear` or :meth:`set_rules` with ``overwrite=True`` + is called this will be overwritten. + :param default_rule: Default rule to use, conf.default_rule will + be used if none is specified. + :param use_conf: Whether to load rules from cache or config file. + :param overwrite: Whether to overwrite existing rules when reload rules + from config file. + """ + + def __init__(self, conf, policy_file=None, rules=None, + default_rule=None, use_conf=True, overwrite=True): + self.conf = conf + + self.default_rule = default_rule or '!' + self.rules = Rules(rules, self.default_rule) + + self.policy_path = None + + self.policy_file = policy_file + self.use_conf = use_conf + self.overwrite = overwrite + self._loaded_files = [] + self._policy_dir_mtimes = {} + self._file_cache = {} + + def set_rules(self, rules, overwrite=True, use_conf=False): + """Create a new :class:`Rules` based on the provided dict of rules. + + :param dict rules: New rules to use. + :param overwrite: Whether to overwrite current rules or update them + with the new rules. + :param use_conf: Whether to reload rules from cache or config file. + """ + + if not isinstance(rules, dict): + raise TypeError(_('Rules must be an instance of dict or Rules, ' + 'got %s instead') % type(rules)) + self.use_conf = use_conf + if overwrite: + self.rules = Rules(rules, self.default_rule) + else: + self.rules.update(rules) + + def clear(self): + """Clears :class:`Enforcer` contents. + + This will clear this instances rules, policy's cache, file cache + and policy's path. + """ + self.set_rules({}) + self.default_rule = None + self.policy_path = None + self._loaded_files = [] + self._policy_dir_mtimes = {} + self._file_cache.clear() + + def load_rules(self, force_reload=False): + """Loads policy_path's rules. + + Policy file is cached and will be reloaded if modified. + + :param force_reload: Whether to reload rules from config file. + """ + + if force_reload: + self.use_conf = force_reload + + @staticmethod + def _is_directory_updated(cache, path): + # Get the current modified time and compare it to what is in + # the cache and check if the new mtime is greater than what + # is in the cache + mtime = 0 + if os.path.exists(path): + # Make a list of all the files + files = [path] + [os.path.join(path, file) for file in + os.listdir(path)] + # Pick the newest one, let's use its time. + mtime = os.path.getmtime(max(files, key=os.path.getmtime)) + cache_info = cache.setdefault(path, {}) + if mtime > cache_info.get('mtime', 0): + cache_info['mtime'] = mtime + return True + return False + + @staticmethod + def _walk_through_policy_directory(path, func, *args): + if not os.path.isdir(path): + raise ValueError('%s is not a directory' % path) + # We do not iterate over sub-directories. + policy_files = next(os.walk(path))[2] + policy_files.sort() + for policy_file in [p for p in policy_files if not p.startswith('.')]: + func(os.path.join(path, policy_file), *args) + + def _get_policy_path(self, path): + """Locate the policy JSON data file/path. + + :param path: It's value can be a full path or related path. When + full path specified, this function just returns the full + path. When related path specified, this function will + search configuration directories to find one that exists. + + :returns: The policy path + + :raises: ConfigFilesNotFoundError if the file/path couldn't + be located. + """ + policy_path = self.conf.find_file(path) + + if policy_path: + return policy_path + + raise cfg.ConfigFilesNotFoundError((path,)) + + def enforce(self, rule, target, creds, do_raise=False, + exc=None, *args, **kwargs): + """Checks authorization of a rule against the target and credentials. + + :param rule: The rule to evaluate. + :type rule: string or :class:`BaseCheck` + :param dict target: As much information about the object being operated + on as possible. + :param dict creds: As much information about the user performing the + action as possible. + :param do_raise: Whether to raise an exception or not if check + fails. + :param exc: Class of the exception to raise if the check fails. + Any remaining arguments passed to :meth:`enforce` (both + positional and keyword arguments) will be passed to + the exception class. If not specified, + :class:`PolicyNotAuthorized` will be used. + + :return: ``False`` if the policy does not allow the action and `exc` is + not provided; otherwise, returns a value that evaluates to + ``True``. Note: for rules using the "case" expression, this + ``True`` value will be the specified string from the + expression. + """ + + self.load_rules() + + # Allow the rule to be a Check tree + if isinstance(rule, _checks.BaseCheck): + result = rule(target, creds, self) + elif not self.rules: + # No rules to reference means we're going to fail closed + result = False + else: + try: + # Evaluate the rule + result = self.rules[rule](target, creds, self) + except KeyError: + LOG.debug('Rule [%s] does not exist', rule) + # If the rule doesn't exist, fail closed + result = False + + # If it is False, raise the exception if requested + if do_raise and not result: + if exc: + raise exc(*args, **kwargs) + + raise PolicyNotAuthorized(rule, target, creds) + + return result diff --git a/orm/common/orm_common/tests/__init__.py b/orm/common/orm_common/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/tests/hooks/__init__.py b/orm/common/orm_common/tests/hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/tests/hooks/test_api_error_hook.py b/orm/common/orm_common/tests/hooks/test_api_error_hook.py new file mode 100755 index 00000000..6287d10e --- /dev/null +++ b/orm/common/orm_common/tests/hooks/test_api_error_hook.py @@ -0,0 +1,68 @@ +import json +import mock +from orm_common.hooks import api_error_hook +from unittest import TestCase +import logging + +logger = logging.getLogger(__name__) + + +class TestAPIErrorHook(TestCase): + @mock.patch.object(api_error_hook, 'err_utils') + @mock.patch.object(api_error_hook, 'json') + def test_after_401(self, mock_json, mock_err_utils): + a = api_error_hook.APIErrorHook() + state = mock.MagicMock() + + mock_err_utils.get_error_dict.return_value = 'B' + mock_json.loads = json.loads + mock_json.dumps = json.dumps + state.response.status_code = 401 + a.after(state) + self.assertEqual(state.response.body, + json.dumps(mock_err_utils.get_error_dict.return_value)) + + @mock.patch.object(api_error_hook, 'err_utils') + def test_after_not_an_error(self, mock_err_utils): + a = api_error_hook.APIErrorHook() + state = mock.MagicMock() + + mock_err_utils.get_error_dict.return_value = 'B' + state.response.body = 'AAAA' + temp = state.response.body + # A successful status code + state.response.status_code = 201 + a.after(state) + # Assert that the response body hasn't changed + self.assertEqual(state.response.body, temp) + + @mock.patch.object(api_error_hook, 'err_utils') + @mock.patch.object(api_error_hook.json, 'loads', + side_effect=ValueError('test')) + def test_after_error(self, mock_json, mock_err_utils): + a = api_error_hook.APIErrorHook() + state = mock.MagicMock() + + mock_err_utils.get_error_dict.return_value = 'B' + state.response.body = 'AAAA' + + mock_json.loads = mock.MagicMock(side_effect=ValueError('sd')) + state.response.status_code = 402 + a.after(state) + self.assertEqual(state.response.body, + json.dumps(mock_err_utils.get_error_dict.return_value)) + + @mock.patch.object(api_error_hook, 'err_utils') + @mock.patch.object(api_error_hook, 'json') + def test_after_success(self, mock_json, mock_err_utils): + a = api_error_hook.APIErrorHook() + state = mock.MagicMock() + + mock_err_utils.get_error_dict.return_value = 'B' + mock_json.loads = json.loads + mock_json.dumps = json.dumps + mock_json.loads = json.loads + state.response.body = '{"debuginfo": null, "faultcode": "Client", "faultstring": "{\\"code\\": 404, \\"created\\": \\"1475768730.95\\", \\"details\\": \\"\\", \\"message\\": \\"customer: q not found\\", \\"type\\": \\"Not Found\\", \\"transaction_id\\": \\"mock_json5efa7416fb4d408cc0e30e4373cf00\\"}"}' + state.response.status_code = 400 + a.after(state) + self.assertEqual(json.loads(state.response.body), json.loads('{"message": "customer: q not found", "created": "1475768730.95", "type": "Not Found", "details": "", "code": 404, "transaction_id": "mock_json5efa7416fb4d408cc0e30e4373cf00"}')) diff --git a/orm/common/orm_common/tests/hooks/test_security_headers_hook.py b/orm/common/orm_common/tests/hooks/test_security_headers_hook.py new file mode 100755 index 00000000..ee5b2420 --- /dev/null +++ b/orm/common/orm_common/tests/hooks/test_security_headers_hook.py @@ -0,0 +1,31 @@ +import mock +from orm_common.hooks import security_headers_hook +from unittest import TestCase + + +class MyHeaders(object): + def __init__(self): + self.headers = {} + + def add(self, key, value): + self.headers[key] = value + + +class TestSecurityHeadersHook(TestCase): + def test_after(self): + s = security_headers_hook.SecurityHeadersHook() + test_headers = MyHeaders() + state = mock.MagicMock() + state.response.headers = test_headers + s.after(state) + + security_headers = {'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', + 'Content-Security-Policy': 'default-src \'self\'', + 'X-Permitted-Cross-Domain-Policies': 'none', + 'X-XSS-Protection': '1; mode=block'} + + for header in security_headers: + self.assertEqual(security_headers[header], + test_headers.headers[header]) diff --git a/orm/common/orm_common/tests/hooks/test_transaction_id_hook.py b/orm/common/orm_common/tests/hooks/test_transaction_id_hook.py new file mode 100755 index 00000000..da1a6c22 --- /dev/null +++ b/orm/common/orm_common/tests/hooks/test_transaction_id_hook.py @@ -0,0 +1,17 @@ +import mock +from orm_common.hooks import transaction_id_hook +from unittest import TestCase +import logging + +logger = logging.getLogger(__name__) + + +class TestTransactionIdHook(TestCase): + @mock.patch.object(transaction_id_hook.utils, 'make_transid', + return_value='test') + def test_before_sanity(self, mock_make_transid): + t = transaction_id_hook.TransactionIdHook() + state = mock.MagicMock() + t.before(state) + self.assertEqual(state.request.transaction_id, 'test') + self.assertEqual(state.request.tracking_id, 'test') diff --git a/orm/common/orm_common/tests/injector/__init__.py b/orm/common/orm_common/tests/injector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/tests/injector/test_injector.py b/orm/common/orm_common/tests/injector/test_injector.py new file mode 100755 index 00000000..1ac3bb04 --- /dev/null +++ b/orm/common/orm_common/tests/injector/test_injector.py @@ -0,0 +1,58 @@ +import mock +from orm_common.injector import injector +from unittest import TestCase +import os +import logging +from orm_common.injector.fang.resource_provider_register import ResourceProviderRegister + +logger = logging.getLogger(__name__) + + +class TestInjector(TestCase): + def setUp(self): + pass + + @mock.patch.object(injector, '_import_file_by_name') + def test_register_providers(self, mock_import_file_by_name): + os.environ['CMS_ENV'] = 'test' + injector.register_providers('CMS_ENV', 'a/b/c', logger) + + @mock.patch.object(injector, '_import_file_by_name') + def test_register_providers_env_not_exist(self, mock_import_file_by_name): + injector.register_providers('CMS_ENV1', 'a/b/c', logger) + + @mock.patch.object(injector, '_import_file_by_name') + def test_register_providers_env_test(self, mock_import_file_by_name): + os.environ['CMS_ENV2'] = '__TEST__' + injector.register_providers('CMS_ENV2', 'a/b/c', logger) + + @mock.patch.object(injector, '_import_file_by_name') + def test_register_providers_with_existing_provider(self, mock_import_file_by_name): + mock_import_file_by_name.return_value = type('module', (object,), {'providers': ['a1', 'b2']})() + os.environ['c3'] = 'test' + injector.register_providers('c3', 'a/b/c', logger) + + def test_get_di(self): + injector.get_di() + + @mock.patch.object(injector, 'logger') + def test_import_file_by_name_ioerror(self, mock_logger): + injector.logger = mock.MagicMock() + # Calling it with ('', '.') should raise an IOError + # (no such file or directory) + self.assertRaises(IOError, injector._import_file_by_name, '', '.') + + @mock.patch.object(injector.imp, 'load_source', return_value='test') + def test_import_file_by_name_sanity(self, mock_load_source): + self.assertEqual(injector._import_file_by_name('', '.'), 'test') + + @mock.patch.object(injector._di.providers, 'register_instance') + def test_override_injected_dependency(self, mock_di): + injector.override_injected_dependency((1, 2,)) + self.assertTrue(mock_di.called) + + ''' + @mock.patch.object(ResourceProviderRegister, 'register_instance') + def test_override_injected_dependency(self, mock_resourceProviderRegister): + injector.override_injected_dependency(mock.Mock()) + ''' diff --git a/orm/common/orm_common/tests/policy/__init__.py b/orm/common/orm_common/tests/policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/tests/policy/test_checks.py b/orm/common/orm_common/tests/policy/test_checks.py new file mode 100755 index 00000000..cf319474 --- /dev/null +++ b/orm/common/orm_common/tests/policy/test_checks.py @@ -0,0 +1,100 @@ +import mock +import unittest + +from orm_common.policy import _checks +from wsme.exc import ClientSideError + + +class TestChecks(unittest.TestCase): + def test_call_simple_checks(self): + check = _checks.FalseCheck() + self.assertFalse(check(1, 2, 3)) + check = _checks.TrueCheck() + self.assertTrue(check(1, 2, 3)) + + check = _checks.GenericCheck('a', 'b') + self.assertFalse(check(1, 2, 3)) + + def test_str_simple_checks(self): + check = _checks.FalseCheck() + self.assertEqual(str(check), '!') + check = _checks.TrueCheck() + self.assertEqual(str(check), '@') + + check = _checks.GenericCheck('a', 'b') + self.assertEqual(str(check), 'a:b') + + def test_call_complex_checks(self): + first_rule = _checks.TrueCheck() + second_rule = _checks.FalseCheck() + + check = _checks.NotCheck(first_rule) + self.assertFalse(check(1, 2, 3)) + + check = _checks.AndCheck([first_rule]) + check.add_check(second_rule) + self.assertFalse(check(1, 2, 3)) + check = _checks.AndCheck([first_rule, first_rule]) + self.assertTrue(check(1, 2, 3)) + + check = _checks.OrCheck([first_rule]) + check.add_check(second_rule) + self.assertTrue(check(1, 2, 3)) + self.assertEqual(check.pop_check(), (check, second_rule,)) + check = _checks.OrCheck([second_rule, second_rule]) + self.assertFalse(check(1, 2, 3)) + + def test_str_complex_checks(self): + first_rule = _checks.TrueCheck() + second_rule = _checks.FalseCheck() + + check = _checks.NotCheck(first_rule) + self.assertEqual(str(check), 'not @') + + check = _checks.AndCheck([first_rule]) + check.add_check(second_rule) + self.assertEqual(str(check), '(@ and !)') + + check = _checks.OrCheck([first_rule]) + check.add_check(second_rule) + self.assertEqual(str(check), '(@ or !)') + + def test_call_custom_checks_error(self): + check = _checks.RoleCheck('a', 'admin') + try: + check(1, mock.MagicMock(), 3) + self.fail('ClientSideError not raised!') + except ClientSideError as exc: + self.assertEqual(exc.code, 403) + + for check_type in (_checks.TenantCheck, + _checks.DomainCheck): + check = check_type('a', 'admin') + # 2 is not a user, so the check will fail + self.assertFalse(check(1, 2, 3)) + + def test_call_custom_checks_success(self): + user = mock.MagicMock() + user.user = {'roles': [{'name': 'admin'}]} + user.tenant = {'name': 'admin'} + user.domain = {'name': 'admin'} + + for check_type in (_checks.RoleCheck, + _checks.TenantCheck, + _checks.DomainCheck): + check = check_type('a', 'admin') + # 2 is not a user, so the check will fail + self.assertTrue(check(1, user, 3)) + + def test_call_rule_check_error(self): + enforcer = mock.MagicMock() + enforcer.rules = {'test': mock.MagicMock( + side_effect=KeyError('test'))} + check = _checks.RuleCheck('rule', 'test') + self.assertFalse(check(1, 2, enforcer)) + + def test_call_rule_check_success(self): + enforcer = mock.MagicMock() + enforcer.rules = {'test': mock.MagicMock(return_value=True)} + check = _checks.RuleCheck('rule', 'test') + self.assertTrue(check(1, 2, enforcer)) diff --git a/orm/common/orm_common/tests/policy/test_policy.py b/orm/common/orm_common/tests/policy/test_policy.py new file mode 100755 index 00000000..c670c04b --- /dev/null +++ b/orm/common/orm_common/tests/policy/test_policy.py @@ -0,0 +1,130 @@ +import mock +import unittest + +from orm_common.policy import policy +from orm_common.utils import api_error_utils as err_utils + + +class TestException(Exception): + pass + + +class TestPolicy(unittest.TestCase): + def setUp(self): + policy._ENFORCER = None + policy._POLICY_FILE = None + policy._TOKEN_CONF = None + + def test_reset(self): + policy._ENFORCER = mock.MagicMock() + policy._POLICY_FILE = mock.MagicMock() + policy.reset() + self.assertIsNone(policy._ENFORCER) + self.assertIsNone(policy._POLICY_FILE) + # Call it a second time when they are both None and see + # that no exception is raised + policy.reset() + self.assertIsNone(policy._ENFORCER) + self.assertIsNone(policy._POLICY_FILE) + + @mock.patch.object(policy, 'open') + @mock.patch.object(policy.qolicy, 'Enforcer') + @mock.patch.object(policy.qolicy, 'Rules') + def test_init_success(self, mock_rules, mock_enforcer, mock_open): + policy_file = 'a' + token_conf = 'b' + mock_rules.load_json.return_value = 'c' + policy.init(policy_file, token_conf) + self.assertEqual(policy._POLICY_FILE, 'a') + self.assertEqual(policy._TOKEN_CONF, 'b') + + def test_init_enforcer_already_exists(self): + policy._ENFORCER = mock.MagicMock() + + # Nothing should happen when the enforcer already exists, so make sure + # that no exception is raised + policy.init('a', 'b') + + @mock.patch.object(policy, 'open') + @mock.patch.object(policy.qolicy, 'Rules') + @mock.patch.object(policy, '_ENFORCER') + def test_reset_rules_no_policy_file(self, mock_enforcer, + mock_rules, mock_open): + self.assertRaises(ValueError, policy.reset_rules) + + @mock.patch.object(policy, 'open') + @mock.patch.object(policy.qolicy, 'Rules') + @mock.patch.object(policy, '_ENFORCER') + def test_reset_rules_success(self, mock_enforcer, + mock_rules, mock_open): + policy._POLICY_FILE = mock.MagicMock() + policy.reset_rules() + self.assertTrue(mock_enforcer.set_rules.called) + + @mock.patch.object(policy, 'reset_rules') + @mock.patch.object(policy.tokens, 'get_token_user', + side_effect=ValueError('test')) + @mock.patch.object(policy, '_ENFORCER') + def test_enforce_enforcer_error(self, mock_enforcer, + mock_get_token_user, + mock_reset_rules): + mock_enforcer.enforce.side_effect = policy.EnforcerError() + self.assertRaises(policy.EnforcerError, policy.enforce, 'action', + 'token', mock.MagicMock()) + + @mock.patch.object(policy, 'reset_rules') + @mock.patch.object(policy.tokens, 'get_token_user') + @mock.patch.object(policy, '_ENFORCER') + def test_enforce_success(self, mock_enforcer, + mock_get_token_user, + mock_reset_rules): + mock_enforcer.enforce.return_value = True + self.assertTrue(policy.enforce('action', 'token', mock.MagicMock())) + + def test_authorize_authorization_disabled(self): + request = mock.MagicMock() + app_conf = mock.MagicMock() + app_conf.authentication.enabled = False + # No exception should be raised + policy.authorize('a', request, app_conf) + + @mock.patch.object(policy, 'enforce') + def test_authorize_no_token(self, mock_enforce): + request = mock.MagicMock() + request.headers.get.return_value = None + app_conf = mock.MagicMock() + app_conf.authentication.enabled = True + # No exception should be raised + policy.authorize('a', request, app_conf) + + @mock.patch.object(policy, 'enforce', side_effect=policy.EnforcerError()) + @mock.patch.object(policy.err_utils, 'get_error', return_value=TestException) + def test_authorize_enforce_failed(self, mock_enforce, mock_get_error): + request = mock.MagicMock() + request.headers.get.return_value = None + app_conf = mock.MagicMock() + app_conf.authentication.enabled = True + + self.assertRaises(TestException, policy.authorize, 'a', request, + app_conf) + + @mock.patch.object(policy, 'enforce', side_effect=ValueError()) + @mock.patch.object(policy.err_utils, 'get_error', return_value=TestException) + def test_authorize_other_error(self, mock_enforce, mock_get_error): + request = mock.MagicMock() + request.headers.get.return_value = None + app_conf = mock.MagicMock() + app_conf.authentication.enabled = True + + self.assertRaises(TestException, policy.authorize, 'a', request, + app_conf) + + @mock.patch.object(policy, 'enforce') + def test_authorize_success(self, mock_enforce): + request = mock.MagicMock() + request.headers.get.return_value = 'test' + app_conf = mock.MagicMock() + app_conf.authentication.enabled = True + + # No exception should be raised + policy.authorize('a', request, app_conf) diff --git a/orm/common/orm_common/tests/utils/__init__.py b/orm/common/orm_common/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/tests/utils/test_api_error_utils.py b/orm/common/orm_common/tests/utils/test_api_error_utils.py new file mode 100755 index 00000000..7b7568b1 --- /dev/null +++ b/orm/common/orm_common/tests/utils/test_api_error_utils.py @@ -0,0 +1,14 @@ +import json +import mock +from orm_common.utils import api_error_utils +from unittest import TestCase + + +class TestCrossApiUtil(TestCase): + @mock.patch.object(api_error_utils.utils, 'get_time_human', return_value=1.337) + def test_get_error_default_message(self, mock_time): + self.assertEqual( + json.loads(api_error_utils.get_error('test', 'a').message), + {"details": "a", "message": "Incompatible JSON body", + "created": "1.337", "code": 400, "type": "Bad Request", + "transaction_id": "test"}) diff --git a/orm/common/orm_common/tests/utils/test_cross_api_utils.py b/orm/common/orm_common/tests/utils/test_cross_api_utils.py new file mode 100755 index 00000000..76037ea0 --- /dev/null +++ b/orm/common/orm_common/tests/utils/test_cross_api_utils.py @@ -0,0 +1,79 @@ +import mock +from orm_common.utils import cross_api_utils +from testfixtures import log_capture +from unittest import TestCase +import requests +import pecan +import logging +import pprint +import time + + +class TestCrossApiUtil(TestCase): + @mock.patch('pecan.conf') + def setUp(self, mock_conf): + self.mock_response = mock.Mock() + cross_api_utils.conf = mock_conf + + def respond(self, value, code): + self.mock_response.json.return_value = value + self.mock_response.status_code = code + return self.mock_response + + def test_set_utils_conf(self): + cross_api_utils.set_utils_conf(None) + self.assertEqual(cross_api_utils.conf, None) + + def test_check_conf_initialization(self): + cross_api_utils.set_utils_conf(None) + self.assertRaises(AssertionError, cross_api_utils._check_conf_initialization) + + @mock.patch('orm_common.utils.cross_api_utils.get_rms_region_group') + def test_is_region_group_exist(self, mock_rms_region_group): + mock_rms_region_group.return_value = 'test_group' + exist = cross_api_utils.is_region_group_exist('test_group_name') + self.assertEqual(exist, True) + + @mock.patch('orm_common.utils.cross_api_utils.get_rms_region_group') + def test_is_region_group_exist_false(self, mock_rms_region_group): + mock_rms_region_group.return_value = None + exist = cross_api_utils.is_region_group_exist('test_group_name') + self.assertEqual(exist, False) + + @mock.patch('orm_common.utils.cross_api_utils.get_rms_region_group') + def test_get_regions_of_group(self, mock_rms_region_group): + mock_rms_region_group.return_value = {'regions': 'group'} + exist = cross_api_utils.get_regions_of_group('test_group_name') + self.assertEqual(exist, 'group') + + @mock.patch('orm_common.utils.cross_api_utils.get_rms_region_group') + def test_get_regions_of_group_false(self, mock_rms_region_group): + mock_rms_region_group.return_value = None + exist = cross_api_utils.get_regions_of_group('test_group_name') + self.assertEqual(exist, None) + + @mock.patch('requests.get') + def test_get_rms_region_group(self, mock_get): + mock_get.return_value = self.respond({'result': 'success'}, 200) + result = cross_api_utils.get_rms_region_group('test_group_name') + self.assertEqual(result, {'result': 'success'}) + + def test_get_rms_region_group_cache_used(self): + cross_api_utils.prev_timestamp = time.time() + cross_api_utils.prev_group_name = 'test_group' + cross_api_utils.prev_resp = 'test_response' + cross_api_utils.conf.api.rms_server.cache_seconds = 14760251830 + self.assertEqual(cross_api_utils.prev_resp, + cross_api_utils.get_rms_region_group( + cross_api_utils.prev_group_name)) + + @mock.patch.object(cross_api_utils, 'logger') + @mock.patch.object(time, 'time', side_effect=ValueError('test')) + def test_get_rms_region_group_cache_used(self, mock_time, mock_logger): + self.assertRaises(ValueError, cross_api_utils.get_rms_region_group, + 'test') + + # @mock.patch('requests.get') + # def test_get_rms_region_group_with_exception(self, mock_get): + # mock_get.side_affect = Exception('boom') + # self.assertRaises(Exception, cross_api_utils.get_rms_region_group, 'test_group_name') diff --git a/orm/common/orm_common/tests/utils/test_utils.py b/orm/common/orm_common/tests/utils/test_utils.py new file mode 100755 index 00000000..d9640ef7 --- /dev/null +++ b/orm/common/orm_common/tests/utils/test_utils.py @@ -0,0 +1,175 @@ +import mock +from orm_common.utils import utils +from testfixtures import log_capture +from unittest import TestCase +import requests +import pecan +import logging +import pprint + + +class TestUtil(TestCase): + @mock.patch('pecan.conf') + def setUp(self, mock_conf): + self.mock_response = mock.Mock() + utils.conf = mock_conf + + def respond(self, value, code): + self.mock_response.json.return_value = value + self.mock_response.status_code = code + return self.mock_response + + @mock.patch('requests.post') + def test_make_uuid(self, mock_post): + mock_post.return_value = self.respond({'uuid': '987654321'}, 200) + uuid = utils.make_uuid() + self.assertEqual(uuid, '987654321') + + @mock.patch('requests.post') + @log_capture('orm_common.utils.utils', level=logging.INFO) + def test_make_uuid_offline(self, mock_post, l): + mock_post.side_effect = Exception('boom') + uuid = utils.make_uuid() + self.assertEqual(uuid, None) + l.check(('orm_common.utils.utils', 'INFO', 'Failed in make_uuid:boom')) + + @mock.patch('requests.post') + def test_make_transid(self, mock_post): + mock_post.return_value = self.respond({'uuid': '987654321'}, 200) + uuid = utils.make_transid() + self.assertEqual(uuid, '987654321') + + @mock.patch('requests.post') + @log_capture('orm_common.utils.utils', level=logging.INFO) + def test_make_transid_offline(self, mock_post, l): + mock_post.side_effect = Exception('boom') + uuid = utils.make_transid() + self.assertEqual(uuid, None) + l.check( + ('orm_common.utils.utils', 'INFO', 'Failed in make_transid:boom')) + + @mock.patch('audit_client.api.audit.init') + @mock.patch('audit_client.api.audit.audit') + def test_audit_trail(self, mock_init, mock_audit): + resp = utils.audit_trail('create customer', '1234', + {'X-RANGER-Client': 'Fred'}, '5678') + self.assertEqual(resp, 200) + + @mock.patch('audit_client.api.audit.audit') + def test_audit_trail_offline(self, mock_audit): + mock_audit.side_effect = Exception('boom') + resp = utils.audit_trail('create customer', '1234', + {'X-RANGER-Client': 'Fred'}, '5678') + self.assertEqual(resp, None) + + @mock.patch('audit_client.api.audit.init') + @mock.patch('audit_client.api.audit.audit') + def test_audit_service_args_least(self, mock_audit, mock_init): + resp = utils.audit_trail('create customer', '1234', + {'X-RANGER-Client': 'Fred'}, '5678') + self.assertEqual(mock_audit.call_args[0][1], 'Fred') # application_id + self.assertEqual(mock_audit.call_args[0][2], '1234') # tracking_id + self.assertEqual(mock_audit.call_args[0][3], '1234') # transaction_id + self.assertEqual(mock_audit.call_args[0][4], + 'create customer') # transaction_type + self.assertEqual(mock_audit.call_args[0][5], '5678') # resource_id + # self.assertEqual(mock_audit.call_args[0][6], 'cms') # service + self.assertEqual(mock_audit.call_args[0][7], '') # user_id + self.assertEqual(mock_audit.call_args[0][8], 'NA') # external_id + self.assertEqual(mock_audit.call_args[0][9], 'CMS') # event_details + # self.assertEqual(mock_audit.call_args[0][10], 'Saved to DB') # status + + @mock.patch('audit_client.api.audit.init') + @mock.patch('audit_client.api.audit.audit') + def test_audit_service_with_tracking(self, mock_audit, mock_init): + utils.audit_trail('create customer', '1234', + {'X-RANGER-Client': 'Fred', + 'X-RANGER-Tracking-Id': 'Track12'}, '5678') + self.assertEqual(mock_audit.call_args[0][1], 'Fred') # application_id + self.assertEqual(mock_audit.call_args[0][2], 'Track12') # tracking_id + self.assertEqual(mock_audit.call_args[0][3], '1234') # transaction_id + self.assertEqual(mock_audit.call_args[0][4], + 'create customer') # transaction_type + self.assertEqual(mock_audit.call_args[0][5], '5678') # resource_id + # self.assertEqual(mock_audit.call_args[0][6], 'cms') # service + self.assertEqual(mock_audit.call_args[0][7], '') # user_id + self.assertEqual(mock_audit.call_args[0][8], 'NA') # external_id + self.assertEqual(mock_audit.call_args[0][9], 'CMS') # event_details + # self.assertEqual(mock_audit.call_args[0][10], 'Saved to DB') # status + + @mock.patch('audit_client.api.audit.init') + @mock.patch('audit_client.api.audit.audit') + def test_audit_service_with_requester(self, mock_audit, mock_init): + resp = utils.audit_trail('create customer', '1234', + {'X-RANGER-Client': 'Fred', + 'X-RANGER-Requester': 'Req04'}, '5678') + self.assertEqual(mock_audit.call_args[0][1], 'Fred') # application_id + self.assertEqual(mock_audit.call_args[0][2], '1234') # tracking_id + self.assertEqual(mock_audit.call_args[0][3], '1234') # transaction_id + self.assertEqual(mock_audit.call_args[0][4], 'create customer') # transaction_type + self.assertEqual(mock_audit.call_args[0][5], '5678') # resource_id + # self.assertEqual(mock_audit.call_args[0][6], 'cms') # service + self.assertEqual(mock_audit.call_args[0][7], 'Req04') # user_id + self.assertEqual(mock_audit.call_args[0][8], 'NA') # external_id + self.assertEqual(mock_audit.call_args[0][9], 'CMS') # event_details + # self.assertEqual(mock_audit.call_args[0][10], 'Saved to DB') # status + + def test_set_utils_conf(self): + utils.set_utils_conf('test') + self.assertEqual(utils.conf, 'test') + + def test_check_conf_initialization(self): + utils.set_utils_conf(None) + self.assertRaises(AssertionError, utils._check_conf_initialization) + + @mock.patch('requests.post') + def test_create_existing_uuid(self, mock_post): + uuid = '987654321' + mock_post.return_value = self.respond({'uuid': uuid}, 200) + returned_uuid = utils.create_existing_uuid(uuid) + self.assertEqual(returned_uuid, uuid) + + @mock.patch('requests.post') + def test_create_existing_uuid_with_exception(self, mock_post): + mock_post.side_effect = Exception('boom') + uuid = '987654321' + returned_uuid = utils.create_existing_uuid(uuid) + self.assertEqual(returned_uuid, None) + + @mock.patch('requests.post') + def test_create_existing_uuid_with_400(self, mock_post): + uuid = '987654321' + mock_post.return_value = self.respond({'uuid': uuid}, 400) + self.assertRaises(TypeError, utils.create_existing_uuid, uuid) + + @mock.patch('pecan.conf') + def test_report_config(self, mock_conf): + expected_value = pprint.pformat(mock_conf.to_dict(), indent=4) + returned_value = utils.report_config(mock_conf) + self.assertEqual(expected_value, returned_value) + + @mock.patch('pecan.conf') + def test_report_config_with_log_write(self, mock_conf): + expected_value = pprint.pformat(mock_conf.to_dict(), indent=4) + returned_value = utils.report_config(mock_conf, True) + self.assertEqual(expected_value, returned_value) + + @mock.patch('requests.get') + def test_get_resource_status_sanity(self, mock_get): + my_response = mock.MagicMock() + my_response.status_code = 200 + my_response.json.return_value = 'test' + mock_get.return_value = my_response + result = utils.get_resource_status('A') + self.assertEqual(result, 'test') + + @mock.patch('requests.get', side_effect=ValueError()) + def test_get_resource_status_get_failed(self, mock_get): + self.assertIsNone(utils.get_resource_status('A')) + + @mock.patch('requests.get') + def test_get_resource_status_invalid_response(self, mock_get): + my_response = mock.MagicMock() + my_response.status_code = 404 + mock_get.return_value = my_response + self.assertIsNone(utils.get_resource_status('A')) diff --git a/orm/common/orm_common/utils/__init__.py b/orm/common/orm_common/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/common/orm_common/utils/api_error_utils.py b/orm/common/orm_common/utils/api_error_utils.py new file mode 100755 index 00000000..e1fa3cf3 --- /dev/null +++ b/orm/common/orm_common/utils/api_error_utils.py @@ -0,0 +1,45 @@ +import json +from orm_common.utils import utils +from wsme.exc import ClientSideError + + +# This method creates a ClientSideError with given parameters +# and returns it to caller. +def get_error(transaction_id, + error_details="", + message=None, + status_code=400): + + err = get_error_dict(status_code, transaction_id, message, error_details) + return ClientSideError(json.dumps(err), status_code) + + +def get_error_dict(status_code, transaction_id, message, error_details=""): + + if not message: + message = error_message[status_code]['message'] + + # In case error like 409.1 we need to remove the dot part. + status_code = int(status_code) + + return { + 'code': status_code, + 'type': error_message[status_code]['type'], + 'created': '{}'.format(utils.get_time_human()), + 'transaction_id': transaction_id, + 'message': message, + 'details': error_details + } + +# Default error messages +error_message = { + 400: {'message': 'Incompatible JSON body', 'type': 'Bad Request'}, + 401: {'message': 'Unable to authenticate', 'type': 'Unauthorized'}, + 403: {'message': 'Not allowed to perform this operation', 'type': 'Forbidden'}, + 404: {'message': 'The specific transaction was not found', 'type': 'Not Found'}, + 405: {'message': 'This method is not allowed', 'type': 'Method Not Allowed'}, + 409: {'message': 'Current resource is busy', 'type': 'Conflict'}, + 409.1: {'message': 'Customer UUID already exists', 'type': 'Conflict'}, + 409.2: {'message': 'Customer name already exists', 'type': 'Conflict'}, + 500: {'message': 'Server error occurred', 'type': 'Internal Server Error'} +} diff --git a/orm/common/orm_common/utils/cross_api_utils.py b/orm/common/orm_common/utils/cross_api_utils.py new file mode 100755 index 00000000..51ddac36 --- /dev/null +++ b/orm/common/orm_common/utils/cross_api_utils.py @@ -0,0 +1,86 @@ +import requests +import logging +from pecan import conf +from audit_client.api import audit +import time + +# from orm_common.logger import get_logger + +# logger = get_logger(__name__) +logger = logging.getLogger(__name__) + +conf = None + + +def set_utils_conf(_conf): + global conf + conf = _conf + + +def _check_conf_initialization(): + if not conf: + raise AssertionError('Configurations wasnt initiated, please run set_utils_conf and pass pecan configuration') + + +def is_region_group_exist(group_name): + """ function to check whether region group exists + returns 200 for ok and None for error + """ + group = get_rms_region_group(group_name) + if group is None: + return False + + return True + + +def get_regions_of_group(group_name): + """ function to get regions associated with group + returns 200 for ok and None for error + """ + group = get_rms_region_group(group_name) + if group is None: + return None + if "regions" not in group: + return None + return group["regions"] + + +prev_group_name = None + + +def get_rms_region_group(group_name): + """ function to call rms api for group info + returns 200 for ok and None for error + """ + global prev_group_name, prev_timestamp, prev_resp + + _check_conf_initialization() + try: + timestamp = time.time() + if group_name == prev_group_name and timestamp - prev_timestamp <= conf.api.rms_server.cache_seconds: + return prev_resp + + headers = { + 'content-type': 'application/json', + } + # GET https://{serverRoot}/v1/orm/groups/{groupId}/ + rms_server_url = '%s%s/%s' % (conf.api.rms_server.base, conf.api.rms_server.groups, group_name) + logger.info("RMS Server URL:" + rms_server_url) + resp = requests.get(rms_server_url, headers=headers, verify=conf.verify) + resp = resp.json() + logger.info("Response from RMS Server" + str(resp)) + prev_resp = resp + prev_group_name = group_name + prev_timestamp = timestamp + return resp + except requests.exceptions.ConnectionError as exp: + nagois = 'CON{}RMS001'.format(conf.server.name.upper()) + logger.error( + 'CRITICAL|{}| Failed in getting data from rms: connection error'.format( + nagois) + str(exp)) + exp.message = 'connection error: Failed to get get data from rms: unable to connect to server' + raise + except Exception as e: + logger.exception(" Exception: " + str(e)) + # logger.log_exception('Failed in get_rms_region_group', e) + raise diff --git a/orm/common/orm_common/utils/dictator.py b/orm/common/orm_common/utils/dictator.py new file mode 100755 index 00000000..7e83fec2 --- /dev/null +++ b/orm/common/orm_common/utils/dictator.py @@ -0,0 +1,23 @@ +"""ORM Dictator module.""" + +DICTATOR = {} + + +def set(key, value): + """Set a key in the Dictator.""" + global DICTATOR + DICTATOR[key] = value + + +def soft_set(key, value): + """Set a key in the Dictator only if it doesn't exist.""" + global DICTATOR + DICTATOR.setdefault(key, value) + + +def get(key, default=None): + """Get a key from the Dictator. + + :return: The value if it exists, default otherwise. + """ + return DICTATOR[key] if key in DICTATOR else default diff --git a/orm/common/orm_common/utils/sanitize.py b/orm/common/orm_common/utils/sanitize.py new file mode 100644 index 00000000..e4f41e82 --- /dev/null +++ b/orm/common/orm_common/utils/sanitize.py @@ -0,0 +1,7 @@ +import re + + +def sanitize_symbol_name(value, symbol_meaning=None): + name_re = re.compile('[A-Za-z0-9_]+$') + symbol_meaning = symbol_meaning if symbol_meaning else "name" + return value if name_re.match(value) else "unauthorized " + symbol_meaning diff --git a/orm/common/orm_common/utils/utils.py b/orm/common/orm_common/utils/utils.py new file mode 100755 index 00000000..0fb95fac --- /dev/null +++ b/orm/common/orm_common/utils/utils.py @@ -0,0 +1,225 @@ +import requests +import logging +from pecan import conf +from audit_client.api import audit +import time +import pprint + +# from cms_rest.logger import get_logger +# + +conf = None +logger = logging.getLogger(__name__) + + +class ResponseError(Exception): + pass + + +class ConnectionError(Exception): + pass + + +def set_utils_conf(_conf): + global conf + conf = _conf + + +def _check_conf_initialization(): + if not conf: + raise AssertionError( + 'Configurations wasnt initiated, please run set_utils_conf and ' + 'pass pecan coniguration') + + +def make_uuid(): + """ function to request new uuid from uuid_generator rest service + returns uuid string + """ + _check_conf_initialization() + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + + try: + logger.debug('Requesting new UUID from URL: {}'.format(url)) + resp = requests.post(url, verify=conf.verify) + except requests.exceptions.ConnectionError as exp: + nagios = 'CON{}UUIDGEN001'.format(conf.server.name.upper()) + logger.critical('CRITICAL|{}|Failed in make_uuid: connection error: {}'.format(nagios, str(exp))) + exp.message = 'connection error: Failed to get uuid: unable to connect to server' + raise + except Exception as e: + logger.info('Failed in make_uuid:' + str(e)) + return None + + resp = resp.json() + return resp['uuid'] + + +def create_existing_uuid(uuid): + """ function to request new uuid from uuid_generator rest service + returns uuid string + :param uuid: + :return: + """ + _check_conf_initialization() + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + + try: + logger.debug('Creating UUID: {}, using URL: {}'.format(uuid, url)) + resp = requests.post(url, data={'uuid': uuid}, verify=conf.verify) + except requests.exceptions.ConnectionError as exp: + nagios = 'CON{}UUIDGEN001'.format(conf.server.name.upper()) + logger.critical('CRITICAL|{}|Failed in create_existing_uuid: connection error: {}'.format(nagios, str(exp))) + exp.message = 'connection error: Failed to get uuid: unable to connect to server' + raise + except Exception as e: + logger.info('Failed in make_uuid:' + str(e)) + return None + + if resp.status_code == 400: + raise TypeError('duplicate key for uuid') + resp = resp.json() + return resp['uuid'] + + +def make_transid(): + """ function to request new uuid of transaction type from uuid_generator + rest service + returns uuid string + """ + _check_conf_initialization() + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + + try: + logger.debug('Requesting transaction ID from: {}'.format(url)) + resp = requests.post(url, data={'uuid_type': 'transaction'}, verify=conf.verify) + except requests.exceptions.ConnectionError as exp: + nagios = 'CON{}UUIDGEN001'.format(conf.server.name.upper()) + logger.critical('CRITICAL|{}|Failed in make_transid: connection error: {}'.format(nagios, str(exp))) + exp.message = 'connection error: Failed to get uuid: unable to connect to server' + raise + except Exception as e: + logger.info('Failed in make_transid:' + str(e)) + return None + + resp = resp.json() + if 'uuid' in resp: + return resp['uuid'] + else: + return None + +audit_setup = False + + +def _get_event_details(cmd): + event = 'unknown' + if 'customer' in cmd: + event = 'CMS' + elif 'image' in cmd: + event = 'IMS' + elif 'flavor' in cmd: + event = 'FMS' + return event + + +def audit_trail(cmd, transaction_id, headers, resource_id, message=None, + event_details=''): + """ function to send item to audit trail rest api + returns 200 for ok and None for error + :param cmd: + :param transaction_id: + :param headers: + :param resource_id: + :param message: + :return: + """ + _check_conf_initialization() + global audit_setup, audit_server_url + if not audit_setup: + audit_server_url = '%s%s' % ( + conf.api.audit_server.base, conf.api.audit_server.trans) + num_of_send_retries = 3 + time_wait_between_retries = 1 + logger.debug('Initializing Audit, using URL: {}'.format( + audit_server_url)) + audit.init(audit_server_url, num_of_send_retries, + time_wait_between_retries, conf.server.name.upper()) + audit_setup = True + + try: + timestamp = long(round(time.time() * 1000)) + application_id = headers[ + 'X-RANGER-Client'] if 'X-RANGER-Client' in headers else \ + 'NA' + tracking_id = headers[ + 'X-RANGER-Tracking-Id'] if 'X-RANGER-Tracking-Id' in headers \ + else transaction_id + # transaction_id is function argument + transaction_type = cmd + # resource_id is function argument + service_name = conf.server.name.upper() + user_id = headers[ + 'X-RANGER-Requester'] if 'X-RANGER-Requester' in headers else \ + '' + external_id = 'NA' + logger.debug('Sending to audit: timestamp: {}, application_id: {}, ' + ' tracking_id: {},' + ' transaction_type: {}'.format(timestamp, application_id, + tracking_id, + transaction_type)) + audit.audit(timestamp, application_id, tracking_id, transaction_id, + transaction_type, resource_id, service_name, user_id, + external_id, event_details) + except Exception as e: + logger.exception('Failed in audit service. ' + str(e)) + return None + + return 200 + + +def report_config(conf, dump_to_log=False, my_logger=None): + """ return the configuration (which is set by config.py) as a string + :param conf: + :param dump_to_log: + :param my_logger: + :return: + """ + + ret = pprint.pformat(conf.to_dict(), indent=4) + effective_logger = my_logger if my_logger else logger + if dump_to_log: + effective_logger.info('Current Configuration:\n' + ret) + + return ret + + +def get_resource_status(resource_id): + """ Get a resource status from RDS. + :param resource_id: + :return: + """ + + url = "{}{}{}".format(conf.api.rds_server.base, + conf.api.rds_server.status, resource_id) + logger.debug('Getting status from: {}'.format(url)) + try: + result = requests.get(url, verify=conf.verify) + except Exception as exception: + logger.debug('Failed to get status: {}'.format(str(exception))) + return None + + if result.status_code != 200: + logger.debug('Got invalid response from RDS: code {}'.format( + result.status_code)) + return None + else: + logger.debug('Got response from RDS: {}'.format(result.json())) + return result.json() + + +def get_time_human(): + """ + this function return the timestamp for output JSON + :return: timestamp in wanted format + """ + return time.strftime("%a, %b %d %Y, %X (%Z)", time.gmtime()) diff --git a/orm/common/tox.ini b/orm/common/tox.ini new file mode 100755 index 00000000..069b0d37 --- /dev/null +++ b/orm/common/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist=py27,cover + +[testenv] +setenv= CMS_ENV=mock + PYTHONPATH={toxinidir}:{toxinidir}/orm_common/extenal_mock/ +deps= -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +[testenv:pep8] +commands = + py.test --pep8 -m pep8 + +[testenv:cover] +commands= + coverage run setup.py test + coverage report --omit=orm_common/policy/_parser.py,orm_common/policy/qolicy.py + coverage html diff --git a/orm/orm_client/__init__.py b/orm/orm_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/__init__.py b/orm/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/audit_trail_manager/__init__.py b/orm/services/audit_trail_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/MANIFEST.in b/orm/services/customer_manager/MANIFEST.in new file mode 100755 index 00000000..c922f11a --- /dev/null +++ b/orm/services/customer_manager/MANIFEST.in @@ -0,0 +1 @@ +recursive-include public * diff --git a/orm/services/customer_manager/__init__.py b/orm/services/customer_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest.conf b/orm/services/customer_manager/cms_rest.conf new file mode 100644 index 00000000..56a429d9 --- /dev/null +++ b/orm/services/customer_manager/cms_rest.conf @@ -0,0 +1,26 @@ +Listen 7080 + + + + WSGIDaemonProcess cms_rest user=orm group=orm threads=5 + WSGIScriptAlias / /opt/app/orm/cms_rest/cms_rest.wsgi + + + Order deny,allow + Deny from all + Allow from localhost + + + + Order deny,allow + Deny from all + Allow from localhost + + + + WSGIProcessGroup cms_rest + WSGIApplicationGroup %{GLOBAL} + Require all granted + Allow from all + + diff --git a/orm/services/customer_manager/cms_rest.wsgi b/orm/services/customer_manager/cms_rest.wsgi new file mode 100644 index 00000000..ed375b2e --- /dev/null +++ b/orm/services/customer_manager/cms_rest.wsgi @@ -0,0 +1,2 @@ +from pecan.deploy import deploy +application = deploy('/opt/app/orm/cms_rest/config.py') diff --git a/orm/services/customer_manager/cms_rest/__init__.py b/orm/services/customer_manager/cms_rest/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/app.py b/orm/services/customer_manager/cms_rest/app.py new file mode 100755 index 00000000..57944115 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/app.py @@ -0,0 +1,36 @@ +from pecan import make_app +from cms_rest import model +from orm_common.utils import utils +from cms_rest.logger import get_logger +from pecan.commands import CommandRunner +from orm_common.policy import policy +from cms_rest.utils import authentication +import os + +logger = get_logger(__name__) + + +def setup_app(config): + model.init_model() + token_conf = authentication._get_token_conf(config) + policy.init(config.authentication.policy_file, token_conf) + app_conf = dict(config.app) + + # setting configurations for utils to be used from now and on + utils.set_utils_conf(config) + + app = make_app( + app_conf.pop('root'), + logging=getattr(config, 'logging', {}), + **app_conf + ) + logger.info('Starting CMS...') + return app + + +def main(): + dir_name = os.path.dirname(__file__) + drive, path_and_file = os.path.splitdrive(dir_name) + path, filename = os.path.split(path_and_file) + runner = CommandRunner() + runner.run(['serve', path+'/config.py']) diff --git a/orm/services/customer_manager/cms_rest/controllers/__init__.py b/orm/services/customer_manager/cms_rest/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/controllers/root.py b/orm/services/customer_manager/cms_rest/controllers/root.py new file mode 100755 index 00000000..ff4ba865 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/root.py @@ -0,0 +1,34 @@ +from pecan import expose, request, response +from webob.exc import status_map +from pecan.secure import SecureController +from cms_rest.controllers.v1 import root as v1 +from cms_rest.utils import authentication +from pecan import conf + + +class RootController(object): + # url/v1/ + v1 = v1.V1Controller() + + @expose(template='json') + def _default(self): + """ + Method to handle GET / + parameters: None + return: dict describing cms rest version information + """ + return { + "versions": { + "values": [ + { + "status": "stable", + "id": "v1", + "links": [ + { + "href": "http://localhost:7080/" + } + ] + } + ] + } + } diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/__init__.py b/orm/services/customer_manager/cms_rest/controllers/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/base.py b/orm/services/customer_manager/cms_rest/controllers/v1/base.py new file mode 100644 index 00000000..8fb3c48b --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/base.py @@ -0,0 +1,48 @@ +import wsme +from pecan import response +from wsme import types as wtypes +import inspect + + +class ClientSideError(wsme.exc.ClientSideError): + def __init__(self, error, status_code=400): + response.translatable_error = error + super(ClientSideError, self).__init__(error, status_code) + + +class InputValueError(ClientSideError): + def __init__(self, name, value, status_code=400): + super(InputValueError, self).__init__("Invalid value for input {} : {}".format(name, value), status_code) + + +class EntityNotFoundError(ClientSideError): + def __init__(self, id): + super(EntityNotFoundError, self).__init__("Entity not found for {}".format(id), status_code=404) + + +class Base(wtypes.DynamicBase): + pass + + ''' + @classmethod + def from_model(cls, m): + return cls(**(m.as_dict())) + + def as_dict(self, model): + valid_keys = inspect.getargspec(model.__init__)[0] + if 'self' in valid_keys: + valid_keys.remove('self') + return self.as_dict_from_keys(valid_keys) + + + def as_dict_from_keys(self, keys): + return dict((k, getattr(self, k)) + for k in keys + if hasattr(self, k) and + getattr(self, k) != wsme.Unset) + + @classmethod + def from_db_and_links(cls, m, links): + return cls(links=links, **(m.as_dict())) + + ''' diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/__init__.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/configuration.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/configuration.py new file mode 100755 index 00000000..e30d4bee --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/configuration.py @@ -0,0 +1,29 @@ +"""Configuration rest API input module.""" + +import logging +from orm_common.utils import utils +from pecan import conf +from pecan import rest +from wsmeext.pecan import wsexpose + + +logger = logging.getLogger(__name__) + + +class ConfigurationController(rest.RestController): + """Configuration controller.""" + + @wsexpose(str, str, status_code=200) + def get(self, dump_to_log='false'): + """get method. + + :param dump_to_log: A boolean string that says whether the + configuration should be written to log + :return: A pretty string that contains the service's configuration + """ + logger.info("Get configuration...") + + dump = dump_to_log.lower() == 'true' + utils.set_utils_conf(conf) + result = utils.report_config(conf, dump, logger) + return result diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/__init__.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/enabled.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/enabled.py new file mode 100755 index 00000000..0b3d0021 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/enabled.py @@ -0,0 +1,55 @@ +from pecan import rest, request +from wsmeext.pecan import wsexpose + +from orm_common.utils import utils +from orm_common.utils import api_error_utils as err_utils +from cms_rest.model.Models import Enabled, CustomerResultWrapper +from cms_rest.logic.customer_logic import CustomerLogic +from cms_rest.logic.error_base import ErrorStatus +from cms_rest.utils import authentication + +from cms_rest.logger import get_logger +LOG = get_logger(__name__) + + +class EnabledController(rest.RestController): + @wsexpose(CustomerResultWrapper, str, body=Enabled, rest_content_types='json') + def put(self, customer_uuid, enable): + authentication.authorize(request, 'customers:enable') + try: + LOG.info("EnabledController - (put) customer id {0} enable: {1}".format(customer_uuid, enable)) + customer_logic = CustomerLogic() + result = customer_logic.enable(customer_uuid, enable, request.transaction_id) + LOG.info("EnabledController - change enable (put) finished well: " + str(result)) + + event_details = 'Customer {} {}'.format(customer_uuid, + 'enabled' if enable.enabled else 'disabled') + utils.audit_trail('Change enable', request.transaction_id, + request.headers, customer_uuid, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("EnabledController - Failed to Change enable", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("EnabledController - change enable (put) - Failed to Change enable", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + return result + + @wsexpose(None, str, rest_content_types='json') + def post(self, customer_id): + raise err_utils.get_error(request.transaction_id, status_code=405) + + @wsexpose(None, str, rest_content_types='json') + def get(self, customer_id): + raise err_utils.get_error(request.transaction_id, status_code=405) + + @wsexpose(None, str, rest_content_types='json') + def delete(self, customer_id): + raise err_utils.get_error(request.transaction_id, status_code=405) diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/metadata.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/metadata.py new file mode 100755 index 00000000..2c4aae20 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/metadata.py @@ -0,0 +1,75 @@ +from pecan import rest, request +from wsmeext.pecan import wsexpose +from cms_rest.model.Models import CustomerResultWrapper +from orm_common.utils import utils +from orm_common.utils import api_error_utils as err_utils + +from cms_rest.logic.error_base import ErrorStatus +from cms_rest.model.Models import MetadataWrapper +import cms_rest.logic.metadata_logic as logic +from cms_rest.utils import authentication + +from cms_rest.logger import get_logger +LOG = get_logger(__name__) + + +class MetadataController(rest.RestController): + @wsexpose(CustomerResultWrapper, str, body=MetadataWrapper, rest_content_types='json') + def post(self, customer_uuid, metadata): + authentication.authorize(request, 'customers:add_metadata') + try: + res = logic.add_customer_metadata(customer_uuid, metadata, request.transaction_id) + + event_details = 'Customer {} metadata added'.format(customer_uuid) + utils.audit_trail('add customer metadata', request.transaction_id, + request.headers, customer_uuid, + event_details=event_details) + return res + except AttributeError as ex: + raise err_utils.get_error(request.transaction_id, + message=ex.message, status_code=409) + except ValueError as ex: + raise err_utils.get_error(request.transaction_id, + message=ex.message, status_code=404) + except ErrorStatus as ex: + LOG.log_exception("MetaDataController - Failed to add metadata", ex) + raise err_utils.get_error(request.transaction_id, + status_code=ex.status_code) + except LookupError as ex: + LOG.log_exception("MetaDataController - {0}".format(ex.message), ex) + raise err_utils.get_error(request.transaction_id, + message=ex.message, status_code=400) + except Exception as ex: + LOG.log_exception("MetaDataController - Failed to add metadata", ex) + raise err_utils.get_error(request.transaction_id, + status_code=500, error_details=str(ex)) + + @wsexpose(CustomerResultWrapper, str, body=MetadataWrapper, rest_content_types='json') + def put(self, customer_uuid, metadata): + authentication.authorize(request, 'customers:update_metadata') + try: + res = logic.update_customer_metadata(customer_uuid, metadata, request.transaction_id) + + event_details = 'Customer {} metadata updated'.format(customer_uuid) + utils.audit_trail('update customer metadata', + request.transaction_id, request.headers, + customer_uuid, event_details=event_details) + return res + except AttributeError as ex: + raise err_utils.get_error(request.transaction_id, + message=ex.message, status_code=400) + except ValueError as ex: + raise err_utils.get_error(request.transaction_id, + message=ex.message, status_code=404) + except ErrorStatus as ex: + LOG.log_exception("MetaDataController - Failed to add metadata", ex) + raise err_utils.get_error(request.transaction_id, + status_code=ex.status_code) + except LookupError as ex: + LOG.log_exception("MetaDataController - {0}".format(ex.message), ex) + raise err_utils.get_error(request.transaction_id, + message=ex.message, status_code=400) + except Exception as ex: + LOG.log_exception("MetaDataController - Failed to add metadata", ex) + raise err_utils.get_error(request.transaction_id, + status_code=500, error_details=str(ex)) diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/regions.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/regions.py new file mode 100755 index 00000000..c830a552 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/regions.py @@ -0,0 +1,133 @@ +from oslo_db.exception import DBDuplicateEntry +from pecan import rest, request +from wsmeext.pecan import wsexpose + +from orm_common.utils import utils +from orm_common.utils import api_error_utils as err_utils + +from cms_rest.controllers.v1.orm.customer.users import UserController +from cms_rest.model.Models import Region, RegionResultWrapper +from cms_rest.logic.customer_logic import CustomerLogic +from cms_rest.logic.error_base import ErrorStatus, DuplicateEntryError +from cms_rest.utils import authentication + +from cms_rest.logger import get_logger +LOG = get_logger(__name__) + + +class RegionController(rest.RestController): + + users = UserController() + + @wsexpose([str], str, str, rest_content_types='json') + def get(self, customer_id, region_id): + return ["This is the regions controller ", "customer id: " + customer_id] + + @wsexpose(RegionResultWrapper, str, body=[Region], rest_content_types='json', status_code=200) + def post(self, customer_id, regions): + LOG.info("RegionController - Add Regions (post) customer id {0} regions: {1}".format(customer_id, str(regions))) + authentication.authorize(request, 'customers:add_region') + try: + customer_logic = CustomerLogic() + result = customer_logic.add_regions(customer_id, regions, request.transaction_id) + LOG.info("RegionController - Add Regions (post) finished well: " + str(result)) + + event_details = 'Customer {} regions: {} added'.format( + customer_id, [r.name for r in regions]) + utils.audit_trail('add regions', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except DBDuplicateEntry as exception: + LOG.log_exception("RegionController - Add Regions (post) - region already exists", exception) + raise err_utils.get_error(request.transaction_id, + status_code=409, + message='Region already exists', + error_details=exception.message) + + except ErrorStatus as exception: + LOG.log_exception("CustomerController - Failed to update regions", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("RegionController - Add Regions (post) - Failed to update regions", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + return result + + @wsexpose(RegionResultWrapper, str, body=[Region], rest_content_types='json', status_code=200) + def put(self, customer_id, regions): + LOG.info("RegionController - Replace Regions (put) customer id {0} regions: {1}".format(customer_id, str(regions))) + authentication.authorize(request, 'customers:update_region') + self.validate_put_url() + try: + customer_logic = CustomerLogic() + result = customer_logic.replace_regions(customer_id, regions, request.transaction_id) + LOG.info("RegionController - Replace Regions (put) finished well: " + str(result)) + + event_details = 'Customer {} regions: {} updated'.format( + customer_id, [r.name for r in regions]) + utils.audit_trail('Replace regions', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("CustomerController - Failed to Replace regions", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("RegionController - Replace Regions (put) - Failed to replace regions", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + return result + + @wsexpose(None, str, str, status_code=204) + def delete(self, customer_id, region_id): + LOG.info("RegionController - Delete Region (delete) customer id {0} region_id: {1}".format(customer_id, region_id)) + authentication.authorize(request, 'customers:delete_region') + try: + customer_logic = CustomerLogic() + customer_logic.delete_region(customer_id, region_id, request.transaction_id) + LOG.info("RegionController - Delete Region (delete) finished well") + + event_details = 'Customer {} region: {} deleted'.format( + customer_id, region_id) + utils.audit_trail('delete region', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except ValueError as exception: + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=404) + except ErrorStatus as exception: + LOG.log_exception("CustomerController - Failed to delete region", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("RegionController - Failed in delete Region", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + @staticmethod + def validate_put_url(): + url_elements = request.path.split('/') + last_index = -2 if url_elements[-1] == '' else -1 + # If there's an element after 'regions', it is a region ID + # which is currently unsupported + if url_elements[last_index - 1] == 'regions': + LOG.debug('Method not allowed for a specific region in Request: {}'.format(request.path)) + raise err_utils.get_error(request.transaction_id, + message='Method not allowed for a specific region', + status_code=405) diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/root.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/root.py new file mode 100755 index 00000000..804c93e1 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/root.py @@ -0,0 +1,184 @@ +from pecan import rest, request, response +import oslo_db +from wsmeext.pecan import wsexpose + +from cms_rest.model.Models import Customer, CustomerResultWrapper, CustomerSummaryResponse +from cms_rest.controllers.v1.orm.customer.users import DefaultUserController +from cms_rest.controllers.v1.orm.customer.regions import RegionController +from cms_rest.controllers.v1.orm.customer.metadata import MetadataController +from cms_rest.controllers.v1.orm.customer.enabled import EnabledController +from cms_rest.logic.customer_logic import CustomerLogic + +from cms_rest.logic.error_base import ErrorStatus +from orm_common.utils import utils +from orm_common.utils import api_error_utils as err_utils +from cms_rest.utils import authentication + +from cms_rest.logger import get_logger + +LOG = get_logger(__name__) + + +class CustomerController(rest.RestController): + regions = RegionController() + users = DefaultUserController() + metadata = MetadataController() + enabled = EnabledController() + + @wsexpose(Customer, str, rest_content_types='json') + def get(self, customer_uuid): + LOG.info("CustomerController - GetCustomerDetails: uuid is " + customer_uuid) + authentication.authorize(request, 'customers:get_one') + try: + customer_logic = CustomerLogic() + result = customer_logic.get_customer(customer_uuid) + LOG.info("CustomerController - GetCustomerDetails finished well: " + str(result)) + + except ErrorStatus as exception: + LOG.log_exception("CustomerController - Failed to GetCustomerDetails", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("CustomerController - Failed to GetCustomerDetails", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + return result + + @wsexpose(CustomerResultWrapper, body=Customer, rest_content_types='json', status_code=201) + def post(self, customer): + LOG.info("CustomerController - CreateCustomer: " + str(customer)) + authentication.authorize(request, 'customers:create') + try: + uuid = None + if not customer.custId: + uuid = utils.make_uuid() + else: + if not CustomerController.validate_cust_id(customer.custId): + utils.audit_trail('create customer', request.transaction_id, request.headers, customer.custId) + raise ErrorStatus('400', None) + try: + uuid = utils.create_existing_uuid(customer.custId) + except TypeError: + raise ErrorStatus(409.1, 'Customer ID {0} already exists'.format(customer.custId)) + + customer_logic = CustomerLogic() + try: + result = customer_logic.create_customer(customer, uuid, request.transaction_id) + except oslo_db.exception.DBDuplicateEntry as exception: + raise ErrorStatus(409.2, 'Customer field {0} already exists'.format(exception.columns)) + + LOG.info("CustomerController - Customer Created: " + str(result)) + event_details = 'Customer {} {} created in regions: {}, with users: {}'.format( + uuid, customer.name, [r.name for r in customer.regions], + [u.id for u in customer.users]) + utils.audit_trail('create customer', request.transaction_id, + request.headers, uuid, + event_details=event_details) + return result + + except ErrorStatus as exception: + LOG.log_exception("CustomerController - Failed to CreateCustomer", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("CustomerController - Failed to CreateCustomer", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(CustomerResultWrapper, str, body=Customer, rest_content_types='json', status_code=200) + def put(self, customer_id, customer): + LOG.info("CustomerController - UpdateCustomer: " + str(customer)) + authentication.authorize(request, 'customers:update') + try: + customer_logic = CustomerLogic() + result = customer_logic.update_customer(customer, customer_id, request.transaction_id) + response.status = 200 + LOG.info("CustomerController - UpdateCustomer finished well: " + str(customer)) + + event_details = 'Customer {} {} updated in regions: {}, with users: {}'.format( + customer_id, customer.name, [r.name for r in customer.regions], + [u.id for u in customer.users]) + utils.audit_trail('update customer', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("Failed in UpdateCustomer", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("CustomerController - Failed to UpdateCustomer", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + return result + + @wsexpose(CustomerSummaryResponse, str, str, str, str, [str], + rest_content_types='json') + def get_all(self, region=None, user=None, starts_with=None, + contains=None, metadata=None): + LOG.info("CustomerController - GetCustomerlist") + authentication.authorize(request, 'customers:get_all') + try: + customer_logic = CustomerLogic() + result = customer_logic.get_customer_list_by_criteria(region, user, + starts_with, + contains, + metadata) + + return result + except ErrorStatus as exception: + LOG.log_exception("CustomerController - Failed to GetCustomerlist", exception) + raise err_utils.get_error(request.transaction_id, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("CustomerController - Failed to GetCustomerlist", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(None, str, rest_content_types='json', status_code=204) + def delete(self, customer_id): + authentication.authorize(request, 'customers:delete') + customer_logic = CustomerLogic() + + try: + LOG.info("CustomerController - DeleteCustomer: uuid is " + customer_id) + customer_logic.delete_customer_by_uuid(customer_id) + LOG.info("CustomerController - DeleteCustomer finished well") + + event_details = 'Customer {} deleted'.format(customer_id) + utils.audit_trail('delete customer', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("CustomerController - Failed to DeleteCustomer", + exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("CustomerController - Failed to DeleteCustomer", + exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @staticmethod + def validate_cust_id(cust_id): + # regex = re.compile('[a-zA-Z]') + # return regex.match(cust_id[0]) + return True diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/users.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/users.py new file mode 100755 index 00000000..ab0876e1 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/customer/users.py @@ -0,0 +1,246 @@ +from pecan import rest, request +from wsmeext.pecan import wsexpose + +from orm_common.utils import utils +from orm_common.utils import api_error_utils as err_utils + +from cms_rest.model.Models import User, UserResultWrapper +from cms_rest.logic.customer_logic import CustomerLogic +from cms_rest.logic.error_base import ErrorStatus, NotFound +from cms_rest.utils import authentication + +from cms_rest.logger import get_logger +LOG = get_logger(__name__) + + +class DefaultUserController(rest.RestController): + + @wsexpose([str], str, rest_content_types='json') + def get(self, customer_id): + return ["This is the users controller ", + "customer id: " + customer_id, + "user " + "default user"] + + @wsexpose(UserResultWrapper, str, body=[User], rest_content_types='json', status_code=200) + def put(self, customer_id, users): # replace default users to customer + LOG.info("DefaultUserController - Replace DefaultUsers (put) customer id {0} users: {1}".format(customer_id, str(users))) + authentication.authorize(request, 'customers:update_default_user') + try: + customer_logic = CustomerLogic() + result = customer_logic.replace_default_users(customer_id, users, request.transaction_id) + LOG.info("DefaultUserController - Replace DefaultUsers (put) Finished well customer id {0} users: {1}".format(customer_id, str(users))) + + except ErrorStatus as exception: + LOG.log_exception("DefaultUserController - Failed to replace default users", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except LookupError as exception: + LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=404) + + except Exception as exception: + result = UserResultWrapper(transaction_id="Users Not Added", users=[]) + LOG.log_exception("DefaultUserController - Failed to replace default users", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + return result + + @wsexpose(UserResultWrapper, str, body=[User], rest_content_types='json', status_code=200) + def post(self, customer_id, users): # add default users to customer + LOG.info("DefaultUserController - Add DefaultUsers (put) customer id {0} users: {1}".format(customer_id, str(users))) + authentication.authorize(request, 'customers:add_default_user') + try: + customer_logic = CustomerLogic() + result = customer_logic.add_default_users(customer_id, users, request.transaction_id) + LOG.info("DefaultUserController - Add DefaultUsers (post) Finished well customer id {0} users: {1}".format( + customer_id, str(users))) + + except ErrorStatus as exception: + LOG.log_exception("DefaultUserController - Failed to add default users", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except LookupError as exception: + LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=404) + + except Exception as exception: + result = UserResultWrapper(transaction_id="Users Not Added", users=[]) + LOG.log_exception("DefaultUserController - Failed to add default users", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + return result + + @wsexpose(None, str, str, status_code=204) + def delete(self, customer_id, user_id): + LOG.info("DefaultUserController - Delete DefaultUsers (delete) customer id {0} user_id: {1}".format(customer_id, user_id)) + authentication.authorize(request, 'customers:delete_default_user') + try: + customer_logic = CustomerLogic() + customer_logic.delete_default_users(customer_id, user_id, request.transaction_id) + LOG.info("DefaultUserController - Delete DefaultUsers (delete) Finished well customer id {0} user_id: {1}".format(customer_id, user_id)) + utils.audit_trail('delete default users', request.transaction_id, request.headers, customer_id) + + except ErrorStatus as exception: + LOG.log_exception("DefaultUserController - Failed to delete default users", exception) + raise err_utils.get_error(request.transaction_id, + status_code=exception.status_code) + + except LookupError as exception: + LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=404) + + except NotFound as e: + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=404) + + except Exception as exception: + LOG.log_exception("DefaultUserController - Failed in Delete default User", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + +class UserController(rest.RestController): + + @staticmethod + def _validate(args): + # validate if user didnt provide input json for users + # to prevent wsme to take the input from url params + if 'users' in args and args['users'] and not request.body: + raise err_utils.get_error(request.transaction_id, + message="bad request, no json body", + status_code=400) + + @wsexpose([str], str, str, rest_content_types='json') + def get(self, customer_id, region_id): + return ["This is the users controller ", + "customer id: " + customer_id, + "region id: " + region_id] + + @wsexpose(UserResultWrapper, str, str, body=[User], rest_content_types='json', status_code=200) + def post(self, customer_id, region_id, users): + self._validate(locals()) # more validations for input + title = "Add users to Region '{}' for customer: '{}', users: {}".format(region_id, customer_id, str(users)) + LOG.info("UserController - {}".format(title)) + authentication.authorize(request, 'customers:add_region_user') + try: + customer_logic = CustomerLogic() + result = customer_logic.add_users(customer_id, region_id, users, request.transaction_id) + LOG.info("UserController - {} Finished well".format(title)) + + event_details = 'Customer {} users: {} added in region {}'.format( + customer_id, [u.id for u in users], region_id) + utils.audit_trail('add users', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("DefaultUserController - Failed to {}".format(title), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except LookupError as exception: + LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=404) + + except Exception as exception: + result = UserResultWrapper(transaction_id="Users Not Added", users=[]) + LOG.log_exception("UserController - Failed to Add Users (post)", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + return result + + @wsexpose(UserResultWrapper, str, str, body=[User], rest_content_types='json', status_code=200) + def put(self, customer_id, region_id, users): + self._validate(locals()) # more validations for input + title = "Replace users to Region '{}' for customer: '{}', users: {}".format(region_id, customer_id, str(users)) + LOG.info("UserController - {}".format(title)) + authentication.authorize(request, 'customers:update_region_user') + try: + customer_logic = CustomerLogic() + result = customer_logic.replace_users(customer_id, region_id, users, request.transaction_id) + LOG.info("UserController - {} Finished well".format(title)) + + event_details = 'Customer {} users: {} updated in region {}'.format( + customer_id, [u.id for u in users], region_id) + utils.audit_trail('replace users', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("DefaultUserController - Failed to {}".format(title), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except LookupError as exception: + LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=404) + + except Exception as exception: + result = UserResultWrapper(transaction_id="Users Not Replaced", users=[]) + LOG.log_exception("UserController - Failed to Replaced Users (put)", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + return result + + @wsexpose(None, str, str, str, status_code=204) + def delete(self, customer_id, region_id, user_id): + LOG.info("UserController - Delete User (delete) customer id {0} region_id: {1} user_id: {2}".format(customer_id, region_id, user_id)) + authentication.authorize(request, 'customers:delete_region_user') + try: + customer_logic = CustomerLogic() + customer_logic.delete_users(customer_id, region_id, user_id, request.transaction_id) + LOG.info("UserController - Delete User (delete) Finished well customer id {0} region_id: {1} user_id: {2}".format(customer_id, region_id, user_id)) + + event_details = 'Customer {} user: {} deleted in region {}'.format( + customer_id, user_id, region_id) + utils.audit_trail('delete users', request.transaction_id, + request.headers, customer_id, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("DefaultUserController - Failed to delete users", exception) + raise err_utils.get_error(request.transaction_id, + status_code=exception.status_code) + + except LookupError as exception: + LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=404) + + except NotFound as e: + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=404) + + except Exception as exception: + LOG.log_exception("UserController - Failed to Delete User (delete) ", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/logs.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/logs.py new file mode 100644 index 00000000..e8d7b1bb --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/logs.py @@ -0,0 +1,65 @@ +import logging + +from pecan import rest +import wsme +from wsmeext.pecan import wsexpose + +logger = logging.getLogger(__name__) + + +class LogChangeResultWSME(wsme.types.DynamicBase): + """log change result wsme type.""" + + result = wsme.wsattr(str, mandatory=True, default=None) + + def __init__(self, **kwargs): + """"init method.""" + super(LogChangeResult, self).__init__(**kwargs) + + +class LogChangeResult(object): + """log change result type.""" + + def __init__(self, result): + """"init method.""" + self.result = result + + +class LogsController(rest.RestController): + """Logs Audit controller.""" + + @wsexpose(LogChangeResultWSME, str, status_code=201, + rest_content_types='json') + def put(self, level): + """update log level. + + :param level: the log level text name + :return: + """ + + logger.info("Changing log level to [{}]".format(level)) + try: + log_level = logging._levelNames.get(level.upper()) + if log_level is not None: + self._change_log_level(log_level) + result = "Log level changed to {}.".format(level) + logger.info(result) + else: + raise Exception( + "The given log level [{}] doesn't exist.".format(level)) + except Exception as e: + result = "Fail to change log_level. Reason: {}".format( + e.message) + logger.error(result) + return LogChangeResult(result) + + @staticmethod + def _change_log_level(log_level): + path = __name__.split('.') + if len(path) > 0: + root = path[0] + root_logger = logging.getLogger(root) + root_logger.setLevel(log_level) + else: + logger.info("Fail to change log_level to [{}]. " + "the given log level doesn't exist.".format(log_level)) diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/orm/root.py b/orm/services/customer_manager/cms_rest/controllers/v1/orm/root.py new file mode 100755 index 00000000..f6708f01 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/orm/root.py @@ -0,0 +1,10 @@ +from cms_rest.controllers.v1.orm.customer.root import CustomerController +from cms_rest.controllers.v1.orm.logs import LogsController +from cms_rest.controllers.v1.orm.configuration import ConfigurationController +from pecan.rest import RestController + + +class OrmController(RestController): + configuration = ConfigurationController() + customers = CustomerController() + logs = LogsController() diff --git a/orm/services/customer_manager/cms_rest/controllers/v1/root.py b/orm/services/customer_manager/cms_rest/controllers/v1/root.py new file mode 100644 index 00000000..e471f361 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/controllers/v1/root.py @@ -0,0 +1,6 @@ +from cms_rest.controllers.v1.orm.root import OrmController +from pecan.rest import RestController + + +class V1Controller(RestController): + orm = OrmController() diff --git a/orm/services/customer_manager/cms_rest/data/__init__.py b/orm/services/customer_manager/cms_rest/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/data/data_manager.py b/orm/services/customer_manager/cms_rest/data/data_manager.py new file mode 100755 index 00000000..6ed9b19f --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/data_manager.py @@ -0,0 +1,279 @@ +import oslo_db +from oslo_db.sqlalchemy import session as db_session +from sqlalchemy.event import listen +from sqlalchemy import or_ +from cms_rest.logic.error_base import ErrorStatus + +from pecan import conf + +import logging + +from cms_rest.data.sql_alchemy.models import CmsRole, CmsUser, Customer, \ + CustomerRegion, Quota, QuotaFieldDetail, \ + Region, UserRole +from cms_rest.data.sql_alchemy.customer_record import CustomerRecord +from cms_rest.data.sql_alchemy.customer_region_record import \ + CustomerRegionRecord +from cms_rest.data.sql_alchemy.user_role_record import UserRoleRecord + +LOG = logging.getLogger(__name__) + + +# event handling +def on_before_flush(session, flush_context, instances): + print("on_before_flush:", str(flush_context)) + for model in session.new: + if hasattr(model, "validate"): + model.validate("new") + + for model in session.dirty: + if hasattr(model, "validate"): + model.validate("dirty") + + +class DataManager(object): + + def __init__(self, connection_string=None): + + if not connection_string: + connection_string = conf.database.connection_string + + self._engine_facade = db_session.EngineFacade(connection_string, autocommit=False) + self._session = None + listen(self.session, 'before_flush', on_before_flush) + self.image_record = None + + def get_engine(self): + return self._engine_facade.get_engine() + + @property + def engine(self): + return self.get_engine() + + def get_session(self): + if not self._session: + self._session = self._engine_facade.get_session() + return self._session + + @property + def session(self): + return self.get_session() + + def flush(self): + try: + self.session.flush() + except oslo_db.exception.DBDuplicateEntry as exception: + raise ErrorStatus(409.2, 'Duplicate Entry {0} already exist'.format(exception.columns)) + except Exception: + raise + + def commit(self): + self.session.commit() + + def expire_all(self): + self.session.expire_all() + + def rollback(self): + self.session.rollback() + + def close(self): + self.session.close() + self.engine.dispose() + + def begin_transaction(self): + pass + # no need to begin transaction - the transaction is open automatically + + def get_all_cms_users(self, start=0, limit=0): + cms_users = self.session.query(CmsUser) + return cms_users.all() + + def get_cusomer_by_id(self, customer_id): + customer = self.session.query(Customer).filter( + Customer.id == customer_id) + return customer.first() + + def get_cusomer_by_uuid(self, uuid): + customer = self.session.query(Customer).filter(Customer.uuid == uuid) + return customer.first() + + def get_cusomer_by_name(self, name): + customer = self.session.query(Customer).filter(Customer.name == name) + return customer.first() + + def get_cusomer_by_uuid_or_name(self, cust): + customer = self.session.query(Customer).filter( + or_(Customer.uuid == cust, + Customer.name == cust)) + + return customer.first() + + def get_quota_by_id(self, quota_id): + quota = self.session.query(Quota).filter(Quota.id == quota_id) + return quota.first() + + def get_record(self, record_name): + if record_name == "Customer" or record_name == "customer": + if not hasattr(self, "customer_record"): + self.customer_record = CustomerRecord(self.session) + return self.customer_record + + if record_name == "CustomerRegion" or record_name == "customer_region": + if not hasattr(self, "customer_region_record"): + self.customer_region_record = CustomerRegionRecord( + self.session) + return self.customer_region_record + + if record_name == "UserRole" or record_name == "user_role": + if not hasattr(self, "user_role_record"): + self.user_role_record = UserRoleRecord(self.session) + return self.user_role_record + return None + + def add_user(self, user): + db_user = self.session.query(CmsUser).filter( + CmsUser.name == user.id).first() + if not (db_user is None): + return db_user + + db_user = CmsUser(name=user.id) + self.session.add(db_user) + self.flush() + + return db_user + + def add_role(self, role): + db_role = self.session.query(CmsRole).filter( + CmsRole.name == role).first() + if not (db_role is None): + return db_role + + db_role = CmsRole(name=role) + self.session.add(db_role) + self.flush() + + return db_role + + def add_quota(self, customer_id, region_id, quota): + quota_attrs = ['compute', 'storage', 'network'] + for quota_type in quota_attrs: + quota_by_type = getattr(quota, quota_type) + if len(quota_by_type) == 0: + continue + + sql_quota = Quota( + customer_id=customer_id, + region_id=region_id, + quota_type=quota_type + ) + self.session.add(sql_quota) + self.flush() + + # FIXME: next line assumes that only one quota of each type is + # available and thus quota_by_type[0] is used + for field_key, field_value in DataManager.get_dict_from_quota( + quota_by_type[0], quota_type).items(): + sql_quota_field_detail = QuotaFieldDetail( + quota_id=sql_quota.id, + field_key=field_key, + field_value=field_value + ) + self.session.add(sql_quota_field_detail) + + self.flush() + + def add_customer(self, customer, uuid): + sql_customer = Customer( + uuid=uuid, + name=customer.name, + enabled=customer.enabled, + description=customer.description + ) + + self.session.add(sql_customer) + self.flush() + + return sql_customer + + def add_user_role(self, user_id, role_id, customer_id, region_id, + adding=False): + try: + sql_user_role = self.session.query(UserRole).filter( + UserRole.customer_id == customer_id, + UserRole.user_id == user_id, + UserRole.region_id == region_id, + UserRole.role_id == role_id).first() + if sql_user_role: + if adding: + raise Exception('Duplicate User Role') + return sql_user_role + + sql_user_role = UserRole( + user_id=user_id, + role_id=role_id, + customer_id=customer_id, + region_id=region_id + ) + + self.session.add(sql_user_role) + self.flush() + + return sql_user_role + except Exception as exception: + raise + + def add_customer_region(self, customer_id, region_id): + customer_region = CustomerRegion( + customer_id=customer_id, + region_id=region_id + ) + + self.session.add(customer_region) + self.flush() + + def add_region(self, region): + db_region = self.session.query(Region).filter( + Region.name == region.name).first() + if not (db_region is None): + return db_region + + db_region = Region(name=region.name, type=region.type) + self.session.add(db_region) + self.flush() + + return db_region + + def get_region_id_by_name(self, name): + region_id = self.session.query(Region.id).filter( + Region.name == name).scalar() + + return region_id + + def get_customer_id_by_uuid(self, uuid): + customer_id = self.session.query(Customer.id).filter( + Customer.uuid == uuid).scalar() + + return customer_id + + @classmethod + def get_dict_from_quota(cls, quota, quota_type): + types = { + 'compute': ['instances', 'injected_files', 'key_pairs', 'ram', + 'vcpus', 'metadata_items', + 'injected_file_content_bytes', 'floating_ips', + 'fixed_ips', 'injected_file_path_bytes', + 'server_groups', 'server_group_members' + ], + 'storage': ['gigabytes', 'snapshots', 'volumes'], + 'network': ['floating_ips', 'networks', 'ports', 'routers', + 'subnets', 'security_groups', 'security_group_rules', + 'health_monitor', 'member', 'nat_instance', 'pool', + 'route_table', 'vip' + ] + } + + quota_dict = {} + for attr in types[quota_type]: + quota_dict[attr] = getattr(quota, attr) + + return quota_dict diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/__init__.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/base.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/base.py new file mode 100644 index 00000000..c64447da --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/sql_alchemy/base.py @@ -0,0 +1,2 @@ +from sqlalchemy.ext.declarative import declarative_base +Base = declarative_base() diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/cms_user_record.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/cms_user_record.py new file mode 100755 index 00000000..c6aa2f5a --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/sql_alchemy/cms_user_record.py @@ -0,0 +1,44 @@ +from cms_rest.data.sql_alchemy.models import CmsUser + +from cms_rest.logger import get_logger +LOG = get_logger(__name__) + + +class CmsUserRecord: + + def __init__(self, session=None): + + # this model uses for the parameters for any access methods - not as instance of record in the table + self.__cms_user = CmsUser() + # self.setRecordData(self.cms_user) + # self.cms_user.Clear() + + self.__TableName = "cms_user" + + if (session): + self.session = session + + def setDBSession(self, session): + self.session = session + + @property + def cms_user(self): + return self.__cms_user + + @cms_user.setter + def cms_user(self, cms_user): + self.__cms_usern = cms_user + + def insert(self, cms_user): + try: + self.session.add(cms_user) + except Exception as exception: + LOG.log_exception("Failed to insert cms_user", exception) + # LOG.error("Failed to insert cms_user" + str(cms_user)+" Exception:" + str(exception)) + raise + + def get_cms_user_id_from_name(self, cms_user_name): + result = self.session.connection().scalar("SELECT id from cms_user WHERE name = \"%s\"" % (cms_user_name)) + if result is not None: + return int(result) + return result diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_record.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_record.py new file mode 100755 index 00000000..fc47a609 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_record.py @@ -0,0 +1,173 @@ +from cms_rest.data.sql_alchemy.models import Customer, Region, CustomerRegion, UserRole, CmsUser, CustomerMetadata +from sqlalchemy import and_, func +from __builtin__ import int + +from cms_rest.logger import get_logger + +LOG = get_logger(__name__) + + +class CustomerRecord: + def __init__(self, session): + + # this model is uses only for the parameters of access mothods, not an instance of model in the database + self.__customers = Customer() + # self.setRecordData(self.__customers) + # self.__customers.Clear() + self.__TableName = "customer" + + if session: + self.setDBSession(session) + + def setDBSession(self, session): + self.session = session + + @property + def customer(self): + return self.__customer + + @customer.setter + def customer(self, customer): + self.__customer = customer + + def insert(self, customer): + try: + self.session.add(customer) + except Exception as exception: + LOG.log_exception("Failed to insert Customer" + str(customer), exception) + # LOG.error("Failed to insert customer" + str(customer) + " Exception:" + str(exception)) + raise + + def delete_by_primary_key(self, customer_id): + result = self.session.connection().execute("delete from customer where id = %d" % (customer_id)) + return result + + def read_by_primary_key(self): + return self.read_customer(self.__customer.id) + + def read_customer(self, customer_id): + try: + customer = self.session.query(Customer).filter(Customer.id == customer_id) + return customer.first() + + except Exception as exception: + message = "Failed to read_customer:customer_id: %d " % (customer_id) + LOG.log_exception(message, exception) + raise + + def read_customer_by_uuid(self, customer_uuid): + try: + customer = self.session.query(Customer).filter(Customer.uuid == customer_uuid) + return customer.first() + + except Exception as exception: + message = "Failed to read_customer:customer_uuid: %d " % customer_uuid + LOG.log_exception(message, exception) + raise + + def get_customer_id_from_uuid(self, uuid): + result = self.session.connection().scalar("SELECT id from customer WHERE uuid = \"%s\"" % uuid) + + if result: + return int(result) + else: + return None + + def delete_customer_by_uuid(self, uuid): + try: + result = self.session.query(Customer).filter( + Customer.uuid == uuid).delete() + return result + + except Exception as exception: + message = "Failed to delete_customer_by_uuid: uuid: {0}".format(uuid) + LOG.log_exception(message, exception) + raise + + def _build_meta_query(self, metadata): + """ + build query for having list of metadata + get list of keys and list of values quereis + :param metadata: + :return: + """ + metadata_values = [value.split(':')[1] for value in metadata if + ':' in value] + query = [CustomerMetadata.field_key.in_( + [key.split(':')[0] if ':' in key else key for key in metadata])] + # check if search by only keys .. + if metadata_values: + query.append(CustomerMetadata.field_value.in_( + [value.split(':')[1] if ':' in value else '' for value in + metadata])) + return query + + def get_customers_by_criteria(self, **criteria): + + try: + LOG.info("get_customers_by_criteria: criteria: {0}".format(criteria)) + region = criteria['region'] if 'region' in criteria else None + user = criteria['user'] if 'user' in criteria else None + rgroup = criteria['rgroup'] if 'rgroup' in criteria else None + starts_with = criteria['starts_with'] if 'starts_with' in criteria else None + contains = criteria['contains'] if 'contains' in criteria else None + metadata = criteria['metadata'] if 'metadata' in criteria else None + + query = self.session.query(Customer) + + if metadata: + query = query.join(CustomerMetadata).filter( + *self._build_meta_query(metadata)).group_by( + CustomerMetadata.customer_id).having( + func.count() == len(metadata)) + + if starts_with: + query = query.filter( + Customer.name.ilike("{}%".format(starts_with))) + + if contains: + query = query.filter( + Customer.name.ilike("%{}%".format(contains))) + + if region: + query = query.join(CustomerRegion).filter(CustomerRegion.customer_id == Customer.id) + query = query.join(Region).filter(Region.id == CustomerRegion.region_id, Region.type == 'single', Region.name == region) + + if user: + query = query.join(UserRole, UserRole.customer_id == Customer.id).filter(UserRole.region_id == Region.id) + query = query.join(CmsUser).filter(CmsUser.id == UserRole.user_id, CmsUser.name == user) + elif user: + query = query.join(UserRole, UserRole.customer_id == Customer.id) + query = query.join(CmsUser).filter(CmsUser.id == UserRole.user_id, CmsUser.name == user) + + if rgroup: + if not region: # avoid same CustomerRegion join twice + query = query.join(CustomerRegion).filter(CustomerRegion.customer_id == Customer.id) + + query = query.join(Region).filter(Region.id == CustomerRegion.region_id, Region.type == 'group', Region.name == rgroup) + + if user: + query = query.join(UserRole, UserRole.customer_id == Customer.id).filter( + UserRole.region_id == Region.id) + query = query.join(CmsUser).filter(CmsUser.id == UserRole.user_id, CmsUser.name == user) + + query = self.customise_query(query, criteria) + return query.all() + + except Exception as exception: + message = "Failed to get_customers_by_criteria: criteria: {0}".format(criteria) + LOG.log_exception(message, exception) + raise + + def customise_query(self, query, kw): + start = int(kw['start']) if 'start' in kw else 0 + limit = int(kw['limit']) if 'limit' in kw else 0 + + if start > 0: + query = query.offset(start) + + if limit > 0: + query = query.limit(limit) + + print str(query) + return query diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_region_record.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_region_record.py new file mode 100755 index 00000000..c3821795 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/sql_alchemy/customer_region_record.py @@ -0,0 +1,92 @@ +from cms_rest.data.sql_alchemy.models import CustomerRegion +from cms_rest.data.sql_alchemy.customer_record import CustomerRecord +from cms_rest.data.sql_alchemy.region_record import RegionRecord + +from cms_rest.logger import get_logger + +LOG = get_logger(__name__) + + +class CustomerRegionRecord: + def __init__(self, session): + + # thie model uses for the parameters for any acceess methods - not as instance of record in the table + self.__customer_region = CustomerRegion() + # self.setRecordData(self.__customers) + # self.__customers.Clear() + + self.__TableName = "customer_region" + + if (session): + self.session = session + + def setDBSession(self, session): + self.session = session + + @property + def customer_region(self): + return self.__customer_region + + @customer_region.setter + def customer_region(self): + self.__customer_region = CustomerRegion() + + def insert(self, customer_region): + try: + self.session.add(customer_region) + except Exception as exception: + LOG.log_exception("Failed to insert customer_region" + str(customer_region), exception) + raise + + def get_regions_for_customer(self, customer_uuid): + customer_regions = [] + + try: + customer_record = CustomerRecord(self.session) + customer_id = customer_record.get_customer_id_from_uuid(customer_uuid) + query = self.session.query(CustomerRegion).filter(CustomerRegion.customer_id == customer_id) + + for customer_region in query.all(): + customer_regions.append(customer_region) + return customer_regions + + except Exception as exception: + message = "Failed to get_region_names_for_customer: %d" % (customer_id) + LOG.log_exception(message, exception) + raise + + def delete_region_for_customer(self, customer_id, region_name): + # customer_id can be a uuid (type of string) or id (type of int) + # if customer_id is uuid I get id from uuid and use the id in the next sql command + if isinstance(customer_id, basestring): + customer_record = CustomerRecord(self.session) + customer_id = customer_record.get_customer_id_from_uuid(customer_id) + # get region id by the name I got (region_name) + region_record = RegionRecord(self.session) + region_id = region_record.get_region_id_from_name(region_name) + if region_id is None: + raise ValueError( + 'region with the region name {0} not found'.format( + region_name)) + result = self.session.connection().execute( + "delete from customer_region where customer_id = %d and region_id = %d" % (customer_id, region_id)) + self.session.flush() + + if result.rowcount == 0: + LOG.warn('region with the region name {0} not found'.format(region_name)) + raise ValueError('region with the region name {0} not found'.format(region_name)) + + LOG.debug("num records deleted: " + str(result.rowcount)) + return result + + def delete_all_regions_for_customer(self, customer_id): # not including default region which is -1 + # customer_id can be a uuid (type of string) or id (type of int) + # if customer_id is uuid I get id from uuid and use the id in the next sql command + if isinstance(customer_id, basestring): + customer_record = CustomerRecord(self.session) + customer_id = customer_record.get_customer_id_from_uuid(customer_id) + + result = self.session.connection().execute( + "delete from customer_region where customer_id = {} and region_id <> -1 ".format(customer_id)) + print "num records deleted from customer regions: " + str(result.rowcount) + return result diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/models.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/models.py new file mode 100755 index 00000000..3fe7d474 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/sql_alchemy/models.py @@ -0,0 +1,406 @@ +from sqlalchemy import Column, Integer, String, SmallInteger, ForeignKey +from sqlalchemy.orm import relationship +from cms_rest.data.sql_alchemy.base import Base +import wsme + +from oslo_db.sqlalchemy import models + +import cms_rest.model.Models as WsmeModels + + +class CMSBaseModel(models.ModelBase): + """Base class from CMS Models.""" + + __table_args__ = {'mysql_engine': 'InnoDB'} + + +''' +' CmsUser is a DataObject and contains all the fields defined in CmsUser table record. +' defined as SqlAlchemy model map to a table +''' + + +class CmsRole(Base, CMSBaseModel): + __tablename__ = 'cms_role' + + id = Column(Integer, primary_key=True) + name = Column(String(64), nullable=False) + + def __json__(self): + return dict( + id=self.id, + name=self.name + ) + + +''' +' CmsUser is a DataObject and contains all the fields defined in CmsUser table record. +' defined as SqlAlchemy model map to a table +''' + + +class CmsUser(Base, CMSBaseModel): + __tablename__ = 'cms_user' + + id = Column(Integer, primary_key=True) + name = Column(String(64), nullable=False, unique=True) + + def __json__(self): + return dict( + id=self.id, + name=self.name + ) + + +''' +' Customer is a DataObject and contains all the fields defined in Customer table record. +' defined as SqlAlchemy model map to a table +''' + + +class Customer(Base, CMSBaseModel): + __tablename__ = "customer" + + id = Column(Integer, primary_key=True) + uuid = Column(String(64), nullable=False, unique=True) + name = Column(String(64), nullable=False, unique=True) + description = Column(String(255), nullable=False) + enabled = Column(SmallInteger, nullable=False) + customer_customer_regions = relationship("CustomerRegion", cascade="all, delete, delete-orphan") + customer_metadata = relationship("CustomerMetadata", cascade="all, delete, delete-orphan") + + def __json__(self): + return dict( + id=self.id, + uuid=self.uuid, + name=self.name, + description=self.description, + enabled=self.enabled, + customer_customer_regions=[customer_region.__json__() for customer_region in + self.customer_customer_regions], + customer_metadata=[customer_metadata.__json__() for customer_metadata in self.customer_metadata] + ) + + def get_dict(self): + return self.__json__() + + def get_proxy_dict(self): + proxy_dict = { + "uuid": self.uuid, + "name": self.name, + "description": self.description, + "enabled": 1 if self.enabled else 0 + } + + default_customer_region = self.get_default_customer_region() + if default_customer_region: + proxy_dict["default_region"] = default_customer_region.get_proxy_dict() + + real_customer_regions = self.get_real_customer_regions() + proxy_dict["regions"] = [customer_region.get_proxy_dict() for customer_region in real_customer_regions] + proxy_dict["metadata"] = [customer_metadata.get_proxy_dict() for customer_metadata in self.customer_metadata] + + return proxy_dict + + def get_default_customer_region(self): + for customer_region in self.customer_customer_regions: + if customer_region.region_id == -1: + return customer_region + return None + + def get_real_customer_regions(self): + real_customer_regions = [] + for customer_region in self.customer_customer_regions: + if customer_region.region_id != -1: + real_customer_regions.append(customer_region) + return real_customer_regions + + def to_wsme(self): + name = self.name + description = self.description + enabled = True if self.enabled else False + regions = [customer_region.to_wsme() for customer_region in self.customer_customer_regions if + customer_region.region_id != -1] + defaultRegion = [customer_region.to_wsme() for customer_region in self.customer_customer_regions if + customer_region.region_id == -1] + metadata = {} + for metadata1 in self.customer_metadata: + metadata[metadata1.field_key] = metadata1.field_value + + result = WsmeModels.Customer(description=description, + enabled=enabled, + name=name, + regions=regions, + users=defaultRegion[0].users if defaultRegion else [], + metadata=metadata, + defaultQuotas=defaultRegion[0].quotas if defaultRegion else [], + custId=self.uuid, + uuid=self.uuid) + return result + + +''' +' CustomerMetadata is a DataObject and contains all the fields defined in customer_metadata table record. +' defined as SqlAlchemy model map to a table +''' + + +class CustomerMetadata(Base, CMSBaseModel): + __tablename__ = "customer_metadata" + + customer_id = Column(Integer, ForeignKey('customer.id'), primary_key=True, nullable=False) + field_key = Column(String(64), primary_key=True, nullable=False) + field_value = Column(String(64), nullable=False) + + def __json__(self): + return dict( + customer_id=self.customer_id, + field_key=self.field_key, + field_value=self.field_value + ) + + def get_proxy_dict(self): + proxy_dict = { + self.field_key: self.field_value + } + + return proxy_dict + + +''' +' CustomerRegion is a DataObject and contains all the fields defined in CustomerRegion table record. +' defined as SqlAlchemy model map to a table +''' + + +class CustomerRegion(Base, CMSBaseModel): + __tablename__ = "customer_region" + + customer_id = Column(Integer, ForeignKey('customer.id'), primary_key=True, nullable=False) + region_id = Column(Integer, ForeignKey('region.id'), primary_key=True, nullable=False, index=True) + + customer_region_quotas = relationship("Quota", + uselist=True, + primaryjoin="and_(CustomerRegion.customer_id==Quota.customer_id," + "CustomerRegion.region_id==Quota.region_id)") + + customer_region_user_roles = relationship("UserRole", + uselist=True, + order_by="UserRole.user_id", + primaryjoin="and_(CustomerRegion.customer_id==UserRole.customer_id," + "CustomerRegion.region_id==UserRole.region_id)") + + region = relationship("Region", viewonly=True) + + def __json__(self): + return dict( + customer_id=self.customer_id, + region_id=self.region_id, + customer_region_quotas=[quota.__json__() for quota in self.customer_region_quotas], + customer_region_user_roles=[user_role.__json__() for user_role in self.customer_region_user_roles] + ) + + def get_proxy_dict(self): + proxy_dict = { + "name": self.region.name, + "action": "modify" + } + proxy_dict["quotas"] = [quota.get_proxy_dict() for quota in self.customer_region_quotas] + + proxy_dict["users"] = [] + user = None + + for user_role in self.customer_region_user_roles: + if user and user["id"] != user_role.user.name: + proxy_dict["users"].append(user) + user = {"id": user_role.user.name, "roles": [user_role.role.name]} + elif user is None: + user = {"id": user_role.user.name, "roles": [user_role.role.name]} + else: + user["roles"].append(user_role.role.name) + if user: + proxy_dict["users"].append(user) + + return proxy_dict + + def to_wsme(self): + name = self.region.name + type = self.region.type + quota = [] + quotas = {} + for region_quota in self.customer_region_quotas: + quotas[region_quota.quota_type] = {} + for quota_field in region_quota.quota_field_details: + quotas[region_quota.quota_type][quota_field.field_key] = quota_field.field_value or wsme.Unset + + if quotas: + compute = None + storage = None + network = None + + if 'compute' in quotas: + compute = [WsmeModels.Compute(**quotas['compute'])] + if 'storage' in quotas: + storage = [WsmeModels.Storage(**quotas['storage'])] + if 'network' in quotas: + network = [WsmeModels.Network(**quotas['network'])] + + quota = [WsmeModels.Quota(compute=compute, storage=storage, network=network)] + + users = [] + user = None + for user_role in self.customer_region_user_roles: + if user and user.id != user_role.user.name: + users.append(user) + user = WsmeModels.User(id=user_role.user.name, role=[user_role.role.name]) + elif user is None: + user = WsmeModels.User(id=user_role.user.name, role=[user_role.role.name]) + else: + user.role.append(user_role.role.name) + if user: + users.append(user) + + region = WsmeModels.Region(name=name, + type=type, + quotas=quota, + users=users) + return region + + +''' +' Quota is a DataObject and contains all the fields defined in Quota table record. +' defined as SqlAlchemy model map to a table +''' + + +class Quota(Base, CMSBaseModel): + __tablename__ = "quota" + + id = Column(Integer, primary_key=True) + customer_id = Column(Integer, ForeignKey('customer_region.customer_id'), nullable=False) + region_id = Column(Integer, ForeignKey('customer_region.region_id'), nullable=False) + quota_type = Column(String(64)) + quota_field_details = relationship("QuotaFieldDetail") + + def __json__(self): + return dict( + id=self.id, + customer_id=self.customer_id, + region_id=self.region_id, + quota_type=self.quota_type, + quota_field_details=[quota_field_detail.__json__() for quota_field_detail in self.quota_field_details] + ) + + def get_proxy_dict(self): + proxy_dict = {} + field_items = {} + for quota_field_detail in self.quota_field_details: + if quota_field_detail.field_value: + key = quota_field_detail.field_key + # key.replace("-", "_") + field_items[key] = quota_field_detail.field_value + + proxy_dict[self.quota_type] = field_items + + return proxy_dict + + def to_wsme(self): + compute = {} + storage = {} + network = {} + for quota_field in self.quota_field_details: + if self.quota_type == "compute": + if not quota_field.field_value: + quota_field.field_value = wsme.Unset + compute[quota_field.field_key] = quota_field.field_value + elif self.quota_type == "storage": + if not quota_field.field_value: + quota_field.field_value = wsme.Unset + storage[quota_field.field_key] = quota_field.field_value + elif self.quota_type == "network": + if not quota_field.field_value: + quota_field.field_value = wsme.Unset + network[quota_field.field_key] = quota_field.field_value + + quota = WsmeModels.Quota(compute=[WsmeModels.Compute(**compute)], + storage=[WsmeModels.Storage(**storage)], + network=[WsmeModels.Network(**network)]) + return quota + + +''' +' QuotaFieldDetail is a DataObject and contains all the fields defined in QuotaFieldDetail table record. +' defined as SqlAlchemy model map to a table +''' + + +class QuotaFieldDetail(Base, CMSBaseModel): + __tablename__ = "quota_field_detail" + + id = Column(Integer, primary_key=True) + # quota_id = Column(Integer, ForeignKey('Quota.id')) + quota_id = Column(Integer, ForeignKey('quota.id'), nullable=False) + field_key = Column(String(64), nullable=False) + field_value = Column(String(64), nullable=False) + + def __json__(self): + return dict( + id=self.id, + quota_id=self.quota_id, + field_key=self.field_key, + field_value=self.field_value + ) + + +''' +' Region is a DataObject and contains all the fields defined in Region table record. +' defined as SqlAlchemy model map to a table +''' + + +class Region(Base, CMSBaseModel): + __tablename__ = "region" + + id = Column(Integer, primary_key=True) + name = Column(String(64), nullable=False, unique=True) + type = Column(String(64), nullable=False) + + def __json__(self): + return dict( + id=self.id, + name=self.name, + type=self.type + ) + + +''' +' UserRole is a DataObject and contains all the fields defined in UserRole table record. +' defined as SqlAlchemy model map to a table +''' + + +class UserRole(Base, CMSBaseModel): + __tablename__ = "user_role" + + customer_id = Column(Integer, ForeignKey('customer_region.customer_id'), primary_key=True, nullable=False) + region_id = Column(Integer, ForeignKey('customer_region.region_id'), primary_key=True, nullable=False) + user_id = Column(Integer, ForeignKey('cms_user.id'), primary_key=True, nullable=False) + role_id = Column(Integer, ForeignKey('cms_role.id'), primary_key=True, nullable=False) + + user = relationship("CmsUser", viewonly=True) + role = relationship("CmsRole", viewonly=True) + + def __json__(self): + return dict( + customer_id=self.customer_id, + region_id=self.region_id, + user_id=self.user_id, + role_id=self.role_id + ) + + def to_wsme(self): + id = "" + role = [] + + user = WsmeModels.User(id=id, role=role) + return user diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/region_record.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/region_record.py new file mode 100755 index 00000000..d6909d1b --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/sql_alchemy/region_record.py @@ -0,0 +1,44 @@ +from cms_rest.data.sql_alchemy.models import Region +from cms_rest.data.sql_alchemy.customer_record import CustomerRecord + +from cms_rest.logger import get_logger +LOG = get_logger(__name__) + + +class RegionRecord: + + def __init__(self, session=None): + + # this model uses for the parameters for any access methods - not as instance of record in the table + self.__region = Region() + # self.setRecordData(self.region) + # self.region.Clear() + + self.__TableName = "region" + + if (session): + self.session = session + + def setDBSession(self, session): + self.session = session + + @property + def region(self): + return self.__region + + @region.setter + def region(self, region): + self.__regionn = region + + def insert(self, region): + try: + self.session.add(region) + except Exception as exception: + LOG.log_exception("Failed to insert region" + str(region), exception) + raise + + def get_region_id_from_name(self, region_name): + result = self.session.connection().scalar("SELECT id from region WHERE name = \"%s\"" % (region_name)) + if result is not None: + return int(result) + return result diff --git a/orm/services/customer_manager/cms_rest/data/sql_alchemy/user_role_record.py b/orm/services/customer_manager/cms_rest/data/sql_alchemy/user_role_record.py new file mode 100755 index 00000000..9ec6f636 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/data/sql_alchemy/user_role_record.py @@ -0,0 +1,83 @@ +from cms_rest.data.sql_alchemy.models import * +from cms_rest.data.sql_alchemy.customer_record import CustomerRecord +from cms_rest.data.sql_alchemy.cms_user_record import CmsUserRecord +from cms_rest.data.sql_alchemy.region_record import RegionRecord +from cms_rest.logic.error_base import NotFound + +from cms_rest.logger import get_logger +LOG = get_logger(__name__) + + +class UserRoleRecord: + + def __init__(self, session=None): + + # this model uses for the parameters for any access methods - not as instance of record in the table + self.__user_role = UserRole() + # self.setRecordData(self.user_role) + # self.user_role.Clear() + + self.__TableName = "user_role" + + if (session): + self.session = session + + def setDBSession(self, session): + self.session = session + + @property + def user_role(self): + return self.__user_role + + @user_role.setter + def user_role(self, user_role): + self.__user_rolen = user_role + + def insert(self, user_role): + try: + self.session.add(user_role) + except Exception as exception: + LOG.log_exception("Failed to insert user_role" + str(user_role), exception) + raise + + def delete_user_from_region(self, customer_id, region_id, user_id): + # customer_id can be a uuid (type of string) or id (type of int) + # if customer_id is uuid I get id from uuid and use the id in the next sql command + if isinstance(customer_id, basestring): + customer_record = CustomerRecord(self.session) + customer_id = customer_record.get_customer_id_from_uuid(customer_id) + + if isinstance(region_id, basestring): + region_query = region_id + region_record = RegionRecord(self.session) + region_id = region_record.get_region_id_from_name(region_id) + if region_id is None: + raise NotFound("region %s is not found" % region_query) + + if isinstance(user_id, basestring): + user_query = user_id + cms_user_record = CmsUserRecord(self.session) + user_id = cms_user_record.get_cms_user_id_from_name(user_id) + if user_id is None: + raise NotFound("user %s is not found" % user_query) + + result = self.session.connection().execute("delete from user_role where customer_id = %d and region_id = %d and user_id = %d" % (customer_id, region_id, user_id)) + print "num records deleted: " + str(result.rowcount) + return result + + def delete_all_users_from_region(self, customer_id, region_id): + # customer_id can be a uuid (type of string) or id (type of int) + # if customer_id is uuid I get id from uuid and use the id in the next sql command + if isinstance(customer_id, basestring): + customer_record = CustomerRecord(self.session) + customer_id = customer_record.get_customer_id_from_uuid(customer_id) + + if isinstance(region_id, basestring): + region_record = RegionRecord(self.session) + region_id = region_record.get_region_id_from_name(region_id) + + result = self.session.connection().execute( + "delete from user_role where customer_id = {} and region_id = {}".format(customer_id, region_id)) + + print "num records deleted: " + str(result.rowcount) + return result diff --git a/orm/services/customer_manager/cms_rest/etc/policy.json b/orm/services/customer_manager/cms_rest/etc/policy.json new file mode 100755 index 00000000..62e5b6e0 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/etc/policy.json @@ -0,0 +1,32 @@ +{ + "default": "!", + "admin": "role:admin", + "admin_support": "role:admin_support", + "admin_viewer": "role:admin_viewer", + + "admin_or_admin_support": "rule:admin or rule:admin_support", + "admin_or_admin_support_or_admin_viewer": "rule:admin or rule:admin_support or rule:admin_viewer", + + "customers:get_one": "rule:admin_or_admin_support_or_admin_viewer", + "customers:get_all": "rule:admin_or_admin_support_or_admin_viewer", + "customers:create": "rule:admin_or_admin_support", + "customers:update": "rule:admin", + "customers:delete": "rule:admin", + + "customers:add_region": "rule:admin_or_admin_support", + "customers:update_region": "rule:admin", + "customers:delete_region": "rule:admin", + + "customers:add_region_user": "rule:admin", + "customers:update_region_user": "rule:admin", + "customers:delete_region_user": "rule:admin", + + "customers:add_default_user": "rule:admin", + "customers:update_default_user": "rule:admin", + "customers:delete_default_user": "rule:admin", + + "customers:add_metadata": "rule:admin", + "customers:update_metadata": "rule:admin", + + "customers:enable": "rule:admin" +} \ No newline at end of file diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/audit_client/__init__.py b/orm/services/customer_manager/cms_rest/extenal_mock/audit_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/audit_client/api/__init__.py b/orm/services/customer_manager/cms_rest/extenal_mock/audit_client/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/audit_client/api/audit.py b/orm/services/customer_manager/cms_rest/extenal_mock/audit_client/api/audit.py new file mode 100644 index 00000000..ec483bdd --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/audit_client/api/audit.py @@ -0,0 +1,6 @@ +def audit(*args, **kwargs): + pass + + +def init(*args, **kwargs): + pass diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/keystone_utils/__init__.py b/orm/services/customer_manager/cms_rest/extenal_mock/keystone_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/keystone_utils/tokens.py b/orm/services/customer_manager/cms_rest/extenal_mock/keystone_utils/tokens.py new file mode 100755 index 00000000..99708117 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/keystone_utils/tokens.py @@ -0,0 +1,6 @@ +def is_token_valid(token_to_validate, lcp_id, conf, token_role): + pass + + +def TokenConf(mech_id, mech_password, rms_url, tenant_name, keystone_version): + pass diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/__init__.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/logger.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/logger.py new file mode 100644 index 00000000..77f80741 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/logger.py @@ -0,0 +1,2 @@ +def get_logger(*a, **k): + pass diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/policy/__init__.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/policy/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/policy/policy.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/policy/policy.py new file mode 100755 index 00000000..9d652b2b --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/policy/policy.py @@ -0,0 +1,6 @@ +def init(*a, **kw): + pass + + +def enforce(*a, **kw): + pass diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils.py new file mode 100755 index 00000000..c3591af4 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils.py @@ -0,0 +1,23 @@ +class utils: + @staticmethod + def set_utils_conf(conf): + pass + + @staticmethod + def report_config(conf, dump_to_log): + pass + + @staticmethod + def create_existing_uuid(uuid): + pass + + +class api_error_utils: + + @staticmethod + def get_error(transaction_id, + error_details="", + message=None, + status_code=400): + + pass diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/__init__.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/api_error_utils.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/api_error_utils.py new file mode 100755 index 00000000..e2b3efee --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/api_error_utils.py @@ -0,0 +1,8 @@ + + +def get_error(transaction_id, + error_details="", + message=None, + status_code=400): + + pass diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/cross_api_utils.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/cross_api_utils.py new file mode 100755 index 00000000..d27dd1c9 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/cross_api_utils.py @@ -0,0 +1,6 @@ +def get_regions_of_group(*a, **k): + pass + + +def set_utils_conf(*a, **k): + pass diff --git a/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/utils.py b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/utils.py new file mode 100755 index 00000000..8cd48467 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/extenal_mock/orm_common/utils/utils.py @@ -0,0 +1,10 @@ +def set_utils_conf(conf): + pass + + +def report_config(conf, dump_to_log): + pass + + +def create_existing_uuid(uuid): + pass diff --git a/orm/services/customer_manager/cms_rest/logger/__init__.py b/orm/services/customer_manager/cms_rest/logger/__init__.py new file mode 100644 index 00000000..1894cbbe --- /dev/null +++ b/orm/services/customer_manager/cms_rest/logger/__init__.py @@ -0,0 +1,10 @@ +import logging + + +def get_logger(name): + logger = logging.getLogger(name) + logger.log_exception = lambda msg, exception: logger.exception(msg + " Exception: " + str(exception)) + + return logger + +__all__ = ['get_logger'] diff --git a/orm/services/customer_manager/cms_rest/logic/__init__.py b/orm/services/customer_manager/cms_rest/logic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/logic/customer_logic.py b/orm/services/customer_manager/cms_rest/logic/customer_logic.py new file mode 100755 index 00000000..df2fc94a --- /dev/null +++ b/orm/services/customer_manager/cms_rest/logic/customer_logic.py @@ -0,0 +1,721 @@ +from cms_rest.model.Models import CustomerResultWrapper +from cms_rest.model.Models import RegionResultWrapper +from cms_rest.model.Models import UserResultWrapper +from cms_rest.model.Models import CustomerSummaryResponse, CustomerSummary +from cms_rest.rds_proxy import RdsProxy +from cms_rest.data.data_manager import DataManager +from cms_rest.data.sql_alchemy.models import UserRole +from cms_rest.logic.error_base import ErrorStatus, NotFound, DuplicateEntryError +from cms_rest.data.sql_alchemy.models import CustomerMetadata +from orm_common.utils.cross_api_utils import get_regions_of_group, set_utils_conf +from orm_common.utils import utils + +from pecan import conf, request + +import pecan +import requests + +from cms_rest.logger import get_logger + +LOG = get_logger(__name__) + + +class CustomerLogic(object): + def build_full_customer(self, customer, uuid, datamanager): + sql_customer = datamanager.add_customer(customer, uuid) + + for key, value in customer.metadata.iteritems(): + metadata = CustomerMetadata(field_key=key, field_value=value) + sql_customer.customer_metadata.append(metadata) + + datamanager.add_customer_region(sql_customer.id, -1) + + default_region_users = [] + for user in customer.users: + sql_user = datamanager.add_user(user) + default_region_users.append(sql_user) + sql_user.sql_roles = [] + for role in user.role: + sql_role = datamanager.add_role(role) + sql_user.sql_roles.append(sql_role) + + default_quotas = [] + for quota in customer.defaultQuotas: + sql_quota = datamanager.add_quota(sql_customer.id, -1, quota) + default_quotas.append(sql_quota) + + for sql_user in default_region_users: + for sql_role in sql_user.sql_roles: + datamanager.add_user_role(sql_user.id, sql_role.id, + sql_customer.id, -1) + + self.add_regions_to_db(customer.regions, sql_customer.id, datamanager, customer.users) + return sql_customer + + def add_regions_to_db(self, regions, sql_customer_id, datamanager, default_users=[]): + for region in regions: + users_roles = self.add_user_and_roles_to_db(region.users, default_users, + datamanager) + + # NOTE: if region has no users there is no need to update the + # default users in that region + # if len(region.users) == 0: + # users_roles.extend(self.add_user_and_roles_to_db( + # customer.users, datamanager)) + # else: + # users_roles.extend(self.add_user_and_roles_to_db( + # region.users, datamanager)) + + sql_region = datamanager.add_region(region) + try: + datamanager.add_customer_region(sql_customer_id, sql_region.id) + except Exception as ex: + if hasattr(ex, 'orig') and ex.orig[0] == 1062: + raise DuplicateEntryError( + 'Error, duplicate entry, region ' + region.name + ' already associated with customer') + raise ex + + for user_role in users_roles: + datamanager.add_user_role(user_role[0].id, user_role[1].id, + sql_customer_id, sql_region.id) + + for quota in region.quotas: + datamanager.add_quota(sql_customer_id, sql_region.id, quota) + + # NOTE: if region has no quotas there is no need to update + # the default quotas in that region + # if len(region.quotas) == 0: + # for quota in customer.defaultQuotas: + # datamanager.add_quota(sql_customer_id, + # sql_region.id, quota) + # else: + # for quota in region.quotas: + # datamanager.add_quota(sql_customer_id, + # sql_region.id, quota) + + def add_user_and_roles_to_db(self, users, default_users, datamanager): + users_roles = [] + for user in users: + sql_user = datamanager.add_user(user) + for role in user.role: + sql_role = datamanager.add_role(role) + users_roles.append((sql_user, sql_role)) + for default_user in default_users: + sql_user = datamanager.add_user(default_user) + for role in default_user.role: + sql_role = datamanager.add_role(role) + users_roles.append((sql_user, sql_role)) + + return users_roles + + def create_customer(self, customer, uuid, transaction_id): + datamanager = DataManager() + try: + customer.handle_region_group() + sql_customer = self.build_full_customer(customer, uuid, datamanager) + customer_result_wrapper = build_response(uuid, transaction_id, 'create') + + sql_customer = self.add_default_users_to_empty_regions(sql_customer) + if sql_customer.customer_customer_regions and len(sql_customer.customer_customer_regions) > 1: + customer_dict = sql_customer.get_proxy_dict() + for region in customer_dict["regions"]: + region["action"] = "create" + + datamanager.flush() # i want to get any exception created by this insert + RdsProxy.send_customer_dict(customer_dict, transaction_id, "POST") + else: + LOG.debug("Customer with no regions - wasn't send to RDS Proxy " + str(customer)) + + datamanager.commit() + + except Exception as exp: + LOG.log_exception("CustomerLogic - Failed to CreateCustomer", exp) + datamanager.rollback() + raise + + return customer_result_wrapper + + def update_customer(self, customer, customer_uuid, transaction_id): + datamanager = DataManager() + try: + customer.validate_model('update') + customer_record = datamanager.get_record('customer') + cutomer_id = customer_record.get_customer_id_from_uuid( + customer_uuid) + + sql_customer = customer_record.read_customer_by_uuid(customer_uuid) + if not sql_customer: + raise ErrorStatus(404, 'customer {0} was not found'.format(customer_uuid)) + old_customer_dict = sql_customer.get_proxy_dict() + customer_record.delete_by_primary_key(cutomer_id) + datamanager.flush() + + sql_customer = self.build_full_customer(customer, customer_uuid, + datamanager) + sql_customer = self.add_default_users_to_empty_regions(sql_customer) + new_customer_dict = sql_customer.get_proxy_dict() + new_customer_dict["regions"] = self.resolve_regions_actions(old_customer_dict["regions"], + new_customer_dict["regions"]) + + customer_result_wrapper = build_response(customer_uuid, transaction_id, 'update') + datamanager.flush() # i want to get any exception created by this insert + if not len(new_customer_dict['regions']) == 0: + RdsProxy.send_customer_dict(new_customer_dict, transaction_id, "PUT") + datamanager.commit() + + return customer_result_wrapper + + except Exception as exp: + LOG.log_exception("CustomerLogic - Failed to CreateCustomer", exp) + datamanager.rollback() + raise + + def resolve_regions_actions(self, old_regions_dict, new_regions_dict): + for region in new_regions_dict: + old_region = next((r for r in old_regions_dict if r["name"] == region["name"]), None) + if old_region: + region["action"] = "modify" + else: + region["action"] = "create" + + for region in old_regions_dict: + new_region = next((r for r in new_regions_dict if r["name"] == region["name"]), None) + if not new_region: + region["action"] = "delete" + new_regions_dict.append(region) + + return new_regions_dict + + def add_users(self, customer_uuid, region_name, users, transaction_id, p_datamanager=None): + datamanager = None + try: + if p_datamanager is None: + datamanager = DataManager() + datamanager.begin_transaction() + else: + datamanager = p_datamanager + + region_id = datamanager.get_region_id_by_name(region_name) + customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) + + if customer_id is None: + raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) + + if region_id is None: + raise ErrorStatus(404, "region {} not found".format(region_name)) + + self.add_users_to_db(datamanager, customer_id, region_id, users, adding=True) + + customer_record = datamanager.get_record('customer') + customer = customer_record.read_customer(customer_id) + + timestamp = utils.get_time_human() + datamanager.flush() # i want to get any exception created by this insert + RdsProxy.send_customer(customer, transaction_id, "PUT") + if p_datamanager is None: + datamanager.commit() + + base_link = '{0}{1}/'.format(conf.server.host_ip, + pecan.request.path) + + result_users = [{'id': user.id, 'added': timestamp, + 'links': {'self': base_link + user.id}} for user in + users] + user_result_wrapper = UserResultWrapper( + transaction_id=transaction_id, users=result_users) + + return user_result_wrapper + except Exception as exception: + if 'Duplicate' in exception.message: + raise ErrorStatus(409, exception.message) + datamanager.rollback() + LOG.log_exception("Failed to add_users", exception) + raise exception + + def replace_users(self, customer_uuid, region_name, users, transaction_id): + datamanager = None + try: + datamanager = DataManager() + datamanager.begin_transaction() + + customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) + if customer_id is None: + raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) + + region_id = datamanager.get_region_id_by_name(region_name) + if region_id is None: + raise ErrorStatus(404, "region {} not found".format(region_name)) + + # delete older default user + user_role_record = datamanager.get_record('user_role') + user_role_record.delete_all_users_from_region(customer_uuid, region_name) # -1 is default region + result = self.add_users(customer_uuid, region_name, users, transaction_id, datamanager) + datamanager.commit() + return result + + except Exception as exception: + datamanager.rollback() + LOG.log_exception("Failed to replace_default_users", exception) + raise + + def add_users_to_db(self, datamanager, customer_id, region_id, users, adding=False): + try: + users_roles = [] + for user in users: + sql_user = datamanager.add_user(user) + for role in user.role: + sql_role = datamanager.add_role(role) + users_roles.append((sql_user, sql_role)) + for user_role in users_roles: + # TODO: change add_use_role to receive sqlalchemy model (UserRole) + datamanager.add_user_role(user_role[0].id, user_role[1].id, + customer_id, region_id, adding) + datamanager.flush() + except Exception as exception: + LOG.log_exception("Failed to add users", exception) + raise + + def delete_users(self, customer_uuid, region_id, user_id, transaction_id): + datamanager = DataManager() + try: + user_role_record = datamanager.get_record('user_role') + + customer = datamanager.get_cusomer_by_uuid(customer_uuid) + if customer is None: + raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) + + result = user_role_record.delete_user_from_region(customer_uuid, + region_id, + user_id) + if result.rowcount == 0: + raise NotFound("user {} is not found".format(user_id)) + + RdsProxy.send_customer(customer, transaction_id, "PUT") + datamanager.commit() + + print "User {0} from region {1} in customer {2} deleted".format( + user_id, region_id, customer_uuid) + except NotFound as e: + datamanager.rollback() + LOG.log_exception("Failed to delete_users, user not found", + e.message) + raise NotFound("Failed to delete users, %s not found" % + e.message) + except Exception as exception: + datamanager.rollback() + LOG.log_exception("Failed to delete_users", exception) + raise exception + + def add_default_users(self, customer_uuid, users, transaction_id, p_datamanager=None): + datamanager = None + try: + if p_datamanager is None: + datamanager = DataManager() + datamanager.begin_transaction() + else: + datamanager = p_datamanager + + customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) + + if customer_id is None: + raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) + + self.add_users_to_db(datamanager, customer_id, -1, users, adding=True) + + customer_record = datamanager.get_record('customer') + customer = customer_record.read_customer(customer_id) + + timestamp = utils.get_time_human() + datamanager.flush() # i want to get any exception created by this insert + if len(customer.customer_customer_regions) > 1: + RdsProxy.send_customer(customer, transaction_id, "PUT") + + if p_datamanager is None: + datamanager.commit() + + base_link = '{0}{1}/'.format(conf.server.host_ip, + pecan.request.path) + + result_users = [{'id': user.id, 'added': timestamp, + 'links': {'self': base_link + user.id}} for user in + users] + user_result_wrapper = UserResultWrapper( + transaction_id=transaction_id, users=result_users) + + return user_result_wrapper + + except Exception as exception: + datamanager.rollback() + if 'Duplicate' in exception.message: + raise ErrorStatus(409, exception.message) + LOG.log_exception("Failed to add_default_users", exception) + raise + + def replace_default_users(self, customer_uuid, users, transaction_id): + datamanager = None + try: + datamanager = DataManager() + datamanager.begin_transaction() + + customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) + if customer_id is None: + raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) + + # delete older default user + user_role_record = datamanager.get_record('user_role') + user_role_record.delete_all_users_from_region(customer_uuid, -1) # -1 is default region + result = self.add_default_users(customer_uuid, users, transaction_id, datamanager) + datamanager.commit() + return result + + except Exception as exception: + datamanager.rollback() + LOG.log_exception("Failed to replace_default_users", exception) + raise + + def delete_default_users(self, customer_uuid, user_id, transaction_id): + datamanager = DataManager() + try: + customer = datamanager.get_cusomer_by_uuid(customer_uuid) + if customer is None: + raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) + + user_role_record = datamanager.get_record('user_role') + result = user_role_record.delete_user_from_region(customer_uuid, + 'DEFAULT', + user_id) + + if result.rowcount == 0: + raise NotFound("user {} is not found".format(user_id)) + + datamanager.commit() + + print "User {0} from region {1} in customer {2} deleted".format( + user_id, 'DEFAULT', customer_uuid) + + except NotFound as e: + datamanager.rollback() + LOG.log_exception("Failed to delete_users, user not found", + e.message) + raise NotFound("Failed to delete users, %s not found" % + e.message) + + except Exception as exp: + datamanager.rollback() + raise exp + + def add_regions(self, customer_uuid, regions, transaction_id): + datamanager = DataManager() + customer_record = datamanager.get_record('customer') + try: + # TODO DataBase action + customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) + if customer_id is None: + raise ErrorStatus(404, + "customer with id {} does not exist".format( + customer_uuid)) + self.add_regions_to_db(regions, customer_id, datamanager) + + sql_customer = customer_record.read_customer_by_uuid(customer_uuid) + + sql_customer = self.add_default_users_to_empty_regions(sql_customer) + new_customer_dict = sql_customer.get_proxy_dict() + + for region in new_customer_dict["regions"]: + new_region = next((r for r in regions if r.name == region["name"]), None) + if new_region: + region["action"] = "create" + else: + region["action"] = "modify" + + timestamp = utils.get_time_human() + datamanager.flush() # i want to get any exception created by this insert + RdsProxy.send_customer_dict(new_customer_dict, transaction_id, "POST") + datamanager.commit() + + base_link = '{0}{1}/'.format(conf.server.host_ip, + pecan.request.path) + + result_regions = [{'id': region.name, 'added': timestamp, + 'links': {'self': base_link + region.name}} for + region in regions] + region_result_wrapper = RegionResultWrapper( + transaction_id=transaction_id, regions=result_regions) + + return region_result_wrapper + except Exception as exp: + datamanager.rollback() + raise + + def replace_regions(self, customer_uuid, regions, transaction_id): + datamanager = DataManager() + customer_record = datamanager.get_record('customer') + customer_region = datamanager.get_record('customer_region') + try: + customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) + if customer_id is None: + raise ErrorStatus(404, + "customer with id {} does not exist".format( + customer_uuid)) + + old_sql_customer = customer_record.read_customer_by_uuid(customer_uuid) + if old_sql_customer is None: + raise ErrorStatus(404, + "customer with id {} does not exist".format( + customer_id)) + old_customer_dict = old_sql_customer.get_proxy_dict() + datamanager.session.expire(old_sql_customer) + + customer_region.delete_all_regions_for_customer(customer_id) + + self.add_regions_to_db(regions, customer_id, datamanager) + timestamp = utils.get_time_human() + + new_sql_customer = datamanager.get_cusomer_by_id(customer_id) + + new_sql_customer = self.add_default_users_to_empty_regions(new_sql_customer) + new_customer_dict = new_sql_customer.get_proxy_dict() + + datamanager.flush() # i want to get any exception created by this insert + + new_customer_dict["regions"] = self.resolve_regions_actions(old_customer_dict["regions"], + new_customer_dict["regions"]) + + RdsProxy.send_customer_dict(new_customer_dict, transaction_id, "PUT") + datamanager.commit() + + base_link = '{0}{1}/'.format(conf.server.host_ip, + pecan.request.path) + + result_regions = [{'id': region.name, 'added': timestamp, + 'links': {'self': base_link + region.name}} for + region in regions] + region_result_wrapper = RegionResultWrapper( + transaction_id=transaction_id, regions=result_regions) + + return region_result_wrapper + except Exception as exp: + datamanager.rollback() + raise exp + + def delete_region(self, customer_id, region_id, transaction_id): + datamanager = DataManager() + try: + customer_region = datamanager.get_record('customer_region') + + sql_customer = datamanager.get_cusomer_by_uuid(customer_id) + if sql_customer is None: + raise ErrorStatus(404, + "customer with id {} does not exist".format( + customer_id)) + customer_dict = sql_customer.get_proxy_dict() + + customer_region.delete_region_for_customer(customer_id, region_id) + datamanager.flush() # i want to get any exception created by this insert + + # i want to get any exception created by this insert + datamanager.flush() + + region = next((r.region for r in sql_customer.customer_customer_regions if r.region.name == region_id), None) + if region: + if region.type == 'group': + set_utils_conf(conf) + regions = get_regions_of_group(region.name) + else: + regions = [region_id] + for region in customer_dict['regions']: + if region['name'] in regions: + region['action'] = 'delete' + + RdsProxy.send_customer_dict(customer_dict, transaction_id, "PUT") + datamanager.commit() + + LOG.debug("Region {0} in customer {1} deleted".format(region_id, + customer_id)) + except Exception as exp: + datamanager.rollback() + raise + + def get_customer(self, customer): + + datamanager = DataManager() + + sql_customer = datamanager.get_cusomer_by_uuid_or_name(customer) + + if not sql_customer: + raise ErrorStatus(404, 'customer: {0} not found'.format(customer)) + + ret_customer = sql_customer.to_wsme() + if sql_customer.get_real_customer_regions(): + # if we have regions in sql_customer + + resp = requests.get(conf.api.rds_server.base + + conf.api.rds_server.status + + sql_customer.uuid, verify=conf.verify).json() + + for item in ret_customer.regions: + for status in resp['regions']: + if status['region'] == item.name: + item.status = status['status'] + if status['error_msg']: + item.error_message = status['error_msg'] + ret_customer.status = resp['status'] + else: + ret_customer.status = 'no regions' + + return ret_customer + + def get_customer_list_by_criteria(self, region, user, starts_with, contains, + metadata): + datamanager = DataManager() + customer_record = datamanager.get_record('customer') + sql_customers = customer_record.get_customers_by_criteria(region=region, + user=user, + starts_with=starts_with, + contains=contains, + metadata=metadata) + + response = CustomerSummaryResponse() + for sql_customer in sql_customers: + # get aggregate status for each customer + customer_status = RdsProxy.get_status(sql_customer.uuid) + customer = CustomerSummary.from_db_model(sql_customer) + if customer_status.status_code == 200: + customer.status = customer_status.json()['status'] + response.customers.append(customer) + + return response + + def enable(self, customer_uuid, enabled, transaction_id): + try: + datamanager = DataManager() + + customer_record = datamanager.get_record('customer') + sql_customer = customer_record.read_customer_by_uuid(customer_uuid) + + if not sql_customer: + raise ErrorStatus(404, 'customer: {0} not found'.format(customer_uuid)) + + sql_customer.enabled = 1 if enabled.enabled else 0 + + RdsProxy.send_customer(sql_customer, transaction_id, "PUT") + + datamanager.flush() # get any exception created by this action + datamanager.commit() + + except Exception as exp: + datamanager.rollback() + raise exp + + def add_default_users_to_empty_regions(self, sql_customer): + if len(sql_customer.customer_customer_regions) > 0: + for region in sql_customer.customer_customer_regions: + if region.region_id == -1: + users = region.customer_region_user_roles[:] + break + + for region in sql_customer.customer_customer_regions: + if region.region_id != -1: + for user in users: + u = UserRole() + u.customer_id = region.customer_id + u.region_id = region.region_id + u.user_id = user.user_id + u.role_id = user.role_id + region.customer_region_user_roles.append(u) + + return sql_customer + + def delete_customer_by_uuid(self, customer_id): + datamanager = DataManager() + + try: + datamanager.begin_transaction() + customer_record = datamanager.get_record('customer') + + sql_customer = customer_record.read_customer_by_uuid(customer_id) + if sql_customer is None: + # The customer does not exist, so the delete operation is + # considered successful + return + + real_regions = sql_customer.get_real_customer_regions() + if len(real_regions) > 0: + # Do not delete a customer that still has some regions + raise ErrorStatus(405, + "Cannot delete a customer that has regions. " + "Please delete the regions first and then " + "delete the customer.") + else: + expected_status = 'Success' + invalid_status = 'N/A' + # Get status from RDS + resp = RdsProxy.get_status(sql_customer.uuid) + if resp.status_code == 200: + status_resp = resp.json() + if 'status' in status_resp.keys(): + LOG.debug( + 'RDS returned status: {}'.format( + status_resp['status'])) + status = status_resp['status'] + else: + # Invalid response from RDS + LOG.error('Response from RDS did not contain status') + status = invalid_status + elif resp.status_code == 404: + # Customer not found in RDS, that means it never had any regions + # So it is OK to delete it + LOG.debug( + 'Resource not found in RDS, so it is OK to delete') + status = expected_status + else: + # Invalid status code from RDS + log_message = 'Invalid response code from RDS: {}'.format( + resp.status_code) + log_message = log_message.replace('\n', '_').replace('\r', + '_') + LOG.warning(log_message) + status = invalid_status + + if status == invalid_status: + raise ErrorStatus(500, "Could not get customer status") + elif status != expected_status: + raise ErrorStatus(409, + "The customer has not been deleted " + "successfully from all of its regions " + "(either the deletion failed on one of the " + "regions or it is still in progress)") + + # OK to delete + customer_record.delete_customer_by_uuid(customer_id) + + datamanager.flush() # i want to get any exception created by this delete + datamanager.commit() + except Exception as exp: + LOG.log_exception("CustomerLogic - Failed to delete customer", exp) + datamanager.rollback() + raise + + +def build_response(customer_uuid, transaction_id, context): + """ + this function generate th customer action response JSON + :param customer_uuid: + :param transaction_id: + :param context: create or update + :return: + """ + # The link should point to the customer itself (/v1/orm/customers/{id}) + link_elements = request.url.split('/') + base_link = '/'.join(link_elements) + if context == 'create': + base_link += customer_uuid + + timestamp = utils.get_time_human() + customer_result_wrapper = CustomerResultWrapper( + transaction_id=transaction_id, + id=customer_uuid, + updated=None, + created=timestamp, + links={'self': base_link}) + return customer_result_wrapper diff --git a/orm/services/customer_manager/cms_rest/logic/error_base.py b/orm/services/customer_manager/cms_rest/logic/error_base.py new file mode 100755 index 00000000..48b70590 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/logic/error_base.py @@ -0,0 +1,20 @@ +class Error(Exception): + pass + + +class ErrorStatus(Error): + def __init__(self, status_code, message=None): + self.status_code = status_code + self.message = message + + +class NotFound(Error): + def __init__(self, message=None, status_code=404): + self.status_code = status_code + self.message = message + + +class DuplicateEntryError(Error): + def __init__(self, message=None, status_code=409): + self.status_code = status_code + self.message = message diff --git a/orm/services/customer_manager/cms_rest/logic/metadata_logic.py b/orm/services/customer_manager/cms_rest/logic/metadata_logic.py new file mode 100755 index 00000000..1de4a6b6 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/logic/metadata_logic.py @@ -0,0 +1,109 @@ +from cms_rest.data.sql_alchemy.models import CustomerMetadata +from cms_rest.data.data_manager import DataManager +from cms_rest.rds_proxy import RdsProxy +from cms_rest.model.Models import CustomerResultWrapper +from orm_common.utils import utils +from pecan import request +from pecan import conf +import json +from cms_rest.logger import get_logger + +logger = get_logger(__name__) + + +def add_customer_metadata(customer_uuid, metadata_wrapper, transaction_id): + sql_metadata_collection = map_metadata(customer_uuid, metadata_wrapper) + + datamanager = DataManager() + + try: + customer_record = datamanager.get_record('customer') + sql_customer = customer_record.read_customer_by_uuid(customer_uuid) + if not sql_customer: + logger.error('customer not found, customer uuid: {0}'.format(customer_uuid)) + raise ValueError('customer not found, customer uuid: {0}'.format(customer_uuid)) + + for metadata in sql_metadata_collection: + metadata_match = [m for m in sql_customer.customer_metadata if m.field_key == metadata.field_key] + if len(metadata_match) > 0: + logger.error('Duplicate metadata key, key already exits: {0}'.format(metadata.field_key)) + raise AttributeError('Duplicate metadata key, key already exits: {0}'.format(metadata.field_key)) + + for metadata in sql_metadata_collection: + sql_customer.customer_metadata.append(metadata) + logger.debug('updating metadata {0}'.format(json.dumps(metadata.get_proxy_dict()))) + + logger.debug('finished appending metadata to customer') + if len(sql_customer.customer_customer_regions) > 1: + RdsProxy.send_customer(sql_customer, transaction_id, "PUT") + datamanager.commit() + + customer_result_wrapper = build_response(customer_uuid, transaction_id) + + return customer_result_wrapper + + except Exception as exp: + datamanager.rollback() + raise exp + + +def update_customer_metadata(customer_uuid, metadata_wrapper, transaction_id): + sql_metadata_collection = map_metadata(customer_uuid, metadata_wrapper) + + datamanager = DataManager() + + try: + customer_record = datamanager.get_record('customer') + sql_customer = customer_record.read_customer_by_uuid(customer_uuid) + + if not sql_customer: + logger.error('customer not found, customer uuid: {0}'.format(customer_uuid)) + raise ValueError('customer not found, customer uuid: {0}'.format(customer_uuid)) + + while len(sql_customer.customer_metadata) > 0: + sql_customer.customer_metadata.remove(sql_customer.customer_metadata[0]) + + for metadata in sql_metadata_collection: + sql_customer.customer_metadata.append(metadata) + logger.debug('updating metadata {0}'.format(json.dumps(metadata.get_proxy_dict()))) + + if len(sql_customer.customer_customer_regions) > 1: + RdsProxy.send_customer(sql_customer, transaction_id, "PUT") + datamanager.commit() + + customer_result_wrapper = build_response(customer_uuid, transaction_id) + + return customer_result_wrapper + + except Exception as exp: + datamanager.rollback() + raise exp + + +def map_metadata(customer_id, metadata_wrapper): + sql_metadata_collection = [] + for key, value in metadata_wrapper.metadata.iteritems(): + sql_metadata = CustomerMetadata() + sql_metadata.customer_id = customer_id + sql_metadata.field_key = key + sql_metadata.field_value = value + + sql_metadata_collection.append(sql_metadata) + return sql_metadata_collection + + +def build_response(customer_uuid, transaction_id): + # The link should point to the customer itself (/v1/orm/customers/{id}), + # so the 'metadata' element should be removed. + link_elements = request.url.split('/') + link_elements.remove('metadata') + base_link = '/'.join(link_elements) + + timestamp = utils.get_time_human() + customer_result_wrapper = CustomerResultWrapper( + transaction_id=transaction_id, + id=customer_uuid, + updated=None, + created=timestamp, + links={'self': base_link}) + return customer_result_wrapper diff --git a/orm/services/customer_manager/cms_rest/model/Model.py b/orm/services/customer_manager/cms_rest/model/Model.py new file mode 100644 index 00000000..f6a4fd67 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/model/Model.py @@ -0,0 +1,38 @@ +import inspect +from wsme import types as wtypes +from wsme.rest.json import tojson + + +class Model(wtypes.DynamicBase): + """Base class for CMS models. + """ + + def tojson(self): + return tojson(type(self), self) + + """ + def __init__(self, **kwds): + self.fields = list(kwds) + for k, v in kwds.iteritems(): + setattr(self, k, v) + + def as_dict(self): + d = {} + for f in self.fields: + v = getattr(self, f) + if isinstance(v, Model): + v = v.as_dict() + elif isinstance(v, list) and v and isinstance(v[0], Model): + v = [sub.as_dict() for sub in v] + d[f] = v + return d + + def __eq__(self, other): + return self.as_dict() == other.as_dict() + + @classmethod + def get_field_names(cls): + fields = inspect.getargspec(cls.__init__)[0] + return set(fields) - set(["self"]) + + """ diff --git a/orm/services/customer_manager/cms_rest/model/Models.py b/orm/services/customer_manager/cms_rest/model/Models.py new file mode 100755 index 00000000..8a9a2d45 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/model/Models.py @@ -0,0 +1,444 @@ +import wsme +from wsme import types as wtypes +from cms_rest.model.Model import Model +from cms_rest.logic.error_base import ErrorStatus +from pecan import conf +from orm_common.utils.cross_api_utils import set_utils_conf, get_regions_of_group + + +class Enabled(Model): + """enable model the customer + + """ + enabled = wsme.wsattr(bool, mandatory=True) + + def __init__(self, enabled=None): + """Create a new enables class. + + :param enabled: customer status + """ + self.enabled = enabled + + +class Compute(Model): + """compute model the customer + + """ + instances = wsme.wsattr(wsme.types.text, mandatory=True) + injected_files = wsme.wsattr(wsme.types.text, mandatory=True, name="injected-files") + key_pairs = wsme.wsattr(wsme.types.text, mandatory=True, name="key-pairs") + ram = wsme.wsattr(wsme.types.text, mandatory=True) + vcpus = wsme.wsattr(wsme.types.text, mandatory=False) + metadata_items = wsme.wsattr(wsme.types.text, mandatory=False, name="metadata-items") + injected_file_content_bytes = wsme.wsattr(wsme.types.text, mandatory=False, name="injected-file-content-bytes") + floating_ips = wsme.wsattr(wsme.types.text, mandatory=False, name="floating-ips") + fixed_ips = wsme.wsattr(wsme.types.text, mandatory=False, name="fixed-ips") + injected_file_path_bytes = wsme.wsattr(wsme.types.text, mandatory=False, name="injected-file-path-bytes") + server_groups = wsme.wsattr(wsme.types.text, mandatory=False, name="server-groups") + server_group_members = wsme.wsattr(wsme.types.text, mandatory=False, name="server-group-members") + + def __init__(self, instances='', injected_files='', key_pairs='', ram='', + vcpus=None, metadata_items=None, injected_file_content_bytes=None, + floating_ips='', fixed_ips='', injected_file_path_bytes='', + server_groups='', server_group_members=''): + """ + Create a new compute instance. + :param instances: + :param injected_files: + :param key_pairs: + :param ram: + :param vcpus: + :param metadata_items: + :param injected_file_content_bytes: + :param floating_ips: + :param fixed_ips: + :param injected_file_path_bytes: + :param server_groups: + :param server_group_members: + """ + self.instances = instances + self.injected_files = injected_files + self.key_pairs = key_pairs + self.ram = ram + if vcpus is None: + self.vcpus = conf.quotas_default_values.compute.vcpus + else: + self.vcpus = vcpus + if metadata_items is None: + self.metadata_items = \ + conf.quotas_default_values.compute.metadata_items + else: + self.metadata_items = metadata_items + if injected_file_content_bytes is None: + self.injected_file_content_bytes = \ + conf.quotas_default_values.compute.injected_file_content_bytes + else: + self.injected_file_content_bytes = injected_file_content_bytes + + self.floating_ips = floating_ips + self.fixed_ips = fixed_ips + self.injected_file_path_bytes = injected_file_path_bytes + self.server_groups = server_groups + self.server_group_members = server_group_members + + +class Storage(Model): + """storage info model for customer + + """ + gigabytes = wsme.wsattr(wsme.types.text, mandatory=True) + snapshots = wsme.wsattr(wsme.types.text, mandatory=True) + volumes = wsme.wsattr(wsme.types.text, mandatory=True) + + def __init__(self, gigabytes='', snapshots='', volumes=''): + """ + create a new Storage instance. + :param gigabytes: + :param snapshots: + :param volumes: + """ + self.gigabytes = gigabytes + self.snapshots = snapshots + self.volumes = volumes + + +class Network(Model): + """network model the customer + + """ + floating_ips = wsme.wsattr(wsme.types.text, mandatory=True, name="floating-ips") + networks = wsme.wsattr(wsme.types.text, mandatory=True) + ports = wsme.wsattr(wsme.types.text, mandatory=True) + routers = wsme.wsattr(wsme.types.text, mandatory=True) + subnets = wsme.wsattr(wsme.types.text, mandatory=True) + security_groups = wsme.wsattr(wsme.types.text, mandatory=False, name="security-groups") + security_group_rules = wsme.wsattr(wsme.types.text, mandatory=False, name="security-group-rules") + health_monitor = wsme.wsattr(wsme.types.text, mandatory=False, name="health-monitor") + member = wsme.wsattr(wsme.types.text, mandatory=False) + nat_instance = wsme.wsattr(wsme.types.text, mandatory=False, name="nat-instance") + pool = wsme.wsattr(wsme.types.text, mandatory=False) + route_table = wsme.wsattr(wsme.types.text, mandatory=False, name="route-table") + vip = wsme.wsattr(wsme.types.text, mandatory=False) + + def __init__(self, floating_ips='', networks='', ports='', routers='', + subnets='', security_groups=None, security_group_rules=None, + health_monitor='', member='', nat_instance='', + pool='', route_table='', vip=''): + + """ + Create a new Network instance. + :param floating_ips: num of floating_ips + :param networks: num of networks + :param ports: num of ports + :param routers: num of routers + :param subnets: num of subnets + :param security_groups: security groups + :param security_group_rules: security group rules + :param health_monitor: + :param member: + :param nat_instance: + :param pool: + :param route_table: + :param vip: + """ + self.floating_ips = floating_ips + self.networks = networks + self.ports = ports + self.routers = routers + self.subnets = subnets + if security_groups is None: + self.security_groups = conf.quotas_default_values.network.security_groups + else: + self.security_groups = security_groups + if security_group_rules is None: + self.security_group_rules = conf.quotas_default_values.network.security_group_rules + else: + self.security_group_rules = security_group_rules + + self.health_monitor = health_monitor + self.member = member + self.nat_instance = nat_instance + self.pool = pool + self.route_table = route_table + self.vip = vip + + +class Quota(Model): + """network model the customer + + """ + compute = wsme.wsattr([Compute], mandatory=False) + storage = wsme.wsattr([Storage], mandatory=False) + network = wsme.wsattr([Network], mandatory=False) + + def __init__(self, compute=None, storage=None, network=None): + """Create a new compute. + + :param compute: compute quota + :param storage: storage quota + :param network: network quota + """ + self.compute = compute + self.storage = storage + self.network = network + + +class User(Model): + """user model the customer + + """ + id = wsme.wsattr(wsme.types.text, mandatory=True) + role = wsme.wsattr([str]) + + def __init__(self, id="", role=[]): + """Create a new compute. + + :param id: user id + :param role: roles this use belong to + """ + self.id = id + self.role = role + + +class Region(Model): + """network model the customer + + """ + name = wsme.wsattr(wsme.types.text, mandatory=True) + type = wsme.wsattr(wsme.types.text, default="single", mandatory=False) + quotas = wsme.wsattr([Quota], mandatory=False) + users = wsme.wsattr([User], mandatory=False) + status = wsme.wsattr(wsme.types.text, mandatory=False) + error_message = wsme.wsattr(wsme.types.text, mandatory=False) + + def __init__(self, name="", type="single", quotas=[], users=[], status="", + error_message=""): + """Create a new compute. + + :param name: region name + :param type: region type + :param quotas: quotas ( array of Quota) + :param users: array of users of specific region + :param status: status of creation + :param error_message: error message if status is error + """ + + self.name = name + self.type = type + self.quotas = quotas + self.users = users + self.status = status + if error_message: + self.error_message = error_message + + +class Customer(Model): + """customer entity with all it's related data + + """ + description = wsme.wsattr(wsme.types.text, mandatory=True) + enabled = wsme.wsattr(bool, mandatory=True) + name = wsme.wsattr(wsme.types.text, mandatory=True) + metadata = wsme.wsattr(wsme.types.DictType(str, str), mandatory=False) + regions = wsme.wsattr([Region], mandatory=False) + users = wsme.wsattr([User], mandatory=True) + defaultQuotas = wsme.wsattr([Quota], mandatory=True) + status = wsme.wsattr(wsme.types.text, mandatory=False) + custId = wsme.wsattr(wsme.types.text, mandatory=False) + uuid = wsme.wsattr(wsme.types.text, mandatory=False) + + def __init__(self, description="", enabled=False, name="", metadata={}, regions=[], users=[], + defaultQuotas=[], status="", custId="", uuid=None): + """Create a new Customer. + + :param description: Server name + :param enabled: I don't know + :param status: status of creation + """ + self.description = description + self.enabled = enabled + self.name = name + self.metadata = metadata + self.regions = regions + self.users = users + self.defaultQuotas = defaultQuotas + self.status = status + self.custId = custId + if uuid is not None: + self.uuid = uuid + + def validate_model(self, context=None): + """ + this function check if the customer model meet the demands + :param context: i.e. 'create 'update' + :return: none + """ + if context == "update": + for region in self.regions: + if region.type == "group": + raise ErrorStatus(400, "region type is invalid for update, \'group\' can be only in create") + + def handle_region_group(self): + regions_to_add = [] + for region in self.regions[:]: # get copy of it to be able to delete from the origin + if region.type == "group": + group_regions = self.get_regions_for_group(region.name) + if not group_regions: + raise ErrorStatus(404, 'Group {} Not found'.format(region.name)) + for group_region in group_regions: + regions_to_add.append(Region(name=group_region, + type='single', + quotas=region.quotas, + users=region.users)) + self.regions.remove(region) + + self.regions.extend(set(regions_to_add)) # remove duplicates if exist + + def get_regions_for_group(self, group_name): + set_utils_conf(conf) + regions = get_regions_of_group(group_name) + return regions + + +""" Customer Result Handler """ + + +class CustomerResult(Model): + id = wsme.wsattr(wsme.types.text, mandatory=True) + updated = wsme.wsattr(wsme.types.text, mandatory=False) + created = wsme.wsattr(wsme.types.text, mandatory=False) + links = wsme.wsattr({str: str}, mandatory=True) + + def __init__(self, id, links={}, updated=None, created=None): + self.id = id + if updated: + self.updated = updated + elif created: + self.created = created + self.links = links + + +class CustomerResultWrapper(Model): + transaction_id = wsme.wsattr(wsme.types.text, mandatory=True) + customer = wsme.wsattr(CustomerResult, mandatory=True) + + def __init__(self, transaction_id, id, links, updated, created): + customer_result = CustomerResult(id, links, updated, created) + self.transaction_id = transaction_id + self.customer = customer_result + + +""" ****************************************************************** """ + +""" User Result Handler """ + + +class UserResult(Model): + id = wsme.wsattr(wsme.types.text, mandatory=True) + added = wsme.wsattr(wsme.types.text, mandatory=False) + links = wsme.wsattr({str: str}, mandatory=True) + + def __init__(self, id=None, added=None, links={}): + self.id = id + self.added = added + self.links = links + + +class UserResultWrapper(Model): + transaction_id = wsme.wsattr(wsme.types.text, mandatory=True) + users = wsme.wsattr([UserResult], mandatory=True) + + def __init__(self, transaction_id, users): + users_result = [UserResult(user['id'], user['added'], user['links']) for user in users] + + self.transaction_id = transaction_id + self.users = users_result + + +class MetadataWrapper(Model): + metadata = wsme.wsattr(wsme.types.DictType(str, str), mandatory=True) + + def __init__(self, metadata={}): + self.metadata = metadata + + +""" ****************************************************************** """ + +""" Region Result Handler """ + + +class RegionResult(Model): + id = wsme.wsattr(wsme.types.text, mandatory=True) + added = wsme.wsattr(wsme.types.text, mandatory=False) + links = wsme.wsattr({str: str}, mandatory=True) + + def __init__(self, id, added=None, links={}): + self.id = id + self.added = added + self.links = links + + +class RegionResultWrapper(Model): + transaction_id = wsme.wsattr(wsme.types.text, mandatory=True) + regions = wsme.wsattr([RegionResult], mandatory=True) + + def __init__(self, transaction_id, regions): + regions_result = [RegionResult(region['id'], region['added'], region['links']) for region in regions] + + self.transaction_id = transaction_id + self.regions = regions_result + + +""" ****************************************************************** """ + +""" CustomerSummary is a DataObject and contains all the fields defined in CustomerSummary structure. """ + + +class CustomerSummary(Model): + name = wsme.wsattr(wsme.types.text) + id = wsme.wsattr(wsme.types.text) + description = wsme.wsattr(wsme.types.text) + enabled = wsme.wsattr(bool, mandatory=True) + num_regions = wsme.wsattr(int, mandatory=True) + status = wsme.wsattr(wtypes.text, mandatory=True) + regions = wsme.wsattr([str], mandatory=True) + + def __init__(self, name='', id='', description='', + enabled=True, status="", regions=[], num_regions=0): + Model.__init__(self) + + self.name = name + self.id = id + self.description = description + self.enabled = enabled + self.num_regions = num_regions + self.status = status + self.regions = regions + + @staticmethod + def from_db_model(sql_customer): + regions = [region.region.name for region in + sql_customer.customer_customer_regions if + region.region_id != -1] + # default region is -1 , check if -1 in customer list if yes it will return (true, flase) equal to (0, 1) + num_regions = len(sql_customer.customer_customer_regions) - (-1 in [region.region_id for region in sql_customer.customer_customer_regions]) + customer = CustomerSummary() + customer.id = sql_customer.uuid + customer.name = sql_customer.name + customer.description = sql_customer.description + customer.enabled = bool(sql_customer.enabled) + customer.num_regions = num_regions + customer.regions = regions + + return customer + + +class CustomerSummaryResponse(Model): + customers = wsme.wsattr([CustomerSummary], mandatory=True) + + def __init__(self): + Model.__init__(self) + self.customers = [] + + +""" ****************************************************************** """ diff --git a/orm/services/customer_manager/cms_rest/model/__init__.py b/orm/services/customer_manager/cms_rest/model/__init__.py new file mode 100644 index 00000000..d983f7bc --- /dev/null +++ b/orm/services/customer_manager/cms_rest/model/__init__.py @@ -0,0 +1,15 @@ +from pecan import conf # noqa + + +def init_model(): + """ + This is a stub method which is called at application startup time. + + If you need to bind to a parsed database configuration, set up tables or + ORM classes, or perform any database initialization, this is the + recommended place to do it. + + For more information working with databases, and some common recipes, + see http://pecan.readthedocs.org/en/latest/databases.html + """ + pass diff --git a/orm/services/customer_manager/cms_rest/rds_proxy.py b/orm/services/customer_manager/cms_rest/rds_proxy.py new file mode 100755 index 00000000..e205abb9 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/rds_proxy.py @@ -0,0 +1,106 @@ +import pprint +import requests +import json +from pecan import conf +from pecan import request +from cms_rest.logic.error_base import ErrorStatus +from cms_rest.logger import get_logger + +LOG = get_logger(__name__) +headers = {'content-type': 'application/json'} + + +class RdsProxy(object): + + @staticmethod + def get_status(resource_id): + try: + LOG.debug( + "Sending to RDS Server to get status: " + conf.api.rds_server.base + conf.api.rds_server.status + resource_id) + resp = requests.get( + conf.api.rds_server.base + conf.api.rds_server.status + resource_id, + verify=conf.verify) + LOG.debug( + "Sending to RDS Server to get status: " + conf.api.rds_server.base + conf.api.rds_server.status + resource_id) + pp = pprint.PrettyPrinter(width=30) + pretty_text = pp.pformat(resp.json()) + LOG.debug("Response from RDS Server:\n" + pretty_text) + return resp + except Exception as exp: + LOG.log_exception( + "CustomerLogic - Failed to Get status for customer : " + resource_id, + exp) + raise + + @staticmethod + def send_customer(customer, transaction_id, method): # method is "POST" or "PUT" + return RdsProxy.send_customer_dict(customer.get_proxy_dict(), transaction_id, method) + + @staticmethod + def send_customer_dict(customer_dict, transaction_id, method): # method is "POST" or "PUT" + data = { + "service_template": + { + "resource": { + "resource_type": "customer" + }, + "model": str(json.dumps(customer_dict)), + "tracking": { + "external_id": "", + "tracking_id": transaction_id + } + } + } + + data_to_display = { + "service_template": + { + "resource": { + "resource_type": "customer" + }, + "model": customer_dict, + "tracking": { + "external_id": "", + "tracking_id": transaction_id + } + } + } + + pp = pprint.PrettyPrinter(width=30) + pretty_text = pp.pformat(data_to_display) + wrapper_json = json.dumps(data) + + headers['X-RANGER-Client'] = request.headers[ + 'X-RANGER-Client'] if 'X-RANGER-Client' in request.headers else \ + 'NA' + headers['X-RANGER-Requester'] = request.headers[ + 'X-RANGER-Requester'] if 'X-RANGER-Requester' in request.headers else \ + '' + + LOG.debug("Wrapper JSON before sending action: {0} to Rds Proxy\n{1}".format(method, pretty_text)) + LOG.info("Sending to RDS Server: " + conf.api.rds_server.base + conf.api.rds_server.resources) + + wrapper_json = json.dumps(data) + + if method == "POST": + resp = requests.post(conf.api.rds_server.base + conf.api.rds_server.resources, + data=wrapper_json, + headers=headers, + verify=conf.verify) + else: + resp = requests.put(conf.api.rds_server.base + conf.api.rds_server.resources, + data=wrapper_json, + headers=headers, + verify=conf.verify) + if resp.content: + LOG.debug("Response Content from rds server: {0}".format(resp.content)) + + content = resp.content + if resp.content: + content = resp.json() + + if resp.content and 200 <= resp.status_code < 300: + content = resp.json() + return content + + raise ErrorStatus(resp.status_code, content) diff --git a/orm/services/customer_manager/cms_rest/templates/error.html b/orm/services/customer_manager/cms_rest/templates/error.html new file mode 100644 index 00000000..f2d97961 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/templates/error.html @@ -0,0 +1,12 @@ +<%inherit file="layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Server Error ${status} + + +## now define the body of the template +
+

Server Error ${status}

+
+

${message}

diff --git a/orm/services/customer_manager/cms_rest/templates/index.html b/orm/services/customer_manager/cms_rest/templates/index.html new file mode 100644 index 00000000..f17c3862 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/templates/index.html @@ -0,0 +1,34 @@ +<%inherit file="layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Welcome to Pecan! + + +## now define the body of the template +
+

+
+ +
+ +

This is a sample Pecan project.

+ +

+ Instructions for getting started can be found online at pecanpy.org +

+ +

+ ...or you can search the documentation here: +

+ +
+
+ + +
+ Enter search terms or a module, class or function name. +
+ +
diff --git a/orm/services/customer_manager/cms_rest/templates/layout.html b/orm/services/customer_manager/cms_rest/templates/layout.html new file mode 100644 index 00000000..40908591 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/templates/layout.html @@ -0,0 +1,22 @@ + + + ${self.title()} + ${self.style()} + ${self.javascript()} + + + ${self.body()} + + + +<%def name="title()"> + Default Title + + +<%def name="style()"> + + + +<%def name="javascript()"> + + diff --git a/orm/services/customer_manager/cms_rest/tests/__init__.py b/orm/services/customer_manager/cms_rest/tests/__init__.py new file mode 100644 index 00000000..78ea5274 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/__init__.py @@ -0,0 +1,22 @@ +import os +from unittest import TestCase +from pecan import set_config +from pecan.testing import load_test_app + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """ + Used for functional tests where you need to test your + literal application and its integration with the framework. + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/orm/services/customer_manager/cms_rest/tests/config.py b/orm/services/customer_manager/cms_rest/tests/config.py new file mode 100755 index 00000000..5d1cd11f --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/config.py @@ -0,0 +1,130 @@ +import os +from cms_rest.tests.simple_hook_mock import SimpleHookMock + +global SimpleHookMock + +# Server Specific Configurations +server = { + 'port': '7080', + 'host': '0.0.0.0', + 'name': 'cms', + 'host_ip': '0.0.0.0' +} + +# Pecan Application Configurations +app = { + 'root': 'cms_rest.controllers.root.RootController', + 'modules': ['cms_rest'], + 'static_root': '%(confdir)s/../../public', + 'template_path': '%(confdir)s/../templates', + 'debug': True, + 'errors': { + '404': '/error/404', + '__force_dict__': True + }, + 'hooks': lambda: [SimpleHookMock()] +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'cms_rest': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +quotas_default_values = { + 'compute': { + 'vcpus': '20', + 'metadata_items': '128', + 'injected_file_content_bytes': '10240' + }, + 'network': { + 'security_groups': '10', + 'security_group_rules': '20' + } +} + +api_options = { + 'mock': { + 'uuid_server': { + 'base': 'http://127.0.0.1:3000/uuid/', + 'uuids': 'v1/uuids' + }, + 'rds_server': { + 'base': 'http://127.0.0.1:3000/rds/', + 'resources': 'v1/rds/resources', + 'status': 'v1/rds/status/resource/' + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } + }, + 'dev': { + 'uuid_server': { + 'base': 'http://127.0.0.1:8090/', + 'uuids': 'v1/uuids' + }, + 'rds_server': { + 'base': 'http://172.20.90.219:8777/', + 'resources': 'v1/rds/resources', + 'status': 'v1/rds/status/resource/' + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } + + } +} + +cms_mode = None +if not ('CMS_ENV' in os.environ) or not (os.environ['CMS_ENV'] in api_options): + print('!!! NO ENVIRONMENT VARIABLE CMS_ENV SPECIFIED OR NO ENV VARIABLE ' + 'WITH THIS NAME AVAILABLE, RUNNING WITH DEFAULT ') + cms_mode = 'dev' +else: + cms_mode = os.environ['CMS_ENV'] + print('Environment variable found, running under <{0}> environment'.format(cms_mode)) + +api = api_options[cms_mode] + +database = { + 'connection_string': 'mysql://root:root@localhost:3306/orm_cms_db' +} + +verify = False + +authentication = { + "enabled": False, + "mech_id": "admin", + "mech_pass": "stack", + "rms_url": "http://127.0.0.1:8080", + "tenant_name": "admin", + "token_role": "admin", + "role_location": {"tenant": "admin"}, + "keystone_version": "2.0", + "policy_file": "/opt/app/orm/aic-orm-cms/cms_rest/etc/policy.json" +} diff --git a/orm/services/customer_manager/cms_rest/tests/logic/__init__.py b/orm/services/customer_manager/cms_rest/tests/logic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/tests/logic/test_customer_logic.py b/orm/services/customer_manager/cms_rest/tests/logic/test_customer_logic.py new file mode 100755 index 00000000..014e0ddc --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/logic/test_customer_logic.py @@ -0,0 +1,563 @@ +from cms_rest.data.sql_alchemy import models as sql_models +from cms_rest.logic import customer_logic +from cms_rest.tests import FunctionalTest +from cms_rest.logic.error_base import ErrorStatus +import cms_rest.model.Models as models +import mock + +customer = None +data_manager_mock = None +record_mock = None +mock_returns_error = False +flow_type = 0 +rowcount = 1 + + +class resultobj(): + def __init__(self, rowcount=1): + self.rowcount = rowcount + + +class RdsStatus(object): + + def __init__(self, status_code=200, status="Success", oy=False): + self.status_code = status_code + self.status = status + self.oy = oy + + def json(self): + if self.oy: + return {} + else: + return {"status": self.status} + + +class TestCustomerLogic(FunctionalTest): + def setUp(self): + global customer + + FunctionalTest.setUp(self) + customer_logic.DataManager = get_mock_datamanager + customer_logic.pecan = mock.MagicMock() + + customer_logic.utils = mock.MagicMock() + customer_logic.utils.make_transid.return_value = 'some_trans_id' + customer_logic.utils.audit_trail.return_value = None + customer_logic.utils.make_uuid.return_value = 'some_uuid' + customer_logic.utils.get_time_human.return_value = '1337' + + customer_logic.RdsProxy = mock.MagicMock() + customer_logic.RdsProxy.send_customer.return_value = None + customer_logic.RdsProxy.get_status.return_value = RdsStatus() + + customer_logic.build_response = mock.MagicMock() + + customer = models.Customer() + user = models.User() + customer.users = [user, models.User()] + user.role = ['user', 'admin'] + + global flow_type + flow_type = 0 + + def tearDown(self): + global mock_returns_error + FunctionalTest.tearDown(self) + + mock_returns_error = False + + def test_create_customer_success_with_regions(self): + customer.regions = [models.Region(name="a")] + logic = customer_logic.CustomerLogic() + logic.create_customer(customer, 'some_uuid', 'some_trans_id') + + # sql_customer, trans_id = customer_logic.RdsProxy.send_customer.call_args_list[0][0] + # assert trans_id is 'some_trans_id' + # assert type(sql_customer) is sql_models.Customer + assert data_manager_mock.commit.called + assert not data_manager_mock.rollback.called + + def test_add_regions_action(self): + regions = [models.Region(), models.Region()] + logic = customer_logic.CustomerLogic() + logic.add_regions('some_uuid', regions, 'some_trans_id') + + res = customer_logic.RdsProxy.send_customer.call_args_list + + def test_create_customer_add_all_default_users(self): + logic = customer_logic.CustomerLogic() + logic.create_customer(customer, 'some_uuid', 'some_trans_id') + + assert data_manager_mock.add_user.call_count == 2 + + def test_create_customer_fail_rollback(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.create_customer, customer, 'some_uuid', 'some_trans_id') + + def test_update_customer_success(self): + logic = customer_logic.CustomerLogic() + logic.update_customer(customer, 'some_uuid', 'some_trans_id') + + assert record_mock.delete_by_primary_key.called + assert data_manager_mock.commit.called + assert not data_manager_mock.rollback.called + + def test_update_customer_fail_rollback(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.update_customer, customer, 'some_uuid', 'some_trans_id') + + def test_add_users_success(self): + logic = customer_logic.CustomerLogic() + users = [models.User(), models.User()] + + logic.add_users('some_uuid', 'some_region', users, 'some_transaction') + + assert data_manager_mock.add_user.call_count == 2 + assert data_manager_mock.commit.called + + def test_add_users_error(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + users = [models.User()] + + self.assertRaises(SystemError, logic.add_users, 'id', 'region', users, 'trans_id') + + def test_replace_users_success(self): + logic = customer_logic.CustomerLogic() + users = [models.User(), models.User()] + + logic.replace_users('some_uuid', 'some_region', users, 'some_transaction') + + assert record_mock.delete_all_users_from_region.called + assert data_manager_mock.add_user.called + assert data_manager_mock.commit.called + + def test_replace_users_error(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + users = [models.User()] + + self.assertRaises(SystemError, logic.replace_users, 'id', 'region', users, 'trans_id') + + def test_add_customer_with_default_users(self): + default_quota = models.Quota() + customer.defaultQuotas = [default_quota] + + default_region = models.Region() + customer.regions = [default_region] + + default_user = models.User() + default_user.role = ['user', 'admin'] + + default_region.users = [default_user] + + logic = customer_logic.CustomerLogic() + + logic.create_customer(customer, 'some_uuid', 'some_trans_id') + + assert data_manager_mock.add_user.called + + def test_add_users_with_roles_success(self): + user = models.User() + user.role = ['user', 'admin'] + + users = [user, models.User()] + + logic = customer_logic.CustomerLogic() + logic.add_users('some_uuid', 'region_name', users, 'some_trans_id') + + assert data_manager_mock.add_user.call_count == 2 + assert data_manager_mock.add_role.call_count == 2 + assert data_manager_mock.commit.called + + def test_delete_users_succes(self): + logic = customer_logic.CustomerLogic() + logic.delete_users('customer_id', 'region_id', 'user_id', 'transaction_is') + + assert record_mock.delete_user_from_region.called + assert data_manager_mock.commit.called + assert customer_logic.RdsProxy.send_customer.called + + def test_delete_users_fail_notfound(self): + global rowcount + rowcount = 0 + logic = customer_logic.CustomerLogic() + with self.assertRaises(customer_logic.NotFound): + logic.delete_users('customer_id', 'region_id', 'user_id', + 'transaction_is') + rowcount = 1 + assert record_mock.delete_user_from_region.called + assert data_manager_mock.rollback.called + + def test_delete_users_error(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.delete_users, 'customer_id', 'region_id', 'user_id', 'transaction_is') + assert data_manager_mock.rollback.called + + def test_add_default_users_with_regions_success(self): + user = models.User() + user.role = ['user', 'admin'] + + users = [user, models.User()] + logic = customer_logic.CustomerLogic() + + logic.add_default_users('customer_uuid', users, 'transaction_id') + + assert data_manager_mock.commit.called + assert data_manager_mock.add_user.call_count == 2 + assert data_manager_mock.add_role.call_count == 2 + + def test_add_default_users_no_regions_success(self): + user = models.User() + user.role = ['user', 'admin'] + + users = [user, models.User()] + logic = customer_logic.CustomerLogic() + + logic.add_default_users('customer_uuid', users, 'transaction_id') + + assert data_manager_mock.commit.called + assert not customer_logic.RdsProxy.send_customer.called + assert data_manager_mock.add_user.call_count == 2 + assert data_manager_mock.add_role.call_count == 2 + + def test_add_default_users_fail(self): + global mock_returns_error + mock_returns_error = True + users = [models.User(), models.User()] + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.add_default_users, 'customer_uuid', users, 'transaction_id') + assert data_manager_mock.rollback.called + + def test_replace_default_users_no_regions_success(self): + user = models.User() + user.role = ['user', 'admin'] + + users = [user, models.User()] + logic = customer_logic.CustomerLogic() + + logic.replace_default_users('customer_uuid', users, 'transaction_id') + + assert data_manager_mock.commit.called + assert record_mock.delete_all_users_from_region.called + assert not customer_logic.RdsProxy.send_customer.called + assert data_manager_mock.add_user.call_count == 2 + assert data_manager_mock.add_role.call_count == 2 + + def test_replace_default_users_fail(self): + global mock_returns_error + mock_returns_error = True + users = [models.User(), models.User()] + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.replace_default_users, 'id', users, 'trans_id') + assert data_manager_mock.rollback.called + + def test_delete_default_users_succes(self): + logic = customer_logic.CustomerLogic() + logic.delete_default_users('customer_id', 'user_id', 'transaction_is') + + assert record_mock.delete_user_from_region.called + assert data_manager_mock.commit.called + + def test_delete_default_users_fail_notfound(self): + global rowcount + rowcount = 0 + logic = customer_logic.CustomerLogic() + with self.assertRaises(customer_logic.NotFound): + logic.delete_default_users('customer_id', 'user_id', + 'transaction_is') + rowcount = 1 + assert record_mock.delete_user_from_region.called + assert data_manager_mock.rollback.called + + def test_delete_default_users_error(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.delete_default_users, 'customer_id', 'user_id', 'transaction_is') + assert data_manager_mock.rollback.called + + def test_add_regions_success(self): + regions = [models.Region(), models.Region()] + logic = customer_logic.CustomerLogic() + + logic.add_regions('customer_uuid', regions, 'transaction_id') + + assert data_manager_mock.commit.called + assert customer_logic.RdsProxy.send_customer_dict.called + # assert data_manager_mock.add_region.called + + def test_add_regions_fail(self): + global mock_returns_error + mock_returns_error = True + regions = [models.Region(), models.Region()] + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.add_regions, 'customer_uuid', regions, 'transaction_id') + assert data_manager_mock.rollback.called + + def test_replace_regions_success(self): + regions = [models.Region(), models.Region()] + logic = customer_logic.CustomerLogic() + + logic.replace_regions('customer_uuid', regions, 'transaction_id') + + assert data_manager_mock.commit.called + assert customer_logic.RdsProxy.send_customer_dict.called + assert record_mock.delete_all_regions_for_customer.called + + def test_replace_regions_fail(self): + global mock_returns_error + mock_returns_error = True + regions = [models.Region(), models.Region()] + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.replace_regions, 'customer_uuid', regions, 'transaction_id') + assert data_manager_mock.rollback.called + + def test_delete_regions_succes(self): + logic = customer_logic.CustomerLogic() + logic.delete_region('customer_id', 'region_id', 'transaction_is') + + assert record_mock.delete_region_for_customer.called + assert data_manager_mock.commit.called + assert customer_logic.RdsProxy.send_customer_dict.called + + def test_delete_regions_error(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.delete_region, 'customer_id', 'region_id', 'transaction_is') + assert data_manager_mock.rollback.called + + def test_get_customer_list_by_criteria(self): + logic = customer_logic.CustomerLogic() + result = logic.get_customer_list_by_criteria(None, None, None, None, + {"key:value"}) + + def test_delete_customer_by_uuid_success(self): + logic = customer_logic.CustomerLogic() + logic.delete_customer_by_uuid('customer_id') + + # Customer found in CMS DB but not found in RDS + customer_logic.RdsProxy.get_status.return_value = RdsStatus( + status_code=404) + logic.delete_customer_by_uuid('customer_id') + + global flow_type + # Change the flow to "customer not found in CMS DB" + flow_type = 1 + logic.delete_customer_by_uuid('customer_id') + + def test_delete_customer_by_uuid_errors(self): + global mock_returns_error + mock_returns_error = True + logic = customer_logic.CustomerLogic() + self.assertRaises(SystemError, logic.delete_customer_by_uuid, 'customer_id') + + # RDS returned an empty json + mock_returns_error = False + customer_logic.RdsProxy.get_status.return_value = RdsStatus(oy=True) + self.assertRaises(customer_logic.ErrorStatus, + logic.delete_customer_by_uuid, + 'customer_id') + + # RDS returned 500 + customer_logic.RdsProxy.get_status.return_value = RdsStatus( + status_code=500) + self.assertRaises(customer_logic.ErrorStatus, + logic.delete_customer_by_uuid, + 'customer_id') + + # RDS returned Error status + customer_logic.RdsProxy.get_status.return_value = RdsStatus( + status='Error') + self.assertRaises(customer_logic.ErrorStatus, + logic.delete_customer_by_uuid, + 'customer_id') + + def test_delete_customer_by_uuid_conflict(self): + global flow_type + flow_type = 2 + logic = customer_logic.CustomerLogic() + self.assertRaises(customer_logic.ErrorStatus, logic.delete_customer_by_uuid, + 'customer_id') + + def test_enable_success(self): + logic = customer_logic.CustomerLogic() + logic.enable('customer_id', models.Enabled(True), 'transaction_is') + + self.assertTrue(record_mock.read_customer_by_uuid.called) + self.assertTrue(customer_logic.RdsProxy.send_customer.called) + + def test_enable_error(self): + global mock_returns_error + mock_returns_error = True + + logic = customer_logic.CustomerLogic() + + self.assertRaises(SystemError, logic.enable, 'id', models.Enabled(True), 'trans_id') + self.assertTrue(data_manager_mock.rollback.called) + + def test_get_customer_success(self): + logic = customer_logic.CustomerLogic() + get_mock = mock.MagicMock() + get_mock.json.return_value = STATUS_JSON + customer_logic.requests.get = mock.MagicMock(return_value=get_mock) + logic.get_customer('customer_id') + + self.assertTrue(data_manager_mock.get_cusomer_by_uuid_or_name.called) + + def test_get_customer_not_found(self): + global flow_type + flow_type = 1 + # customer_logic.requests.get = mock.MagicMock(return_value=None) + + logic = customer_logic.CustomerLogic() + + self.assertRaises(ErrorStatus, logic.get_customer, 'id') + + +def get_mock_datamanager(): + global data_manager_mock + global record_mock + global rowcount + + sql_customer = sql_models.Customer(name='a') + sql_customer.customer_customer_regions = [] + + sql_customer.add_default_users_to_empty_regions = sql_customer + + data_manager_mock = mock.MagicMock() + record_mock = mock.MagicMock() + record_mock.get_customers_by_criteria.return_value = [sql_customer] + + def _get_proxy_dict(): + return { + "uuid": 'a', + "name": 'a', + "description": 'a', + "enabled": 1, + "regions": [] + } + + def _get_customer(): + def mock_to_wsme(): + return models.Customer(regions=[models.Region(name='DPK', status='Success')]) + + def mock_get_real(): + return True + + sql_customer = sql_models.Customer() + sql_customer.get_real_customer_regions = mock_get_real + sql_customer.to_wsme = mock_to_wsme + sql_customer.uuid = '1337' + sql_customer.status = 'Success' + sql_customer.name = 'DPK' + + return sql_customer + + def _add_customer(*args, **kwargs): + global sql_customer + sql_customer = sql_models.Customer() + sql_customer.customer_customer_regions = [sql_models.CustomerRegion(region_id=-1)] + sql_customer.customer_customer_regions.customer_region_user_roles = [sql_models.UserRole()] + sql_customer.add_default_users_to_empty_regions = sql_customer + sql_customer.get_proxy_dict = _get_proxy_dict + return sql_customer + + def _update_customer(*args, **kwargs): + global sql_customer + sql_customer = sql_models.Customer(name='a') + sql_customer.customer_customer_regions = [sql_models.CustomerRegion(region_id=-1)] + sql_customer.customer_customer_regions.customer_region_user_roles = [sql_models.UserRole()] + sql_customer.add_default_users_to_empty_regions = sql_customer + sql_customer.get_proxy_dict = _get_proxy_dict + return sql_customer + + def _add_region(*args, **kwargs): + global sql_customer + region = sql_models.Region() + + # sql_customer.customer_customer_regions.append(region) + return region + + def _add_users(*args, **kwargs): + global sql_customer + users = sql_models.UserRole() + + sql_customer.customer_customer_regions.customer_region_user_roles = users + return users + + if not mock_returns_error: + data_manager_mock.add_customer = _add_customer + data_manager_mock.update_customer = _update_customer + data_manager_mock.add_region = _add_region + data_manager_mock.add_user.return_value = sql_models.CmsUser() + data_manager_mock.get_cusomer_by_uuid_or_name.return_value = _get_customer() + + record_mock.delete_region_for_customer.return_value = None + record_mock.delete_customer_by_uuid.return_value = None + if flow_type == 1: + record_mock.read_customer_by_uuid.return_value = None + data_manager_mock.get_cusomer_by_uuid_or_name.return_value = None + elif flow_type == 2: + q = mock.MagicMock() + q.get_real_customer_regions.return_value = [mock.MagicMock()] + record_mock.read_customer_by_uuid.return_value = q + + record_mock.delete_user_from_region.return_value = resultobj(rowcount) + else: + record_mock.read_customer_by_uuid.side_effect = SystemError() + data_manager_mock.add_customer.side_effect = SystemError() + data_manager_mock.add_user.side_effect = SystemError() + data_manager_mock.add_region.side_effect = SystemError() + + record_mock.delete_region_for_customer.side_effect = SystemError() + record_mock.delete_user_from_region.side_effect = SystemError() + record_mock.delete_customer_by_uuid.side_effect = SystemError() + + data_manager_mock.get_record.return_value = record_mock + data_manager_mock.add_user.return_value = sql_models.CmsUser() + data_manager_mock.add_role.return_value = sql_models.CmsRole() + + data_manager_mock.get_cusomer_by_id.return_value = sql_customer + + return data_manager_mock + + +STATUS_JSON = { + "regions": [ + { + "status": "Success", + "region": "DPK", + "error_code": "", + "error_msg": "" + } + ], + "status": "Success" +} diff --git a/orm/services/customer_manager/cms_rest/tests/rest/__init__.py b/orm/services/customer_manager/cms_rest/tests/rest/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/tests/rest/test_customer.py b/orm/services/customer_manager/cms_rest/tests/rest/test_customer.py new file mode 100755 index 00000000..142e79e7 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/rest/test_customer.py @@ -0,0 +1,475 @@ +import mock +import requests +import sqlalchemy +from wsme.exc import ClientSideError + +from cms_rest.controllers.v1.orm.customer import root +from cms_rest.logic.error_base import ErrorStatus + +from cms_rest.model import Models +from cms_rest.tests import FunctionalTest +from cms_rest.tests import test_utils + +customer_logic_mock = None + + +class TestCustomerController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + root.authentication = mock.MagicMock() + + root.CustomerLogic = get_mock_customer_logic + root.CustomerLogic.return_error = 0 + + root.utils = mock.MagicMock() + root.utils.make_transid.return_value = 'some_trans_id' + root.utils.audit_trail.return_value = None + root.utils.make_uuid.return_value = 'some_uuid' + + root.err_utils = mock.MagicMock() + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_create_customer(self): + # given + requests.post = mock.MagicMock(return_value=ResponseMock(201)) + + # when + response = self.app.post_json('/v1/orm/customers', CUSTOMER_JSON) + + # assert + assert response.status_int == 201 + assert root.utils.audit_trail.called + assert root.utils.make_uuid.called + assert customer_logic_mock.create_customer.called + + def test_create_customer_fail(self): + # given + requests.post = mock.MagicMock() + + root.CustomerLogic.return_error = 1 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.post_json('/v1/orm/customers', CUSTOMER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + @mock.patch.object(root, 'CustomerLogic') + def test_create_flavor_duplicate_name(self, mock_customerlogic): + my_mock = mock.MagicMock() + my_mock.create_customer = mock.MagicMock( + side_effect=sqlalchemy.exc.IntegrityError( + 'a', 'b', + 'Duplicate entry \'customer\' for key \'name_idx\'')) + mock_customerlogic.return_value = my_mock + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 409)) + + response = self.app.post_json('/v1/orm/customers', CUSTOMER_JSON, + expect_errors=True) + + self.assertEqual(response.status_int, 409) + + def test_create_flavor_duplicate_uuid(self): + CUSTOMER_JSON['custId'] = 'test' + create_existing_uuid = root.utils.create_existing_uuid + + root.utils.create_existing_uuid = mock.MagicMock(side_effect=TypeError('test')) + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 409)) + response = self.app.post_json('/v1/orm/customers', CUSTOMER_JSON, + expect_errors=True) + + root.utils.create_existing_uuid = create_existing_uuid + del CUSTOMER_JSON['custId'] + + self.assertEqual(response.status_int, 409) + + @mock.patch.object(root, 'CustomerLogic') + def test_create_flavor_other_error(self, mock_customerlogic): + my_mock = mock.MagicMock() + my_mock.create_customer = mock.MagicMock( + side_effect=sqlalchemy.exc.IntegrityError( + 'a', 'b', + 'test \'customer\' for key \'name_idx\'')) + mock_customerlogic.return_value = my_mock + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + response = self.app.post_json('/v1/orm/customers', CUSTOMER_JSON, + expect_errors=True) + + self.assertEqual(response.status_int, 500) + + def test_create_customer_fail_bad_request(self): + # given + requests.post = mock.MagicMock() + + root.CustomerLogic.return_error = 2 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.post_json('/v1/orm/customers', CUSTOMER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_update_customer(self): + # given + requests.put = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.put_json('/v1/orm/customers/some_id', CUSTOMER_JSON) + + # assert + assert response.status_int == 200 + assert root.utils.audit_trail.called + assert customer_logic_mock.update_customer.called + + def test_update_customer_fail(self): + # given + requests.put = mock.MagicMock() + + root.CustomerLogic.return_error = 1 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.put_json('/v1/orm/customers/some_id', CUSTOMER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_update_customer_fail_bad_request(self): + # given + requests.put = mock.MagicMock() + + root.CustomerLogic.return_error = 2 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.put_json('/v1/orm/customers/some_id', CUSTOMER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_get_customer(self): + # given + requests.get = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.get('/v1/orm/customers/some_id') + + # assert + assert response.status_int == 200 + assert customer_logic_mock.get_customer.called + + def test_get_customer_fail_bad_request(self): + # given + requests.put = mock.MagicMock() + + root.CustomerLogic.return_error = 1 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.get('/v1/orm/customers/some_id', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + assert customer_logic_mock.get_customer.called + + def test_get_customer_fail(self): + # given + requests.put = mock.MagicMock() + + root.CustomerLogic.return_error = 2 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.get('/v1/orm/customers/some_id', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + assert customer_logic_mock.get_customer.called + + def test_get_list_customer(self): + # given + requests.get = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.get('/v1/orm/customers?region=SAN1') + + # assert + assert customer_logic_mock.get_customer_list_by_criteria.called + + def test_get_list_customer_fail(self): + # given + requests.get = mock.MagicMock() + root.CustomerLogic.return_error = 1 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.get('/v1/orm/customers?region=region', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_get_list_customer_bad_request(self): + # given + requests.get = mock.MagicMock() + root.CustomerLogic.return_error = 2 + + root.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.get('/v1/orm/customers?region=region', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + @mock.patch.object(root, 'authentication') + def test_delete_customer_success(self, mock_auth): + response = self.app.delete('/v1/orm/customers/test') + self.assertEqual(response.status_int, 204) + + @mock.patch.object(root, 'authentication') + def test_delete_customer_conflict(self, mock_auth): + root.CustomerLogic.return_error = 2 + root.err_utils.get_error = test_utils.get_error + response = self.app.delete('/v1/orm/customers/test', expect_errors=True) + + self.assertEqual(response.status_int, 409) + + @mock.patch.object(root, 'authentication') + def test_delete_customer_error(self, mock_auth): + root.CustomerLogic.return_error = 1 + root.err_utils.get_error = test_utils.get_error + response = self.app.delete('/v1/orm/customers/test', expect_errors=True) + + self.assertEqual(response.status_int, 500) + + +def get_mock_customer_logic(): + global customer_logic_mock + customer_logic_mock = mock.MagicMock() + + if root.CustomerLogic.return_error == 0: + res = Models.CustomerResultWrapper(transaction_id='1', + id='1', + links={}, + updated=None, + created='1') + + list_res = Models.CustomerSummaryResponse() + list_res.customers.append(Models.CustomerSummary(name='1', id='1', description='1')) + + customer_logic_mock.create_customer.return_value = res + customer_logic_mock.update_customer.return_value = res + customer_logic_mock.get_customer.return_value = Models.Customer(**RET_CUSTOMER_JSON) + customer_logic_mock.get_customer_list_by_criteria.return_value = list_res + + elif root.CustomerLogic.return_error == 1: + customer_logic_mock.create_customer.side_effect = SystemError() + customer_logic_mock.update_customer.side_effect = SystemError() + customer_logic_mock.get_customer.side_effect = SystemError() + customer_logic_mock.delete_customer_by_uuid.side_effect = SystemError() + customer_logic_mock.get_customer_list_by_criteria.side_effect = SystemError() + + else: + customer_logic_mock.create_customer.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.update_customer.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.get_customer.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.delete_customer_by_uuid.side_effect = ErrorStatus( + status_code=409) + customer_logic_mock.get_customer_list_by_criteria.side_effect = ErrorStatus(status_code=404) + + return customer_logic_mock + + +class ResponseMock: + def __init__(self, status_code=200): + self.status_code = status_code + + +CUSTOMER_JSON = { + "description": "Customer description", + "enabled": True, + "name": "myDomain", + "metadata": { + "my_server_name": "Apache1", + "ocx_cust": "123456889" + }, + "regions": [ + { + "name": "SAN1", + "type": "single", + "quotas": [ + { + "compute": [ + { + "instances": "10", + "injected-files": "10", + "key-pairs": "10", + "ram": "10" + } + ], + "storage": [ + { + "gigabytes": "10", + "snapshots": "10", + "volumes": "10" + } + ], + "network": [ + { + "floating-ips": "10", + "networks": "10", + "ports": "10", + "routers": "10", + "subnets": "10" + } + ] + } + ] + }, + { + "name": "AIC_MEDIUM", + "type": "group", + "quotas": [ + { + "compute": [ + { + "instances": "10", + "injected-files": "10", + "key-pairs": "10", + "ram": "10" + } + ], + "storage": [ + { + "gigabytes": "10", + "snapshots": "10", + "volumes": "10" + } + ], + "network": [ + { + "floating-ips": "10", + "networks": "10", + "ports": "10", + "routers": "10", + "subnets": "10" + } + ] + } + ] + } + ], + "users": [ + { + "id": "userId1", + "role": [ + "admin", + "other" + ] + }, + { + "id": "userId2", + "role": [ + "storage" + ] + } + ], + "defaultQuotas": [ + { + "compute": [ + { + "instances": "10", + "injected-files": "10", + "key-pairs": "10", + "ram": "10" + } + ], + "storage": [ + { + "gigabytes": "10", + "snapshots": "10", + "volumes": "10" + } + ], + "network": [ + { + "floating-ips": "10", + "networks": "10", + "ports": "10", + "routers": "10", + "subnets": "10" + } + ] + } + ] +} + +RET_CUSTOMER_JSON = { + "description": "Customer description", + "enabled": True, + "name": "myDomain", + "metadata": { + "my_server_name": "Apache1", + "ocx_cust": "123456889" + }, + "regions": [Models.Region(**{"name": "SAN1", "type": "single", "quotas": [Models.Quota(**{ + "compute": [Models.Compute(instances='1', injected_files='1', key_pairs='1', ram='1', + vcpus='1', metadata_items='1', injected_file_content_bytes='1', + floating_ips='1', fixed_ips='1', injected_file_path_bytes='1', + server_groups='1', server_group_members='1')], + "storage": [Models.Storage(gigabytes='1', snapshots='1', volumes='1')], + "network": [Models.Network(floating_ips='1', networks='1', ports='1', routers='1', subnets='1', + security_groups='1', security_group_rules='1', health_monitor='1', + member='1', pool='1', nat_instance='1', route_table='1', vip='1')] + })]})], + "users": [Models.User(** + {"id": "userId1", "role": ["admin", "other"]}) + ], + "defaultQuotas": [Models.Quota(**{ + "compute": [Models.Compute(instances='1', injected_files='1', key_pairs='1', ram='1', + vcpus='1', metadata_items='1', injected_file_content_bytes='1', + floating_ips='1', fixed_ips='1', injected_file_path_bytes='1', + server_groups='1', server_group_members='1')], + "storage": [Models.Storage(gigabytes='1', snapshots='1', volumes='1')], + "network": [Models.Network(floating_ips='1', networks='1', ports='1', routers='1', subnets='1', + security_groups='1', security_group_rules='1', health_monitor='1', + member='1', pool='1', nat_instance='1', route_table='1', vip='1')] + })] +} + +INVALID_CREATE_CUSTOMER_DATA = { + "descriptionInvalid": "Customer description", + "enabled": True, + "name": "myDomain", + "metadata": { + "my_server_name": "Apache1", + "ocx_cust": "123456889" + } +} diff --git a/orm/services/customer_manager/cms_rest/tests/rest/test_enable.py b/orm/services/customer_manager/cms_rest/tests/rest/test_enable.py new file mode 100755 index 00000000..ccbb3733 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/rest/test_enable.py @@ -0,0 +1,101 @@ +import mock +import requests + +from cms_rest.controllers.v1.orm.customer import enabled +from cms_rest.logic.error_base import ErrorStatus +from cms_rest.model import Models +from cms_rest.tests import FunctionalTest + +customer_logic_mock = None + + +class TestEnabledController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + enabled.authentication = mock.MagicMock() + + enabled.CustomerLogic = get_mock_customer_logic + enabled.CustomerLogic.return_error = 0 + + enabled.utils = mock.MagicMock() + enabled.utils.make_transid.return_value = 'some_trans_id' + enabled.utils.audit_trail.return_value = None + enabled.utils.make_uuid.return_value = 'some_uuid' + + enabled.err_utils = mock.MagicMock() + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_enable(self): + # given + requests.put = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/enabled/', ENABLED_JSON) + + # assert + self.assertTrue(response.status_int, 200) + self.assertTrue(enabled.utils.audit_trail.called) + self.assertTrue(customer_logic_mock.enable.called) + + def test_enable_fail(self): + # given + requests.put = mock.MagicMock() + enabled.CustomerLogic.return_error = 2 + enabled.CustomerLogic = get_mock_customer_logic + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/enabled/', + ENABLED_JSON, expect_errors=True) + + # assert + self.assertTrue(response.status_int, 404) + self.assertTrue(customer_logic_mock.enable.called) + + def test_enable_bad_request(self): + # given + requests.put = mock.MagicMock() + enabled.CustomerLogic.return_error = 1 + enabled.CustomerLogic = get_mock_customer_logic + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/enabled/', + ENABLED_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(customer_logic_mock.enable.called) + + +def get_mock_customer_logic(): + global customer_logic_mock + customer_logic_mock = mock.MagicMock() + + if enabled.CustomerLogic.return_error == 0: + res = Models.CustomerResultWrapper(transaction_id='1', + id='1', + links={}, + updated=None, + created='1') + + customer_logic_mock.enable.return_value = res + + elif enabled.CustomerLogic.return_error == 1: + customer_logic_mock.enable.side_effect = SystemError() + + elif enabled.CustomerLogic.return_error == 2: + customer_logic_mock.enable.side_effect = ErrorStatus(status_code=404) + + return customer_logic_mock + + +class ResponseMock: + def __init__(self, status_code=200): + self.status_code = status_code + + +ENABLED_JSON = { + "enabled": "true" +} diff --git a/orm/services/customer_manager/cms_rest/tests/rest/test_metadata.py b/orm/services/customer_manager/cms_rest/tests/rest/test_metadata.py new file mode 100755 index 00000000..6e7f2057 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/rest/test_metadata.py @@ -0,0 +1,243 @@ +import mock +import requests + +from cms_rest.controllers.v1.orm.customer import metadata +from cms_rest.logic.error_base import ErrorStatus +from cms_rest.model import Models +from cms_rest.tests import FunctionalTest + +metadata_logic_mock = None + + +class TestMetadataController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + metadata.authentication = mock.MagicMock() + + metadata.logic.return_error = 0 + metadata.logic = get_mock_customer_logic() + + metadata.utils = mock.MagicMock() + metadata.utils.make_transid.return_value = 'some_trans_id' + metadata.utils.audit_trail.return_value = None + metadata.utils.make_uuid.return_value = 'some_uuid' + + metadata.err_utils = mock.MagicMock() + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_add_metadata(self): + # given + requests.post = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/metadata/', METADATA_JSON) + + # assert + self.assertTrue(response.status_int, 200) + self.assertTrue(metadata.utils.audit_trail.called) + self.assertTrue(metadata_logic_mock.add_customer_metadata.called) + + def test_add_metadata_fail(self): + # given + requests.post = mock.MagicMock() + metadata.logic.return_error = 2 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertTrue(response.status_int, 404) + self.assertTrue(metadata_logic_mock.add_customer_metadata.called) + + def test_add_metadata_bad_request(self): + # given + requests.post = mock.MagicMock() + metadata.logic.return_error = 1 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.add_customer_metadata.called) + + def test_add_metadata_lu_Error(self): + # given + requests.post = mock.MagicMock() + metadata.logic.return_error = 3 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.add_customer_metadata.called) + + def test_add_metadata_att_Error(self): + # given + requests.post = mock.MagicMock() + metadata.logic.return_error = 4 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.add_customer_metadata.called) + + def test_add_metadata_value_Error(self): + # given + requests.post = mock.MagicMock() + metadata.logic.return_error = 5 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.add_customer_metadata.called) + + def test_update_metadata(self): + # given + requests.put = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/metadata/', METADATA_JSON) + + # assert + self.assertTrue(response.status_int, 200) + self.assertTrue(metadata.utils.audit_trail.called) + self.assertTrue(metadata_logic_mock.update_customer_metadata.called) + + def test_update_metadata_fail(self): + # given + requests.put = mock.MagicMock() + metadata.logic.return_error = 2 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertTrue(response.status_int, 404) + self.assertTrue(metadata_logic_mock.update_customer_metadata.called) + + def test_update_metadata_bad_request(self): + # given + requests.put = mock.MagicMock() + metadata.logic.return_error = 1 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.update_customer_metadata.called) + + def test_update_metadata_lu_Error(self): + # given + requests.put = mock.MagicMock() + metadata.logic.return_error = 3 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.update_customer_metadata.called) + + def test_update_metadata_att_Error(self): + # given + requests.put = mock.MagicMock() + metadata.logic.return_error = 4 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.update_customer_metadata.called) + + def test_update_metadata_value_Error(self): + # given + requests.put = mock.MagicMock() + metadata.logic.return_error = 5 + metadata.logic = get_mock_customer_logic() + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/metadata/', + METADATA_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + self.assertTrue(metadata_logic_mock.update_customer_metadata.called) + + +def get_mock_customer_logic(): + global metadata_logic_mock + metadata_logic_mock = mock.MagicMock() + + if metadata.logic.return_error == 0: + res = Models.CustomerResultWrapper(transaction_id='1', + id='1', + links={}, + updated=None, + created='1') + + metadata_logic_mock.add_customer_metadata.return_value = res + metadata_logic_mock.update_customer_metadata.return_value = res + + elif metadata.logic.return_error == 1: + metadata_logic_mock.add_customer_metadata.side_effect = SystemError() + metadata_logic_mock.update_customer_metadata.side_effect = SystemError() + + elif metadata.logic.return_error == 2: + metadata_logic_mock.add_customer_metadata.side_effect = ErrorStatus(status_code=404) + metadata_logic_mock.update_customer_metadata.side_effect = ErrorStatus(status_code=404) + + elif metadata.logic.return_error == 3: + metadata_logic_mock.add_customer_metadata.side_effect = LookupError() + metadata_logic_mock.update_customer_metadata.side_effect = LookupError() + + elif metadata.logic.return_error == 4: + metadata_logic_mock.add_customer_metadata.side_effect = AttributeError() + metadata_logic_mock.update_customer_metadata.side_effect = AttributeError() + + elif metadata.logic.return_error == 5: + metadata_logic_mock.add_customer_metadata.side_effect = ValueError() + metadata_logic_mock.update_customer_metadata.side_effect = ValueError() + + return metadata_logic_mock + + +class ResponseMock: + def __init__(self, status_code=200): + self.status_code = status_code + + +METADATA_JSON = { + "metadata": { + "my_server_name": "Apache1", + "ocx_cust": "12356889" + } +} diff --git a/orm/services/customer_manager/cms_rest/tests/rest/test_regions.py b/orm/services/customer_manager/cms_rest/tests/rest/test_regions.py new file mode 100755 index 00000000..07b135fd --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/rest/test_regions.py @@ -0,0 +1,240 @@ +import mock +import requests + +from wsme.exc import ClientSideError + +from cms_rest.controllers.v1.orm.customer import regions +from cms_rest.logic.error_base import ErrorStatus +from cms_rest.model import Models +from cms_rest.tests import FunctionalTest + +customer_logic_mock = None + + +class TestRegionController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + regions.authentication = mock.MagicMock() + + regions.CustomerLogic = get_mock_customer_logic + regions.CustomerLogic.return_error = 0 + + regions.utils = mock.MagicMock() + regions.utils.make_transid.return_value = 'some_trans_id' + regions.utils.audit_trail.return_value = None + regions.utils.make_uuid.return_value = 'some_uuid' + + regions.err_utils = mock.MagicMock() + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_add_regions(self): + # given + requests.post = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/regions/', REGION_JSON) + + # assert + assert response.status_int == 200 + assert regions.utils.audit_trail.called + assert customer_logic_mock.add_regions.called + + def test_add_regions_fail(self): + # given + requests.post = mock.MagicMock() + + regions.CustomerLogic.return_error = 1 + + regions.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/regions/', REGION_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_replace_regions_specific_region(self): + regions.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 405)) + + response = self.app.put_json( + '/v1/orm/customers/{customer id}/regions/test', REGION_JSON, + expect_errors=True) + self.assertEqual(response.status_int, 405) + + def test_add_regions_fail_bad(self): + # given + requests.post = mock.MagicMock() + + regions.CustomerLogic.return_error = 2 + + regions.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/regions/', REGION_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_replace_regions(self): + # given + requests.put = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/regions/', REGION_JSON) + + # assert + assert response.status_int == 200 + assert regions.utils.audit_trail.called + assert customer_logic_mock.replace_regions.called + + def test_replace_regions_fail(self): + # given + requests.put = mock.MagicMock() + + regions.CustomerLogic.return_error = 1 + + regions.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/regions/', REGION_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_replace_regions_fail_bad(self): + # given + requests.put = mock.MagicMock() + + regions.CustomerLogic.return_error = 2 + + regions.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/regions/', REGION_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_delete_regions(self): + # given + requests.delete = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.delete('/v1/orm/customers/{customer id}/regions/{region_id}') + + # assert + assert response.status_int == 204 + assert regions.utils.audit_trail.called + assert customer_logic_mock.delete_region.called + + def test_delete_regions_fail_bad(self): + # given + requests.delete = mock.MagicMock() + + regions.CustomerLogic.return_error = 1 + + regions.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.delete('/v1/orm/customers/{customer id}/regions/{region_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_delete_regions_fail(self): + # given + requests.delete = mock.MagicMock() + + regions.CustomerLogic.return_error = 2 + + regions.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.delete('/v1/orm/customers/{customer id}/regions/{region_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_get(self): + # given + requests.get = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.get('/v1/orm/customers/some_id/regions/some_id') + + # assert + assert response.status_int == 200 + + +def get_mock_customer_logic(): + global customer_logic_mock + customer_logic_mock = mock.MagicMock() + + if regions.CustomerLogic.return_error == 0: + res = Models.RegionResultWrapper(transaction_id='1', regions=[]) + + customer_logic_mock.add_regions.return_value = res + customer_logic_mock.replace_regions.return_value = res + + elif regions.CustomerLogic.return_error == 1: + customer_logic_mock.add_regions.side_effect = SystemError() + customer_logic_mock.replace_regions.side_effect = SystemError() + customer_logic_mock.delete_region.side_effect = SystemError() + + else: + customer_logic_mock.add_regions.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.replace_regions.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.delete_region.side_effect = ErrorStatus(status_code=404) + + return customer_logic_mock + + +class ResponseMock: + def __init__(self, status_code=200): + self.status_code = status_code + + +REGION_JSON = [ + { + "name": "SAN1", + "type": "single", + "quotas": [ + { + "compute": [ + { + "instances": "10", + "injected-files": "10", + "key-pairs": "10", + "ram": "10" + } + ], + "storage": [ + { + "gigabytes": "10", + "snapshots": "10", + "volumes": "10" + } + ], + "network": [ + { + "floating-ips": "10", + "networks": "10", + "ports": "10", + "routers": "10", + "subnets": "10" + } + ] + } + ] + } +] diff --git a/orm/services/customer_manager/cms_rest/tests/rest/test_users.py b/orm/services/customer_manager/cms_rest/tests/rest/test_users.py new file mode 100755 index 00000000..fc1f6d70 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/rest/test_users.py @@ -0,0 +1,363 @@ +import mock +import requests +from wsme.exc import ClientSideError + +from cms_rest.controllers.v1.orm.customer import users +from cms_rest.logic.error_base import ErrorStatus +from cms_rest.model import Models +from cms_rest.tests import FunctionalTest + +customer_logic_mock = None + + +class TestUserController(FunctionalTest): + def setUp(self): + global original_audit_trail + FunctionalTest.setUp(self) + + users.authentication = mock.MagicMock() + + users.CustomerLogic = get_mock_customer_logic + users.CustomerLogic.return_error = 0 + + users.utils = mock.MagicMock() + + users.utils = mock.MagicMock() + users.utils.make_transid.return_value = 'some_trans_id' + users.utils.audit_trail.return_value = None + users.utils.make_uuid.return_value = 'some_uuid' + users.utils.make_userstransid.return_value = 'some_trans_id' + + users.err_utils = mock.MagicMock() + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_add_default_users(self): + # given + requests.post = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/users/', USER_JSON) + + # assert + self.assertEqual(response.status_int, 200) + self.assertTrue(customer_logic_mock.add_default_users.called) + + def test_add_default_users_fail(self): + # given + requests.post = mock.MagicMock() + + users.CustomerLogic.return_error = 1 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/users/', USER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_add_default_users_fail_bad_request(self): + # given + requests.post = mock.MagicMock() + + users.CustomerLogic.return_error = 2 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.post_json('/v1/orm/customers/{customer id}/users/', USER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_replace_default_users(self): + # given + requests.put = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/users/', USER_JSON) + + # assert + self.assertEqual(response.status_int, 200) + self.assertTrue(customer_logic_mock.replace_default_users.called) + + def test_replace_default_users_fail(self): + # given + requests.put = mock.MagicMock() + + users.CustomerLogic.return_error = 1 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/users/', + USER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_replace_default_users_fail_bad_request(self): + # given + requests.put = mock.MagicMock() + + users.CustomerLogic.return_error = 2 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.put_json('/v1/orm/customers/{customer id}/users/', + USER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_delete_default_user(self): + # given + requests.delete = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.delete('/v1/orm/customers/{customer id}/users/{user_id}') + + # assert + self.assertEqual(response.status_int, 204) + self.assertTrue(users.utils.audit_trail.called) + self.assertTrue(customer_logic_mock.delete_default_users.called) + + def test_delete_default_user_fail(self): + # given + requests.delete = mock.MagicMock() + + users.CustomerLogic.return_error = 1 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.delete('/v1/orm/customers/{customer id}/users/{user_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_delete_default_user_fail_bad_request(self): + # given + requests.delete = mock.MagicMock() + + users.CustomerLogic.return_error = 2 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.delete('/v1/orm/customers/{customer id}/users/{user_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_get_default(self): + # given + requests.get = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.get('/v1/orm/customers/some_id/users') + + # assert + self.assertEqual(response.status_int, 200) + + def test_add_users(self): + # given + requests.post = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.post_json('/v1/orm/customers/{some_id}/regions/{some_id}/users/', USER_JSON) + + # assert + self.assertEqual(response.status_int, 200) + self.assertTrue(users.utils.audit_trail.called) + self.assertTrue(customer_logic_mock.add_users.called) + + def test_add_users_fail(self): + # given + requests.post = mock.MagicMock() + + users.CustomerLogic.return_error = 1 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.post_json('/v1/orm/customers/{some_id}/regions/{some_id}/users/', USER_JSON, + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_add_users_fail_bad_request(self): + # given + requests.post = mock.MagicMock() + + users.CustomerLogic.return_error = 2 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.post_json('/v1/orm/customers/{some_id}/regions/{some_id}/users/', USER_JSON, + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_replace_users(self): + # given + requests.put = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.put_json('/v1/orm/customers/{some_id}/regions/{some_id}/users/', USER_JSON) + + # assert + self.assertEqual(response.status_int, 200) + self.assertTrue(users.utils.audit_trail.called) + self.assertTrue(customer_logic_mock.replace_users.called) + + def test_replace_users_fail(self): + # given + requests.put = mock.MagicMock() + + users.CustomerLogic.return_error = 1 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.put_json('/v1/orm/customers/{some_id}/regions/{some_id}/users/', + USER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_replace_users_fail_bad_request(self): + # given + requests.put = mock.MagicMock() + + users.CustomerLogic.return_error = 2 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.put_json('/v1/orm/customers/{some_id}/regions/{some_id}/users/', + USER_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_delete_user(self): + # given + requests.delete = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.delete('/v1/orm/customers/{some_id}/regions/{some_id}/users/{user_id}') + + # assert + self.assertEqual(response.status_int, 204) + self.assertTrue(users.utils.audit_trail.called) + self.assertTrue(customer_logic_mock.delete_users.called) + + def test_delete_user_fail(self): + # given + requests.delete = mock.MagicMock() + + users.CustomerLogic.return_error = 1 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 500)) + + # when + response = self.app.delete('/v1/orm/customers/{some_id}/regions/{some_id}/users/{user_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_delete_user_fail_bad_request(self): + # given + requests.delete = mock.MagicMock() + + users.CustomerLogic.return_error = 2 + + users.err_utils.get_error = mock.MagicMock(return_value=ClientSideError("blabla", + 404)) + + # when + response = self.app.delete('/v1/orm/customers/{some_id}/regions/{some_id}/users/{user_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_get(self): + # given + requests.get = mock.MagicMock(return_value=ResponseMock(200)) + + # when + response = self.app.get('/v1/orm/customers/{some_id}/regions/{some_id}/users') + + # assert + self.assertEqual(response.status_int, 200) + + +def get_mock_customer_logic(): + global customer_logic_mock + customer_logic_mock = mock.MagicMock() + + if users.CustomerLogic.return_error == 0: + res = Models.UserResultWrapper(transaction_id='1', users=[]) + + customer_logic_mock.add_default_users.return_value = res + customer_logic_mock.add_users.return_value = res + customer_logic_mock.replace_default_users.return_value = res + customer_logic_mock.replace_users.return_value = res + + elif users.CustomerLogic.return_error == 1: + customer_logic_mock.add_users.side_effect = SystemError() + customer_logic_mock.add_default_users.side_effect = SystemError() + customer_logic_mock.replace_users.side_effect = SystemError() + customer_logic_mock.replace_default_users.side_effect = SystemError() + customer_logic_mock.delete_users.side_effect = SystemError() + customer_logic_mock.delete_default_users.side_effect = SystemError() + + else: + customer_logic_mock.add_users.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.add_default_users.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.replace_users.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.replace_default_users.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.delete_users.side_effect = ErrorStatus(status_code=404) + customer_logic_mock.delete_default_users.side_effect = ErrorStatus(status_code=404) + + return customer_logic_mock + + +class ResponseMock: + def __init__(self, status_code=200): + self.status_code = status_code + + +USER_JSON = [ + { + "id": "userId1", + "role": [ + "admin", + "other" + ] + }, + { + "id": "userId2", + "role": [ + "storage" + ] + } +] diff --git a/orm/services/customer_manager/cms_rest/tests/simple_hook_mock.py b/orm/services/customer_manager/cms_rest/tests/simple_hook_mock.py new file mode 100644 index 00000000..73b98ed1 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/simple_hook_mock.py @@ -0,0 +1,6 @@ +from pecan.hooks import PecanHook + + +class SimpleHookMock(PecanHook): + def before(self, state): + setattr(state.request, 'transaction_id', 'some_id') diff --git a/orm/services/customer_manager/cms_rest/tests/test_authentication.py b/orm/services/customer_manager/cms_rest/tests/test_authentication.py new file mode 100644 index 00000000..418e2e3f --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/test_authentication.py @@ -0,0 +1,53 @@ +import mock +from cms_rest.tests import FunctionalTest +from pecan import conf + +from cms_rest.utils import authentication + + +class TestUtil(FunctionalTest): + + def setUp(self): + FunctionalTest.setUp(self) + self.mock_response = mock.Mock() + + @mock.patch('keystone_utils.tokens.TokenConf') + def test_get_token_conf(self, mock_TokenConf): + mock_TokenConf.return_value = 123 + token_conf = authentication._get_token_conf(conf) + self.assertEqual(token_conf, 123) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_token_valid(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', True) + mock_get_token_conf.return_value = 123 + mock_is_token_valid.return_value = True + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, True) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_token_invalid(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', True) + mock_get_token_conf.return_value = 123 + mock_is_token_valid.return_value = False + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, False) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_disabled(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', False) + mock_get_token_conf.return_value = 123 + mock_is_token_valid.return_value = False + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, True) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_is_token_valid_breaks(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', True) + mock_is_token_valid.side_effect = Exception('boom') + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, False) diff --git a/orm/services/customer_manager/cms_rest/tests/test_configuration.py b/orm/services/customer_manager/cms_rest/tests/test_configuration.py new file mode 100755 index 00000000..d9528ee8 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/test_configuration.py @@ -0,0 +1,14 @@ +"""Get configuration module unittests.""" +from cms_rest.tests import FunctionalTest +from mock import patch + + +class TestGetConfiguration(FunctionalTest): + """Main get configuration test case.""" + + @patch('orm_common.utils.utils.report_config') + def test_get_configuration_success(self, mock_report): + """Test get_configuration returns the expected value on success.""" + mock_report.return_value = '12345' + response = self.app.get('/v1/orm/configuration') + self.assertEqual(response.json, '12345') diff --git a/orm/services/customer_manager/cms_rest/tests/test_models.py b/orm/services/customer_manager/cms_rest/tests/test_models.py new file mode 100755 index 00000000..91009893 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/test_models.py @@ -0,0 +1,49 @@ +import mock +from cms_rest.tests import FunctionalTest + +from cms_rest.model import Models as models + +GROUP_REGIONS = [ + "DPK", + "SNA1", + "SNA2" +] + + +class TestModels(FunctionalTest): + + def setUp(self): + FunctionalTest.setUp(self) + models.get_regions_of_group = mock.MagicMock(return_value=GROUP_REGIONS) + models.set_utils_conf = mock.MagicMock() + + def test_handle_group_success(self): + cust = get_cust_model() + cust.handle_region_group() + + self.assertEqual(len(cust.regions), 3) + + def test_handle_group_not_found(self): + models.get_regions_of_group = mock.MagicMock(return_value=None) + cust = get_cust_model() + + self.assertRaises(models.ErrorStatus, cust.handle_region_group,) + + +def get_cust_model(): + """ + this function create a customer model object for testing + :return: new customer object + """ + + cust = models.Customer(enabled=False, + name='a', + metadata={'a': 'b'}, + regions=[models.Region(name='r1', + type='group', + quotas=[models.Quota()], + users=[models.User(id='a', role=['admin'])])], + users=[models.User(id='b', role=['admin'])], + defaultQuotas=[models.Quota()]) + + return cust diff --git a/orm/services/customer_manager/cms_rest/tests/test_rds_proxy.py b/orm/services/customer_manager/cms_rest/tests/test_rds_proxy.py new file mode 100755 index 00000000..b612ad07 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/test_rds_proxy.py @@ -0,0 +1,55 @@ +import mock +from mock import MagicMock +from cms_rest.tests import FunctionalTest +from cms_rest.logger import get_logger +from testfixtures import log_capture, compare, Comparison as C +import logging +from cms_rest import rds_proxy +from cms_rest.data.sql_alchemy import models +from cms_rest.logic.error_base import ErrorStatus + + +class Response: + def __init__(self, status_code, content): + self.status_code = status_code + self.content = content + + def json(self): + return self.content + + +class TestUtil(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + self.rp = rds_proxy.RdsProxy() + + @mock.patch.object(rds_proxy, 'request') + @mock.patch('requests.post') + @log_capture('cms_rest.rds_proxy') + def test_send_good(self, mock_post, mock_request, l): + resp = Response(200, 'my content') + mock_post.return_value = resp + send_res = self.rp.send_customer(models.Customer(), "1234", "POST") + self.assertRegexpMatches(l.records[-3].getMessage(), 'Wrapper JSON before sending action') + self.assertRegexpMatches(l.records[-1].getMessage(), 'Response Content from rds server') + self.assertEqual(send_res, 'my content') + + @mock.patch.object(rds_proxy, 'request') + @mock.patch('requests.post') + @log_capture('cms_rest.rds_proxy') + def test_bad_status(self, mock_post, mock_request, l): + resp = Response(400, 'my content') + mock_post.return_value = resp + self.assertRaises(ErrorStatus, self.rp.send_customer, models.Customer(), "1234", "POST") + self.assertRegexpMatches(l.records[-3].getMessage(), 'Wrapper JSON before sending action') + self.assertRegexpMatches(l.records[-1].getMessage(), 'Response Content from rds server') + + @mock.patch.object(rds_proxy, 'request') + @mock.patch('requests.post') + @log_capture('cms_rest.rds_proxy') + def test_no_content(self, mock_post, mock_request, l): + resp = Response(200, None) + mock_post.return_value = resp + self.assertRaises(ErrorStatus, self.rp.send_customer, models.Customer(), "1234", "POST") + for r in l.records: + self.assertNotRegexpMatches(r.getMessage(), 'Response Content from rds server') diff --git a/orm/services/customer_manager/cms_rest/tests/test_utils.py b/orm/services/customer_manager/cms_rest/tests/test_utils.py new file mode 100755 index 00000000..a56e0953 --- /dev/null +++ b/orm/services/customer_manager/cms_rest/tests/test_utils.py @@ -0,0 +1,14 @@ +import json +from wsme.exc import ClientSideError + + +def get_error(transaction_id, status_code, error_details=None, + message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) diff --git a/orm/services/customer_manager/cms_rest/utils/__init__.py b/orm/services/customer_manager/cms_rest/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/customer_manager/cms_rest/utils/authentication.py b/orm/services/customer_manager/cms_rest/utils/authentication.py new file mode 100755 index 00000000..46b1b27a --- /dev/null +++ b/orm/services/customer_manager/cms_rest/utils/authentication.py @@ -0,0 +1,58 @@ +import logging +from keystone_utils import tokens +from orm_common.policy import policy +from pecan import conf + +from orm_common.utils import api_error_utils as err_utils +logger = logging.getLogger(__name__) + + +def authorize(request, action): + if not _is_authorization_enabled(conf): + return + + auth_region = request.headers.get('X-Auth-Region') + if not auth_region: + raise err_utils.get_error('N/A', message='X-Auth-Region is missing', + status_code=401) + policy.authorize(action, request, conf) + + +def _is_authorization_enabled(app_conf): + return app_conf.authentication.enabled + + +def _get_token_conf(app_conf): + mech_id = app_conf.authentication.mech_id + mech_password = app_conf.authentication.mech_pass + rms_url = app_conf.authentication.rms_url + tenant_name = app_conf.authentication.tenant_name + keystone_version = app_conf.authentication.keystone_version + conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name, + keystone_version) + return conf + + +def check_permissions(app_conf, token_to_validate, lcp_id): + logger.debug("Check permissions...start") + token_role = app_conf.authentication.token_role + try: + if _is_authorization_enabled(app_conf): + if token_to_validate is not None and lcp_id is not None and str(token_to_validate).strip() != '' and str(lcp_id).strip() != '': + token_conf = _get_token_conf(app_conf) + logger.debug("Authorization: validating token=[{}] on lcp_id=[{}]".format(token_to_validate, lcp_id)) + is_permitted = tokens.is_token_valid(token_to_validate, lcp_id, token_conf, token_role, app_conf.authentication.role_location) + logger.debug("Authorization: The token=[{}] on lcp_id=[{}] is [{}]" + .format(token_to_validate, lcp_id, "valid" if is_permitted else "invalid")) + else: + raise Exception("Token=[{}] and/or Region=[{}] are empty/none.".format(token_to_validate, lcp_id)) + else: + logger.debug("The authentication service is disabled. No authentication is needed.") + is_permitted = True + except Exception as e: + msg = "Fail to validate request. due to {}.".format(e.message) + logger.error(msg) + logger.exception(e) + is_permitted = False + logger.debug("Check permissions...end") + return is_permitted diff --git a/orm/services/customer_manager/config.py b/orm/services/customer_manager/config.py new file mode 100755 index 00000000..9fa85c38 --- /dev/null +++ b/orm/services/customer_manager/config.py @@ -0,0 +1,125 @@ +import os +from orm_common.hooks.transaction_id_hook import TransactionIdHook +from orm_common.hooks.security_headers_hook import SecurityHeadersHook +from orm_common.hooks.api_error_hook import APIErrorHook + +global TransactionIdHook +global APIErrorHook +global SecurityHeadersHook + +# Server Specific Configurations +server = { + 'port': '7080', + 'host': '0.0.0.0', + 'name': 'cms', + 'host_ip': '0.0.0.0' +} + +# Pecan Application Configurations + +app = { + 'root': 'cms_rest.controllers.root.RootController', + 'modules': ['cms_rest'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/cms_rest/templates', + 'debug': True, + 'hooks': lambda: [TransactionIdHook(), APIErrorHook(), SecurityHeadersHook()] +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'cms_rest': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'orm_common': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'keystone_utils': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'orm_common': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'keystone_utils': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'audit_client': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + }, + 'Logfile': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'maxBytes': 50000000, + 'backupCount': 10, + 'filename': '/opt/app/orm/cms_rest/cms_rest.log', + 'formatter': 'simple' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +quotas_default_values = { + 'compute': { + 'vcpus': '20', + 'metadata_items': '128', + 'injected_file_content_bytes': '10240' + }, + 'network': { + 'security_groups': '10', + 'security_group_rules': '20' + } +} + +database = { + 'connection_string': 'mysql://root:stack@localhost:3306/orm_cms_db' +} + +api = { + 'uuid_server': { + 'base': 'http://127.0.0.1:8090/', + 'uuids': 'v1/uuids' + }, + 'rds_server': { + 'base': 'http://127.0.0.1:8777/', + 'resources': 'v1/rds/resources', + 'status': 'v1/rds/status/resource/' + }, + 'rms_server': { + 'base': 'http://127.0.0.1:8080/', + 'regions': 'v2/orm/regions', + 'groups': 'v2/orm/groups', + 'cache_seconds': 30 + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } +} + +verify = False + +authentication = { + "enabled": True, + "mech_id": "admin", + "mech_pass": "stack", + "rms_url": "http://127.0.0.1:8080", + "tenant_name": "admin", + "token_role": "admin", + # The Keystone collection under which the role was granted. + # The key can be either "tenant" (for Keystone v2.0) or "domain" + # (for Keystone v3) and the value is the tenant/domain name. + "role_location": {"tenant": "admin"}, + # The Keystone version currently in use. Can be either "2.0" or "3". + "keystone_version": "2.0", + "policy_file": "/opt/app/orm/cms_rest/cms_rest/etc/policy.json" +} diff --git a/orm/services/customer_manager/gulpfile.js b/orm/services/customer_manager/gulpfile.js new file mode 100755 index 00000000..0621b269 --- /dev/null +++ b/orm/services/customer_manager/gulpfile.js @@ -0,0 +1,21 @@ +var gulp = require('gulp'); +var nodemon = require('gulp-nodemon'); +var shell = require('gulp-shell'); +var minimist = require('minimist'); +var env = require('gulp-env'); + +var defaultOptions = { + string: 'env', + default: { env: 'dev' } +}; +var options = minimist(process.argv.slice(2), defaultOptions); + +gulp.task('default', ['run-mock', 'run-pecan']); + +gulp.task('run-pecan', shell.task(['export CMS_ENV=' + options.env + '; pecan serve config.py --reload'])); +gulp.task('run-mock', function(){ + nodemon({ + script: 'rds_mock/bin/www', + env: {'NODE_ENV': 'development'} + }); +}); \ No newline at end of file diff --git a/orm/services/customer_manager/htmlcov/cms_rest___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest___init___py.html new file mode 100644 index 00000000..c68bcbc0 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for cms_rest/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_app_py.html b/orm/services/customer_manager/htmlcov/cms_rest_app_py.html new file mode 100644 index 00000000..fe68435b --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_app_py.html @@ -0,0 +1,161 @@ + + + + + + + + + + + Coverage for cms_rest/app.py: 83% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+ +
+

from pecan import make_app 

+

from cms_rest import model 

+

from orm_common.utils import utils 

+

from cms_rest.logger import get_logger 

+

from pecan.commands import CommandRunner 

+

from orm_common.policy import policy 

+

from cms_rest.utils import authentication 

+

 

+

logger = get_logger(__name__) 

+

 

+

 

+

def setup_app(config): 

+

model.init_model() 

+

token_conf = authentication._get_token_conf(config) 

+

policy.init(config.authentication.policy_file, token_conf) 

+

app_conf = dict(config.app) 

+

 

+

# setting configurations for utils to be used from now and on 

+

utils.set_utils_conf(config) 

+

 

+

app = make_app( 

+

app_conf.pop('root'), 

+

logging=getattr(config, 'logging', {}), 

+

**app_conf 

+

) 

+

logger.info('Starting CMS...') 

+

return app 

+

 

+

 

+

def main(): 

+

runner = CommandRunner() 

+

runner.run(['serve', '../config.py']) 

+

 

+

34 ↛ 35line 34 didn't jump to line 35, because the condition on line 34 was never trueif __name__ == "__main__": 

+

main() 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers___init___py.html new file mode 100644 index 00000000..aabf50d6 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_root_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_root_py.html new file mode 100644 index 00000000..67d2afa9 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_root_py.html @@ -0,0 +1,159 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/root.py: 90% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+ +
+

from pecan import expose, request, response 

+

from webob.exc import status_map 

+

from pecan.secure import SecureController 

+

from cms_rest.controllers.v1 import root as v1 

+

from cms_rest.utils import authentication 

+

from pecan import conf 

+

 

+

 

+

class RootController(object): 

+

# url/v1/ 

+

v1 = v1.V1Controller() 

+

 

+

@expose(template='json') 

+

def _default(self): 

+

""" 

+

Method to handle GET / 

+

parameters: None 

+

return: dict describing cms rest version information 

+

""" 

+

return { 

+

"versions": { 

+

"values": [ 

+

{ 

+

"status": "stable", 

+

"id": "v1", 

+

"links": [ 

+

{ 

+

"href": "http://localhost:7080/" 

+

} 

+

] 

+

} 

+

] 

+

} 

+

} 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1___init___py.html new file mode 100644 index 00000000..3f476b00 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_base_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_base_py.html new file mode 100644 index 00000000..4a02153d --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_base_py.html @@ -0,0 +1,187 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/base.py: 75% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+ +
+

import wsme 

+

from pecan import response 

+

from wsme import types as wtypes 

+

import inspect 

+

 

+

 

+

class ClientSideError(wsme.exc.ClientSideError): 

+

def __init__(self, error, status_code=400): 

+

response.translatable_error = error 

+

super(ClientSideError, self).__init__(error, status_code) 

+

 

+

 

+

class InputValueError(ClientSideError): 

+

def __init__(self, name, value, status_code=400): 

+

super(InputValueError, self).__init__("Invalid value for input {} : {}".format(name, value), status_code) 

+

 

+

 

+

class EntityNotFoundError(ClientSideError): 

+

def __init__(self, id): 

+

super(EntityNotFoundError, self).__init__("Entity not found for {}".format(id), status_code=404) 

+

 

+

 

+

class Base(wtypes.DynamicBase): 

+

pass 

+

 

+

''' 

+

@classmethod 

+

def from_model(cls, m): 

+

return cls(**(m.as_dict())) 

+

 

+

def as_dict(self, model): 

+

valid_keys = inspect.getargspec(model.__init__)[0] 

+

if 'self' in valid_keys: 

+

valid_keys.remove('self') 

+

return self.as_dict_from_keys(valid_keys) 

+

 

+

 

+

def as_dict_from_keys(self, keys): 

+

return dict((k, getattr(self, k)) 

+

for k in keys 

+

if hasattr(self, k) and 

+

getattr(self, k) != wsme.Unset) 

+

 

+

@classmethod 

+

def from_db_and_links(cls, m, links): 

+

return cls(links=links, **(m.as_dict())) 

+

 

+

''' 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm___init___py.html new file mode 100644 index 00000000..6a9316ac --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_configuration_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_configuration_py.html new file mode 100644 index 00000000..e4905b32 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_configuration_py.html @@ -0,0 +1,149 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/configuration.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+ +
+

"""Configuration rest API input module.""" 

+

 

+

import logging 

+

from orm_common.utils import utils 

+

from pecan import conf 

+

from pecan import rest 

+

from wsmeext.pecan import wsexpose 

+

 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class ConfigurationController(rest.RestController): 

+

"""Configuration controller.""" 

+

 

+

@wsexpose(str, str, status_code=200) 

+

def get(self, dump_to_log='false'): 

+

"""get method. 

+

 

+

:param dump_to_log: A boolean string that says whether the 

+

configuration should be written to log 

+

:return: A pretty string that contains the service's configuration 

+

""" 

+

logger.info("Get configuration...") 

+

 

+

dump = dump_to_log.lower() == 'true' 

+

utils.set_utils_conf(conf) 

+

result = utils.report_config(conf, dump, logger) 

+

return result 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer___init___py.html new file mode 100644 index 00000000..f562f282 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/customer/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_enabled_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_enabled_py.html new file mode 100644 index 00000000..bc48ca15 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_enabled_py.html @@ -0,0 +1,201 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/customer/enabled.py: 91% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+ +
+

from pecan import rest, request 

+

from wsmeext.pecan import wsexpose 

+

 

+

from orm_common.utils import utils 

+

from orm_common.utils import api_error_utils as err_utils 

+

from cms_rest.model.Models import Enabled, CustomerResultWrapper 

+

from cms_rest.logic.customer_logic import CustomerLogic 

+

from cms_rest.logic.error_base import ErrorStatus 

+

from cms_rest.utils import authentication 

+

 

+

from cms_rest.logger import get_logger 

+

LOG = get_logger(__name__) 

+

 

+

 

+

class EnabledController(rest.RestController): 

+

@wsexpose(CustomerResultWrapper, str, body=Enabled, rest_content_types='json') 

+

def put(self, customer_uuid, enable): 

+

authentication.authorize(request, 'customers:enable') 

+

try: 

+

LOG.info("EnabledController - (put) customer id {0} enable: {1}".format(customer_uuid, enable)) 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.enable(customer_uuid, enable, request.transaction_id) 

+

LOG.info("EnabledController - change enable (put) finished well: " + str(result)) 

+

 

+

event_details = 'Customer {} {}'.format(customer_uuid, 

+

'enabled' if enable.enabled else 'disabled') 

+

utils.audit_trail('Change enable', request.transaction_id, 

+

request.headers, customer_uuid, 

+

event_details=event_details) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("EnabledController - Failed to Change enable", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("EnabledController - change enable (put) - Failed to Change enable", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

return result 

+

 

+

@wsexpose(None, str, rest_content_types='json') 

+

def post(self, customer_id): 

+

raise err_utils.get_error(request.transaction_id, status_code=405) 

+

 

+

@wsexpose(None, str, rest_content_types='json') 

+

def get(self, customer_id): 

+

raise err_utils.get_error(request.transaction_id, status_code=405) 

+

 

+

@wsexpose(None, str, rest_content_types='json') 

+

def delete(self, customer_id): 

+

raise err_utils.get_error(request.transaction_id, status_code=405) 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_metadata_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_metadata_py.html new file mode 100644 index 00000000..a9b7212b --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_metadata_py.html @@ -0,0 +1,241 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/customer/metadata.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+ +
+

from pecan import rest, request 

+

from wsmeext.pecan import wsexpose 

+

from cms_rest.model.Models import CustomerResultWrapper 

+

from orm_common.utils import utils 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

from cms_rest.logic.error_base import ErrorStatus 

+

from cms_rest.model.Models import MetadataWrapper 

+

import cms_rest.logic.metadata_logic as logic 

+

from cms_rest.utils import authentication 

+

 

+

from cms_rest.logger import get_logger 

+

LOG = get_logger(__name__) 

+

 

+

 

+

class MetadataController(rest.RestController): 

+

@wsexpose(CustomerResultWrapper, str, body=MetadataWrapper, rest_content_types='json') 

+

def post(self, customer_uuid, metadata): 

+

authentication.authorize(request, 'customers:add_metadata') 

+

try: 

+

res = logic.add_customer_metadata(customer_uuid, metadata, request.transaction_id) 

+

 

+

event_details = 'Customer {} metadata added'.format(customer_uuid) 

+

utils.audit_trail('add customer metadata', request.transaction_id, 

+

request.headers, customer_uuid, 

+

event_details=event_details) 

+

return res 

+

except AttributeError as ex: 

+

raise err_utils.get_error(request.transaction_id, 

+

message=ex.message, status_code=409) 

+

except ValueError as ex: 

+

raise err_utils.get_error(request.transaction_id, 

+

message=ex.message, status_code=404) 

+

except ErrorStatus as ex: 

+

LOG.log_exception("MetaDataController - Failed to add metadata", ex) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=ex.status_code) 

+

except LookupError as ex: 

+

LOG.log_exception("MetaDataController - {0}".format(ex.message), ex) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=ex.message, status_code=400) 

+

except Exception as ex: 

+

LOG.log_exception("MetaDataController - Failed to add metadata", ex) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, error_details=str(ex)) 

+

 

+

@wsexpose(CustomerResultWrapper, str, body=MetadataWrapper, rest_content_types='json') 

+

def put(self, customer_uuid, metadata): 

+

authentication.authorize(request, 'customers:update_metadata') 

+

try: 

+

res = logic.update_customer_metadata(customer_uuid, metadata, request.transaction_id) 

+

 

+

event_details = 'Customer {} metadata updated'.format(customer_uuid) 

+

utils.audit_trail('update customer metadata', 

+

request.transaction_id, request.headers, 

+

customer_uuid, event_details=event_details) 

+

return res 

+

except AttributeError as ex: 

+

raise err_utils.get_error(request.transaction_id, 

+

message=ex.message, status_code=400) 

+

except ValueError as ex: 

+

raise err_utils.get_error(request.transaction_id, 

+

message=ex.message, status_code=404) 

+

except ErrorStatus as ex: 

+

LOG.log_exception("MetaDataController - Failed to add metadata", ex) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=ex.status_code) 

+

except LookupError as ex: 

+

LOG.log_exception("MetaDataController - {0}".format(ex.message), ex) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=ex.message, status_code=400) 

+

except Exception as ex: 

+

LOG.log_exception("MetaDataController - Failed to add metadata", ex) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, error_details=str(ex)) 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_regions_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_regions_py.html new file mode 100644 index 00000000..0be6fee3 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_regions_py.html @@ -0,0 +1,357 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/customer/regions.py: 94% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+ +
+

from oslo_db.exception import DBDuplicateEntry 

+

from pecan import rest, request 

+

from wsmeext.pecan import wsexpose 

+

 

+

from orm_common.utils import utils 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

from cms_rest.controllers.v1.orm.customer.users import UserController 

+

from cms_rest.model.Models import Region, RegionResultWrapper 

+

from cms_rest.logic.customer_logic import CustomerLogic 

+

from cms_rest.logic.error_base import ErrorStatus, DuplicateEntryError 

+

from cms_rest.utils import authentication 

+

 

+

from cms_rest.logger import get_logger 

+

LOG = get_logger(__name__) 

+

 

+

 

+

class RegionController(rest.RestController): 

+

 

+

users = UserController() 

+

 

+

@wsexpose([str], str, str, rest_content_types='json') 

+

def get(self, customer_id, region_id): 

+

return ["This is the regions controller ", "customer id: " + customer_id] 

+

 

+

@wsexpose(RegionResultWrapper, str, body=[Region], rest_content_types='json', status_code=200) 

+

def post(self, customer_id, regions): 

+

LOG.info("RegionController - Add Regions (post) customer id {0} regions: {1}".format(customer_id, str(regions))) 

+

authentication.authorize(request, 'customers:add_region') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.add_regions(customer_id, regions, request.transaction_id) 

+

LOG.info("RegionController - Add Regions (post) finished well: " + str(result)) 

+

 

+

event_details = 'Customer {} regions: {} added'.format( 

+

customer_id, [r.name for r in regions]) 

+

utils.audit_trail('add regions', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

41 ↛ 42line 41 didn't jump to line 42, because the exception caught by line 41 didn't happen except DBDuplicateEntry as exception: 

+

LOG.log_exception("RegionController - Add Regions (post) - region already exists", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=409, 

+

message='Region already exists', 

+

error_details=exception.message) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("CustomerController - Failed to update regions", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("RegionController - Add Regions (post) - Failed to update regions", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

return result 

+

 

+

@wsexpose(RegionResultWrapper, str, body=[Region], rest_content_types='json', status_code=200) 

+

def put(self, customer_id, regions): 

+

LOG.info("RegionController - Replace Regions (put) customer id {0} regions: {1}".format(customer_id, str(regions))) 

+

authentication.authorize(request, 'customers:update_region') 

+

self.validate_put_url() 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.replace_regions(customer_id, regions, request.transaction_id) 

+

LOG.info("RegionController - Replace Regions (put) finished well: " + str(result)) 

+

 

+

event_details = 'Customer {} regions: {} updated'.format( 

+

customer_id, [r.name for r in regions]) 

+

utils.audit_trail('Replace regions', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("CustomerController - Failed to Replace regions", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("RegionController - Replace Regions (put) - Failed to replace regions", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

return result 

+

 

+

@wsexpose(None, str, str, status_code=204) 

+

def delete(self, customer_id, region_id): 

+

LOG.info("RegionController - Delete Region (delete) customer id {0} region_id: {1}".format(customer_id, region_id)) 

+

authentication.authorize(request, 'customers:delete_region') 

+

try: 

+

customer_logic = CustomerLogic() 

+

customer_logic.delete_region(customer_id, region_id, request.transaction_id) 

+

LOG.info("RegionController - Delete Region (delete) finished well") 

+

 

+

event_details = 'Customer {} region: {} deleted'.format( 

+

customer_id, region_id) 

+

utils.audit_trail('delete region', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

107 ↛ 108line 107 didn't jump to line 108, because the exception caught by line 107 didn't happen except ValueError as exception: 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=404) 

+

except ErrorStatus as exception: 

+

LOG.log_exception("CustomerController - Failed to delete region", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("RegionController - Failed in delete Region", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

@staticmethod 

+

def validate_put_url(): 

+

url_elements = request.path.split('/') 

+

last_index = -2 if url_elements[-1] == '' else -1 

+

# If there's an element after 'regions', it is a region ID 

+

# which is currently unsupported 

+

if url_elements[last_index - 1] == 'regions': 

+

LOG.debug('Method not allowed for a specific region in Request: {}'.format(request.path)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message='Method not allowed for a specific region', 

+

status_code=405) 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_root_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_root_py.html new file mode 100644 index 00000000..ed5e7db5 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_root_py.html @@ -0,0 +1,459 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/customer/root.py: 97% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+ +
+

from pecan import rest, request, response 

+

import oslo_db 

+

from wsmeext.pecan import wsexpose 

+

 

+

from cms_rest.model.Models import Customer, CustomerResultWrapper, CustomerSummaryResponse 

+

from cms_rest.controllers.v1.orm.customer.users import DefaultUserController 

+

from cms_rest.controllers.v1.orm.customer.regions import RegionController 

+

from cms_rest.controllers.v1.orm.customer.metadata import MetadataController 

+

from cms_rest.controllers.v1.orm.customer.enabled import EnabledController 

+

from cms_rest.logic.customer_logic import CustomerLogic 

+

 

+

from cms_rest.logic.error_base import ErrorStatus 

+

from orm_common.utils import utils 

+

from orm_common.utils import api_error_utils as err_utils 

+

from cms_rest.utils import authentication 

+

 

+

from cms_rest.logger import get_logger 

+

 

+

LOG = get_logger(__name__) 

+

 

+

 

+

class CustomerController(rest.RestController): 

+

regions = RegionController() 

+

users = DefaultUserController() 

+

metadata = MetadataController() 

+

enabled = EnabledController() 

+

 

+

@wsexpose(Customer, str, rest_content_types='json') 

+

def get(self, customer_uuid): 

+

LOG.info("CustomerController - GetCustomerDetails: uuid is " + customer_uuid) 

+

authentication.authorize(request, 'customers:get_one') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.get_customer(customer_uuid) 

+

LOG.info("CustomerController - GetCustomerDetails finished well: " + str(result)) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("CustomerController - Failed to GetCustomerDetails", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("CustomerController - Failed to GetCustomerDetails", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

return result 

+

 

+

@wsexpose(CustomerResultWrapper, body=Customer, rest_content_types='json', status_code=201) 

+

def post(self, customer): 

+

LOG.info("CustomerController - CreateCustomer: " + str(customer)) 

+

authentication.authorize(request, 'customers:create') 

+

try: 

+

uuid = None 

+

if not customer.custId: 

+

uuid = utils.make_uuid() 

+

else: 

+

60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true if not CustomerController.validate_cust_id(customer.custId): 

+

utils.audit_trail('create customer', request.transaction_id, request.headers, customer.custId) 

+

raise ErrorStatus('400', None) 

+

try: 

+

uuid = utils.create_existing_uuid(customer.custId) 

+

except TypeError: 

+

raise ErrorStatus(409.1, 'Customer ID {0} already exists'.format(customer.custId)) 

+

 

+

customer_logic = CustomerLogic() 

+

try: 

+

result = customer_logic.create_customer(customer, uuid, request.transaction_id) 

+

except oslo_db.exception.DBDuplicateEntry as exception: 

+

raise ErrorStatus(409.2, 'Customer field {0} already exists'.format(exception.columns)) 

+

 

+

LOG.info("CustomerController - Customer Created: " + str(result)) 

+

event_details = 'Customer {} {} created in regions: {}, with users: {}'.format( 

+

uuid, customer.name, [r.name for r in customer.regions], 

+

[u.id for u in customer.users]) 

+

utils.audit_trail('create customer', request.transaction_id, 

+

request.headers, uuid, 

+

event_details=event_details) 

+

return result 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("CustomerController - Failed to CreateCustomer", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("CustomerController - Failed to CreateCustomer", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(CustomerResultWrapper, str, body=Customer, rest_content_types='json', status_code=200) 

+

def put(self, customer_id, customer): 

+

LOG.info("CustomerController - UpdateCustomer: " + str(customer)) 

+

authentication.authorize(request, 'customers:update') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.update_customer(customer, customer_id, request.transaction_id) 

+

response.status = 200 

+

LOG.info("CustomerController - UpdateCustomer finished well: " + str(customer)) 

+

 

+

event_details = 'Customer {} {} updated in regions: {}, with users: {}'.format( 

+

customer_id, customer.name, [r.name for r in customer.regions], 

+

[u.id for u in customer.users]) 

+

utils.audit_trail('update customer', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("Failed in UpdateCustomer", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("CustomerController - Failed to UpdateCustomer", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

return result 

+

 

+

@wsexpose(CustomerSummaryResponse, str, str, str, str, [str], 

+

rest_content_types='json') 

+

def get_all(self, region=None, user=None, starts_with=None, 

+

contains=None, metadata=None): 

+

LOG.info("CustomerController - GetCustomerlist") 

+

authentication.authorize(request, 'customers:get_all') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.get_customer_list_by_criteria(region, user, 

+

starts_with, 

+

contains, 

+

metadata) 

+

 

+

return result 

+

except ErrorStatus as exception: 

+

LOG.log_exception("CustomerController - Failed to GetCustomerlist", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("CustomerController - Failed to GetCustomerlist", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(None, str, rest_content_types='json', status_code=204) 

+

def delete(self, customer_id): 

+

authentication.authorize(request, 'customers:delete') 

+

customer_logic = CustomerLogic() 

+

 

+

try: 

+

LOG.info("CustomerController - DeleteCustomer: uuid is " + customer_id) 

+

customer_logic.delete_customer_by_uuid(customer_id) 

+

LOG.info("CustomerController - DeleteCustomer finished well") 

+

 

+

event_details = 'Customer {} deleted'.format(customer_id) 

+

utils.audit_trail('delete customer', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("CustomerController - Failed to DeleteCustomer", 

+

exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("CustomerController - Failed to DeleteCustomer", 

+

exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@staticmethod 

+

def validate_cust_id(cust_id): 

+

# regex = re.compile('[a-zA-Z]') 

+

# return regex.match(cust_id[0]) 

+

return True 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_users_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_users_py.html new file mode 100644 index 00000000..1da00572 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_customer_users_py.html @@ -0,0 +1,583 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/customer/users.py: 86% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+ +
+

from pecan import rest, request 

+

from wsmeext.pecan import wsexpose 

+

 

+

from orm_common.utils import utils 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

from cms_rest.model.Models import User, UserResultWrapper 

+

from cms_rest.logic.customer_logic import CustomerLogic 

+

from cms_rest.logic.error_base import ErrorStatus, NotFound 

+

from cms_rest.utils import authentication 

+

 

+

from cms_rest.logger import get_logger 

+

LOG = get_logger(__name__) 

+

 

+

 

+

class DefaultUserController(rest.RestController): 

+

 

+

@wsexpose([str], str, rest_content_types='json') 

+

def get(self, customer_id): 

+

return ["This is the users controller ", 

+

"customer id: " + customer_id, 

+

"user " + "default user"] 

+

 

+

@wsexpose(UserResultWrapper, str, body=[User], rest_content_types='json', status_code=200) 

+

def put(self, customer_id, users): # replace default users to customer 

+

LOG.info("DefaultUserController - Replace DefaultUsers (put) customer id {0} users: {1}".format(customer_id, str(users))) 

+

authentication.authorize(request, 'customers:update_default_user') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.replace_default_users(customer_id, users, request.transaction_id) 

+

LOG.info("DefaultUserController - Replace DefaultUsers (put) Finished well customer id {0} users: {1}".format(customer_id, str(users))) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("DefaultUserController - Failed to replace default users", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

39 ↛ 40line 39 didn't jump to line 40, because the exception caught by line 39 didn't happen except LookupError as exception: 

+

LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=404) 

+

 

+

except Exception as exception: 

+

result = UserResultWrapper(transaction_id="Users Not Added", users=[]) 

+

LOG.log_exception("DefaultUserController - Failed to replace default users", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

return result 

+

 

+

@wsexpose(UserResultWrapper, str, body=[User], rest_content_types='json', status_code=200) 

+

def post(self, customer_id, users): # add default users to customer 

+

LOG.info("DefaultUserController - Add DefaultUsers (put) customer id {0} users: {1}".format(customer_id, str(users))) 

+

authentication.authorize(request, 'customers:add_default_user') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.add_default_users(customer_id, users, request.transaction_id) 

+

LOG.info("DefaultUserController - Add DefaultUsers (post) Finished well customer id {0} users: {1}".format( 

+

customer_id, str(users))) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("DefaultUserController - Failed to add default users", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

70 ↛ 71line 70 didn't jump to line 71, because the exception caught by line 70 didn't happen except LookupError as exception: 

+

LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=404) 

+

 

+

except Exception as exception: 

+

result = UserResultWrapper(transaction_id="Users Not Added", users=[]) 

+

LOG.log_exception("DefaultUserController - Failed to add default users", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

return result 

+

 

+

@wsexpose(None, str, str, status_code=204) 

+

def delete(self, customer_id, user_id): 

+

LOG.info("DefaultUserController - Delete DefaultUsers (delete) customer id {0} user_id: {1}".format(customer_id, user_id)) 

+

authentication.authorize(request, 'customers:delete_default_user') 

+

try: 

+

customer_logic = CustomerLogic() 

+

customer_logic.delete_default_users(customer_id, user_id, request.transaction_id) 

+

LOG.info("DefaultUserController - Delete DefaultUsers (delete) Finished well customer id {0} user_id: {1}".format(customer_id, user_id)) 

+

utils.audit_trail('delete default users', request.transaction_id, request.headers, customer_id) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("DefaultUserController - Failed to delete default users", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exception.status_code) 

+

 

+

100 ↛ 101line 100 didn't jump to line 101, because the exception caught by line 100 didn't happen except LookupError as exception: 

+

LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=404) 

+

 

+

106 ↛ 107line 106 didn't jump to line 107, because the exception caught by line 106 didn't happen except NotFound as e: 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=404) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("DefaultUserController - Failed in Delete default User", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

 

+

class UserController(rest.RestController): 

+

 

+

@staticmethod 

+

def _validate(args): 

+

# validate if user didnt provide input json for users 

+

# to prevent wsme to take the input from url params 

+

124 ↛ 125line 124 didn't jump to line 125, because the condition on line 124 was never true if 'users' in args and args['users'] and not request.body: 

+

raise err_utils.get_error(request.transaction_id, 

+

message="bad request, no json body", 

+

status_code=400) 

+

 

+

@wsexpose([str], str, str, rest_content_types='json') 

+

def get(self, customer_id, region_id): 

+

return ["This is the users controller ", 

+

"customer id: " + customer_id, 

+

"region id: " + region_id] 

+

 

+

@wsexpose(UserResultWrapper, str, str, body=[User], rest_content_types='json', status_code=200) 

+

def post(self, customer_id, region_id, users): 

+

self._validate(locals()) # more validations for input 

+

title = "Add users to Region '{}' for customer: '{}', users: {}".format(region_id, customer_id, str(users)) 

+

LOG.info("UserController - {}".format(title)) 

+

authentication.authorize(request, 'customers:add_region_user') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.add_users(customer_id, region_id, users, request.transaction_id) 

+

LOG.info("UserController - {} Finished well".format(title)) 

+

 

+

event_details = 'Customer {} users: {} added in region {}'.format( 

+

customer_id, [u.id for u in users], region_id) 

+

utils.audit_trail('add users', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("DefaultUserController - Failed to {}".format(title), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

158 ↛ 159line 158 didn't jump to line 159, because the exception caught by line 158 didn't happen except LookupError as exception: 

+

LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=404) 

+

 

+

except Exception as exception: 

+

result = UserResultWrapper(transaction_id="Users Not Added", users=[]) 

+

LOG.log_exception("UserController - Failed to Add Users (post)", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

return result 

+

 

+

@wsexpose(UserResultWrapper, str, str, body=[User], rest_content_types='json', status_code=200) 

+

def put(self, customer_id, region_id, users): 

+

self._validate(locals()) # more validations for input 

+

title = "Replace users to Region '{}' for customer: '{}', users: {}".format(region_id, customer_id, str(users)) 

+

LOG.info("UserController - {}".format(title)) 

+

authentication.authorize(request, 'customers:update_region_user') 

+

try: 

+

customer_logic = CustomerLogic() 

+

result = customer_logic.replace_users(customer_id, region_id, users, request.transaction_id) 

+

LOG.info("UserController - {} Finished well".format(title)) 

+

 

+

event_details = 'Customer {} users: {} updated in region {}'.format( 

+

customer_id, [u.id for u in users], region_id) 

+

utils.audit_trail('replace users', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("DefaultUserController - Failed to {}".format(title), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=exception.status_code) 

+

 

+

196 ↛ 197line 196 didn't jump to line 197, because the exception caught by line 196 didn't happen except LookupError as exception: 

+

LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=404) 

+

 

+

except Exception as exception: 

+

result = UserResultWrapper(transaction_id="Users Not Replaced", users=[]) 

+

LOG.log_exception("UserController - Failed to Replaced Users (put)", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+

 

+

return result 

+

 

+

@wsexpose(None, str, str, str, status_code=204) 

+

def delete(self, customer_id, region_id, user_id): 

+

LOG.info("UserController - Delete User (delete) customer id {0} region_id: {1} user_id: {2}".format(customer_id, region_id, user_id)) 

+

authentication.authorize(request, 'customers:delete_region_user') 

+

try: 

+

customer_logic = CustomerLogic() 

+

customer_logic.delete_users(customer_id, region_id, user_id, request.transaction_id) 

+

LOG.info("UserController - Delete User (delete) Finished well customer id {0} region_id: {1} user_id: {2}".format(customer_id, region_id, user_id)) 

+

 

+

event_details = 'Customer {} user: {} deleted in region {}'.format( 

+

customer_id, user_id, region_id) 

+

utils.audit_trail('delete users', request.transaction_id, 

+

request.headers, customer_id, 

+

event_details=event_details) 

+

 

+

except ErrorStatus as exception: 

+

LOG.log_exception("DefaultUserController - Failed to delete users", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exception.status_code) 

+

 

+

231 ↛ 232line 231 didn't jump to line 232, because the exception caught by line 231 didn't happen except LookupError as exception: 

+

LOG.log_exception("DefaultUserController - {0}".format(exception.message), exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exception.message, 

+

status_code=404) 

+

 

+

237 ↛ 238line 237 didn't jump to line 238, because the exception caught by line 237 didn't happen except NotFound as e: 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=404) 

+

 

+

except Exception as exception: 

+

LOG.log_exception("UserController - Failed to Delete User (delete) ", exception) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=str(exception)) 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_logs_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_logs_py.html new file mode 100644 index 00000000..fe17dd7e --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_logs_py.html @@ -0,0 +1,221 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/logs.py: 35% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+ +
+

import logging 

+

 

+

from pecan import rest 

+

import wsme 

+

from wsmeext.pecan import wsexpose 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class LogChangeResultWSME(wsme.types.DynamicBase): 

+

"""log change result wsme type.""" 

+

 

+

result = wsme.wsattr(str, mandatory=True, default=None) 

+

 

+

def __init__(self, **kwargs): 

+

""""init method.""" 

+

super(LogChangeResult, self).__init__(**kwargs) 

+

 

+

 

+

class LogChangeResult(object): 

+

"""log change result type.""" 

+

 

+

def __init__(self, result): 

+

""""init method.""" 

+

self.result = result 

+

 

+

 

+

class LogsController(rest.RestController): 

+

"""Logs Audit controller.""" 

+

 

+

@wsexpose(LogChangeResultWSME, str, status_code=201, 

+

rest_content_types='json') 

+

def put(self, level): 

+

"""update log level. 

+

 

+

:param level: the log level text name 

+

:return: 

+

""" 

+

 

+

logger.info("Changing log level to [{}]".format(level)) 

+

try: 

+

log_level = logging._levelNames.get(level.upper()) 

+

if log_level is not None: 

+

self._change_log_level(log_level) 

+

result = "Log level changed to {}.".format(level) 

+

logger.info(result) 

+

else: 

+

raise Exception( 

+

"The given log level [{}] doesn't exist.".format(level)) 

+

except Exception as e: 

+

result = "Fail to change log_level. Reason: {}".format( 

+

e.message) 

+

logger.error(result) 

+

return LogChangeResult(result) 

+

 

+

@staticmethod 

+

def _change_log_level(log_level): 

+

path = __name__.split('.') 

+

if len(path) > 0: 

+

root = path[0] 

+

root_logger = logging.getLogger(root) 

+

root_logger.setLevel(log_level) 

+

else: 

+

logger.info("Fail to change log_level to [{}]. " 

+

"the given log level doesn't exist.".format(log_level)) 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_root_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_root_py.html new file mode 100644 index 00000000..ac73c82f --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_orm_root_py.html @@ -0,0 +1,111 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/orm/root.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+ +
+

from cms_rest.controllers.v1.orm.customer.root import CustomerController 

+

from cms_rest.controllers.v1.orm.logs import LogsController 

+

from cms_rest.controllers.v1.orm.configuration import ConfigurationController 

+

from pecan.rest import RestController 

+

 

+

 

+

class OrmController(RestController): 

+

configuration = ConfigurationController() 

+

customers = CustomerController() 

+

logs = LogsController() 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_root_py.html b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_root_py.html new file mode 100644 index 00000000..1e15fccd --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_controllers_v1_root_py.html @@ -0,0 +1,103 @@ + + + + + + + + + + + Coverage for cms_rest/controllers/v1/root.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+ +
+

from cms_rest.controllers.v1.orm.root import OrmController 

+

from pecan.rest import RestController 

+

 

+

 

+

class V1Controller(RestController): 

+

orm = OrmController() 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_logger___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_logger___init___py.html new file mode 100644 index 00000000..15956208 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_logger___init___py.html @@ -0,0 +1,111 @@ + + + + + + + + + + + Coverage for cms_rest/logger/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+ +
+

import logging 

+

 

+

 

+

def get_logger(name): 

+

logger = logging.getLogger(name) 

+

logger.log_exception = lambda msg, exception: logger.exception(msg + " Exception: " + str(exception)) 

+

 

+

return logger 

+

 

+

__all__ = ['get_logger'] 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_logic___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_logic___init___py.html new file mode 100644 index 00000000..2666bf1c --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_logic___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for cms_rest/logic/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_logic_customer_logic_py.html b/orm/services/customer_manager/htmlcov/cms_rest_logic_customer_logic_py.html new file mode 100644 index 00000000..ad4ebeba --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_logic_customer_logic_py.html @@ -0,0 +1,1535 @@ + + + + + + + + + + + Coverage for cms_rest/logic/customer_logic.py: 81% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+

500

+

501

+

502

+

503

+

504

+

505

+

506

+

507

+

508

+

509

+

510

+

511

+

512

+

513

+

514

+

515

+

516

+

517

+

518

+

519

+

520

+

521

+

522

+

523

+

524

+

525

+

526

+

527

+

528

+

529

+

530

+

531

+

532

+

533

+

534

+

535

+

536

+

537

+

538

+

539

+

540

+

541

+

542

+

543

+

544

+

545

+

546

+

547

+

548

+

549

+

550

+

551

+

552

+

553

+

554

+

555

+

556

+

557

+

558

+

559

+

560

+

561

+

562

+

563

+

564

+

565

+

566

+

567

+

568

+

569

+

570

+

571

+

572

+

573

+

574

+

575

+

576

+

577

+

578

+

579

+

580

+

581

+

582

+

583

+

584

+

585

+

586

+

587

+

588

+

589

+

590

+

591

+

592

+

593

+

594

+

595

+

596

+

597

+

598

+

599

+

600

+

601

+

602

+

603

+

604

+

605

+

606

+

607

+

608

+

609

+

610

+

611

+

612

+

613

+

614

+

615

+

616

+

617

+

618

+

619

+

620

+

621

+

622

+

623

+

624

+

625

+

626

+

627

+

628

+

629

+

630

+

631

+

632

+

633

+

634

+

635

+

636

+

637

+

638

+

639

+

640

+

641

+

642

+

643

+

644

+

645

+

646

+

647

+

648

+

649

+

650

+

651

+

652

+

653

+

654

+

655

+

656

+

657

+

658

+

659

+

660

+

661

+

662

+

663

+

664

+

665

+

666

+

667

+

668

+

669

+

670

+

671

+

672

+

673

+

674

+

675

+

676

+

677

+

678

+

679

+

680

+

681

+

682

+

683

+

684

+

685

+

686

+

687

+

688

+

689

+

690

+

691

+

692

+

693

+

694

+

695

+

696

+

697

+

698

+

699

+

700

+

701

+

702

+

703

+

704

+

705

+

706

+

707

+

708

+

709

+

710

+

711

+

712

+

713

+

714

+

715

+

716

+

717

+

718

+

719

+

720

+

721

+

722

+ +
+

from cms_rest.model.Models import CustomerResultWrapper 

+

from cms_rest.model.Models import RegionResultWrapper 

+

from cms_rest.model.Models import UserResultWrapper 

+

from cms_rest.model.Models import CustomerSummaryResponse, CustomerSummary 

+

from cms_rest.rds_proxy import RdsProxy 

+

from cms_rest.data.data_manager import DataManager 

+

from cms_rest.data.sql_alchemy.models import UserRole 

+

from cms_rest.logic.error_base import ErrorStatus, NotFound, DuplicateEntryError 

+

from cms_rest.data.sql_alchemy.models import CustomerMetadata 

+

from orm_common.utils.cross_api_utils import get_regions_of_group, set_utils_conf 

+

from orm_common.utils import utils 

+

 

+

from pecan import conf, request 

+

 

+

import pecan 

+

import requests 

+

 

+

from cms_rest.logger import get_logger 

+

 

+

LOG = get_logger(__name__) 

+

 

+

 

+

class CustomerLogic(object): 

+

def build_full_customer(self, customer, uuid, datamanager): 

+

sql_customer = datamanager.add_customer(customer, uuid) 

+

 

+

27 ↛ 28line 27 didn't jump to line 28, because the loop on line 27 never started for key, value in customer.metadata.iteritems(): 

+

metadata = CustomerMetadata(field_key=key, field_value=value) 

+

sql_customer.customer_metadata.append(metadata) 

+

 

+

datamanager.add_customer_region(sql_customer.id, -1) 

+

 

+

default_region_users = [] 

+

for user in customer.users: 

+

sql_user = datamanager.add_user(user) 

+

default_region_users.append(sql_user) 

+

sql_user.sql_roles = [] 

+

for role in user.role: 

+

sql_role = datamanager.add_role(role) 

+

sql_user.sql_roles.append(sql_role) 

+

 

+

default_quotas = [] 

+

for quota in customer.defaultQuotas: 

+

sql_quota = datamanager.add_quota(sql_customer.id, -1, quota) 

+

default_quotas.append(sql_quota) 

+

 

+

for sql_user in default_region_users: 

+

48 ↛ 49line 48 didn't jump to line 49, because the loop on line 48 never started for sql_role in sql_user.sql_roles: 

+

datamanager.add_user_role(sql_user.id, sql_role.id, 

+

sql_customer.id, -1) 

+

 

+

self.add_regions_to_db(customer.regions, sql_customer.id, datamanager, customer.users) 

+

return sql_customer 

+

 

+

def add_regions_to_db(self, regions, sql_customer_id, datamanager, default_users=[]): 

+

for region in regions: 

+

users_roles = self.add_user_and_roles_to_db(region.users, default_users, 

+

datamanager) 

+

 

+

# NOTE: if region has no users there is no need to update the 

+

# default users in that region 

+

# if len(region.users) == 0: 

+

# users_roles.extend(self.add_user_and_roles_to_db( 

+

# customer.users, datamanager)) 

+

# else: 

+

# users_roles.extend(self.add_user_and_roles_to_db( 

+

# region.users, datamanager)) 

+

 

+

sql_region = datamanager.add_region(region) 

+

try: 

+

datamanager.add_customer_region(sql_customer_id, sql_region.id) 

+

except Exception as ex: 

+

if hasattr(ex, 'orig') and ex.orig[0] == 1062: 

+

raise DuplicateEntryError( 

+

'Error, duplicate entry, region ' + region.name + ' already associated with customer') 

+

raise ex 

+

 

+

for user_role in users_roles: 

+

datamanager.add_user_role(user_role[0].id, user_role[1].id, 

+

sql_customer_id, sql_region.id) 

+

 

+

82 ↛ 83line 82 didn't jump to line 83, because the loop on line 82 never started for quota in region.quotas: 

+

datamanager.add_quota(sql_customer_id, sql_region.id, quota) 

+

 

+

# NOTE: if region has no quotas there is no need to update 

+

# the default quotas in that region 

+

# if len(region.quotas) == 0: 

+

# for quota in customer.defaultQuotas: 

+

# datamanager.add_quota(sql_customer_id, 

+

# sql_region.id, quota) 

+

# else: 

+

# for quota in region.quotas: 

+

# datamanager.add_quota(sql_customer_id, 

+

# sql_region.id, quota) 

+

 

+

def add_user_and_roles_to_db(self, users, default_users, datamanager): 

+

users_roles = [] 

+

for user in users: 

+

sql_user = datamanager.add_user(user) 

+

for role in user.role: 

+

sql_role = datamanager.add_role(role) 

+

users_roles.append((sql_user, sql_role)) 

+

for de_user in default_users: 

+

sql_user = datamanager.add_user(de_user) 

+

for role in de_user.role: 

+

sql_role = datamanager.add_role(role) 

+

users_roles.append((sql_user, sql_role)) 

+

 

+

return users_roles 

+

 

+

def create_customer(self, customer, uuid, transaction_id): 

+

datamanager = DataManager() 

+

try: 

+

customer.handle_region_group() 

+

sql_customer = self.build_full_customer(customer, uuid, datamanager) 

+

customer_result_wrapper = build_response(uuid, transaction_id, 'create') 

+

 

+

sql_customer = self.add_default_users_to_empty_regions(sql_customer) 

+

119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true if sql_customer.customer_customer_regions and len(sql_customer.customer_customer_regions) > 1: 

+

customer_dict = sql_customer.get_proxy_dict() 

+

for region in customer_dict["regions"]: 

+

region["action"] = "create" 

+

 

+

datamanager.flush() # i want to get any exception created by this insert 

+

RdsProxy.send_customer_dict(customer_dict, transaction_id, "POST") 

+

else: 

+

LOG.debug("Customer with no regions - wasn't send to RDS Proxy " + str(customer)) 

+

 

+

datamanager.commit() 

+

 

+

except Exception as exp: 

+

LOG.log_exception("CustomerLogic - Failed to CreateCustomer", exp) 

+

datamanager.rollback() 

+

raise 

+

 

+

return customer_result_wrapper 

+

 

+

def update_customer(self, customer, customer_uuid, transaction_id): 

+

datamanager = DataManager() 

+

try: 

+

customer.validate_model('update') 

+

customer_record = datamanager.get_record('customer') 

+

cutomer_id = customer_record.get_customer_id_from_uuid( 

+

customer_uuid) 

+

 

+

sql_customer = customer_record.read_customer_by_uuid(customer_uuid) 

+

147 ↛ 148line 147 didn't jump to line 148, because the condition on line 147 was never true if not sql_customer: 

+

raise ErrorStatus(404, 'customer {0} was not found'.format(customer_uuid)) 

+

old_customer_dict = sql_customer.get_proxy_dict() 

+

customer_record.delete_by_primary_key(cutomer_id) 

+

datamanager.flush() 

+

 

+

sql_customer = self.build_full_customer(customer, customer_uuid, 

+

datamanager) 

+

sql_customer = self.add_default_users_to_empty_regions(sql_customer) 

+

new_customer_dict = sql_customer.get_proxy_dict() 

+

new_customer_dict["regions"] = self.resolve_regions_actions(old_customer_dict["regions"], 

+

new_customer_dict["regions"]) 

+

 

+

customer_result_wrapper = build_response(customer_uuid, transaction_id, 'update') 

+

datamanager.flush() # i want to get any exception created by this insert 

+

162 ↛ 163line 162 didn't jump to line 163, because the condition on line 162 was never true if not len(new_customer_dict['regions']) == 0: 

+

RdsProxy.send_customer_dict(new_customer_dict, transaction_id, "PUT") 

+

datamanager.commit() 

+

 

+

return customer_result_wrapper 

+

 

+

except Exception as exp: 

+

LOG.log_exception("CustomerLogic - Failed to CreateCustomer", exp) 

+

datamanager.rollback() 

+

raise 

+

 

+

def resolve_regions_actions(self, old_regions_dict, new_regions_dict): 

+

174 ↛ 175line 174 didn't jump to line 175, because the loop on line 174 never started for region in new_regions_dict: 

+

old_region = next((r for r in old_regions_dict if r["name"] == region["name"]), None) 

+

if old_region: 

+

region["action"] = "modify" 

+

else: 

+

region["action"] = "create" 

+

 

+

181 ↛ 182line 181 didn't jump to line 182, because the loop on line 181 never started for region in old_regions_dict: 

+

new_region = next((r for r in new_regions_dict if r["name"] == region["name"]), None) 

+

if not new_region: 

+

region["action"] = "delete" 

+

new_regions_dict.append(region) 

+

 

+

return new_regions_dict 

+

 

+

def add_users(self, customer_uuid, region_name, users, transaction_id, p_datamanager=None): 

+

datamanager = None 

+

try: 

+

if p_datamanager is None: 

+

datamanager = DataManager() 

+

datamanager.begin_transaction() 

+

else: 

+

datamanager = p_datamanager 

+

 

+

region_id = datamanager.get_region_id_by_name(region_name) 

+

customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) 

+

 

+

201 ↛ 202line 201 didn't jump to line 202, because the condition on line 201 was never true if customer_id is None: 

+

raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) 

+

 

+

204 ↛ 205line 204 didn't jump to line 205, because the condition on line 204 was never true if region_id is None: 

+

raise ErrorStatus(404, "region {} not found".format(region_name)) 

+

 

+

self.add_users_to_db(datamanager, customer_id, region_id, users, adding=True) 

+

 

+

customer_record = datamanager.get_record('customer') 

+

customer = customer_record.read_customer(customer_id) 

+

 

+

timestamp = utils.get_time_human() 

+

datamanager.flush() # i want to get any exception created by this insert 

+

RdsProxy.send_customer(customer, transaction_id, "PUT") 

+

if p_datamanager is None: 

+

datamanager.commit() 

+

 

+

base_link = '{0}{1}/'.format(conf.server.host_ip, 

+

pecan.request.path) 

+

 

+

result_users = [{'id': user.id, 'added': timestamp, 

+

'links': {'self': base_link + user.id}} for user in 

+

users] 

+

user_result_wrapper = UserResultWrapper( 

+

transaction_id=transaction_id, users=result_users) 

+

 

+

return user_result_wrapper 

+

except Exception as exception: 

+

229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true if 'Duplicate' in exception.message: 

+

raise ErrorStatus(409, exception.message) 

+

datamanager.rollback() 

+

LOG.log_exception("Failed to add_users", exception) 

+

raise exception 

+

 

+

def replace_users(self, customer_uuid, region_name, users, transaction_id): 

+

datamanager = None 

+

try: 

+

datamanager = DataManager() 

+

datamanager.begin_transaction() 

+

 

+

customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) 

+

242 ↛ 243line 242 didn't jump to line 243, because the condition on line 242 was never true if customer_id is None: 

+

raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) 

+

 

+

region_id = datamanager.get_region_id_by_name(region_name) 

+

246 ↛ 247line 246 didn't jump to line 247, because the condition on line 246 was never true if region_id is None: 

+

raise ErrorStatus(404, "region {} not found".format(region_name)) 

+

 

+

# delete older default user 

+

user_role_record = datamanager.get_record('user_role') 

+

user_role_record.delete_all_users_from_region(customer_uuid, region_name) # -1 is default region 

+

result = self.add_users(customer_uuid, region_name, users, transaction_id, datamanager) 

+

datamanager.commit() 

+

return result 

+

 

+

except Exception as exception: 

+

datamanager.rollback() 

+

LOG.log_exception("Failed to replace_default_users", exception) 

+

raise 

+

 

+

def add_users_to_db(self, datamanager, customer_id, region_id, users, adding=False): 

+

try: 

+

users_roles = [] 

+

for user in users: 

+

sql_user = datamanager.add_user(user) 

+

for role in user.role: 

+

sql_role = datamanager.add_role(role) 

+

users_roles.append((sql_user, sql_role)) 

+

for user_role in users_roles: 

+

# TODO: change add_use_role to receive sqlalchemy model (UserRole) 

+

datamanager.add_user_role(user_role[0].id, user_role[1].id, 

+

customer_id, region_id, adding) 

+

datamanager.flush() 

+

except Exception as exception: 

+

LOG.log_exception("Failed to add users", exception) 

+

raise 

+

 

+

def delete_users(self, customer_uuid, region_id, user_id, transaction_id): 

+

datamanager = DataManager() 

+

try: 

+

user_role_record = datamanager.get_record('user_role') 

+

 

+

customer = datamanager.get_cusomer_by_uuid(customer_uuid) 

+

284 ↛ 285line 284 didn't jump to line 285, because the condition on line 284 was never true if customer is None: 

+

raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) 

+

 

+

result = user_role_record.delete_user_from_region(customer_uuid, 

+

region_id, 

+

user_id) 

+

if result.rowcount == 0: 

+

raise NotFound("user {} is not found".format(user_id)) 

+

 

+

RdsProxy.send_customer(customer, transaction_id, "PUT") 

+

datamanager.commit() 

+

 

+

print "User {0} from region {1} in customer {2} deleted".format( 

+

user_id, region_id, customer_uuid) 

+

except NotFound as e: 

+

datamanager.rollback() 

+

LOG.log_exception("Failed to delete_users, user not found", 

+

e.message) 

+

raise NotFound("Failed to delete users, %s not found" % 

+

e.message) 

+

except Exception as exception: 

+

datamanager.rollback() 

+

LOG.log_exception("Failed to delete_users", exception) 

+

raise exception 

+

 

+

def add_default_users(self, customer_uuid, users, transaction_id, p_datamanager=None): 

+

datamanager = None 

+

try: 

+

if p_datamanager is None: 

+

datamanager = DataManager() 

+

datamanager.begin_transaction() 

+

else: 

+

datamanager = p_datamanager 

+

 

+

customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) 

+

 

+

320 ↛ 321line 320 didn't jump to line 321, because the condition on line 320 was never true if customer_id is None: 

+

raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) 

+

 

+

self.add_users_to_db(datamanager, customer_id, -1, users, adding=True) 

+

 

+

customer_record = datamanager.get_record('customer') 

+

customer = customer_record.read_customer(customer_id) 

+

 

+

timestamp = utils.get_time_human() 

+

datamanager.flush() # i want to get any exception created by this insert 

+

330 ↛ 331line 330 didn't jump to line 331, because the condition on line 330 was never true if len(customer.customer_customer_regions) > 1: 

+

RdsProxy.send_customer(customer, transaction_id, "PUT") 

+

 

+

if p_datamanager is None: 

+

datamanager.commit() 

+

 

+

base_link = '{0}{1}/'.format(conf.server.host_ip, 

+

pecan.request.path) 

+

 

+

result_users = [{'id': user.id, 'added': timestamp, 

+

'links': {'self': base_link + user.id}} for user in 

+

users] 

+

user_result_wrapper = UserResultWrapper( 

+

transaction_id=transaction_id, users=result_users) 

+

 

+

return user_result_wrapper 

+

 

+

except Exception as exception: 

+

datamanager.rollback() 

+

349 ↛ 350line 349 didn't jump to line 350, because the condition on line 349 was never true if 'Duplicate' in exception.message: 

+

raise ErrorStatus(409, exception.message) 

+

LOG.log_exception("Failed to add_default_users", exception) 

+

raise 

+

 

+

def replace_default_users(self, customer_uuid, users, transaction_id): 

+

datamanager = None 

+

try: 

+

datamanager = DataManager() 

+

datamanager.begin_transaction() 

+

 

+

customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) 

+

361 ↛ 362line 361 didn't jump to line 362, because the condition on line 361 was never true if customer_id is None: 

+

raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) 

+

 

+

# delete older default user 

+

user_role_record = datamanager.get_record('user_role') 

+

user_role_record.delete_all_users_from_region(customer_uuid, -1) # -1 is default region 

+

result = self.add_default_users(customer_uuid, users, transaction_id, datamanager) 

+

datamanager.commit() 

+

return result 

+

 

+

except Exception as exception: 

+

datamanager.rollback() 

+

LOG.log_exception("Failed to replace_default_users", exception) 

+

raise 

+

 

+

def delete_default_users(self, customer_uuid, user_id, transaction_id): 

+

datamanager = DataManager() 

+

try: 

+

customer = datamanager.get_cusomer_by_uuid(customer_uuid) 

+

380 ↛ 381line 380 didn't jump to line 381, because the condition on line 380 was never true if customer is None: 

+

raise ErrorStatus(404, "customer {} does not exist".format(customer_uuid)) 

+

 

+

user_role_record = datamanager.get_record('user_role') 

+

result = user_role_record.delete_user_from_region(customer_uuid, 

+

'DEFAULT', 

+

user_id) 

+

 

+

if result.rowcount == 0: 

+

raise NotFound("user {} is not found".format(user_id)) 

+

 

+

datamanager.commit() 

+

 

+

print "User {0} from region {1} in customer {2} deleted".format( 

+

user_id, 'DEFAULT', customer_uuid) 

+

 

+

except NotFound as e: 

+

datamanager.rollback() 

+

LOG.log_exception("Failed to delete_users, user not found", 

+

e.message) 

+

raise NotFound("Failed to delete users, %s not found" % 

+

e.message) 

+

 

+

except Exception as exp: 

+

datamanager.rollback() 

+

raise exp 

+

 

+

def add_regions(self, customer_uuid, regions, transaction_id): 

+

datamanager = DataManager() 

+

customer_record = datamanager.get_record('customer') 

+

try: 

+

# TODO DataBase action 

+

customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) 

+

413 ↛ 414line 413 didn't jump to line 414, because the condition on line 413 was never true if customer_id is None: 

+

raise ErrorStatus(404, 

+

"customer with id {} does not exist".format( 

+

customer_uuid)) 

+

self.add_regions_to_db(regions, customer_id, datamanager) 

+

 

+

sql_customer = customer_record.read_customer_by_uuid(customer_uuid) 

+

 

+

sql_customer = self.add_default_users_to_empty_regions(sql_customer) 

+

new_customer_dict = sql_customer.get_proxy_dict() 

+

 

+

424 ↛ 425line 424 didn't jump to line 425, because the loop on line 424 never started for region in new_customer_dict["regions"]: 

+

new_region = next((r for r in regions if r.name == region["name"]), None) 

+

if new_region: 

+

region["action"] = "create" 

+

else: 

+

region["action"] = "modify" 

+

 

+

timestamp = utils.get_time_human() 

+

datamanager.flush() # i want to get any exception created by this insert 

+

RdsProxy.send_customer_dict(new_customer_dict, transaction_id, "POST") 

+

datamanager.commit() 

+

 

+

base_link = '{0}{1}/'.format(conf.server.host_ip, 

+

pecan.request.path) 

+

 

+

result_regions = [{'id': region.name, 'added': timestamp, 

+

'links': {'self': base_link + region.name}} for 

+

region in regions] 

+

region_result_wrapper = RegionResultWrapper( 

+

transaction_id=transaction_id, regions=result_regions) 

+

 

+

return region_result_wrapper 

+

except Exception as exp: 

+

datamanager.rollback() 

+

raise 

+

 

+

def replace_regions(self, customer_uuid, regions, transaction_id): 

+

datamanager = DataManager() 

+

customer_record = datamanager.get_record('customer') 

+

customer_region = datamanager.get_record('customer_region') 

+

try: 

+

customer_id = datamanager.get_customer_id_by_uuid(customer_uuid) 

+

456 ↛ 457line 456 didn't jump to line 457, because the condition on line 456 was never true if customer_id is None: 

+

raise ErrorStatus(404, 

+

"customer with id {} does not exist".format( 

+

customer_uuid)) 

+

 

+

old_sql_customer = customer_record.read_customer_by_uuid(customer_uuid) 

+

462 ↛ 463line 462 didn't jump to line 463, because the condition on line 462 was never true if old_sql_customer is None: 

+

raise ErrorStatus(404, 

+

"customer with id {} does not exist".format( 

+

customer_id)) 

+

old_customer_dict = old_sql_customer.get_proxy_dict() 

+

datamanager.session.expire(old_sql_customer) 

+

 

+

customer_region.delete_all_regions_for_customer(customer_id) 

+

 

+

self.add_regions_to_db(regions, customer_id, datamanager) 

+

timestamp = utils.get_time_human() 

+

 

+

new_sql_customer = datamanager.get_cusomer_by_id(customer_id) 

+

 

+

new_sql_customer = self.add_default_users_to_empty_regions(new_sql_customer) 

+

new_customer_dict = new_sql_customer.get_proxy_dict() 

+

 

+

datamanager.flush() # i want to get any exception created by this insert 

+

 

+

new_customer_dict["regions"] = self.resolve_regions_actions(old_customer_dict["regions"], 

+

new_customer_dict["regions"]) 

+

 

+

RdsProxy.send_customer_dict(new_customer_dict, transaction_id, "PUT") 

+

datamanager.commit() 

+

 

+

base_link = '{0}{1}/'.format(conf.server.host_ip, 

+

pecan.request.path) 

+

 

+

result_regions = [{'id': region.name, 'added': timestamp, 

+

'links': {'self': base_link + region.name}} for 

+

region in regions] 

+

region_result_wrapper = RegionResultWrapper( 

+

transaction_id=transaction_id, regions=result_regions) 

+

 

+

return region_result_wrapper 

+

except Exception as exp: 

+

datamanager.rollback() 

+

raise exp 

+

 

+

def delete_region(self, customer_id, region_id, transaction_id): 

+

datamanager = DataManager() 

+

try: 

+

customer_region = datamanager.get_record('customer_region') 

+

 

+

sql_customer = datamanager.get_cusomer_by_uuid(customer_id) 

+

507 ↛ 508line 507 didn't jump to line 508, because the condition on line 507 was never true if sql_customer is None: 

+

raise ErrorStatus(404, 

+

"customer with id {} does not exist".format( 

+

customer_id)) 

+

customer_dict = sql_customer.get_proxy_dict() 

+

 

+

customer_region.delete_region_for_customer(customer_id, region_id) 

+

datamanager.flush() # i want to get any exception created by this insert 

+

 

+

# i want to get any exception created by this insert 

+

datamanager.flush() 

+

 

+

region = next((r.region for r in sql_customer.customer_customer_regions if r.region.name == region_id), None) 

+

520 ↛ 521line 520 didn't jump to line 521, because the condition on line 520 was never true if region: 

+

if region.type == 'group': 

+

set_utils_conf(conf) 

+

regions = get_regions_of_group(region.name) 

+

else: 

+

regions = [region_id] 

+

526 ↛ 527line 526 didn't jump to line 527, because the loop on line 526 never started for region in customer_dict['regions']: 

+

if region['name'] in regions: 

+

region['action'] = 'delete' 

+

 

+

RdsProxy.send_customer_dict(customer_dict, transaction_id, "PUT") 

+

datamanager.commit() 

+

 

+

LOG.debug("Region {0} in customer {1} deleted".format(region_id, 

+

customer_id)) 

+

except Exception as exp: 

+

datamanager.rollback() 

+

raise 

+

 

+

def get_customer(self, customer): 

+

 

+

datamanager = DataManager() 

+

 

+

sql_customer = datamanager.get_cusomer_by_uuid_or_name(customer) 

+

 

+

if not sql_customer: 

+

raise ErrorStatus(404, 'customer: {0} not found'.format(customer)) 

+

 

+

ret_customer = sql_customer.to_wsme() 

+

549 ↛ 564line 549 didn't jump to line 564, because the condition on line 549 was never false if sql_customer.get_real_customer_regions(): 

+

# if we have regions in sql_customer 

+

 

+

resp = requests.get(conf.api.rds_server.base + 

+

conf.api.rds_server.status + 

+

sql_customer.uuid, verify=conf.verify).json() 

+

 

+

for item in ret_customer.regions: 

+

for status in resp['regions']: 

+

558 ↛ 557line 558 didn't jump to line 557, because the condition on line 558 was never false if status['region'] == item.name: 

+

item.status = status['status'] 

+

560 ↛ 561line 560 didn't jump to line 561, because the condition on line 560 was never true if status['error_msg']: 

+

item.error_message = status['error_msg'] 

+

ret_customer.status = resp['status'] 

+

else: 

+

ret_customer.status = 'no regions' 

+

 

+

return ret_customer 

+

 

+

def get_customer_list_by_criteria(self, region, user, starts_with, contains, 

+

metadata): 

+

datamanager = DataManager() 

+

customer_record = datamanager.get_record('customer') 

+

sql_customers = customer_record.get_customers_by_criteria(region=region, 

+

user=user, 

+

starts_with=starts_with, 

+

contains=contains, 

+

metadata=metadata) 

+

 

+

response = CustomerSummaryResponse() 

+

for sql_customer in sql_customers: 

+

# get aggregate status for each customer 

+

customer_status = RdsProxy.get_status(sql_customer.uuid) 

+

customer = CustomerSummary.from_db_model(sql_customer) 

+

583 ↛ 585line 583 didn't jump to line 585, because the condition on line 583 was never false if customer_status.status_code == 200: 

+

customer.status = customer_status.json()['status'] 

+

response.customers.append(customer) 

+

 

+

return response 

+

 

+

def enable(self, customer_uuid, enabled, transaction_id): 

+

try: 

+

datamanager = DataManager() 

+

 

+

customer_record = datamanager.get_record('customer') 

+

sql_customer = customer_record.read_customer_by_uuid(customer_uuid) 

+

 

+

596 ↛ 597line 596 didn't jump to line 597, because the condition on line 596 was never true if not sql_customer: 

+

raise ErrorStatus(404, 'customer: {0} not found'.format(customer_uuid)) 

+

 

+

sql_customer.enabled = 1 if enabled.enabled else 0 

+

 

+

RdsProxy.send_customer(sql_customer, transaction_id, "PUT") 

+

 

+

datamanager.flush() # get any exception created by this action 

+

datamanager.commit() 

+

 

+

except Exception as exp: 

+

datamanager.rollback() 

+

raise exp 

+

 

+

def add_default_users_to_empty_regions(self, sql_customer): 

+

if len(sql_customer.customer_customer_regions) > 0: 

+

for region in sql_customer.customer_customer_regions: 

+

613 ↛ 612line 613 didn't jump to line 612, because the condition on line 613 was never false if region.region_id == -1: 

+

users = region.customer_region_user_roles 

+

 

+

for region in sql_customer.customer_customer_regions: 

+

617 ↛ 616line 617 didn't jump to line 616, because the condition on line 617 was never false if len(region.customer_region_user_roles) == 0: 

+

new_users = [] 

+

619 ↛ 620line 619 didn't jump to line 620, because the loop on line 619 never started for user in users: 

+

u = UserRole() 

+

u.customer_id = region.customer_id 

+

u.region_id = region.region_id 

+

u.user_id = user.user_id 

+

u.role_id = user.role_id 

+

new_users.append(u) 

+

region.customer_region_user_roles = new_users 

+

 

+

return sql_customer 

+

 

+

def delete_customer_by_uuid(self, customer_id): 

+

datamanager = DataManager() 

+

 

+

try: 

+

datamanager.begin_transaction() 

+

customer_record = datamanager.get_record('customer') 

+

 

+

sql_customer = customer_record.read_customer_by_uuid(customer_id) 

+

if sql_customer is None: 

+

# The customer does not exist, so the delete operation is 

+

# considered successful 

+

return 

+

 

+

real_regions = sql_customer.get_real_customer_regions() 

+

if len(real_regions) > 0: 

+

# Do not delete a customer that still has some regions 

+

raise ErrorStatus(405, 

+

"Cannot delete a customer that has regions. " 

+

"Please delete the regions first and then " 

+

"delete the customer.") 

+

else: 

+

expected_status = 'Success' 

+

invalid_status = 'N/A' 

+

# Get status from RDS 

+

resp = RdsProxy.get_status(sql_customer.uuid) 

+

if resp.status_code == 200: 

+

status_resp = resp.json() 

+

if 'status' in status_resp.keys(): 

+

LOG.debug( 

+

'RDS returned status: {}'.format( 

+

status_resp['status'])) 

+

status = status_resp['status'] 

+

else: 

+

# Invalid response from RDS 

+

LOG.error('Response from RDS did not contain status') 

+

status = invalid_status 

+

elif resp.status_code == 404: 

+

# Customer not found in RDS, that means it never had any regions 

+

# So it is OK to delete it 

+

LOG.debug( 

+

'Resource not found in RDS, so it is OK to delete') 

+

status = expected_status 

+

else: 

+

# Invalid status code from RDS 

+

log_message = 'Invalid response code from RDS: {}'.format( 

+

resp.status_code) 

+

log_message = log_message.replace('\n', '_').replace('\r', 

+

'_') 

+

LOG.warning(log_message) 

+

status = invalid_status 

+

 

+

if status == invalid_status: 

+

raise ErrorStatus(500, "Could not get customer status") 

+

elif status != expected_status: 

+

raise ErrorStatus(409, 

+

"The customer has not been deleted " 

+

"successfully from all of its regions " 

+

"(either the deletion failed on one of the " 

+

"regions or it is still in progress)") 

+

 

+

# OK to delete 

+

customer_record.delete_customer_by_uuid(customer_id) 

+

 

+

datamanager.flush() # i want to get any exception created by this delete 

+

datamanager.commit() 

+

except Exception as exp: 

+

LOG.log_exception("CustomerLogic - Failed to delete customer", exp) 

+

datamanager.rollback() 

+

raise 

+

 

+

 

+

def build_response(customer_uuid, transaction_id, context): 

+

""" 

+

this function generate th customer action response JSON 

+

:param customer_uuid: 

+

:param transaction_id: 

+

:param context: create or update 

+

:return: 

+

""" 

+

# The link should point to the customer itself (/v1/orm/customers/{id}) 

+

link_elements = request.url.split('/') 

+

base_link = '/'.join(link_elements) 

+

if context == 'create': 

+

base_link += customer_uuid 

+

 

+

timestamp = utils.get_time_human() 

+

customer_result_wrapper = CustomerResultWrapper( 

+

transaction_id=transaction_id, 

+

id=customer_uuid, 

+

updated=None, 

+

created=timestamp, 

+

links={'self': base_link}) 

+

return customer_result_wrapper 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_logic_error_base_py.html b/orm/services/customer_manager/htmlcov/cms_rest_logic_error_base_py.html new file mode 100644 index 00000000..e89498cc --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_logic_error_base_py.html @@ -0,0 +1,131 @@ + + + + + + + + + + + Coverage for cms_rest/logic/error_base.py: 86% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+ +
+

class Error(Exception): 

+

pass 

+

 

+

 

+

class ErrorStatus(Error): 

+

def __init__(self, status_code, message=None): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class NotFound(Error): 

+

def __init__(self, message=None, status_code=404): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class DuplicateEntryError(Error): 

+

def __init__(self, message=None, status_code=409): 

+

self.status_code = status_code 

+

self.message = message 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_logic_metadata_logic_py.html b/orm/services/customer_manager/htmlcov/cms_rest_logic_metadata_logic_py.html new file mode 100644 index 00000000..881481a3 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_logic_metadata_logic_py.html @@ -0,0 +1,309 @@ + + + + + + + + + + + Coverage for cms_rest/logic/metadata_logic.py: 15% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+ +
+

from cms_rest.data.sql_alchemy.models import CustomerMetadata 

+

from cms_rest.data.data_manager import DataManager 

+

from cms_rest.rds_proxy import RdsProxy 

+

from cms_rest.model.Models import CustomerResultWrapper 

+

from orm_common.utils import utils 

+

from pecan import request 

+

from pecan import conf 

+

import json 

+

from cms_rest.logger import get_logger 

+

 

+

logger = get_logger(__name__) 

+

 

+

 

+

def add_customer_metadata(customer_uuid, metadata_wrapper, transaction_id): 

+

sql_metadata_collection = map_metadata(customer_uuid, metadata_wrapper) 

+

 

+

datamanager = DataManager() 

+

 

+

try: 

+

customer_record = datamanager.get_record('customer') 

+

sql_customer = customer_record.read_customer_by_uuid(customer_uuid) 

+

if not sql_customer: 

+

logger.error('customer not found, customer uuid: {0}'.format(customer_uuid)) 

+

raise ValueError('customer not found, customer uuid: {0}'.format(customer_uuid)) 

+

 

+

for metadata in sql_metadata_collection: 

+

metadata_match = [m for m in sql_customer.customer_metadata if m.field_key == metadata.field_key] 

+

if len(metadata_match) > 0: 

+

logger.error('Duplicate metadata key, key already exits: {0}'.format(metadata.field_key)) 

+

raise AttributeError('Duplicate metadata key, key already exits: {0}'.format(metadata.field_key)) 

+

 

+

for metadata in sql_metadata_collection: 

+

sql_customer.customer_metadata.append(metadata) 

+

logger.debug('updating metadata {0}'.format(json.dumps(metadata.get_proxy_dict()))) 

+

 

+

logger.debug('finished appending metadata to customer') 

+

if len(sql_customer.customer_customer_regions) > 1: 

+

RdsProxy.send_customer(sql_customer, transaction_id, "PUT") 

+

datamanager.commit() 

+

 

+

customer_result_wrapper = build_response(customer_uuid, transaction_id) 

+

 

+

return customer_result_wrapper 

+

 

+

except Exception as exp: 

+

datamanager.rollback() 

+

raise exp 

+

 

+

 

+

def update_customer_metadata(customer_uuid, metadata_wrapper, transaction_id): 

+

sql_metadata_collection = map_metadata(customer_uuid, metadata_wrapper) 

+

 

+

datamanager = DataManager() 

+

 

+

try: 

+

customer_record = datamanager.get_record('customer') 

+

sql_customer = customer_record.read_customer_by_uuid(customer_uuid) 

+

 

+

if not sql_customer: 

+

logger.error('customer not found, customer uuid: {0}'.format(customer_uuid)) 

+

raise ValueError('customer not found, customer uuid: {0}'.format(customer_uuid)) 

+

 

+

while len(sql_customer.customer_metadata) > 0: 

+

sql_customer.customer_metadata.remove(sql_customer.customer_metadata[0]) 

+

 

+

for metadata in sql_metadata_collection: 

+

sql_customer.customer_metadata.append(metadata) 

+

logger.debug('updating metadata {0}'.format(json.dumps(metadata.get_proxy_dict()))) 

+

 

+

if len(sql_customer.customer_customer_regions) > 1: 

+

RdsProxy.send_customer(sql_customer, transaction_id, "PUT") 

+

datamanager.commit() 

+

 

+

customer_result_wrapper = build_response(customer_uuid, transaction_id) 

+

 

+

return customer_result_wrapper 

+

 

+

except Exception as exp: 

+

datamanager.rollback() 

+

raise exp 

+

 

+

 

+

def map_metadata(customer_id, metadata_wrapper): 

+

sql_metadata_collection = [] 

+

for key, value in metadata_wrapper.metadata.iteritems(): 

+

sql_metadata = CustomerMetadata() 

+

sql_metadata.customer_id = customer_id 

+

sql_metadata.field_key = key 

+

sql_metadata.field_value = value 

+

 

+

sql_metadata_collection.append(sql_metadata) 

+

return sql_metadata_collection 

+

 

+

 

+

def build_response(customer_uuid, transaction_id): 

+

# The link should point to the customer itself (/v1/orm/customers/{id}), 

+

# so the 'metadata' element should be removed. 

+

link_elements = request.url.split('/') 

+

link_elements.remove('metadata') 

+

base_link = '/'.join(link_elements) 

+

 

+

timestamp = utils.get_time_human() 

+

customer_result_wrapper = CustomerResultWrapper( 

+

transaction_id=transaction_id, 

+

id=customer_uuid, 

+

updated=None, 

+

created=timestamp, 

+

links={'self': base_link}) 

+

return customer_result_wrapper 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_model_Model_py.html b/orm/services/customer_manager/htmlcov/cms_rest_model_Model_py.html new file mode 100644 index 00000000..e3a7e21d --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_model_Model_py.html @@ -0,0 +1,167 @@ + + + + + + + + + + + Coverage for cms_rest/model/Model.py: 86% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+ +
+

import inspect 

+

from wsme import types as wtypes 

+

from wsme.rest.json import tojson 

+

 

+

 

+

class Model(wtypes.DynamicBase): 

+

"""Base class for CMS models. 

+

""" 

+

 

+

def tojson(self): 

+

return tojson(type(self), self) 

+

 

+

""" 

+

def __init__(self, **kwds): 

+

self.fields = list(kwds) 

+

for k, v in kwds.iteritems(): 

+

setattr(self, k, v) 

+

 

+

def as_dict(self): 

+

d = {} 

+

for f in self.fields: 

+

v = getattr(self, f) 

+

if isinstance(v, Model): 

+

v = v.as_dict() 

+

elif isinstance(v, list) and v and isinstance(v[0], Model): 

+

v = [sub.as_dict() for sub in v] 

+

d[f] = v 

+

return d 

+

 

+

def __eq__(self, other): 

+

return self.as_dict() == other.as_dict() 

+

 

+

@classmethod 

+

def get_field_names(cls): 

+

fields = inspect.getargspec(cls.__init__)[0] 

+

return set(fields) - set(["self"]) 

+

 

+

""" 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_model_Models_py.html b/orm/services/customer_manager/htmlcov/cms_rest_model_Models_py.html new file mode 100644 index 00000000..1f97e481 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_model_Models_py.html @@ -0,0 +1,979 @@ + + + + + + + + + + + Coverage for cms_rest/model/Models.py: 95% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+ +
+

import wsme 

+

from wsme import types as wtypes 

+

from cms_rest.model.Model import Model 

+

from cms_rest.logic.error_base import ErrorStatus 

+

from pecan import conf 

+

from orm_common.utils.cross_api_utils import set_utils_conf, get_regions_of_group 

+

 

+

 

+

class Enabled(Model): 

+

"""enable model the customer 

+

 

+

""" 

+

enabled = wsme.wsattr(bool, mandatory=True) 

+

 

+

def __init__(self, enabled=None): 

+

"""Create a new enables class. 

+

 

+

:param enabled: customer status 

+

""" 

+

self.enabled = enabled 

+

 

+

 

+

class Compute(Model): 

+

"""compute model the customer 

+

 

+

""" 

+

instances = wsme.wsattr(wsme.types.text, mandatory=True) 

+

injected_files = wsme.wsattr(wsme.types.text, mandatory=True, name="injected-files") 

+

key_pairs = wsme.wsattr(wsme.types.text, mandatory=True, name="key-pairs") 

+

ram = wsme.wsattr(wsme.types.text, mandatory=True) 

+

vcpus = wsme.wsattr(wsme.types.text, mandatory=False) 

+

metadata_items = wsme.wsattr(wsme.types.text, mandatory=False, name="metadata-items") 

+

injected_file_content_bytes = wsme.wsattr(wsme.types.text, mandatory=False, name="injected-file-content-bytes") 

+

floating_ips = wsme.wsattr(wsme.types.text, mandatory=False, name="floating-ips") 

+

fixed_ips = wsme.wsattr(wsme.types.text, mandatory=False, name="fixed-ips") 

+

injected_file_path_bytes = wsme.wsattr(wsme.types.text, mandatory=False, name="injected-file-path-bytes") 

+

server_groups = wsme.wsattr(wsme.types.text, mandatory=False, name="server-groups") 

+

server_group_members = wsme.wsattr(wsme.types.text, mandatory=False, name="server-group-members") 

+

 

+

def __init__(self, instances='', injected_files='', key_pairs='', ram='', 

+

vcpus=None, metadata_items=None, injected_file_content_bytes=None, 

+

floating_ips='', fixed_ips='', injected_file_path_bytes='', 

+

server_groups='', server_group_members=''): 

+

""" 

+

Create a new compute instance. 

+

:param instances: 

+

:param injected_files: 

+

:param key_pairs: 

+

:param ram: 

+

:param vcpus: 

+

:param metadata_items: 

+

:param injected_file_content_bytes: 

+

:param floating_ips: 

+

:param fixed_ips: 

+

:param injected_file_path_bytes: 

+

:param server_groups: 

+

:param server_group_members: 

+

""" 

+

self.instances = instances 

+

self.injected_files = injected_files 

+

self.key_pairs = key_pairs 

+

self.ram = ram 

+

if vcpus is None: 

+

self.vcpus = conf.quotas_default_values.compute.vcpus 

+

else: 

+

self.vcpus = vcpus 

+

if metadata_items is None: 

+

self.metadata_items = \ 

+

conf.quotas_default_values.compute.metadata_items 

+

else: 

+

self.metadata_items = metadata_items 

+

if injected_file_content_bytes is None: 

+

self.injected_file_content_bytes = \ 

+

conf.quotas_default_values.compute.injected_file_content_bytes 

+

else: 

+

self.injected_file_content_bytes = injected_file_content_bytes 

+

 

+

self.floating_ips = floating_ips 

+

self.fixed_ips = fixed_ips 

+

self.injected_file_path_bytes = injected_file_path_bytes 

+

self.server_groups = server_groups 

+

self.server_group_members = server_group_members 

+

 

+

 

+

class Storage(Model): 

+

"""storage info model for customer 

+

 

+

""" 

+

gigabytes = wsme.wsattr(wsme.types.text, mandatory=True) 

+

snapshots = wsme.wsattr(wsme.types.text, mandatory=True) 

+

volumes = wsme.wsattr(wsme.types.text, mandatory=True) 

+

 

+

def __init__(self, gigabytes='', snapshots='', volumes=''): 

+

""" 

+

create a new Storage instance. 

+

:param gigabytes: 

+

:param snapshots: 

+

:param volumes: 

+

""" 

+

self.gigabytes = gigabytes 

+

self.snapshots = snapshots 

+

self.volumes = volumes 

+

 

+

 

+

class Network(Model): 

+

"""network model the customer 

+

 

+

""" 

+

floating_ips = wsme.wsattr(wsme.types.text, mandatory=True, name="floating-ips") 

+

networks = wsme.wsattr(wsme.types.text, mandatory=True) 

+

ports = wsme.wsattr(wsme.types.text, mandatory=True) 

+

routers = wsme.wsattr(wsme.types.text, mandatory=True) 

+

subnets = wsme.wsattr(wsme.types.text, mandatory=True) 

+

security_groups = wsme.wsattr(wsme.types.text, mandatory=False, name="security-groups") 

+

security_group_rules = wsme.wsattr(wsme.types.text, mandatory=False, name="security-group-rules") 

+

health_monitor = wsme.wsattr(wsme.types.text, mandatory=False, name="health-monitor") 

+

member = wsme.wsattr(wsme.types.text, mandatory=False) 

+

nat_instance = wsme.wsattr(wsme.types.text, mandatory=False, name="nat-instance") 

+

pool = wsme.wsattr(wsme.types.text, mandatory=False) 

+

route_table = wsme.wsattr(wsme.types.text, mandatory=False, name="route-table") 

+

vip = wsme.wsattr(wsme.types.text, mandatory=False) 

+

 

+

def __init__(self, floating_ips='', networks='', ports='', routers='', 

+

subnets='', security_groups=None, security_group_rules=None, 

+

health_monitor='', member='', nat_instance='', 

+

pool='', route_table='', vip=''): 

+

 

+

""" 

+

Create a new Network instance. 

+

:param floating_ips: num of floating_ips 

+

:param networks: num of networks 

+

:param ports: num of ports 

+

:param routers: num of routers 

+

:param subnets: num of subnets 

+

:param security_groups: security groups 

+

:param security_group_rules: security group rules 

+

:param health_monitor: 

+

:param member: 

+

:param nat_instance: 

+

:param pool: 

+

:param route_table: 

+

:param vip: 

+

""" 

+

self.floating_ips = floating_ips 

+

self.networks = networks 

+

self.ports = ports 

+

self.routers = routers 

+

self.subnets = subnets 

+

if security_groups is None: 

+

self.security_groups = conf.quotas_default_values.network.security_groups 

+

else: 

+

self.security_groups = security_groups 

+

if security_group_rules is None: 

+

self.security_group_rules = conf.quotas_default_values.network.security_group_rules 

+

else: 

+

self.security_group_rules = security_group_rules 

+

 

+

self.health_monitor = health_monitor 

+

self.member = member 

+

self.nat_instance = nat_instance 

+

self.pool = pool 

+

self.route_table = route_table 

+

self.vip = vip 

+

 

+

 

+

class Quota(Model): 

+

"""network model the customer 

+

 

+

""" 

+

compute = wsme.wsattr([Compute], mandatory=False) 

+

storage = wsme.wsattr([Storage], mandatory=False) 

+

network = wsme.wsattr([Network], mandatory=False) 

+

 

+

def __init__(self, compute=None, storage=None, network=None): 

+

"""Create a new compute. 

+

 

+

:param compute: compute quota 

+

:param storage: storage quota 

+

:param network: network quota 

+

""" 

+

self.compute = compute 

+

self.storage = storage 

+

self.network = network 

+

 

+

 

+

class User(Model): 

+

"""user model the customer 

+

 

+

""" 

+

id = wsme.wsattr(wsme.types.text, mandatory=True) 

+

role = wsme.wsattr([str]) 

+

 

+

def __init__(self, id="", role=[]): 

+

"""Create a new compute. 

+

 

+

:param id: user id 

+

:param role: roles this use belong to 

+

""" 

+

self.id = id 

+

self.role = role 

+

 

+

 

+

class Region(Model): 

+

"""network model the customer 

+

 

+

""" 

+

name = wsme.wsattr(wsme.types.text, mandatory=True) 

+

type = wsme.wsattr(wsme.types.text, default="single", mandatory=False) 

+

quotas = wsme.wsattr([Quota], mandatory=False) 

+

users = wsme.wsattr([User], mandatory=False) 

+

status = wsme.wsattr(wsme.types.text, mandatory=False) 

+

error_message = wsme.wsattr(wsme.types.text, mandatory=False) 

+

 

+

def __init__(self, name="", type="single", quotas=[], users=[], status="", 

+

error_message=""): 

+

"""Create a new compute. 

+

 

+

:param name: region name 

+

:param type: region type 

+

:param quotas: quotas ( array of Quota) 

+

:param users: array of users of specific region 

+

:param status: status of creation 

+

:param error_message: error message if status is error 

+

""" 

+

 

+

self.name = name 

+

self.type = type 

+

self.quotas = quotas 

+

self.users = users 

+

self.status = status 

+

231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true if error_message: 

+

self.error_message = error_message 

+

 

+

 

+

class Customer(Model): 

+

"""customer entity with all it's related data 

+

 

+

""" 

+

description = wsme.wsattr(wsme.types.text, mandatory=True) 

+

enabled = wsme.wsattr(bool, mandatory=True) 

+

name = wsme.wsattr(wsme.types.text, mandatory=True) 

+

metadata = wsme.wsattr(wsme.types.DictType(str, str), mandatory=False) 

+

regions = wsme.wsattr([Region], mandatory=False) 

+

users = wsme.wsattr([User], mandatory=True) 

+

defaultQuotas = wsme.wsattr([Quota], mandatory=True) 

+

status = wsme.wsattr(wsme.types.text, mandatory=False) 

+

custId = wsme.wsattr(wsme.types.text, mandatory=False) 

+

uuid = wsme.wsattr(wsme.types.text, mandatory=False) 

+

 

+

def __init__(self, description="", enabled=False, name="", metadata={}, regions=[], users=[], 

+

defaultQuotas=[], status="", custId="", uuid=None): 

+

"""Create a new Customer. 

+

 

+

:param description: Server name 

+

:param enabled: I don't know 

+

:param status: status of creation 

+

""" 

+

self.description = description 

+

self.enabled = enabled 

+

self.name = name 

+

self.metadata = metadata 

+

self.regions = regions 

+

self.users = users 

+

self.defaultQuotas = defaultQuotas 

+

self.status = status 

+

self.custId = custId 

+

267 ↛ 268line 267 didn't jump to line 268, because the condition on line 267 was never true if uuid is not None: 

+

self.uuid = uuid 

+

 

+

def validate_model(self, context=None): 

+

""" 

+

this function check if the customer model meet the demands 

+

:param context: i.e. 'create 'update' 

+

:return: none 

+

""" 

+

276 ↛ exitline 276 didn't return from function 'validate_model', because the condition on line 276 was never false if context == "update": 

+

277 ↛ 278line 277 didn't jump to line 278, because the loop on line 277 never started for region in self.regions: 

+

if region.type == "group": 

+

raise ErrorStatus(400, "region type is invalid for update, \'group\' can be only in create") 

+

 

+

def handle_region_group(self): 

+

regions_to_add = [] 

+

for region in self.regions[:]: # get copy of it to be able to delete from the origin 

+

if region.type == "group": 

+

group_regions = self.get_regions_for_group(region.name) 

+

if not group_regions: 

+

raise ErrorStatus(404, 'Group {} Not found'.format(region.name)) 

+

for group_region in group_regions: 

+

regions_to_add.append(Region(name=group_region, 

+

type='single', 

+

quotas=region.quotas, 

+

users=region.users)) 

+

self.regions.remove(region) 

+

 

+

self.regions.extend(set(regions_to_add)) # remove duplicates if exist 

+

 

+

def get_regions_for_group(self, group_name): 

+

set_utils_conf(conf) 

+

regions = get_regions_of_group(group_name) 

+

return regions 

+

 

+

 

+

""" Customer Result Handler """ 

+

 

+

 

+

class CustomerResult(Model): 

+

id = wsme.wsattr(wsme.types.text, mandatory=True) 

+

updated = wsme.wsattr(wsme.types.text, mandatory=False) 

+

created = wsme.wsattr(wsme.types.text, mandatory=False) 

+

links = wsme.wsattr({str: str}, mandatory=True) 

+

 

+

def __init__(self, id, links={}, updated=None, created=None): 

+

self.id = id 

+

314 ↛ 315line 314 didn't jump to line 315, because the condition on line 314 was never true if updated: 

+

self.updated = updated 

+

316 ↛ 318line 316 didn't jump to line 318, because the condition on line 316 was never false elif created: 

+

self.created = created 

+

self.links = links 

+

 

+

 

+

class CustomerResultWrapper(Model): 

+

transaction_id = wsme.wsattr(wsme.types.text, mandatory=True) 

+

customer = wsme.wsattr(CustomerResult, mandatory=True) 

+

 

+

def __init__(self, transaction_id, id, links, updated, created): 

+

customer_result = CustomerResult(id, links, updated, created) 

+

self.transaction_id = transaction_id 

+

self.customer = customer_result 

+

 

+

 

+

""" ****************************************************************** """ 

+

 

+

""" User Result Handler """ 

+

 

+

 

+

class UserResult(Model): 

+

id = wsme.wsattr(wsme.types.text, mandatory=True) 

+

added = wsme.wsattr(wsme.types.text, mandatory=False) 

+

links = wsme.wsattr({str: str}, mandatory=True) 

+

 

+

def __init__(self, id=None, added=None, links={}): 

+

self.id = id 

+

self.added = added 

+

self.links = links 

+

 

+

 

+

class UserResultWrapper(Model): 

+

transaction_id = wsme.wsattr(wsme.types.text, mandatory=True) 

+

users = wsme.wsattr([UserResult], mandatory=True) 

+

 

+

def __init__(self, transaction_id, users): 

+

users_result = [UserResult(user['id'], user['added'], user['links']) for user in users] 

+

 

+

self.transaction_id = transaction_id 

+

self.users = users_result 

+

 

+

 

+

class MetadataWrapper(Model): 

+

metadata = wsme.wsattr(wsme.types.DictType(str, str), mandatory=True) 

+

 

+

def __init__(self, metadata={}): 

+

self.metadata = metadata 

+

 

+

 

+

""" ****************************************************************** """ 

+

 

+

""" Region Result Handler """ 

+

 

+

 

+

class RegionResult(Model): 

+

id = wsme.wsattr(wsme.types.text, mandatory=True) 

+

added = wsme.wsattr(wsme.types.text, mandatory=False) 

+

links = wsme.wsattr({str: str}, mandatory=True) 

+

 

+

def __init__(self, id, added=None, links={}): 

+

self.id = id 

+

self.added = added 

+

self.links = links 

+

 

+

 

+

class RegionResultWrapper(Model): 

+

transaction_id = wsme.wsattr(wsme.types.text, mandatory=True) 

+

regions = wsme.wsattr([RegionResult], mandatory=True) 

+

 

+

def __init__(self, transaction_id, regions): 

+

regions_result = [RegionResult(region['id'], region['added'], region['links']) for region in regions] 

+

 

+

self.transaction_id = transaction_id 

+

self.regions = regions_result 

+

 

+

 

+

""" ****************************************************************** """ 

+

 

+

""" CustomerSummary is a DataObject and contains all the fields defined in CustomerSummary structure. """ 

+

 

+

 

+

class CustomerSummary(Model): 

+

name = wsme.wsattr(wsme.types.text) 

+

id = wsme.wsattr(wsme.types.text) 

+

description = wsme.wsattr(wsme.types.text) 

+

enabled = wsme.wsattr(bool, mandatory=True) 

+

num_regions = wsme.wsattr(int, mandatory=True) 

+

status = wsme.wsattr(wtypes.text, mandatory=True) 

+

regions = wsme.wsattr([str], mandatory=True) 

+

 

+

def __init__(self, name='', id='', description='', 

+

enabled=True, status="", regions=[], num_regions=0): 

+

Model.__init__(self) 

+

 

+

self.name = name 

+

self.id = id 

+

self.description = description 

+

self.enabled = enabled 

+

self.num_regions = num_regions 

+

self.status = status 

+

self.regions = regions 

+

 

+

@staticmethod 

+

def from_db_model(sql_customer): 

+

regions = [region.region.name for region in 

+

sql_customer.customer_customer_regions if 

+

region.region_id != -1] 

+

# default region is -1 , check if -1 in customer list if yes it will return (true, flase) equal to (0, 1) 

+

num_regions = len(sql_customer.customer_customer_regions) - (-1 in [region.region_id for region in sql_customer.customer_customer_regions]) 

+

customer = CustomerSummary() 

+

customer.id = sql_customer.uuid 

+

customer.name = sql_customer.name 

+

customer.description = sql_customer.description 

+

customer.enabled = bool(sql_customer.enabled) 

+

customer.num_regions = num_regions 

+

customer.regions = regions 

+

 

+

return customer 

+

 

+

 

+

class CustomerSummaryResponse(Model): 

+

customers = wsme.wsattr([CustomerSummary], mandatory=True) 

+

 

+

def __init__(self): 

+

Model.__init__(self) 

+

self.customers = [] 

+

 

+

 

+

""" ****************************************************************** """ 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_model___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_model___init___py.html new file mode 100644 index 00000000..eeaf9eca --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_model___init___py.html @@ -0,0 +1,121 @@ + + + + + + + + + + + Coverage for cms_rest/model/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+ +
+

from pecan import conf # noqa 

+

 

+

 

+

def init_model(): 

+

""" 

+

This is a stub method which is called at application startup time. 

+

 

+

If you need to bind to a parsed database configuration, set up tables or 

+

ORM classes, or perform any database initialization, this is the 

+

recommended place to do it. 

+

 

+

For more information working with databases, and some common recipes, 

+

see http://pecan.readthedocs.org/en/latest/databases.html 

+

""" 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_rds_proxy_py.html b/orm/services/customer_manager/htmlcov/cms_rest_rds_proxy_py.html new file mode 100644 index 00000000..524ca8ec --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_rds_proxy_py.html @@ -0,0 +1,303 @@ + + + + + + + + + + + Coverage for cms_rest/rds_proxy.py: 76% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+ +
+

import pprint 

+

import requests 

+

import json 

+

from pecan import conf 

+

from pecan import request 

+

from cms_rest.logic.error_base import ErrorStatus 

+

from cms_rest.logger import get_logger 

+

 

+

LOG = get_logger(__name__) 

+

headers = {'content-type': 'application/json'} 

+

 

+

 

+

class RdsProxy(object): 

+

 

+

@staticmethod 

+

def get_status(resource_id): 

+

try: 

+

LOG.debug( 

+

"Sending to RDS Server to get status: " + conf.api.rds_server.base + conf.api.rds_server.status + resource_id) 

+

resp = requests.get( 

+

conf.api.rds_server.base + conf.api.rds_server.status + resource_id, 

+

verify=conf.verify) 

+

LOG.debug( 

+

"Sending to RDS Server to get status: " + conf.api.rds_server.base + conf.api.rds_server.status + resource_id) 

+

pp = pprint.PrettyPrinter(width=30) 

+

pretty_text = pp.pformat(resp.json()) 

+

LOG.debug("Response from RDS Server:\n" + pretty_text) 

+

return resp 

+

except Exception as exp: 

+

LOG.log_exception( 

+

"CustomerLogic - Failed to Get status for customer : " + resource_id, 

+

exp) 

+

raise 

+

 

+

@staticmethod 

+

def send_customer(customer, transaction_id, method): # method is "POST" or "PUT" 

+

return RdsProxy.send_customer_dict(customer.get_proxy_dict(), transaction_id, method) 

+

 

+

@staticmethod 

+

def send_customer_dict(customer_dict, transaction_id, method): # method is "POST" or "PUT" 

+

data = { 

+

"service_template": 

+

{ 

+

"resource": { 

+

"resource_type": "customer" 

+

}, 

+

"model": str(json.dumps(customer_dict)), 

+

"tracking": { 

+

"external_id": "", 

+

"tracking_id": transaction_id 

+

} 

+

} 

+

} 

+

 

+

data_to_display = { 

+

"service_template": 

+

{ 

+

"resource": { 

+

"resource_type": "customer" 

+

}, 

+

"model": customer_dict, 

+

"tracking": { 

+

"external_id": "", 

+

"tracking_id": transaction_id 

+

} 

+

} 

+

} 

+

 

+

pp = pprint.PrettyPrinter(width=30) 

+

pretty_text = pp.pformat(data_to_display) 

+

wrapper_json = json.dumps(data) 

+

 

+

headers['X-RANGER-Client'] = request.headers[ 

+

'X-RANGER-Client'] if 'X-RANGER-Client' in request.headers else \ 

+

'NA' 

+

headers['X-RANGER-Requester'] = request.headers[ 

+

'X-RANGER-Requester'] if 'X-RANGER-Requester' in request.headers else \ 

+

'' 

+

 

+

LOG.debug("Wrapper JSON before sending action: {0} to Rds Proxy\n{1}".format(method, pretty_text)) 

+

LOG.info("Sending to RDS Server: " + conf.api.rds_server.base + conf.api.rds_server.resources) 

+

 

+

wrapper_json = json.dumps(data) 

+

 

+

85 ↛ 91line 85 didn't jump to line 91, because the condition on line 85 was never false if method == "POST": 

+

resp = requests.post(conf.api.rds_server.base + conf.api.rds_server.resources, 

+

data=wrapper_json, 

+

headers=headers, 

+

verify=conf.verify) 

+

else: 

+

resp = requests.put(conf.api.rds_server.base + conf.api.rds_server.resources, 

+

data=wrapper_json, 

+

headers=headers, 

+

verify=conf.verify) 

+

if resp.content: 

+

LOG.debug("Response Content from rds server: {0}".format(resp.content)) 

+

 

+

content = resp.content 

+

if resp.content: 

+

content = resp.json() 

+

 

+

if resp.content and 200 <= resp.status_code < 300: 

+

content = resp.json() 

+

return content 

+

 

+

raise ErrorStatus(resp.status_code, content) 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_utils___init___py.html b/orm/services/customer_manager/htmlcov/cms_rest_utils___init___py.html new file mode 100644 index 00000000..448d1ece --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_utils___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for cms_rest/utils/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/cms_rest_utils_authentication_py.html b/orm/services/customer_manager/htmlcov/cms_rest_utils_authentication_py.html new file mode 100644 index 00000000..be78d7b5 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/cms_rest_utils_authentication_py.html @@ -0,0 +1,207 @@ + + + + + + + + + + + Coverage for cms_rest/utils/authentication.py: 76% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+ +
+

import logging 

+

from keystone_utils import tokens 

+

from orm_common.policy import policy 

+

from pecan import conf 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

def authorize(request, action): 

+

if not _is_authorization_enabled(conf): 

+

return 

+

 

+

auth_region = request.headers.get('X-Auth-Region') 

+

if not auth_region: 

+

raise err_utils.get_error('N/A', message='X-Auth-Region is missing', 

+

status_code=401) 

+

policy.authorize(action, request, conf) 

+

 

+

 

+

def _is_authorization_enabled(app_conf): 

+

return app_conf.authentication.enabled 

+

 

+

 

+

def _get_token_conf(app_conf): 

+

mech_id = app_conf.authentication.mech_id 

+

mech_password = app_conf.authentication.mech_pass 

+

rms_url = app_conf.authentication.rms_url 

+

tenant_name = app_conf.authentication.tenant_name 

+

keystone_version = app_conf.authentication.keystone_version 

+

conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name, 

+

keystone_version) 

+

return conf 

+

 

+

 

+

def check_permissions(app_conf, token_to_validate, lcp_id): 

+

logger.debug("Check permissions...start") 

+

token_role = app_conf.authentication.token_role 

+

try: 

+

if _is_authorization_enabled(app_conf): 

+

41 ↛ 48line 41 didn't jump to line 48, because the condition on line 41 was never false if token_to_validate is not None and lcp_id is not None and str(token_to_validate).strip() != '' and str(lcp_id).strip() != '': 

+

token_conf = _get_token_conf(app_conf) 

+

logger.debug("Authorization: validating token=[{}] on lcp_id=[{}]".format(token_to_validate, lcp_id)) 

+

is_permitted = tokens.is_token_valid(token_to_validate, lcp_id, token_conf, token_role, app_conf.authentication.role_location) 

+

logger.debug("Authorization: The token=[{}] on lcp_id=[{}] is [{}]" 

+

.format(token_to_validate, lcp_id, "valid" if is_permitted else "invalid")) 

+

else: 

+

raise Exception("Token=[{}] and/or Region=[{}] are empty/none.".format(token_to_validate, lcp_id)) 

+

else: 

+

logger.debug("The authentication service is disabled. No authentication is needed.") 

+

is_permitted = True 

+

except Exception as e: 

+

msg = "Fail to validate request. due to {}.".format(e.message) 

+

logger.error(msg) 

+

logger.exception(e) 

+

is_permitted = False 

+

logger.debug("Check permissions...end") 

+

return is_permitted 

+ +
+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/coverage_html.js b/orm/services/customer_manager/htmlcov/coverage_html.js new file mode 100644 index 00000000..f6f5de20 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/coverage_html.js @@ -0,0 +1,584 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// Find all the elements with shortkey_* class, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + $("*[class*='shortkey_']").each(function (i, e) { + $.each($(e).attr("class").split(" "), function (i, c) { + if (/^shortkey_/.test(c)) { + $(document).bind('keydown', c.substr(9), function () { + $(e).click(); + }); + } + }); + }); +}; + +// Create the events for the help panel. +coverage.wire_up_help_panel = function () { + $("#keyboard_icon").click(function () { + // Show the help panel, and position it so the keyboard icon in the + // panel is in the same place as the keyboard icon in the header. + $(".help_panel").show(); + var koff = $("#keyboard_icon").offset(); + var poff = $("#panel_icon").position(); + $(".help_panel").offset({ + top: koff.top-poff.top, + left: koff.left-poff.left + }); + }); + $("#panel_icon").click(function () { + $(".help_panel").hide(); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + var table = $("table.index"); + var table_rows = table.find("tbody tr"); + var table_row_names = table_rows.find("td.name a"); + var no_rows = $("#no_rows"); + + // Create a duplicate table footer that we can modify with dynamic summed values. + var table_footer = $("table.index tfoot tr"); + var table_dynamic_footer = table_footer.clone(); + table_dynamic_footer.attr('class', 'total_dynamic hidden'); + table_footer.after(table_dynamic_footer); + + // Observe filter keyevents. + $("#filter").on("keyup change", $.debounce(150, function (event) { + var filter_value = $(this).val(); + + if (filter_value === "") { + // Filter box is empty, remove all filtering. + table_rows.removeClass("hidden"); + + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + + // Hide placeholder, show table. + if (no_rows.length > 0) { + no_rows.hide(); + } + table.show(); + + } + else { + // Filter table items by value. + var hidden = 0; + var shown = 0; + + // Hide / show elements. + $.each(table_row_names, function () { + var element = $(this).parents("tr"); + + if ($(this).text().indexOf(filter_value) === -1) { + // hide + element.addClass("hidden"); + hidden++; + } + else { + // show + element.removeClass("hidden"); + shown++; + } + }); + + // Show placeholder if no rows will be displayed. + if (no_rows.length > 0) { + if (shown === 0) { + // Show placeholder, hide table. + no_rows.show(); + table.hide(); + } + else { + // Hide placeholder, show table. + no_rows.hide(); + table.show(); + } + } + + // Manage dynamic header: + if (hidden > 0) { + // Calculate new dynamic sum values based on visible rows. + for (var column = 2; column < 20; column++) { + // Calculate summed value. + var cells = table_rows.find('td:nth-child(' + column + ')'); + if (!cells.length) { + // No more columns...! + break; + } + + var sum = 0, numer = 0, denom = 0; + $.each(cells.filter(':visible'), function () { + var ratio = $(this).data("ratio"); + if (ratio) { + var splitted = ratio.split(" "); + numer += parseInt(splitted[0], 10); + denom += parseInt(splitted[1], 10); + } + else { + sum += parseInt(this.innerHTML, 10); + } + }); + + // Get footer cell element. + var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); + + // Set value into dynamic footer cell element. + if (cells[0].innerHTML.indexOf('%') > -1) { + // Percentage columns use the numerator and denominator, + // and adapt to the number of decimal places. + var match = /\.([0-9]+)/.exec(cells[0].innerHTML); + var places = 0; + if (match) { + places = match[1].length; + } + var pct = numer * 100 / denom; + footer_cell.text(pct.toFixed(places) + '%'); + } + else { + footer_cell.text(sum); + } + } + + // Hide standard footer, show dynamic footer. + table_footer.addClass("hidden"); + table_dynamic_footer.removeClass("hidden"); + } + else { + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + $("#filter").trigger("change"); +}; + +// Loaded on index.html +coverage.index_ready = function ($) { + // Look for a cookie containing previous sort settings: + var sort_list = []; + var cookie_name = "COVERAGE_INDEX_SORT"; + var i; + + // This almost makes it worth installing the jQuery cookie plugin: + if (document.cookie.indexOf(cookie_name) > -1) { + var cookies = document.cookie.split(";"); + for (i = 0; i < cookies.length; i++) { + var parts = cookies[i].split("="); + + if ($.trim(parts[0]) === cookie_name && parts[1]) { + sort_list = eval("[[" + parts[1] + "]]"); + break; + } + } + } + + // Create a new widget which exists only to save and restore + // the sort order: + $.tablesorter.addWidget({ + id: "persistentSort", + + // Format is called by the widget before displaying: + format: function (table) { + if (table.config.sortList.length === 0 && sort_list.length > 0) { + // This table hasn't been sorted before - we'll use + // our stored settings: + $(table).trigger('sorton', [sort_list]); + } + else { + // This is not the first load - something has + // already defined sorting so we'll just update + // our stored value to match: + sort_list = table.config.sortList; + } + } + }); + + // Configure our tablesorter to handle the variable number of + // columns produced depending on report options: + var headers = []; + var col_count = $("table.index > thead > tr > th").length; + + headers[0] = { sorter: 'text' }; + for (i = 1; i < col_count-1; i++) { + headers[i] = { sorter: 'digit' }; + } + headers[col_count-1] = { sorter: 'percent' }; + + // Enable the table sorter: + $("table.index").tablesorter({ + widgets: ['persistentSort'], + headers: headers + }); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + coverage.wire_up_filter(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + }); +}; + +// -- pyfile stuff -- + +coverage.pyfile_ready = function ($) { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === 'n') { + $(frag).addClass('highlight'); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + $(document) + .bind('keydown', 'j', coverage.to_next_chunk_nicely) + .bind('keydown', 'k', coverage.to_prev_chunk_nicely) + .bind('keydown', '0', coverage.to_top) + .bind('keydown', '1', coverage.to_first_chunk) + ; + + $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); + $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); + $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); + $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + + coverage.init_scroll_markers(); + + // Rebuild scroll markers after window high changing + $(window).resize(coverage.resize_scroll_markers); +}; + +coverage.toggle_lines = function (btn, cls) { + btn = $(btn); + var hide = "hide_"+cls; + if (btn.hasClass(hide)) { + $("#source ."+cls).removeClass(hide); + btn.removeClass(hide); + } + else { + $("#source ."+cls).addClass(hide); + btn.addClass(hide); + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return $("#t" + n); +}; + +// Return the nth line number div. +coverage.num_elt = function (n) { + return $("#n" + n); +}; + +// Return the container of all the code. +coverage.code_container = function () { + return $(".linenos"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.is_transparent = function (color) { + // Different browsers return different colors for "none". + return color === "transparent" || color === "rgba(0, 0, 0, 0)"; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var color, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + if (!c.is_transparent(color)) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_color = color; + while (next_color === color) { + probe++; + probe_line = c.line_elt(probe); + next_color = probe_line.css("background-color"); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + var c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var color = probe_line.css("background-color"); + while (probe > 0 && c.is_transparent(color)) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_color = color; + while (prev_color === color) { + probe--; + probe_line = c.line_elt(probe); + prev_color = probe_line.css("background-color"); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Return the line number of the line nearest pixel position pos +coverage.line_at_pos = function (pos) { + var l1 = coverage.line_elt(1), + l2 = coverage.line_elt(2), + result; + if (l1.length && l2.length) { + var l1_top = l1.offset().top, + line_height = l2.offset().top - l1_top, + nlines = (pos - l1_top) / line_height; + if (nlines < 1) { + result = 1; + } + else { + result = Math.ceil(nlines); + } + } + else { + result = 1; + } + return result; +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + var top = coverage.line_elt(coverage.sel_begin); + var next = coverage.line_elt(coverage.sel_end-1); + + return ( + (top.isOnScreen() ? 1 : 0) + + (next.isOnScreen() ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: select the top line on + // the screen. + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (probe_line.length === 0) { + return; + } + var the_color = probe_line.css("background-color"); + if (!c.is_transparent(the_color)) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var color = the_color; + while (probe > 0 && color === the_color) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + color = probe_line.css("background-color"); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + color = the_color; + while (color === the_color) { + probe++; + probe_line = c.line_elt(probe); + color = probe_line.css("background-color"); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + c.code_container().find(".highlight").removeClass("highlight"); + for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { + c.num_elt(probe).addClass("highlight"); + } + + c.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + // Need to move the page. The html,body trick makes it scroll in all + // browsers, got it from http://stackoverflow.com/questions/3042651 + var top = coverage.line_elt(coverage.sel_begin); + var top_pos = parseInt(top.offset().top, 10); + coverage.scroll_window(top_pos - 30); + } +}; + +coverage.scroll_window = function (to_pos) { + $("html,body").animate({scrollTop: to_pos}, 200); +}; + +coverage.finish_scrolling = function () { + $("html,body").stop(true, true); +}; + +coverage.init_scroll_markers = function () { + var c = coverage; + // Init some variables + c.lines_len = $('td.text p').length; + c.body_h = $('body').height(); + c.header_h = $('div#header').height(); + c.missed_lines = $('td.text p.mis, td.text p.par'); + + // Build html + c.resize_scroll_markers(); +}; + +coverage.resize_scroll_markers = function () { + var c = coverage, + min_line_height = 3, + max_line_height = 10, + visible_window_h = $(window).height(); + + $('#scroll_marker').remove(); + // Don't build markers if the window has no scroll bar. + if (c.body_h <= visible_window_h) { + return; + } + + $("body").append("
 
"); + var scroll_marker = $('#scroll_marker'), + marker_scale = scroll_marker.height() / c.body_h, + line_height = scroll_marker.height() / c.lines_len; + + // Line height must be between the extremes. + if (line_height > min_line_height) { + if (line_height > max_line_height) { + line_height = max_line_height; + } + } + else { + line_height = min_line_height; + } + + var previous_line = -99, + last_mark, + last_top; + + c.missed_lines.each(function () { + var line_top = Math.round($(this).offset().top * marker_scale), + id_name = $(this).attr('id'), + line_number = parseInt(id_name.substring(1, id_name.length)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.css({ + 'height': line_top + line_height - last_top + }); + } + else { + // Add colored line in scroll_marker block. + scroll_marker.append('
'); + last_mark = $('#m' + line_number); + last_mark.css({ + 'height': line_height, + 'top': line_top + }); + last_top = line_top; + } + + previous_line = line_number; + }); +}; diff --git a/orm/services/customer_manager/htmlcov/index.html b/orm/services/customer_manager/htmlcov/index.html new file mode 100644 index 00000000..db1c2411 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/index.html @@ -0,0 +1,440 @@ + + + + + + + + Coverage report + + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ n + s + m + x + + b + p + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedbranchespartialcoverage
Total141720303125582%
cms_rest/__init__.py00000100%
cms_rest/app.py22302183%
cms_rest/controllers/__init__.py00000100%
cms_rest/controllers/root.py10100090%
cms_rest/controllers/v1/__init__.py00000100%
cms_rest/controllers/v1/base.py16400075%
cms_rest/controllers/v1/orm/__init__.py00000100%
cms_rest/controllers/v1/orm/configuration.py140000100%
cms_rest/controllers/v1/orm/customer/__init__.py00000100%
cms_rest/controllers/v1/orm/customer/enabled.py33302091%
cms_rest/controllers/v1/orm/customer/metadata.py5200160100%
cms_rest/controllers/v1/orm/customer/regions.py753012294%
cms_rest/controllers/v1/orm/customer/root.py1113014197%
cms_rest/controllers/v1/orm/customer/users.py13815030986%
cms_rest/controllers/v1/orm/logs.py332004035%
cms_rest/controllers/v1/orm/root.py80000100%
cms_rest/controllers/v1/root.py40000100%
cms_rest/logger/__init__.py60020100%
cms_rest/logic/__init__.py00000100%
cms_rest/logic/customer_logic.py4666401623481%
cms_rest/logic/error_base.py14200086%
cms_rest/logic/metadata_logic.py7460020015%
cms_rest/model/Model.py7100086%
cms_rest/model/Models.py2415032695%
cms_rest/model/__init__.py30000100%
cms_rest/rds_proxy.py471208176%
cms_rest/utils/__init__.py00000100%
cms_rest/utils/authentication.py43708176%
+ +

+ No items found using the specified filter. +

+
+ + + + + diff --git a/orm/services/customer_manager/htmlcov/jquery.ba-throttle-debounce.min.js b/orm/services/customer_manager/htmlcov/jquery.ba-throttle-debounce.min.js new file mode 100644 index 00000000..648fe5d3 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/jquery.ba-throttle-debounce.min.js @@ -0,0 +1,9 @@ +/* + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); diff --git a/orm/services/customer_manager/htmlcov/jquery.hotkeys.js b/orm/services/customer_manager/htmlcov/jquery.hotkeys.js new file mode 100644 index 00000000..73c552fc --- /dev/null +++ b/orm/services/customer_manager/htmlcov/jquery.hotkeys.js @@ -0,0 +1,100 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 188: ",", 190: ".", + 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "); + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + event.target.type === "text" || $(event.target).prop('contenteditable') == 'true' )) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); diff --git a/orm/services/customer_manager/htmlcov/jquery.isonscreen.js b/orm/services/customer_manager/htmlcov/jquery.isonscreen.js new file mode 100644 index 00000000..0182ebd2 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/jquery.isonscreen.js @@ -0,0 +1,53 @@ +/* Copyright (c) 2010 + * @author Laurence Wheway + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * @version 1.2.0 + */ +(function($) { + jQuery.extend({ + isOnScreen: function(box, container) { + //ensure numbers come in as intgers (not strings) and remove 'px' is it's there + for(var i in box){box[i] = parseFloat(box[i])}; + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( box.left+box.width-container.left > 0 && + box.left < container.width+container.left && + box.top+box.height-container.top > 0 && + box.top < container.height+container.top + ) return true; + return false; + } + }) + + + jQuery.fn.isOnScreen = function (container) { + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( $(this).offset().left+$(this).width()-container.left > 0 && + $(this).offset().left < container.width+container.left && + $(this).offset().top+$(this).height()-container.top > 0 && + $(this).offset().top < container.height+container.top + ) return true; + return false; + } +})(jQuery); diff --git a/orm/services/customer_manager/htmlcov/jquery.min.js b/orm/services/customer_manager/htmlcov/jquery.min.js new file mode 100644 index 00000000..e2efc335 --- /dev/null +++ b/orm/services/customer_manager/htmlcov/jquery.min.js @@ -0,0 +1,9404 @@ +/*! + * jQuery JavaScript Library v1.7.2 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Fri Jul 5 14:07:58 UTC 2013 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.2", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, pass ) { + var exec, + bulk = key == null, + i = 0, + length = elems.length; + + // Sets many values + if ( key && typeof key === "object" ) { + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); + } + chainable = 1; + + // Sets one value + } else if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = pass === undefined && jQuery.isFunction( value ); + + if ( bulk ) { + // Bulk operations only iterate when executing function values + if ( exec ) { + exec = fn; + fn = function( elem, key, value ) { + return exec.call( jQuery( elem ), value ); + }; + + // Otherwise they run against the entire set + } else { + fn.call( elems, value ); + fn = null; + } + } + + if ( fn ) { + for (; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + } + + chainable = 1; + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + fired = true; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + pixelMargin: true + }; + + // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead + jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for ( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, + paddingMarginBorderVisibility, paddingMarginBorder, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + paddingMarginBorder = "padding:0;margin:0;border:"; + positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; + paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; + style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; + html = "
" + + "" + + "
"; + + container = document.createElement("div"); + container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
t
"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + div.innerHTML = ""; + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.innerHTML = ""; + div.style.width = div.style.padding = "1px"; + div.style.border = 0; + div.style.overflow = "hidden"; + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = "block"; + div.style.overflow = "visible"; + div.innerHTML = "
"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + } + + div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + if ( window.getComputedStyle ) { + div.style.marginTop = "1%"; + support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; + } + + if ( typeof container.style.zoom !== "undefined" ) { + container.style.zoom = 1; + } + + body.removeChild( container ); + marginDiv = div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, part, attr, name, l, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attr = elem.attributes; + for ( l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split( ".", 2 ); + parts[1] = parts[1] ? "." + parts[1] : ""; + part = parts[1] + "!"; + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + data = this.triggerHandler( "getData" + part, [ parts[0] ] ); + + // Try to fetch any internally stored data first + if ( data === undefined && elem ) { + data = jQuery.data( elem, key ); + data = dataAttr( elem, key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } + + parts[1] = value; + this.each(function() { + var self = jQuery( this ); + + self.triggerHandler( "setData" + part, parts ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + part, parts ); + }); + }, null, value, arguments.length > 1, null, false ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise( object ); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, isBool, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + isBool = rboolean.test( name ); + + // See #9699 for explanation of this approach (setting first, then removal) + // Do not do this for boolean attributes (see #10870) + if ( !isBool ) { + jQuery.attr( elem, name, "" ); + } + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( isBool && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true, + coords: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: selector && quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + special = jQuery.event.special[ event.type ] || {}, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers that should run if there are delegated events + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + + // Don't process events on disabled elements (#6911, #8165) + if ( cur.disabled !== true ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { // && selector != null + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} +// Expose origPOS +// "global" as in regardless of relation to brackets/parens +Expr.match.globalPOS = origPOS; + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.globalPOS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /]", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*", "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + diff --git a/orm/services/flavor_manager/fms_rest/tests/__init__.py b/orm/services/flavor_manager/fms_rest/tests/__init__.py new file mode 100644 index 00000000..78ea5274 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/__init__.py @@ -0,0 +1,22 @@ +import os +from unittest import TestCase +from pecan import set_config +from pecan.testing import load_test_app + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """ + Used for functional tests where you need to test your + literal application and its integration with the framework. + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/orm/services/flavor_manager/fms_rest/tests/config.py b/orm/services/flavor_manager/fms_rest/tests/config.py new file mode 100755 index 00000000..c72b6987 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/config.py @@ -0,0 +1,137 @@ +from fms_rest.tests.simple_hook_mock import SimpleHookMock + +global SimpleHookMock + +# Server Specific Configurations +server = { + 'port': '8082', + 'host': '0.0.0.0', + 'name': 'fms' +} + +# Pecan Application Configurations +app = { + 'root': 'fms_rest.controllers.root.RootController', + 'modules': ['fms_rest'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/fms_rest/templates', + 'debug': True, + 'errors': { + '__force_dict__': True + }, + 'hooks': lambda: [SimpleHookMock()] +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'fms_rest': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +database = { + 'host': 'localhost', + 'username': 'root', + 'password': 'root', + 'db_name': 'orm_fms_db', + +} + +# this table is for calculating default extra specs needed +extra_spec_needed_table = { + "ns": { + "aggregate_instance_extra_specs____ns": "true", + "hw____mem_page_size": "large" + }, + "nd": { + "aggregate_instance_extra_specs____nd": "true", + "hw____mem_page_size": "large" + }, + "nv": { + "aggregate_instance_extra_specs____nv": "true", + "hw____mem_page_size": "large" + }, + "gv": { + "aggregate_instance_extra_specs____gv": "true", + "aggregate_instance_extra_specs____c2": "true", + "hw____numa_nodes": "2" + }, + "ss": { + "aggregate_instance_extra_specs____ss": "true" + } +} + +# any key will be added to extra_spec_needed_table need to be added here +default_extra_spec_calculated_table = { + "aggregate_instance_extra_specs____ns": "", + "aggregate_instance_extra_specs____nd": "", + "aggregate_instance_extra_specs____nv": "", + "aggregate_instance_extra_specs____gv": "", + "aggregate_instance_extra_specs____c2": "", + "aggregate_instance_extra_specs____ss": "", + "aggregate_instance_extra_specs____c2": "", + "aggregate_instance_extra_specs____c4": "", + "aggregate_instance_extra_specs____v": "", + "hw____mem_page_size": "", + "hw____cpu_policy": "", + "hw____numa_nodes": "" +} + +database['connection_string'] = 'mysql://{0}:{1}@{2}:3306/{3}'.format(database['username'], + database['password'], + database['host'], + database['db_name']) + +application_root = 'http://localhost:{0}'.format(server['port']) + +api = { + 'uuid_server': { + 'base': 'http://127.0.0.1:8090/', + 'uuids': 'v1/uuids' + }, + 'rds_server': { + 'base': 'http://127.0.0.1:8777/', + 'resources': 'v1/rds/resources', + 'status': 'v1/rds/status/resource/' + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } + +} + +verify = False + +authentication = { + "enabled": False, + "mech_id": "admin", + "mech_pass": "stack", + "rms_url": "http://172.20.90.174:8080", + "tenant_name": "admin", + "keystone_version": "2.0", + # "policy_file": "/opt/app/orm/aic-orm-fms/fms_rest/etc/policy.json", + "policy_file": "/orm/aic-orm-fms/fms_rest/etc/policy.json" +} diff --git a/orm/services/flavor_manager/fms_rest/tests/data/__init__.py b/orm/services/flavor_manager/fms_rest/tests/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/flavor_manager/fms_rest/tests/data/test_wsme_models.py b/orm/services/flavor_manager/fms_rest/tests/data/test_wsme_models.py new file mode 100755 index 00000000..cc08e833 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/data/test_wsme_models.py @@ -0,0 +1,79 @@ +from fms_rest.tests import FunctionalTest +from fms_rest.data.sql_alchemy import db_models +import fms_rest.data.wsme.models as wsme_models + + +class TestWsmeModels(FunctionalTest): + def test_flavor_wrapper_from_db_model(self): + sql_flavor = db_models.Flavor() + sql_flavor.description = 'desc' + sql_flavor.disk = 1 + sql_flavor.ephemeral = 1 + sql_flavor.flavor_extra_specs = [db_models.FlavorExtraSpec('key1', 'val1'), + db_models.FlavorExtraSpec('key2', 'val2')] + sql_flavor.flavor_tag = [db_models.FlavorExtraSpec('key1', 'val1'), + db_models.FlavorExtraSpec('key2', 'val2')] + sql_flavor.flavor_options = [db_models.FlavorExtraSpec('key1', 'val1'), + db_models.FlavorExtraSpec('key2', 'val2')] + sql_flavor.flavor_regions = [db_models.FlavorRegion('region1'), + db_models.FlavorRegion('region2')] + sql_flavor.flavor_tenants = [db_models.FlavorTenant('tenant1'), + db_models.FlavorTenant('tenant2')] + sql_flavor.id = 'id' + sql_flavor.internal_id = 1 + sql_flavor.ram = 1 + sql_flavor.visibility = 'visibility' + sql_flavor.vcpus = 1 + sql_flavor.series = "gv" + sql_flavor.swap = 1 + sql_flavor.disk = 1 + sql_flavor.name = 'name' + + wsme_flavors = wsme_models.FlavorWrapper.from_db_model(sql_flavor) + + self.assertEqual(len(wsme_flavors.flavor.regions), 2) + self.assertEqual(len(wsme_flavors.flavor.tenants), 2) + self.assertEqual(wsme_flavors.flavor.extra_specs['key1'], 'val1') + self.assertEqual(wsme_flavors.flavor.extra_specs['key2'], 'val2') + + def test_flavor_wrapper_to_db_model(self): + flavor_wrapper = wsme_models.FlavorWrapper() + flavor_wrapper.flavor = wsme_models.Flavor() + + flavor_wrapper.flavor.description = 'desc' + flavor_wrapper.flavor.disk = '1' + flavor_wrapper.flavor.ephemeral = '1' + flavor_wrapper.flavor.extra_specs = {'key1': 'val1', 'key2': 'val2'} + flavor_wrapper.flavor.tag = {'key1': 'val1', 'key2': 'val2'} + flavor_wrapper.flavor.options = {'key1': 'val1', 'key2': 'val2'} + flavor_wrapper.flavor.regions = [wsme_models.Region('region1'), + wsme_models.Region('region2')] + flavor_wrapper.flavor.tenants = ['tenant1', 'tenant2'] + flavor_wrapper.flavor.id = 'id' + flavor_wrapper.flavor.ram = '1' + flavor_wrapper.flavor.visibility = 'visibility' + flavor_wrapper.flavor.vcpus = '1' + flavor_wrapper.flavor.swap = '1' + flavor_wrapper.flavor.disk = '1' + flavor_wrapper.flavor.name = 'name' + flavor_wrapper.flavor.series = 'ns' + + sql_flavor = flavor_wrapper.to_db_model() + + self.assertEqual(len(sql_flavor.flavor_regions), 2) + self.assertEqual(len(sql_flavor.flavor_tenants), 2) + + spec = next(s for s in sql_flavor.flavor_extra_specs if s.key_name == 'key1') + self.assertEqual(spec.key_value, 'val1') + + def test_flavor_summary_from_db_model(self): + sql_flavor = db_models.Flavor() + sql_flavor.id = 'some id' + sql_flavor.name = 'some name' + sql_flavor.description = 'some_decription' + + flavor_summary = wsme_models.FlavorSummary.from_db_model(sql_flavor) + + self.assertEqual(flavor_summary.id, sql_flavor.id) + self.assertEqual(flavor_summary.name, sql_flavor.name) + self.assertEqual(flavor_summary.description, sql_flavor.description) diff --git a/orm/services/flavor_manager/fms_rest/tests/logic/__init__.py b/orm/services/flavor_manager/fms_rest/tests/logic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/flavor_manager/fms_rest/tests/logic/test_flavor_logic.py b/orm/services/flavor_manager/fms_rest/tests/logic/test_flavor_logic.py new file mode 100755 index 00000000..cef5355e --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/logic/test_flavor_logic.py @@ -0,0 +1,768 @@ +from fms_rest.tests import FunctionalTest +from orm_common.injector import injector +from mock import MagicMock +from mock import patch +from fms_rest.data.wsme import models +from fms_rest.data.wsme.models import * +from fms_rest.data.sql_alchemy import db_models +import fms_rest.logic.flavor_logic as flavor_logic +from fms_rest.logic.error_base import NotFoundError, ConflictError + + +class OES(): + + def __init__(self): + pass + + def to_db_model(self): + return None + +error = None +FLAVOR_MOCK = None + + +class TestFlavorLogic(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + global FLAVOR_MOCK + FLAVOR_MOCK = MagicMock() + + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + injector.override_injected_dependency(('rds_proxy', rds_proxy_mock)) + + def tearDown(self): + global error + error = None + FunctionalTest.tearDown(self) + + def test_get_fixed_uuid_valid_uuid(self): + # With dashes + test_uuid = 'f8391e94-b332-4d7f-956c-07c6096b9140' + expected_result = test_uuid.replace('-', '') + self.assertEqual(expected_result, flavor_logic.get_fixed_uuid( + test_uuid)) + + # Without dashes + test_uuid = 'f8391e94b3324d7f956c07c6096b9140' + self.assertEqual(test_uuid, flavor_logic.get_fixed_uuid(test_uuid)) + + def test_get_fixed_uuid_not_a_uuid(self): + self.assertRaises(flavor_logic.ErrorStatus, + flavor_logic.get_fixed_uuid, 'test') + + def test_get_fixed_uuid_not_a_version_4_uuid(self): + self.assertRaises(flavor_logic.ErrorStatus, + flavor_logic.get_fixed_uuid, + 'f8391e94-b332-1d7f-956c-07c6096b9140') + + @patch.object(flavor_logic, 'FlavorWrapper') + def test_create_flavor_duplicate_entry(self, mock_flavorwrapper): + mock_flavorwrapper.from_db_model.return_value = get_flavor_mock() + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + flavor = get_flavor_mock() + flavor.flavor.validate_model = MagicMock( + side_effect=flavor_logic.ErrorStatus(409, 'Duplicate entry')) + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.create_flavor, + flavor, 'uuid', 'transaction') + + @patch.object(flavor_logic, 'FlavorWrapper') + def test_create_flavor_success(self, mock_flavorwrapper): + mock_flavorwrapper.from_db_model.return_value = get_flavor_mock() + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + + res_flavor = flavor_logic.create_flavor(get_flavor_mock(), 'uuid', + 'transaction') + self.assertEqual(res_flavor.flavor.profile, 'N1') + self.assertEqual(res_flavor.flavor.ram, '1024') + self.assertEqual(res_flavor.flavor.vcpus, '1') + self.assertEqual(res_flavor.flavor.series, 'ss') + self.assertEqual(res_flavor.flavor.id, 'g') + + flavor = get_flavor_mock() + flavor.flavor.ephemeral = '' + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.create_flavor, + flavor, 'uuid', 'transaction') + + flavor.flavor.validate_model = MagicMock() + res_flavor = flavor_logic.create_flavor(flavor, 'uuid', + 'transaction') + self.assertEqual(res_flavor.flavor.profile, 'N1') + self.assertEqual(res_flavor.flavor.ram, '1024') + self.assertEqual(res_flavor.flavor.vcpus, '1') + self.assertEqual(res_flavor.flavor.series, 'ss') + self.assertEqual(res_flavor.flavor.id, 'g') + + # + # def test_get_flavor_by_uuid_check_statuses_ok(self): + # flavor_logic.get_flavor_by_uuid("SampleUUId") + + @patch.object(flavor_logic, 'ExtraSpecsWrapper', return_value=MagicMock()) + def test_get_extra_specs_success(self, extra_spec_wrapper): + global error + error = 3 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + extra_spec_wrapper.from_db_model.return_value = {"key", "value"} + result = flavor_logic.get_extra_specs_uuid(123, "transaction_id") + self.assertEqual({"key", "value"}, result) + + @patch.object(flavor_logic, 'ExtraSpecsWrapper', return_value=MagicMock()) + def test_get_extra_specs_not_found(self, extra_spec_wrapper): + global error + error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(flavor_logic.NotFoundError): + flavor_logic.get_extra_specs_uuid(123, "transaction_id") + + @patch.object(flavor_logic, 'ExtraSpecsWrapper', return_value=MagicMock()) + def test_get_extra_specs_general_error(self, extra_spec_wrapper): + global error + error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(Exception) as cm: + flavor_logic.get_extra_specs_uuid(123, "transaction_id") + + @patch.object(flavor_logic, 'send_to_rds_if_needed', return_value=True) + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + def test_delete_extra_specs_success(self, mock_flavorwrapper, + mock_send_rds): + flavor_logic.delete_extra_specs(123, "transaction_id") + + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + def test_delete_extra_specs_not_found(self, mock_flavorwrapper): + global error + error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(flavor_logic.NotFoundError): + flavor_logic.delete_extra_specs(123, "transaction_id") + + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + def test_delete_extra_specs_bad_req(self, mock_flavorwrapper): + global error + error = 3 + extra_spec_needed = db_models.FlavorExtraSpec("key1", "value") + get_extra_spec_needed = MagicMock() + get_extra_spec_needed.get_extra_spec_needed.return_value = [extra_spec_needed] + mock_flavorwrapper.from_db_model.return_value = get_extra_spec_needed + with self.assertRaises(ErrorStatus): + flavor_logic.delete_extra_specs(123, "transaction_id", "key1") + + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + def test_delete_extra_specs_one(self, mock_flavorwrapper): + global error + error = 3 + extra_spec_needed = db_models.FlavorExtraSpec("key1", "value") + get_extra_spec_needed = MagicMock() + get_extra_spec_needed.get_extra_spec_needed.return_value = [ + extra_spec_needed] + mock_flavorwrapper.from_db_model.return_value = get_extra_spec_needed + flavor_logic.delete_extra_specs(123, "transaction_id", "key2") + + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + def test_delete_extra_specs_general_error(self, mock_flavorwrapper): + global error + error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(Exception) as cm: + flavor_logic.delete_extra_specs(123, "transaction_id") + + @patch.object(flavor_logic.ExtraSpecsWrapper, 'from_db_model', + return_value=True) + @patch.object(flavor_logic, 'send_to_rds_if_needed', return_value=True) + def test_add_extra_specs_success(self, mock_send_rds, extra_specs_wrapper): + flavor_logic.add_extra_specs(123, OES(), "transaction_id") + + def test_add_extra_specs_not_found(self): + global error + error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(flavor_logic.NotFoundError): + flavor_logic.add_extra_specs(123, OES(), "transaction_id") + + def test_add_extra_specs_gen_exp(self): + global error + error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(Exception): + flavor_logic.add_extra_specs(123, OES(), "transaction_id") + + @patch.object(flavor_logic.ExtraSpecsWrapper, 'from_db_model') + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + @patch.object(flavor_logic, 'send_to_rds_if_needed', return_value=True) + def test_update_extra_specs_success(self, mock_send_rds, + flavor_wrapper, + extra_specs_wrapper): + extra_specs_wrapper.return_value = extra_specs_json + result = flavor_logic.update_extra_specs(123, OES(), "transaction_id") + self.assertEqual(result, extra_specs_json) + + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + @patch.object(flavor_logic, 'send_to_rds_if_needed', return_value=True) + def test_update_extra_specs_not_found(self, mock_send_rds, + extra_specs_wrapper): + global error + error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(flavor_logic.NotFoundError): + flavor_logic.update_extra_specs(123, OES(), "transaction_id") + + @patch.object(flavor_logic, 'FlavorWrapper', return_value=MagicMock()) + @patch.object(flavor_logic, 'send_to_rds_if_needed', return_value=True) + def test_update_extra_specs_any_except(self, mock_send_rds, + extra_specs_wrapper): + global error + error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_datamanager_mock())) + with self.assertRaises(Exception): + flavor_logic.update_extra_specs(123, OES(), "transaction_id") + + def test_add_tags_success(self): + global error + error = 0 + datamanager = get_datamanager_mock() + injector.override_injected_dependency(('data_manager', datamanager)) + + tag = TagsWrapper(tags={'a': 'b'}) + ret = flavor_logic.add_tags('some_id', tag, 'trans_id') + + assert datamanager.return_value.commit.called + + def test_add_tags_not_found(self): + global error + error = 1 + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + + self.assertRaises(NotFoundError, flavor_logic.add_tags, 'a', None, 'a') + + def test_add_tags_error(self): + global error + error = 2 + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + + self.assertRaises(Exception, flavor_logic.add_tags, 'a', None, 'a') + + @patch.object(flavor_logic.FlavorWrapper, 'from_db_model') + def test_get_tags_success(self, mock_from_db_model): + my_flavor = MagicMock() + my_flavor.flavor.tag = {'test': 'A'} + mock_from_db_model.return_value = my_flavor + + global error + error = 4 + injector.override_injected_dependency(('data_manager', + get_datamanager_mock)) + + ret = flavor_logic.get_tags('some_id') + + self.assertEqual(ret, my_flavor.flavor.tag) + + def test_get_tags_not_found(self): + global error + error = 1 + injector.override_injected_dependency(('data_manager', + get_datamanager_mock)) + + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.get_tags, + 'some_id') + + def test_get_tags_error(self): + global error + error = 2 + injector.override_injected_dependency(('data_manager', + get_datamanager_mock)) + + self.assertRaises(SystemError, flavor_logic.get_tags, 'some_id') + + def test_update_tags_success(self): + global error + error = 0 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + tag = TagsWrapper(tags={'a': 'b'}) + ret = flavor_logic.update_tags('some_id', tag, 'trans_id') + + self.assertEqual(ret.tags, tag.tags) + + def test_update_tags_not_found(self): + global error + error = 1 + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + + self.assertRaises(NotFoundError, flavor_logic.update_tags, '', None, '') + + def test_update_tags_error(self): + global error + error = 2 + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + + self.assertRaises(Exception, flavor_logic.update_tags, 'a', None, 'a') + + def test_delete_tags_success(self): + tag = TagsWrapper(tags={'a': 'b'}) + flavor_logic.delete_tags('some_id', tag, 'trans_id') + + def test_delete_all_tags_success(self): + flavor_logic.delete_tags('some_id', None, 'trans_id') + + def test_delete_tags_not_found(self): + global error + error = 6 + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + + # Even when the tag was not found, an exception shouldn't be raised + flavor_logic.delete_tags('some_id', None, 'trans_id') + + error = 7 + self.assertRaises(flavor_logic.NotFoundError, flavor_logic.delete_tags, + 'some_id', None, 'trans_id') + + error = 8 + # This case should not raise an exception + flavor_logic.delete_tags('some_id', None, 'trans_id') + + def test_delete_tags_error(self): + global error + error = 2 + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + + self.assertRaises(Exception, flavor_logic.delete_tags, 'a', None, 'a') + + error = 9 + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.delete_tags, + 'a', None, 'a') + + def test_delete_flavor_by_uuid_success(self): + global error + error = 31 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + + flavor_logic.delete_flavor_by_uuid('some_id') + + error = 33 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + flavor_logic.delete_flavor_by_uuid('some_id') + + def test_delete_flavor_by_uuid_bad_status(self): + global error + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + + # Run once for response with no status and once for an invalid + # response code + for error_value in (32, 40,): + error = error_value + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + + try: + flavor_logic.delete_flavor_by_uuid('some_id') + self.fail('ErrorStatus not raised!') + except flavor_logic.ErrorStatus as e: + self.assertEqual(e.status_code, 500) + + # RDS returned OK, but the resource status is Error + error = 34 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + try: + flavor_logic.delete_flavor_by_uuid('some_id') + self.fail('ErrorStatus not raised!') + except flavor_logic.ErrorStatus as e: + self.assertEqual(e.status_code, 409) + + def test_delete_flavor_by_uuid_flavor_not_found(self): + global error + error = 1 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + + flavor_logic.delete_flavor_by_uuid('some_id') + + def test_delete_flavor_by_uuid_flavor_has_regions(self): + global error + error = 3 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + + try: + flavor_logic.delete_flavor_by_uuid('some_id') + self.fail('ErrorStatus not raised!') + except flavor_logic.ErrorStatus as e: + self.assertEqual(e.status_code, 405) + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_add_regions_success(self, mock_gfbu, mock_strin): + ret_flavor = MagicMock() + ret_flavor.flavor.regions = [Region(name='test_region')] + mock_gfbu.return_value = ret_flavor + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + + res_regions = flavor_logic.add_regions('uuid', RegionWrapper( + [Region(name='test_region')]), 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_add_regions_errors(self, mock_gfbu, mock_strin): + ret_flavor = MagicMock() + ret_flavor.flavor.regions = [Region(name='test_region')] + mock_gfbu.return_value = ret_flavor + global error + + error = 1 + injector.override_injected_dependency(('data_manager', get_datamanager_mock)) + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.add_regions, + 'uuid', RegionWrapper([Region(name='test_region')]), + 'transaction') + + error = 4 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + + mock_strin.side_effect = flavor_logic.FlushError() + self.assertRaises(flavor_logic.FlushError, flavor_logic.add_regions, + 'uuid', RegionWrapper([Region(name='test_region')]), + 'transaction') + mock_strin.side_effect = flavor_logic.FlushError( + 'conflicts with persistent instance') + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.add_regions, + 'uuid', RegionWrapper([Region(name='test_region')]), + 'transaction') + mock_strin.side_effect = ValueError() + self.assertRaises(ValueError, flavor_logic.add_regions, + 'uuid', RegionWrapper([Region(name='test_region')]), + 'transaction') + mock_strin.side_effect = ValueError( + 'conflicts with persistent instance') + self.assertRaises(flavor_logic.ConflictError, flavor_logic.add_regions, + 'uuid', RegionWrapper([Region(name='test_region')]), + 'transaction') + + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.add_regions, + 'uuid', RegionWrapper([Region(name='')]), + 'transaction') + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.add_regions, + 'uuid', RegionWrapper([Region(name='test_region', type='group')]), + 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_delete_region_success(self, mock_gfbu, mock_strin): + ret_flavor = MagicMock() + ret_flavor.flavor.regions = [Region(name='test_region')] + mock_gfbu.return_value = ret_flavor + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + + res_regions = flavor_logic.delete_region('uuid', RegionWrapper( + [Region(name='test_region')]), 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_delete_region_errors(self, mock_gfbu, mock_strin): + ret_flavor = MagicMock() + ret_flavor.flavor.regions = [Region(name='test_region')] + mock_gfbu.return_value = ret_flavor + global error + + error = 1 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + self.assertRaises(flavor_logic.ErrorStatus, flavor_logic.delete_region, + 'uuid', 'test_region', 'transaction') + + error = 2 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + self.assertRaises(SystemError, flavor_logic.delete_region, + 'uuid', 'test_region', 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_add_tenants_success(self, mock_gfbu, mock_strin): + ret_flavor = MagicMock() + tenants = ['test_tenant'] + ret_flavor.flavor.tenants = tenants + mock_gfbu.return_value = ret_flavor + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + + res_tenants = flavor_logic.add_tenants('uuid', + TenantWrapper(tenants), + 'transaction') + self.assertEqual(res_tenants.tenants, tenants) + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_add_tenants_errors(self, mock_gfbu, mock_strin): + ret_flavor = MagicMock() + tenants = ['test_tenant'] + ret_flavor.flavor.tenants = tenants + mock_gfbu.return_value = ret_flavor + global error + + error = 1 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + self.assertRaises(flavor_logic.ErrorStatus, + flavor_logic.add_tenants, 'uuid', + TenantWrapper(tenants), + 'transaction') + + # Flavor is public + error = 5 + self.assertRaises(flavor_logic.ErrorStatus, + flavor_logic.add_tenants, 'uuid', + TenantWrapper(tenants), + 'transaction') + + error = 31 + moq = MagicMock() + moq.tenants = [1337] + self.assertRaises(ValueError, + flavor_logic.add_tenants, 'uuid', + moq, + 'transaction') + + mock_strin.side_effect = flavor_logic.FlushError( + 'conflicts with persistent instance') + self.assertRaises(flavor_logic.ConflictError, + flavor_logic.add_tenants, 'uuid', + TenantWrapper(tenants), + 'transaction') + + mock_strin.side_effect = flavor_logic.FlushError('') + self.assertRaises(flavor_logic.FlushError, + flavor_logic.add_tenants, 'uuid', + TenantWrapper(tenants), + 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_delete_tenant_success(self, mock_gfbu, mock_strin): + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + + flavor_logic.delete_tenant('uuid', 'tenant_id', 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + def test_delete_tenant_errors(self, mock_gfbu, mock_strin): + ret_flavor = MagicMock() + tenants = ['test_tenant'] + ret_flavor.flavor.tenants = tenants + mock_gfbu.return_value = ret_flavor + global error + + error = 1 + injector.override_injected_dependency( + ('data_manager', get_datamanager_mock)) + self.assertRaises(flavor_logic.ErrorStatus, + flavor_logic.delete_tenant, 'uuid', + 'tenant_id', + 'transaction') + + # Flavor is public + error = 5 + self.assertRaises(ValueError, + flavor_logic.delete_tenant, 'uuid', + 'tenant_id', + 'transaction') + + global FLAVOR_MOCK + tenant = MagicMock() + tenant.tenant_id = 'tenant_id' + FLAVOR_MOCK.flavor_tenants = [tenant] + error = 6 + self.assertRaises(ValueError, + flavor_logic.delete_tenant, 'uuid', + 'tenant_id', + 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + @patch.object(models, 'request') + @patch.object(flavor_logic, 'ExtraSpecsWrapper') + def test_add_extra_specs_success(self, mock_extra_specs_wrapper, + mock_request, mock_gfbu, mock_strin): + extra_specs = ExtraSpecsWrapper({'a': 'b'}) + mock_extra_specs_wrapper.from_db_model.return_value = extra_specs + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + res_extra_specs = flavor_logic.add_extra_specs('uuid', + extra_specs, + 'transaction') + self.assertEqual(res_extra_specs.os_extra_specs, {'a': 'b'}) + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + @patch.object(models, 'request') + @patch.object(flavor_logic, 'ExtraSpecsWrapper') + def test_add_extra_specs_conflict_error(self, mock_extra_specs_wrapper, + mock_request, mock_gfbu, + mock_strin): + mock_strin.side_effect = ValueError( + 'conflicts with persistent instance') + extra_specs = ExtraSpecsWrapper({'a': 'b'}) + mock_extra_specs_wrapper.from_db_model.return_value = extra_specs + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + self.assertRaises(flavor_logic.ConflictError, + flavor_logic.add_extra_specs, 'uuid', + extra_specs, + 'transaction') + + @patch.object(flavor_logic, 'send_to_rds_if_needed') + @patch.object(flavor_logic, 'get_flavor_by_uuid') + @patch.object(models, 'request') + @patch.object(flavor_logic, 'ExtraSpecsWrapper') + @patch.object(flavor_logic, 'FlavorWrapper') + def test_update_extra_specs_success(self, mock_flavor_wrapper, + mock_extra_specs_wrapper, + mock_request, mock_gfbu, mock_strin): + extra_specs = ExtraSpecsWrapper({'a': 'b'}) + mock_extra_specs_wrapper.from_db_model.return_value = extra_specs + global error + error = 31 + injector.override_injected_dependency( + ('rds_proxy', get_rds_proxy_mock())) + res_extra_specs = flavor_logic.update_extra_specs('uuid', + extra_specs, + 'transaction') + self.assertEqual(res_extra_specs.os_extra_specs, {'a': 'b'}) + + +def get_datamanager_mock(): + def get_record(record_type): + global error + if record_type == 'flavor': + record = MagicMock() + db_model = db_models.Flavor() + db_model.remove_tag = MagicMock() + db_model.remove_all_tags = MagicMock() + record.get_flavor_by_id.return_value = db_model + + if error == 1: + record.get_flavor_by_id.return_value = None + elif error == 2: + record.get_flavor_by_id.side_effect = SystemError() + elif error == 3: + moq = MagicMock() + moq.get_existing_region_names.return_value = ['region'] + record.get_flavor_by_id.return_value = moq + elif error == 4: + record.get_flavor_by_id.return_value = db_models.Flavor() + elif error == 5: + record.get_flavor_by_id.return_value = db_models.Flavor( + visibility='public') + elif error == 6: + record.get_flavor_by_id.return_value = FLAVOR_MOCK + elif error == 7: + record.get_flavor_by_id.side_effect = flavor_logic.NotFoundError() + elif error == 8: + record.get_flavor_by_id.side_effect = flavor_logic.ErrorStatus( + 404) + elif error == 9: + record.get_flavor_by_id.side_effect = flavor_logic.ErrorStatus( + 500) + else: + record.get_flavor_by_id.return_value = MagicMock() + return record + + mock = MagicMock() + mock.get_record = get_record + + return mock + + +def get_rds_proxy_mock(): + def get_status(resource_id): + global error + response = MagicMock() + + if error == 31: + response.status_code = 200 + response.json.return_value = {'status': 'Success'} + elif error == 32: + response.status_code = 200 + response.json.return_value = {} + elif error == 33: + response.status_code = 404 + elif error == 34: + response.status_code = 200 + response.json.return_value = {'status': 'Error'} + else: + response.status_code = 500 + + return response + + mock = MagicMock() + mock.get_status = get_status + + return mock + + +def get_flavor_mock(): + flavor_mock = FlavorWrapper() + flavor_mock.flavor = Flavor(ram='1024', vcpus='1', series='ss', id='g') + flavor_mock.flavor.profile = 'N1' + + return flavor_mock + + +rds_proxy_mock = MagicMock() +rds_proxy_mock.get_status.return_value = { + "status": "pending", + "regions": [ + { + "region": "dla1", + "timestamp": "1451599200", + "ord-transaction-id": "0649c5be323f4792afbc1efdd480847d", + "resource-id": "12fde398643acbed32f8097c98aec20", + "ord-notifier-id": "", + "status": "success", + "error-code": "200", + "error-msg": "OK" + } + ] +} + +extra_specs_json = { + "os_extra_specs": { + "name357": "region_name1", + "name4467": "2", + "name66767": "222234556" + } +} diff --git a/orm/services/flavor_manager/fms_rest/tests/mocks/__init__.py b/orm/services/flavor_manager/fms_rest/tests/mocks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/flavor_manager/fms_rest/tests/rest/__init__.py b/orm/services/flavor_manager/fms_rest/tests/rest/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/flavor_manager/fms_rest/tests/rest/test_flavors.py b/orm/services/flavor_manager/fms_rest/tests/rest/test_flavors.py new file mode 100755 index 00000000..14ecf544 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/rest/test_flavors.py @@ -0,0 +1,423 @@ +import copy +import requests +from fms_rest.tests import FunctionalTest +from orm_common.injector import injector +from mock import MagicMock +from mock import patch +import sqlalchemy + +from fms_rest.data.wsme import models +from fms_rest.logic.error_base import ErrorStatus +from fms_rest.controllers.v1.orm.flavors import flavors +from fms_rest.tests import test_utils + +utils_mock = None +flavor_logic_mock = None + +return_error = 0 + + +class TestFlavorController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_create_flavor(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 0 + + # when + response = self.app.post_json('/v1/orm/flavors', FLAVOR_JSON) + + # assert + assert response.status_int == 201 + assert utils_mock.audit_trail.called + assert utils_mock.make_uuid.called + assert flavor_logic_mock.create_flavor.called + + def test_create_flavor_predefined_id(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + test_json = copy.deepcopy(FLAVOR_JSON) + test_json['flavor']['id'] = 'test' + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + # when + response = self.app.post_json('/v1/orm/flavors', test_json) + + # assert + self.assertEqual(response.status_int, 201) + + def test_create_flavor_existing_predefined_id(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + test_json = copy.deepcopy(FLAVOR_JSON) + test_json['flavor']['id'] = 'test' + global return_error + return_error = 1 + injector.override_injected_dependency(('utils', get_utils_mock())) + + # when + response = self.app.post_json('/v1/orm/flavors', test_json, + expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 409) + + def test_create_flavor_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.post = MagicMock(return_value=ResponseMock(400, "failed")) + + # when + response = self.app.post_json('/v1/orm/flavors', FLAVOR_JSON, + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + @patch.object(flavors, 'di') + def test_create_flavor_duplicate_entry(self, mock_di): + my_mock = MagicMock() + my_mock.create_flavor = MagicMock( + side_effect=sqlalchemy.exc.IntegrityError( + 'a', 'b', + 'Duplicate entry \'flavor\' for key \'name_idx\'')) + mock_di.resolver.unpack = MagicMock( + return_value=(my_mock, MagicMock(),)) + + response = self.app.post_json('/v1/orm/flavors', FLAVOR_JSON, + expect_errors=True) + + self.assertEqual(response.status_int, 500) + + @patch.object(flavors, 'di') + def test_create_flavor_other_error(self, mock_di): + my_mock = MagicMock() + my_mock.create_flavor = MagicMock( + side_effect=sqlalchemy.exc.IntegrityError( + 'a', 'b', + 'test \'flavor\' for key \'name_idx\'')) + mock_di.resolver.unpack = MagicMock( + return_value=(my_mock, MagicMock(),)) + + response = self.app.post_json('/v1/orm/flavors', FLAVOR_JSON, + expect_errors=True) + + self.assertEqual(response.status_int, 500) + + @patch.object(flavors, 'err_utils') + def test_create_flavor_bad_request(self, mock_err_utils): + # given + global return_error + return_error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.post = MagicMock(return_value=ResponseMock(400, "failed")) + mock_err_utils.get_error = test_utils.get_error + + # when + response = self.app.post_json('/v1/orm/flavors', FLAVOR_JSON, + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_update_flavor(self): + # given + + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.put = MagicMock() + + # when + response = self.app.put_json('/v1/orm/flavors/some_id', FLAVOR_JSON, + expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 405) + + def test_get_flavor(self): + # given + + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.get = MagicMock(return_value=ResponseMock(200, "updated")) + + # when + response = self.app.get('/v1/orm/flavors/some_id') + + # assert + assert flavor_logic_mock.get_flavor_by_uuid_or_name.called + + def test_get_flavor_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.get = MagicMock() + + # when + response = self.app.get('/v1/orm/flavors/some_id', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_get_flavor_bad_request(self): + # given + global return_error + return_error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.get = MagicMock() + + # when + response = self.app.get('/v1/orm/flavors/some_id', expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + + # 8/8/16 Bug DE226006 - This bug fix is to return 405 for every attempt + # to delete flavor. + def test_delete_flavor(self): + # given + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.delete = MagicMock() + + # when + response = self.app.delete('/v1/orm/flavors/some_id', + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 204) + + def test_delete_flavor_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.delete = MagicMock() + + # when + response = self.app.delete('/v1/orm/flavors/some_id', + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + @patch.object(flavors, 'err_utils') + def test_delete_flavor_bad_request(self, mock_err_utils): + # given + global return_error + return_error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.delete = MagicMock() + mock_err_utils.get_error = test_utils.get_error + + # when + response = self.app.delete('/v1/orm/flavors/some_id', + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 409) + + def test_get_all_flavor(self): + # given + + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.get = MagicMock(return_value=ResponseMock(200, "updated")) + + # when + response = self.app.get('/v1/orm/flavors?region=SAN1') + + # assert + assert flavor_logic_mock.get_flavor_list_by_params.called + + def test_get_all_flavor_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.get = MagicMock() + + # when + response = self.app.get('/v1/orm/flavors?region=region', + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_get_all_flavor_bad_request(self): + # given + global return_error + return_error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + requests.get = MagicMock() + + # when + response = self.app.get('/v1/orm/flavors?region=region', + expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + + +class ResponseMock: + def __init__(self, status_code=200, message=""): + self.status_code = status_code + self.message = message + + +def get_flavor_logic_mock(): + global flavor_logic_mock + flavor_logic_mock = MagicMock() + + if return_error == 0: + flavor_logic_mock.update_flavor.return_value = RET_FLAVOR_JSON + flavor_logic_mock.create_flavor.return_value = RET_FLAVOR_JSON + flavor_logic_mock.get_flavor_by_uuid.return_value = RET_FLAVOR_JSON + flavor_logic_mock.get_flavor_by_uuid_or_name.return_value = \ + RET_FLAVOR_JSON + flavor_logic_mock.get_flavor_list_by_params.return_value = FILTER_RET + elif return_error == 1: + flavor_logic_mock.update_flavor.side_effect = SystemError() + flavor_logic_mock.create_flavor.side_effect = SystemError() + flavor_logic_mock.get_flavor_by_uuid.side_effect = SystemError() + flavor_logic_mock.get_flavor_by_uuid_or_name.side_effect = \ + SystemError() + flavor_logic_mock.get_flavor_list_by_params.side_effect = SystemError() + flavor_logic_mock.delete_flavor_by_uuid.side_effect = SystemError() + else: + flavor_logic_mock.update_flavor.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.create_flavor.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.get_flavor_by_uuid.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.get_flavor_list_by_params.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.delete_flavor_by_uuid.side_effect = ErrorStatus( + status_code=409) + flavor_logic_mock.get_flavor_by_uuid_or_name.side_effect = ErrorStatus( + status_code=404) + + return flavor_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + if return_error: + utils_mock.create_existing_uuid.side_effect = TypeError('test') + else: + utils_mock.create_existing_uuid.return_value = 'some_uuid' + + return utils_mock + + +FLAVOR_JSON = { + "flavor": { + "description": "A standard 2GB Ram 2 vCPUs 50GB Disk, Flavor", + "series": "nv", + "ram": "05", + "vcpus": "2", + "disk": "50", + "swap": "1024", + "ephemeral": "0", + "extra-specs": { + "key1": "value1", + "key2": "value2", + "key3": "value3" + }, + "options": { + "option2": "valueoption2", + "option3": "valueoption3", + "option1": "valueoption1" + }, + "tag": { + "tags1": "valuetags1", + "tags2": "valuetags2", + "tags3": "valuetags3" + }, + "regions": [ + {"name": "0", "type": "single"}, + {"name": "1", "type": "single"} + ], + "visibility": "private", + "tenants": [ + "4f7b9561-af8b-4cc0-87e2-319270dad49e", + "070be05e-26e2-4519-a46d-224cbf8558f4" + ], + "status": "complete" + } +} + +RET_FLAVOR_JSON = models.FlavorWrapper( + models.Flavor( + description="A standard 2GB Ram 2 vCPUs 50GB Disk, Flavor", + series="nv", + ram="05", + vcpus="2", + disk="50", + swap="1024", + ephemeral="0", + extra_specs={ + "key1": "value1" + }, + tag={ + "tags1": "valuetags1" + }, + options={ + "option1": "valueoption1" + }, + regions=[models.Region(name='1')], + visibility="private", + tenants=[ + "4f7b9561-af8b-4cc0-87e2-319270dad49e", + "070be05e-26e2-4519-a46d-224cbf8558f4" + ], + status="complete" + ) +) + +# FILTER_RET = [models.Flavor()] + +FILTER_RET = models.FlavorListFullResponse() +FILTER_RET.flavors.append( + models.Flavor(name='1', id='1', description='1')) diff --git a/orm/services/flavor_manager/fms_rest/tests/rest/test_logs.py b/orm/services/flavor_manager/fms_rest/tests/rest/test_logs.py new file mode 100755 index 00000000..6b1a5381 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/rest/test_logs.py @@ -0,0 +1,25 @@ +"""Logs module unittests.""" +from fms_rest.tests import FunctionalTest + + +class TestLogs(FunctionalTest): + """logs tests.""" + + def test_change_log_level_fail(self): + response = self.app.put('/v1/orm/logs/1') + expected_result = { + "result": "Fail to change log_level. Reason: " + "The given log level [1] doesn't exist."} + self.assertEqual(expected_result, response.json) + + def test_change_log_level_none(self): + response = self.app.put('/v1/orm/logs', expect_errors=True) + expected_result = 'Missing argument: "level"' + self.assertEqual(response.json["faultstring"], expected_result) + self.assertEqual(response.status_code, 400) + + def test_change_log_level_success(self): + response = self.app.put('/v1/orm/logs/debug') + expected_result = {'result': 'Log level changed to debug.'} + self.assertEqual(response.json, expected_result) + self.assertEqual(response.status_code, 201) diff --git a/orm/services/flavor_manager/fms_rest/tests/rest/test_os_extra_specs.py b/orm/services/flavor_manager/fms_rest/tests/rest/test_os_extra_specs.py new file mode 100755 index 00000000..74221c1a --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/rest/test_os_extra_specs.py @@ -0,0 +1,195 @@ +import json + +from fms_rest.controllers.v1.orm.flavors import os_extra_specs as es +from fms_rest.data.wsme import models +from fms_rest.logic.error_base import NotFoundError +from fms_rest.tests import FunctionalTest +from orm_common.injector import injector +from mock import MagicMock +from mock import patch + + +class TestOsExtraSpecsController(FunctionalTest): + """unittes.""" + + @patch.object(es, 'di') + def test_create_os_flavor_specs_success(self, mock_di): + mock_di.resolver.unpack.return_value =\ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + response = self.app.post_json('/v1/orm/flavors/123/os_extra_specs', + flavor_extra_specs_json) + self.assertEqual(response.json, flavor_extra_specs_json) + self.assertEqual(response.status_code, 201) + + # def test_create_os_extra_specs_bad_json(self): + # global flavor_extra_specs_json + # backup = dict(flavor_extra_specs_json) + # del(flavor_extra_specs_json["os_extra_specs"]) + # response = self.app.post_json('/v1/orm/flavors/123/os_extra_specs', + # flavor_extra_specs_json, + # expect_errors=True) + # flavor_extra_specs_json = backup + # self.assertEqual(response.status_code, 400) + + @patch.object(es, 'di') + def test_create_os_flavor_specs_audit_trial_fails(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + utils_mock.audit_trail.side_effect = SystemError() + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.post_json('/v1/orm/flavors/123/os_extra_specs', + flavor_extra_specs_json, + expect_errors=True) + self.assertEqual(response.status_code, 500) + + @patch.object(es, 'di') + def test_create_os_flavor_specs_flavor_not_found(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + flavor_mock.add_extra_specs.side_effect = NotFoundError(404, + "not found") + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.post_json('/v1/orm/flavors/123/os_extra_specs', + flavor_extra_specs_json, + expect_errors=True) + + # dict_body = json.loads(response.body) + # my_json = json.loads(dict_body['faultstring']) + + # self.assertEqual(response.status_code, 404) + # self.assertEqual(my_json['message'], 'not found') + + @patch.object(es, 'di') + def test_update_os_flavor_specs_success(self, mock_di): + mock_di.resolver.unpack.return_value = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + response = self.app.put_json('/v1/orm/flavors/123/os_extra_specs', + flavor_extra_specs_json) + self.assertEqual(response.json, flavor_extra_specs_json) + self.assertEqual(response.status_code, 200) + + # def test_update_os_extra_specs_bad_json(self): + # global flavor_extra_specs_json + # backup = dict(flavor_extra_specs_json) + # del (flavor_extra_specs_json["os_extra_specs"]) + # response = self.app.put_json('/v1/orm/flavors/123/os_extra_specs', + # flavor_extra_specs_json, + # expect_errors=True) + # flavor_extra_specs_json = backup + # self.assertEqual(response.status_code, 400) + + @patch.object(es, 'di') + def test_update_os_flavor_specs_audit_trial_fails(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + utils_mock.audit_trail.side_effect = SystemError() + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.put_json('/v1/orm/flavors/123/os_extra_specs', + flavor_extra_specs_json, + expect_errors=True) + self.assertEqual(response.status_code, 500) + + @patch.object(es, 'di') + def test_update_os_flavor_specs_flavor_not_found(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + flavor_mock.update_extra_specs.side_effect = NotFoundError(404, + "not found") + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.put_json('/v1/orm/flavors/123/os_extra_specs', + flavor_extra_specs_json, + expect_errors=True) + + # dict_body = json.loads(response.body) + # my_json = json.loads(dict_body['faultstring']) + + # self.assertEqual(response.status_code, 404) + # self.assertEqual(my_json['message'], 'not found') + + @patch.object(es, 'di') + def test_get_os_flavor_specs_flavor_not_found(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + flavor_mock.get_extra_specs_uuid.side_effect = NotFoundError(404, + "not found") + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.get('/v1/orm/flavors/123/os_extra_specs', + expect_errors=True) + + # dict_body = json.loads(response.body) + # my_json = json.loads(dict_body['faultstring']) + + # self.assertEqual(response.status_code, 404) + # self.assertEqual(my_json['message'], 'not found') + + @patch.object(es, 'di') + def test_get_os_flavor_specs_success(self, mock_di): + mock_di.resolver.unpack.return_value = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + response = self.app.get('/v1/orm/flavors/123/os_extra_specs') + self.assertEqual(response.json, flavor_extra_specs_json) + self.assertEqual(response.status_code, 200) + + @patch.object(es, 'di') + def test_get_os_flavor_specs_audit_trial_fails(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + utils_mock.audit_trail.side_effect = SystemError() + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.get('/v1/orm/flavors/123/os_extra_specs', + expect_errors=True) + self.assertEqual(response.status_code, 500) + + @patch.object(es, 'di') + def test_delete_os_flavor_specs_success(self, mock_di): + mock_di.resolver.unpack.return_value = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + response = self.app.delete('/v1/orm/flavors/123/os_extra_specs') + self.assertEqual(response.status_code, 204) + + @patch.object(es, 'di') + def test_delete_os_flavor_specs_audit_trial_fails(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + utils_mock.audit_trail.side_effect = SystemError() + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.delete('/v1/orm/flavors/123/os_extra_specs', + flavor_extra_specs_json, + expect_errors=True) + self.assertEqual(response.status_code, 500) + + @patch.object(es, 'di') + def test_delete_os_flavor_specs_flavor_not_found(self, mock_di): + flavor_mock, utils_mock = \ + get_utils_flavor_logic_mock(flavor_extra_specs_json) + flavor_mock.delete_extra_specs.side_effect = NotFoundError(404, + "not found") + mock_di.resolver.unpack.return_value = flavor_mock, utils_mock + response = self.app.delete('/v1/orm/flavors/123/os_extra_specs', + expect_errors=True) + + # dict_body = json.loads(response.body) + # my_json = json.loads(dict_body['faultstring']) + + # self.assertEqual(response.status_code, 404) + # self.assertEqual(my_json['message'], 'not found') + + +def get_utils_flavor_logic_mock(input_json=None): + es = models.ExtraSpecsWrapper(input_json["os_extra_specs"]) + utils_mock = MagicMock() + flavor_logic_mock = MagicMock() + flavor_logic_mock.add_extra_specs.return_value = es + flavor_logic_mock.update_extra_specs.return_value = es + flavor_logic_mock.get_extra_specs_uuid.return_value = es + utils_mock.audit_trail.return_value = None + return flavor_logic_mock, utils_mock + + +flavor_extra_specs_json = { + "os_extra_specs": { + "name357": "region_name1", + "name4467": "2", + "name66767": "222234556" + } +} diff --git a/orm/services/flavor_manager/fms_rest/tests/rest/test_regions.py b/orm/services/flavor_manager/fms_rest/tests/rest/test_regions.py new file mode 100755 index 00000000..bb968684 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/rest/test_regions.py @@ -0,0 +1,150 @@ +import requests +from fms_rest.tests import FunctionalTest +from orm_common.injector import injector +from mock import MagicMock +from mock import patch + +from fms_rest.controllers.v1.orm.flavors import regions +from fms_rest.data.wsme import models +from fms_rest.logic.error_base import ErrorStatus +from fms_rest.tests import test_utils + +utils_mock = None +region_logic_mock = None + +return_error = 0 + + +class TestRegionController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('flavor_logic', get_region_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_add_regions(self): + # given + requests.post = MagicMock(return_value=ResponseMock(200, "added")) + + global return_error + return_error = 0 + + # when + response = self.app.post_json('/v1/orm/flavors/flavor_id/regions', REGION_JSON) + + # assert + assert utils_mock.audit_trail.called + assert region_logic_mock.add_regions.called + + def test_add_regions_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency(('flavor_logic', get_region_logic_mock())) + requests.post = MagicMock() + + # when + response = self.app.post_json('/v1/orm/flavors/{flavor_id}/regions/', REGION_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + @patch.object(regions, 'err_utils') + def test_add_regions_bad_request(self, mock_err_utils): + # given + global return_error + return_error = 2 + injector.override_injected_dependency(('flavor_logic', get_region_logic_mock())) + requests.post = MagicMock() + mock_err_utils.get_error = test_utils.get_error + + # when + response = self.app.post_json('/v1/orm/flavors/{flavor_id}/regions/', REGION_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 404) + + def test_delete_region(self): + # given + global return_error + return_error = 0 + injector.override_injected_dependency(('flavor_logic', get_region_logic_mock())) + requests.delete = MagicMock(return_value=ResponseMock(204)) + + # when + self.app.delete('/v1/orm/flavors/flavor_id/regions/region_id') + + # assert + assert utils_mock.audit_trail.called + assert region_logic_mock.delete_region.called + + def test_delete_region_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency(('flavor_logic', get_region_logic_mock())) + requests.delete = MagicMock() + + # when + response = self.app.delete('/v1/orm/flavors/flavor_id/regions/{region_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_delete_region_bad_request(self): + # given + global return_error + return_error = 2 + injector.override_injected_dependency(('flavor_logic', get_region_logic_mock())) + requests.delete = MagicMock() + + # when + response = self.app.delete('/v1/orm/flavors/flavor_id/regions/{region_id}', expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + + +class ResponseMock: + def __init__(self, status_code=200, message=""): + self.status_code = status_code + self.message = message + + +def get_region_logic_mock(): + global region_logic_mock + region_logic_mock = MagicMock() + + if return_error == 0: + region_logic_mock.add_regions.return_value = RET_REGION_JSON + elif return_error == 1: + region_logic_mock.add_regions.side_effect = SystemError() + region_logic_mock.delete_region.side_effect = SystemError() + else: + region_logic_mock.add_regions.side_effect = ErrorStatus(status_code=404) + region_logic_mock.delete_region.side_effect = ErrorStatus(status_code=404) + + return region_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + return utils_mock + + +REGION_JSON = { + "regions": [ + {"name": "76", "type": "single"} + ] +} + +RET_REGION_JSON = models.RegionWrapper([models.Region(name='76', status='done')]) diff --git a/orm/services/flavor_manager/fms_rest/tests/rest/test_tags.py b/orm/services/flavor_manager/fms_rest/tests/rest/test_tags.py new file mode 100644 index 00000000..568031bd --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/rest/test_tags.py @@ -0,0 +1,354 @@ +import requests +from fms_rest.tests import FunctionalTest +from orm_common.injector import injector +from mock import MagicMock +from mock import patch +import sqlalchemy + +from fms_rest.data.wsme import models +from fms_rest.logic.error_base import ErrorStatus +from fms_rest.controllers.v1.orm.flavors import flavors + +utils_mock = None +flavor_logic_mock = None + +return_error = 0 + + +class TestTagsController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_create_tags_success(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.post_json('/v1/orm/flavors/test/tags', FLAVOR_JSON) + + # assert + self.assertEqual(response.status_int, 201) + + def test_create_tags_exception_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.post_json('/v1/orm/flavors/test/tags', FLAVOR_JSON, + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + # assert utils_mock.audit_trail.called + + def test_create_tags_errorstatus_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.post_json('/v1/orm/flavors/test/tags', FLAVOR_JSON, + expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + # assert utils_mock.audit_trail.called + + def test_update_tags_success(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.put_json('/v1/orm/flavors/test/tags', FLAVOR_JSON) + + # assert + self.assertEqual(response.status_int, 200) + # assert utils_mock.audit_trail.called + + def test_update_tags_exception_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.put_json('/v1/orm/flavors/test/tags', FLAVOR_JSON, + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + # assert utils_mock.audit_trail.called + + def test_update_tags_errorstatus_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.put_json('/v1/orm/flavors/test/tags', FLAVOR_JSON, + expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + # assert utils_mock.audit_trail.called + + def test_delete_tags_success(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.delete('/v1/orm/flavors/test/tags') + + # assert + self.assertEqual(response.status_int, 204) + # assert utils_mock.audit_trail.called + + def test_delete_tags_exception_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.delete('/v1/orm/flavors/test/tags', + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + # assert utils_mock.audit_trail.called + + def test_delete_tags_error_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.delete('/v1/orm/flavors/test/tags', + expect_errors=True) + # assert + self.assertEqual(response.status_int, 500) + + def test_get_tags_success(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 0 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.get('/v1/orm/flavors/test/tags') + + # assert + # assert response.status_int == 200 + + def test_get_tags_exception_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 1 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.get('/v1/orm/flavors/test/tags', + expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + # assert utils_mock.audit_trail.called + + def test_get_tags_errorstatus_raised(self): + # given + requests.post = MagicMock(return_value=ResponseMock(201, "created")) + + global return_error + return_error = 2 + injector.override_injected_dependency( + ('flavor_logic', get_flavor_logic_mock())) + + # when + response = self.app.get('/v1/orm/flavors/test/tags', + expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + + +class ResponseMock: + def __init__(self, status_code=200, message=""): + self.status_code = status_code + self.message = message + + +def get_flavor_logic_mock(): + global flavor_logic_mock + flavor_logic_mock = MagicMock() + + if return_error == 0: + flavor_logic_mock.get_tags.return_value = FLAVOR_JSON['tags'] + flavor_logic_mock.add_tags.return_value = RET_FLAVOR_JSON + flavor_logic_mock.delete_tags.return_value = RET_FLAVOR_JSON + flavor_logic_mock.update_tags.return_value = RET_FLAVOR_JSON + flavor_logic_mock.update_flavor.return_value = RET_FLAVOR_JSON + flavor_logic_mock.create_flavor.return_value = RET_FLAVOR_JSON + flavor_logic_mock.get_flavor_by_uuid.return_value = RET_FLAVOR_JSON + flavor_logic_mock.get_flavor_by_uuid_or_name.return_value = \ + RET_FLAVOR_JSON + flavor_logic_mock.get_flavor_list_by_params.return_value = FILTER_RET + elif return_error == 1: + flavor_logic_mock.get_tags.side_effect = SystemError() + flavor_logic_mock.add_tags.side_effect = SystemError() + flavor_logic_mock.delete_tags.side_effect = SystemError() + flavor_logic_mock.update_tags.side_effect = SystemError() + flavor_logic_mock.update_flavor.side_effect = SystemError() + flavor_logic_mock.create_flavor.side_effect = SystemError() + flavor_logic_mock.get_flavor_by_uuid.side_effect = SystemError() + flavor_logic_mock.get_flavor_by_uuid_or_name.side_effect = \ + SystemError() + flavor_logic_mock.get_flavor_list_by_params.side_effect = SystemError() + flavor_logic_mock.delete_flavor_by_uuid.side_effect = SystemError() + else: + flavor_logic_mock.get_tags.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.add_tags.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.delete_tags.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.update_tags.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.update_flavor.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.create_flavor.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.get_flavor_by_uuid.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.get_flavor_list_by_params.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.delete_flavor_by_uuid.side_effect = ErrorStatus( + status_code=404) + flavor_logic_mock.get_flavor_by_uuid_or_name.side_effect = ErrorStatus( + status_code=404) + + return flavor_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + if return_error: + utils_mock.create_existing_uuid.side_effect = TypeError('test') + else: + utils_mock.create_existing_uuid.return_value = 'some_uuid' + + return utils_mock + + +FLAVOR_JSON = { + "tags": { + "A": "B" + } +} + +RET_FLAVOR_JSON = models.TagsWrapper( + FLAVOR_JSON["tags"] +) + +FILTER_RET = [ + { + "status": "Error", + "description": "A standard 2GB Ram 2 vCPUs 50GB Disk, Flavor", + "series": "ss", + "extra-specs": { + "aggregate_instance_extra_specs:ss": "true", + "spec2": "valuespec2", + "spec3": "valuespec3", + "spec1": "valuespec1" + }, + "ram": "1024", + "ephemeral": "1", + "visibility": "private", + "options": { + "option2": "valueoption2", + "option3": "valueoption3", + "bundle": "test", + "option1": "valueoption1" + }, + "regions": [ + { + "status": "Error", + "type": "single", + "name": "lcp_1" + } + ], + "vcpus": "20", + "tag": { + "tags1": "valuetags1", + "tags2": "valuetags2", + "tags3": "valuetags3" + }, + "swap": "1", + "disk": "2048", + "tenants": [ + "070be05e-26e2-4519-a46d-224cbf8558f4", + "4f7b9561-af8b-4cc0-87e2-319270dad49e" + ], + "id": "e2b86034eeec4b7abd01b9e0287a13ff", + "name": "ss.c20r1d2048s1e1.bundleoption1option2option3" + }] + +FILTER_RET = models.FlavorListFullResponse() +FILTER_RET.flavors.append( + models.Flavor(name='1', id='1', description='1')) diff --git a/orm/services/flavor_manager/fms_rest/tests/rest/test_tenants.py b/orm/services/flavor_manager/fms_rest/tests/rest/test_tenants.py new file mode 100755 index 00000000..1b3ec133 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/rest/test_tenants.py @@ -0,0 +1,145 @@ +import requests +from fms_rest.tests import FunctionalTest +from orm_common.injector import injector +from mock import MagicMock + +from fms_rest.data.wsme import models +from fms_rest.logic.error_base import ErrorStatus + +utils_mock = None +tenant_logic_mock = None + +return_error = 0 + + +class TestTenantController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('flavor_logic', get_tenant_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_add_tenants(self): + # given + requests.post = MagicMock(return_value=ResponseMock(200, "added")) + + global return_error + return_error = 0 + + # when + response = self.app.post_json('/v1/orm/flavors/flavor_id/tenants', TENANT_JSON) + + # assert + assert utils_mock.audit_trail.called + assert tenant_logic_mock.add_tenants.called + + def test_add_tenants_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency(('flavor_logic', get_tenant_logic_mock())) + requests.post = MagicMock() + + # when + response = self.app.post_json('/v1/orm/flavors/{flavor_id}/tenants/', TENANT_JSON, expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_add_tenants_bad_request(self): + # given + global return_error + return_error = 2 + injector.override_injected_dependency(('flavor_logic', get_tenant_logic_mock())) + requests.post = MagicMock() + + # when + response = self.app.post_json('/v1/orm/flavors/{flavor_id}/tenants/', TENANT_JSON, expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + + def test_delete_tenant(self): + # given + global return_error + return_error = 0 + injector.override_injected_dependency(('flavor_logic', get_tenant_logic_mock())) + requests.delete = MagicMock(return_value=ResponseMock(204)) + + # when + self.app.delete('/v1/orm/flavors/flavor_id/tenants/tenant_id') + + # assert + assert utils_mock.audit_trail.called + assert tenant_logic_mock.delete_tenant.called + + def test_delete_tenant_fail(self): + # given + global return_error + return_error = 1 + injector.override_injected_dependency(('flavor_logic', get_tenant_logic_mock())) + requests.delete = MagicMock() + + # when + response = self.app.delete('/v1/orm/flavors/flavor_id/tenants/{tenant_id}', expect_errors=True) + + # assert + self.assertEqual(response.status_int, 500) + + def test_delete_tenant_bad_request(self): + # given + global return_error + return_error = 2 + injector.override_injected_dependency(('flavor_logic', get_tenant_logic_mock())) + requests.delete = MagicMock() + + # when + response = self.app.delete('/v1/orm/flavors/flavor_id/tenants/{tenant_id}', expect_errors=True) + + # assert + # self.assertEqual(response.status_int, 404) + + +class ResponseMock: + def __init__(self, status_code=200, message=""): + self.status_code = status_code + self.message = message + + +def get_tenant_logic_mock(): + global tenant_logic_mock + tenant_logic_mock = MagicMock() + + if return_error == 0: + tenant_logic_mock.add_tenants.return_value = RET_TENANT_JSON + elif return_error == 1: + tenant_logic_mock.add_tenants.side_effect = SystemError() + tenant_logic_mock.delete_tenant.side_effect = SystemError() + else: + tenant_logic_mock.add_tenants.side_effect = ErrorStatus(status_code=404) + tenant_logic_mock.delete_tenant.side_effect = ErrorStatus(status_code=404) + + return tenant_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + return utils_mock + + +TENANT_JSON = { + "tenants": [ + "tenant1" + ] +} + +RET_TENANT_JSON = models.TenantWrapper(tenants=["tenant1", "tenant2"]) diff --git a/orm/services/flavor_manager/fms_rest/tests/simple_hook_mock.py b/orm/services/flavor_manager/fms_rest/tests/simple_hook_mock.py new file mode 100755 index 00000000..73b98ed1 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/simple_hook_mock.py @@ -0,0 +1,6 @@ +from pecan.hooks import PecanHook + + +class SimpleHookMock(PecanHook): + def before(self, state): + setattr(state.request, 'transaction_id', 'some_id') diff --git a/orm/services/flavor_manager/fms_rest/tests/test_authentication.py b/orm/services/flavor_manager/fms_rest/tests/test_authentication.py new file mode 100755 index 00000000..e6deb140 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/test_authentication.py @@ -0,0 +1,53 @@ +import mock +from fms_rest.tests import FunctionalTest +from pecan import conf + +from fms_rest.utils import authentication + + +class TestUtil(FunctionalTest): + + def setUp(self): + FunctionalTest.setUp(self) + self.mock_response = mock.Mock() + + @mock.patch('keystone_utils.tokens.TokenConf') + def test_get_token_conf(self, mock_TokenConf): + mock_TokenConf.return_value = 123 + token_conf = authentication.get_token_conf(conf) + self.assertEqual(token_conf, 123) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_token_valid(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', True) + mock_get_token_conf.return_value = 123 + mock_is_token_valid.return_value = True + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, True) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_token_invalid(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', True) + mock_get_token_conf.return_value = 123 + mock_is_token_valid.return_value = False + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, False) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_disabled(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', False) + mock_get_token_conf.return_value = 123 + mock_is_token_valid.return_value = False + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, True) + + @mock.patch('keystone_utils.tokens.is_token_valid') + @mock.patch('keystone_utils.tokens.TokenConf') + def test_check_permissions_is_token_valid_breaks(self, mock_get_token_conf, mock_is_token_valid): + setattr(conf.authentication, 'enabled', True) + mock_is_token_valid.side_effect = Exception('boom') + is_permitted = authentication.check_permissions(conf, 'asher', 0) + self.assertEqual(is_permitted, False) diff --git a/orm/services/flavor_manager/fms_rest/tests/test_configuration.py b/orm/services/flavor_manager/fms_rest/tests/test_configuration.py new file mode 100755 index 00000000..c5d03376 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/test_configuration.py @@ -0,0 +1,15 @@ +"""Get configuration module unittests.""" +from fms_rest.controllers.v1.orm import configuration +from fms_rest.tests import FunctionalTest +from mock import patch + + +class TestGetConfiguration(FunctionalTest): + """Main get configuration test case.""" + + @patch('orm_common.utils.utils.report_config') + def test_get_configuration_success(self, mock_report): + """Test get_configuration returns the expected value on success.""" + mock_report.return_value = '12345' + response = self.app.get('/v1/orm/configuration') + self.assertEqual(response.json, '12345') diff --git a/orm/services/flavor_manager/fms_rest/tests/test_models.py b/orm/services/flavor_manager/fms_rest/tests/test_models.py new file mode 100755 index 00000000..21cda170 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/test_models.py @@ -0,0 +1,41 @@ +import mock +from fms_rest.tests import FunctionalTest + +from fms_rest.data.wsme import models + +GROUP_REGIONS = [ + "DPK", + "SNA1", + "SNA2" +] + + +class TestModels(FunctionalTest): + + def setUp(self): + FunctionalTest.setUp(self) + models.get_regions_of_group = mock.MagicMock(return_value=GROUP_REGIONS) + models.set_utils_conf = mock.MagicMock() + + def test_handle_group_success(self): + flavor = get_flavor_model() + flavor.handle_region_groups() + + self.assertEqual(len(flavor.regions), 3) + + def test_handle_group_not_found(self): + models.get_regions_of_group = mock.MagicMock(return_value=None) + flavor = get_flavor_model() + + self.assertRaises(ValueError, flavor.handle_region_groups,) + + +def get_flavor_model(): + """ + this function create a customer model object for testing + :return: new customer object + """ + + flavor = models.Flavor(id='1', regions=[models.Region(name='r1', type='group')]) + + return flavor diff --git a/orm/services/flavor_manager/fms_rest/tests/test_rds_proxy.py b/orm/services/flavor_manager/fms_rest/tests/test_rds_proxy.py new file mode 100755 index 00000000..53708c64 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/test_rds_proxy.py @@ -0,0 +1,45 @@ +import mock +from fms_rest.tests import FunctionalTest +from fms_rest import proxies +from fms_rest.data.sql_alchemy import db_models +from fms_rest.logic.error_base import ErrorStatus +from testfixtures import log_capture, compare + + +class Response: + def __init__(self, status_code, content): + self.status_code = status_code + self.content = content + + def json(self): + return self.content + + +class TestUtil(FunctionalTest): + + @mock.patch.object(proxies.rds_proxy, 'request') + @mock.patch('requests.post') + @log_capture('fms_rest.proxies.rds_proxy') + def test_send_good(self, mock_post, mock_request, l): + resp = Response(200, 'my content') + mock_post.return_value = resp + send_res = proxies.rds_proxy.send_flavor(db_models.Flavor().todict(), "1234", "post") + # self.assertRegexpMatches(l.records[-2].getMessage(), 'Wrapper JSON before sending action') + # self.assertRegexpMatches(l.records[-1].getMessage(), 'return from rds server status code') + + @mock.patch('requests.post') + @log_capture('fms_rest.proxies.rds_proxy') + def test_bad_status(self, mock_post, l): + resp = Response(400, 'my content') + mock_post.return_value = resp + # self.assertRegexpMatches(l.records[-2].getMessage(), 'Wrapper JSON before sending action') + # self.assertRegexpMatches(l.records[-1].getMessage(), 'return from rds server status code') + + @mock.patch('requests.post') + @log_capture('fms_rest.proxies.rds_proxy') + def test_no_content(self, mock_post, l): + resp = Response(200, None) + mock_post.return_value = resp + # self.assertRaises(ErrorStatus, proxies.rds_proxy.send_flavor, db_models.Flavor(), "1234") + for r in l.records: + self.assertNotRegexpMatches(r.getMessage(), 'return from rds server status code') diff --git a/orm/services/flavor_manager/fms_rest/tests/test_utils.py b/orm/services/flavor_manager/fms_rest/tests/test_utils.py new file mode 100755 index 00000000..a56e0953 --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/tests/test_utils.py @@ -0,0 +1,14 @@ +import json +from wsme.exc import ClientSideError + + +def get_error(transaction_id, status_code, error_details=None, + message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) diff --git a/orm/services/flavor_manager/fms_rest/utils/__init__.py b/orm/services/flavor_manager/fms_rest/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/flavor_manager/fms_rest/utils/authentication.py b/orm/services/flavor_manager/fms_rest/utils/authentication.py new file mode 100644 index 00000000..99cb1c4b --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/utils/authentication.py @@ -0,0 +1,55 @@ +import logging + +from keystone_utils import tokens +from orm_common.policy import policy +from orm_common.utils import api_error_utils as err_utils + +from pecan import conf +logger = logging.getLogger(__name__) + + +def authorize(request, action): + if not _is_authorization_enabled(conf): + return + + auth_region = request.headers.get('X-Auth-Region') + policy.authorize(action, request, conf) + + +def _is_authorization_enabled(app_conf): + return app_conf.authentication.enabled + + +def get_token_conf(app_conf): + mech_id = app_conf.authentication.mech_id + mech_password = app_conf.authentication.mech_pass + rms_url = app_conf.authentication.rms_url + tenant_name = app_conf.authentication.tenant_name + keystone_version = app_conf.authentication.keystone_version + conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name, + keystone_version) + return conf + + +def check_permissions(app_conf, token_to_validate, lcp_id): + logger.debug("Check permissions...start") + try: + if _is_authorization_enabled(app_conf): + if token_to_validate is not None and lcp_id is not None and str(token_to_validate).strip() != '' and str(lcp_id).strip() != '': + token_conf = get_token_conf(app_conf) + logger.debug("Authorization: validating token=[{}] on lcp_id=[{}]".format(token_to_validate, lcp_id)) + is_permitted = tokens.is_token_valid(token_to_validate, lcp_id, token_conf) + logger.debug("Authorization: The token=[{}] on lcp_id=[{}] is [{}]" + .format(token_to_validate, lcp_id, "valid" if is_permitted else "invalid")) + else: + raise Exception("Token=[{}] and/or Region=[{}] are empty/none.".format(token_to_validate, lcp_id)) + else: + logger.debug("The authentication service is disabled. No authentication is needed.") + is_permitted = True + except Exception as e: + msg = "Fail to validate request. due to {}.".format(e.message) + logger.error(msg) + logger.exception(e) + is_permitted = False + logger.debug("Check permissions...end") + return is_permitted diff --git a/orm/services/flavor_manager/fms_rest/utils/utils.py b/orm/services/flavor_manager/fms_rest/utils/utils.py new file mode 100755 index 00000000..3a02d50b --- /dev/null +++ b/orm/services/flavor_manager/fms_rest/utils/utils.py @@ -0,0 +1,130 @@ +from pecan import conf +import time +from orm_common.injector import injector + +from fms_rest.logger import get_logger + +logger = get_logger(__name__) + +di = injector.get_di() + + +@di.dependsOn('requests') +def make_uuid(): + """ function to request new uuid from uuid_generator rest service + returns uuid string + """ + + requests = di.resolver.unpack(make_uuid) + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + + try: + resp = requests.post(url, verify=conf.verify) + except requests.exceptions.ConnectionError as exp: + nagios = 'CON{}UUIDGEN001'.format(conf.server.name.upper()) + logger.critical( + 'CRITICAL|{}|Failed in make_uuid: connection error: {}'.format( + nagios, str(exp))) + exp.message = 'connection error: Failed to get uuid: unable to connect to server' + raise + except Exception as e: + logger.info('Failed in make_uuid:' + str(e)) + raise Exception('Failed in make_uuid:' + str(e)) + + resp = resp.json() + return resp['uuid'] + + +@di.dependsOn('requests') +def create_existing_uuid(uuid): + """ function to request new uuid from uuid_generator rest service + returns uuid string + """ + + requests = di.resolver.unpack(make_uuid) + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + + try: + logger.debug('Creating UUID: {}, using URL: {}'.format(uuid, url)) + resp = requests.post(url, data={'uuid': uuid}, verify=conf.verify) + except requests.exceptions.ConnectionError as exp: + nagios = 'CON{}UUIDGEN001'.format(conf.server.name.upper()) + logger.critical( + 'CRITICAL|{}|Failed in create_existing_uuid: connection error: {}'.format( + nagios, str(exp))) + exp.message = 'connection error: Failed to get uuid: unable to connect to server' + raise + except Exception as e: + logger.info('Failed in make_uuid:' + str(e)) + return None + + if resp.status_code == 400: + raise TypeError('duplicate key for uuid') + resp = resp.json() + return resp['uuid'] + + +@di.dependsOn('requests') +def make_transid(): + """ function to request new uuid of transaction type from uuid_generator rest service + returns uuid string + """ + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + requests = di.resolver.unpack(make_uuid) + + try: + resp = requests.post(url, data={'uuid_type': 'transaction'}, + verify=conf.verify) + except requests.exceptions.ConnectionError as exp: + nagios = 'CON{}UUIDGEN001'.format(conf.server.name.upper()) + logger.critical( + 'CRITICAL|{}|Failed in make_transid: connection error: {}'.format( + nagios, str(exp))) + exp.message = 'connection error: Failed to get uuid: unable to connect to server' + raise + except Exception as e: + logger.info('Failed in make_transid:' + str(e)) + raise Exception('Failed in make_transid:' + str(e)) + + resp = resp.json() + if 'uuid' in resp: + return resp['uuid'] + else: + return None + +audit_setup = False + + +@di.dependsOn('audit_client') +def audit_trail(cmd, transaction_id, headers, resource_id, event_details=''): + """ function to send item to audit trail rest api + returns 200 for ok and None for error + """ + audit = di.resolver.unpack(audit_trail) + + global audit_setup, audit_server_url + if not audit_setup: + audit_server_url = '%s%s' % (conf.api.audit_server.base, conf.api.audit_server.trans) + num_of_send_retries = 3 + time_wait_between_retries = 1 + audit.init(audit_server_url, num_of_send_retries, + time_wait_between_retries, conf.server.name.upper()) + audit_setup = True + + try: + timestamp = long(round(time.time() * 1000)) + application_id = headers['X-RANGER-Client'] if 'X-RANGER-Client' in headers else 'NA' + tracking_id = headers['X-RANGER-Tracking-Id'] if 'X-RANGER-Tracking-Id' in headers else transaction_id + # transaction_id is function argument + transaction_type = cmd + # resource_id is function argument + service_name = conf.server.name.upper() + user_id = headers['X-RANGER-Requester'] if 'X-RANGER-Requester' in headers else '' + external_id = 'NA' + audit.audit(timestamp, application_id, tracking_id, transaction_id, transaction_type, + resource_id, service_name, user_id, external_id, event_details) + except Exception as e: + logger.log_exception('Failed in audit_trail(cmd=%s, id=%s) url: %s' % (cmd, id, audit_server_url), e) + raise e + + return 200 diff --git a/orm/services/flavor_manager/pycharm_init.py b/orm/services/flavor_manager/pycharm_init.py new file mode 100755 index 00000000..469a65d9 --- /dev/null +++ b/orm/services/flavor_manager/pycharm_init.py @@ -0,0 +1,3 @@ +from pecan.commands import CommandRunner +runner = CommandRunner() +runner.run(['serve', 'config.py']) diff --git a/orm/services/flavor_manager/run_pecan.py b/orm/services/flavor_manager/run_pecan.py new file mode 100644 index 00000000..4d1ce01d --- /dev/null +++ b/orm/services/flavor_manager/run_pecan.py @@ -0,0 +1,6 @@ +''' +' this script is running the pecan web server inside ide so we can set break points in the code and debug our code +''' +from pecan.commands import CommandRunner +runner = CommandRunner() +runner.run(['serve', 'config.py']) diff --git a/orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_create_db.sql b/orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_create_db.sql new file mode 100755 index 00000000..9ebd7f5f --- /dev/null +++ b/orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_create_db.sql @@ -0,0 +1,102 @@ +create database if not exists orm_fms_db DEFAULT CHARACTER SET utf8 COLLATE utf8_bin; +use orm_fms_db; + +#***** +#* MySql script for Creating Table Flavor +#***** + +create table if not exists flavor + ( + internal_id bigint auto_increment not null, + id varchar(64) not null, + name varchar(250) not null, + alias varchar(64) null, + description varchar(100) not null, + series enum('ns', 'nd', 'nv', 'gv', 'ss') not null, + ram integer not null, + vcpus integer not null, + disk integer not null, + swap integer not null, + ephemeral integer not null, + visibility varchar(10) not null, + primary key (internal_id), + unique id (id), + index series (series), + index visibility (visibility), + unique name_idx (name) + ); +# + + +#***** +#* MySql script for Creating Table flavor_extra_spec +#***** + +create table if not exists flavor_extra_spec + ( + flavor_internal_id bigint not null, + key_name varchar(64) not null, + key_value varchar(64) not null, + foreign key (flavor_internal_id) references flavor(internal_id) ON DELETE CASCADE ON UPDATE NO ACTION, + primary key (flavor_internal_id,key_name) + ); +# + + +#***** +#* MySql script for Creating Table flavor_region +#***** + +create table if not exists flavor_region + ( + flavor_internal_id bigint not null, + region_name varchar(64) not null, + region_type varchar(32) not null, + foreign key (flavor_internal_id) references flavor(internal_id) ON DELETE CASCADE ON UPDATE NO ACTION, + primary key (flavor_internal_id,region_name) + ); +# + + +#***** +#* MySql script for Creating Table flavor_tenant +#***** + +create table if not exists flavor_tenant + ( + flavor_internal_id bigint not null, + tenant_id varchar(64) not null, + foreign key (flavor_internal_id) references flavor(internal_id) ON DELETE CASCADE ON UPDATE NO ACTION, + primary key (flavor_internal_id,tenant_id)); +# + + +#***** +#* MySql script for Creating Table flavor_tag +#***** + +create table if not exists flavor_tag + ( + flavor_internal_id bigint not null, + key_name varchar(64) not null, + key_value varchar(64) not null, + foreign key (flavor_internal_id) references flavor(internal_id) ON DELETE CASCADE ON UPDATE NO ACTION, + primary key (flavor_internal_id,key_name) + ); +# + + +#***** +#* MySql script for Creating Table flavor_option +#***** + +create table if not exists flavor_option + ( + flavor_internal_id bigint not null, + key_name varchar(64) not null, + key_value varchar(64) not null, + foreign key (flavor_internal_id) references flavor(internal_id) ON DELETE CASCADE ON UPDATE NO ACTION, + primary key (flavor_internal_id,key_name) + ); +# + diff --git a/orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_update_db.sql b/orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_update_db.sql new file mode 100755 index 00000000..b3da1f01 --- /dev/null +++ b/orm/services/flavor_manager/scripts/db_scripts/aic_orm_fms_update_db.sql @@ -0,0 +1,26 @@ +USE orm_fms_db; + +DELIMITER ;; + +DROP PROCEDURE IF EXISTS add_regoion_type ;; +CREATE PROCEDURE add_regoion_type() +BEGIN + + -- add a column safely + IF NOT EXISTS( (SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() + AND COLUMN_NAME='region_type' AND TABLE_NAME='flavor_region') ) THEN + ALTER TABLE flavor_region ADD region_type varchar(32) NOT NULL DEFAULT 'single'; + END IF; + + IF NOT EXISTS( (SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() + AND COLUMN_NAME='alias' AND TABLE_NAME='flavor') ) THEN + ALTER TABLE flavor ADD alias varchar(64) NULL; + END IF; + +END ;; + +CALL add_regoion_type() ;; +ALTER TABLE `flavor` CHANGE COLUMN `name` `name` VARCHAR(250) NOT NULL;; + +DELIMITER ; + diff --git a/orm/services/flavor_manager/scripts/shell_scripts/create_db.sh b/orm/services/flavor_manager/scripts/shell_scripts/create_db.sh new file mode 100644 index 00000000..2cf28dba --- /dev/null +++ b/orm/services/flavor_manager/scripts/shell_scripts/create_db.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +echo Creating database: orm_rds +echo Creating table: resource_status + +mysql -uroot -pstack < ../db_scripts/aic_orm_fms_create_db.sql + +echo Done ! diff --git a/orm/services/flavor_manager/swagger/swagger.yaml b/orm/services/flavor_manager/swagger/swagger.yaml new file mode 100644 index 00000000..4a7ab70e --- /dev/null +++ b/orm/services/flavor_manager/swagger/swagger.yaml @@ -0,0 +1,748 @@ +# this is an example of the Uber API +# as a demonstration of an API spec in YAML +swagger: '2.0' + +info: + version: 3.5.0 + title: Flavors API + description: Flavors api's + All api's should supply two header parameters + X-Auth-Token - Token received from keystone + X-Auth-Region - The region + There is an optional header parameter X-RANGER-Client which tells who is the client for the api + + contact: + email: zb593m@att.com + +# the domain of the service +host: 135.76.2.229 +# array of all schemes that your API supports +schemes: + - https +# will be prefixed to all paths +basePath: /v1/orm +produces: + - application/json + +paths: + /flavors: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + + post: + summary: create a new flavor + description: | + The post flavors endpoint create a new flavor and send it to the RDS which send it to Heat to create this flavor in each region needed + Return all data of the new flavor + + parameters: + - name: full_flavor + in: body + description: Full flavor to create. + schema: + $ref: "#/definitions/Flavor" + required: true + x-example_1: + { + "name": "zion" + } + + tags: + - Flavors + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/Flavor' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + get: + summary: get a list of flavors by criteria (visibility, region, tenant, series, start_with, contains, alias) + description: | + The get flavors endpoint retrieve all flavors matched to the above criterias + parameters: + - name: visibility + in: query + type: "string" + enum: [ + "public", + "private" + ] + description: public or private flavor + - name: region + in: query + type: "string" + description: region name. + required: false + - name: tenant + in: query + type: "string" + description: tenant name. + required: false + - name: series + in: query + type: "string" + description: series code + required: false + - name: start_with + in: query + type: "string" + description: name start with + required: false + - name: contains + in: query + type: "string" + description: name contains + required: false + - name: alias + in: query + type: "string" + description: alias of flavor. + required: false + tags: + - Flavors + responses: + 200: + description: List of flavors matched all criteria + schema: + $ref: '#/definitions/Flavors' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid_or_name}: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + get: + summary: get a flavor by uuid or name + description: | + The get flavors endpoint retrieve a specipic flavor by uuid or name, you get the uuid and the name from the result of creation of this flavor or by using list api + parameters: + - name: flavor_uuid_or_name + in: path + type: string + description: uuid or name of the requested flavor. + required: true + tags: + - Flavors + responses: + 200: + description: The requested flavor + schema: + $ref: '#/definitions/Flavor' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + delete: + summary: delete a flavor by uuid or name + description: | + The delete flavors endpoint delete a specipic flavor by uuid or name, you get the uuid and the name from the result of creation of this flavor or by using list api + parameters: + - name: flavor_uuid_or_name + in: path + type: string + description: uuid or name of the requested flavor. + required: true + tags: + - Flavors + responses: + 204: + description: Flavor deleted + schema: + $ref: '#/definitions/Flavor' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/regions: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + post: + summary: add new regions to flavor + description: | + The post regions inflavors endpoint add one or more regions to the already existing regions for this flavor + Return the full flavor after adding regions + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + - name: list_of_regions + in: body + schema: + $ref: '#/definitions/RegionsWrapper' + description: list of regions to add to a flavor. + required: true + tags: + - Regions + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/Flavor' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/regions/{region_name}: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + delete: + summary: delete region from flavor + description: | + The delete region from flavor endpoint delete a region from flavor + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + - name: region_name + in: path + type: string + description: region name + required: true + tags: + - Regions + + responses: + 201: + description: The region was deleted from flavor + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/tags: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + get: + summary: get tags of flavor + description: | + The get tags of flavor endpoint get all tags related to supplied flavor + Return list of flavor tags + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + tags: + - Tags + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/TagsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + post: + summary: add new tags to flavor + description: | + The post tags inflavors endpoint add one or more tags to the already existing tags for this flavor + Return list of flavor tags adding tags + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + + - name: list_of_tags + in: body + schema: + $ref: '#/definitions/TagsWrapper' + description: list of tagss to add to the flavor. + required: true + tags: + - Tags + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/TagsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + put: + summary: replace flavors tag by the supplied list + description: | + The put tags inflavors endpoint replace existing tags by new tags suplied + Return list of flavor tags after replacing tags + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + + - name: list_of_tags + in: body + schema: + $ref: '#/definitions/TagsWrapper' + description: list of tagss to add to the flavor. + required: true + tags: + - Tags + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/TagsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/tagss/{tag_name}: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + delete: + summary: delete specific tag by name from flavor + description: | + The delete tag from flavor endpoint delete a tag from flavor + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + - name: tag_name + in: path + type: string + description: tag name to delete + required: true + tags: + - Tags + + responses: + 201: + description: The region was deleted from flavor + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/os_extra_spec: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + get: + summary: get extra specs of flavor + description: | + The get extra specs of flavor endpoint get all extra specs related to supplied flavor + Return list of flavor extra specs + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + tags: + - ExtraSpecs + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/ExtraSpecsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + post: + summary: add new extra specs to flavor + description: | + The post extra specs inflavors endpoint add one or more extra specs to the already existing extra specs for this flavor + Return list of flavor extra specs after adding extra specs + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + + - name: list_of_extra_specs + in: body + schema: + $ref: '#/definitions/ExtraSpec' + description: list of extra specs to add to the flavor. + required: true + tags: + - ExtraSpecs + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/ExtraSpecsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + put: + summary: replace flavors extra specs by the supplied list + description: | + The put extra specs to flavor endpoint replace existing extra specs by new extra specs suplied + Return list of flavor extra specs after replacing tags + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + + - name: list_of_extra_specs + in: body + schema: + $ref: '#/definitions/ExtraSpecsWrapper' + description: list of extra specs to replace for the flavor. + required: true + tags: + - ExtraSpecs + + responses: + 200: + description: The new created flavor + schema: + $ref: '#/definitions/TagsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/os_extra_spec/{extra_spec_name}: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + delete: + summary: delete specific extra spec by name from flavor + description: | + The delete extra spec from flavor endpoint delete a extra spec from flavor + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + - name: extra_spec_name + in: path + type: string + description: extra sepc name to delete + required: true + tags: + - ExtraSpecs + + responses: + 201: + description: The extra spec was deleted from flavor + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/tenants: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + post: + summary: add new tenants to flavor + description: | + The post tenants to flavor endpoint add one or more tenants to the already existing tenants for this flavor + Return list of flavor tenants after adding tenants + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + - name: tenants_list + in: body + schema: + $ref: '#/definitions/TenantsWrapper' + description: list of tenants to add to the flavor. + required: true + tags: + - Tenants + + responses: + 200: + description: list of current flavor tenants after adding tenant/s + schema: + $ref: '#/definitions/TenantsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /flavors/{flavor_uuid}/tenants/{tenant_name}: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + delete: + summary: delete specific tenant by name from flavor + description: | + The delete tenant from flavor endpoint delete a tenant from flavor + + parameters: + - name: flavor_uuid + in: path + type: string + description: flavor uuid + required: true + - name: tenant_name + in: path + type: string + description: tenant name to delete + required: true + tags: + - Tenants + + responses: + 201: + description: The region was deleted from flavor + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' +definitions: + Flavor: + type: object + required: + - series + - ram + - vcpus + - disk + - visibility + properties: + id: + type: string + description: id + name: + type: string + description: name + alias: + type: string + description: alias + description: + type: string + description: description + example: A standard 2GB Ram 2 vCPUs 50GB Disk, Flavor + series: + type: string + description: series + example: nd + ram: + type: string + description: ram + example: 20 + vcpus: + type: string + description: vcpus + example: 8 + disk: + type: string + description: disk size + example: 2048 + swap: + type: string + description: swap + example: 0 + ephemeral: + type: string + description: ephemeral + example: 11 + regions: + type: array + items: + $ref: '#/definitions/RegionsInput' + visibility: + type: string + description: visibility 'public' or 'private' + example: public + tenants: + type: array + items: + type: string + status: + type: string + description: status + tags: + $ref: '#/definitions/Tags' + options: + $ref: '#/definitions/Dictionary' + extra_specs: + $ref: '#/definitions/ExtraSpec' + + Flavors: + type: array + items: + $ref: '#/definitions/Flavor' + + RegionInput: + type: object + required: + - name + properties: + name: + type: string + example: mtn13 + type: + type: string + description: single or group + example: single + default: single + + RegionsInput: + type: array + items: + $ref: '#/definitions/RegionInput' + + RegionOutput: + type: object + properties: + name: + type: string + example: mtn13 + type: + type: string + description: single or group + example: single + default: single + status: + type: string + error_message: + type: string + + RegionsOutput: + type: array + items: + $ref: '#/definitions/RegionOutput' + + RegionsWrapper: + type: object + properties: + regions: + $ref: '#/definitions/RegionsInput' + + Tags: + $ref: '#/definitions/Dictionary' + + TagsWrapper: + type: object + properties: + tags: + $ref: '#/definitions/Tags' + + ExtraSpec: + $ref: '#/definitions/Dictionary' + + ExtraSpecsWrapper: + type: object + properties: + os_extra_specs: + $ref: '#/definitions/Dictionary' + extra_specs: + $ref: '#/definitions/Dictionary' + + Tenant: + type: string + example: 4f7b9561-af8b-4cc0-87e2-319270dad49e + + Tenants: + type: array + items: + $ref: '#/definitions/Tenant' + + TenantsWrapper: + type: object + properties: + tenants: + $ref: '#/definitions/Tenants' + + Dictionary: + type: object + additionalProperties: + type: "string" + + Error: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + +parameters: + Token: + name: X-Auth-Token + in: header + description: Token from keystone + required: true + type: string + Region: + name: X-Auth-Region + in: header + description: Region + required: true + type: string + Client: + name: X-RANGER-Client + in: header + description: Client name + required: false + type: string + diff --git a/orm/services/flavor_manager/tox.ini b/orm/services/flavor_manager/tox.ini new file mode 100755 index 00000000..b550d1ba --- /dev/null +++ b/orm/services/flavor_manager/tox.ini @@ -0,0 +1,23 @@ +[tox] +envlist=py27,cover + +[testenv] +setenv= FMS_ENV=mock + PYTHONPATH={toxinidir}:{toxinidir}/fms_rest/external_mock/ +deps= -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + + +[testenv:pep8] +commands = +# pip install git+ssh://jenkins@gerrit.mtn5.cci.att.com:29418/aic-orm-common@dev +# pip install ../aic-orm-common + py.test --pep8 -m pep8 + +[testenv:cover] +commands= +# pip install git+ssh://jenkins@gerrit.mtn5.cci.att.com:29418/aic-orm-common@dev +# pip install ../aic-orm-common + coverage run setup.py test + coverage report + coverage html --omit=fms_rest/data/sql_alchemy/*,fms_rest/utils/utils.py,.tox/* diff --git a/orm/services/id_generator/__init__.py b/orm/services/id_generator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/MANIFEST.in b/orm/services/image_manager/MANIFEST.in new file mode 100644 index 00000000..c922f11a --- /dev/null +++ b/orm/services/image_manager/MANIFEST.in @@ -0,0 +1 @@ +recursive-include public * diff --git a/orm/services/image_manager/__init__.py b/orm/services/image_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/config.py b/orm/services/image_manager/config.py new file mode 100755 index 00000000..c2cd1d53 --- /dev/null +++ b/orm/services/image_manager/config.py @@ -0,0 +1,122 @@ +from orm_common.hooks.api_error_hook import APIErrorHook +from orm_common.hooks.security_headers_hook import SecurityHeadersHook +from ims.hooks.service_hooks import TransIdHook + +global TransIdHook +global APIErrorHook +global SecurityHeadersHook + +# Server Specific Configurations +server = { + 'port': '8084', + 'host': '0.0.0.0', + 'name': 'ims' +} + +# Pecan Application Configurations +app = { + 'root': 'ims.controllers.root.RootController', + 'modules': ['ims'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/ims/templates', + 'debug': True, + 'errors': { + '__force_dict__': True + }, + 'hooks': lambda: [TransIdHook(), APIErrorHook(), SecurityHeadersHook()] +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'ims': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], + 'propagate': False}, + 'audit_client': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], + 'propagate': False}, + 'orm_common': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], + 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], + 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + }, + 'Logfile': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'maxBytes': 50000000, + 'backupCount': 10, + 'filename': '/opt/app/orm/ims/ims.log', + 'formatter': 'simple' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +database = { + 'host': 'localhost', + 'username': 'root', + 'password': 'stack', + 'db_name': 'orm_ims_db', + +} + +verify = False + +database['connection_string'] = 'mysql://{0}:{1}@{2}:3306/{3}'.format(database['username'], + database['password'], + database['host'], + database['db_name']) + +application_root = 'http://localhost:{0}'.format(server['port']) + +api = { + 'uuid_server': { + 'base': 'http://127.0.0.1:8090/', + 'uuids': 'v1/uuids' + }, + 'rds_server': { + 'base': 'http://127.0.0.1:8777/', + 'resources': 'v1/rds/resources', + 'status': 'v1/rds/status/resource/' + }, + 'rms_server': { + 'base': 'http://127.0.0.1:8080/', + 'groups': 'v2/orm/groups', + 'regions': 'v2/orm/regions', + 'cache_seconds': 60 + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } + +} + +authentication = { + "enabled": True, + "mech_id": "admin", + "mech_pass": "stack", + "rms_url": "http://127.0.0.1:8080", + "tenant_name": "admin", + "token_role": "admin", + # The Keystone version currently in use. Can be either "2.0" or "3" + "keystone_version": "2.0", + "policy_file": "/opt/app/orm/aic-orm-ims/ims/etc/policy.json" +} diff --git a/orm/services/image_manager/data_manager_test.py b/orm/services/image_manager/data_manager_test.py new file mode 100755 index 00000000..6fa2cfa7 --- /dev/null +++ b/orm/services/image_manager/data_manager_test.py @@ -0,0 +1,229 @@ +from ims.logger import get_logger +import traceback + +from ims.persistency.sql_alchemy.data_manager import DataManager + + +# conf = imp.load_source('config.py', '../config.py') + +from pecan.testing import load_test_app +import os +from pecan import conf +from ims.persistency.sql_alchemy.db_models import Image, ImageProperty, ImageRegion, ImageCustomer + +image_id = "Id 11" # image id + +LOG = get_logger(__name__) + + +def main(): + try: + # prepare_service() + + print conf.database + + data_manager = DataManager() + + image_record = data_manager.get_record("Image") + + image = image_record.get_image(image_id) + + print image.regions + print image.properties + + all_images = image_record.get_all_images(start=0, limit=50) + print all_images + + # LOG.debug("TestDatabase finished well") + + except Exception as exception: + print("Exception" + str(exception)) + # LOG.error("Exception in TestDatabase: " + str(exception)) + + +def delete(): + try: + # prepare_service() + + print conf.database + + data_manager = DataManager() + + image_record = data_manager.get_record("Image") + + data_manager.begin_transaction() + + result = image_record.delete_by_id(image_id) + + data_manager.commit() + print "Nm records deleted: " + str(result.rowcount) + # LOG.debug("TestDatabase finished well") + + except Exception as exception: + print("Exception" + str(exception)) + + # LOG.error("Exception in TestDatabase: " + str(exception)) + + +def main2(): + # get customer by id of 1 + try: + # prepare_service() + + data_manager = DataManager() + + image_record = data_manager.get_record("Image") + + criterias = {"visibility": "public", "region": "North", "tenant": "Tenanat-1", "start": 0, "limit": 10, + "profile": "NS"} + images = image_record.get_images_by_criteria(**criterias) + + print len(images) + + except Exception as exception: + LOG.log_exception("Failed to get_images_by_criteria: ", exception) + # log_exception(LOG, "Failed to read customer: 10", exception) + # log_exception("Failed to read customer: 10", exception) + # LOG.error("Exception in TestDatabase: " + str(exception)) + # print_exception(exception) + + +def main3(): + try: + # prepare_service() + + print conf.database + + data_manager = DataManager() + + image_record = data_manager.get_record("Image") + + image = image_record.get_image(image_id) + + print image.image_extra_specs + print image.image_regions + print image.image_tenants + + region = ImageRegion(region_name="Israel") + image.add_region(region) + + region = ImageRegion(region_name="Israel2") + image.add_region(region) + + tenant = ImageCustomer(tenant_id="Zion") + image.add_tenant(tenant) + + tenant = ImageCustomer(tenant_id="Zion2") + image.add_tenant(tenant) + + data_manager.commit() + + # LOG.debug("TestDatabase finished well") + + except Exception as exception: + print("Exception" + str(exception)) + # LOG.error("Exception in TestDatabase: " + str(exception)) + + +def main4(): + try: + # prepare_service() + + print conf.database + + data_manager = DataManager() + + image_record = data_manager.get_record("Image") + + image = image_record.get_image(image_id) + + print image.image_extra_specs + print image.image_regions + print image.image_tenants + + image.remove_region("Israel") + image.remove_region("Israel2") + + image.remove_tenant("Zion") + image.remove_tenant("Zion2") + + data_manager.commit() + + # LOG.debug("TestDatabase finished well") + + except Exception as exception: + print("Exception" + str(exception)) + # LOG.error("Exception in TestDatabase: " + str(exception)) + + +def insert_data(): + try: + # prepare_service() + + print conf.database + + data_manager = DataManager() + + image_record = data_manager.get_record("Image") + + image_property1 = ImageProperty(key_name="key_1", key_value="key_valu1") + image_property2 = ImageProperty(key_name="key_2", key_value="key_valu2") + image_property3 = ImageProperty(key_name="key_3", key_value="key_valu3") + + image_region1 = ImageRegion(region_name="region1", region_type="single") + image_region2 = ImageRegion(region_name="region2", region_type="single") + + image = Image(name="Name1", + id="Id 10", + enabled=1, + protected="protected", + url="Http:\\zion.com", + visibility="puplic", + disk_format="disk format", + container_format="container_format", + min_disk=512, + owner="zion", + schema="big_data", + min_ram=1) + + image.properties.append(image_property1) + image.properties.append(image_property2) + image.properties.append(image_property3) + image.regions.append(image_region1) + image.regions.append(image_region2) + image_record.insert(image) + + data_manager.commit() + + # LOG.debug("TestDatabase finished well") + + except Exception as exception: + print("Exception" + str(exception)) + # LOG.error("Exception in TestDatabase: " + str(exception)) + + +def print_exception(): + try: + print "*** print_exc:" + traceback.print_exc() + print "*** format_exception:" + print traceback.format_exc() + print "*** extract_tb:" + print traceback.extract_tb() + print "*** format_tb:" + print traceback.format_tb() + except Exception as exception1: + print "*** print_exc:" + traceback.print_exc() + + +if __name__ == "__main__": + app = load_test_app(os.path.join( + os.path.dirname(__file__), + './config.py' + )) + + # main() + insert_data() + delete() + # main4() diff --git a/orm/services/image_manager/ims.conf b/orm/services/image_manager/ims.conf new file mode 100644 index 00000000..35d71ad2 --- /dev/null +++ b/orm/services/image_manager/ims.conf @@ -0,0 +1,26 @@ +Listen 8084 + + + + WSGIDaemonProcess ims user=orm group=orm threads=5 + WSGIScriptAlias / /opt/app/orm/ims/ims.wsgi + + + Order deny,allow + Deny from all + Allow from localhost + + + + Order deny,allow + Deny from all + Allow from localhost + + + + WSGIProcessGroup ims + WSGIApplicationGroup %{GLOBAL} + Require all granted + Allow from all + + diff --git a/orm/services/image_manager/ims.wsgi b/orm/services/image_manager/ims.wsgi new file mode 100644 index 00000000..91e0f31a --- /dev/null +++ b/orm/services/image_manager/ims.wsgi @@ -0,0 +1,2 @@ +from pecan.deploy import deploy +application = deploy('/opt/app/orm/ims/config.py') diff --git a/orm/services/image_manager/ims/__init__.py b/orm/services/image_manager/ims/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/app.py b/orm/services/image_manager/ims/app.py new file mode 100755 index 00000000..df13bbe3 --- /dev/null +++ b/orm/services/image_manager/ims/app.py @@ -0,0 +1,36 @@ +from pecan import make_app +from pecan import conf +from ims.logger import get_logger +from orm_common.utils.utils import set_utils_conf +from pecan.commands import CommandRunner + +from ims.utils import authentication as auth +from orm_common.policy import policy + +import os +logger = get_logger(__name__) + + +def setup_app(config): + token_conf = auth._get_token_conf(config) + policy.init(config.authentication.policy_file, token_conf) + app_conf = dict(config.app) + + app = make_app( + app_conf.pop('root'), + logging=getattr(config, 'logging', {}), + **app_conf + ) + + set_utils_conf(conf) + + logger.info('Starting IMS...') + return app + + +def main(): + dir_name = os.path.dirname(__file__) + drive, path_and_file = os.path.splitdrive(dir_name) + path, filename = os.path.split(path_and_file) + runner = CommandRunner() + runner.run(['serve', path+'/config.py']) diff --git a/orm/services/image_manager/ims/controllers/__init__.py b/orm/services/image_manager/ims/controllers/__init__.py new file mode 100755 index 00000000..b601ad2a --- /dev/null +++ b/orm/services/image_manager/ims/controllers/__init__.py @@ -0,0 +1,10 @@ +"""Init package.""" +import os +from ims.logger import get_logger +from orm_common.injector import injector +import ims.di_providers as di_providers + +logger = get_logger(__name__) + +_current_dirname = os.path.dirname(os.path.realpath(di_providers.__file__)) +injector.register_providers('IMS_ENV', _current_dirname, logger) diff --git a/orm/services/image_manager/ims/controllers/root.py b/orm/services/image_manager/ims/controllers/root.py new file mode 100755 index 00000000..181cbbda --- /dev/null +++ b/orm/services/image_manager/ims/controllers/root.py @@ -0,0 +1,45 @@ +from pecan import conf +from pecan import rest +from pecan import expose, request + +from webob.exc import status_map +from pecan.secure import SecureController +from ims.controllers.v1.v1 import V1Controller +from ims.utils import authentication + + +class RootController(object): + v1 = V1Controller() + + @expose(template='json') + def get(self): + """ + Method to handle GET / + prameters: None + return: dict describing image command version information + """ + + return { + "versions": { + "values": [ + { + "orm": "stable", + "id": "v1", + "links": [ + { + "href": conf.application_root + } + ] + } + ] + } + } + + @expose('error.html') + def error(self, status): + try: + status = int(status) + except ValueError: # pragma: no cover + status = 500 + message = getattr(status_map.get(status), 'explanation', '') + return dict(status=status, message=message) diff --git a/orm/services/image_manager/ims/controllers/v1/__init__.py b/orm/services/image_manager/ims/controllers/v1/__init__.py new file mode 100755 index 00000000..d09c9979 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/__init__.py @@ -0,0 +1,9 @@ +# import os +# from orm_common.logger import get_logger +# #from orm_common.injector import injector +# import ims_rest.di_providers as di_providers +# +# logger = get_logger(__name__) +# +# _current_dirname = os.path.dirname(os.path.realpath(di_providers.__file__)) +# injector.register_providers('IMS_ENV', _current_dirname, logger) diff --git a/orm/services/image_manager/ims/controllers/v1/orm/__init__.py b/orm/services/image_manager/ims/controllers/v1/orm/__init__.py new file mode 100755 index 00000000..c3d63034 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/__init__.py @@ -0,0 +1 @@ +"""Init package.""" diff --git a/orm/services/image_manager/ims/controllers/v1/orm/configuration.py b/orm/services/image_manager/ims/controllers/v1/orm/configuration.py new file mode 100755 index 00000000..e30d4bee --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/configuration.py @@ -0,0 +1,29 @@ +"""Configuration rest API input module.""" + +import logging +from orm_common.utils import utils +from pecan import conf +from pecan import rest +from wsmeext.pecan import wsexpose + + +logger = logging.getLogger(__name__) + + +class ConfigurationController(rest.RestController): + """Configuration controller.""" + + @wsexpose(str, str, status_code=200) + def get(self, dump_to_log='false'): + """get method. + + :param dump_to_log: A boolean string that says whether the + configuration should be written to log + :return: A pretty string that contains the service's configuration + """ + logger.info("Get configuration...") + + dump = dump_to_log.lower() == 'true' + utils.set_utils_conf(conf) + result = utils.report_config(conf, dump, logger) + return result diff --git a/orm/services/image_manager/ims/controllers/v1/orm/images/__init__.py b/orm/services/image_manager/ims/controllers/v1/orm/images/__init__.py new file mode 100755 index 00000000..c3d63034 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/images/__init__.py @@ -0,0 +1 @@ +"""Init package.""" diff --git a/orm/services/image_manager/ims/controllers/v1/orm/images/base.py b/orm/services/image_manager/ims/controllers/v1/orm/images/base.py new file mode 100755 index 00000000..3032e9b7 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/images/base.py @@ -0,0 +1,49 @@ +import wsme + + +class ClientSideError(wsme.exc.ClientSideError): + def __init__(self, error, status_code=400): + super(ClientSideError, self).__init__(error, status_code) + + +class JsonError(wsme.exc.ClientSideError): + def __init__(self, status_code=400, message='incompatible JSON body'): + super(JsonError, self).__init__(message, status_code) + + +class AuthenticationHeaderError(ClientSideError): + def __init__(self, error, status_code=401, + message='Missing/expired/incorrect authentication header'): + super(AuthenticationHeaderError, self).__init__(message, status_code) + + +class AuthenticationFailed(ClientSideError): + def __init__(self, status_code=403, + message='The authenticated user is not allowed to create' + ' customers'): + super(AuthenticationFailed, self).__init__(message, status_code) + + +class NotFound(ClientSideError): + def __init__(self, status_code=404, message="Not Found"): + super(NotFound, self).__init__(message, status_code) + + +class NoContent(ClientSideError): + def __init__(self, status_code=204, message="Not Content"): + super(NoContent, self).__init__(message, status_code) + + +class BusyError(ClientSideError): + def __init__(self, status_code=409, message='Current resource is busy'): + super(BusyError, self).__init__(message, status_code) + + +error_strategy = { + '400': JsonError, + '401': AuthenticationHeaderError, + '403': AuthenticationFailed, + '404': NotFound, + '204': NoContent, + '409': BusyError +} diff --git a/orm/services/image_manager/ims/controllers/v1/orm/images/customers.py b/orm/services/image_manager/ims/controllers/v1/orm/images/customers.py new file mode 100755 index 00000000..4fc9aae8 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/images/customers.py @@ -0,0 +1,107 @@ +from pecan import rest, request +from pecan.core import abort +from wsmeext.pecan import wsexpose +from orm_common.injector import injector + +from ims.persistency.wsme.models import ImageWrapper, CustomerWrapper +from ims.logic.error_base import ErrorStatus +from orm_common.utils import api_error_utils as err_utils +from ims.utils import authentication as auth + +from ims.logger import get_logger + +LOG = get_logger(__name__) + +di = injector.get_di() + + +@di.dependsOn('image_logic') +@di.dependsOn('utils') +class CustomerController(rest.RestController): + + @wsexpose(ImageWrapper, str, body=CustomerWrapper, rest_content_types='json', status_code=201) + def post(self, image_id, cust_wrapper): + image_logic, utils = di.resolver.unpack(CustomerController) + auth.authorize(request, "tenant:create") + try: + LOG.info("CustomerController - add tenants: " + str(cust_wrapper)) + + result = image_logic.add_customers(image_id, cust_wrapper, request.transaction_id) + + LOG.info("CustomerController - tenants added: " + str(result)) + + event_details = 'Image {} tenants: {} added'.format( + image_id, cust_wrapper.customers) + utils.audit_trail('add tenants', request.transaction_id, + request.headers, image_id, + event_details=event_details) + return result + + except ErrorStatus as exception: + LOG.log_exception("TenantController - Failed to add tenants", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + except Exception as exception: + LOG.log_exception("TenantController - Failed to add tenants", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + @wsexpose(ImageWrapper, str, body=CustomerWrapper, rest_content_types='json', status_code=200) + def put(self, image_id, cust_wrapper): + image_logic, utils = di.resolver.unpack(CustomerController) + auth.authorize(request, "tenant:update") + try: + LOG.info("CustomerController - replace tenants: " + str(cust_wrapper)) + + result = image_logic.replace_customers(image_id, cust_wrapper, request.transaction_id) + + LOG.info("CustomerController - tenants replaced: " + str(result)) + + event_details = 'Image {} tenants: {} updated'.format( + image_id, cust_wrapper.customers) + utils.audit_trail('replace tenants', request.transaction_id, + request.headers, image_id, + event_details=event_details) + return result + + except ErrorStatus as exception: + LOG.log_exception("TenantController - Failed to replace tenants", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + except Exception as exception: + LOG.log_exception("TenantController - Failed to replace tenants", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + @wsexpose(None, str, str, rest_content_types='json', status_code=204) + def delete(self, image_id, cust_id): + image_logic, utils = di.resolver.unpack(CustomerController) + auth.authorize(request, "tenant:delete") + + try: + LOG.info("TenantController - delete tenant: " + str(cust_id)) + + result = image_logic.delete_customer(image_id, cust_id, request.transaction_id) + + LOG.info("TenantController - tenant deleted: " + str(result)) + + event_details = 'Image {} tenant {} deleted'.format( + image_id, cust_id) + utils.audit_trail('delete tenant', request.transaction_id, + request.headers, image_id, + event_details=event_details) + + except ErrorStatus as exception: + LOG.log_exception("TenantController - Failed to delete tenant", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + except Exception as exception: + LOG.log_exception("TenantController - Failed to delete tenant", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) diff --git a/orm/services/image_manager/ims/controllers/v1/orm/images/enabled.py b/orm/services/image_manager/ims/controllers/v1/orm/images/enabled.py new file mode 100755 index 00000000..93ea5c52 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/images/enabled.py @@ -0,0 +1,74 @@ +"""Status (activate/deactivate) Image rest API input module.""" + +from pecan import conf, rest, request +from wsmeext.pecan import wsexpose +from ims.logger import get_logger +from orm_common.injector import injector +from ims.logic.error_base import ErrorStatus +from ims.persistency.wsme.models import Enabled, ImageWrapper +from orm_common.utils import api_error_utils as err_utils +from ims.utils import authentication as auth + +di = injector.get_di() + +LOG = get_logger(__name__) + + +@di.dependsOn('image_logic') +@di.dependsOn('utils') +class EnabledController(rest.RestController): + """Status controller.""" + + @wsexpose(ImageWrapper, str, body=Enabled, rest_content_types='json', status_code=200) + def put(self, image_id, enabled): + image_logic, utils = di.resolver.unpack(EnabledController) + auth.authorize(request, "image:enable") + try: + LOG.info("EnabledController - received enabled = {}".format(enabled.enabled)) + result = image_logic.enable_image(image_id, enabled.enabled * 1, request.transaction_id) + status = "activated" + if not enabled.enabled: + status = "deactivated" + LOG.info("EnabledController - Image was successfully {}".format(status)) + + event_details = 'Image {} {}'.format( + image_id, 'active' if enabled.enabled else 'inactive') + utils.audit_trail('activate image', request.transaction_id, + request.headers, image_id, + event_details=event_details) + return result + + except ErrorStatus as exception: + LOG.log_exception("Failed in EnableImage", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("Failed in EnableImage", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + @wsexpose(None, str, body=Enabled, rest_content_types='json', + status_code=200) + def post(self, image_id, enabled): + image_logic, utils = di.resolver.unpack(EnabledController) + auth.authorize(request, "image:enable") + try: + LOG.debug("method not allowed only put allowed") + raise ErrorStatus(405, + "method not allowed only 'put' method allowed") + return None + + except ErrorStatus as exception: + LOG.log_exception("Failed in EnableImage", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("Failed in EnableImage", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) diff --git a/orm/services/image_manager/ims/controllers/v1/orm/images/images.py b/orm/services/image_manager/ims/controllers/v1/orm/images/images.py new file mode 100755 index 00000000..1a8aa564 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/images/images.py @@ -0,0 +1,201 @@ +from pecan import rest, request + +from wsmeext.pecan import wsexpose +from pecan import expose, abort +import oslo_db +from ims.controllers.v1.orm.images.regions import RegionController +from ims.controllers.v1.orm.images.customers import CustomerController +from ims.controllers.v1.orm.images.enabled import EnabledController + +from ims.persistency.wsme.models import ImageWrapper, ImageSummaryResponse +from ims.logic.error_base import ErrorStatus +from ims.logger import get_logger +from orm_common.injector import injector +from orm_common.utils import api_error_utils as err_utils +from ims.utils import authentication as auth + + +di = injector.get_di() +LOG = get_logger(__name__) + + +@di.dependsOn('image_logic') +@di.dependsOn('utils') +class ImageController(rest.RestController): + regions = RegionController() + customers = CustomerController() + enabled = EnabledController() + + @wsexpose(ImageWrapper, str, body=ImageWrapper, rest_content_types='json', status_code=201) + def post(self, invalid_extra_param=None, image_wrapper=None): + image_logic, utils = di.resolver.unpack(ImageController) + uuid = "FailedToGetFromUUIDGen" + auth.authorize(request, "image:create") + + if not image_wrapper: + raise err_utils.get_error(request.transaction_id, + message="Body not supplied", + status_code=400) + + if invalid_extra_param: + raise err_utils.get_error(request.transaction_id, + message="URL has invalid extra param '{}' ".format(invalid_extra_param), + status_code=405) + try: + LOG.info("ImageController - Create image: " + str(image_wrapper.image.name)) + image_wrapper.image.owner = request.headers.get('X-RANGER-Owner') or '' + + if not image_wrapper.image.id: + uuid = utils.make_uuid() + else: + try: + uuid = utils.create_existing_uuid(image_wrapper.id) + except TypeError: + raise ErrorStatus(409.1, message='Image UUID already exists') + + try: + ret_image = image_logic.create_image(image_wrapper, uuid, + request.transaction_id) + except oslo_db.exception.DBDuplicateEntry as exception: + raise ErrorStatus(409.2, 'The field {0} already exists'.format(exception.columns)) + + LOG.info("ImageController - Image Created: " + str(ret_image)) + + event_details = 'Image {} {} {}, visibility: {}, created in regions: {} with tenants: {}'.format( + uuid, image_wrapper.image.name, image_wrapper.image.url, + image_wrapper.image.visibility, + [r.name for r in image_wrapper.image.regions], + image_wrapper.image.customers) + utils.audit_trail('create image', request.transaction_id, + request.headers, uuid, + event_details=event_details) + return ret_image + + except ErrorStatus as exception: + LOG.log_exception("ImageController - Failed to CreateImage", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("ImageController - Failed to CreateImage", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(ImageWrapper, str, body=ImageWrapper, rest_content_types='json', status_code=200) + def put(self, image_id, image_wrapper): + image_logic, utils = di.resolver.unpack(ImageController) + auth.authorize(request, "image:update") + try: + LOG.info("ImageController - UpdateImage: " + str(image_wrapper.image.name)) + try: + result = image_logic.update_image(image_wrapper, image_id, + request.transaction_id) + except oslo_db.exception.DBDuplicateEntry as exception: + raise ErrorStatus(409.2, 'The field {0} already exists'.format(exception.columns)) + + LOG.info("ImageController - UpdateImage finished well: " + str(image_wrapper.image.name)) + + event_details = 'Image {} {} {}, visibility: {}, created in regions: {} with tenants: {}'.format( + image_id, image_wrapper.image.name, image_wrapper.image.url, + image_wrapper.image.visibility, + [r.name for r in image_wrapper.image.regions], + image_wrapper.image.customers) + utils.audit_trail('update image', request.transaction_id, + request.headers, image_id, + event_details=event_details) + return result + + except ErrorStatus as exception: + LOG.log_exception("Failed in UpdateImage", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("ImageController - Failed to UpdateImage", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + @wsexpose(ImageWrapper, str, str, str, str, str, rest_content_types='json') + def get(self, image_uuid): + image_logic, utils = di.resolver.unpack(ImageController) + LOG.info("ImageController - GetImageDetails: uuid is {}".format( + image_uuid)) + auth.authorize(request, "image:get_one") + + try: + return image_logic.get_image_by_uuid(image_uuid) + + except ErrorStatus as exception: + LOG.log_exception("ImageController - Failed to GetImageDetails", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("ImageController - Failed to GetImageDetails", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + @wsexpose(ImageSummaryResponse, str, str, str, rest_content_types='json') + def get_all(self, visibility=None, region=None, tenant=None): + image_logic, utils = di.resolver.unpack(ImageController) + auth.authorize(request, "image:list") + + try: + LOG.info("ImageController - GetImagelist") + + result = image_logic.get_image_list_by_params(visibility, region, tenant) + return result + + except ErrorStatus as exception: + LOG.log_exception("ImageController - Failed to GetImagelist", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + + except Exception as exception: + LOG.log_exception("ImageController - Failed to GetImagelist", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exception)) + + @wsexpose(None, str, rest_content_types='json', status_code=204) + def delete(self, image_uuid): + image_logic, utils = di.resolver.unpack(ImageController) + LOG.info("Got image delete request") + auth.authorize(request, "image:delete") + try: + LOG.info("ImageController - delete image: image id:" + image_uuid) + image_logic.delete_image_by_uuid(image_uuid, request.transaction_id) + LOG.info("ImageController - delete image finished well: ") + + event_details = 'Image {} deleted'.format(image_uuid) + utils.audit_trail('delete image', request.transaction_id, + request.headers, image_uuid, + event_details=event_details) + + except ErrorStatus as exp: + LOG.log_exception("ImageController - Failed to delete image", exp) + raise err_utils.get_error(request.transaction_id, + message=exp.message, + status_code=exp.status_code) + + except Exception as exp: + LOG.log_exception("ImageController - Failed to delete image", exp) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=str(exp)) + + ''' + @expose() + def _lookup(self, primary_key, *remainder): + # + # This function is called when pecan does not find controller for the request + # + abort(405, "Invalid URL") + ''' diff --git a/orm/services/image_manager/ims/controllers/v1/orm/images/metadata.py b/orm/services/image_manager/ims/controllers/v1/orm/images/metadata.py new file mode 100644 index 00000000..c2601ba9 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/images/metadata.py @@ -0,0 +1,43 @@ +from pecan import rest, request, response +from wsmeext.pecan import wsexpose +from ims.persistency.wsme.models import MetadataWrapper + +from orm_common.injector import injector + +from ims.logic.error_base import ErrorStatus + +from ims.logger import get_logger +from orm_common.utils import api_error_utils as err_utils +from ims.utils import authentication as auth + +LOG = get_logger(__name__) + +di = injector.get_di() + + +@di.dependsOn('metadata_logic') +@di.dependsOn('utils') +class MetadataController(rest.RestController): + @wsexpose(str, str, str, body=MetadataWrapper, rest_content_types='json', status_code=200) + def post(self, image_id, region_name, metadata_wrapper): # add metadata to region + metadata_logic, utils = di.resolver.unpack(MetadataController) + auth.authorize(request, "metadata:create") + + try: + LOG.info("MetadataController - add metadata: " + str(metadata_wrapper)) + + metadata_logic.add_metadata(image_id, region_name, metadata_wrapper) + + LOG.info("MetadataController - metadata added") + return "OK" + + except ErrorStatus as exception: + LOG.log_exception("MetadataController - Failed to add metadata", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + except Exception as exception: + LOG.log_exception("MetadataController - Failed to add metadata", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) diff --git a/orm/services/image_manager/ims/controllers/v1/orm/images/regions.py b/orm/services/image_manager/ims/controllers/v1/orm/images/regions.py new file mode 100755 index 00000000..6128a39b --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/images/regions.py @@ -0,0 +1,122 @@ +from pecan import rest, request, response +from wsmeext.pecan import wsexpose + +from ims.controllers.v1.orm.images.metadata import MetadataController +from ims.persistency.wsme.models import RegionWrapper + +from orm_common.injector import injector + +from ims.logic.error_base import ErrorStatus + +from ims.logger import get_logger +from orm_common.utils import api_error_utils as err_utils +from ims.utils import authentication as auth + +LOG = get_logger(__name__) + +di = injector.get_di() + + +@di.dependsOn('image_logic') +@di.dependsOn('utils') +class RegionController(rest.RestController): + metadata = MetadataController() + + @wsexpose([str], str, rest_content_types='json') + def get(self, image_id): + # get region has been unfeatured + raise err_utils.get_error(request.transaction_id, + status_code=405) + + @wsexpose(RegionWrapper, str, body=RegionWrapper, rest_content_types='json', status_code=201) + def post(self, image_id, region_wrapper): # add regions to image + image_logic, utils = di.resolver.unpack(RegionController) + auth.authorize(request, "region:create") + + try: + if not region_wrapper.regions: + raise ErrorStatus(400, + " bad resquest please provide correct json") + LOG.info("RegionController - add regions: " + str(region_wrapper)) + + result = image_logic.add_regions(image_id, region_wrapper, request.transaction_id) + + LOG.info("RegionController - regions added: " + str(result)) + + event_details = 'Image {} regions: {} added'.format( + image_id, [r.name for r in region_wrapper.regions]) + utils.audit_trail('add regions', request.transaction_id, + request.headers, image_id, + event_details=event_details) + return result + + except ErrorStatus as exception: + LOG.log_exception("RegionController - Failed to add region", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + except Exception as exception: + LOG.log_exception("RegionController - Failed to add region", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(RegionWrapper, str, body=RegionWrapper, rest_content_types='json', status_code=200) + def put(self, image_id, region_wrapper): # add regions to image + image_logic, utils = di.resolver.unpack(RegionController) + auth.authorize(request, "region:update") + try: + if not region_wrapper.regions: + raise ErrorStatus(400, + " bad resquest please provide correct json") + LOG.info("RegionController - replace regions: " + str(region_wrapper)) + + result = image_logic.replace_regions(image_id, region_wrapper, request.transaction_id) + + LOG.info("RegionController - regions replaced: " + str(result)) + + event_details = 'Image {} regions: {} updated'.format( + image_id, [r.name for r in region_wrapper.regions]) + utils.audit_trail('replace regions', request.transaction_id, + request.headers, image_id, + event_details=event_details) + return result + + except ErrorStatus as exception: + LOG.log_exception("RegionController - Failed to replace region", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + except Exception as exception: + LOG.log_exception("RegionController - Failed to replace region", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(None, str, str, rest_content_types='json', status_code=204) + def delete(self, image_id, region_name): + image_logic, utils = di.resolver.unpack(RegionController) + auth.authorize(request, "region:delete") + try: + LOG.info("RegionController - delete region: " + str(region_name)) + + result = image_logic.delete_region(image_id, region_name, request.transaction_id) + + LOG.info("RegionController - region deleted: " + str(result)) + + event_details = 'Image {} region {} deleted'.format(image_id, + region_name) + utils.audit_trail('delete region', request.transaction_id, + request.headers, image_id, + event_details=event_details) + + except ErrorStatus as exception: # include NotFoundError + LOG.log_exception("RegionController - Failed to delete region", exception) + raise err_utils.get_error(request.transaction_id, + message=exception.message, + status_code=exception.status_code) + except Exception as exception: + LOG.log_exception("RegionController - Failed to delete region", exception) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) diff --git a/orm/services/image_manager/ims/controllers/v1/orm/logs.py b/orm/services/image_manager/ims/controllers/v1/orm/logs.py new file mode 100755 index 00000000..0be7d9f6 --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/logs.py @@ -0,0 +1,72 @@ +import logging + +from pecan import rest +import wsme +from wsmeext.pecan import wsexpose + + +logger = logging.getLogger(__name__) + + +class ClientSideError(wsme.exc.ClientSideError): + def __init__(self, error, status_code=400): + super(ClientSideError, self).__init__(error, status_code) + + +class LogChangeResultWSME(wsme.types.DynamicBase): + """log change result wsme type.""" + + result = wsme.wsattr(str, mandatory=True, default=None) + + def __init__(self, **kwargs): + """"init method.""" + super(LogChangeResult, self).__init__(**kwargs) + + +class LogChangeResult(object): + """log change result type.""" + + def __init__(self, result): + """"init method.""" + self.result = result + + +class LogsController(rest.RestController): + """Logs Audit controller.""" + + @wsexpose(LogChangeResultWSME, str, status_code=201, + rest_content_types='json') + def put(self, level): + """update log level. + + :param level: the log level text name + :return: + """ + + logger.info("Changing log level to [{}]".format(level)) + try: + log_level = logging._levelNames.get(level.upper()) + if log_level is not None: + self._change_log_level(log_level) + result = "Log level changed to {}.".format(level) + logger.info(result) + else: + raise Exception( + "The given log level [{}] doesn't exist.".format(level)) + except Exception as e: + result = "Fail to change log_level. Reason: {}".format( + e.message) + logger.error(result) + raise ClientSideError(error=e.message) + return LogChangeResult(result) + + @staticmethod + def _change_log_level(log_level): + path = __name__.split('.') + if len(path) > 0: + root = path[0] + root_logger = logging.getLogger(root) + root_logger.setLevel(log_level) + else: + logger.info("Fail to change log_level to [{}]. " + "the given log level doesn't exist.".format(log_level)) diff --git a/orm/services/image_manager/ims/controllers/v1/orm/orm.py b/orm/services/image_manager/ims/controllers/v1/orm/orm.py new file mode 100644 index 00000000..0f648c6b --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/orm.py @@ -0,0 +1,11 @@ +from ims.controllers.v1.orm.configuration import ConfigurationController +from ims.controllers.v1.orm.images.images import ImageController +from ims.controllers.v1.orm.logs import LogsController +from pecan.rest import RestController + + +class OrmController(RestController): + + configuration = ConfigurationController() + images = ImageController() + logs = LogsController() diff --git a/orm/services/image_manager/ims/controllers/v1/orm/root.py b/orm/services/image_manager/ims/controllers/v1/orm/root.py new file mode 100755 index 00000000..d0089bdb --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/orm/root.py @@ -0,0 +1,8 @@ +"""ORM controller module.""" +from ims.controllers.v1.orm.images import images + + +class OrmController(object): + """ORM root controller class.""" + + images = images.ImageController() diff --git a/orm/services/image_manager/ims/controllers/v1/root.py b/orm/services/image_manager/ims/controllers/v1/root.py new file mode 100755 index 00000000..bc1522fc --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/root.py @@ -0,0 +1,8 @@ +"""V1 controller module.""" +from ims.controllers.v1.orm import root + + +class V1Controller(object): + """V1 root controller class.""" + + orm = root.OrmController() diff --git a/orm/services/image_manager/ims/controllers/v1/v1.py b/orm/services/image_manager/ims/controllers/v1/v1.py new file mode 100644 index 00000000..41549fab --- /dev/null +++ b/orm/services/image_manager/ims/controllers/v1/v1.py @@ -0,0 +1,7 @@ +from ims.controllers.v1.orm.orm import OrmController +from pecan.rest import RestController + + +class V1Controller(RestController): + + orm = OrmController() diff --git a/orm/services/image_manager/ims/di_providers/__init__.py b/orm/services/image_manager/ims/di_providers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/di_providers/mock_providers.py b/orm/services/image_manager/ims/di_providers/mock_providers.py new file mode 100755 index 00000000..626f0713 --- /dev/null +++ b/orm/services/image_manager/ims/di_providers/mock_providers.py @@ -0,0 +1,18 @@ +from ims.ims_mocks import requests_mock +from ims.ims_mocks import audit_mock + +from ims.persistency.sql_alchemy.data_manager import DataManager +from ims.logic import image_logic +from ims.logic import metadata_logic +from ims.proxies import rds_proxy +from orm_common.utils import utils + +providers = [ + ('rds_proxy', rds_proxy), + ('image_logic', image_logic), + ('metadata_logic', metadata_logic), + ('requests', requests_mock), + ('data_manager', DataManager), + ('utils', utils), + ('audit_client', audit_mock) +] diff --git a/orm/services/image_manager/ims/di_providers/prod_providers.py b/orm/services/image_manager/ims/di_providers/prod_providers.py new file mode 100644 index 00000000..845459c4 --- /dev/null +++ b/orm/services/image_manager/ims/di_providers/prod_providers.py @@ -0,0 +1,19 @@ +import requests +from audit_client.api import audit + +from ims.proxies import rds_proxy +from ims.logic import image_logic +from ims.logic import metadata_logic +from ims.persistency.sql_alchemy.data_manager import DataManager +from orm_common.utils import utils + + +providers = [ + ('rds_proxy', rds_proxy), + ('image_logic', image_logic), + ('metadata_logic', metadata_logic), + ('requests', requests), + ('data_manager', DataManager), + ('utils', utils), + ('audit_client', audit) +] diff --git a/orm/services/image_manager/ims/etc/policy.json b/orm/services/image_manager/ims/etc/policy.json new file mode 100755 index 00000000..758bc02b --- /dev/null +++ b/orm/services/image_manager/ims/etc/policy.json @@ -0,0 +1,25 @@ +{ + "default": "!", + "admin": "role:admin", + "admin_support": "role:admin_support", + "admin_viewer": "role:admin_viewer", + "orm": "user:m01687", + + "admin_or_support": "role:admin or role:admin_support", + "admin_or_support_or_viewer": "rule:admin or rule:admin_support or rule:admin_viewer", + + "image:create": "rule:admin_or_support", + "image:list": "rule:admin_or_support_or_viewer", + "image:get_one": "rule:admin_or_support_or_viewer", + "image:update": "rule:admin", + "image:delete": "rule:admin", + "region:delete": "rule:admin", + "region:create": "rule:admin_or_support", + "region:update": "rule:admin", + "image:enable": "rule:admin", + "tenant:create": "rule:admin_or_support", + "tenant:delete": "rule:admin", + "tenant:update": "rule:admin", + + "metadata:create": "rule:orm" +} \ No newline at end of file diff --git a/orm/services/image_manager/ims/external_mock/audit_client/__init__.py b/orm/services/image_manager/ims/external_mock/audit_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/external_mock/audit_client/api/__init__.py b/orm/services/image_manager/ims/external_mock/audit_client/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/external_mock/audit_client/api/audit.py b/orm/services/image_manager/ims/external_mock/audit_client/api/audit.py new file mode 100644 index 00000000..ec483bdd --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/audit_client/api/audit.py @@ -0,0 +1,6 @@ +def audit(*args, **kwargs): + pass + + +def init(*args, **kwargs): + pass diff --git a/orm/services/image_manager/ims/external_mock/keystone_utils/__init__.py b/orm/services/image_manager/ims/external_mock/keystone_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/external_mock/keystone_utils/tokens.py b/orm/services/image_manager/ims/external_mock/keystone_utils/tokens.py new file mode 100755 index 00000000..cb45e879 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/keystone_utils/tokens.py @@ -0,0 +1,10 @@ +def get_token_user(*a, **k): + pass + + +def is_token_valid(token_to_validate, lcp_id, conf, token_role): + pass + + +def TokenConf(mech_id, mech_password, rms_url, tenant_name, key_ep=None): + pass diff --git a/orm/services/image_manager/ims/external_mock/orm_common/__init__.py b/orm/services/image_manager/ims/external_mock/orm_common/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/external_mock/orm_common/hooks/__init__.py b/orm/services/image_manager/ims/external_mock/orm_common/hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/external_mock/orm_common/hooks/transaction_id_hook.py b/orm/services/image_manager/ims/external_mock/orm_common/hooks/transaction_id_hook.py new file mode 100644 index 00000000..d156a1fa --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/hooks/transaction_id_hook.py @@ -0,0 +1,7 @@ +# from pecan.hooks import PecanHook + + +class TransactionIdHook(): + + def before(self, state): + pass diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/__init__.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/__init__.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/__init__.py new file mode 100755 index 00000000..18404b50 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/__init__.py @@ -0,0 +1,7 @@ +''' +''' + +from .di import Di +from .dependency_register import DependencyRegister +from .resource_provider_register import ResourceProviderRegister +from .resolver import DependencyResolver diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/dependency_register.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/dependency_register.py new file mode 100755 index 00000000..7b4d7093 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/dependency_register.py @@ -0,0 +1,77 @@ +from functools import partial +import inspect + +from .errors import DependentNotFoundError + +try: + import click +except ImportError: + click = None + + +class DependencyRegister: + def __init__(self): + # Maps dependents to names of resources they require + self.dependents = {} + # Maps names of resources to their dependents + self.resources = {} + + @classmethod + def _unwrap_func(cls, decorated_func): + ''' + This unwraps a decorated func, returning the inner wrapped func. + + This may become unnecessary with Python 3.4's inspect.unwrap(). + ''' + if click is not None: + # Workaround for click.command() decorator not setting + # __wrapped__ + if isinstance(decorated_func, click.Command): + return cls._unwrap_func(decorated_func.callback) + + if hasattr(decorated_func, '__wrapped__'): + # Recursion: unwrap more if needed + return cls._unwrap_func(decorated_func.__wrapped__) + else: + # decorated_func isn't actually decorated, no more + # unwrapping to do + return decorated_func + + @classmethod + def _unwrap_dependent(cls, dependent): + # Dependent is effectively a class. Classes are registered as is. + if inspect.isclass(dependent): + return dependent + # dependent is some other kind of callable, eg a function + else: + return cls._unwrap_func(dependent) + + def _register_dependent(self, dependent, resource_name): + if dependent not in self.dependents: + self.dependents[dependent] = [] + self.dependents[dependent].insert(0, resource_name) + + def _register_resource_dependency(self, resource_name, dependent): + if resource_name not in self.resources: + self.resources[resource_name] = set() + self.resources[resource_name].add(dependent) + + def register(self, resource_name, dependent=None): + if dependent is None: + # Give a partial usable as a decorator + return partial(self.register, resource_name) + + dependent = self._unwrap_dependent(dependent) + self._register_dependent(dependent, resource_name) + self._register_resource_dependency(resource_name, dependent) + + # Return dependent to ease use as decorator + return dependent + + def query_resources(self, dependent): + dependent = self._unwrap_dependent(dependent) + + if dependent not in self.dependents: + raise DependentNotFoundError(dependent=dependent) + + return self.dependents[dependent] diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/di.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/di.py new file mode 100755 index 00000000..6a436354 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/di.py @@ -0,0 +1,16 @@ +from .dependency_register import DependencyRegister +from .resource_provider_register import ResourceProviderRegister +from .resolver import DependencyResolver + + +class Di: + def __init__(self, namespace=None): + self.namespace = namespace + self.dependencies = DependencyRegister() + self.providers = ResourceProviderRegister() + self.resolver = DependencyResolver( + dependency_register=self.dependencies, + resource_provider_register=self.providers) + + # For use as a decorator + self.dependsOn = self.dependencies.register diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/errors.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/errors.py new file mode 100755 index 00000000..607cfb98 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/errors.py @@ -0,0 +1,47 @@ +class FangError(Exception): + pass + + +class DependentNotFoundError(FangError): + def __init__(self, dependent=None): + self.dependent = dependent + if dependent: + message = ( + "Couldn't find dependencies registered for {!r}" + "".format(dependent)) + else: + message = ( + "Couldn't find dependencies registered for the given " + "dependent") + super(DependentNotFoundError, self).__init__(message) + + +class ProviderAlreadyRegisteredError(FangError): + def __init__(self, resource_name=None, existing_provider=None): + self.resource_name = resource_name + self.existing_provider = existing_provider + if resource_name and existing_provider: + message = ( + 'A provider ({provider!r}) has already been ' + 'registered for resource {resource_name!r}'.format( + provider=existing_provider, + resource_name=resource_name)) + else: + message = ( + 'A provider has already been registered for the ' + 'resource') + super(ProviderAlreadyRegisteredError, self).__init__(message) + + +class ProviderNotFoundError(FangError): + def __init__(self, resource_name=None): + self.resource_name = resource_name + if resource_name: + message = ( + "A provider could not be found for resource {!r}" + "".format(resource_name)) + else: + message = ( + "A provider could not be found for the requested " + "resource") + super(ProviderNotFoundError, self).__init__(message) diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resolver.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resolver.py new file mode 100755 index 00000000..5a58f8d6 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resolver.py @@ -0,0 +1,44 @@ +from .errors import ProviderNotFoundError + + +# This is effectively what is sometimes termed a "dependency injection +# container". +class DependencyResolver: + def __init__( + self, + dependency_register=None, + resource_provider_register=None): + self.dependency_register = dependency_register + self.resource_provider_register = resource_provider_register + + # Methods delegated to other objects + self.query_dependents_resources = \ + self.dependency_register.query_resources + self.resolve = self.resource_provider_register.resolve + + def resolve_all_dependencies(self, dependent): + return [ + self.resolve(resource_name) + for resource_name in + self.query_dependents_resources(dependent)] + + def unpack(self, dependent): + resources = self.resolve_all_dependencies(dependent) + + # Never return a length-1 list/tuple, to allow easier unpacking + # eg, avoid need for comma in: + # my_one_dep, = my_resolver.unpack_dependencies(my_func) + if len(resources) == 1: + return resources[0] + else: + return resources + + def are_all_dependencies_met_for(self, dependent): + for resource_name in self.query_dependents_resources(dependent): + try: + self.resolve(resource_name) + except ProviderNotFoundError as e: + # TODO: Add error logging here + return False + else: + return True diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resource_provider_register.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resource_provider_register.py new file mode 100755 index 00000000..58da4619 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/injector/fang/resource_provider_register.py @@ -0,0 +1,68 @@ +from functools import partial + +from .errors import ( + FangError, + ProviderAlreadyRegisteredError, + ProviderNotFoundError) + + +class ResourceProviderRegister: + def __init__(self, namespace=None): + self.namespace = namespace + # Maps resource names to a provider + self.resource_providers = {} + + def register(self, resource_name, provider=None, allow_override=False): + if provider is None: + # Give a partial usable as a decorator + return partial( + self.register, + resource_name, allow_override=allow_override) + + if ((not allow_override) and resource_name in self.resource_providers): + raise ProviderAlreadyRegisteredError( + resource_name=resource_name, + existing_provider=self.resource_providers[resource_name]) + + self.resource_providers[resource_name] = provider + + # Return provider to ease use as decorator + return provider + + register_callable = register + + # For registering providers which always return the same instance + def register_instance(self, resource_name, instance=None, **kwargs): + if instance is None: + # Give a partial usable as a decorator + return partial(self.register_instance, resource_name, **kwargs) + + self.register(resource_name, provider=(lambda: instance), **kwargs) + return instance + + def mass_register(self, resource_names_to_providers, **kwargs): + for resource_name, provider in resource_names_to_providers.items(): + self.register_instance(resource_name, provider, **kwargs) + + def load(self, other_register, allow_overrides=False): + if not allow_overrides: + own_keys = self.resource_providers.keys() + other_keys = other_register.resource_providers.keys() + common_keys = own_keys & other_keys + if common_keys: + # TODO Add new FangError sub-class? + raise FangError( + 'This register already has providers for keys: ' + '{!r}'.format(common_keys)) + + self.resource_providers.update( + other_register.resource_providers) + + def clear(self): + self.resource_providers.clear() + + def resolve(self, resource_name): + if resource_name not in self.resource_providers: + raise ProviderNotFoundError(resource_name=resource_name) + + return self.resource_providers[resource_name]() diff --git a/orm/services/image_manager/ims/external_mock/orm_common/injector/injector.py b/orm/services/image_manager/ims/external_mock/orm_common/injector/injector.py new file mode 100755 index 00000000..39264441 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/injector/injector.py @@ -0,0 +1,55 @@ +from orm_common.injector import fang +import os +import imp + +_di = fang.Di() +logger = None + + +def register_providers(env_variable, providers_dir_path, _logger): + global logger + logger = _logger + + # TODO: change all prints to logger + logger.info('Initializing dependency injector') + logger.info('Checking {0} variable'.format(env_variable)) + + env = None + if not (env_variable in os.environ): + logger.warn('No {0} variable found using `prod` injector'.format(env_variable)) + env = 'prod' + elif os.environ[env_variable] == '__TEST__': + logger.info('__TEST__ variable found, explicitly skipping provider registration!!!') + return + else: + env = os.environ[env_variable] + logger.info('{0} found setting injector to {1} environment'.format(env_variable, env)) + + logger.info('Setting injector providers') + + module = _import_file_by_name(env, providers_dir_path) + + for provider in module.providers: + logger.info('Setting provider `{0}` to {1}'.format(provider[0], provider[1])) + _di.providers.register_instance(provider[0], provider[1]) + + +def get_di(): + return _di + + +def override_injected_dependency(dep_tuple): + _di.providers.register_instance(dep_tuple[0], dep_tuple[1], allow_override=True) + + +def _import_file_by_name(env, providers_dir_path): + file_path = os.path.join(providers_dir_path, '{0}_providers.py'.format(env)) + try: + module = imp.load_source('fms_providers', file_path) + except IOError as ex: + logger.log_exception( + 'File with providers for the {0} environment, path: {1} wasnt found! Crushing!!!'.format(env, file_path), + ex) + raise ex + + return module diff --git a/orm/services/image_manager/ims/external_mock/orm_common/logger/__init__.py b/orm/services/image_manager/ims/external_mock/orm_common/logger/__init__.py new file mode 100755 index 00000000..1894cbbe --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/logger/__init__.py @@ -0,0 +1,10 @@ +import logging + + +def get_logger(name): + logger = logging.getLogger(name) + logger.log_exception = lambda msg, exception: logger.exception(msg + " Exception: " + str(exception)) + + return logger + +__all__ = ['get_logger'] diff --git a/orm/services/image_manager/ims/external_mock/orm_common/policy/__init__.py b/orm/services/image_manager/ims/external_mock/orm_common/policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/external_mock/orm_common/policy/policy.py b/orm/services/image_manager/ims/external_mock/orm_common/policy/policy.py new file mode 100644 index 00000000..0e021c4e --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/policy/policy.py @@ -0,0 +1,6 @@ +def authorize(action, request, app_conf, keystone_ep=None): + pass + + +def init(*a, **b): + pass diff --git a/orm/services/image_manager/ims/external_mock/orm_common/utils.py b/orm/services/image_manager/ims/external_mock/orm_common/utils.py new file mode 100755 index 00000000..2debd1a1 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/utils.py @@ -0,0 +1,8 @@ +class utils: + @staticmethod + def set_utils_conf(conf): + pass + + @staticmethod + def report_config(conf, dump_to_log): + pass diff --git a/orm/services/image_manager/ims/external_mock/orm_common/utils/__init__.py b/orm/services/image_manager/ims/external_mock/orm_common/utils/__init__.py new file mode 100755 index 00000000..2140b170 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/utils/__init__.py @@ -0,0 +1,10 @@ +import cross_api_utils +import utils + + +def set_utils_conf(conf): + cross_api_utils.set_utils_conf(conf) + utils.set_utils_conf(conf) + + +__all__ = ['cross_api_utils', 'utils'] diff --git a/orm/services/image_manager/ims/external_mock/orm_common/utils/api_error_utils.py b/orm/services/image_manager/ims/external_mock/orm_common/utils/api_error_utils.py new file mode 100644 index 00000000..653221e2 --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/utils/api_error_utils.py @@ -0,0 +1,2 @@ +def get_error(*args, **kwargs): + pass diff --git a/orm/services/image_manager/ims/external_mock/orm_common/utils/cross_api_utils.py b/orm/services/image_manager/ims/external_mock/orm_common/utils/cross_api_utils.py new file mode 100755 index 00000000..11233bbe --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/utils/cross_api_utils.py @@ -0,0 +1,109 @@ +import requests +import logging +from pecan import conf +from audit_client.api import audit +import time + +from orm_common.logger import get_logger + +logger = get_logger(__name__) + +conf = None + + +def set_utils_conf(_conf): + global conf + conf = _conf + + +def _check_conf_initialization(): + if not conf: + raise AssertionError( + 'Configuration wasnt initiated, please run set_utils_conf and ' + 'pass pecan configuration') + + +def is_region_exist(region_name): + """ function to check whether region exists + returns 200 for ok and None for error + """ + region = get_rms_region(region_name) + if region is None: + return False + + return True + + +def is_region_group_exist(group_name): + """ function to check whether region group exists + returns 200 for ok and None for error + """ + group = get_rms_region_group(group_name) + if group is None: + return False + + return True + + +def get_regions_of_group(group_name): + """ function to get regions associated with group + returns 200 for ok and None for error + """ + group = get_rms_region_group(group_name) + if group is None: + return None + + return group["Regions"] + + +def get_rms_region(region_name): + """ function to call rms api for region info + returns 200 for ok and None for error + """ + _check_conf_initialization() + try: + headers = { + 'content-type': 'application/json', + } + rms_server_url = '%s%s/%s' % ( + conf.api.rms_server.base, conf.api.rms_server.regions, region_name) + resp = requests.get(rms_server_url, headers=headers).json() + return resp + except Exception as e: + logger.log_exception('Failed in get_rms_region', e) + return None + + return 200 + + +prev_group_name = None + + +def get_rms_region_group(group_name): + """ function to call rms api for group info + returns 200 for ok and None for error + """ + global prev_group_name, prev_timestamp, prev_resp + + _check_conf_initialization() + try: + timestamp = time.time() + if group_name == prev_group_name and timestamp - prev_timestamp <= \ + conf.api.rms_server.cache_seconds: + return prev_resp + + headers = { + 'content-type': 'application/json', + } + rms_server_url = '%s%s/%s' % ( + conf.api.rms_server.base, conf.api.rms_server.groups, group_name) + resp = requests.get(rms_server_url, headers=headers).json() + prev_resp = resp + prev_group_name = group_name + prev_timestamp = timestamp + return resp + except Exception as e: + logger.log_exception('Failed in get_rms_region_group', e) + return None + + return 200 diff --git a/orm/services/image_manager/ims/external_mock/orm_common/utils/utils.py b/orm/services/image_manager/ims/external_mock/orm_common/utils/utils.py new file mode 100755 index 00000000..49d90ecc --- /dev/null +++ b/orm/services/image_manager/ims/external_mock/orm_common/utils/utils.py @@ -0,0 +1,116 @@ +import requests +import pprint +import logging +from pecan import conf +from audit_client.api import audit +import time + +from orm_common.logger import get_logger + +logger = get_logger(__name__) + +conf = None + + +def set_utils_conf(_conf): + global conf + conf = _conf + + +def _check_conf_initialization(): + if not conf: + raise AssertionError( + 'Configurations wasnt initiated, please run set_utils_conf and ' + 'pass pecan coniguration') + + +def make_uuid(): + """ function to request new uuid from uuid_generator rest service + returns uuid string + """ + _check_conf_initialization() + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + + try: + resp = requests.post(url) + except Exception as e: + logger.info('Failed in make_uuid:' + str(e)) + raise Exception('Failed in make_uuid:' + str(e)) + + resp = resp.json() + return resp['uuid'] + + +def make_transid(): + """ function to request new uuid of transaction type from uuid_generator + rest service + returns uuid string + """ + _check_conf_initialization() + url = conf.api.uuid_server.base + conf.api.uuid_server.uuids + + try: + resp = requests.post(url, data={'uuid_type': 'transaction'}) + except Exception as e: + logger.info('Failed in make_transid:' + str(e)) + raise Exception('Failed in make_transid:' + str(e)) + + resp = resp.json() + return resp['uuid'] + + +audit_setup = False + + +def audit_trail(cmd, transaction_id, headers, resource_id, message): + """ function to send item to audit trail rest api + returns 200 for ok and None for error + """ + + _check_conf_initialization() + global audit_setup, audit_server_url + if not audit_setup: + audit_server_url = '%s%s' % (conf.api.audit_server.base, + conf.api.audit_server.trans) + num_of_send_retries = 3 + time_wait_between_retries = 1 + audit.init(audit_server_url, num_of_send_retries, + time_wait_between_retries) + audit_setup = True + + try: + timestamp = long(round(time.time() * 1000)) + application_id = headers['X-RANGER-Client'] + tracking_id = headers[ + 'X-RANGER-Tracking-Id'] if 'X-RANGER-Tracking-Id' in headers \ + else transaction_id + # transaction_id is function argument + transaction_type = cmd + # resource_id is function argument + service_name = conf.server.name + user_id = headers[ + 'X-RANGER-Requester'] if 'X-RANGER-Requester' in headers else 'NA' + external_id = 'NA' + event_details = 'CMS' + status = message + audit.audit(timestamp, application_id, tracking_id, transaction_id, + transaction_type, + resource_id, service_name, user_id, external_id, + event_details, status) + except Exception as e: + logger.log_exception('Failed in audit service', e) + return None + + return 200 + + +def report_config(conf, dump_to_log=False, my_logger=None): + """ return the configuration (which is set by config.py) as a string + """ + + ret = pprint.pformat(conf.to_dict(), indent=4) + effective_logger = my_logger if my_logger else logger + if dump_to_log: + effective_logger.info('Current Configuration:\n' + ret) + + return ret diff --git a/orm/services/image_manager/ims/hooks/__init__.py b/orm/services/image_manager/ims/hooks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/hooks/service_hooks.py b/orm/services/image_manager/ims/hooks/service_hooks.py new file mode 100755 index 00000000..235b31d4 --- /dev/null +++ b/orm/services/image_manager/ims/hooks/service_hooks.py @@ -0,0 +1,12 @@ +from orm_common.hooks.transaction_id_hook import TransactionIdHook +from orm_common.utils import utils + + +class TransIdHook(TransactionIdHook): + + def before(self, state): + transaction_id = utils.make_transid() + tracking_id = state.request.headers['X-RANGER-Tracking-Id'] \ + if 'X-RANGER-Tracking-Id' in state.request.headers else transaction_id + setattr(state.request, 'transaction_id', transaction_id) + setattr(state.request, 'tracking_id', tracking_id) diff --git a/orm/services/image_manager/ims/ims_mocks/__init__.py b/orm/services/image_manager/ims/ims_mocks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/ims_mocks/audit_mock.py b/orm/services/image_manager/ims/ims_mocks/audit_mock.py new file mode 100755 index 00000000..d4bc9af2 --- /dev/null +++ b/orm/services/image_manager/ims/ims_mocks/audit_mock.py @@ -0,0 +1,8 @@ +def init(audit_server_url, num_of_send_retries, time_wait_between_retries): + pass + + +def audit(timestamp, application_id, tracking_id, transaction_id, + transaction_type, resource_id, service_name, user_id=None, + external_id=None, event_details=None, status=None): + return 200 diff --git a/orm/services/image_manager/ims/ims_mocks/requests_mock.py b/orm/services/image_manager/ims/ims_mocks/requests_mock.py new file mode 100755 index 00000000..e1020b1e --- /dev/null +++ b/orm/services/image_manager/ims/ims_mocks/requests_mock.py @@ -0,0 +1,110 @@ +import copy +import uuid +from mock import MagicMock +from ims.persistency.sql_alchemy.data_manager import DataManager + +from ims.logger import get_logger + +logger = get_logger(__name__) + + +def post(url, **kwargs): + if 'rds/resources' in url: + logger.debug('MOCK: requests.post called for rds/resources') + return _build_rds_response() + + elif 'uuids' in url: + logger.debug('MOCK: requests.post called for uuid') + return _build_uuid_response() + + +def delete(url, **kwargs): + if 'rds/resources' in url: + logger.debug('MOCK: requests.deletr called for rds/resources') + return _build_rds_response() + + else: + raise Exception("No delete action for this url".format(url)) + + +def get(url): + if 'status/resource' in url: + logger.debug('MOCK: requests.get called for status/resources') + return _build_status_response(url) + + +def _build_uuid_response(): + res = MagicMock() + res.json.return_value = { + 'uuid': str(uuid.uuid1()) + } + + return res + + +def _build_rds_response(): + response = MagicMock() + response.status_code = 201 + response.content = {"image": {"profile": "p1", + "status": "Error", + "description": "A standard 2GB Ram 2 vCPUs " + "50GB Disk, Flavor", + "extra-specs": {"key3": "value3", + "key2": "value2", + "key1": "value1"}, + "ram": "4096", + "ephemeral": "0", + "visibility": "private", + "regions": [ + { + "status": "Error", + "name": "dkk12" + }, { + "status": "Error", + "name": "san12"}], + "vcpus": "2", + "swap": "1024", + "disk": "50", + "tenants": [ + "070be05e-26e2-4519-a46d-224cbf8558f4", + "4f7b9561-af8b-4cc0-87e2-319270dad49e"], + "id": "a5310ede-1c15-11e6-86bb-005056a50d38", + "name": "fr4096v2d50" + } + } + + response.json.return_value = response.content + return response + + +def _build_status_response(url): + uuid_index = url.find('status/resource/') + 16 + uuid = url[uuid_index:] + datamanager = DataManager() + image_record = datamanager.get_record('image') + sql_image = image_record.get_image_by_id(uuid) + _status_response['regions'] = [] + for sql_region in sql_image.image_regions: + new_region = copy.copy(_region_mock) + new_region['region'] = sql_region.region_name + _status_response['regions'].append(new_region) + mock = MagicMock() + mock.json.return_value = _status_response + return mock + + +_region_mock = { + "region": "dla1", + "timestamp": "1451599200", + "ord-transaction-id": "0649c5be323f4792afbc1efdd480847d", + "resource-id": "12fde398643acbed32f8097c98aec20", + "ord-notifier-id": "", + "status": "success", + "error-code": "200", + "error-msg": "OK" +} + +_status_response = { + "status": "pending", + "regions": [] +} diff --git a/orm/services/image_manager/ims/logger/__init__.py b/orm/services/image_manager/ims/logger/__init__.py new file mode 100755 index 00000000..1894cbbe --- /dev/null +++ b/orm/services/image_manager/ims/logger/__init__.py @@ -0,0 +1,10 @@ +import logging + + +def get_logger(name): + logger = logging.getLogger(name) + logger.log_exception = lambda msg, exception: logger.exception(msg + " Exception: " + str(exception)) + + return logger + +__all__ = ['get_logger'] diff --git a/orm/services/image_manager/ims/logic/__init__.py b/orm/services/image_manager/ims/logic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/logic/error_base.py b/orm/services/image_manager/ims/logic/error_base.py new file mode 100755 index 00000000..7cc7e13c --- /dev/null +++ b/orm/services/image_manager/ims/logic/error_base.py @@ -0,0 +1,38 @@ + +class Error(Exception): + pass + + +class ErrorStatus(Error): + + def __init__(self, status_code, message=""): + self.status_code = status_code + self.message = message + + +class NoContentError(ErrorStatus): + + def __init__(self, message='', status_code=204): + self.status_code = status_code + self.message = message + + +class NotFoundError(ErrorStatus): + + def __init__(self, message='', status_code=404): + self.status_code = status_code + self.message = message + + +class DuplicateEntityError(ErrorStatus): + + def __init__(self, status_code=409, message="item already exist"): + self.status_code = status_code + self.message = message + + +class ConflictError(ErrorStatus): + + def __init__(self, status_code=409, message="conflict error"): + self.status_code = status_code + self.message = message diff --git a/orm/services/image_manager/ims/logic/image_logic.py b/orm/services/image_manager/ims/logic/image_logic.py new file mode 100755 index 00000000..f0429b9e --- /dev/null +++ b/orm/services/image_manager/ims/logic/image_logic.py @@ -0,0 +1,551 @@ +from ims.logger import get_logger +from ims.persistency.wsme.models import ImageWrapper, ImageSummaryResponse +from ims.persistency.wsme.models import Region, ImageSummary, RegionWrapper +from ims.persistency.sql_alchemy.db_models import ImageRegion, ImageCustomer +from ims.logic.error_base import ErrorStatus, NotFoundError, NoContentError +from ims.utils import utils as ImsUtils + +from orm_common.utils import utils +from orm_common.injector import injector +import time + +from pecan import request, conf + +LOG = get_logger(__name__) + +di = injector.get_di() + + +@di.dependsOn('data_manager') +def create_image(image_wrapper, image_uuid, transaction_id): + DataManager = di.resolver.unpack(create_image) + datamanager = DataManager() + + image_wrapper.image.id = image_uuid + + image_wrapper.image.created_at = str(int(time.time())) + image_wrapper.image.updated_at = image_wrapper.image.created_at + + try: + image_wrapper.handle_region_group() + image_wrapper.validate_model() + sql_image = image_wrapper.to_db_model() + + image_rec = datamanager.get_record('image') + + datamanager.begin_transaction() + image_rec.insert(sql_image) + datamanager.flush() # i want to get any exception created by this + # insert + existing_region_names = [] + send_to_rds_if_needed(sql_image, existing_region_names, "post", + transaction_id) + + datamanager.commit() + + ret_image = get_image_by_uuid(image_uuid) + return ret_image + + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to CreateImage", exp) + datamanager.rollback() + raise + + +@di.dependsOn('rds_proxy') +def send_to_rds_if_needed(sql_image, existing_region_names, http_action, + transaction_id): + rds_proxy = di.resolver.unpack(send_to_rds_if_needed) + if (sql_image.regions and len(sql_image.regions) > 0) or len( + existing_region_names) > 0: + image_dict = sql_image.get_proxy_dict() + update_region_actions(image_dict, existing_region_names, http_action) + if image_dict['regions'] or len(existing_region_names) > 0: + LOG.debug("Image is valid to send to RDS - sending to RDS Proxy ") + rds_proxy.send_image(image_dict, transaction_id, http_action) + else: + LOG.debug("Group with no regions - wasn't send to RDS Proxy " + str( + sql_image)) + else: + LOG.debug("Image with no regions - wasn't send to RDS Proxy " + str( + sql_image)) + + +@di.dependsOn('data_manager') +def update_image(image_wrapper, image_uuid, transaction_id, http_action="put"): + DataManager = di.resolver.unpack(update_image) + datamanager = DataManager() + + try: + image_wrapper.validate_model('update') + new_image = image_wrapper.to_db_model() + new_image.id = image_uuid + + datamanager.begin_transaction() + + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + + if sql_image is None: + raise NotFoundError(status_code=404, + message="Image {0} does not exist for update".format( + image_uuid)) + + new_image.owner = sql_image.owner + existing_regions = sql_image.get_existing_region_names() + new_image.created_at = int(sql_image.created_at) + new_image.updated_at = int(time.time()) + # result = image_rec.delete_image_by_id(image_uuid) + datamanager.get_session().delete(sql_image) + # del sql_image + image_rec.insert(new_image) + datamanager.flush() + + send_to_rds_if_needed(new_image, existing_regions, http_action, + transaction_id) + + datamanager.commit() + + ret_image = get_image_by_uuid(image_uuid) + + return ret_image + + except Exception as exp: + datamanager.rollback() + LOG.log_exception("ImageLogic - Failed to update image", exp) + raise + + +@di.dependsOn('rds_proxy') +@di.dependsOn('data_manager') +def delete_image_by_uuid(image_uuid, transaction_id): + rds_proxy, DataManager = di.resolver.unpack(delete_image_by_uuid) + datamanager = DataManager() + + try: + + datamanager.begin_transaction() + image_rec = datamanager.get_record('image') + + sql_image = image_rec.get_image_by_id(image_uuid) + if sql_image is None: + return + + image_existing_region_names = sql_image.get_existing_region_names() + if len(image_existing_region_names) > 0: + # Do not delete a flavor that still has some regions + raise ErrorStatus(405, + "Cannot delete a image with regions. " + "Please delete the regions first and then " + "delete the image. ") + + # Get status from RDS + image_status = rds_proxy.get_status(sql_image.id, False) + + status_resp = None + + if image_status.status_code == 200: + status_resp = image_status.json()['status'] + LOG.debug('RDS returned status: {}'.format(status_resp)) + + elif image_status.status_code == 404: + status_resp = 'Success' + + else: + # fail to get status from rds + raise ErrorStatus(500, "fail to get status for this resource " + "deleting image not allowed ") + + if status_resp != 'Success': + raise ErrorStatus(405, "not allowed as aggregate status " + "have to be Success (either the deletion" + " failed on one of the regions or it is " + "still in progress)") + + image_rec.delete_image_by_id(image_uuid) + datamanager.flush() # i want to get any exception created by this + + # delete + datamanager.commit() + + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to delete image", exp) + datamanager.rollback() + raise + + +@di.dependsOn('data_manager') +def add_regions(image_uuid, regions, transaction_id): + DataManager = di.resolver.unpack(add_regions) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + if not sql_image: + raise ErrorStatus(404, 'image with id: {0} not found'.format( + image_uuid)) + + existing_region_names = sql_image.get_existing_region_names() + + for region in regions.regions: + db_region = ImageRegion(region_name=region.name, region_type=region.type) + sql_image.add_region(db_region) + + datamanager.flush() # i want to get any exception created by + # previous actions against the database + + send_to_rds_if_needed(sql_image, existing_region_names, "put", + transaction_id) + + datamanager.commit() + + image_wrapper = get_image_by_uuid(image_uuid) + ret = RegionWrapper(regions=image_wrapper.image.regions) + return ret + + except ErrorStatus as exp: + LOG.log_exception("ImageLogic - Failed to add regions", exp) + datamanager.rollback() + raise exp + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to add regions", exp) + datamanager.rollback() + raise exp + + +@di.dependsOn('data_manager') +def replace_regions(image_uuid, regions, transaction_id): + DataManager = di.resolver.unpack(replace_regions) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + if not sql_image: + raise ErrorStatus(404, 'image with id: {0} not found'.format( + image_uuid)) + + existing_region_names = sql_image.get_existing_region_names() + + sql_image.remove_all_regions() + datamanager.flush() + + for region in regions.regions: + db_region = ImageRegion(region_name=region.name, region_type=region.type) + sql_image.add_region(db_region) + datamanager.flush() # i want to get any exception created by + # previous actions against the database + + send_to_rds_if_needed(sql_image, existing_region_names, "put", + transaction_id) + + datamanager.commit() + + image_wrapper = get_image_by_uuid(image_uuid) + ret = RegionWrapper(regions=image_wrapper.image.regions) + return ret + + except ErrorStatus as exp: + LOG.log_exception("ImageLogic - Failed to replace regions", exp) + datamanager.rollback() + raise exp + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to repalce regions", exp) + datamanager.rollback() + raise exp + + +@di.dependsOn('data_manager') +def delete_region(image_uuid, region_name, transaction_id): + DataManager = di.resolver.unpack(delete_region) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + if not sql_image: + raise ErrorStatus(404, 'image with id: {0} not found'.format( + image_uuid)) + + existing_region_names = sql_image.get_existing_region_names() + + sql_image.remove_region(region_name) + + datamanager.flush() # i want to get any exception created by + # previous actions against the database + send_to_rds_if_needed(sql_image, existing_region_names, "put", + transaction_id) + + datamanager.commit() + + except ErrorStatus as exp: + LOG.log_exception("ImageLogic - Failed to update image", exp) + datamanager.rollback() + raise + + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to delete region", exp) + datamanager.rollback() + raise + + +@di.dependsOn('data_manager') +def add_customers(image_uuid, customers, transaction_id): + DataManager = di.resolver.unpack(add_customers) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + if not sql_image: + raise ErrorStatus(404, 'image with id: {0} not found'.format( + image_uuid)) + + if sql_image.visibility == "public": + raise ErrorStatus(400, 'Cannot add Customers to public Image') + + existing_region_names = sql_image.get_existing_region_names() + + for user in customers.customers: + db_Customer = ImageCustomer(customer_id=user) + sql_image.add_customer(db_Customer) + + datamanager.flush() # i want to get any exception created by + # previous actions against the database + send_to_rds_if_needed(sql_image, existing_region_names, "put", + transaction_id) + datamanager.commit() + + ret_image = get_image_by_uuid(image_uuid) + return ret_image + + except Exception as exp: + if 'conflicts with persistent instance' in exp.message or 'Duplicate entry' in exp.message: + raise ErrorStatus(409, "Duplicate Customer for Image") + LOG.log_exception("ImageLogic - Failed to add Customers", exp) + datamanager.rollback() + raise + + +@di.dependsOn('data_manager') +def replace_customers(image_uuid, customers, transaction_id): + DataManager = di.resolver.unpack(replace_customers) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + if not sql_image: + raise ErrorStatus(404, 'image {0} not found'.format(image_uuid)) + + if sql_image.visibility == "public": + raise ValueError('Cannot add Customers to public Image') + + existing_region_names = sql_image.get_existing_region_names() + sql_image.remove_all_customers() + datamanager.flush() + + for cust in customers.customers: + db_Customer = ImageCustomer(customer_id=cust) + sql_image.add_customer(db_Customer) + + datamanager.flush() # get exception created by previous db actions + send_to_rds_if_needed(sql_image, existing_region_names, "put", + transaction_id) + datamanager.commit() + + ret_image = get_image_by_uuid(image_uuid) + return ret_image + + except Exception as exp: + if 'conflicts with persistent instance' in exp.message or 'Duplicate entry' in exp.message: + raise ErrorStatus(409, "Duplicate Customer for Image") + LOG.log_exception("ImageLogic - Failed to add Customers", exp) + datamanager.rollback() + raise + + +@di.dependsOn('data_manager') +def delete_customer(image_uuid, customer_id, transaction_id): + DataManager = di.resolver.unpack(delete_customer) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + if not sql_image: + raise ErrorStatus(404, 'image {0} not found'.format(image_uuid)) + # if trying to delete the only one Customer then return value error + if sql_image.visibility == "public": + raise ValueError("Image {} is public, no customers".format(image_uuid)) + + if len(sql_image.customers) == 1 and \ + sql_image.customers[0].customer_id == customer_id: + raise ValueError('Private Image must have at least one Customer - ' + 'You are trying to delete the only one Customer') + + existing_region_names = sql_image.get_existing_region_names() + sql_image.remove_customer(customer_id) + + datamanager.flush() # i want to get any exception created by + # previous actions against the database + send_to_rds_if_needed(sql_image, existing_region_names, "put", + transaction_id) + datamanager.commit() + + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to delete Customer", exp) + datamanager.rollback() + raise + + +@di.dependsOn('data_manager') +@di.dependsOn('rds_proxy') +def get_image_by_uuid(image_uuid): + DataManager, rds_proxy = di.resolver.unpack(get_image_by_uuid) + datamanager = DataManager() + + LOG.debug("Get image by uuid : {}".format(image_uuid)) + try: + + datamanager.begin_transaction() + + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_uuid) + + if not sql_image: + raise NotFoundError(status_code=404, + message="Image {0} not found ".format( + image_uuid)) + + image_wrapper = ImageWrapper.from_db_model(sql_image) + + # get image own link + image_wrapper.image.links, image_wrapper.image.self_link = ImsUtils.get_server_links(image_uuid) + # convert time stamp format to human readable time + image_wrapper.image.created_at = ImsUtils.convert_time_human( + image_wrapper.image.created_at) + image_wrapper.image.updated_at = ImsUtils.convert_time_human( + image_wrapper.image.updated_at) + # Get the status from RDS + image_status = rds_proxy.get_status(image_wrapper.image.id, False) + + if image_status.status_code != 200: + return image_wrapper + image_status = image_status.json() + image_wrapper.image.status = image_status['status'] + # update status for all regions + for result_regions in image_wrapper.image.regions: + for status_region in image_status['regions']: + if result_regions.name == status_region['region']: + result_regions.status = status_region['status'] + + except NotFoundError as exp: + datamanager.rollback() + LOG.log_exception("ImageLogic - Failed to update image", exp) + raise exp + + except Exception as exp: + datamanager.rollback() + LOG.log_exception("ImageLogic - Failed to delete Customer", exp) + raise + + return image_wrapper + + +@di.dependsOn('data_manager') +def get_image_list_by_params(visibility, region, Customer): + DataManager = di.resolver.unpack(get_image_list_by_params) + + datamanager = DataManager() + try: + image_record = datamanager.get_record('image') + sql_images = image_record.get_images_by_criteria(visibility=visibility, + region=region, + Customer=Customer) + + response = ImageSummaryResponse() + for sql_image in sql_images: + image = ImageSummary.from_db_model(sql_image) + response.images.append(image) + + return response + + except ErrorStatus as exp: + LOG.log_exception("ImageLogic - Failed to get list", exp) + raise + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to get list", exp) + raise + + +def update_region_actions(image_dict, existing_region_names, action="put"): + if action == "delete": + set_regions_action(image_dict, "delete") + elif action == "post": + set_regions_action(image_dict, "create") + else: # put + for region in image_dict["regions"]: + if region["name"] in existing_region_names: + region["action"] = "modify" + else: + region["action"] = "create" + + # add deleted regions + for exist_region_name in existing_region_names: + if region_name_exist_in_regions(exist_region_name, + image_dict["regions"]): + continue + else: + image_dict["regions"].append( + {"name": exist_region_name, "action": "delete"}) + + +def region_name_exist_in_regions(region_name, regions): + for region in regions: + if region["name"] == region_name: + return True + return False + + +def set_regions_action(image_dict, action): + for region in image_dict["regions"]: + region["action"] = action + + +@di.dependsOn('data_manager') +def enable_image(image_uuid, int_enabled, transaction_id): + DataManager = di.resolver.unpack(enable_image) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image(image_uuid) + if not sql_image: + raise ErrorStatus(404, 'Image with id: {0} not found'.format( + image_uuid)) + + sql_image.enabled = int_enabled + + existing_region_names = sql_image.get_existing_region_names() + + datamanager.flush() # i want to get any exception created by this + # insert method + + send_to_rds_if_needed(sql_image, existing_region_names, "put", + transaction_id) + + datamanager.commit() + + ret_image = get_image_by_uuid(image_uuid) + return ret_image + + except ErrorStatus as exp: + LOG.log_exception("ImageLogic - Failed to change image activation value", exp) + datamanager.rollback() + raise exp + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to change image activation value", exp) + datamanager.rollback() + raise exp diff --git a/orm/services/image_manager/ims/logic/metadata_logic.py b/orm/services/image_manager/ims/logic/metadata_logic.py new file mode 100644 index 00000000..5b42f958 --- /dev/null +++ b/orm/services/image_manager/ims/logic/metadata_logic.py @@ -0,0 +1,33 @@ +from ims.logger import get_logger +from ims.logic.error_base import ErrorStatus +from orm_common.injector import injector + +LOG = get_logger(__name__) + +di = injector.get_di() + + +@di.dependsOn('data_manager') +def add_metadata(image_id, region_name, metadata_wrapper): + DataManager = di.resolver.unpack(add_metadata) + datamanager = DataManager() + + try: + image_rec = datamanager.get_record('image') + sql_image = image_rec.get_image_by_id(image_id) + if not sql_image: + raise ErrorStatus(404, 'image {0} not found'.format(image_id)) + + for region in sql_image.regions: + if region.region_name == region_name: + region.checksum = metadata_wrapper.metadata.checksum + region.size = metadata_wrapper.metadata.size + region.virtual_size = metadata_wrapper.metadata.virtual_size + + datamanager.flush() + datamanager.commit() + + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to add regions", exp) + datamanager.rollback() + raise diff --git a/orm/services/image_manager/ims/persistency/__init__.py b/orm/services/image_manager/ims/persistency/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/persistency/sql_alchemy/__init__.py b/orm/services/image_manager/ims/persistency/sql_alchemy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/persistency/sql_alchemy/data_manager.py b/orm/services/image_manager/ims/persistency/sql_alchemy/data_manager.py new file mode 100755 index 00000000..c3a69f3e --- /dev/null +++ b/orm/services/image_manager/ims/persistency/sql_alchemy/data_manager.py @@ -0,0 +1,89 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.event import listen +from sqlalchemy import exc +from pecan import conf +from oslo_db.sqlalchemy import session as db_session +from ims.logger import get_logger + +from ims.persistency.sql_alchemy.image.image_record import ImageRecord +from ims.logic.error_base import DuplicateEntityError + + +LOG = get_logger(__name__) + + +# event handling +def on_before_flush(session, flush_context, instances): + print("on_before_flush:", str(flush_context)) + for model in session.new: + if hasattr(model, "validate"): + model.validate("new") + + for model in session.dirty: + if hasattr(model, "validate"): + model.validate("dirty") + + +class DataManager(object): + + def __init__(self, connection_string=None): + + if not connection_string: + connection_string = conf.database.connection_string + + self._engine_facade = db_session.EngineFacade(connection_string, autocommit=False) + self._session = None + listen(self.session, 'before_flush', on_before_flush) + self.image_record = None + + def get_engine(self): + return self._engine_facade.get_engine() + + @property + def engine(self): + return self.get_engine() + + def get_session(self): + if not self._session: + self._session = self._engine_facade.get_session() + return self._session + + @property + def session(self): + return self.get_session() + + def flush(self): + try: + self.session.flush() + except Exception as exp: + raise + + def commit(self): + self.session.commit() + + def expire_all(self): + self.session.expire_all() + + def close(self): + self.session.close() + + def rollback(self): + self.session.rollback() + + def close(self): + self.session.close() + self.engine.dispose() + + def begin_transaction(self): + pass + # no need to begin transaction - the transaction is open automatically + + def get_record(self, record_name): + if record_name == "Image" or record_name == "image": + if not self.image_record: + self.image_record = ImageRecord(self.session) + return self.image_record + return None + +# 7540 ProCG uses this line - don't edit it diff --git a/orm/services/image_manager/ims/persistency/sql_alchemy/db_models.py b/orm/services/image_manager/ims/persistency/sql_alchemy/db_models.py new file mode 100755 index 00000000..64d991f2 --- /dev/null +++ b/orm/services/image_manager/ims/persistency/sql_alchemy/db_models.py @@ -0,0 +1,408 @@ +### +# This file was generated by ProCG version 2.0 +# +# File name: ims\persitency\sql_alchemy\db_models.py +# Language: Python 2.7 +# Database: My Sql +# +# Copyright (c) 2002-2016 iGenXSoft. +# For more information visit http://www.igenxsoft.com +### + +from sqlalchemy import (Column, Integer, SmallInteger, String, + ForeignKeyConstraint) +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from oslo_db.sqlalchemy import models + +from ims.logger import get_logger +from orm_common.utils.cross_api_utils import get_regions_of_group, set_utils_conf +from pecan import conf +from ims.logic.error_base import NotFoundError +from ims.logic.error_base import ErrorStatus +Base = declarative_base() + +LOG = get_logger(__name__) + + +class IMSBaseModel(models.ModelBase): + + """Base class from IMS Models.""" + + __table_args__ = {'mysql_engine': 'InnoDB'} + + +class Image(Base, IMSBaseModel): + ### + # Image is a DataObject and contains all the fields defined in Image table + # record. Defined as SqlAlchemy model map to a table + ### + __tablename__ = "image" + + def __init__(self, + id=None, + name=None, + enabled=None, + url=None, + protected=None, + visibility=None, + disk_format=None, + container_format=None, + min_disk=None, + min_ram=None, + owner=None, + schema=None, + created_at=None, + updated_at=None): + + self.id = id + self.name = name + self.enabled = enabled + self.url = url + self.protected = protected + self.visibility = visibility + self.disk_format = disk_format + self.container_format = container_format + self.min_disk = min_disk + self.min_ram = min_ram + self.owner = owner + self.schema = schema + self.created_at = created_at + self.updated_at = updated_at + + id = Column(String, primary_key=True) + name = Column(String) + enabled = Column(SmallInteger) + url = Column(String) + protected = Column(SmallInteger) + visibility = Column(String) + disk_format = Column(String) + container_format = Column(String) + min_disk = Column(Integer) + min_ram = Column(Integer) + owner = Column(String) + schema = Column(String) + created_at = Column(Integer) + updated_at = Column(Integer) + properties = relationship("ImageProperty", uselist=True, + cascade="all, delete, delete-orphan", + primaryjoin="and_(Image.id==ImageProperty.image_id)") + regions = relationship("ImageRegion", uselist=True, + cascade="all, delete, delete-orphan", + primaryjoin="and_(Image.id==ImageRegion.image_id)") + tags = relationship("ImageTag", uselist=True, + cascade="all, delete, delete-orphan", + primaryjoin="and_(Image.id==ImageTag.image_id)") + customers = relationship("ImageCustomer", + uselist=True, + cascade="all, delete, delete-orphan", + primaryjoin="and_(Image.id==ImageCustomer.image_id)") + + def __json__(self): + properties = {} + # don't send tags to rds server + # tags = {} + + for prop in self.properties: + properties[prop.key_name] = prop.key_value + + return dict( + id=self.id, + name=self.name, + enabled=self.enabled * 1, + url=self.url, + protected=self.protected * 1, + visibility=self.visibility, + disk_format=self.disk_format, + container_format=self.container_format, + min_disk=self.min_disk, + min_ram=self.min_ram, + owner=self.owner, + schema=self.schema, + created_at=self.created_at, + updated_at=self.updated_at, + properties=properties, + regions=self.get_regions_json(), + customers=[customer.__json__() for customer in self.customers] + ) + + def get_proxy_dict(self): + proxy_dict = self.__json__() + + return proxy_dict + + def get_regions_json(self): + regions_json = [] + for region in self.regions: + region_json = region.__json__() + regions_json.append(region_json) + return regions_json + + def get_existing_region_names(self): + existing_region_names = [] + for region in self.get_proxy_dict()['regions']: + existing_region_names.append(region['name']) + return existing_region_names + + def add_region(self, image_region): + assert isinstance(image_region, ImageRegion) + try: + LOG.debug("add region {0} to image {1}".format(str(image_region), + str(self))) + region_list = filter(lambda region: region.region_name == image_region.region_name, self.regions) + if len(region_list) > 0: + raise ErrorStatus(409, "Region {} already exist in Image {}".format(image_region.region_name, self.name)) + self.regions.append(image_region) + + except Exception as exception: + LOG.log_exception("Failed to add region {0} to image {1}".format( + str(image_region), str(self)), exception) + raise + + def remove_region(self, region_name): + assert isinstance(region_name, basestring) + try: + LOG.debug("remove regions {0} from image {1}".format(region_name, + str(self))) + region_found = False + for region in reversed(self.regions): + if region.region_name == region_name: + self.regions.remove(region) + region_found = True + + if not region_found: + raise NotFoundError("Region {} not found in Image {} (name - {}) ".format(region_name, self.id, self.name)) + + except Exception as exception: + LOG.log_exception( + "Failed to remove region {0} from image {1}".format( + region_name, str(self)), exception) + raise + + def remove_all_regions(self): + try: + LOG.debug("remove regions from image {0}".format(str(self))) + for region in reversed(self.regions): + self.regions.remove(region) + + except Exception as exception: + LOG.log_exception("Failed to remove regions from image {0}".format(str(self)), exception) + raise + + def add_customer(self, image_customer): + assert isinstance(image_customer, ImageCustomer) + try: + LOG.debug("add customer {0} to image {1}".format(str(image_customer), str(self))) + self.customers.append(image_customer) + + except Exception as exception: + LOG.log_exception("Failed to add customer {0} from image {1}".format(str(image_customer), str(self)), exception) + + def remove_customer(self, customer_id): + assert isinstance(customer_id, basestring) + try: + LOG.debug("remove customers {0} from image {1}".format(customer_id, + str(self))) + + for customer in reversed(self.customers): + if customer.customer_id == customer_id: + self.customers.remove(customer) + + except Exception as exception: + LOG.log_exception( + "Failed to remove customer {0} from image {1}".format( + customer_id, str(self)), exception) + + def remove_all_customers(self): + try: + LOG.debug("remove customers from image {0}".format(str(self))) + + for customer in reversed(self.customers): + self.customers.remove(customer) + + except Exception as exception: + LOG.log_exception("Failed to remove customers from image" + " {0}".format(str(self)), exception) + + def __repr__(self): + text = "Image(id='{}', name='{}', enabled={}, url='{}', " \ + "protected='{}', visibility='{}', disk_format='{}', " \ + "container_format='{}', min_disk={}, min_ram={}, owner='{}', " \ + "schema='{}', created_at='{}', updated_at='{}')"\ + .format(self.id, self.name, self.enabled, self.url, self.protected, + self.visibility, self.disk_format, self.container_format, + self.min_disk, self.min_ram, self.owner, self.schema, + self.created_at, self.updated_at) + return text + + +class ImageProperty(Base, IMSBaseModel): + ### + # ImageProperty is a DataObject and contains all the fields defined in + # ImageProperty table record. Defined as SqlAlchemy model map to a table + ### + __tablename__ = "image_property" + + def __init__(self, + image_id=None, + key_name=None, + key_value=None): + + self.image_id = image_id + self.key_name = key_name + self.key_value = key_value + + image_id = Column(String, primary_key=True) + key_name = Column(String, primary_key=True) + key_value = Column(String) + image = relationship("Image", uselist=False, + primaryjoin="and_(ImageProperty.image_id==Image.id)") + __table_args__ = ( + ForeignKeyConstraint( + ["image_id"], + ["image.id"], + name="image_properties_ibfk_1" + ), + ) + + def __json__(self): + return dict( + image_id=self.image_id, + key_name=self.key_name, + key_value=self.key_value + ) + + def __repr__(self): + text = "ImageProperty(image_id='{}', key_name='{}'," \ + " key_value='{}')".format(self.image_id, self.key_name, + self.key_value) + return text + + +class ImageRegion(Base, IMSBaseModel): + ### + # ImageRegion is a DataObject and contains all the fields defined in + # ImageRegion table record. Defined as SqlAlchemy model map to a table + ### + def __init__(self, region_name=None, region_type=None, + checksum="", size="", virtual_size=""): + Base.__init__(self) + self.region_name = region_name + self.region_type = region_type + self.checksum = checksum + self.size = size + self.virtual_size = virtual_size + + __tablename__ = "image_region" + + image_id = Column(String, primary_key=True) + region_name = Column(String, primary_key=True) + region_type = Column(String) + checksum = Column(String) + size = Column(String) + virtual_size = Column(String) + image = relationship("Image", uselist=False, + primaryjoin="and_(ImageRegion.image_id==Image.id)") + __table_args__ = ( + ForeignKeyConstraint( + ["image_id"], + ["image.id"], + name="image_region_ibfk_1" + ), + ) + + def __json__(self): + return dict( + image_id=self.image_id, + name=self.region_name, + type=self.region_type, + checksum=self.checksum, + size=self.size, + virtual_size=self.virtual_size + ) + + @staticmethod + def get_group_regions(group_name): + set_utils_conf(conf) + regions = get_regions_of_group(group_name) + return regions + + def __repr__(self): + text = "ImageRegion(image_id='{}', region_name='{}'," \ + "region_type='{}', checksum='{}', size='{}'," \ + "virtual_size='{}')".format(self.image_id, self.region_name, + self.region_type, self.checksum, + self.size, self.virtual_size) + return text + + +class ImageTag(Base, IMSBaseModel): + ### + # ImageTag is a DataObject and contains all the fields defined in ImageTag + # table record. Defined as SqlAlchemy model map to a table + ### + __tablename__ = "image_tag" + + def __init__(self, + image_id=None, + tag=None): + + self.image_id = image_id + self.tag = tag + + image_id = Column(String, primary_key=True) + tag = Column(String, primary_key=True) + __table_args__ = ( + ForeignKeyConstraint( + ["image_id"], + ["image.id"], + name="image_tags_ibfk_1" + ), + ) + + def __json__(self): + return dict( + image_id=self.image_id, + tag=self.tag + ) + + def __repr__(self): + text = "ImageTag(image_id='{}', tag='{}')".format( + self.image_id, self.tag) + return text + + +class ImageCustomer(Base, IMSBaseModel): + ### + # ImageCustomer is a DataObject and contains all the fields defined in + # ImageCustomer table record. Defined as SqlAlchemy model map to a table + ### + __tablename__ = "image_customer" + + def __init__(self, + image_id=None, + customer_id=None): + + self.image_id = image_id + self.customer_id = customer_id + + image_id = Column(String, primary_key=True) + customer_id = Column(String, primary_key=True) + __table_args__ = ( + ForeignKeyConstraint( + ["image_id"], + ["image.id"], + name="image_customer_ibfk_1" + ), + ) + + def __json__(self): + return dict( + image_id=self.image_id, + customer_id=self.customer_id + ) + + def __repr__(self): + text = "ImageCustomer(image_id='{}', customer_id='{}')".format( + self.image_id, self.customer_id) + return text diff --git a/orm/services/image_manager/ims/persistency/sql_alchemy/image/__init__.py b/orm/services/image_manager/ims/persistency/sql_alchemy/image/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/persistency/sql_alchemy/image/image_record.py b/orm/services/image_manager/ims/persistency/sql_alchemy/image/image_record.py new file mode 100755 index 00000000..4b35d8c3 --- /dev/null +++ b/orm/services/image_manager/ims/persistency/sql_alchemy/image/image_record.py @@ -0,0 +1,181 @@ +from ims.persistency.sql_alchemy.db_models import Image +from ims.persistency.sql_alchemy.db_models import ImageRegion +from ims.persistency.sql_alchemy.db_models import ImageCustomer +from ims.persistency.sql_alchemy.infra.record import Record +from ims.logger import get_logger + +LOG = get_logger(__name__) + + +class ImageRecord(Record): + def __init__(self, session): + + # this model is uses only for the parameters of access mothods, not an instance of model in the database + self.__image = Image() + # self.set_record_data(self.__image) + # self.__image.clear() + Record.__init__(self) + self.__image = None + self.__TableName = "image" + + if session: + self.set_db_session(session) + + @property + def image(self): + return self.__image + + @image.setter + def image(self, image): + self.__image = image + + def insert(self, image): + try: + self.session.add(image) + except Exception as exception: + LOG.log_exception("Failed to insert image" + str(image), exception) + # LOG.error("Failed to insert image" + str(image) + " Exception:" + str(exception)) + raise + + def get_image(self, id): + try: + query = self.session.query(Image).filter(Image.id == id) + return query.first() + except Exception as exception: + message = "Failed to read_image: id: {0}".format(id) + LOG.log_exception(message, exception) + raise + + def delete_image_by_id(self, id): + try: + result = self.session.connection().execute("delete from image where id = '{0}'".format(id)) + return result + + except Exception as exception: + message = "Failed to delete_by_internal_id: internal_id: {0}".format(id) + LOG.log_exception(message, exception) + raise + + def create_images_by_name_query(self, name): + try: + query = self.session.query(Image).filter(Image.name == name) + return query + except Exception as exception: + message = "Failed to create_images_by_name_query: name: {0}".format(name) + LOG.log_exception(message, exception) + raise + + def get_images_by_name(self, name, **kw): + try: + query = self.create_images_by_name_query(name) + query = self.customise_query(query, kw) + return query.all() + except Exception as exception: + message = "Failed to get_images_by_name: name: {0}".format(name) + LOG.log_exception(message, exception) + raise + + def get_image_by_id(self, id): + try: + image = self.session.query(Image).filter(Image.id == id) + return image.first() + + except Exception as exception: + message = "Failed to get_image_by_id: id: {0}".format(id) + LOG.log_exception(message, exception) + raise + + def get_count_of_images_by_name(self, name): + try: + query = self.create_images_by_name_query(name) + return query.count() + except Exception as exception: + message = "Failed to get_images_by_name_count: name: {0}".format(name) + LOG.log_exception(message, exception) + raise + + def create_images_by_visibility_query(self, visibility): + try: + query = self.session.query(Image).filter(Image.visibility == visibility) + return query + except Exception as exception: + message = "Failed to create_images_by_visibility_query: visibility: {0}".format(visibility) + LOG.log_exception(message, exception) + raise + + def get_images_by_visibility(self, visibility, **kw): + try: + query = self.create_images_by_visibility_query(visibility) + query = self.customise_query(query, kw) + return query.all() + except Exception as exception: + message = "Failed to get_images_by_visibility: visibility: {0}".format(visibility) + LOG.log_exception(message, exception) + raise + + def get_count_of_images_by_visibility(self, visibility): + try: + query = self.create_images_by_visibility_query(visibility) + return query.count() + except Exception as exception: + message = "Failed to get_images_by_visibility_count: visibility: {0}".format(visibility) + LOG.log_exception(message, exception) + raise + + def create_all_images_query(self): + try: + query = self.session.query(Image).filter() + return query + except Exception as exception: + message = "Failed to create_all_images_query: ".format() + LOG.log_exception(message, exception) + raise + + def get_all_images(self, **kw): + try: + query = self.create_all_images_query() + query = self.customise_query(query, kw) + return query.all() + except Exception as exception: + message = "Failed to get_all_images: ".format() + LOG.log_exception(message, exception) + raise + + def get_count_of_all_images(self): + try: + query = self.create_all_images_query() + return query.count() + except Exception as exception: + message = "Failed to get_all_images_count: ".format() + LOG.log_exception(message, exception) + raise + + def get_images_by_criteria(self, **criteria): + try: + LOG.debug("get_images_by_criteria: criteria: {0}".format(criteria)) + visibility = criteria[ + 'visibility'] if 'visibility' in criteria else None + region = criteria['region'] if 'region' in criteria else None + Customer = criteria['Customer'] if 'Customer' in criteria else None + + query = self.session.query(Image) + + if region: + query = query.join(ImageRegion).filter( + ImageRegion.image_id == Image.id, + ImageRegion.region_name == region) + if Customer: + query = query.join(ImageCustomer).filter( + ImageCustomer.image_id == Image.id, + ImageCustomer.customer_id == Customer) + if visibility: + query = query.filter(Image.visibility == visibility) + + query = self.customise_query(query, criteria) + return query.all() + + except Exception as exception: + message = "Failed to get_images_by_criteria: criteria: {0}".format( + criteria) + LOG.log_exception(message, exception) + raise diff --git a/orm/services/image_manager/ims/persistency/sql_alchemy/infra/__init__.py b/orm/services/image_manager/ims/persistency/sql_alchemy/infra/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/persistency/sql_alchemy/infra/record.py b/orm/services/image_manager/ims/persistency/sql_alchemy/infra/record.py new file mode 100755 index 00000000..69327707 --- /dev/null +++ b/orm/services/image_manager/ims/persistency/sql_alchemy/infra/record.py @@ -0,0 +1,28 @@ +from ims.logger import get_logger + +LOG = get_logger(__name__) + + +class Record(object): + + def __init__(self): + self.session = None + + def set_db_session(self, session): + self.session = session + + @staticmethod + def customise_query(query, kw): + start = int(kw['start']) if 'start' in kw else 0 + limit = int(kw['limit']) if 'limit' in kw else 0 + + if start > 0: + query = query.offset(start) + + if limit > 0: + query = query.limit(limit) + + print str(query) + return query + +# 5644 ProCG uses this line - don't edit it diff --git a/orm/services/image_manager/ims/persistency/wsme/__init__.py b/orm/services/image_manager/ims/persistency/wsme/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/persistency/wsme/base.py b/orm/services/image_manager/ims/persistency/wsme/base.py new file mode 100755 index 00000000..e15d05c1 --- /dev/null +++ b/orm/services/image_manager/ims/persistency/wsme/base.py @@ -0,0 +1,15 @@ +"""Base model module.""" +from wsme.rest.json import tojson +from wsme import types as wtypes + + +class Model(wtypes.DynamicBase): + """Base class for IMS models.""" + + def to_db_model(self): + """Get the object's DB model.""" + raise NotImplementedError("This function was not implemented") + + def tojson(self): + """Get the object's JSON representation.""" + return tojson(type(self), self) diff --git a/orm/services/image_manager/ims/persistency/wsme/models.py b/orm/services/image_manager/ims/persistency/wsme/models.py new file mode 100755 index 00000000..3e3488ab --- /dev/null +++ b/orm/services/image_manager/ims/persistency/wsme/models.py @@ -0,0 +1,463 @@ +"""Image model module.""" +import wsme +from pecan import conf +from pecan import request + +from ims.logic.error_base import ErrorStatus +from ims.persistency.wsme.base import Model +from ims.persistency.sql_alchemy import db_models +from orm_common.utils.cross_api_utils import set_utils_conf, get_regions_of_group + + +class Metadata(Model): + """region metadata model + """ + checksum = wsme.wsattr(wsme.types.text, mandatory=False) + size = wsme.wsattr(wsme.types.text, mandatory=False) + virtual_size = wsme.wsattr(wsme.types.text, mandatory=False) + + def __init__(self, checksum='', size='', virtual_size=''): + """region metadata model + + :param checksum: image checksum + :param size: image size + :param virtual_size: image virtual size in lcp + """ + + self.checksum = checksum + self.size = size + self.virtual_size = virtual_size + + +class MetadataWrapper(Model): + """region metadata model + """ + metadata = wsme.wsattr(Metadata, mandatory=False) + + def __init__(self, metadata=Metadata()): + """region metadata model + + :param metadata: metadata class + """ + + self.metadata = metadata + + +class Region(Model): + + name = wsme.wsattr(str, mandatory=True) + type = wsme.wsattr(str, default="single", mandatory=False) + + # Output-only fields + status = wsme.wsattr(str, mandatory=False) + checksum = wsme.wsattr(wsme.types.text, mandatory=False) + size = wsme.wsattr(wsme.types.text, mandatory=False) + virtual_size = wsme.wsattr(wsme.types.text, mandatory=False) + + def __init__(self, name="", type="single", status="", checksum='', + size='', virtual_size=''): + """region array + + :param name: region names + :param type: region type single/group + :param status: region creation status + :param checksum: image checksum + :param size: image size + :param virtual_size: image virtual size in lcp + """ + + self.name = name + self.type = type + self.status = status + self.checksum = checksum + self.size = size + self.virtual_size = virtual_size + + def to_db_model(self): + region_rec = db_models.ImageRegion() + region_rec.region_name = self.name + region_rec.region_type = self.type + region_rec.checksum = self.checksum + region_rec.size = self.size + region_rec.virtual_size = self.virtual_size + + return region_rec + + +class RegionWrapper(Model): # pragma: no cover + """regions model + """ + regions = wsme.wsattr([Region], mandatory=False) + + def __init__(self, regions=[]): + """ + :param regions: array of regions + """ + self.regions = regions + + +class CustomerWrapper(Model): # pragma: no cover + """customers model + """ + customers = wsme.wsattr(wsme.types.ArrayType(str), mandatory=False) + + def __init__(self, customers=[]): + """ + :param regions: array of regions + """ + self.customers = customers + + +class Image(Model): + """Image entity with all its related data.""" + + default_min_ram = 1024 + default_min_disk = 1 + default_protected = False + + name = wsme.wsattr(wsme.types.text, mandatory=True) + enabled = wsme.wsattr(bool, mandatory=False) + url = wsme.wsattr(wsme.types.text, mandatory=True) + visibility = wsme.wsattr(wsme.types.text, mandatory=True) + disk_format = wsme.wsattr(wsme.types.text, mandatory=True, + name='disk-format') + container_format = wsme.wsattr(wsme.types.text, mandatory=True, + name='container-format') + min_disk = wsme.wsattr(wsme.types.IntegerType(minimum=0), mandatory=False, + default=default_min_disk, name='min-disk') + min_ram = wsme.wsattr(wsme.types.IntegerType(minimum=0), mandatory=False, + default=default_min_ram, name='min-ram') + tags = wsme.wsattr(wsme.types.ArrayType(str), mandatory=False) + properties = wsme.wsattr(wsme.types.DictType(str, str), mandatory=False) + regions = wsme.wsattr(wsme.types.ArrayType(Region), mandatory=False) + customers = wsme.wsattr(wsme.types.ArrayType(str), mandatory=False) + owner = wsme.wsattr(wsme.types.text, mandatory=False) + schema = wsme.wsattr(wsme.types.text, mandatory=False) + protected = wsme.wsattr(bool, mandatory=False, default=default_protected) + + # Output-only fields + id = wsme.wsattr(wsme.types.text, mandatory=False) + status = wsme.wsattr(wsme.types.text, mandatory=False) + created_at = wsme.wsattr(wsme.types.IntegerType(minimum=0), + mandatory=False, name='created-at') + updated_at = wsme.wsattr(wsme.types.IntegerType(minimum=0), + mandatory=False, name='updated-at') + locations = wsme.wsattr(wsme.types.ArrayType(str), mandatory=False) + self_link = wsme.wsattr(wsme.types.text, mandatory=False, name='self') + file = wsme.wsattr(wsme.types.text, mandatory=False) + links = wsme.wsattr(wsme.types.DictType(str, str), mandatory=False) + + def __init__(self, + id='', + name='', + enabled=True, + url='', + visibility='', + disk_format='', + container_format='', + min_disk=default_min_disk, + min_ram=default_min_ram, + tags=[], + properties={}, + regions=[], + customers=[], + status='', + created_at=0, + updated_at=0, + locations=[], + self_link='', + protected=default_protected, + file='', + owner='', + schema='', + links={}): + """Create a new Image. + + :param id: Image UUID + :param name: Image name + :param url: Image URL + :param visibility: Image visibility (public | private) + :param disk_format: Image file format + :param container_format: Image container format + :param min_disk: Minimum disk size required + :param min_ram: Minimum RAM required + :param tags: Image tags + :param properties: Image properties + :param regions: Regions to use the image + :param customers: Customers to use the image + :param owner: Image owner + :param schema: Image schema + :param protected: Is the image protected from deletion + """ + self.id = id + self.name = name + self.enabled = enabled + self.url = url + self.visibility = visibility + self.disk_format = disk_format + self.container_format = container_format + self.min_disk = min_disk + self.min_ram = min_ram + self.tags = tags + self.properties = properties + self.regions = regions + self.customers = customers + + self.status = status + self.created_at = created_at + self.updated_at = updated_at + self.locations = locations + self.self_link = self_link + self.protected = protected + self.file = file + self.owner = owner + self.schema = schema + self.links = links + + def validate_model(self, context=None): + # Validate visibility + if self.visibility == 'public' and self.customers: + raise ErrorStatus(400, + 'Visibility is public but some customers were' + ' specified!') + elif self.visibility == 'private' and not self.customers: + raise ErrorStatus(400, + 'Visibility is private but no customers were' + ' specified!') + + # Validate disk format + valid_disk_formats = ('ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', + 'qcow2', 'vdi', 'iso',) + if self.disk_format not in valid_disk_formats: + raise ErrorStatus(400, 'Invalid disk format!') + + # validate customer input unique + customer_input = set() + for customer in self.customers: + if customer in customer_input: + raise ErrorStatus(400, "customer {} exist more than one".format(customer)) + customer_input.add(customer) + # Validate container format + valid_container_formats = ('ami', 'ari', 'aki', 'bare', 'ovf', 'ova', + 'docker') + if self.container_format not in valid_container_formats: + raise ErrorStatus(400, 'Invalid container format! {}'.format(self.container_format)) + + # Validate min-disk and min-ram (wsme automatically converts booleans + # to int, and isinstance(False, int) returns True, so that is how we + # validate the type) + if 'min-disk' in request.json['image'] and not type( + request.json['image']['min-disk']) == int: + raise ErrorStatus(400, 'min-disk must be an integer!') + if 'min-ram' in request.json['image'] and not type( + request.json['image']['min-ram']) == int: + raise ErrorStatus(400, 'min-ram must be an integer!') + + if self.min_disk != wsme.Unset and int(self.min_disk) > 2147483646 or int(self.min_disk) < 0: + raise ErrorStatus(400, + 'value must be positive less than 2147483646') + if self.min_ram != wsme.Unset and int(self.min_ram) > 2147483646 or int(self.min_ram) < 0: + raise ErrorStatus(400, + 'value must be positive less than 2147483646') + if context == "update": + for region in self.regions: + if region.type == "group": + raise ErrorStatus(400, "region {} type is invalid for update, \'group\' can be only in create".format(region.name)) + + def to_db_model(self): + image = db_models.Image() + tags = [] + properties = [] + customers = [] + regions = [] + + for tag in self.tags: + tag_rec = db_models.ImageTag() + tag_rec.tag = tag + tags.append(tag_rec) + + for key, value in self.properties.iteritems(): + prop = db_models.ImageProperty() + prop.key_name = key + prop.key_value = value + properties.append(prop) + + for region in self.regions: + regions.append(region.to_db_model()) + + for customer in self.customers: + customer_rec = db_models.ImageCustomer() + customer_rec.customer_id = customer + customers.append(customer_rec) + + image.id = self.id + image.name = self.name + image.enabled = self.enabled + image.url = self.url + image.visibility = self.visibility + image.disk_format = self.disk_format + image.container_format = self.container_format + image.min_disk = self.min_disk + image.min_ram = self.min_ram + image.status = self.status + image.created_at = self.created_at + image.updated_at = self.updated_at + image.locations = self.locations + image.self_link = self.self_link + image.protected = self.protected + image.file = self.file + image.owner = self.owner + image.schema = self.schema + image.links = self.links + + image.tags = tags + image.properties = properties + image.regions = regions + image.customers = customers + + return image + + @staticmethod + def from_db_model(sql_image): + image = Image() + image.id = sql_image.id + image.name = sql_image.name + image.enabled = sql_image.enabled == 1 + image.url = sql_image.url + image.visibility = sql_image.visibility + image.disk_format = sql_image.disk_format + image.container_format = sql_image.container_format + image.min_disk = sql_image.min_disk + image.min_ram = sql_image.min_ram + image.protected = sql_image.protected == 1 + image.owner = sql_image.owner + image.schema = sql_image.schema + image.created_at = sql_image.created_at + image.updated_at = sql_image.updated_at + + for attribute in ('status', 'self_link', 'file'): + setattr(image, attribute, getattr(sql_image, attribute, '')) + + for attribute in ('created_at', 'updated_at'): + setattr(image, attribute, getattr(sql_image, attribute, 0)) + + setattr(image, 'locations', getattr(sql_image, 'locations', [])) + setattr(image, 'links', getattr(sql_image, 'links', {})) + + image.customers = [] + for customer in sql_image.customers: + image.customers.append(customer.customer_id) + + image.regions = [] + for sql_region in sql_image.regions: + region = Region() + region.name = sql_region.region_name + region.type = sql_region.region_type + region.checksum = sql_region.checksum + region.size = sql_region.size + region.virtual_size = sql_region.virtual_size + image.regions.append(region) + + image.tags = [] + for tag in sql_image.tags: + image.tags.append(tag.tag) + + image.properties = {} + for prop in sql_image.properties: + image.properties[prop.key_name] = prop.key_value + + return image + + def handle_region_group(self): + regions_to_add = [] + for region in self.regions[:]: # get copy of it to be able to delete from the origin + if region.type == "group": + group_regions = self.get_regions_for_group(region.name) + if group_regions is None: + raise ErrorStatus(404, "Group {} does not exist".format(region.name)) + for group_region in group_regions: + regions_to_add.append(Region(name=group_region, type='single')) + self.regions.remove(region) + + self.regions.extend(set(regions_to_add)) # remove duplicates if exist + + def get_regions_for_group(self, group_name): + set_utils_conf(conf) + regions = get_regions_of_group(group_name) + return regions + + +class ImageWrapper(Model): + """image model + + """ + image = wsme.wsattr(Image, mandatory=True, name='image') + + def __init__(self, image=Image()): + """ + + :param image: image dict + """ + + self.image = image + + def to_db_model(self): + return self.image.to_db_model() + + def validate_model(self, context=None): + return self.image.validate_model(context) + + def handle_region_group(self): + return self.image.handle_region_group() + + def get_extra_spec_needed(self): + return self.image.get_extra_spec_needed() + + @staticmethod + def from_db_model(sql_image): + image = ImageWrapper() + image.image = Image.from_db_model(sql_image) + return image + + +''' +' ImageSummary a DataObject contains all the fields defined in ImageSummary. +''' + + +class ImageSummary(Model): + name = wsme.wsattr(wsme.types.text) + id = wsme.wsattr(wsme.types.text) + visibility = wsme.wsattr(wsme.types.text) + + def __init__(self, name='', id='', visibility=''): + Model.__init__(self) + + self.name = name + self.id = id + self.visibility = visibility + + @staticmethod + def from_db_model(sql_image): + image = ImageSummary() + image.id = sql_image.id + image.name = sql_image.name + image.visibility = sql_image.visibility + + return image + + +class ImageSummaryResponse(Model): + images = wsme.wsattr([ImageSummary], mandatory=True) + + def __init__(self): # pragma: no cover + Model.__init__(self) + self.images = [] + + +class Enabled(Model): + enabled = wsme.wsattr(bool, mandatory=True) + + def __init__(self, enabled=None): # pragma: no cover + Model.__init__(self) + self.enabled = enabled diff --git a/orm/services/image_manager/ims/proxies/__init__.py b/orm/services/image_manager/ims/proxies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/proxies/rds_proxy.py b/orm/services/image_manager/ims/proxies/rds_proxy.py new file mode 100755 index 00000000..969c3ac8 --- /dev/null +++ b/orm/services/image_manager/ims/proxies/rds_proxy.py @@ -0,0 +1,104 @@ +import json +import pprint +from pecan import conf +from pecan import request +from ims.logger import get_logger +from orm_common.injector import injector +from ims.logic.error_base import ErrorStatus +di = injector.get_di() + +LOG = get_logger(__name__) + +headers = {'content-type': 'application/json'} + + +@di.dependsOn('requests') +def send_image(image_dict, transaction_id, action="put"): + # action can be "post" for creating image or "delete" for deleting image + requests = di.resolver.unpack(send_image) + + data = { + "service_template": + { + "resource": { + "resource_type": "image" + }, + "model": str(json.dumps(image_dict)), + "tracking": { + "external_id": "", + "tracking_id": transaction_id + } + } + } + + data_to_display = { + "service_template": + { + "resource": { + "resource_type": "image" + }, + "model": image_dict, + "tracking": { + "external_id": "", + "tracking_id": transaction_id + } + } + } + try: + pp = pprint.PrettyPrinter(width=30) + pretty_text = pp.pformat(data_to_display) + wrapper_json = json.dumps(data) + + headers['X-RANGER-Client'] = request.headers[ + 'X-RANGER-Client'] if 'X-RANGER-Client' in request.headers else \ + 'NA' + headers['X-RANGER-Requester'] = request.headers[ + 'X-RANGER-Requester'] if 'X-RANGER-Requester' in request.headers else \ + '' + + LOG.debug("Wrapper JSON before sending action: {0} to Rds Proxy {1}".format(action, pretty_text)) + LOG.info("Sending to RDS Server: " + conf.api.rds_server.base + conf.api.rds_server.resources) + if action == "post": + resp = requests.post(conf.api.rds_server.base + conf.api.rds_server.resources, + data=wrapper_json, + headers=headers, verify=conf.verify) + elif action == "put": + resp = requests.put(conf.api.rds_server.base + conf.api.rds_server.resources, + data=wrapper_json, + headers=headers, verify=conf.verify) + elif action == "delete": + resp = requests.delete(conf.api.rds_server.base + conf.api.rds_server.resources, + data=wrapper_json, + headers=headers, verify=conf.verify) + else: + raise Exception("Invalid action in RdxProxy.send_image(" + "image_dict, transaction_id, action) " + "action can be post or delete, " + "got {0}".format(action)) + + content = resp.content + LOG.debug("return from rds server status code: {0} " + "content: {1}".format(resp.status_code, resp.content)) + if resp.content and 200 <= resp.status_code < 300: + content = resp.json() + else: + # In case of error from rds, the response is WSME format response. + # the error message is within the 'faultstring' + raise ErrorStatus(resp.status_code, + json.loads(content)["faultstring"]) + + except Exception as exp: + LOG.log_exception("ImageLogic - Failed to update image", exp) + raise exp + + return content + + +@di.dependsOn('requests') +def get_status(resource_id, json_convert=True): + requests = di.resolver.unpack(send_image) + + resp = requests.get(conf.api.rds_server.base + conf.api.rds_server.status + resource_id, verify=conf.verify) + if json_convert: + resp = resp.json() + return resp diff --git a/orm/services/image_manager/ims/tests/__init__.py b/orm/services/image_manager/ims/tests/__init__.py new file mode 100755 index 00000000..d726c618 --- /dev/null +++ b/orm/services/image_manager/ims/tests/__init__.py @@ -0,0 +1,22 @@ +import os +from pecan import set_config +from pecan.testing import load_test_app +from unittest import TestCase + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """Used for functional tests where you need to lcp_core your + + literal application and its integration with the framework. + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/orm/services/image_manager/ims/tests/config.py b/orm/services/image_manager/ims/tests/config.py new file mode 100755 index 00000000..21f0a721 --- /dev/null +++ b/orm/services/image_manager/ims/tests/config.py @@ -0,0 +1,107 @@ +from ims.tests.simple_hook_mock import SimpleHookMock + +global SimpleHookMock + +# Server Specific Configurations +server = { + 'port': '8084', + 'host': '0.0.0.0', + 'name': 'ims' +} + +# Pecan Application Configurations +app = { + 'root': 'ims.controllers.root.RootController', + 'modules': ['ims'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/ims/templates', + 'debug': True, + 'errors': { + '__force_dict__': True + }, + 'hooks': lambda: [SimpleHookMock()] +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'ims': {'level': 'DEBUG', 'handlers': ['console'], + 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], + 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +database = { + 'host': 'localhost', + 'username': 'root', + 'password': 'stack', + 'db_name': 'orm_ims_db', + +} + +database['connection_string'] = 'mysql://{0}:{1}@{2}:3306/{3}'.format(database['username'], + database['password'], + database['host'], + database['db_name']) + +application_root = 'http://localhost:{0}'.format(server['port']) + +api = { + 'uuid_server': { + 'base': 'http://127.0.0.1:8090/', + 'uuids': 'v1/uuids' + }, + 'rds_server': { + # 'base': 'http://172.20.90.179:8777/', + 'base': 'http://127.0.0.1:8777/', + 'resources': 'v1/rds/resources', + 'status': 'v1/rds/status/resource/' + }, + 'rms_server': { + 'base': 'http://127.0.0.1:8080/', + # 'base': 'http://172.20.90.179:8080/', + 'groups': 'v1/orm/groups', + 'regions': 'v1/orm/regions', + 'cache_seconds': 60 + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } + +} + +verify = False + +authentication = { + "enabled": False, + "mech_id": "admin", + "mech_pass": "stack", + "rms_url": "http://172.20.90.174:8080", + "tenant_name": "admin", + "keystone_version": "2.0", + "token_role": "admin", + "policy_file": "/opt/app/orm/aic-orm-ims/ims/etc/policy.json" +} diff --git a/orm/services/image_manager/ims/tests/controllers/__init__.py b/orm/services/image_manager/ims/tests/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/controllers/v1/__init__.py b/orm/services/image_manager/ims/tests/controllers/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/__init__.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/images/__init__.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_customers.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_customers.py new file mode 100755 index 00000000..6c9999a0 --- /dev/null +++ b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_customers.py @@ -0,0 +1,186 @@ +import json +from ims.tests import FunctionalTest +from orm_common.injector import injector +from ims.controllers.v1.orm.images import customers +from ims.persistency.wsme.models import ImageWrapper + +import mock +from ims.logic.error_base import ErrorStatus +from wsme.exc import ClientSideError + + +utils_mock = None +image_logic_mock = None + +return_error = 0 + + +class TestTenantController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_add_customers_sanity(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + response = self.app.post_json('/v1/orm/images/id/customers', CUSTOMERS) + self.assertEqual(ImageWrapper().tojson(), response.json) + self.assertEqual(201, response.status_int) + + @mock.patch.object(customers, 'err_utils') + def test_add_customers_errorstatus_raised(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.post_json('/v1/orm/images/id/customers', CUSTOMERS, + expect_errors=True) + + self.assertEqual(404, response.status_int) + + @mock.patch.object(customers, 'err_utils') + def test_add_customers_other_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.post_json('/v1/orm/images/id/customers', CUSTOMERS, + expect_errors=True) + + self.assertEqual(500, response.status_int) + + def test_update_customers_success(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + response = self.app.put_json('/v1/orm/images/id/customers', CUSTOMERS) + + self.assertEqual(ImageWrapper().tojson(), response.json) + self.assertEqual(200, response.status_code) + + @mock.patch.object(customers, 'err_utils') + def test_update_customers_NotFound(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/id/customers', CUSTOMERS, + expect_errors=True) + + self.assertEqual(404, response.status_code) + + @mock.patch.object(customers, 'err_utils') + def test_update_customers_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/id/customers', CUSTOMERS, + expect_errors=True) + self.assertEqual(500, response.status_code) + + def test_delete_success(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/id/customers/1') + self.assertEqual(response.status_int, 204) + + @mock.patch.object(customers, 'err_utils') + def test_delete_not_found_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/id/customers/1', + expect_errors=True) + self.assertEqual(response.status_int, 404) + + @mock.patch.object(customers, 'err_utils') + def test_delete_general_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/id/customers/1', + expect_errors=True) + self.assertEqual(response.status_int, 500) + + +class ResponseMock: + def __init__(self, status_code=200, message=""): + self.status_code = status_code + self.message = message + + +def get_logic_mock(): + global image_logic_mock + image_logic_mock = mock.MagicMock() + + if return_error == 0: + image_logic_mock.add_customers.return_value = ImageWrapper() + image_logic_mock.replace_customers.return_value = ImageWrapper() + elif return_error == 1: + image_logic_mock.add_customers.side_effect = SystemError() + image_logic_mock.replace_customers.side_effect = SystemError() + image_logic_mock.delete_customer.side_effect = SystemError() + elif return_error == 2: + image_logic_mock.add_customers.side_effect = ErrorStatus(status_code=404) + image_logic_mock.replace_customers.side_effect = ErrorStatus(status_code=404) + image_logic_mock.delete_customer.side_effect = ErrorStatus(status_code=404) + + return image_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = mock.MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + if return_error: + utils_mock.create_existing_uuid.side_effect = TypeError('test') + else: + utils_mock.create_existing_uuid.return_value = 'some_uuid' + + return utils_mock + + +def get_error(transaction_id, status_code, error_details=None, + message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) + + +CUSTOMERS = { + "customers": [ + "tenant1" + ] +} diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_enabled.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_enabled.py new file mode 100755 index 00000000..bf1d897a --- /dev/null +++ b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_enabled.py @@ -0,0 +1,110 @@ +import mock +import json +from ims.controllers.v1.orm.images import enabled +from ims.persistency.wsme.models import ImageWrapper + +from ims.logic.error_base import ErrorStatus + +from ims.tests import FunctionalTest +from wsme.exc import ClientSideError +from orm_common.injector import injector + +return_error = 0 + + +class TestGetImageDetails(FunctionalTest): + """Main put for image activate/deactivate test case.""" + + def setUp(self): + FunctionalTest.setUp(self) + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_enabled_put_sanity(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + response = self.app.put_json('/v1/orm/images/a/enabled/', ENABLED_JSON) + + self.assertEqual(ImageWrapper().tojson(), response.json) + self.assertEqual(200, response.status_code) + + @mock.patch.object(enabled, 'err_utils') + def test_enabled_put_image_not_found(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/a/enabled/', ENABLED_JSON, + expect_errors=True) + + self.assertEqual(404, response.status_code) + + @mock.patch.object(enabled, 'err_utils') + def test_enabled_put_other_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/a/enabled/', ENABLED_JSON, + expect_errors=True) + + self.assertEqual(500, response.status_code) + + +def get_logic_mock(): + global image_logic_mock + image_logic_mock = mock.MagicMock() + + if return_error == 0: + image_logic_mock.enable_image.return_value = ImageWrapper() + elif return_error == 1: + image_logic_mock.enable_image.side_effect = SystemError() + elif return_error == 2: + image_logic_mock.enable_image.side_effect = ErrorStatus( + status_code=404) + elif return_error == 3: + image_logic_mock.enable_image.side_effect = ErrorStatus( + status_code=409) + + return image_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = mock.MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + if return_error: + utils_mock.create_existing_uuid.side_effect = TypeError('test') + else: + utils_mock.create_existing_uuid.return_value = 'some_uuid' + + return utils_mock + + +def get_error(transaction_id, status_code, error_details=None, + message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) + + +ENABLED_JSON = { + "enabled": False +} diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_images.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_images.py new file mode 100755 index 00000000..126f3c7d --- /dev/null +++ b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_images.py @@ -0,0 +1,332 @@ +"""Images unittests module.""" +from ims.controllers.v1.orm.images import images +from ims.persistency.wsme.models import ImageWrapper, ImageSummaryResponse +from ims.tests import FunctionalTest +from ims.logic.error_base import ErrorStatus +import mock +import json +from wsme.exc import ClientSideError +from orm_common.injector import injector + + +utils_mock = None +image_logic_mock = None + +return_error = 0 + + +class TestGetImageDetails(FunctionalTest): + """Main get_image_details test case.""" + + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_get_sanity(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.get('/v1/orm/images/test') + self.assertEqual(ImageWrapper().tojson(), response.json) + + @mock.patch.object(images, 'err_utils') + def test_get_image_not_found(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.get('/v1/orm/images/test', expect_errors=True) + self.assertEqual(404, response.status_code) + + @mock.patch.object(images, 'err_utils') + def test_get_image_other_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.get('/v1/orm/images/test', expect_errors=True) + self.assertEqual(500, response.status_int) + + +class TestDeleteImage(FunctionalTest): + """main test delete image.""" + + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_delete_success(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/test') + self.assertEqual(response.status_int, 204) + + @mock.patch.object(images, 'err_utils') + def test_delete_not_found_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/test', expect_errors=True) + self.assertEqual(response.status_int, 404) + + @mock.patch.object(images, 'err_utils') + def test_delete_general_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/test', expect_errors=True) + self.assertEqual(response.status_int, 500) + + +class TestUpdateImage(FunctionalTest): + """test update image.""" + + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_update_image_success(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + response = self.app.put_json('/v1/orm/images/updatetest', image_json) + + self.assertEqual(ImageWrapper().tojson(), response.json) + self.assertEqual(200, response.status_code) + + @mock.patch.object(images, 'err_utils') + def test_update_image_NotFound(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/updatetest', image_json, + expect_errors=True) + + self.assertEqual(404, response.status_code) + + @mock.patch.object(images, 'err_utils') + def test_update_image_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/updatetest', image_json, + expect_errors=True) + self.assertEqual(500, response.status_code) + + +class TestListImage(FunctionalTest): + """main test delete image.""" + + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_list_success(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.get('/v1/orm/images/?region=SAN1') + + self.assertEqual(ImageSummaryResponse().tojson(), response.json) + self.assertEqual(response.status_int, 200) + + @mock.patch.object(images, 'err_utils') + def test_list_not_found_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.get('/v1/orm/images/?region=rd', expect_errors=True) + + self.assertEqual(404, response.status_int) + + @mock.patch.object(images, 'err_utils') + def test_list_general_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.get('/v1/orm/images/?region=SAN1', expect_errors=True) + + self.assertEqual(500, response.status_int) + + +class TestCreateImage(FunctionalTest): + """Main create_image test case.""" + + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_create_sanity(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.post_json('/v1/orm/images', image_json) + self.assertEqual(ImageWrapper().tojson(), response.json) + self.assertEqual(201, response.status_int) + + @mock.patch.object(images, 'err_utils') + def test_create_errorstatus_raised(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 3 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.post_json('/v1/orm/images', image_json, + expect_errors=True) + + self.assertEqual(409, response.status_int) + + @mock.patch.object(images, 'err_utils') + def test_create_other_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.post_json('/v1/orm/images', image_json, + expect_errors=True) + + self.assertEqual(500, response.status_int) + + +def get_logic_mock(): + global image_logic_mock + image_logic_mock = mock.MagicMock() + + if return_error == 0: + image_logic_mock.update_image.return_value = ImageWrapper() + image_logic_mock.create_image.return_value = ImageWrapper() + image_logic_mock.get_image_by_uuid.return_value = ImageWrapper() + image_logic_mock.get_image_list_by_params.return_value = ImageSummaryResponse() + elif return_error == 1: + image_logic_mock.update_image.side_effect = SystemError() + image_logic_mock.create_image.side_effect = SystemError() + image_logic_mock.get_image_by_uuid.side_effect = SystemError() + image_logic_mock.get_image_list_by_params.side_effect = SystemError() + image_logic_mock.delete_image_by_uuid.side_effect = SystemError() + elif return_error == 2: + image_logic_mock.update_image.side_effect = ErrorStatus( + status_code=404) + image_logic_mock.create_image.side_effect = ErrorStatus( + status_code=404) + image_logic_mock.get_image_by_uuid.side_effect = ErrorStatus( + status_code=404) + image_logic_mock.get_image_list_by_params.side_effect = ErrorStatus( + status_code=404) + image_logic_mock.delete_image_by_uuid.side_effect = ErrorStatus( + status_code=404) + elif return_error == 3: + image_logic_mock.create_image.side_effect = ErrorStatus( + status_code=409) + image_logic_mock.update_image.side_effect = ErrorStatus( + status_code=409) + + return image_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = mock.MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + if return_error: + utils_mock.create_existing_uuid.side_effect = TypeError('test') + else: + utils_mock.create_existing_uuid.return_value = 'some_uuid' + + return utils_mock + + +def get_error(transaction_id, status_code, error_details=None, + message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) + + +image_json = \ + { + "image": + { + "name": "abcde1e236", + "url": "https://mirrors.it.att.com/images/image-name", + "visibility": "private", + "disk-format": "raw", + "container-format": "bare", + "min-disk": 1, + "min-ram": 1, + "tags": ["tag"], + "properties": { + "property1": "value1" + }, + "regions": [{ + "name": "rdm1", + "type": "single" + }], + "customers": [ + "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + ] + } + } diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_metadata.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_metadata.py new file mode 100755 index 00000000..df7186d1 --- /dev/null +++ b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_metadata.py @@ -0,0 +1,77 @@ +import mock +import json +from wsme.exc import ClientSideError +from ims.tests import FunctionalTest + +from ims.controllers.v1.orm.images import metadata + + +metadata_input = { + "metadata": { + "checksum": "1", + "virtual_size": "@", + "size": "3" + } +} + + +class TestMetaDataController(FunctionalTest): + """metadata controller(api) unittests.""" + + @staticmethod + def get_error(transaction_id, status_code, error_details=None, + message=None): + return ClientSideError(json.dumps( + {'code': status_code, 'type': 'test', 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test'}), status_code=status_code) + + def setUp(self): + FunctionalTest.setUp(self) + + def tearDown(self): + FunctionalTest.tearDown(self) + + @mock.patch.object(metadata, 'di') + def test_post_metadata_success(self, mock_di): + mock_di.resolver.unpack.return_value = get_mocks() + response = self.app.post_json( + '/v1/orm/images/image_id/regions/region_name/metadata', + metadata_input) + self.assertEqual(200, response.status_code) + + @mock.patch.object(metadata, 'err_utils') + @mock.patch.object(metadata, 'di') + def test_post_metadata_not_found(self, mock_di, mock_error_utils): + mock_error_utils.get_error = self.get_error + mock_di.resolver.unpack.return_value = get_mocks(error=404) + response = self.app.post_json( + '/v1/orm/images/image_id/regions/region_name/metadata', + metadata_input, expect_errors=True) + self.assertEqual(404, response.status_code) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + 'not found') + + @mock.patch.object(metadata, 'err_utils') + @mock.patch.object(metadata, 'di') + def test_post_metadata_error(self, mock_di, mock_error_utils): + mock_error_utils.get_error = self.get_error + mock_di.resolver.unpack.return_value = get_mocks(error=500) + response = self.app.post_json( + '/v1/orm/images/image_id/regions/region_name/metadata', + metadata_input, expect_errors=True) + self.assertEqual(500, response.status_code) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + 'unknown error') + + +def get_mocks(error=None): + + metadata_logic = mock.MagicMock() + utils = mock.MagicMock() + metadata_logic.add_metadata.return_value = mock.MagicMock() + if error: + metadata_logic.add_metadata.side_effect = {404: metadata.ErrorStatus(error, 'not found'), + 500: Exception("unknown error")}[error] + return metadata_logic, utils diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_regions.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_regions.py new file mode 100755 index 00000000..3d204ba1 --- /dev/null +++ b/orm/services/image_manager/ims/tests/controllers/v1/orm/images/test_regions.py @@ -0,0 +1,188 @@ +import json +from ims.tests import FunctionalTest +from orm_common.injector import injector +from ims.controllers.v1.orm.images import regions +from ims.persistency.wsme.models import RegionWrapper + +import mock +from ims.logic.error_base import ErrorStatus +from wsme.exc import ClientSideError + + +utils_mock = None +image_logic_mock = None + +return_error = 0 + + +class TestRegionController(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + injector.override_injected_dependency(('image_logic', get_logic_mock())) + injector.override_injected_dependency(('utils', get_utils_mock())) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_add_regions_sanity(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + response = self.app.post_json('/v1/orm/images/id/regions', REGIONS) + self.assertEqual(RegionWrapper().tojson(), response.json) + self.assertEqual(201, response.status_int) + + @mock.patch.object(regions, 'err_utils') + def test_add_regions_errorstatus_raised(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.post_json('/v1/orm/images/id/regions', REGIONS, + expect_errors=True) + + self.assertEqual(404, response.status_int) + + @mock.patch.object(regions, 'err_utils') + def test_add_regions_other_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.post_json('/v1/orm/images/id/regions', REGIONS, + expect_errors=True) + + self.assertEqual(500, response.status_int) + + def test_update_regions_success(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + response = self.app.put_json('/v1/orm/images/id/regions', REGIONS) + + self.assertEqual(RegionWrapper().tojson(), response.json) + self.assertEqual(200, response.status_code) + + @mock.patch.object(regions, 'err_utils') + def test_update_regions_NotFound(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/id/regions', REGIONS, + expect_errors=True) + + self.assertEqual(404, response.status_code) + + @mock.patch.object(regions, 'err_utils') + def test_update_regions_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.put_json('/v1/orm/images/id/regions', REGIONS, + expect_errors=True) + self.assertEqual(500, response.status_code) + + def test_delete_success(self): + global return_error + return_error = 0 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/id/regions/1') + self.assertEqual(response.status_int, 204) + + @mock.patch.object(regions, 'err_utils') + def test_delete_not_found_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 2 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/id/regions/1', + expect_errors=True) + self.assertEqual(response.status_int, 404) + + @mock.patch.object(regions, 'err_utils') + def test_delete_general_error(self, mock_err_utils): + mock_err_utils.get_error = get_error + + global return_error + return_error = 1 + injector.override_injected_dependency(('image_logic', get_logic_mock())) + + response = self.app.delete('/v1/orm/images/id/regions/1', + expect_errors=True) + self.assertEqual(response.status_int, 500) + + +class ResponseMock: + def __init__(self, status_code=200, message=""): + self.status_code = status_code + self.message = message + + +def get_logic_mock(): + global image_logic_mock + image_logic_mock = mock.MagicMock() + + if return_error == 0: + image_logic_mock.add_regions.return_value = RegionWrapper() + image_logic_mock.replace_regions.return_value = RegionWrapper() + elif return_error == 1: + image_logic_mock.add_regions.side_effect = SystemError() + image_logic_mock.replace_regions.side_effect = SystemError() + image_logic_mock.delete_region.side_effect = SystemError() + elif return_error == 2: + image_logic_mock.add_regions.side_effect = ErrorStatus(status_code=404) + image_logic_mock.replace_regions.side_effect = ErrorStatus(status_code=404) + image_logic_mock.delete_region.side_effect = ErrorStatus(status_code=404) + + return image_logic_mock + + +def get_utils_mock(): + global utils_mock + utils_mock = mock.MagicMock() + + utils_mock.make_transid.return_value = 'some_trans_id' + utils_mock.audit_trail.return_value = None + utils_mock.make_uuid.return_value = 'some_uuid' + + if return_error: + utils_mock.create_existing_uuid.side_effect = TypeError('test') + else: + utils_mock.create_existing_uuid.return_value = 'some_uuid' + + return utils_mock + + +def get_error(transaction_id, status_code, error_details=None, message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) + + +REGIONS = { + "regions": [ + { + "name": "rdm1", + "type": "single" + } + ] +} diff --git a/orm/services/image_manager/ims/tests/controllers/v1/orm/test_logs.py b/orm/services/image_manager/ims/tests/controllers/v1/orm/test_logs.py new file mode 100755 index 00000000..30799d77 --- /dev/null +++ b/orm/services/image_manager/ims/tests/controllers/v1/orm/test_logs.py @@ -0,0 +1,42 @@ +from ims.tests import FunctionalTest + + +class TestLogsController(FunctionalTest): + """logs controller unittests.""" + + def setUp(self): + FunctionalTest.setUp(self) + + def tearDown(self): + FunctionalTest.tearDown(self) + + def test_logs_api_put_success(self): + level = 'info' + response = self.app.put('/v1/orm/logs/{}'.format(level)) + self.assertEqual(response.json, + {"result": "Log level changed to {}.".format(level)}) + self.assertEqual(201, response.status_code) + + def test_logs_api_put_level_none(self): + response = self.app.put('/v1/orm/logs/', expect_errors=True) + self.assertEqual(response.status_code, 400) + + def test_logs_api_put_level_bad(self): + level = "not_valid_level" + response = self.app.put('/v1/orm/logs/{}'.format(level), + expect_errors=True) + print response + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json['faultstring'], + "The given log level [{}] doesn't exist.".format( + level)) + + def test_logs_api_put_level_bad(self): + level = "not_valid_level" + response = self.app.put('/v1/orm/logs/{}'.format(level), + expect_errors=True) + print response + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json['faultstring'], + "The given log level [{}] doesn't exist.".format( + level)) diff --git a/orm/services/image_manager/ims/tests/logic/__init__.py b/orm/services/image_manager/ims/tests/logic/__init__.py new file mode 100755 index 00000000..d726c618 --- /dev/null +++ b/orm/services/image_manager/ims/tests/logic/__init__.py @@ -0,0 +1,22 @@ +import os +from pecan import set_config +from pecan.testing import load_test_app +from unittest import TestCase + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """Used for functional tests where you need to lcp_core your + + literal application and its integration with the framework. + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/orm/services/image_manager/ims/tests/logic/test_image_logic.py b/orm/services/image_manager/ims/tests/logic/test_image_logic.py new file mode 100755 index 00000000..f95d90bd --- /dev/null +++ b/orm/services/image_manager/ims/tests/logic/test_image_logic.py @@ -0,0 +1,704 @@ +import mock +from ims.logic import image_logic +from ims.tests import FunctionalTest +from ims.persistency.sql_alchemy.db_models import Image +from ims.persistency.wsme import models + + +class RDSGetStatus(): + def __init__(self, status_code=200): + self.status_code = status_code + + def json(self): + return {'status': 'Success'} + + +class ImageTest(): + def __init__(self, id=None, status=None, regions=[], min_ram=None, + customers=[]): + self.id = id + self.status = status + self.regions = regions + self.min_ram = min_ram + self.created_at = "12345678" + self.updated_at = "12345678" + self.customers = customers + + def to_db_model(self): + return ImageTest() + + def validate_model(self, context=None): + pass + + +class ImageWrapperTest(): + def __init__(self, image=ImageTest(id=1, status='')): + self.image = image + + def to_db_model(self): + return ImageWrapperTest() + + def validate_model(self, context=None): + pass + + +class MyError(Exception): + def __init__(self, message=None): + self.message = message + + +class RdsResponse(object): + """class.""" + + def __init__(self): + self.status_code = 200 + + def json(self): + return {'status': 'Success'} + + +resolved_regions = [{'type': 'single', 'name': 'rdm1'}] + +visibility = "private" +regions = [] +image_status_dict = {u'regions': [{u'status': u'Submitted', + u'resource_id': + u'edf1a8152b974eb28a6f4aa3dee3190d', + u'timestamp': 1471954276950, + u'region': u'rdm1', + u'ord_notifier_id': u'', + u'ord_transaction_id': + u'b18836a0-692a-11e6-82f3-005056a5129b', + u'error_code': u'', u'error_msg': u''}], + u'status': u'Pending'} + + +class TestImageLogic(FunctionalTest): + @mock.patch.object(image_logic, 'di') + def test_get_image_by_uuid_image_not_found(self, mock_di): + mock_rds_proxy = mock.MagicMock() + my_get_image = mock.MagicMock() + my_get_image.get_image_by_id.return_value = None + my_get_record = mock.MagicMock() + my_get_record.get_record.return_value = my_get_image + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm, mock_rds_proxy + try: + image_logic.get_image_by_uuid('te') + except image_logic.ErrorStatus as e: + self.assertEqual(e.status_code, 404) + + @mock.patch.object(image_logic.ImsUtils, 'get_server_links', + return_value=["ip", "path"]) + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'utils') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_get_image_by_uuid_image_no_status(self, mock_image, + mock_utils, mock_di, mock_links): + mock_rds_proxy = mock.MagicMock() + mock_rds_proxy.get_status.return_value = RDSGetStatus(status_code=404) + mock_image.from_db_model.return_value = ImageWrapperTest() + my_get_image = mock.MagicMock() + my_get_record = mock.MagicMock() + my_get_record.get_record.return_value = my_get_image + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm, mock_rds_proxy + result = image_logic.get_image_by_uuid('test') + self.assertEqual(result.image.status, '') + + @mock.patch.object(image_logic.ImsUtils, 'get_server_links', + return_value=["ip", "path"]) + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'utils') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_get_image_by_uuid_image_sanity(self, mock_image, + mock_utils, mock_di, mock_links): + mock_rds_proxy = mock.MagicMock() + mock_rds_proxy.get_status.return_value = RDSGetStatus() + mock_utils.get_resource_status.return_value = {'status': 'Success'} + my_get_image = mock.MagicMock() + my_get_record = mock.MagicMock() + my_get_record.get_record.return_value = my_get_image + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm, mock_rds_proxy + result = image_logic.get_image_by_uuid('test') + self.assertEqual(result.image.status, 'Success') + + +class TestDeleteImageLogic(FunctionalTest): + """test delete image.""" + + @mock.patch.object(image_logic, 'update_region_actions', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_delete_image_success(self, mock_di, mock_update_region): + mock_rds_proxy, mock_data_manager = get_data_manager_mock( + get_existing_region_names=[]) + mock_rds_proxy.get_status.return_value = RdsResponse() + mock_di.resolver.unpack.return_value = (mock_rds_proxy, + mock_data_manager) + global regions + regions = [] + image_logic.delete_image_by_uuid("image_uuid", "transaction_id") + + regions = [] + image_logic.delete_image_by_uuid("image_uuid", "transaction_id") + + @mock.patch.object(image_logic, 'update_region_actions', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_delete_image_success_nords(self, mock_di, mock_update_region): + mock_rds_proxy, mock_data_manager = \ + get_data_manager_mock(imagejson={"regions": {}}, + get_existing_region_names=[]) + mock_rds_proxy.get_status.return_value = RdsResponse() + mock_di.resolver.unpack.return_value = (mock_rds_proxy, + mock_data_manager) + image_logic.delete_image_by_uuid("image_uuid", "transaction_id") + + @mock.patch.object(image_logic, 'update_region_actions', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_delete_image_notfound_error(self, mock_di, mock_update_region): + mock_rds_proxy, mock_data_manager = \ + get_data_manager_mock(mock_sql_image=None) + mock_di.resolver.unpack.return_value = (mock_rds_proxy, + mock_data_manager) + try: + image_logic.delete_image_by_uuid("image_uuid", "transaction_id") + except Exception as e: + self.assertEqual(204, e.status_code) + + @mock.patch.object(image_logic, 'update_region_actions', + side_effect=ValueError('test')) + @mock.patch.object(image_logic, 'di') + def test_delete_image_other_error(self, mock_di, mock_update_region): + mock_rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = (mock_rds_proxy, + mock_data_manager) + self.assertRaises(image_logic.ErrorStatus, image_logic.delete_image_by_uuid, + "image_uuid", "transaction_id") + + +class TestUpdateImage(FunctionalTest): + """tests for update image.""" + + @mock.patch.object(image_logic, 'request') + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageTest(id="image_id")) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_update_image_success(self, mock_di, mock_send_to_rds_if_needed, + mock_get_image_by_uuid, request): + request.headers = {'X-RANGER-Requester': "orm_orm"} + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + result = image_logic.update_image(ImageTest(), "imgae_id", + "transaction_id") + + self.assertEqual("image_id", result.id) + + @mock.patch.object(image_logic, 'request') + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageTest(id="image_id")) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_update_image_notfound(self, mock_di, mock_send_to_rds_if_needed, + mock_get_image_by_uuid, request): + rds_proxy, mock_data_manager = get_data_manager_mock( + mock_sql_image=None) + mock_di.resolver.unpack.return_value = mock_data_manager + try: + result = image_logic.update_image(ImageTest(), "image_id", + "transaction_id") + except Exception as e: + self.assertEqual(404, e.status_code) + + @mock.patch.object(image_logic, 'request') + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageTest(id="image_id")) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', + side_effect=Exception("rds not found")) + @mock.patch.object(image_logic, 'di') + def test_update_image_anyerror(self, mock_di, mock_send_to_rds_if_needed, + mock_get_image_by_uuid, request): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + + self.assertRaises(Exception, image_logic.update_image, ImageTest(), + "imgae_id", "transaction_id") + + +class TestActivateImageLogic(FunctionalTest): + """test activate/deactivate image.""" + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageTest(**{'status': 'Success'})) + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'utils') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_activate_image_activate_no_activated_image(self, + mock_image, + mock_utils, + mock_di, + mock_by_uuid): + mock_utils.get_resource_status.return_value = {'status': 'Success'} + + my_enabled = mock.MagicMock() + my_enabled.enabled = 0 + + my_get_image = mock.MagicMock() + my_get_image.get_image = mock.MagicMock(return_value=my_enabled) + + my_get_record = mock.MagicMock() + my_get_record.get_record.return_value = my_get_image + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm + result = image_logic.enable_image("test_id", 1, "transaction_id") + self.assertEqual(result.status, 'Success') + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageTest(**{'status': 'Success'})) + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'utils') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_activate_image_already_activated(self, mock_image, mock_utils, + mock_di, + mock_get_image_by_uuid): + + mock_utils.get_resource_status.return_value = {'status': 'Success'} + my_enabled = mock.MagicMock() + my_enabled.enabled = 1 + + my_get_image = mock.MagicMock() + my_get_image.get_image = mock.MagicMock(return_value=my_enabled) + + my_get_record = mock.MagicMock() + my_get_record.get_record.return_value = my_get_image + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm + result = image_logic.enable_image("test_id", 1, "transaction_id") + self.assertEqual(result.status, 'Success') + + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'utils') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_activate_image_image_not_found(self, mock_image, + mock_utils, + mock_di): + mock_utils.get_resource_status.return_value = {'status': 'Success'} + my_get_image = mock.MagicMock() + my_get_image.get_image.return_value = None + my_get_record = mock.MagicMock() + my_get_record.get_record.return_value = my_get_image + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm + try: + image_logic.enable_image("test_id", 1, "transaction_id") + except image_logic.ErrorStatus as e: + self.assertEqual(e.status_code, 404) + + @mock.patch.object(image_logic, 'LOG') + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'utils') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_activate_image_image_other_exception(self, + mock_image, + mock_utils, + mock_di, + log_moc): + my_get_image = mock.MagicMock() + my_get_image.get_image = mock.MagicMock(side_effect=MyError("activate_test")) + + my_get_record = mock.MagicMock() + my_get_record.get_record.return_value = my_get_image + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm + try: + image_logic.enable_image("test_id", 1, "transaction_id") + except Exception as e: + self.assertEqual(e.message, 'activate_test') + + +class TestListImageLogic(FunctionalTest): + @mock.patch.object(image_logic, 'di') + def test_list_image_not_found(self, mock_di): + my_get_image = mock.MagicMock() + my_get_image.get_image.return_value = None + my_get_record = mock.MagicMock() + my_get_record.get_record.side_effect = image_logic.ErrorStatus(404, 'a') + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm + try: + image_logic.get_image_list_by_params('a', 'b', 'c') + except image_logic.ErrorStatus as e: + self.assertEqual(e.status_code, 404) + + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_list_image_error(self, mock_image, mock_di): + my_get_image = mock.MagicMock() + my_get_record = mock.MagicMock() + my_get_record.get_record.side_effect = SystemError() + my_dm = mock.MagicMock(return_value=my_get_record) + + mock_di.resolver.unpack.return_value = my_dm + try: + image_logic.get_image_list_by_params('a', 'b', 'c') + except Exception as e: + self.assertEqual(e.message, '') + + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_list_image_sanity(self, mock_image, mock_di): + imagejson = [{"regions": {"name": "mdt1"}}] + mock_data_manager = mock.MagicMock() + mock_image_rec = mock.MagicMock() + image_json = mock.MagicMock() + image_json.return_value = imagejson + + mock_image_rec.get_images_by_criteria.return_value = Image() + my_dm = mock.MagicMock(mock_data_manager) + + mock_di.resolver.unpack.return_value = my_dm + result = image_logic.get_image_list_by_params('a', 'b', 'c') + self.assertEqual(len(result.images), 0) + + +class TestCreateImage(FunctionalTest): + def setUp(self): + FunctionalTest.setUp(self) + + def tearDown(self): + FunctionalTest.tearDown(self) + + @mock.patch.object(image_logic, 'request') + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'ImageWrapper') + @mock.patch.object(image_logic, 'get_image_by_uuid', return_value='test') + def test_create_image_sanity(self, mock_image, mock_di, mock_req, mock_get): + my_image = mock.MagicMock() + my_dm = mock.MagicMock() + my_dm.get_record.return_value = my_image + my_mock = mock.MagicMock(return_value=my_dm) + mock_di.resolver.unpack.return_value = my_mock + + result = image_logic.create_image(mock.MagicMock(), 'test1', 'test2') + self.assertEqual('test', result) + + @mock.patch.object(image_logic, 'request') + @mock.patch.object(image_logic, 'di') + @mock.patch.object(image_logic, 'ImageWrapper') + def test_create_image_validate_model_failure(self, mock_image, mock_di, + mock_request): + image = mock.MagicMock() + image.validate_model.side_effect = ValueError('test') + + self.assertRaises(ValueError, image_logic.create_image, image, + 'test1', 'test2') + + +class TestAddRegions(FunctionalTest): + """tests for add regions.""" + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest(regions=[models.Region('region')]))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_add_regions_success(self, mock_di, mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + regions_wrapper = mock.MagicMock() + regions_wrapper.regions = [mock.MagicMock()] + result = image_logic.add_regions('uuid', regions_wrapper, + 'transaction') + + self.assertEqual(len(result.regions), 1) + self.assertEqual('region', result.regions[0].name) + + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_add_regions_image_not_found(self, mock_di, mock_send_to_rds_if_needed): + rds_proxy, mock_data_manager = get_data_manager_mock(mock_sql_image=None) + mock_di.resolver.unpack.return_value = mock_data_manager + regions_wrapper = mock.MagicMock() + regions_wrapper.regions = [mock.MagicMock()] + self.assertRaises(image_logic.ErrorStatus, image_logic.add_regions, + 'uuid', regions_wrapper, 'transaction') + + @mock.patch.object(image_logic, 'get_image_by_uuid', + side_effect=ValueError('test')) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_add_regions_other_error(self, mock_di, mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + regions_wrapper = mock.MagicMock() + regions_wrapper.regions = [mock.MagicMock()] + self.assertRaises(ValueError, image_logic.add_regions, + 'uuid', regions_wrapper, 'transaction') + + +class TestReplaceRegions(FunctionalTest): + """tests for replace regions.""" + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest(regions=[models.Region('region')]))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_replace_regions_success(self, mock_di, mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + regions_wrapper = mock.MagicMock() + regions_wrapper.regions = [mock.MagicMock()] + result = image_logic.replace_regions('uuid', regions_wrapper, + 'transaction') + + self.assertEqual(len(result.regions), 1) + self.assertEqual('region', result.regions[0].name) + + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_replace_regions_image_not_found(self, mock_di, mock_send_to_rds_if_needed): + rds_proxy, mock_data_manager = get_data_manager_mock(mock_sql_image=None) + mock_di.resolver.unpack.return_value = mock_data_manager + regions_wrapper = mock.MagicMock() + regions_wrapper.regions = [mock.MagicMock()] + self.assertRaises(image_logic.ErrorStatus, image_logic.replace_regions, + 'uuid', regions_wrapper, 'transaction') + + @mock.patch.object(image_logic, 'get_image_by_uuid', + side_effect=ValueError('test')) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_replace_regions_other_error(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + regions_wrapper = mock.MagicMock() + regions_wrapper.regions = [mock.MagicMock()] + self.assertRaises(ValueError, image_logic.replace_regions, + 'uuid', regions_wrapper, 'transaction') + + +class TestDeleteRegion(FunctionalTest): + """tests for delete region.""" + + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_delete_region_success(self, mock_di, mock_send_to_rds_if_needed): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + result = image_logic.delete_region('uuid', mock.MagicMock(), + 'transaction') + + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_delete_region_image_not_found(self, mock_di, + mock_send_to_rds_if_needed): + rds_proxy, mock_data_manager = get_data_manager_mock( + mock_sql_image=None) + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(image_logic.ErrorStatus, image_logic.delete_region, + 'uuid', mock.MagicMock(), 'transaction') + + @mock.patch.object(image_logic, 'get_image_by_uuid', + side_effect=ValueError('test')) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', + side_effect=ValueError) + @mock.patch.object(image_logic, 'di') + def test_delete_region_other_error(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(ValueError, image_logic.delete_region, + 'uuid', mock.MagicMock(), 'transaction') + + +class TestAddCustomers(FunctionalTest): + """tests for add customers.""" + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_add_customers_success(self, mock_di, mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + customers_wrapper = models.CustomerWrapper(['customer']) + result = image_logic.add_customers('uuid', customers_wrapper, + 'transaction') + self.assertEqual(result.image.customers.customers, ['customer']) + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_add_customers_image_not_found(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock( + mock_sql_image=None) + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(image_logic.ErrorStatus, image_logic.add_customers, + 'uuid', mock.MagicMock(), + 'transaction') + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_add_customers_public_image(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + global visibility + visibility = 'public' + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(image_logic.ErrorStatus, image_logic.add_customers, + 'uuid', mock.MagicMock(), + 'transaction') + visibility = 'private' + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', + side_effect=ValueError('Duplicate entry')) + @mock.patch.object(image_logic, 'di') + def test_add_customers_duplicate_entry(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(image_logic.ErrorStatus, image_logic.add_customers, + 'uuid', mock.MagicMock(), + 'transaction') + + +class TestReplaceCustomers(FunctionalTest): + """tests for replace customers.""" + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_replace_customers_success(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + global visibility + visibility = 'private' + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + customers_wrapper = models.CustomerWrapper(['customer']) + result = image_logic.replace_customers('uuid', customers_wrapper, + 'transaction') + self.assertEqual(result.image.customers.customers, ['customer']) + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_replace_customers_image_not_found(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock( + mock_sql_image=None) + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(image_logic.ErrorStatus, + image_logic.replace_customers, + 'uuid', mock.MagicMock(), + 'transaction') + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', return_value=True) + @mock.patch.object(image_logic, 'di') + def test_replace_customers_public_image(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + global visibility + visibility = 'public' + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(ValueError, + image_logic.replace_customers, + 'uuid', mock.MagicMock(), + 'transaction') + visibility = 'private' + + @mock.patch.object(image_logic, 'get_image_by_uuid', + return_value=ImageWrapperTest( + image=ImageTest( + customers=models.CustomerWrapper(['customer'])))) + @mock.patch.object(image_logic, 'send_to_rds_if_needed', + side_effect=ValueError('Duplicate entry')) + @mock.patch.object(image_logic, 'di') + def test_replace_customers_duplicate_entry(self, mock_di, + mock_send_to_rds_if_needed, + mock_get_image_by_uuid): + rds_proxy, mock_data_manager = get_data_manager_mock() + mock_di.resolver.unpack.return_value = mock_data_manager + self.assertRaises(image_logic.ErrorStatus, + image_logic.replace_customers, + 'uuid', mock.MagicMock(), + 'transaction') + + +def get_data_manager_mock(get_existing_region_names={"name": "mdt1"}, + imagejson={"regions": {"name": "mdt1"}}, + delete_image_by_id=True, + begin_transaction=True, + flush=True, + send_image=True, + mock_sql_image=True): + mock_rds_proxy = mock.MagicMock() + mock_data_manager = mock.MagicMock() + mock_data_manager_return_value = mock.MagicMock() + mock_image_rec = mock.MagicMock() + image_json = mock.MagicMock() + image_json.return_value = imagejson + if mock_sql_image: + mock_sql_image = mock.MagicMock() + mock_sql_image.__json__ = image_json + mock_sql_image.visibility = visibility + mock_sql_image.get_proxy_dict = mock.MagicMock(return_value={'regions': regions}) + mock_sql_image.get_existing_region_names.return_value = \ + get_existing_region_names + mock_image_rec.get_image_by_id.return_value = mock_sql_image + mock_image_rec.delete_image_by_id.return_value = delete_image_by_id + mock_image_rec.insert.return_value = True + mock_data_manager_return_value.begin_transaction.return_value = \ + begin_transaction + mock_data_manager_return_value.flush.return_value = flush + mock_data_manager_return_value.get_record.return_value = mock_image_rec + mock_rds_proxy.send_image.return_value = send_image + mock_data_manager.return_value = mock_data_manager_return_value + + return mock_rds_proxy, mock_data_manager diff --git a/orm/services/image_manager/ims/tests/logic/test_meta_data.py b/orm/services/image_manager/ims/tests/logic/test_meta_data.py new file mode 100755 index 00000000..97558e7d --- /dev/null +++ b/orm/services/image_manager/ims/tests/logic/test_meta_data.py @@ -0,0 +1,54 @@ +from ims.logic import metadata_logic +from ims.tests import FunctionalTest +from ims.persistency.sql_alchemy.db_models import ImageRegion +from ims.persistency.wsme.models import MetadataWrapper, Metadata +from ims.persistency.wsme import models +import mock + + +class TestMetaData(FunctionalTest): + """metadata uni tests.""" + + def setUp(self): + FunctionalTest.setUp(self) + + def tearDown(self): + FunctionalTest.tearDown(self) + + @mock.patch.object(metadata_logic, 'di') + def test_add_metadtat_sucess(self, metadta_mock): + data_manager = get_data_maneger_mock_metadata(image_rec=True) + metadta_mock.resolver.unpack.return_value = data_manager + result = metadata_logic.add_metadata("id", "region", {}) + + @mock.patch.object(metadata_logic, 'di') + def test_add_metadtat_notfound(self, metadta_mock): + data_manager = get_data_maneger_mock_metadata() + metadta_mock.resolver.unpack.return_value = data_manager + with self.assertRaises(metadata_logic.ErrorStatus): + metadata_logic.add_metadata("id", "region", {}) + + @mock.patch.object(metadata_logic, 'di') + def test_add_metadtat_with_regions_success(self, metadta_mock): + data_manager = get_data_maneger_mock_metadata(image_rec=True, + regions=[ImageRegion(region_name="region")]) + metadta_mock.resolver.unpack.return_value = data_manager + metadata_logic.add_metadata("id", "region", + MetadataWrapper(Metadata("1", "2", "3"))) + + +def get_data_maneger_mock_metadata(image_rec=None, regions=[]): + data_manager = mock.MagicMock() + + DataManager = mock.MagicMock() + db_record = mock.MagicMock() + sql_record = mock.MagicMock() + + sql_record.regions = regions + db_record.get_image_by_id.return_value = None + if image_rec: + db_record.get_image_by_id.return_value = sql_record + + DataManager.get_record.return_value = db_record + data_manager.return_value = DataManager + return data_manager diff --git a/orm/services/image_manager/ims/tests/persistency/__init__.py b/orm/services/image_manager/ims/tests/persistency/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/persistency/sql_alchemy/__init__.py b/orm/services/image_manager/ims/tests/persistency/sql_alchemy/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/persistency/sql_alchemy/images/__init__.py b/orm/services/image_manager/ims/tests/persistency/sql_alchemy/images/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/persistency/sql_alchemy/images/test_image_record.py b/orm/services/image_manager/ims/tests/persistency/sql_alchemy/images/test_image_record.py new file mode 100755 index 00000000..58661cc3 --- /dev/null +++ b/orm/services/image_manager/ims/tests/persistency/sql_alchemy/images/test_image_record.py @@ -0,0 +1,166 @@ +from ims.persistency.sql_alchemy import data_manager +from ims.tests import FunctionalTest +from ims.persistency.sql_alchemy.db_models import Image + +import mock + + +class MyError(Exception): + def __init__(self, message=None): + self.message = message + + +class TestImageRecord(FunctionalTest): + def test_insert_sanity(self): + session = mock.MagicMock() + session.add = mock.MagicMock() + + dm = data_manager.ImageRecord(session) + + dm.insert(Image()) + + assert session.add.called + + def test_insert_error(self): + add = mock.MagicMock(return_value=SystemError()) + session = mock.MagicMock(return_value=add) + + dm = data_manager.ImageRecord(session) + + self.assertRaises(Exception, dm.insert) + + def test_get_images_by_name_sanity(self): + session = mock.MagicMock() + all = mock.MagicMock() + query = mock.MagicMock(return_value=all) + + dm = data_manager.ImageRecord(session) + + dm.create_images_by_name_query = mock.MagicMock() + dm.customise_query = mock.MagicMock(return_value=query) + + dm.get_images_by_name('a') + + assert query.all.called + + def test_get_images_by_name_error(self): + session = mock.MagicMock() + all = mock.MagicMock() + query = mock.MagicMock(return_value=SystemError()) + + dm = data_manager.ImageRecord(session) + + dm.create_images_by_name_query = mock.MagicMock(query) + dm.customise_query = mock.MagicMock(return_value=SystemError()) + + self.assertRaises(Exception, dm.get_images_by_name) + + self.assertRaises(Exception, dm.insert) + + def test_get_images_by_criteria_sanity(self): + join = mock.MagicMock() + filter = mock.MagicMock(return_value=[Image()]) + query = mock.MagicMock() + query.join = mock.MagicMock(return_value=join) + query.filter = mock.MagicMock(return_value=filter) + query.all = mock.MagicMock(return_value=[Image()]) + join.filter = mock.MagicMock(return_value=filter) + + session = mock.MagicMock() + session.query = mock.MagicMock(return_value=query) + + dm = data_manager.ImageRecord(session) + dm.customise_query = mock.MagicMock() + + r = dm.get_images_by_criteria(visibility='a', region='b', Customer='c') + + self.assertEqual(len(r), 0) + + def test_get_images_by_criteria_error(self): + join = mock.MagicMock() + filter = mock.MagicMock(return_value=[Image()]) + query = mock.MagicMock() + query.join = mock.MagicMock(return_value=join) + query.filter = mock.MagicMock(return_value=filter) + query.all = mock.MagicMock(return_value=SystemError()) + join.filter = mock.MagicMock(return_value=filter) + + session = mock.MagicMock() + session.query = mock.MagicMock(return_value=query) + + dm = data_manager.ImageRecord(session) + dm.customise_query = mock.MagicMock(return_value=SystemError()) + + self.assertRaises(Exception, dm.get_images_by_criteria) + + def test_create_images_by_name_query_sanity(self): + filter = mock.MagicMock(return_value=[Image()]) + query = mock.MagicMock() + query.filter = mock.MagicMock(filter) + + session = mock.MagicMock() + session.query = mock.MagicMock(query) + + dm = data_manager.ImageRecord(session) + + r = dm.create_images_by_name_query('a') + + self.assertEqual(len(r), 0) + + def test_create_images_by_name_query_error(self): + query = mock.MagicMock() + query.filter = mock.MagicMock(return_value=SystemError()) + + session = mock.MagicMock() + session.query = mock.MagicMock(return_value=SystemError()) + + dm = data_manager.ImageRecord(session) + + self.assertRaises(Exception, dm.create_images_by_name_query, 'a') + + def test_get_image_sanity(self): + query = mock.MagicMock() + query.first = mock.MagicMock(Image()) + + session = mock.MagicMock() + session.query = mock.MagicMock(query) + + dm = data_manager.ImageRecord(session) + + r = dm.get_image('a') + + self.assertEqual(len(r), 0) + + def test_get_image_query_error(self): + query = mock.MagicMock() + query.first = mock.MagicMock(return_value=SystemError()) + + session = mock.MagicMock() + session.query = mock.MagicMock(return_value=SystemError()) + + dm = data_manager.ImageRecord(session) + + self.assertRaises(Exception, dm.get_image, 'a') + + def test_delete_image_by_id_sanity(self): + format = mock.MagicMock() + execute = mock.MagicMock(return_value=format) + + session = mock.MagicMock() + session.connection = mock.MagicMock(return_value=execute) + + dm = data_manager.ImageRecord(session) + dm.customise_query = mock.MagicMock() + + dm.delete_image_by_id('a') + + assert session.connection.called + + def test_delete_image_by_id_error(self): + session = mock.MagicMock() + session.connection = mock.MagicMock(return_value=SystemError()) + + dm = data_manager.ImageRecord(session) + dm.customise_query = mock.MagicMock() + + self.assertRaises(Exception, dm.delete_image_by_id, 'a') diff --git a/orm/services/image_manager/ims/tests/proxies/__init__.py b/orm/services/image_manager/ims/tests/proxies/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/tests/proxies/rds_proxy.py b/orm/services/image_manager/ims/tests/proxies/rds_proxy.py new file mode 100755 index 00000000..8dce962b --- /dev/null +++ b/orm/services/image_manager/ims/tests/proxies/rds_proxy.py @@ -0,0 +1,86 @@ +import mock +from ims.proxies import rds_proxy +from ims.tests import FunctionalTest + + +class Response: + def __init__(self, status_code, content): + self.status_code = status_code + self.content = content + + def json(self): + return {"res": self.content} + + +class TestRdsProxy(FunctionalTest): + """rds proxy unittests.""" + + def setUp(self): + FunctionalTest.setUp(self) + + def tearDown(self): + FunctionalTest.tearDown(self) + + @mock.patch.object(rds_proxy, 'di') + @mock.patch.object(rds_proxy, 'request') + def test_send_post_rds_success(self, mock_request, mock_di): + req = mock.MagicMock() + req.post.return_value = Response(201, "any cont") + mock_di.resolver.unpack.return_value = req + result = rds_proxy.send_image({"not real": "only for test"}, "tran_id", + "post") + self.assertEqual(result, {'res': 'any cont'}) + + @mock.patch.object(rds_proxy, 'di') + @mock.patch.object(rds_proxy, 'request') + def test_send_put_rds_success(self, mock_request, mock_di): + req = mock.MagicMock() + req.put.return_value = Response(200, "any cont") + mock_di.resolver.unpack.return_value = req + result = rds_proxy.send_image({"not real": "only for test"}, "tran_id", + "put") + self.assertEqual(result, {'res': 'any cont'}) + + @mock.patch.object(rds_proxy, 'di') + @mock.patch.object(rds_proxy, 'request') + def test_send_delete_rds_success(self, mock_request, mock_di): + req = mock.MagicMock() + req.delete.return_value = Response(204, "any cont") + mock_di.resolver.unpack.return_value = req + result = rds_proxy.send_image({"not real": "only for test"}, "tran_id", + "delete") + self.assertEqual(result, {'res': 'any cont'}) + + @mock.patch.object(rds_proxy, 'di') + def test_send_bad_rds_bad(self, mock_di): + req = mock.MagicMock() + req.post.return_value = Response(204, "any cont") + mock_di.resolver.unpack.return_value = req + with self.assertRaises(Exception) as exp: + rds_proxy.send_image({"not real": "only for test"}, "tran_id", + "any") + + @mock.patch.object(rds_proxy, 'di') + @mock.patch.object(rds_proxy, 'request') + def test_send_rds_req_bad_resp(self, mock_request, mock_di): + req = mock.MagicMock() + req.post.return_value = Response(301, '{"faultstring": ":("}') + mock_di.resolver.unpack.return_value = req + with self.assertRaises(rds_proxy.ErrorStatus): + rds_proxy.send_image({"not real": "only for test"}, "tran_id", + "post") + + @mock.patch.object(rds_proxy, 'di') + def test_get_rsource_status_rds(self, mock_di): + req = mock.MagicMock() + req.get.return_value = Response(200, "any cont") + mock_di.resolver.unpack.return_value = req + result = rds_proxy.get_status(resource_id="123abc", json_convert=True) + self.assertEqual(result, {'res': 'any cont'}) + + @mock.patch.object(rds_proxy, 'di') + def test_get_rsource_status_rds_nojson(self, mock_di): + req = mock.MagicMock() + req.get.return_value = Response(200, "any cont") + mock_di.resolver.unpack.return_value = req + rds_proxy.get_status(resource_id="123abc", json_convert=False) diff --git a/orm/services/image_manager/ims/tests/simple_hook_mock.py b/orm/services/image_manager/ims/tests/simple_hook_mock.py new file mode 100755 index 00000000..73b98ed1 --- /dev/null +++ b/orm/services/image_manager/ims/tests/simple_hook_mock.py @@ -0,0 +1,6 @@ +from pecan.hooks import PecanHook + + +class SimpleHookMock(PecanHook): + def before(self, state): + setattr(state.request, 'transaction_id', 'some_id') diff --git a/orm/services/image_manager/ims/tests/test_models.py b/orm/services/image_manager/ims/tests/test_models.py new file mode 100644 index 00000000..9df3f2db --- /dev/null +++ b/orm/services/image_manager/ims/tests/test_models.py @@ -0,0 +1,41 @@ +import mock +from ims.tests import FunctionalTest + +from ims.persistency.wsme import models + +GROUP_REGIONS = [ + "DPK", + "SNA1", + "SNA2" +] + + +class TestModels(FunctionalTest): + + def setUp(self): + FunctionalTest.setUp(self) + models.get_regions_of_group = mock.MagicMock(return_value=GROUP_REGIONS) + models.set_utils_conf = mock.MagicMock() + + def test_handle_group_success(self): + image = get_image_model() + image.handle_region_group() + + self.assertEqual(len(image.regions), 3) + + def test_handle_group_not_found(self): + models.get_regions_of_group = mock.MagicMock(return_value=None) + image = get_image_model() + + self.assertRaises(models.ErrorStatus, image.handle_region_group,) + + +def get_image_model(): + """ + this function create a customer model object for testing + :return: new customer object + """ + + image = models.Image(id='a', regions=[models.Region(name='r1', type='group')]) + + return image diff --git a/orm/services/image_manager/ims/utils/__init__.py b/orm/services/image_manager/ims/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/image_manager/ims/utils/authentication.py b/orm/services/image_manager/ims/utils/authentication.py new file mode 100755 index 00000000..3104b391 --- /dev/null +++ b/orm/services/image_manager/ims/utils/authentication.py @@ -0,0 +1,61 @@ +import logging +from keystone_utils import tokens +from orm_common.utils import api_error_utils as err_utils +from pecan import conf +from orm_common.policy import policy + +logger = logging.getLogger(__name__) + + +def _is_authorization_enabled(app_conf): + return app_conf.authentication.enabled + + +def _get_token_conf(app_conf): + mech_id = app_conf.authentication.mech_id + mech_password = app_conf.authentication.mech_pass + rms_url = app_conf.authentication.rms_url + tenant_name = app_conf.authentication.tenant_name + keystone_version = app_conf.authentication.keystone_version + conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name, + keystone_version) + return conf + + +def check_permissions(app_conf, token_to_validate, lcp_id): + logger.debug("Check permissions...start") + try: + if _is_authorization_enabled(app_conf): + if token_to_validate is not None and lcp_id is not None and str(token_to_validate).strip() != '' and str(lcp_id).strip() != '': + token_conf = _get_token_conf(app_conf) + logger.debug("Authorization: validating token=[{}] on lcp_id=[{}]".format(token_to_validate, lcp_id)) + is_permitted = tokens.is_token_valid(token_to_validate, lcp_id, token_conf) + logger.debug("Authorization: The token=[{}] on lcp_id=[{}] is [{}]" + .format(token_to_validate, lcp_id, "valid" if is_permitted else "invalid")) + else: + raise Exception("Token=[{}] and/or Region=[{}] are empty/none.".format(token_to_validate, lcp_id)) + else: + logger.debug("The authentication service is disabled. No authentication is needed.") + is_permitted = True + except Exception as e: + msg = "Fail to validate request. due to {}.".format(e.message) + logger.error(msg) + logger.exception(e) + is_permitted = False + logger.debug("Check permissions...end") + return is_permitted + + +def authorize(request, action): + if not _is_authorization_enabled(conf): + return + + auth_region = request.headers.get('X-Auth-Region') + auth_token = request.headers.get('X-Auth-Token') + message = "missing header {}".format( + 'X-Auth-Region' if auth_region is None else 'X-Auth-Token') + if auth_region is None or auth_token is None: + raise err_utils.get_error(request.transaction_id, + message="missing header {}".format(message), + status_code=400) + policy.authorize(action, request, conf) diff --git a/orm/services/image_manager/ims/utils/utils.py b/orm/services/image_manager/ims/utils/utils.py new file mode 100755 index 00000000..ce9a953f --- /dev/null +++ b/orm/services/image_manager/ims/utils/utils.py @@ -0,0 +1,16 @@ +from pecan import conf, request +import time + + +def convert_time_human(time_stamp): + return time.ctime(int(time_stamp)) + + +def get_server_links(id=None): + links = {'self': '{}'.format(request.url)} + self_links = '{}'.format(request.upath_info) + if id and id not in request.path: + links['self'] += '{}{}'.format('' if request.path[-1] == '/' else '/', + id) + self_links += '{}{}'.format('' if request.path[-1] == '/' else '/', id) + return links, self_links diff --git a/orm/services/image_manager/pycharm_init.py b/orm/services/image_manager/pycharm_init.py new file mode 100644 index 00000000..469a65d9 --- /dev/null +++ b/orm/services/image_manager/pycharm_init.py @@ -0,0 +1,3 @@ +from pecan.commands import CommandRunner +runner = CommandRunner() +runner.run(['serve', 'config.py']) diff --git a/orm/services/image_manager/scripts/db_scripts/create_db.sql b/orm/services/image_manager/scripts/db_scripts/create_db.sql new file mode 100755 index 00000000..df1384a7 --- /dev/null +++ b/orm/services/image_manager/scripts/db_scripts/create_db.sql @@ -0,0 +1,86 @@ +create database if not exists orm_ims_db DEFAULT CHARACTER SET utf8 COLLATE utf8_bin; +use orm_ims_db; + +#***** +#* MySql script for Creating Table image +#***** + +create table if not exists image + ( + id varchar(64) not null, + name varchar(64) not null, + enabled smallint not null, + url varchar(250) not null, + protected smallint not null, + visibility varchar(10) not null, + disk_format varchar(64) not null, + container_format varchar(64) not null, + min_disk integer not null, + min_ram integer not null, + owner varchar(128) not null, + `schema` varchar(128) not null, + created_at integer not null, + updated_at integer not null, + primary key (id), + unique name (name), + index visibility (visibility) + ); +# + +#***** +#* MySql script for Creating Table image_property +#***** + +create table if not exists image_property + ( + image_id varchar(64) not null, + key_name varchar(64) not null, + key_value varchar(64) not null, + primary key (image_id,key_name), + foreign key (image_id) references image(id) ON DELETE CASCADE ON UPDATE NO ACTION + ); +# + +#***** +#* MySql script for Creating Table image_region +#***** + +create table if not exists image_region + ( + image_id varchar(64) not null, + region_name varchar(64) not null, + region_type varchar(32) not null, + checksum varchar(64) not null, + size varchar(64) not null, + virtual_size varchar(64) not null, + primary key (image_id,region_name), + foreign key (image_id) references image(id) ON DELETE CASCADE ON UPDATE NO ACTION + ); +# + +#***** +#* MySql script for Creating Table image_tag +#***** + +create table if not exists image_tag + ( + image_id varchar(64) not null, + tag varchar(64) not null, + primary key (image_id,tag), + foreign key (image_id) references image(id) ON DELETE CASCADE ON UPDATE NO ACTION + ); +# + + +#***** +#* MySql script for Creating Table image_customer +#***** + +create table if not exists image_customer + ( + image_id varchar(64) not null, + customer_id varchar(64) not null, + primary key (image_id,customer_id), + foreign key (image_id) references image(id) ON DELETE CASCADE ON UPDATE NO ACTION + ); +# diff --git a/orm/services/image_manager/scripts/db_scripts/update_db.sql b/orm/services/image_manager/scripts/db_scripts/update_db.sql new file mode 100755 index 00000000..abec7984 --- /dev/null +++ b/orm/services/image_manager/scripts/db_scripts/update_db.sql @@ -0,0 +1,29 @@ +USE orm_ims_db; + +DELIMITER ;; + +DROP PROCEDURE IF EXISTS add_region_properies; +CREATE PROCEDURE add_region_properies() +BEGIN + + -- add a column safely + IF NOT EXISTS( (SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() + AND COLUMN_NAME='checksum' AND TABLE_NAME='image_region') ) THEN + ALTER TABLE image_region ADD checksum varchar(64) NOT NULL; + END IF; + + IF NOT EXISTS( (SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() + AND COLUMN_NAME='size' AND TABLE_NAME='image_region') ) THEN + ALTER TABLE image_region ADD size varchar(64) NOT NULL; + END IF; + + IF NOT EXISTS( (SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() + AND COLUMN_NAME='virtual_size' AND TABLE_NAME='image_region') ) THEN + ALTER TABLE image_region ADD virtual_size varchar(64) NOT NULL; + END IF; +END ;; + +CALL add_region_properies() ;; + +DELIMITER ; + diff --git a/orm/services/image_manager/scripts/shell_scripts/create_db.sh b/orm/services/image_manager/scripts/shell_scripts/create_db.sh new file mode 100644 index 00000000..4f93fc8c --- /dev/null +++ b/orm/services/image_manager/scripts/shell_scripts/create_db.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo Creating database: orm_ims_db + +mysql -uroot -pstack < ../db_scripts/create_db.sql + +echo Done ! diff --git a/orm/services/image_manager/scripts/shell_scripts/update_db.sh b/orm/services/image_manager/scripts/shell_scripts/update_db.sh new file mode 100755 index 00000000..79ca5c57 --- /dev/null +++ b/orm/services/image_manager/scripts/shell_scripts/update_db.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo Creating database: orm_ims_db + +mysql -uroot -pstack < ../db_scripts/update_db.sql + +echo Done ! diff --git a/orm/services/image_manager/swagger/swagger.yaml b/orm/services/image_manager/swagger/swagger.yaml new file mode 100755 index 00000000..87bb21f1 --- /dev/null +++ b/orm/services/image_manager/swagger/swagger.yaml @@ -0,0 +1,751 @@ +# this is an example of the Uber API +# as a demonstration of an API spec in YAML +swagger: '2.0' +info: + version: 3.5.0 + title: Image API'S + description: Image api's + All api's should supply two header parameters + X-Auth-Token - Token received from keystone + X-Auth-Region - The region + There is an optional header parameter X-RANGER-Client which tells who is the client for the api's + +# the domain of the service +host: 135.76.2.229 +# array of all schemes that your API supports +schemes: + - https + +# will be prefixed to all paths +basePath: /v1/orm +produces: + - application/json + +paths: + /image: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + post: + summary: create image + description: | + The post image endpoint create a new image and send it to the Heat to create this image in each region needed + Return all data of the new image + parameters: + - name: full image + in: body + description: input body to create full image + schema: + $ref: '#/definitions/Image' + required: true + + tags: + - Image + + responses: + 201: + description: image is created + schema: + $ref: '#/definitions/Image' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + 409: + description: Duplicate Error + schema: + $ref: '#/definitions/409' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + get: + summary: get a list of imagess by criteria (visibility, region, tenant) + description: | + The get images retrieve all imagess matched to the above criterias + parameters: + - name: visibility + in: query + type: "string" + enum: [ + "public", + "private" + ] + required: false + description: public or private image + - name: region + in: query + type: "string" + description: region name + required: false + - name: tenant + in: query + type: "string" + description: tenant name. + required: false + tags: + - Image + responses: + 200: + description: list of images by criteria + schema: + $ref: '#/definitions/Images' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /images/{image_uuid_or_name}: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + get: + summary: get image (one) by id or name + description: retrieve image has this param if not found get image by name that maches this param + parameters: + - name: image_uuid_or_name + in: path + type: string + description: uuid or name of the requested image. + required: true + tags: + - Image + responses: + 200: + description: the requested image + schema: + $ref: '#/definitions/Image' + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + delete: + summary: delete image (one) by id or name + description: delete image need to remove all regions one by one before deleting the image other wise will fail to delete + parameters: + - name: image_uuid_or_name + in: path + type: string + description: uuid or name of the image to delete. + required: true + tags: + - Image + responses: + 405: + description: metod not allowed + schema: + $ref: '#/definitions/Error' + 204: + description: no content + + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + put: + summary: get image (one) by id or name + description: retrieve image has this param if not found get image by name that maches this param + parameters: + - name: image_uuid_or_name + in: path + type: string + description: uuid or name of the requested image. + required: true + - name: full image + in: body + description: input body to create full image + schema: + $ref: '#/definitions/Image' + required: true + tags: + - Image + responses: + 201: + description: image is created + schema: + $ref: '#/definitions/Image' + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /image/{image_uuid}/regions: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + post: + summary: add region to an image + description: | + it will add the regions in the body to the image and send them to heat to be created + parameters: + - name: image_uuid + in: path + type: string + description: uuid of the requested image. + required: true + - name: regions + in: body + description: list of full regions + schema: + $ref: '#/definitions/Regions' + required: true + tags: + - Regions + responses: + 201: + description: regions added to image + schema: + $ref: '#/definitions/Regions' + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 409: + description: Duplicate Error + schema: + $ref: '#/definitions/409' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + put: + summary: replace region in an image + description: | + it will remove all existing regions in the image and replace them in the regions in request body and the old regions will be sent to be removed from heat and the new ones will be sent as created + parameters: + - name: image_uuid + in: path + type: string + description: uuid of the requested image. + required: true + - name: regions + in: body + description: list of full regions + schema: + $ref: '#/definitions/Regions' + required: true + tags: + - Regions + responses: + 201: + description: regions added to image + schema: + $ref: '#/definitions/Regions' + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /image/{image_uuid}/regions/{region_name}: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + delete: + summary: delete region from image by region name name + description: delete region from image by region name name and send it to heat to be removed + parameters: + - name: image_uuid + in: path + type: string + description: uuid of the requested image. + required: true + - name: region_name + in: path + type: string + description: name of the region need to delete + required: true + tags: + - Regions + responses: + 204: + description: no content + 404: + description: image not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /image/{image_uuid}/enabled/: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + put: + summary: enable and disable image + description: got in all regions related to this image and set them to enable/disable and send them to head to be modified + parameters: + - name: image_uuid + in: path + type: string + description: uuid of the requested image. + required: true + - name: enabled input json + in: body + description: input body enable/disable image + schema: + $ref: '#/definitions/Enable' + required: true + tags: + - Enabled + responses: + 200: + description: image is created + schema: + $ref: '#/definitions/Image' + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /images/{image_uuid}/regions/{region_name}/metadata: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + post: + summary: add metadata to region in an image + description: add metadata to specified region related to image this operation does not require send this region to heat its only in the ims db (it will replace existing ones) + parameters: + - name: image_uuid + in: path + type: string + description: uuid of the requested image. + required: true + - name: region_name + in: path + type: string + description: region name of requested region + required: true + - name: metadata input json + in: body + description: the metadata json that need to be added to the region + schema: + $ref: '#/definitions/MetaDataWrapper' + required: true + tags: + - Metadata + + responses: + 201: + description: added metadata to region + schema: + type: string + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /images/{image_uuid}/customers/: + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - $ref: "#/parameters/Client" + post: + summary: add customer to an image + description: add customer to private image + parameters: + - name: image_uuid + in: path + type: string + description: uuid of the requested image. + required: true + - name: customer input json + in: body + description: input body customer + schema: + $ref: '#/definitions/Customer' + required: true + tags: + - Customers + responses: + 200: + description: customers added to image + schema: + $ref: '#/definitions/Image' + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 409: + description: Duplicate Error + schema: + $ref: '#/definitions/409' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + put: + summary: replace image customers + description: replace all customers to specific image + parameters: + - name: image_uuid + in: path + type: string + description: uuid of the requested image. + required: true + - name: customer input json + in: body + description: input body customer + schema: + $ref: '#/definitions/Customer' + required: true + tags: + - Customers + responses: + 200: + description: customers added to image + schema: + $ref: '#/definitions/Image' + 404: + description: image not found + schema: + $ref: '#/definitions/404' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + +definitions: + Image: + type: object + required: + - name + - url + - visibility + - disk-format + - container-format + properties: + name: + type: string + description: name + example: Ubuntu + enabled: + type: boolean + description: tells if image enabled + example: true + url: + type: string + description: url + example: /v1/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file + visibility: + type: string + description: if image visable + example: private + disk-format: + type: string + description: disk-format + example: raw + container-format: + type: string + description: container-format + example: bare + min-disk: + type: integer + description: int32 + example: 0 + min-ram: + type: integer + description: int32 + example: 1024 + tags: + type: array + items: + type: string + example: tag1 + properties: + $ref: '#/definitions/Dictionary' + regions: + type: array + items: + $ref: '#/definitions/Region' + customers: + type: array + items: + type: string + example: 1a15e3ea-bc4f-4aec-8a98-8feb2537a354 + owner: + type: string + description: owner + example: bab7d5c60cd041a0a36f7c4b6e1dd978 + schema: + type: string + description: schema + example: /v2/schemas/image + protected: + type: boolean + description: if image is protected + example: true + + Enable: + type: object + properties: + enabled: + type: boolean + description: can be true or false + example: true + + Customer: + type: object + properties: + customers: + type: array + items: + type: string + + Images: + type: array + items: + properties: + name: + type: string + description: name + visibility: + type: string + description: name + id: + type: string + description: name + + MetaDataWrapper: + type: object + properties: + metadata: + $ref: '#/definitions/MetaData' + + MetaData: + type: object + properties: + checksum: + type: string + example: '1024' + size: + type: string + example: '123' + virtual_size: + type: string + example: '123' + + Region: + type: object + required: + - name + - type + properties: + name: + type: string + example: tn17 + type: + type: string + description: single or group + example: single + status: + type: string + readOnly: true + example: error + error_message: + type: string + readOnly: true + example: fail to create + + OutputRegion: + type: object + properties: + name: + type: string + example: tn17 + type: + type: string + description: single or group + example: single + status: + type: string + error_message: + type: string + checksum: + type: string + example: '1024' + size: + type: string + example: '123' + virtual_size: + type: string + example: '123' + + OutputRegions: + type: array + items: + $ref: '#/definitions/OutputRegion' + + Regions: + type: array + items: + $ref: '#/definitions/Region' + + 200: + type: object + properties: + name: + type: string + description: name + visibility: + type: string + description: name + id: + type: string + description: name + + Error: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + 409: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + 400: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + 404: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + Dictionary: + type: object + additionalProperties: + type: "string" + example: {"property1": "value1"} + + +parameters: + Token: + name: X-Auth-Token + in: header + description: Token from keystone + required: true + type: string + + Region: + name: X-Auth-Region + in: header + description: Region + required: true + type: string + + Client: + name: X-Auth-Client + in: header + description: Client name + required: false + type: string diff --git a/orm/services/image_manager/tox.ini b/orm/services/image_manager/tox.ini new file mode 100755 index 00000000..0b65030e --- /dev/null +++ b/orm/services/image_manager/tox.ini @@ -0,0 +1,29 @@ +[tox] +envlist=py27,pep8,cover + +[testenv] +setenv= PYTHONPATH={toxinidir}:{toxinidir}/ims/external_mock/ +deps= -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +;commands= +; pip install git+ssh://jenkins@gerrit.mtn5.cci.att.com:29418/aic-orm-common@dev +;# pip install ../aic-orm-common/ +; +; python setup.py testr --coverage --slowest +; coverage report --omit=ims/persistency/sql_alchemy/*,ims/persistency/wsme/* +; coverage html --omit=ims/persistency/sql_alchemy/*,ims/persistency/wsme/* + +[testenv:pep8] +commands= +# pip install git+ssh://jenkins@gerrit.mtn5.cci.att.com:29418/aic-orm-common@master +# pip install ../aic-orm-common/ + py.test --pep8 -m pep8 + +[testenv:cover] +commands= +# pip install git+ssh://jenkins@gerrit.mtn5.cci.att.com:29418/aic-orm-common@master +# pip install ../aic-orm-common/ + coverage run setup.py test + coverage report + coverage html diff --git a/orm/services/region_manager/MANIFEST.in b/orm/services/region_manager/MANIFEST.in new file mode 100755 index 00000000..c922f11a --- /dev/null +++ b/orm/services/region_manager/MANIFEST.in @@ -0,0 +1 @@ +recursive-include public * diff --git a/orm/services/region_manager/__init__.py b/orm/services/region_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/config.py b/orm/services/region_manager/config.py new file mode 100755 index 00000000..4474beac --- /dev/null +++ b/orm/services/region_manager/config.py @@ -0,0 +1,119 @@ +from orm_common.hooks.api_error_hook import APIErrorHook +from orm_common.hooks.security_headers_hook import SecurityHeadersHook +from orm_common.hooks.transaction_id_hook import TransactionIdHook + +global TransactionIdHook +global APIErrorHook +global SecurityHeadersHook + +# Server Specific Configurations +server = { + 'port': '8080', + 'host': '0.0.0.0', + 'name': 'rms' +} + +# Pecan Application Configurations +app = { + 'root': 'rms.controllers.root.RootController', + 'modules': ['rms'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/rms/templates', + 'debug': True, + 'hooks': lambda: [TransactionIdHook(), APIErrorHook(), SecurityHeadersHook()] +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'rms': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], + 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], + 'propagate': False}, + 'audit_client': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], + 'propagate': False}, + 'orm_common': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], + 'propagate': False}, + 'keystone_utils': {'level': 'DEBUG', + 'handlers': ['console', 'Logfile'], + 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + }, + 'Logfile': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'maxBytes': 50000000, + 'backupCount': 10, + 'filename': '/opt/app/orm/rms/rms.log', + 'formatter': 'simple' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +# user input validations +region_options = { + 'allowed_status_values': [ + 'functional', + 'maintenance', + 'down', + 'building' + ], + 'endpoints_types_must_have': [ + 'dashboard', + 'identity', + 'ord' + ] +} + +# DB configurations +database = { + 'url': 'mysql://root:stack@127.0.0.1/orm_rms_db?charset=utf8', + 'max_retries': 3, + 'retries_interval': 10 +} + +endpoints = { + 'lcp': 'http://127.0.0.1:8082/lcp' +} + +api = { + 'uuid_server': { + 'base': 'http://127.0.0.1:8090/', + 'uuids': 'v1/uuids' + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } +} + +verify = False + +authentication = { + "enabled": True, + "mech_id": "admin", + "mech_pass": "stack", + "tenant_name": "admin", + # The Keystone version currently in use. Can be either "2.0" or "3" + "keystone_version": "2.0", + "policy_file": "/opt/app/orm/rms/rms/etc/policy.json" +} diff --git a/orm/services/region_manager/cover/coverage_html.js b/orm/services/region_manager/cover/coverage_html.js new file mode 100644 index 00000000..f6f5de20 --- /dev/null +++ b/orm/services/region_manager/cover/coverage_html.js @@ -0,0 +1,584 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// Find all the elements with shortkey_* class, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + $("*[class*='shortkey_']").each(function (i, e) { + $.each($(e).attr("class").split(" "), function (i, c) { + if (/^shortkey_/.test(c)) { + $(document).bind('keydown', c.substr(9), function () { + $(e).click(); + }); + } + }); + }); +}; + +// Create the events for the help panel. +coverage.wire_up_help_panel = function () { + $("#keyboard_icon").click(function () { + // Show the help panel, and position it so the keyboard icon in the + // panel is in the same place as the keyboard icon in the header. + $(".help_panel").show(); + var koff = $("#keyboard_icon").offset(); + var poff = $("#panel_icon").position(); + $(".help_panel").offset({ + top: koff.top-poff.top, + left: koff.left-poff.left + }); + }); + $("#panel_icon").click(function () { + $(".help_panel").hide(); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + var table = $("table.index"); + var table_rows = table.find("tbody tr"); + var table_row_names = table_rows.find("td.name a"); + var no_rows = $("#no_rows"); + + // Create a duplicate table footer that we can modify with dynamic summed values. + var table_footer = $("table.index tfoot tr"); + var table_dynamic_footer = table_footer.clone(); + table_dynamic_footer.attr('class', 'total_dynamic hidden'); + table_footer.after(table_dynamic_footer); + + // Observe filter keyevents. + $("#filter").on("keyup change", $.debounce(150, function (event) { + var filter_value = $(this).val(); + + if (filter_value === "") { + // Filter box is empty, remove all filtering. + table_rows.removeClass("hidden"); + + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + + // Hide placeholder, show table. + if (no_rows.length > 0) { + no_rows.hide(); + } + table.show(); + + } + else { + // Filter table items by value. + var hidden = 0; + var shown = 0; + + // Hide / show elements. + $.each(table_row_names, function () { + var element = $(this).parents("tr"); + + if ($(this).text().indexOf(filter_value) === -1) { + // hide + element.addClass("hidden"); + hidden++; + } + else { + // show + element.removeClass("hidden"); + shown++; + } + }); + + // Show placeholder if no rows will be displayed. + if (no_rows.length > 0) { + if (shown === 0) { + // Show placeholder, hide table. + no_rows.show(); + table.hide(); + } + else { + // Hide placeholder, show table. + no_rows.hide(); + table.show(); + } + } + + // Manage dynamic header: + if (hidden > 0) { + // Calculate new dynamic sum values based on visible rows. + for (var column = 2; column < 20; column++) { + // Calculate summed value. + var cells = table_rows.find('td:nth-child(' + column + ')'); + if (!cells.length) { + // No more columns...! + break; + } + + var sum = 0, numer = 0, denom = 0; + $.each(cells.filter(':visible'), function () { + var ratio = $(this).data("ratio"); + if (ratio) { + var splitted = ratio.split(" "); + numer += parseInt(splitted[0], 10); + denom += parseInt(splitted[1], 10); + } + else { + sum += parseInt(this.innerHTML, 10); + } + }); + + // Get footer cell element. + var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); + + // Set value into dynamic footer cell element. + if (cells[0].innerHTML.indexOf('%') > -1) { + // Percentage columns use the numerator and denominator, + // and adapt to the number of decimal places. + var match = /\.([0-9]+)/.exec(cells[0].innerHTML); + var places = 0; + if (match) { + places = match[1].length; + } + var pct = numer * 100 / denom; + footer_cell.text(pct.toFixed(places) + '%'); + } + else { + footer_cell.text(sum); + } + } + + // Hide standard footer, show dynamic footer. + table_footer.addClass("hidden"); + table_dynamic_footer.removeClass("hidden"); + } + else { + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + $("#filter").trigger("change"); +}; + +// Loaded on index.html +coverage.index_ready = function ($) { + // Look for a cookie containing previous sort settings: + var sort_list = []; + var cookie_name = "COVERAGE_INDEX_SORT"; + var i; + + // This almost makes it worth installing the jQuery cookie plugin: + if (document.cookie.indexOf(cookie_name) > -1) { + var cookies = document.cookie.split(";"); + for (i = 0; i < cookies.length; i++) { + var parts = cookies[i].split("="); + + if ($.trim(parts[0]) === cookie_name && parts[1]) { + sort_list = eval("[[" + parts[1] + "]]"); + break; + } + } + } + + // Create a new widget which exists only to save and restore + // the sort order: + $.tablesorter.addWidget({ + id: "persistentSort", + + // Format is called by the widget before displaying: + format: function (table) { + if (table.config.sortList.length === 0 && sort_list.length > 0) { + // This table hasn't been sorted before - we'll use + // our stored settings: + $(table).trigger('sorton', [sort_list]); + } + else { + // This is not the first load - something has + // already defined sorting so we'll just update + // our stored value to match: + sort_list = table.config.sortList; + } + } + }); + + // Configure our tablesorter to handle the variable number of + // columns produced depending on report options: + var headers = []; + var col_count = $("table.index > thead > tr > th").length; + + headers[0] = { sorter: 'text' }; + for (i = 1; i < col_count-1; i++) { + headers[i] = { sorter: 'digit' }; + } + headers[col_count-1] = { sorter: 'percent' }; + + // Enable the table sorter: + $("table.index").tablesorter({ + widgets: ['persistentSort'], + headers: headers + }); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + coverage.wire_up_filter(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + }); +}; + +// -- pyfile stuff -- + +coverage.pyfile_ready = function ($) { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === 'n') { + $(frag).addClass('highlight'); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + $(document) + .bind('keydown', 'j', coverage.to_next_chunk_nicely) + .bind('keydown', 'k', coverage.to_prev_chunk_nicely) + .bind('keydown', '0', coverage.to_top) + .bind('keydown', '1', coverage.to_first_chunk) + ; + + $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); + $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); + $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); + $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + + coverage.init_scroll_markers(); + + // Rebuild scroll markers after window high changing + $(window).resize(coverage.resize_scroll_markers); +}; + +coverage.toggle_lines = function (btn, cls) { + btn = $(btn); + var hide = "hide_"+cls; + if (btn.hasClass(hide)) { + $("#source ."+cls).removeClass(hide); + btn.removeClass(hide); + } + else { + $("#source ."+cls).addClass(hide); + btn.addClass(hide); + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return $("#t" + n); +}; + +// Return the nth line number div. +coverage.num_elt = function (n) { + return $("#n" + n); +}; + +// Return the container of all the code. +coverage.code_container = function () { + return $(".linenos"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.is_transparent = function (color) { + // Different browsers return different colors for "none". + return color === "transparent" || color === "rgba(0, 0, 0, 0)"; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var color, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + if (!c.is_transparent(color)) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_color = color; + while (next_color === color) { + probe++; + probe_line = c.line_elt(probe); + next_color = probe_line.css("background-color"); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + var c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var color = probe_line.css("background-color"); + while (probe > 0 && c.is_transparent(color)) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_color = color; + while (prev_color === color) { + probe--; + probe_line = c.line_elt(probe); + prev_color = probe_line.css("background-color"); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Return the line number of the line nearest pixel position pos +coverage.line_at_pos = function (pos) { + var l1 = coverage.line_elt(1), + l2 = coverage.line_elt(2), + result; + if (l1.length && l2.length) { + var l1_top = l1.offset().top, + line_height = l2.offset().top - l1_top, + nlines = (pos - l1_top) / line_height; + if (nlines < 1) { + result = 1; + } + else { + result = Math.ceil(nlines); + } + } + else { + result = 1; + } + return result; +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + var top = coverage.line_elt(coverage.sel_begin); + var next = coverage.line_elt(coverage.sel_end-1); + + return ( + (top.isOnScreen() ? 1 : 0) + + (next.isOnScreen() ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: select the top line on + // the screen. + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (probe_line.length === 0) { + return; + } + var the_color = probe_line.css("background-color"); + if (!c.is_transparent(the_color)) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var color = the_color; + while (probe > 0 && color === the_color) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + color = probe_line.css("background-color"); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + color = the_color; + while (color === the_color) { + probe++; + probe_line = c.line_elt(probe); + color = probe_line.css("background-color"); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + c.code_container().find(".highlight").removeClass("highlight"); + for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { + c.num_elt(probe).addClass("highlight"); + } + + c.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + // Need to move the page. The html,body trick makes it scroll in all + // browsers, got it from http://stackoverflow.com/questions/3042651 + var top = coverage.line_elt(coverage.sel_begin); + var top_pos = parseInt(top.offset().top, 10); + coverage.scroll_window(top_pos - 30); + } +}; + +coverage.scroll_window = function (to_pos) { + $("html,body").animate({scrollTop: to_pos}, 200); +}; + +coverage.finish_scrolling = function () { + $("html,body").stop(true, true); +}; + +coverage.init_scroll_markers = function () { + var c = coverage; + // Init some variables + c.lines_len = $('td.text p').length; + c.body_h = $('body').height(); + c.header_h = $('div#header').height(); + c.missed_lines = $('td.text p.mis, td.text p.par'); + + // Build html + c.resize_scroll_markers(); +}; + +coverage.resize_scroll_markers = function () { + var c = coverage, + min_line_height = 3, + max_line_height = 10, + visible_window_h = $(window).height(); + + $('#scroll_marker').remove(); + // Don't build markers if the window has no scroll bar. + if (c.body_h <= visible_window_h) { + return; + } + + $("body").append("
 
"); + var scroll_marker = $('#scroll_marker'), + marker_scale = scroll_marker.height() / c.body_h, + line_height = scroll_marker.height() / c.lines_len; + + // Line height must be between the extremes. + if (line_height > min_line_height) { + if (line_height > max_line_height) { + line_height = max_line_height; + } + } + else { + line_height = min_line_height; + } + + var previous_line = -99, + last_mark, + last_top; + + c.missed_lines.each(function () { + var line_top = Math.round($(this).offset().top * marker_scale), + id_name = $(this).attr('id'), + line_number = parseInt(id_name.substring(1, id_name.length)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.css({ + 'height': line_top + line_height - last_top + }); + } + else { + // Add colored line in scroll_marker block. + scroll_marker.append('
'); + last_mark = $('#m' + line_number); + last_mark.css({ + 'height': line_height, + 'top': line_top + }); + last_top = line_top; + } + + previous_line = line_number; + }); +}; diff --git a/orm/services/region_manager/cover/index.html b/orm/services/region_manager/cover/index.html new file mode 100644 index 00000000..672002e7 --- /dev/null +++ b/orm/services/region_manager/cover/index.html @@ -0,0 +1,455 @@ + + + + + + + + Coverage report + + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total1287160088%
rms/__init__.py000100%
rms/controllers/__init__.py000100%
rms/controllers/configuration.py1700100%
rms/controllers/lcp_controller.py5600100%
rms/controllers/logs.py362094%
rms/controllers/root.py121092%
rms/controllers/v2/__init__.py000100%
rms/controllers/v2/orm/__init__.py000100%
rms/controllers/v2/orm/resources/__init__.py000100%
rms/controllers/v2/orm/resources/groups.py12422082%
rms/controllers/v2/orm/resources/metadata.py8519078%
rms/controllers/v2/orm/resources/regions.py1875097%
rms/controllers/v2/orm/resources/status.py473094%
rms/controllers/v2/orm/root.py500100%
rms/controllers/v2/root.py300100%
rms/external_mock/__init__.py000100%
rms/external_mock/audit_client/__init__.py000100%
rms/external_mock/audit_client/api/__init__.py000100%
rms/external_mock/audit_client/api/audit.py4400%
rms/external_mock/keystone_utils/__init__.py000100%
rms/external_mock/keystone_utils/tokens.py51080%
rms/external_mock/orm_common/__init__.py000100%
rms/external_mock/orm_common/policy/__init__.py000100%
rms/external_mock/orm_common/policy/policy.py62067%
rms/external_mock/orm_common/utils/__init__.py000100%
rms/external_mock/orm_common/utils/api_error_utils.py21050%
rms/external_mock/orm_common/utils/utils.py61083%
rms/model/__init__.py300100%
rms/model/model.py1007093%
rms/model/url_parm.py634094%
rms/services/__init__.py000100%
rms/services/error_base.py182089%
rms/services/services.py17055068%
rms/storage/__init__.py000100%
rms/storage/base_data_manager.py2400100%
rms/storage/data_manager_factory.py1200100%
rms/storage/my_sql/__init__.py000100%
rms/storage/my_sql/data_manager.py27031089%
rms/utils/__init__.py000100%
rms/utils/authentication.py3200100%
+ +

+ No items found using the specified filter. +

+
+ + + + + diff --git a/orm/services/region_manager/cover/jquery.ba-throttle-debounce.min.js b/orm/services/region_manager/cover/jquery.ba-throttle-debounce.min.js new file mode 100644 index 00000000..648fe5d3 --- /dev/null +++ b/orm/services/region_manager/cover/jquery.ba-throttle-debounce.min.js @@ -0,0 +1,9 @@ +/* + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); diff --git a/orm/services/region_manager/cover/jquery.hotkeys.js b/orm/services/region_manager/cover/jquery.hotkeys.js new file mode 100644 index 00000000..09b21e03 --- /dev/null +++ b/orm/services/region_manager/cover/jquery.hotkeys.js @@ -0,0 +1,99 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "); + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + event.target.type === "text") ) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); diff --git a/orm/services/region_manager/cover/jquery.isonscreen.js b/orm/services/region_manager/cover/jquery.isonscreen.js new file mode 100644 index 00000000..0182ebd2 --- /dev/null +++ b/orm/services/region_manager/cover/jquery.isonscreen.js @@ -0,0 +1,53 @@ +/* Copyright (c) 2010 + * @author Laurence Wheway + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * @version 1.2.0 + */ +(function($) { + jQuery.extend({ + isOnScreen: function(box, container) { + //ensure numbers come in as intgers (not strings) and remove 'px' is it's there + for(var i in box){box[i] = parseFloat(box[i])}; + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( box.left+box.width-container.left > 0 && + box.left < container.width+container.left && + box.top+box.height-container.top > 0 && + box.top < container.height+container.top + ) return true; + return false; + } + }) + + + jQuery.fn.isOnScreen = function (container) { + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( $(this).offset().left+$(this).width()-container.left > 0 && + $(this).offset().left < container.width+container.left && + $(this).offset().top+$(this).height()-container.top > 0 && + $(this).offset().top < container.height+container.top + ) return true; + return false; + } +})(jQuery); diff --git a/orm/services/region_manager/cover/jquery.min.js b/orm/services/region_manager/cover/jquery.min.js new file mode 100644 index 00000000..e2efc335 --- /dev/null +++ b/orm/services/region_manager/cover/jquery.min.js @@ -0,0 +1,9404 @@ +/*! + * jQuery JavaScript Library v1.7.2 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Fri Jul 5 14:07:58 UTC 2013 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.2", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, pass ) { + var exec, + bulk = key == null, + i = 0, + length = elems.length; + + // Sets many values + if ( key && typeof key === "object" ) { + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); + } + chainable = 1; + + // Sets one value + } else if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = pass === undefined && jQuery.isFunction( value ); + + if ( bulk ) { + // Bulk operations only iterate when executing function values + if ( exec ) { + exec = fn; + fn = function( elem, key, value ) { + return exec.call( jQuery( elem ), value ); + }; + + // Otherwise they run against the entire set + } else { + fn.call( elems, value ); + fn = null; + } + } + + if ( fn ) { + for (; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + } + + chainable = 1; + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + fired = true; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + pixelMargin: true + }; + + // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead + jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for ( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, + paddingMarginBorderVisibility, paddingMarginBorder, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + paddingMarginBorder = "padding:0;margin:0;border:"; + positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; + paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; + style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; + html = "
" + + "" + + "
"; + + container = document.createElement("div"); + container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
t
"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + div.innerHTML = ""; + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.innerHTML = ""; + div.style.width = div.style.padding = "1px"; + div.style.border = 0; + div.style.overflow = "hidden"; + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = "block"; + div.style.overflow = "visible"; + div.innerHTML = "
"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + } + + div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + if ( window.getComputedStyle ) { + div.style.marginTop = "1%"; + support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; + } + + if ( typeof container.style.zoom !== "undefined" ) { + container.style.zoom = 1; + } + + body.removeChild( container ); + marginDiv = div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, part, attr, name, l, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attr = elem.attributes; + for ( l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split( ".", 2 ); + parts[1] = parts[1] ? "." + parts[1] : ""; + part = parts[1] + "!"; + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + data = this.triggerHandler( "getData" + part, [ parts[0] ] ); + + // Try to fetch any internally stored data first + if ( data === undefined && elem ) { + data = jQuery.data( elem, key ); + data = dataAttr( elem, key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } + + parts[1] = value; + this.each(function() { + var self = jQuery( this ); + + self.triggerHandler( "setData" + part, parts ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + part, parts ); + }); + }, null, value, arguments.length > 1, null, false ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise( object ); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, isBool, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + isBool = rboolean.test( name ); + + // See #9699 for explanation of this approach (setting first, then removal) + // Do not do this for boolean attributes (see #10870) + if ( !isBool ) { + jQuery.attr( elem, name, "" ); + } + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( isBool && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true, + coords: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: selector && quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + special = jQuery.event.special[ event.type ] || {}, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers that should run if there are delegated events + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + + // Don't process events on disabled elements (#6911, #8165) + if ( cur.disabled !== true ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { // && selector != null + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} +// Expose origPOS +// "global" as in regardless of relation to brackets/parens +Expr.match.globalPOS = origPOS; + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.globalPOS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /]", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*", "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers___init___py.html b/orm/services/region_manager/cover/rms_controllers___init___py.html new file mode 100644 index 00000000..e9a4bb7f --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/controllers/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_configuration_py.html b/orm/services/region_manager/cover/rms_controllers_configuration_py.html new file mode 100644 index 00000000..5fd6bb6f --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_configuration_py.html @@ -0,0 +1,157 @@ + + + + + + + + + + + Coverage for rms/controllers/configuration.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+ +
+

"""Configuration rest API input module.""" 

+

 

+

import logging 

+

 

+

from orm_common.utils import utils 

+

 

+

from pecan import conf 

+

from pecan import request 

+

from pecan import rest 

+

from wsmeext.pecan import wsexpose 

+

 

+

from rms.utils import authentication 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class ConfigurationController(rest.RestController): 

+

"""Configuration controller.""" 

+

 

+

@wsexpose(str, str, status_code=200) 

+

def get(self, dump_to_log='false'): 

+

"""get method. 

+

 

+

:param dump_to_log: A boolean string that says whether the 

+

configuration should be written to log 

+

:return: A pretty string that contains the service's configuration 

+

""" 

+

logger.info("Get configuration...") 

+

authentication.authorize(request, 'configuration:get') 

+

 

+

dump = dump_to_log.lower() == 'true' 

+

utils.set_utils_conf(conf) 

+

result = utils.report_config(conf, dump, logger) 

+

return result 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_lcp_controller_py.html b/orm/services/region_manager/cover/rms_controllers_lcp_controller_py.html new file mode 100644 index 00000000..90c29c78 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_lcp_controller_py.html @@ -0,0 +1,329 @@ + + + + + + + + + + + Coverage for rms/controllers/lcp_controller.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+ +
+

import logging 

+

 

+

from pecan import rest, request 

+

from pecan import conf 

+

 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

from rms.model import url_parm 

+

from rms.services.error_base import ErrorStatus 

+

from rms.services import services 

+

from rms.utils import authentication 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class LcpController(rest.RestController): 

+

 

+

@wsexpose(wtypes.text, rest_content_types='json') 

+

def get_all(self): 

+

""" 

+

This function is called when receiving /lcp without a parameter. 

+

parameter: 

+

None. 

+

return: entire list of lcp. 

+

""" 

+

logger.info('Received a GET request for all LCPs') 

+

authentication.authorize(request, 'lcp:get_all') 

+

 

+

zones = [] 

+

 

+

try: 

+

zones = get_zones() 

+

logger.debug('Returning LCP list: %s' % (zones,)) 

+

return zones 

+

 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(wtypes.text, str, rest_content_types='json') 

+

def get_one(self, lcp_id): 

+

 

+

logger.info('Received a GET request for LCP %s' % (id,)) 

+

authentication.authorize(request, 'lcp:get_one') 

+

 

+

zones = [] 

+

try: 

+

 

+

zones = get_zones() 

+

 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

for zone in zones: 

+

if zone["id"] == lcp_id: 

+

logger.debug('Returning: %s' % (zone,)) 

+

return zone 

+

 

+

error_msg = 'LCP %s not found' % (lcp_id,) 

+

logger.info(error_msg) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=error_msg, 

+

status_code=404) 

+

 

+

 

+

def get_zones(): 

+

""" 

+

This function returns the lcp list from CSV file. 

+

parameter: 

+

None. 

+

return: 

+

zone list in json format. 

+

""" 

+

logger.debug('Enter get_zones function') 

+

result = [] 

+

 

+

try: 

+

url_args = url_parm.UrlParms() 

+

zones = services.get_regions_data(url_args) 

+

 

+

for zone in zones.regions: 

+

result.append(build_zone_response(zone)) 

+

 

+

logger.debug("Available regions: {}".format(', '.join( 

+

[region["zone_name"] for region in result]))) 

+

 

+

except ErrorStatus as e: 

+

logger.debug(e.message) 

+

finally: 

+

return result 

+

 

+

 

+

def build_zone_response(zone): 

+

 

+

end_points_dict = {"identity": "", 

+

"dashboard": "", 

+

"ord": ""} 

+

for end_point in zone.endpoints: 

+

end_points_dict[end_point.type] = end_point.publicurl 

+

 

+

return dict( 

+

zone_name=zone.name, 

+

id=zone.id, 

+

status="1" if zone.status == "functional" else "0", 

+

design_type=zone.design_type, 

+

location_type=zone.location_type, 

+

vLCP_name=zone.vlcp_name, 

+

AIC_version=zone.ranger_agent_version, 

+

OS_version=zone.open_stack_version, 

+

keystone_EP=end_points_dict["identity"], 

+

horizon_EP=end_points_dict["dashboard"], 

+

ORD_EP=end_points_dict["ord"] 

+

) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_logs_py.html b/orm/services/region_manager/cover/rms_controllers_logs_py.html new file mode 100644 index 00000000..51ddc2a1 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_logs_py.html @@ -0,0 +1,237 @@ + + + + + + + + + + + Coverage for rms/controllers/logs.py: 94% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+ +
+

import logging 

+

from pecan import rest, request 

+

import wsme 

+

from wsmeext.pecan import wsexpose 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

from rms.utils import authentication 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class LogChangeResultWSME(wsme.types.DynamicBase): 

+

"""log change result wsme type.""" 

+

 

+

result = wsme.wsattr(str, mandatory=True, default=None) 

+

 

+

def __init__(self, **kwargs): 

+

""""init method.""" 

+

super(LogChangeResult, self).__init__(**kwargs) 

+

 

+

 

+

class LogChangeResult(object): 

+

"""log change result type.""" 

+

 

+

def __init__(self, result): 

+

""""init method.""" 

+

self.result = result 

+

 

+

 

+

class LogsController(rest.RestController): 

+

"""Logs Audit controller.""" 

+

 

+

@wsexpose(LogChangeResultWSME, str, status_code=201, 

+

rest_content_types='json') 

+

def put(self, level): 

+

"""update log level. 

+

 

+

:param level: the log level text name 

+

:return: 

+

""" 

+

 

+

logger.info("Changing log level to [{}]".format(level)) 

+

authentication.authorize(request, 'log:update') 

+

 

+

try: 

+

log_level = logging._levelNames.get(level.upper()) 

+

if log_level is not None: 

+

self._change_log_level(log_level) 

+

result = "Log level changed to {}.".format(level) 

+

logger.info(result) 

+

else: 

+

raise Exception( 

+

"The given log level [{}] doesn't exist.".format(level)) 

+

 

+

return LogChangeResult(result) 

+

 

+

except Exception as exception: 

+

logger.error("Fail to change log_level. Reason: {}".format( 

+

exception.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@staticmethod 

+

def _change_log_level(log_level): 

+

path = __name__.split('.') 

+

if len(path) > 0: 

+

root = path[0] 

+

root_logger = logging.getLogger(root) 

+

root_logger.setLevel(log_level) 

+

else: 

+

logger.info("Fail to change log_level to [{}]. " 

+

"the given log level doesn't exist.".format(log_level)) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_root_py.html b/orm/services/region_manager/cover/rms_controllers_root_py.html new file mode 100644 index 00000000..9a9cdf4b --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_root_py.html @@ -0,0 +1,159 @@ + + + + + + + + + + + Coverage for rms/controllers/root.py: 92% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+ +
+

from pecan import expose 

+

from lcp_controller import LcpController 

+

from logs import LogsController 

+

from configuration import ConfigurationController 

+

from rms.controllers.v2 import root 

+

 

+

 

+

class RootController(object): 

+

lcp = LcpController() 

+

logs = LogsController() 

+

configuration = ConfigurationController() 

+

v2 = root.V2Controller() 

+

 

+

@expose(template='json') 

+

def _default(self): 

+

""" 

+

Method to handle GET / 

+

parameters: None 

+

return: dict describing lcp rest version information 

+

""" 

+

return { 

+

"versions": { 

+

"values": [ 

+

{ 

+

"status": "stable", 

+

"id": "v2", 

+

"links": [ 

+

{ 

+

"href": "http://localhost:8789/" 

+

} 

+

] 

+

} 

+

] 

+

} 

+

} 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2___init___py.html b/orm/services/region_manager/cover/rms_controllers_v2___init___py.html new file mode 100644 index 00000000..bf5cfb53 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""orm package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_orm___init___py.html b/orm/services/region_manager/cover/rms_controllers_v2_orm___init___py.html new file mode 100644 index 00000000..40a61780 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_orm___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""resource package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_orm_resources___init___py.html b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources___init___py.html new file mode 100644 index 00000000..80509395 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""orm package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_groups_py.html b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_groups_py.html new file mode 100644 index 00000000..76366806 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_groups_py.html @@ -0,0 +1,597 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/groups.py: 82% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+ +
+

"""rest module.""" 

+

import logging 

+

import time 

+

import wsme 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

from rms.services import error_base 

+

from rms.services import services as GroupService 

+

from rms.utils import authentication 

+

from pecan import rest, request 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

from rms.model import model as PythonModel 

+

 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class Groups(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

id = wsme.wsattr(wtypes.text, mandatory=True) 

+

name = wsme.wsattr(wtypes.text, mandatory=True) 

+

description = wsme.wsattr(wtypes.text, mandatory=True) 

+

regions = wsme.wsattr([str], mandatory=True) 

+

 

+

def __init__(self, id=None, name=None, description=None, regions=[]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.id = id 

+

self.name = name 

+

self.description = description 

+

self.regions = regions 

+

 

+

def _to_python_obj(self): 

+

obj = PythonModel.Groups() 

+

obj.id = self.id 

+

obj.name = self.name 

+

obj.description = self.description 

+

obj.regions = self.regions 

+

return obj 

+

 

+

 

+

class GroupWrapper(wtypes.DynamicBase): 

+

"""main cotain lis of groups.""" 

+

 

+

groups = wsme.wsattr([Groups], mandatory=True) 

+

 

+

def __init__(self, groups=[]): 

+

""" 

+

 

+

:param group: 

+

""" 

+

self.groups = groups 

+

 

+

 

+

class OutputResource(wtypes.DynamicBase): 

+

"""class method returned json body.""" 

+

 

+

id = wsme.wsattr(wtypes.text, mandatory=True) 

+

name = wsme.wsattr(wtypes.text, mandatory=True) 

+

created = wsme.wsattr(wtypes.text, mandatory=True) 

+

links = wsme.wsattr({str: str}, mandatory=True) 

+

 

+

def __init__(self, id=None, name=None, created=None, links={}): 

+

"""init function. 

+

 

+

:param id: 

+

:param created: 

+

:param links: 

+

""" 

+

self.id = id 

+

self.name = name 

+

self.created = created 

+

self.links = links 

+

 

+

 

+

class Result(wtypes.DynamicBase): 

+

"""class method json headers.""" 

+

 

+

group = wsme.wsattr(OutputResource, mandatory=True) 

+

 

+

def __init__(self, group=OutputResource()): 

+

"""init dunction. 

+

 

+

:param group: The created group 

+

""" 

+

self.group = group 

+

 

+

 

+

class GroupsController(rest.RestController): 

+

"""controller get resource.""" 

+

 

+

@wsexpose(Groups, str, status_code=200, 

+

rest_content_types='json') 

+

def get(self, id=None): 

+

"""Handle get request. 

+

 

+

:param id: Group ID 

+

:return: 200 OK on success, 404 Not Found otherwise. 

+

""" 

+

logger.info("Entered Get Group: id = {}".format(id)) 

+

authentication.authorize(request, 'group:get_one') 

+

 

+

try: 

+

 

+

result = GroupService.get_groups_data(id) 

+

logger.debug('Returning group, regions: {}'.format(result.regions)) 

+

return result 

+

 

+

except error_base.NotFoundError as e: 

+

logger.error("GroupsController - Group not found") 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=404) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(GroupWrapper, status_code=200, rest_content_types='json') 

+

def get_all(self): 

+

logger.info("gett all groups") 

+

authentication.authorize(request, 'group:get_all') 

+

try: 

+

 

+

logger.debug("api-get all groups") 

+

groups_wrraper = GroupService.get_all_groups() 

+

logger.debug("got groups {}".format(groups_wrraper)) 

+

 

+

except Exception as exp: 

+

logger.error("api--fail to get all groups") 

+

logger.exception(exp) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

return groups_wrraper 

+

 

+

@wsexpose(Result, body=Groups, status_code=201, rest_content_types='json') 

+

def post(self, group_input): 

+

"""Handle post request. 

+

 

+

:param group_input: json data 

+

:return: 201 created on success, 409 otherwise. 

+

""" 

+

logger.info("Entered Create Group") 

+

logger.debug("id = {}, name = {}, description = {}, regions = {}".format( 

+

group_input.id, 

+

group_input.name, 

+

group_input.description, 

+

group_input.regions)) 

+

authentication.authorize(request, 'group:create') 

+

 

+

try: 

+

# May raise an exception which will return status code 400 

+

GroupService.create_group_in_db(group_input.id, 

+

group_input.name, 

+

group_input.description, 

+

group_input.regions) 

+

logger.debug("Group created successfully in DB") 

+

 

+

# Create the group output data with the correct timestamp and link 

+

group = OutputResource(group_input.id, 

+

group_input.name, 

+

repr(int(time.time() * 1000)), 

+

{'self': '{}/v2/orm/groups/{}'.format( 

+

request.application_url, 

+

group_input.id)}) 

+

 

+

event_details = 'Region group {} {} created with regions: {}'.format( 

+

group_input.id, group_input.name, group_input.regions) 

+

utils.audit_trail('create group', request.transaction_id, 

+

request.headers, group_input.id, 

+

event_details=event_details) 

+

return Result(group) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error("GroupsController - {}".format(e.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(None, str, status_code=204, rest_content_types='json') 

+

def delete(self, group_id): 

+

logger.info("delete group") 

+

authentication.authorize(request, 'group:delete') 

+

 

+

try: 

+

 

+

logger.debug("delete group with id {}".format(group_id)) 

+

GroupService.delete_group(group_id) 

+

logger.debug("done") 

+

 

+

event_details = 'Region group {} deleted'.format(group_id) 

+

utils.audit_trail('delete group', request.transaction_id, 

+

request.headers, group_id, 

+

event_details=event_details) 

+

 

+

except Exception as exp: 

+

 

+

logger.exception("fail to delete group :- {}".format(exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exp.message) 

+

return 

+

 

+

@wsexpose(Result, str, body=Groups, status_code=201, 

+

rest_content_types='json') 

+

def put(self, group_id, group): 

+

logger.info("update group") 

+

authentication.authorize(request, 'group:update') 

+

 

+

try: 

+

logger.debug("update group - id {}".format(group_id)) 

+

result = GroupService.update_group(group, group_id) 

+

logger.debug("group updated to :- {}".format(result)) 

+

 

+

# build result 

+

group_result = OutputResource(result.id, result.name, 

+

repr(int(time.time() * 1000)), { 

+

'self': '{}/v2/orm/groups/{}'.format( 

+

request.application_url, 

+

result.id)}) 

+

 

+

event_details = 'Region group {} {} updated with regions: {}'.format( 

+

group_id, group.name, group.regions) 

+

utils.audit_trail('update group', request.transaction_id, 

+

request.headers, group_id, 

+

event_details=event_details) 

+

 

+

except error_base.ErrorStatus as exp: 

+

logger.error("group to update not found {}".format(exp)) 

+

logger.exception(exp) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

except Exception as exp: 

+

logger.error("fail to update groupt -- id {}".format(group_id)) 

+

logger.exception(exp) 

+

raise 

+

 

+

return Result(group_result) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_metadata_py.html b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_metadata_py.html new file mode 100644 index 00000000..fcb8e1ed --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_metadata_py.html @@ -0,0 +1,437 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/metadata.py: 78% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+ +
+

import json 

+

 

+

import logging 

+

 

+

from pecan import rest, request 

+

import wsme 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

 

+

from rms.services import error_base 

+

from rms.services import services as RegionService 

+

 

+

from rms.utils import authentication 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class MetaData(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

metadata = wsme.wsattr({str: [str]}, mandatory=True) 

+

 

+

def __init__(self, metadata={}): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.metadata = metadata 

+

 

+

 

+

class RegionMetadataController(rest.RestController): 

+

 

+

@wsexpose(MetaData, str, status_code=200, rest_content_types='json') 

+

def get(self, region_id): 

+

logger.info("Get metadata for region id: {}".format(region_id)) 

+

authentication.authorize(request, 'metadata:get') 

+

 

+

try: 

+

region = RegionService.get_region_by_id_or_name(region_id) 

+

logger.debug("Got region metadata: {}".format(region.metadata)) 

+

return MetaData(region.metadata) 

+

 

+

except error_base.ErrorStatus as exp: 

+

logger.error("RegionsController - {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

except Exception as exp: 

+

logger.exception(exp.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exp.message) 

+

 

+

@wsexpose(MetaData, str, body=MetaData, status_code=201, 

+

rest_content_types='json') 

+

def post(self, region_id, metadata_input): 

+

"""Handle post request. 

+

:param region_id: region_id to add metadata to. 

+

:param metadata_input: json data 

+

:return: 201 created on success, 409 duplicate entry, 404 not found 

+

""" 

+

logger.info("Entered Create region metadata") 

+

logger.debug("Got metadata: {}".format(metadata_input)) 

+

authentication.authorize(request, 'metadata:create') 

+

 

+

try: 

+

self._validate_request_input() 

+

# May raise an exception which will return status code 400 

+

result = RegionService.add_region_metadata(region_id, 

+

metadata_input.metadata) 

+

logger.debug("Metadata was successfully added to " 

+

"region: {}. New metadata: {}".format(region_id, result)) 

+

 

+

event_details = 'Region {} metadata added'.format(region_id) 

+

utils.audit_trail('create metadata', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

return MetaData(result) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(MetaData, str, body=MetaData, status_code=201, 

+

rest_content_types='json') 

+

def put(self, region_id, metadata_input): 

+

"""Handle put request. 

+

:param region_id: region_id to update metadata to. 

+

:param metadata_input: json data 

+

:return: 201 created on success, 404 not found 

+

""" 

+

logger.info("Entered update region metadata") 

+

logger.debug("Got metadata: {}".format(metadata_input)) 

+

authentication.authorize(request, 'metadata:update') 

+

 

+

try: 

+

self._validate_request_input() 

+

# May raise an exception which will return status code 400 

+

result = RegionService.update_region_metadata(region_id, 

+

metadata_input.metadata) 

+

logger.debug("Metadata was successfully added to " 

+

"region: {}. New metadata: {}".format(region_id, result)) 

+

 

+

event_details = 'Region {} metadata updated'.format(region_id) 

+

utils.audit_trail('update metadata', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

return MetaData(result) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(None, str, str, status_code=204, rest_content_types='json') 

+

def delete(self, region_id, metadata_key): 

+

"""Handle delete request. 

+

:param region_id: region_id to update metadata to. 

+

:param metadata_key: metadata key to be deleted 

+

:return: 204 deleted 

+

""" 

+

logger.info("Entered delete region metadata with " 

+

"key: {}".format(metadata_key)) 

+

authentication.authorize(request, 'metadata:delete') 

+

 

+

try: 

+

# May raise an exception which will return status code 400 

+

result = RegionService.delete_metadata_from_region(region_id, 

+

metadata_key) 

+

logger.debug("Metadata was successfully deleted.") 

+

 

+

event_details = 'Region {} metadata {} deleted'.format( 

+

region_id, metadata_key) 

+

utils.audit_trail('delete metadata', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

def _validate_request_input(self): 

+

data_dict = json.loads(request.body) 

+

logger.debug("Got {}".format(data_dict)) 

+

for k, v in data_dict['metadata'].iteritems(): 

+

if isinstance(v, basestring): 

+

logger.error("Invalid json. value type list is expected, " 

+

"received string, for metadata key {}".format(k)) 

+

raise error_base.ErrorStatus(400, "Invalid json. Expecting list " 

+

"of metadata values, got string") 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_regions_py.html b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_regions_py.html new file mode 100644 index 00000000..02014699 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_regions_py.html @@ -0,0 +1,819 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/regions.py: 97% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+ +
+

"""rest module.""" 

+

import logging 

+

 

+

from pecan import rest, request 

+

import wsme 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

 

+

from rms.model import url_parm 

+

from rms.model import model as PythonModel 

+

from rms.services import error_base 

+

from rms.services import services as RegionService 

+

 

+

from rms.controllers.v2.orm.resources.metadata import RegionMetadataController 

+

from rms.controllers.v2.orm.resources.status import RegionStatusController 

+

 

+

from rms.utils import authentication 

+

 

+

from orm_common.policy import policy 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class Address(wtypes.DynamicBase): 

+

"""wsme class for address json.""" 

+

 

+

country = wsme.wsattr(wtypes.text, mandatory=True) 

+

state = wsme.wsattr(wtypes.text, mandatory=True) 

+

city = wsme.wsattr(wtypes.text, mandatory=True) 

+

street = wsme.wsattr(wtypes.text, mandatory=True) 

+

zip = wsme.wsattr(wtypes.text, mandatory=True) 

+

 

+

def __init__(self, country=None, state=None, city=None, 

+

street=None, zip=None): 

+

""" 

+

 

+

:param country: 

+

:param state: 

+

:param city: 

+

:param street: 

+

:param zip: 

+

""" 

+

self.country = country 

+

self.state = state 

+

self.city = city 

+

self.street = street 

+

self.zip = zip 

+

 

+

def _to_clean_python_obj(self): 

+

obj = PythonModel.Address() 

+

obj.country = self.country 

+

obj.state = self.state 

+

obj.city = self.city 

+

obj.street = self.street 

+

obj.zip = self.zip 

+

return obj 

+

 

+

 

+

class EndPoint(wtypes.DynamicBase): 

+

"""class method endpoints body.""" 

+

 

+

publicurl = wsme.wsattr(wtypes.text, mandatory=True, name="publicURL") 

+

type = wsme.wsattr(wtypes.text, mandatory=True) 

+

 

+

def __init__(self, publicurl=None, type=None): 

+

"""init function. 

+

 

+

:param publicURL: field 

+

:param typee: field 

+

:return: 

+

""" 

+

self.type = type 

+

self.publicurl = publicurl 

+

 

+

def _to_clean_python_obj(self): 

+

obj = PythonModel.EndPoint() 

+

obj.publicurl = self.publicurl 

+

obj.type = self.type 

+

return obj 

+

 

+

 

+

class RegionsData(wtypes.DynamicBase): 

+

"""class method json header.""" 

+

 

+

status = wsme.wsattr(wtypes.text, mandatory=True) 

+

id = wsme.wsattr(wtypes.text, mandatory=True) 

+

name = wsme.wsattr(wtypes.text, mandatory=False) 

+

ranger_agent_version = wsme.wsattr(wtypes.text, mandatory=True, name="rangerAgentVersion") 

+

open_stack_version = wsme.wsattr(wtypes.text, mandatory=True, name="OSVersion") 

+

clli = wsme.wsattr(wtypes.text, mandatory=True, name="CLLI") 

+

metadata = wsme.wsattr({str: [str]}, mandatory=True) 

+

endpoints = wsme.wsattr([EndPoint], mandatory=True) 

+

address = wsme.wsattr(Address, mandatory=True) 

+

design_type = wsme.wsattr(wtypes.text, mandatory=True, name="designType") 

+

location_type = wsme.wsattr(wtypes.text, mandatory=True, name="locationType") 

+

vlcp_name = wsme.wsattr(wtypes.text, mandatory=True, name="vlcpName") 

+

is_ecomp = wsme.wsattr(bool, mandatory=False, name="is_ecomp", 

+

default=False) 

+

is_ssp = wsme.wsattr(bool, mandatory=False, name="is_ssp", 

+

default=False) 

+

purpose_of_region = wsme.wsattr(wtypes.text, mandatory=True, name="purpose_of_region") 

+

 

+

def __init__(self, status=None, id=None, name=None, clli=None, design_type=None, 

+

location_type=None, vlcp_name=None, open_stack_version=None, 

+

address=Address(), ranger_agent_version=None, metadata={}, 

+

endpoint=[EndPoint()], is_ecomp=False, is_ssp=False, 

+

purpose_of_region=None): 

+

""" 

+

 

+

:param status: 

+

:param id: 

+

:param name: 

+

:param clli: 

+

:param design_type: 

+

:param location_type: 

+

:param vlcp_name: 

+

:param open_stack_version: 

+

:param address: 

+

:param ranger_agent_version: 

+

:param metadata: 

+

:param endpoint: 

+

:param is_ecomp: 

+

:param is_ssp: 

+

:param purpose_of_region 

+

""" 

+

self.status = status 

+

self.id = id 

+

self.name = self.id 

+

self.clli = clli 

+

self.ranger_agent_version = ranger_agent_version 

+

self.metadata = metadata 

+

self.endpoint = endpoint 

+

self.design_type = design_type 

+

self.location_type = location_type 

+

self.vlcp_name = vlcp_name 

+

self.address = address 

+

self.open_stack_version = open_stack_version 

+

self.is_ecomp = is_ecomp 

+

self.is_ssp = is_ssp 

+

self.purpose_of_region = purpose_of_region 

+

 

+

def _to_clean_python_obj(self): 

+

obj = PythonModel.RegionData() 

+

obj.endpoints = [] 

+

obj.status = self.status 

+

obj.id = self.id 

+

obj.name = self.name 

+

obj.ranger_agent_version = self.ranger_agent_version 

+

obj.clli = self.clli 

+

obj.metadata = self.metadata 

+

for endpoint in self.endpoints: 

+

obj.endpoints.append(endpoint._to_clean_python_obj()) 

+

obj.address = self.address._to_clean_python_obj() 

+

obj.design_type = self.design_type 

+

obj.location_type = self.location_type 

+

obj.vlcp_name = self.vlcp_name 

+

obj.open_stack_version = self.open_stack_version 

+

obj.is_ecomp = self.is_ecomp 

+

obj.is_ssp = self.is_ssp 

+

obj.purpose_of_region = self.purpose_of_region 

+

return obj 

+

 

+

 

+

class Regions(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

regions = wsme.wsattr([RegionsData], mandatory=True) 

+

 

+

def __init__(self, regions=[RegionsData()]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.regions = regions 

+

 

+

 

+

class RegionsController(rest.RestController): 

+

"""controller get resource.""" 

+

metadata = RegionMetadataController() 

+

status = RegionStatusController() 

+

 

+

@wsexpose(Regions, str, str, [str], str, str, str, str, str, str, str, 

+

str, str, str, str, str, str, status_code=200, rest_content_types='json') 

+

def get_all(self, type=None, status=None, metadata=None, rangerAgentVersion=None, 

+

clli=None, regionname=None, osversion=None, valet=None, 

+

state=None, country=None, city=None, street=None, zip=None, 

+

is_ecomp=False, is_ssp=False, purpose_of_region=None): 

+

"""get regions. 

+

 

+

:param type: query field 

+

:param status: query field 

+

:param metadata: query field 

+

:param rangerAgentVersion: query field 

+

:param clli: query field 

+

:param regionname: query field 

+

:param osversion: query field 

+

:param valet: query field 

+

:param state: query field 

+

:param country: query field 

+

:param city: query field 

+

:param street: query field 

+

:param zip: query field 

+

:param is_ecomp: query field 

+

:param is_ssp: query field 

+

:param purpose_of_region: query field 

+

:return: json from db 

+

:exception: EntityNotFoundError 404 

+

""" 

+

logger.info("Entered Get Regions") 

+

authentication.authorize(request, 'region:get_all') 

+

 

+

url_args = {'type': type, 'status': status, 'metadata': metadata, 

+

'rangerAgentVersion': rangerAgentVersion, 'clli': clli, 'regionname': regionname, 

+

'osversion': osversion, 'valet': valet, 'state': state, 

+

'country': country, 'city': city, 'street': street, 'zip': zip, 

+

'is_ecomp': is_ecomp, 'is_ssp': is_ssp, 

+

'purpose_of_region': purpose_of_region} 

+

logger.debug("Parameters: {}".format(str(url_args))) 

+

 

+

try: 

+

url_args = url_parm.UrlParms(**url_args) 

+

 

+

result = RegionService.get_regions_data(url_args) 

+

 

+

logger.debug("Returning regions: {}".format(', '.join( 

+

[region.name for region in result.regions]))) 

+

 

+

return result 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error("RegionsController {}".format(e.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exception.message) 

+

 

+

@wsexpose(RegionsData, str, status_code=200, rest_content_types='json') 

+

def get_one(self, id_or_name): 

+

logger.info("API: Entered get region by id or name: {}".format(id_or_name)) 

+

authentication.authorize(request, 'region:get_one') 

+

 

+

try: 

+

result = RegionService.get_region_by_id_or_name(id_or_name) 

+

logger.debug("API: Got region {} success: {}".format(id_or_name, result)) 

+

except error_base.ErrorStatus as exp: 

+

logger.error("RegionsController {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

except Exception as exp: 

+

logger.exception(exp.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exp.message) 

+

 

+

return result 

+

 

+

@wsexpose(RegionsData, body=RegionsData, status_code=201, rest_content_types='json') 

+

def post(self, full_region_input): 

+

logger.info("API: CreateRegion") 

+

authentication.authorize(request, 'region:create') 

+

 

+

try: 

+

logger.debug("API: create region .. data = : {}".format(full_region_input)) 

+

result = RegionService.create_full_region(full_region_input) 

+

logger.debug("API: region created : {}".format(result)) 

+

 

+

event_details = 'Region {} {} created: AICversion {}, OSversion {}, CLLI {}'.format( 

+

full_region_input.name, full_region_input.design_type, 

+

full_region_input.ranger_agent_version, 

+

full_region_input.open_stack_version, full_region_input.clli) 

+

utils.audit_trail('create region', request.transaction_id, 

+

request.headers, full_region_input.id, 

+

event_details=event_details) 

+

except error_base.InputValueError as exp: 

+

logger.exception("Error in save region {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exp.status_code, 

+

message=exp.message) 

+

 

+

except error_base.ConflictError as exp: 

+

logger.exception("Conflict error {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

 

+

except Exception as exp: 

+

logger.exception("Error in creating region .. reason:- {}".format(exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exp.message) 

+

 

+

return result 

+

 

+

@wsexpose(None, str, rest_content_types='json', status_code=204) 

+

def delete(self, region_id): 

+

logger.info("Delete Region") 

+

authentication.authorize(request, 'region:delete') 

+

 

+

try: 

+

 

+

logger.debug("delete region {}".format(region_id)) 

+

result = RegionService.delete_region(region_id) 

+

logger.debug("region deleted") 

+

 

+

event_details = 'Region {} deleted'.format(region_id) 

+

utils.audit_trail('delete region', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

except Exception as exp: 

+

logger.exception( 

+

"error in deleting region .. reason:- {}".format(exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exp.message) 

+

return 

+

 

+

@wsexpose(RegionsData, str, body=RegionsData, status_code=201, 

+

rest_content_types='json') 

+

def put(self, region_id, region): 

+

logger.info("API: update region") 

+

authentication.authorize(request, 'region:update') 

+

 

+

try: 

+

 

+

logger.debug( 

+

"region to update {} with{}".format(region_id, region)) 

+

result = RegionService.update_region(region_id, region) 

+

logger.debug("API: region {} updated".format(region_id)) 

+

 

+

event_details = 'Region {} {} modified: AICversion {}, OSversion {}, CLLI {}'.format( 

+

region.name, region.design_type, region.ranger_agent_version, 

+

region.open_stack_version, region.clli) 

+

utils.audit_trail('update region', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

except error_base.NotFoundError as exp: 

+

logger.exception("region {} not found".format(region_id)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exp.status_code, 

+

message=exp.message) 

+

 

+

except error_base.InputValueError as exp: 

+

logger.exception("not valid input {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exp.status_code, 

+

message=exp.message) 

+

except Exception as exp: 

+

logger.exception( 

+

"API: error in updating region {}.. reason:- {}".format(region_id, 

+

exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exp.message) 

+

return result 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_status_py.html b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_status_py.html new file mode 100644 index 00000000..f719798a --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_orm_resources_status_py.html @@ -0,0 +1,299 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/status.py: 94% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+ +
+

import logging 

+

 

+

import pecan 

+

from pecan import rest, request, conf 

+

 

+

import wsme 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

from rms.services import error_base 

+

from rms.services import services as RegionService 

+

from rms.utils import authentication 

+

 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class RegionStatus(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

status = wsme.wsattr(str, mandatory=True) 

+

links = wsme.wsattr({str: str}, mandatory=False) 

+

 

+

def __init__(self, status=None, links=None): 

+

""" 

+

RegionStatus wrapper 

+

:param status: 

+

""" 

+

self.status = status 

+

self.links = links 

+

 

+

 

+

class RegionStatusController(rest.RestController): 

+

 

+

@wsexpose(RegionStatus, str, body=RegionStatus, status_code=201, 

+

rest_content_types='json') 

+

def put(self, region_id, new_status): 

+

""" 

+

Handle put request to modify region status 

+

:param region_id: 

+

:param new_status: 

+

:return: 200 for updated, 404 for region not found 

+

400 invalid status 

+

""" 

+

logger.info("Entered update region status") 

+

logger.debug("Got status: {}".format(new_status.status)) 

+

 

+

authentication.authorize(request, 'status:put') 

+

 

+

try: 

+

allowed_status = conf.region_options.allowed_status_values[:] 

+

 

+

if new_status.status not in allowed_status: 

+

logger.error("Invalid status. Region status " 

+

"must be one of {}".format(allowed_status)) 

+

raise error_base.InputValueError( 

+

message="Invalid status. Region status " 

+

"must be one of {}".format(allowed_status)) 

+

 

+

# May raise an exception which will return status code 400 

+

status = RegionService.update_region_status(region_id, new_status.status) 

+

base_link = 'https://{0}:{1}{2}'.format(conf.server.host, conf.server.port, 

+

pecan.request.path) 

+

link = {'self': base_link} 

+

 

+

logger.debug("Region status for region id {}, was successfully " 

+

"changed to: {}.".format(region_id, new_status.status)) 

+

 

+

event_details = 'Region {} status updated to {}'.format( 

+

region_id, new_status.status) 

+

utils.audit_trail('Update status', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

return RegionStatus(status, link) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(str, str, rest_content_types='json') 

+

def get(self, region_id): 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=405) 

+

 

+

@wsexpose(RegionStatus, str, body=RegionStatus, status_code=200, 

+

rest_content_types='json') 

+

def post(self, region_id, status): 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=405) 

+

 

+

@wsexpose(str, str, rest_content_types='json') 

+

def delete(self, region_id): 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=405) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_orm_root_py.html b/orm/services/region_manager/cover/rms_controllers_v2_orm_root_py.html new file mode 100644 index 00000000..df7c358c --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_orm_root_py.html @@ -0,0 +1,109 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/root.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+ +
+

"""ORM controller module.""" 

+

from rms.controllers.v2.orm.resources import groups 

+

from rms.controllers.v2.orm.resources import regions 

+

 

+

 

+

class OrmController(object): 

+

"""ORM controller class.""" 

+

 

+

regions = regions.RegionsController() 

+

groups = groups.GroupsController() 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_controllers_v2_root_py.html b/orm/services/region_manager/cover/rms_controllers_v2_root_py.html new file mode 100644 index 00000000..addd8179 --- /dev/null +++ b/orm/services/region_manager/cover/rms_controllers_v2_root_py.html @@ -0,0 +1,105 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/root.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+ +
+

"""V2 root controller module.""" 

+

from rms.controllers.v2.orm import root 

+

 

+

 

+

class V2Controller(object): 

+

"""V2 root controller class.""" 

+

 

+

orm = root.OrmController() 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock___init___py.html b/orm/services/region_manager/cover/rms_external_mock___init___py.html new file mode 100644 index 00000000..301a20c8 --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_audit_client___init___py.html b/orm/services/region_manager/cover/rms_external_mock_audit_client___init___py.html new file mode 100644 index 00000000..0ba1d066 --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_audit_client___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/audit_client/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_audit_client_api___init___py.html b/orm/services/region_manager/cover/rms_external_mock_audit_client_api___init___py.html new file mode 100644 index 00000000..9415cc4a --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_audit_client_api___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/audit_client/api/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_audit_client_api_audit_py.html b/orm/services/region_manager/cover/rms_external_mock_audit_client_api_audit_py.html new file mode 100644 index 00000000..92fe9996 --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_audit_client_api_audit_py.html @@ -0,0 +1,101 @@ + + + + + + + + + + + Coverage for rms/external_mock/audit_client/api/audit.py: 0% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+ +
+

def audit(*args, **kwargs): 

+

pass 

+

 

+

 

+

def init(*args, **kwargs): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_keystone_utils___init___py.html b/orm/services/region_manager/cover/rms_external_mock_keystone_utils___init___py.html new file mode 100644 index 00000000..c91da3f1 --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_keystone_utils___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/keystone_utils/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_keystone_utils_tokens_py.html b/orm/services/region_manager/cover/rms_external_mock_keystone_utils_tokens_py.html new file mode 100644 index 00000000..b833593b --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_keystone_utils_tokens_py.html @@ -0,0 +1,103 @@ + + + + + + + + + + + Coverage for rms/external_mock/keystone_utils/tokens.py: 80% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+ +
+

def get_token_user(*a, **k): 

+

pass 

+

 

+

 

+

class TokenConf(object): 

+

def __init__(self, *a, **kw): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_orm_common___init___py.html b/orm/services/region_manager/cover/rms_external_mock_orm_common___init___py.html new file mode 100644 index 00000000..9d0575ed --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_orm_common___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_orm_common_policy___init___py.html b/orm/services/region_manager/cover/rms_external_mock_orm_common_policy___init___py.html new file mode 100644 index 00000000..4367b302 --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_orm_common_policy___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/policy/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_orm_common_policy_policy_py.html b/orm/services/region_manager/cover/rms_external_mock_orm_common_policy_policy_py.html new file mode 100644 index 00000000..972d1dcf --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_orm_common_policy_policy_py.html @@ -0,0 +1,109 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/policy/policy.py: 67% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+ +
+

def init(*a, **kw): 

+

pass 

+

 

+

 

+

def enforce(*a, **kw): 

+

pass 

+

 

+

 

+

def authorize(*a, **kw): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_orm_common_utils___init___py.html b/orm/services/region_manager/cover/rms_external_mock_orm_common_utils___init___py.html new file mode 100644 index 00000000..e339ed1a --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_orm_common_utils___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/utils/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_orm_common_utils_api_error_utils_py.html b/orm/services/region_manager/cover/rms_external_mock_orm_common_utils_api_error_utils_py.html new file mode 100644 index 00000000..9979bae3 --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_orm_common_utils_api_error_utils_py.html @@ -0,0 +1,93 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/utils/api_error_utils.py: 50% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+ +
+

def get_error(*args, **kwargs): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_external_mock_orm_common_utils_utils_py.html b/orm/services/region_manager/cover/rms_external_mock_orm_common_utils_utils_py.html new file mode 100644 index 00000000..fa2be4b5 --- /dev/null +++ b/orm/services/region_manager/cover/rms_external_mock_orm_common_utils_utils_py.html @@ -0,0 +1,119 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/utils/utils.py: 83% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+ +
+

"""Utils module mock.""" 

+

 

+

 

+

def report_config(conf, dump=False): 

+

"""Mock report_config function.""" 

+

pass 

+

 

+

 

+

def set_utils_conf(conf): 

+

"""Mock set_utils_conf function.""" 

+

pass 

+

 

+

 

+

def audit_trail(*args, **kwargs): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_model___init___py.html b/orm/services/region_manager/cover/rms_model___init___py.html new file mode 100644 index 00000000..dd0a069f --- /dev/null +++ b/orm/services/region_manager/cover/rms_model___init___py.html @@ -0,0 +1,119 @@ + + + + + + + + + + + Coverage for rms/model/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+ +
+

from pecan import conf # noqa 

+

 

+

 

+

def init_model(): 

+

""" 

+

This is a stub method which is called at application startup time. 

+

 

+

If you need to bind to a parsed database configuration, set up tables or 

+

ORM classes, or perform any database initialization, this is the 

+

recommended place to do it. 

+

 

+

For more information working with databases, and some common recipes, 

+

see http://pecan.readthedocs.org/en/latest/databases.html 

+

""" 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_model_model_py.html b/orm/services/region_manager/cover/rms_model_model_py.html new file mode 100644 index 00000000..d4a4be2a --- /dev/null +++ b/orm/services/region_manager/cover/rms_model_model_py.html @@ -0,0 +1,475 @@ + + + + + + + + + + + Coverage for rms/model/model.py: 93% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+ +
+

"""model module.""" 

+

from rms.services import error_base 

+

from pecan import conf 

+

 

+

 

+

class Address(object): 

+

"""address class.""" 

+

 

+

def __init__(self, country=None, state=None, city=None, 

+

street=None, zip=None): 

+

""" 

+

 

+

:param country: 

+

:param state: 

+

:param city: 

+

:param street: 

+

:param zip: 

+

""" 

+

self.country = country 

+

self.state = state 

+

self.city = city 

+

self.street = street 

+

self.zip = zip 

+

 

+

 

+

class EndPoint(object): 

+

"""class method endpoints body.""" 

+

 

+

def __init__(self, publicurl=None, type=None): 

+

"""init function. 

+

 

+

:param public_url: field 

+

:param type: field 

+

:return: 

+

""" 

+

self.type = type 

+

self.publicurl = publicurl 

+

 

+

 

+

class RegionData(object): 

+

"""class method json header.""" 

+

 

+

def __init__(self, status=None, id=None, name=None, clli=None, 

+

ranger_agent_version=None, design_type=None, location_type=None, 

+

vlcp_name=None, open_stack_version=None, 

+

address=Address(), is_ecomp=None, is_ssp=None, 

+

purpose_of_region=None, metadata={}, endpoints=[EndPoint()]): 

+

""" 

+

 

+

:param status: 

+

:param id: 

+

:param name: 

+

:param clli: 

+

:param ranger_agent_version: 

+

:param design_type: 

+

:param location_type: 

+

:param vlcp_name: 

+

:param open_stack_version: 

+

:param address: 

+

:param is_ecomp 

+

:param is_ssp 

+

:param purpose_of_region 

+

:param metadata: 

+

:param endpoints: 

+

""" 

+

self.status = status 

+

self.id = id 

+

# make id and name always the same 

+

self.name = self.id 

+

self.clli = clli 

+

self.ranger_agent_version = ranger_agent_version 

+

self.metadata = metadata 

+

self.endpoints = endpoints 

+

self.design_type = design_type 

+

self.location_type = location_type 

+

self.vlcp_name = vlcp_name 

+

self.open_stack_version = open_stack_version 

+

self.address = address 

+

self.is_ecomp = is_ecomp 

+

self.is_ssp = is_ssp 

+

self.purpose_of_region = purpose_of_region 

+

 

+

def _validate_end_points(self, endpoints_types_must_have): 

+

ep_duplicate = [] 

+

for endpoint in self.endpoints: 

+

if endpoint.type not in ep_duplicate: 

+

ep_duplicate.append(endpoint.type) 

+

else: 

+

raise error_base.InputValueError( 

+

message="Invalid endpoints. Duplicate endpoint " 

+

"type {}".format(endpoint.type)) 

+

try: 

+

endpoints_types_must_have.remove(endpoint.type) 

+

except: 

+

pass 

+

if len(endpoints_types_must_have) > 0: 

+

raise error_base.InputValueError( 

+

message="Invalid endpoints. Endpoint type '{}' " 

+

"is missing".format(endpoints_types_must_have)) 

+

 

+

def _validate_status(self, allowed_status): 

+

if self.status not in allowed_status: 

+

raise error_base.InputValueError( 

+

message="Invalid status. Region status must be " 

+

"one of {}".format(allowed_status)) 

+

return 

+

 

+

def _validate_model(self): 

+

allowed_status = conf.region_options.allowed_status_values[:] 

+

endpoints_types_must_have = conf.region_options.endpoints_types_must_have[:] 

+

self._validate_status(allowed_status) 

+

self._validate_end_points(endpoints_types_must_have) 

+

return 

+

 

+

def _to_db_model_dict(self): 

+

end_points = [] 

+

 

+

for endpoint in self.endpoints: 

+

ep = {} 

+

ep['type'] = endpoint.type 

+

ep['url'] = endpoint.publicurl 

+

end_points.append(ep) 

+

 

+

db_model_dict = {} 

+

db_model_dict['region_id'] = self.id 

+

db_model_dict['name'] = self.name 

+

db_model_dict['address_state'] = self.address.state 

+

db_model_dict['address_country'] = self.address.country 

+

db_model_dict['address_city'] = self.address.city 

+

db_model_dict['address_street'] = self.address.street 

+

db_model_dict['address_zip'] = self.address.zip 

+

db_model_dict['region_status'] = self.status 

+

db_model_dict['ranger_agent_version'] = self.ranger_agent_version 

+

db_model_dict['open_stack_version'] = self.open_stack_version 

+

db_model_dict['design_type'] = self.design_type 

+

db_model_dict['location_type'] = self.location_type 

+

db_model_dict['vlcp_name'] = self.location_type 

+

db_model_dict['clli'] = self.clli 

+

db_model_dict['is_ecomp'] = self.is_ecomp * 1 

+

db_model_dict['is_ssp'] = self.is_ssp * 1 

+

db_model_dict['purpose_of_region'] = self.purpose_of_region 

+

db_model_dict['end_point_list'] = end_points 

+

db_model_dict['meta_data_dict'] = self.metadata 

+

return db_model_dict 

+

 

+

 

+

class Regions(object): 

+

"""main json header.""" 

+

 

+

def __init__(self, regions=[RegionData()]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.regions = regions 

+

 

+

 

+

class Groups(object): 

+

"""main json header.""" 

+

 

+

def __init__(self, id=None, name=None, 

+

description=None, regions=[]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.id = id 

+

self.name = name 

+

self.description = description 

+

self.regions = regions 

+

 

+

def _to_db_model_dict(self): 

+

db_dict = {} 

+

db_dict['group_name'] = self.name 

+

db_dict['group_description'] = self.description 

+

db_dict['group_regions'] = self.regions 

+

return db_dict 

+

 

+

 

+

class GroupsWrraper(object): 

+

"""list of groups.""" 

+

 

+

def __init__(self, groups=None): 

+

""" 

+

 

+

:param groups: 

+

""" 

+

if groups is None: 

+

self.groups = [] 

+

else: 

+

self.groups = groups 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_model_url_parm_py.html b/orm/services/region_manager/cover/rms_model_url_parm_py.html new file mode 100644 index 00000000..c8234f07 --- /dev/null +++ b/orm/services/region_manager/cover/rms_model_url_parm_py.html @@ -0,0 +1,307 @@ + + + + + + + + + + + Coverage for rms/model/url_parm.py: 94% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+ +
+

"""module.""" 

+

 

+

 

+

class UrlParms(object): 

+

"""class method.""" 

+

 

+

def __init__(self, type=None, status=None, metadata=None, rangerAgentVersion=None, 

+

clli=None, regionname=None, osversion=None, valet=None, 

+

state=None, country=None, city=None, street=None, zip=None, 

+

is_ecomp=None, is_ssp=None, purpose_of_region=None): 

+

"""init method. 

+

 

+

:param type: 

+

:param status: 

+

:param metadata: 

+

:param rangerAgentVersion: 

+

:param clli: 

+

:param regionname: 

+

:param osversion: 

+

:param valet: 

+

:param state: 

+

:param country: 

+

:param city: 

+

:param street: 

+

:param zip: 

+

""" 

+

if type: 

+

self.location_type = type 

+

if status: 

+

self.region_status = status 

+

if metadata: 

+

self.metadata = metadata 

+

if rangerAgentVersion: 

+

self.ranger_agent_version = rangerAgentVersion 

+

if clli: 

+

self.clli = clli 

+

if regionname: 

+

self.name = regionname 

+

if osversion: 

+

self.open_stack_version = osversion 

+

if valet: 

+

self.valet = valet 

+

if state: 

+

self.address_state = state 

+

if country: 

+

self.address_country = country 

+

if city: 

+

self.address_city = city 

+

if street: 

+

self.address_street = street 

+

if zip: 

+

self.address_zip = zip 

+

if is_ecomp: 

+

self.is_ecomp = is_ecomp 

+

if is_ssp: 

+

self.is_ssp = is_ssp 

+

if purpose_of_region: 

+

self.purpose_of_region 

+

 

+

def _build_query(self): 

+

"""nuild db query. 

+

 

+

:return: 

+

""" 

+

metadatadict = None 

+

regiondict = None 

+

if self.__dict__: 

+

metadatadict = self._build_metadata_dict() 

+

regiondict = self._build_region_dict() 

+

return regiondict, metadatadict, None 

+

 

+

def _build_metadata_dict(self): 

+

"""meta_data dict. 

+

 

+

:return: metadata dict 

+

""" 

+

metadata = None 

+

if 'metadata' in self.__dict__: 

+

metadata = {'ref_keys': [], 'meta_data_pairs': [], 

+

'meta_data_keys': []} 

+

for metadata_item in self.metadata: 

+

if ':' in metadata_item: 

+

key = metadata_item.split(':')[0] 

+

metadata['ref_keys'].append(key) 

+

metadata['meta_data_pairs'].\ 

+

append({'metadata_key': key, 

+

'metadata_value': metadata_item.split(':')[1]}) 

+

else: 

+

metadata['meta_data_keys'].append(metadata_item) 

+

# Now clean irrelevant values 

+

keys_list = [] 

+

for item in metadata['meta_data_keys']: 

+

if item not in metadata['ref_keys']: 

+

keys_list.append(item) 

+

 

+

metadata['meta_data_keys'] = keys_list 

+

 

+

return metadata 

+

 

+

def _build_region_dict(self): 

+

"""region dict. 

+

 

+

:return:regin dict 

+

""" 

+

regiondict = {} 

+

for key, value in self.__dict__.items(): 

+

if key != 'metadata': 

+

regiondict[key] = value 

+

return regiondict 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_services___init___py.html b/orm/services/region_manager/cover/rms_services___init___py.html new file mode 100644 index 00000000..430566d0 --- /dev/null +++ b/orm/services/region_manager/cover/rms_services___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/services/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""services package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_services_error_base_py.html b/orm/services/region_manager/cover/rms_services_error_base_py.html new file mode 100644 index 00000000..3488d3a4 --- /dev/null +++ b/orm/services/region_manager/cover/rms_services_error_base_py.html @@ -0,0 +1,155 @@ + + + + + + + + + + + Coverage for rms/services/error_base.py: 89% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+ +
+

"""Exceptions module.""" 

+

 

+

 

+

class Error(Exception): 

+

pass 

+

 

+

 

+

class ErrorStatus(Error): 

+

 

+

def __init__(self, status_code, message=""): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class NotFoundError(ErrorStatus): 

+

 

+

def __init__(self, status_code=404, message="Not found"): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class ConflictError(ErrorStatus): 

+

 

+

def __init__(self, status_code=409, message="Conflict error"): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class InputValueError(ErrorStatus): 

+

 

+

def __init__(self, status_code=400, message="value not allowed"): 

+

self.status_code = status_code 

+

self.message = message 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_services_services_py.html b/orm/services/region_manager/cover/rms_services_services_py.html new file mode 100644 index 00000000..0192f62f --- /dev/null +++ b/orm/services/region_manager/cover/rms_services_services_py.html @@ -0,0 +1,661 @@ + + + + + + + + + + + Coverage for rms/services/services.py: 68% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+ +
+

"""DB actions wrapper module.""" 

+

import logging 

+

from rms.model.model import Groups 

+

from rms.model.model import Regions 

+

from rms.services import error_base 

+

from rms.storage import base_data_manager 

+

from rms.storage import data_manager_factory 

+

 

+

LOG = logging.getLogger(__name__) 

+

 

+

 

+

def get_regions_data(url_parms): 

+

"""get region from db. 

+

 

+

:param url_parms: the parameters got in the url to make the query 

+

:return: region model for json output 

+

:raise: NoContentError( status code 404) 

+

""" 

+

region_dict, metadata_dict, end_point = url_parms._build_query() 

+

db = data_manager_factory.get_data_manager() 

+

regions = db.get_regions(region_dict, metadata_dict, end_point) 

+

if not regions: 

+

raise error_base.NotFoundError(message="No regions found for the given search parameters") 

+

return Regions(regions) 

+

 

+

 

+

def get_region_by_id_or_name(region_id_or_name): 

+

""" 

+

 

+

:param region_id_or_name: 

+

:return: region object (wsme format) 

+

""" 

+

LOG.debug("LOGIC:- get region data by id or name {}".format(region_id_or_name)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

region = db.get_region_by_id_or_name(region_id_or_name) 

+

 

+

if not region: 

+

raise error_base.NotFoundError(message="Region {} not found".format(region_id_or_name)) 

+

 

+

except Exception as exp: 

+

LOG.exception("error in get region by id/name") 

+

raise 

+

 

+

return region 

+

 

+

 

+

def update_region(region_id, region): 

+

""" 

+

:param region: 

+

:return: 

+

""" 

+

LOG.debug("logic:- update region {}".format(region)) 

+

try: 

+

 

+

region = region._to_clean_python_obj() 

+

region._validate_model() 

+

region_dict = region._to_db_model_dict() 

+

 

+

db = data_manager_factory.get_data_manager() 

+

db.update_region(region_to_update=region_id, **region_dict) 

+

LOG.debug("region {} updated".format(region_id)) 

+

result = get_region_by_id_or_name(region_id) 

+

 

+

except error_base.NotFoundError as exp: 

+

LOG.exception("fail to update region {}".format(exp.message)) 

+

raise 

+

except Exception as exp: 

+

LOG.exception("fail to update region {}".format(exp)) 

+

raise 

+

return result 

+

 

+

 

+

def delete_region(region_id): 

+

""" 

+

 

+

:param region_id: 

+

:return: 

+

""" 

+

LOG.debug("logic:- delete region {}".format(region_id)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

db.delete_region(region_id) 

+

LOG.debug("region deleted") 

+

except Exception as exp: 

+

LOG.exception("fail to delete region {}".format(exp)) 

+

raise 

+

return 

+

 

+

 

+

def create_full_region(full_region): 

+

"""create region logic. 

+

 

+

:param full_region obj: 

+

:return: 

+

:raise: input value error(status code 400) 

+

""" 

+

LOG.debug("logic:- save region ") 

+

try: 

+

 

+

full_region = full_region._to_clean_python_obj() 

+

full_region._validate_model() 

+

 

+

full_region_db_dict = full_region._to_db_model_dict() 

+

LOG.debug("region to save {}".format(full_region_db_dict)) 

+

db = data_manager_factory.get_data_manager() 

+

db.add_region(**full_region_db_dict) 

+

LOG.debug("region added") 

+

result = get_region_by_id_or_name(full_region.id) 

+

 

+

except error_base.InputValueError as exp: 

+

LOG.exception("error in save region {}".format(exp.message)) 

+

raise 

+

except base_data_manager.DuplicateEntryError as exp: 

+

LOG.exception("error in save region {}".format(exp.message)) 

+

raise error_base.ConflictError(message=exp.message) 

+

except Exception as exp: 

+

LOG.exception("error in save region {}".format(exp.message)) 

+

raise 

+

 

+

return result 

+

 

+

 

+

def add_region_metadata(region_id, metadata_dict): 

+

LOG.debug("Add metadata: {} to region id : {}".format(metadata_dict, 

+

region_id)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

result = db.add_meta_data_to_region(region_id, metadata_dict) 

+

if not result: 

+

raise error_base.NotFoundError(message="Region {} not found".format(region_id)) 

+

else: 

+

return result.metadata 

+

 

+

except Exception as exp: 

+

LOG.exception("Error getting metadata for region id:".format(region_id)) 

+

raise 

+

 

+

 

+

def update_region_metadata(region_id, metadata_dict): 

+

LOG.debug("Update metadata to region id : {}. " 

+

"New metadata: {}".format(region_id, metadata_dict)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

result = db.update_region_meta_data(region_id, metadata_dict) 

+

if not result: 

+

raise error_base.NotFoundError(message="Region {} not " 

+

"found".format(region_id)) 

+

else: 

+

return result.metadata 

+

 

+

except Exception as exp: 

+

LOG.exception("Error getting metadata for region id:".format(region_id)) 

+

raise 

+

 

+

 

+

def delete_metadata_from_region(region_id, metadata_key): 

+

LOG.info("Delete metadata key: {} from region id : {}." 

+

.format(metadata_key, region_id)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

db.delete_region_metadata(region_id, metadata_key) 

+

 

+

except Exception as exp: 

+

LOG.exception("Error getting metadata for region id:".format(region_id)) 

+

raise 

+

 

+

 

+

def get_groups_data(name): 

+

"""get group from db. 

+

 

+

:param name: groupe name 

+

:return: groupe object with its regions 

+

:raise: NoContentError( status code 404) 

+

""" 

+

db = data_manager_factory.get_data_manager() 

+

groups = db.get_group(name) 

+

if not groups: 

+

raise error_base.NotFoundError(message="Group {} not found".format(name)) 

+

return Groups(**groups) 

+

 

+

 

+

def get_all_groups(): 

+

""" 

+

 

+

:return: 

+

""" 

+

try: 

+

LOG.debug("logic - get all groups") 

+

db = data_manager_factory.get_data_manager() 

+

all_groups = db.get_all_groups() 

+

LOG.debug("logic - got all groups {}".format(all_groups)) 

+

 

+

except Exception as exp: 

+

LOG.error("fail to get all groups") 

+

LOG.exception(exp) 

+

raise 

+

 

+

return all_groups 

+

 

+

 

+

def delete_group(group_id): 

+

""" 

+

 

+

:param group_id: 

+

:return: 

+

""" 

+

LOG.debug("delete group logic") 

+

try: 

+

 

+

db = data_manager_factory.get_data_manager() 

+

LOG.debug("delete group id {} from db".format(group_id)) 

+

db.delete_group(group_id) 

+

 

+

except Exception as exp: 

+

LOG.exception(exp) 

+

raise 

+

return 

+

 

+

 

+

def create_group_in_db(group_id, group_name, description, regions): 

+

"""Create a region group in the database. 

+

 

+

:param group_id: The ID of the group to create 

+

:param group_name: The name of the group to create 

+

:param description: The group description 

+

:param regions: A list of regions inside the group 

+

:raise: GroupExistsError (status code 400) if the group already exists 

+

""" 

+

try: 

+

manager = data_manager_factory.get_data_manager() 

+

manager.add_group(group_id, group_name, description, regions) 

+

except error_base.ConflictError: 

+

LOG.exception("Group {} already exists".format(group_id)) 

+

raise error_base.ConflictError( 

+

message="Group {} already exists".format(group_id)) 

+

except error_base.InputValueError: 

+

LOG.exception("Some of the regions not found") 

+

raise error_base.NotFoundError( 

+

message="Some of the regions not found") 

+

 

+

 

+

def update_group(group, group_id): 

+

result = None 

+

LOG.debug("update group logic") 

+

try: 

+

group = group._to_python_obj() 

+

db_manager = data_manager_factory.get_data_manager() 

+

LOG.debug("update group to {}".format(group._to_db_model_dict())) 

+

db_manager.update_group(group_id=group_id, **group._to_db_model_dict()) 

+

LOG.debug("group updated") 

+

# make sure it updated 

+

groups = db_manager.get_group(group_id) 

+

 

+

except error_base.NotFoundError: 

+

LOG.error("Group {} not found") 

+

raise 

+

except error_base.InputValueError: 

+

LOG.exception("Some of the regions not found") 

+

raise error_base.NotFoundError( 

+

message="Some of the regions not found") 

+

except Exception as exp: 

+

LOG.error("Failed to update group {}".format(group.group_id)) 

+

LOG.exception(exp) 

+

raise 

+

 

+

return Groups(**groups) 

+

 

+

 

+

def update_region_status(region_id, new_status): 

+

"""Update region. 

+

 

+

:param region_id: 

+

:param new_status: 

+

:return: 

+

""" 

+

LOG.debug("Update region id: {} status to: {}".format(region_id, 

+

new_status)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

result = db.update_region_status(region_id, new_status) 

+

return result 

+

 

+

except Exception as exp: 

+

LOG.exception("Error updating status for region id:".format(region_id)) 

+

raise 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_storage___init___py.html b/orm/services/region_manager/cover/rms_storage___init___py.html new file mode 100644 index 00000000..b9e43969 --- /dev/null +++ b/orm/services/region_manager/cover/rms_storage___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/storage/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_storage_base_data_manager_py.html b/orm/services/region_manager/cover/rms_storage_base_data_manager_py.html new file mode 100644 index 00000000..c3b0d045 --- /dev/null +++ b/orm/services/region_manager/cover/rms_storage_base_data_manager_py.html @@ -0,0 +1,331 @@ + + + + + + + + + + + Coverage for rms/storage/base_data_manager.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+ +
+

 

+

class BaseDataManager(object): 

+

 

+

def __init__(self, url, 

+

max_retries, 

+

retry_interval): 

+

pass 

+

 

+

def add_region(self, 

+

region_id, 

+

name, 

+

address_state, 

+

address_country, 

+

address_city, 

+

address_street, 

+

address_zip, 

+

region_status, 

+

ranger_agent_version, 

+

open_stack_version, 

+

design_type, 

+

location_type, 

+

vlcp_name, 

+

clli, 

+

description, 

+

meta_data_list, 

+

end_point_list, 

+

is_ecomp, 

+

is_ssp, 

+

purpose_of_region): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def delete_region(self, 

+

region_id): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

def get_regions(self, 

+

region_filters_dict, 

+

meta_data_dict, 

+

end_point_dict): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def get_all_regions(self): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def add_meta_data_to_region(self, 

+

region_id, 

+

key, 

+

value, 

+

description): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def remove_meta_data_from_region(self, 

+

region_id, 

+

key): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def add_end_point_to_region(self, 

+

region_id, 

+

end_point_type, 

+

end_point_url, 

+

description): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def remove_end_point_from_region(self, 

+

region_id, 

+

end_point_type): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

def add_group(self, 

+

group_id, 

+

group_name, 

+

group_description, 

+

region_ids_list): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def delete_group(self, 

+

group_name): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

def get_group(self, group_id): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def get_all_groups(self): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def add_region_to_group(self, 

+

group_id, 

+

region_id): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def remove_region_from_group(self, 

+

group_id, 

+

region_id): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

 

+

class SQLDBError(Exception): 

+

pass 

+

 

+

 

+

class EntityNotFound(Exception): 

+

"""if item not found in DB.""" 

+

pass 

+

 

+

 

+

class DuplicateEntryError(Exception): 

+

"""A group already exists.""" 

+

pass 

+

 

+

 

+

class InputValueError(Exception): 

+

""" unvalid input from user""" 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_storage_data_manager_factory_py.html b/orm/services/region_manager/cover/rms_storage_data_manager_factory_py.html new file mode 100644 index 00000000..87d1ef0d --- /dev/null +++ b/orm/services/region_manager/cover/rms_storage_data_manager_factory_py.html @@ -0,0 +1,129 @@ + + + + + + + + + + + Coverage for rms/storage/data_manager_factory.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+ +
+

import logging 

+

 

+

from pecan import conf 

+

 

+

from rms.storage.my_sql.data_manager import DataManager 

+

 

+

LOG = logging.getLogger(__name__) 

+

 

+

 

+

def get_data_manager(): 

+

try: 

+

dm = DataManager(url=conf.database.url, 

+

max_retries=conf.database.max_retries, 

+

retries_interval=conf.database.retries_interval) 

+

return dm 

+

except Exception: 

+

nagios_message = "CRITICAL|CONDB001 - Could not establish " \ 

+

"database connection" 

+

LOG.error(nagios_message) 

+

raise Exception("Could not establish database connection") 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_storage_my_sql___init___py.html b/orm/services/region_manager/cover/rms_storage_my_sql___init___py.html new file mode 100644 index 00000000..a6e95539 --- /dev/null +++ b/orm/services/region_manager/cover/rms_storage_my_sql___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/storage/my_sql/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_storage_my_sql_data_manager_py.html b/orm/services/region_manager/cover/rms_storage_my_sql_data_manager_py.html new file mode 100644 index 00000000..589a433b --- /dev/null +++ b/orm/services/region_manager/cover/rms_storage_my_sql_data_manager_py.html @@ -0,0 +1,1147 @@ + + + + + + + + + + + Coverage for rms/storage/my_sql/data_manager.py: 89% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+

500

+

501

+

502

+

503

+

504

+

505

+

506

+

507

+

508

+

509

+

510

+

511

+

512

+

513

+

514

+

515

+

516

+

517

+

518

+

519

+

520

+

521

+

522

+

523

+

524

+

525

+

526

+

527

+

528

+

529

+ +
+

import logging 

+

 

+

import oslo_db 

+

from oslo_db.sqlalchemy import session as db_session 

+

from sqlalchemy.ext.declarative.api import declarative_base 

+

from sqlalchemy.sql import or_ 

+

 

+

from rms.services import error_base as ServiceBase 

+

from data_models import Region, RegionEndPoint, Group 

+

from data_models import RegionMetaData, GroupRegion 

+

from rms.services import error_base 

+

from rms.storage.base_data_manager import BaseDataManager, DuplicateEntryError, EntityNotFound 

+

from rms.model import model as PythonModels 

+

 

+

Base = declarative_base() 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class DataManager(BaseDataManager): 

+

 

+

def __init__(self, url, max_retries, retries_interval): 

+

self._engine_facade = db_session.EngineFacade(url, 

+

max_retries=max_retries, 

+

retry_interval=retries_interval) 

+

 

+

def add_region(self, 

+

region_id, 

+

name, 

+

address_state, 

+

address_country, 

+

address_city, 

+

address_street, 

+

address_zip, 

+

region_status, 

+

ranger_agent_version, 

+

open_stack_version, 

+

design_type, 

+

location_type, 

+

vlcp_name, 

+

clli, 

+

# a list of dictionaries of format 

+

# {"type":"", "url":"", "description":"" 

+

end_point_list, 

+

# a dictionary of key,value pairs 

+

# {"key":"value", } 

+

meta_data_dict, 

+

is_ecomp, 

+

is_ssp, 

+

purpose_of_region="", 

+

description=""): 

+

""" add a new region to the `region` table 

+

add also the regions give meta_data and end_points to the `region_end_point` and 

+

`region_meta_data` tables if given. 

+

handle duplicate errors if raised""" 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

region = Region(region_id=region_id, 

+

name=name, 

+

address_state=address_state, 

+

address_country=address_country, 

+

address_city=address_city, 

+

address_street=address_street, 

+

address_zip=address_zip, 

+

region_status=region_status, 

+

ranger_agent_version=ranger_agent_version, 

+

open_stack_version=open_stack_version, 

+

design_type=design_type, 

+

location_type=location_type, 

+

vlcp_name=vlcp_name, 

+

clli=clli, 

+

description=description, 

+

is_ecomp=is_ecomp * 1, 

+

is_ssp=is_ssp * 1, 

+

purpose_of_region=purpose_of_region) 

+

 

+

if end_point_list is not None: 

+

for end_point in end_point_list: 

+

region_end_point = RegionEndPoint( 

+

end_point_type=end_point["type"], 

+

public_url=end_point["url"]) 

+

region.end_points.append(region_end_point) 

+

 

+

if meta_data_dict is not None: 

+

for k, v in meta_data_dict.iteritems(): 

+

for list_item in v: 

+

region.meta_data.append( 

+

RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

 

+

session.add(region) 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.warning("Duplicate entry: {}".format(str(e))) 

+

raise DuplicateEntryError("Region {} already " 

+

"exist".format(region_id)) 

+

 

+

def update_region(self, 

+

region_to_update, 

+

region_id, 

+

name, 

+

address_state, 

+

address_country, 

+

address_city, 

+

address_street, 

+

address_zip, 

+

region_status, 

+

ranger_agent_version, 

+

open_stack_version, 

+

design_type, 

+

location_type, 

+

vlcp_name, 

+

clli, 

+

# a list of dictionaries of format 

+

# {"type":"", "url":"", "description":"" 

+

end_point_list, 

+

# a list of dictionaries of format 

+

# {"key":"", "value":"", "description":"" 

+

meta_data_dict, 

+

is_ecomp, 

+

is_ssp, 

+

purpose_of_region="", 

+

description=""): 

+

""" add a new region to the `region` table 

+

add also the regions give meta_data and end_points to the `region_end_point` and 

+

`region_meta_data` tables if given. 

+

handle duplicate errors if raised""" 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

# remove all childs as with update need to replace them 

+

session.query(RegionMetaData).filter_by(region_id=region_to_update).delete() 

+

session.query(RegionEndPoint).filter_by(region_id=region_to_update).delete() 

+

 

+

record = session.query(Region).filter_by(region_id=region_to_update).first() 

+

if record is not None: 

+

# record.region_id = region_id # ignore id and name when update 

+

# record.name = name 

+

record.address_state = address_state 

+

record.address_country = address_country 

+

record.address_city = address_city 

+

record.address_street = address_street 

+

record.address_zip = address_zip 

+

record.region_status = region_status 

+

record.ranger_agent_version = ranger_agent_version 

+

record.open_stack_version = open_stack_version 

+

record.design_type = design_type 

+

record.location_type = location_type 

+

record.vlcp_name = vlcp_name 

+

record.clli = clli 

+

record.description = description 

+

record.is_ecomp = is_ecomp * 1 

+

record.is_ssp = is_ssp * 1 

+

record.purpose_of_region = purpose_of_region 

+

 

+

if end_point_list is not None: 

+

for end_point in end_point_list: 

+

region_end_point = RegionEndPoint( 

+

end_point_type=end_point["type"], 

+

public_url=end_point["url"] 

+

) 

+

record.end_points.append(region_end_point) 

+

 

+

if meta_data_dict is not None: 

+

for k, v in meta_data_dict.iteritems(): 

+

for list_item in v: 

+

record.meta_data.append( 

+

RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

else: 

+

raise EntityNotFound("Region {} not found".format( 

+

region_to_update)) 

+

except EntityNotFound as exp: 

+

logger.exception( 

+

"fail to update entity with id {} not found".format( 

+

region_to_update)) 

+

raise ServiceBase.NotFoundError(message=exp.message) 

+

except Exception as exp: 

+

logger.exception("fail to update region {}".format(str(exp))) 

+

raise 

+

 

+

def delete_region(self, region_id): 

+

# delete a region from `region` table and also the region's 

+

# entries from `region_meta_data` and `region_end_points` tables 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(Region).filter_by(region_id=region_id).delete() 

+

 

+

def get_all_regions(self): 

+

return self.get_regions(None, None, None) 

+

 

+

def get_regions(self, 

+

region_filters_dict, 

+

meta_data_dict, 

+

end_point_dict): 

+

logger.debug("Get regions") 

+

records_model = [] 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

records = session.query(Region) 

+

if region_filters_dict is not None: 

+

records = records.filter_by(**region_filters_dict) 

+

 

+

if meta_data_dict is not None: 

+

regions = self._get_regions_for_meta_data_dict(meta_data_dict, 

+

session) 

+

query = [] 

+

query.append((Region.region_id.in_(regions))) 

+

records = records.filter(*query) 

+

 

+

if end_point_dict is not None: 

+

records = records.join(RegionEndPoint).\ 

+

filter_by(**end_point_dict) 

+

if records is not None: 

+

for record in records: 

+

records_model.append(record.to_wsme()) 

+

return records_model 

+

 

+

def _get_regions_for_meta_data_dict(self, meta_data_dict, session): 

+

result_lists = [] 

+

for key in meta_data_dict['meta_data_keys']: 

+

md_q = session.query(RegionMetaData). \ 

+

filter(RegionMetaData.meta_data_key == key).all() 

+

temp_result_list = [] 

+

if md_q is not None: 

+

for record in md_q: 

+

temp_result_list.append(record.region_id) 

+

result_lists.append(set(temp_result_list)) 

+

logger.debug(set(temp_result_list)) 

+

for item in meta_data_dict['meta_data_pairs']: 

+

md_q = session.query(RegionMetaData). \ 

+

filter(RegionMetaData.meta_data_key == item['metadata_key'], 

+

RegionMetaData.meta_data_value == item['metadata_value']).all() 

+

temp_result_list = [] 

+

if md_q is not None: 

+

for record in md_q: 

+

temp_result_list.append(record.region_id) 

+

result_lists.append(set(temp_result_list)) 

+

logger.debug(set(temp_result_list)) 

+

 

+

result = [] 

+

if result_lists: 

+

result = result_lists[0] 

+

for l in result_lists: 

+

result = result.intersection(l) 

+

else: 

+

result = None 

+

logger.debug(result) 

+

return result 

+

 

+

def get_region_by_id_or_name(self, region_id_or_name): 

+

logger.debug("Get region by id or name: {}".format(region_id_or_name)) 

+

try: 

+

 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

record = session.query(Region) 

+

record = record.filter(or_(Region.region_id == region_id_or_name, 

+

Region.name == region_id_or_name)) 

+

if record.first(): 

+

return record.first().to_wsme() 

+

return None 

+

 

+

except Exception as exp: 

+

logger.exception("DB error filtering by id/name") 

+

raise 

+

 

+

def add_meta_data_to_region(self, region_id, 

+

metadata_dict): 

+

""" 

+

:param region_id: 

+

:param metadata_dict: 

+

:return: 

+

""" 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

record = session.query(Region).\ 

+

filter_by(region_id=region_id).first() 

+

 

+

if record is not None: 

+

region_metadata = [] 

+

for k, v in metadata_dict.iteritems(): 

+

for list_item in v: 

+

region_metadata.append(RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

session.add_all(region_metadata) 

+

return record.to_wsme() 

+

else: 

+

logger.error("Region {} does not exist. " 

+

"Meta Data was not added!".format(region_id)) 

+

return None 

+

 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.warning("Duplicate entry: {}".format(str(e))) 

+

raise error_base.ConflictError(message="Duplicate metadata value " 

+

"in region {}".format(region_id)) 

+

 

+

def update_region_meta_data(self, region_id, 

+

metadata_dict): 

+

""" 

+

Replace existing metadata for given region_id 

+

:param region_id: 

+

:param metadata_dict: 

+

:return: 

+

""" 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

 

+

record = session.query(Region). \ 

+

filter_by(region_id=region_id).first() 

+

if not record: 

+

msg = "Region {} not found".format(region_id) 

+

logger.info(msg) 

+

raise error_base.NotFoundError(message=msg) 

+

 

+

session.query(RegionMetaData).\ 

+

filter_by(region_id=region_id).delete() 

+

 

+

region_metadata = [] 

+

for k, v in metadata_dict.iteritems(): 

+

for list_item in v: 

+

region_metadata.append(RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

 

+

session.add_all(region_metadata) 

+

return record.to_wsme() 

+

 

+

def delete_region_metadata(self, region_id, key): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

record = session.query(Region). \ 

+

filter_by(region_id=region_id).first() 

+

 

+

if not record: 

+

msg = "Region {} not found".format(region_id) 

+

logger.info(msg) 

+

raise error_base.NotFoundError(message=msg) 

+

 

+

session.query(RegionMetaData).filter_by(region_id=region_id, 

+

meta_data_key=key).delete() 

+

 

+

def update_region_status(self, region_id, region_status): 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

 

+

record = session.query(Region).filter_by(region_id=region_id).first() 

+

if record is not None: 

+

record.region_status = region_status 

+

else: 

+

msg = "Region {} not found".format(region_id) 

+

logger.info(msg) 

+

raise error_base.NotFoundError(message=msg) 

+

return record.region_status 

+

 

+

except Exception as exp: 

+

logger.exception("failed to update region {}".format(str(exp))) 

+

raise 

+

""" 

+

def add_end_point_to_region(self, 

+

region_id, 

+

end_point_type, 

+

end_point_url, 

+

description): 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

record = session.query(Region).filter_by(region_id=region_id).\ 

+

first() 

+

if record is not None: 

+

session.add( 

+

RegionEndPoint(region_id=region_id, 

+

end_point_type=end_point_type, 

+

public_url=end_point_url, 

+

description=description)) 

+

else: 

+

logger.error("Region {} does not exist. " 

+

"End point was not added !".format(region_id)) 

+

 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.warning("Duplicate entry: {}".format(str(e))) 

+

raise SQLDBError("Duplicate entry error") 

+

 

+

def remove_end_point_from_region(self, 

+

region_id, 

+

end_point_type): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(Region).filter_by(region_id=region_id, 

+

end_point_type=end_point_type).\ 

+

delete() 

+

""" 

+

 

+

# Handle group management operations 

+

def add_group(self, 

+

group_id, 

+

group_name, 

+

group_description, 

+

region_ids_list): 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

session.add(Group(group_id=group_id, 

+

name=group_name, 

+

description=group_description)) 

+

 

+

session.flush() # add the groupe if not rollback 

+

 

+

if region_ids_list is not None: 

+

group_regions = [] 

+

for region_id in region_ids_list: 

+

group_regions.append(GroupRegion(group_id=group_id, 

+

region_id=region_id)) 

+

session.add_all(group_regions) 

+

except oslo_db.exception.DBReferenceError as e: 

+

logger.error("Reference error: {}".format(str(e))) 

+

raise error_base.InputValueError("Reference error") 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.error("Duplicate entry: {}".format(str(e))) 

+

raise error_base.ConflictError("Duplicate entry error") 

+

 

+

def delete_group(self, group_id): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(Group).filter_by(group_id=group_id).delete() 

+

 

+

def get_all_groups(self): 

+

logger.debug("DB- Get all groups") 

+

records_model = PythonModels.GroupsWrraper() 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

groups = session.query(Group) 

+

for a_group in groups: 

+

group_model = PythonModels.Groups() 

+

group_model.id = a_group.group_id 

+

group_model.name = a_group.name 

+

group_model.description = a_group.description 

+

regions = [] 

+

group_regions = session.query(GroupRegion).\ 

+

filter_by(group_id=a_group.group_id) 

+

for group_region in group_regions: 

+

regions.append(group_region.region_id) 

+

 

+

group_model.regions = regions 

+

records_model.groups.append(group_model) 

+

return records_model 

+

 

+

def update_group(self, group_id, group_name, group_description, 

+

group_regions): 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

# in update scenario delete all child records 

+

session.query(GroupRegion).filter_by( 

+

group_id=group_id).delete() 

+

 

+

group_record = session.query(Group).filter_by( 

+

group_id=group_id).first() 

+

if group_record is None: 

+

raise error_base.NotFoundError( 

+

message="Group {} not found".format(group_id)) 

+

# only desc and regions can be changed 

+

group_record.description = group_description 

+

group_record.name = group_name 

+

regions = [] 

+

for region_id in group_regions: 

+

regions.append(GroupRegion(region_id=region_id, 

+

group_id=group_id)) 

+

session.add_all(regions) 

+

 

+

except error_base.NotFoundError as exp: 

+

logger.error(exp.message) 

+

raise 

+

except oslo_db.exception.DBReferenceError as e: 

+

logger.error("Reference error: {}".format(str(e))) 

+

raise error_base.InputValueError("Reference error") 

+

except Exception as exp: 

+

logger.error("failed to update group {}".format(group_id)) 

+

logger.exception(exp) 

+

raise 

+

return 

+

 

+

def get_group(self, group_id): 

+

logger.debug("Get group by name") 

+

group_model = None 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

a_group = session.query(Group).filter_by(group_id=group_id)\ 

+

.first() 

+

if a_group is not None: 

+

group_model = {"id": a_group.group_id, 

+

"name": a_group.name, 

+

"description": a_group.description} 

+

regions = [] 

+

group_regions = session.query(GroupRegion). \ 

+

filter_by(group_id=a_group.group_id) 

+

for group_region in group_regions: 

+

regions.append(group_region.region_id) 

+

group_model["regions"] = regions 

+

return group_model 

+

 

+

""" 

+

def add_region_to_group(self, 

+

group_id, 

+

region_id): 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

session.add(GroupRegion(group_id=group_id, 

+

region_id=region_id)) 

+

except oslo_db.exception.DBReferenceError as e: 

+

logger.error("Refernce error: {}".format(str(e))) 

+

raise SQLDBError("Reference error") 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.error("Duplicate entry: {}".format(str(e))) 

+

raise SQLDBError("Duplicate entry error") 

+

 

+

def remove_region_from_group(self, 

+

group_id, 

+

region_id): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(GroupRegion).filter_by(group_id=group_id, 

+

region_id=region_id).delete() 

+

""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_utils___init___py.html b/orm/services/region_manager/cover/rms_utils___init___py.html new file mode 100644 index 00000000..e9506082 --- /dev/null +++ b/orm/services/region_manager/cover/rms_utils___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/utils/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/cover/rms_utils_authentication_py.html b/orm/services/region_manager/cover/rms_utils_authentication_py.html new file mode 100644 index 00000000..70124848 --- /dev/null +++ b/orm/services/region_manager/cover/rms_utils_authentication_py.html @@ -0,0 +1,195 @@ + + + + + + + + + + + Coverage for rms/utils/authentication.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+ +
+

import logging 

+

 

+

from keystone_utils import tokens 

+

from orm_common.policy import policy 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

from pecan import conf 

+

 

+

from rms.services import services as RegionService 

+

 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

def _get_keystone_ep(auth_region): 

+

result = RegionService.get_region_by_id_or_name(auth_region) 

+

for ep in result.endpoints: 

+

if ep.type == 'identity': 

+

return ep.publicurl 

+

 

+

# Keystone EP not found 

+

return None 

+

 

+

 

+

def authorize(request, action): 

+

if not _is_authorization_enabled(conf): 

+

return 

+

 

+

auth_region = request.headers.get('X-Auth-Region') 

+

try: 

+

keystone_ep = _get_keystone_ep(auth_region) 

+

except Exception: 

+

# Failed to find Keystone EP - we'll set it to None instead of failing 

+

# because the rule might be to let everyone pass 

+

keystone_ep = None 

+

 

+

policy.authorize(action, request, conf, keystone_ep=keystone_ep) 

+

 

+

 

+

def _is_authorization_enabled(app_conf): 

+

return app_conf.authentication.enabled 

+

 

+

 

+

def get_token_conf(app_conf): 

+

mech_id = app_conf.authentication.mech_id 

+

mech_password = app_conf.authentication.mech_pass 

+

# RMS URL is not necessary since this service is RMS 

+

rms_url = '' 

+

tenant_name = app_conf.authentication.tenant_name 

+

keystone_version = app_conf.authentication.keystone_version 

+

conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name, 

+

keystone_version) 

+

return conf 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/cover/status.json b/orm/services/region_manager/cover/status.json new file mode 100644 index 00000000..ae70bd7f --- /dev/null +++ b/orm/services/region_manager/cover/status.json @@ -0,0 +1 @@ +{"files":{"rms_controllers_v2_orm_resources___init___py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/__init__.py","html_filename":"rms_controllers_v2_orm_resources___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"103b3a1826780f920626a4f622bcee7c"},"rms___init___py":{"index":{"relative_filename":"rms/__init__.py","html_filename":"rms___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_external_mock_audit_client_api___init___py":{"index":{"relative_filename":"rms/external_mock/audit_client/api/__init__.py","html_filename":"rms_external_mock_audit_client_api___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"19a064df3967067a2ec86c467e92d140"},"rms_controllers_lcp_controller_py":{"index":{"relative_filename":"rms/controllers/lcp_controller.py","html_filename":"rms_controllers_lcp_controller_py.html","nums":[1,56,0,0,0,0,0]},"hash":"a032e4765d0af628116fb4a525003601"},"rms_utils_authentication_py":{"index":{"relative_filename":"rms/utils/authentication.py","html_filename":"rms_utils_authentication_py.html","nums":[1,32,0,0,0,0,0]},"hash":"9816983ff8e57e8d1a9ed68b7c30ff50"},"rms_external_mock_orm_common___init___py":{"index":{"relative_filename":"rms/external_mock/orm_common/__init__.py","html_filename":"rms_external_mock_orm_common___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_model___init___py":{"index":{"relative_filename":"rms/model/__init__.py","html_filename":"rms_model___init___py.html","nums":[1,3,0,0,0,0,0]},"hash":"cf7982d8674fcb1858c46e0ed829f1e2"},"rms_storage_my_sql___init___py":{"index":{"relative_filename":"rms/storage/my_sql/__init__.py","html_filename":"rms_storage_my_sql___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_model_url_parm_py":{"index":{"relative_filename":"rms/model/url_parm.py","html_filename":"rms_model_url_parm_py.html","nums":[1,63,0,4,0,0,0]},"hash":"554a5d26967309c0321bb2d5ca4de2d0"},"rms_controllers_root_py":{"index":{"relative_filename":"rms/controllers/root.py","html_filename":"rms_controllers_root_py.html","nums":[1,12,0,1,0,0,0]},"hash":"22b13e705390f352b9e7a78c2f83d7b9"},"rms_external_mock_keystone_utils___init___py":{"index":{"relative_filename":"rms/external_mock/keystone_utils/__init__.py","html_filename":"rms_external_mock_keystone_utils___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_controllers_v2_orm_resources_groups_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/groups.py","html_filename":"rms_controllers_v2_orm_resources_groups_py.html","nums":[1,124,0,22,0,0,0]},"hash":"38e755d11e73209da31ce4c4faff8d49"},"rms_controllers_v2_orm_resources_status_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/status.py","html_filename":"rms_controllers_v2_orm_resources_status_py.html","nums":[1,47,0,3,0,0,0]},"hash":"afc1e6ecd339095ff57e6b9c7c9c819d"},"rms_services_error_base_py":{"index":{"relative_filename":"rms/services/error_base.py","html_filename":"rms_services_error_base_py.html","nums":[1,18,0,2,0,0,0]},"hash":"9cf5cf89d59dd8f73527b7bec0f6f11d"},"rms_external_mock_orm_common_policy_policy_py":{"index":{"relative_filename":"rms/external_mock/orm_common/policy/policy.py","html_filename":"rms_external_mock_orm_common_policy_policy_py.html","nums":[1,6,0,2,0,0,0]},"hash":"7ea56b6475a95231ec2271c5fb347853"},"rms_storage___init___py":{"index":{"relative_filename":"rms/storage/__init__.py","html_filename":"rms_storage___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_external_mock_orm_common_policy___init___py":{"index":{"relative_filename":"rms/external_mock/orm_common/policy/__init__.py","html_filename":"rms_external_mock_orm_common_policy___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_controllers_v2_orm___init___py":{"index":{"relative_filename":"rms/controllers/v2/orm/__init__.py","html_filename":"rms_controllers_v2_orm___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"f820fd7cab97fc8512bec0556f3f002c"},"rms_controllers_v2___init___py":{"index":{"relative_filename":"rms/controllers/v2/__init__.py","html_filename":"rms_controllers_v2___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"103b3a1826780f920626a4f622bcee7c"},"rms_controllers_v2_orm_resources_regions_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/regions.py","html_filename":"rms_controllers_v2_orm_resources_regions_py.html","nums":[1,187,0,5,0,0,0]},"hash":"31c3b8e26317be7b0c19896d4f008617"},"rms_model_model_py":{"index":{"relative_filename":"rms/model/model.py","html_filename":"rms_model_model_py.html","nums":[1,100,0,7,0,0,0]},"hash":"29463d225fbe8d7a76bd2c96fc854ea9"},"rms_external_mock_orm_common_utils_utils_py":{"index":{"relative_filename":"rms/external_mock/orm_common/utils/utils.py","html_filename":"rms_external_mock_orm_common_utils_utils_py.html","nums":[1,6,0,1,0,0,0]},"hash":"3fd1f8e4472126ea5ccdbf31e97d61ea"},"rms_services___init___py":{"index":{"relative_filename":"rms/services/__init__.py","html_filename":"rms_services___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"8ac99c36cc7a0f004e5b32bd37a07666"},"rms_controllers_configuration_py":{"index":{"relative_filename":"rms/controllers/configuration.py","html_filename":"rms_controllers_configuration_py.html","nums":[1,17,0,0,0,0,0]},"hash":"b26d7e4d8662a3399ee6bfcca7910f6a"},"rms_storage_data_manager_factory_py":{"index":{"relative_filename":"rms/storage/data_manager_factory.py","html_filename":"rms_storage_data_manager_factory_py.html","nums":[1,12,0,0,0,0,0]},"hash":"187c41f6ce3b54925ae87043fbd31c9d"},"rms_storage_base_data_manager_py":{"index":{"relative_filename":"rms/storage/base_data_manager.py","html_filename":"rms_storage_base_data_manager_py.html","nums":[1,24,0,0,0,0,0]},"hash":"161f32081cdb26e50f07e76b8b1a9a5d"},"rms_controllers_v2_orm_resources_metadata_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/metadata.py","html_filename":"rms_controllers_v2_orm_resources_metadata_py.html","nums":[1,85,0,19,0,0,0]},"hash":"7bb73ee1f834e6f8998371ce19f77ade"},"rms_external_mock___init___py":{"index":{"relative_filename":"rms/external_mock/__init__.py","html_filename":"rms_external_mock___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"19a064df3967067a2ec86c467e92d140"},"rms_storage_my_sql_data_manager_py":{"index":{"relative_filename":"rms/storage/my_sql/data_manager.py","html_filename":"rms_storage_my_sql_data_manager_py.html","nums":[1,270,0,31,0,0,0]},"hash":"5a1cbfe3ec7b9c95011d5586e27057cb"},"rms_external_mock_keystone_utils_tokens_py":{"index":{"relative_filename":"rms/external_mock/keystone_utils/tokens.py","html_filename":"rms_external_mock_keystone_utils_tokens_py.html","nums":[1,5,0,1,0,0,0]},"hash":"9e3b83a68d18e91870ada743cc6e8aaf"},"rms_controllers_v2_root_py":{"index":{"relative_filename":"rms/controllers/v2/root.py","html_filename":"rms_controllers_v2_root_py.html","nums":[1,3,0,0,0,0,0]},"hash":"a6ab4c4b9cc373374c0cdf71e05f92b2"},"rms_external_mock_orm_common_utils_api_error_utils_py":{"index":{"relative_filename":"rms/external_mock/orm_common/utils/api_error_utils.py","html_filename":"rms_external_mock_orm_common_utils_api_error_utils_py.html","nums":[1,2,0,1,0,0,0]},"hash":"5b097d36789f1b7dea3d91b3b8d3718a"},"rms_services_services_py":{"index":{"relative_filename":"rms/services/services.py","html_filename":"rms_services_services_py.html","nums":[1,170,0,55,0,0,0]},"hash":"ee882c9e5fa6c871e813a62a7126e7f8"},"rms_external_mock_audit_client_api_audit_py":{"index":{"relative_filename":"rms/external_mock/audit_client/api/audit.py","html_filename":"rms_external_mock_audit_client_api_audit_py.html","nums":[1,4,0,4,0,0,0]},"hash":"b0c363258cf77b3628c98037bd768ab4"},"rms_controllers___init___py":{"index":{"relative_filename":"rms/controllers/__init__.py","html_filename":"rms_controllers___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_controllers_logs_py":{"index":{"relative_filename":"rms/controllers/logs.py","html_filename":"rms_controllers_logs_py.html","nums":[1,36,0,2,0,0,0]},"hash":"8e5e06cf27f60958af7f39bc4c28a5b9"},"rms_external_mock_audit_client___init___py":{"index":{"relative_filename":"rms/external_mock/audit_client/__init__.py","html_filename":"rms_external_mock_audit_client___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"19a064df3967067a2ec86c467e92d140"},"rms_controllers_v2_orm_root_py":{"index":{"relative_filename":"rms/controllers/v2/orm/root.py","html_filename":"rms_controllers_v2_orm_root_py.html","nums":[1,5,0,0,0,0,0]},"hash":"7d9f45649dfaa9de7107f13e60452b2e"},"rms_external_mock_orm_common_utils___init___py":{"index":{"relative_filename":"rms/external_mock/orm_common/utils/__init__.py","html_filename":"rms_external_mock_orm_common_utils___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_utils___init___py":{"index":{"relative_filename":"rms/utils/__init__.py","html_filename":"rms_utils___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"}},"version":"4.3.4","settings":"952e86a4dc73f0a7bf1b5052a0d6c45a","format":1} \ No newline at end of file diff --git a/orm/services/region_manager/cover/style.css b/orm/services/region_manager/cover/style.css new file mode 100644 index 00000000..86b82091 --- /dev/null +++ b/orm/services/region_manager/cover/style.css @@ -0,0 +1,375 @@ +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ + +/* CSS styles for coverage.py. */ + +/* Page-wide styles */ +html, body, h1, h2, h3, p, table, td, th { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; + } + +/* Set baseline grid to 16 pt. */ +body { + font-family: georgia, serif; + font-size: 1em; + } + +html>body { + font-size: 16px; + } + +/* Set base font size to 12/16 */ +p { + font-size: .75em; /* 12/16 */ + line-height: 1.33333333em; /* 16/12 */ + } + +table { + border-collapse: collapse; + } +td { + vertical-align: top; +} +table tr.hidden { + display: none !important; + } + +p#no_rows { + display: none; + font-size: 1.2em; + } + +a.nav { + text-decoration: none; + color: inherit; + } +a.nav:hover { + text-decoration: underline; + color: inherit; + } + +/* Page structure */ +#header { + background: #f8f8f8; + width: 100%; + border-bottom: 1px solid #eee; + } + +#source { + padding: 1em; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + } + +.indexfile #footer { + margin: 1em 3em; + } + +.pyfile #footer { + margin: 1em 1em; + } + +#footer .content { + padding: 0; + font-size: 85%; + font-family: verdana, sans-serif; + color: #666666; + font-style: italic; + } + +#index { + margin: 1em 0 0 3em; + } + +/* Header styles */ +#header .content { + padding: 1em 3em; + } + +h1 { + font-size: 1.25em; + display: inline-block; +} + +#filter_container { + display: inline-block; + float: right; + margin: 0 2em 0 0; +} +#filter_container input { + width: 10em; +} + +h2.stats { + margin-top: .5em; + font-size: 1em; +} +.stats span { + border: 1px solid; + padding: .1em .25em; + margin: 0 .1em; + cursor: pointer; + border-color: #999 #ccc #ccc #999; +} +.stats span.hide_run, .stats span.hide_exc, +.stats span.hide_mis, .stats span.hide_par, +.stats span.par.hide_run.hide_par { + border-color: #ccc #999 #999 #ccc; +} +.stats span.par.hide_run { + border-color: #999 #ccc #ccc #999; +} + +.stats span.run { + background: #ddffdd; +} +.stats span.exc { + background: #eeeeee; +} +.stats span.mis { + background: #ffdddd; +} +.stats span.hide_run { + background: #eeffee; +} +.stats span.hide_exc { + background: #f5f5f5; +} +.stats span.hide_mis { + background: #ffeeee; +} +.stats span.par { + background: #ffffaa; +} +.stats span.hide_par { + background: #ffffcc; +} + +/* Help panel */ +#keyboard_icon { + float: right; + margin: 5px; + cursor: pointer; +} + +.help_panel { + position: absolute; + background: #ffffcc; + padding: .5em; + border: 1px solid #883; + display: none; +} + +.indexfile .help_panel { + width: 20em; height: 4em; +} + +.pyfile .help_panel { + width: 16em; height: 8em; +} + +.help_panel .legend { + font-style: italic; + margin-bottom: 1em; +} + +#panel_icon { + float: right; + cursor: pointer; +} + +.keyhelp { + margin: .75em; +} + +.keyhelp .key { + border: 1px solid black; + border-color: #888 #333 #333 #888; + padding: .1em .35em; + font-family: monospace; + font-weight: bold; + background: #eee; +} + +/* Source file styles */ +.linenos p { + text-align: right; + margin: 0; + padding: 0 .5em; + color: #999999; + font-family: verdana, sans-serif; + font-size: .625em; /* 10/16 */ + line-height: 1.6em; /* 16/10 */ + } +.linenos p.highlight { + background: #ffdd00; + } +.linenos p a { + text-decoration: none; + color: #999999; + } +.linenos p a:hover { + text-decoration: underline; + color: #999999; + } + +td.text { + width: 100%; + } +.text p { + margin: 0; + padding: 0 0 0 .5em; + border-left: 2px solid #ffffff; + white-space: pre; + position: relative; + } + +.text p.mis { + background: #ffdddd; + border-left: 2px solid #ff0000; + } +.text p.run, .text p.run.hide_par { + background: #ddffdd; + border-left: 2px solid #00ff00; + } +.text p.exc { + background: #eeeeee; + border-left: 2px solid #808080; + } +.text p.par, .text p.par.hide_run { + background: #ffffaa; + border-left: 2px solid #eeee99; + } +.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, +.text p.hide_run.hide_par { + background: inherit; + } + +.text span.annotate { + font-family: georgia; + color: #666; + float: right; + padding-right: .5em; + } +.text p.hide_par span.annotate { + display: none; + } +.text span.annotate.long { + display: none; + } +.text p:hover span.annotate.long { + display: block; + max-width: 50%; + white-space: normal; + float: right; + position: absolute; + top: 1.75em; + right: 1em; + width: 30em; + height: auto; + color: #333; + background: #ffffcc; + border: 1px solid #888; + padding: .25em .5em; + z-index: 999; + border-radius: .2em; + box-shadow: #cccccc .2em .2em .2em; + } + +/* Syntax coloring */ +.text .com { + color: green; + font-style: italic; + line-height: 1px; + } +.text .key { + font-weight: bold; + line-height: 1px; + } +.text .str { + color: #000080; + } + +/* index styles */ +#index td, #index th { + text-align: right; + width: 5em; + padding: .25em .5em; + border-bottom: 1px solid #eee; + } +#index th { + font-style: italic; + color: #333; + border-bottom: 1px solid #ccc; + cursor: pointer; + } +#index th:hover { + background: #eee; + border-bottom: 1px solid #999; + } +#index td.left, #index th.left { + padding-left: 0; + } +#index td.right, #index th.right { + padding-right: 0; + } +#index th.headerSortDown, #index th.headerSortUp { + border-bottom: 1px solid #000; + white-space: nowrap; + background: #eee; + } +#index th.headerSortDown:after { + content: " ↓"; +} +#index th.headerSortUp:after { + content: " ↑"; +} +#index td.name, #index th.name { + text-align: left; + width: auto; + } +#index td.name a { + text-decoration: none; + color: #000; + } +#index tr.total, +#index tr.total_dynamic { + } +#index tr.total td, +#index tr.total_dynamic td { + font-weight: bold; + border-top: 1px solid #ccc; + border-bottom: none; + } +#index tr.file:hover { + background: #eeeeee; + } +#index tr.file:hover td.name { + text-decoration: underline; + color: #000; + } + +/* scroll marker styles */ +#scroll_marker { + position: fixed; + right: 0; + top: 0; + width: 16px; + height: 100%; + background: white; + border-left: 1px solid #eee; + } + +#scroll_marker .marker { + background: #eedddd; + position: absolute; + min-height: 3px; + width: 100%; + } diff --git a/orm/services/region_manager/csv2db.py b/orm/services/region_manager/csv2db.py new file mode 100755 index 00000000..5961dafc --- /dev/null +++ b/orm/services/region_manager/csv2db.py @@ -0,0 +1,57 @@ +import logging +import csv + +from rms.storage.base_data_manager import SQLDBError + +from rms.storage.my_sql.data_manager import DataManager +import config + +logger = logging.getLogger(__name__) + + +def load_csv2db(data_manager): + logger.info('Loading csv to db..') + + try: + + with open('rms/resources/regions.csv') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + end_point_list = [{"type": "ord", + "url": row["ord_url"] + }, + {"type": "dashboard", + "url": row["horizon_url"], + }, + {"type": "identity", + "url": row["keystone_url"], + }] + data_manager.add_region(row["region_id"], + row["region_name"], + row["address_state"], + row["address_country"], + row["address_city"], + row["address_street"], + row["address_zip"], + row["region_status"], + row["ranger_agent_version"], + row["open_stack_version"], + row["location_type"], + row["vlcp_name"], + row["clli"], + row["design_type"], + end_point_list, + None, + row["description"]) + + except SQLDBError as e: + logger.error("SQL error raised {}".format(e.message)) + + +def main(): + db_url = config.database['url'] + data_manager = DataManager(db_url, 3, 3) + load_csv2db(data_manager) + +if __name__ == "__main__": + main() diff --git a/orm/services/region_manager/htmlcov/coverage_html.js b/orm/services/region_manager/htmlcov/coverage_html.js new file mode 100644 index 00000000..f6f5de20 --- /dev/null +++ b/orm/services/region_manager/htmlcov/coverage_html.js @@ -0,0 +1,584 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// Find all the elements with shortkey_* class, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + $("*[class*='shortkey_']").each(function (i, e) { + $.each($(e).attr("class").split(" "), function (i, c) { + if (/^shortkey_/.test(c)) { + $(document).bind('keydown', c.substr(9), function () { + $(e).click(); + }); + } + }); + }); +}; + +// Create the events for the help panel. +coverage.wire_up_help_panel = function () { + $("#keyboard_icon").click(function () { + // Show the help panel, and position it so the keyboard icon in the + // panel is in the same place as the keyboard icon in the header. + $(".help_panel").show(); + var koff = $("#keyboard_icon").offset(); + var poff = $("#panel_icon").position(); + $(".help_panel").offset({ + top: koff.top-poff.top, + left: koff.left-poff.left + }); + }); + $("#panel_icon").click(function () { + $(".help_panel").hide(); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + var table = $("table.index"); + var table_rows = table.find("tbody tr"); + var table_row_names = table_rows.find("td.name a"); + var no_rows = $("#no_rows"); + + // Create a duplicate table footer that we can modify with dynamic summed values. + var table_footer = $("table.index tfoot tr"); + var table_dynamic_footer = table_footer.clone(); + table_dynamic_footer.attr('class', 'total_dynamic hidden'); + table_footer.after(table_dynamic_footer); + + // Observe filter keyevents. + $("#filter").on("keyup change", $.debounce(150, function (event) { + var filter_value = $(this).val(); + + if (filter_value === "") { + // Filter box is empty, remove all filtering. + table_rows.removeClass("hidden"); + + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + + // Hide placeholder, show table. + if (no_rows.length > 0) { + no_rows.hide(); + } + table.show(); + + } + else { + // Filter table items by value. + var hidden = 0; + var shown = 0; + + // Hide / show elements. + $.each(table_row_names, function () { + var element = $(this).parents("tr"); + + if ($(this).text().indexOf(filter_value) === -1) { + // hide + element.addClass("hidden"); + hidden++; + } + else { + // show + element.removeClass("hidden"); + shown++; + } + }); + + // Show placeholder if no rows will be displayed. + if (no_rows.length > 0) { + if (shown === 0) { + // Show placeholder, hide table. + no_rows.show(); + table.hide(); + } + else { + // Hide placeholder, show table. + no_rows.hide(); + table.show(); + } + } + + // Manage dynamic header: + if (hidden > 0) { + // Calculate new dynamic sum values based on visible rows. + for (var column = 2; column < 20; column++) { + // Calculate summed value. + var cells = table_rows.find('td:nth-child(' + column + ')'); + if (!cells.length) { + // No more columns...! + break; + } + + var sum = 0, numer = 0, denom = 0; + $.each(cells.filter(':visible'), function () { + var ratio = $(this).data("ratio"); + if (ratio) { + var splitted = ratio.split(" "); + numer += parseInt(splitted[0], 10); + denom += parseInt(splitted[1], 10); + } + else { + sum += parseInt(this.innerHTML, 10); + } + }); + + // Get footer cell element. + var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); + + // Set value into dynamic footer cell element. + if (cells[0].innerHTML.indexOf('%') > -1) { + // Percentage columns use the numerator and denominator, + // and adapt to the number of decimal places. + var match = /\.([0-9]+)/.exec(cells[0].innerHTML); + var places = 0; + if (match) { + places = match[1].length; + } + var pct = numer * 100 / denom; + footer_cell.text(pct.toFixed(places) + '%'); + } + else { + footer_cell.text(sum); + } + } + + // Hide standard footer, show dynamic footer. + table_footer.addClass("hidden"); + table_dynamic_footer.removeClass("hidden"); + } + else { + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + $("#filter").trigger("change"); +}; + +// Loaded on index.html +coverage.index_ready = function ($) { + // Look for a cookie containing previous sort settings: + var sort_list = []; + var cookie_name = "COVERAGE_INDEX_SORT"; + var i; + + // This almost makes it worth installing the jQuery cookie plugin: + if (document.cookie.indexOf(cookie_name) > -1) { + var cookies = document.cookie.split(";"); + for (i = 0; i < cookies.length; i++) { + var parts = cookies[i].split("="); + + if ($.trim(parts[0]) === cookie_name && parts[1]) { + sort_list = eval("[[" + parts[1] + "]]"); + break; + } + } + } + + // Create a new widget which exists only to save and restore + // the sort order: + $.tablesorter.addWidget({ + id: "persistentSort", + + // Format is called by the widget before displaying: + format: function (table) { + if (table.config.sortList.length === 0 && sort_list.length > 0) { + // This table hasn't been sorted before - we'll use + // our stored settings: + $(table).trigger('sorton', [sort_list]); + } + else { + // This is not the first load - something has + // already defined sorting so we'll just update + // our stored value to match: + sort_list = table.config.sortList; + } + } + }); + + // Configure our tablesorter to handle the variable number of + // columns produced depending on report options: + var headers = []; + var col_count = $("table.index > thead > tr > th").length; + + headers[0] = { sorter: 'text' }; + for (i = 1; i < col_count-1; i++) { + headers[i] = { sorter: 'digit' }; + } + headers[col_count-1] = { sorter: 'percent' }; + + // Enable the table sorter: + $("table.index").tablesorter({ + widgets: ['persistentSort'], + headers: headers + }); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + coverage.wire_up_filter(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + }); +}; + +// -- pyfile stuff -- + +coverage.pyfile_ready = function ($) { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === 'n') { + $(frag).addClass('highlight'); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + $(document) + .bind('keydown', 'j', coverage.to_next_chunk_nicely) + .bind('keydown', 'k', coverage.to_prev_chunk_nicely) + .bind('keydown', '0', coverage.to_top) + .bind('keydown', '1', coverage.to_first_chunk) + ; + + $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); + $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); + $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); + $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + + coverage.init_scroll_markers(); + + // Rebuild scroll markers after window high changing + $(window).resize(coverage.resize_scroll_markers); +}; + +coverage.toggle_lines = function (btn, cls) { + btn = $(btn); + var hide = "hide_"+cls; + if (btn.hasClass(hide)) { + $("#source ."+cls).removeClass(hide); + btn.removeClass(hide); + } + else { + $("#source ."+cls).addClass(hide); + btn.addClass(hide); + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return $("#t" + n); +}; + +// Return the nth line number div. +coverage.num_elt = function (n) { + return $("#n" + n); +}; + +// Return the container of all the code. +coverage.code_container = function () { + return $(".linenos"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.is_transparent = function (color) { + // Different browsers return different colors for "none". + return color === "transparent" || color === "rgba(0, 0, 0, 0)"; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var color, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + if (!c.is_transparent(color)) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_color = color; + while (next_color === color) { + probe++; + probe_line = c.line_elt(probe); + next_color = probe_line.css("background-color"); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + var c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var color = probe_line.css("background-color"); + while (probe > 0 && c.is_transparent(color)) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_color = color; + while (prev_color === color) { + probe--; + probe_line = c.line_elt(probe); + prev_color = probe_line.css("background-color"); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Return the line number of the line nearest pixel position pos +coverage.line_at_pos = function (pos) { + var l1 = coverage.line_elt(1), + l2 = coverage.line_elt(2), + result; + if (l1.length && l2.length) { + var l1_top = l1.offset().top, + line_height = l2.offset().top - l1_top, + nlines = (pos - l1_top) / line_height; + if (nlines < 1) { + result = 1; + } + else { + result = Math.ceil(nlines); + } + } + else { + result = 1; + } + return result; +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + var top = coverage.line_elt(coverage.sel_begin); + var next = coverage.line_elt(coverage.sel_end-1); + + return ( + (top.isOnScreen() ? 1 : 0) + + (next.isOnScreen() ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: select the top line on + // the screen. + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (probe_line.length === 0) { + return; + } + var the_color = probe_line.css("background-color"); + if (!c.is_transparent(the_color)) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var color = the_color; + while (probe > 0 && color === the_color) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + color = probe_line.css("background-color"); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + color = the_color; + while (color === the_color) { + probe++; + probe_line = c.line_elt(probe); + color = probe_line.css("background-color"); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + c.code_container().find(".highlight").removeClass("highlight"); + for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { + c.num_elt(probe).addClass("highlight"); + } + + c.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + // Need to move the page. The html,body trick makes it scroll in all + // browsers, got it from http://stackoverflow.com/questions/3042651 + var top = coverage.line_elt(coverage.sel_begin); + var top_pos = parseInt(top.offset().top, 10); + coverage.scroll_window(top_pos - 30); + } +}; + +coverage.scroll_window = function (to_pos) { + $("html,body").animate({scrollTop: to_pos}, 200); +}; + +coverage.finish_scrolling = function () { + $("html,body").stop(true, true); +}; + +coverage.init_scroll_markers = function () { + var c = coverage; + // Init some variables + c.lines_len = $('td.text p').length; + c.body_h = $('body').height(); + c.header_h = $('div#header').height(); + c.missed_lines = $('td.text p.mis, td.text p.par'); + + // Build html + c.resize_scroll_markers(); +}; + +coverage.resize_scroll_markers = function () { + var c = coverage, + min_line_height = 3, + max_line_height = 10, + visible_window_h = $(window).height(); + + $('#scroll_marker').remove(); + // Don't build markers if the window has no scroll bar. + if (c.body_h <= visible_window_h) { + return; + } + + $("body").append("
 
"); + var scroll_marker = $('#scroll_marker'), + marker_scale = scroll_marker.height() / c.body_h, + line_height = scroll_marker.height() / c.lines_len; + + // Line height must be between the extremes. + if (line_height > min_line_height) { + if (line_height > max_line_height) { + line_height = max_line_height; + } + } + else { + line_height = min_line_height; + } + + var previous_line = -99, + last_mark, + last_top; + + c.missed_lines.each(function () { + var line_top = Math.round($(this).offset().top * marker_scale), + id_name = $(this).attr('id'), + line_number = parseInt(id_name.substring(1, id_name.length)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.css({ + 'height': line_top + line_height - last_top + }); + } + else { + // Add colored line in scroll_marker block. + scroll_marker.append('
'); + last_mark = $('#m' + line_number); + last_mark.css({ + 'height': line_height, + 'top': line_top + }); + last_top = line_top; + } + + previous_line = line_number; + }); +}; diff --git a/orm/services/region_manager/htmlcov/index.html b/orm/services/region_manager/htmlcov/index.html new file mode 100644 index 00000000..672002e7 --- /dev/null +++ b/orm/services/region_manager/htmlcov/index.html @@ -0,0 +1,455 @@ + + + + + + + + Coverage report + + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total1287160088%
rms/__init__.py000100%
rms/controllers/__init__.py000100%
rms/controllers/configuration.py1700100%
rms/controllers/lcp_controller.py5600100%
rms/controllers/logs.py362094%
rms/controllers/root.py121092%
rms/controllers/v2/__init__.py000100%
rms/controllers/v2/orm/__init__.py000100%
rms/controllers/v2/orm/resources/__init__.py000100%
rms/controllers/v2/orm/resources/groups.py12422082%
rms/controllers/v2/orm/resources/metadata.py8519078%
rms/controllers/v2/orm/resources/regions.py1875097%
rms/controllers/v2/orm/resources/status.py473094%
rms/controllers/v2/orm/root.py500100%
rms/controllers/v2/root.py300100%
rms/external_mock/__init__.py000100%
rms/external_mock/audit_client/__init__.py000100%
rms/external_mock/audit_client/api/__init__.py000100%
rms/external_mock/audit_client/api/audit.py4400%
rms/external_mock/keystone_utils/__init__.py000100%
rms/external_mock/keystone_utils/tokens.py51080%
rms/external_mock/orm_common/__init__.py000100%
rms/external_mock/orm_common/policy/__init__.py000100%
rms/external_mock/orm_common/policy/policy.py62067%
rms/external_mock/orm_common/utils/__init__.py000100%
rms/external_mock/orm_common/utils/api_error_utils.py21050%
rms/external_mock/orm_common/utils/utils.py61083%
rms/model/__init__.py300100%
rms/model/model.py1007093%
rms/model/url_parm.py634094%
rms/services/__init__.py000100%
rms/services/error_base.py182089%
rms/services/services.py17055068%
rms/storage/__init__.py000100%
rms/storage/base_data_manager.py2400100%
rms/storage/data_manager_factory.py1200100%
rms/storage/my_sql/__init__.py000100%
rms/storage/my_sql/data_manager.py27031089%
rms/utils/__init__.py000100%
rms/utils/authentication.py3200100%
+ +

+ No items found using the specified filter. +

+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/jquery.ba-throttle-debounce.min.js b/orm/services/region_manager/htmlcov/jquery.ba-throttle-debounce.min.js new file mode 100644 index 00000000..648fe5d3 --- /dev/null +++ b/orm/services/region_manager/htmlcov/jquery.ba-throttle-debounce.min.js @@ -0,0 +1,9 @@ +/* + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); diff --git a/orm/services/region_manager/htmlcov/jquery.hotkeys.js b/orm/services/region_manager/htmlcov/jquery.hotkeys.js new file mode 100644 index 00000000..09b21e03 --- /dev/null +++ b/orm/services/region_manager/htmlcov/jquery.hotkeys.js @@ -0,0 +1,99 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "); + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + event.target.type === "text") ) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); diff --git a/orm/services/region_manager/htmlcov/jquery.isonscreen.js b/orm/services/region_manager/htmlcov/jquery.isonscreen.js new file mode 100644 index 00000000..0182ebd2 --- /dev/null +++ b/orm/services/region_manager/htmlcov/jquery.isonscreen.js @@ -0,0 +1,53 @@ +/* Copyright (c) 2010 + * @author Laurence Wheway + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * @version 1.2.0 + */ +(function($) { + jQuery.extend({ + isOnScreen: function(box, container) { + //ensure numbers come in as intgers (not strings) and remove 'px' is it's there + for(var i in box){box[i] = parseFloat(box[i])}; + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( box.left+box.width-container.left > 0 && + box.left < container.width+container.left && + box.top+box.height-container.top > 0 && + box.top < container.height+container.top + ) return true; + return false; + } + }) + + + jQuery.fn.isOnScreen = function (container) { + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( $(this).offset().left+$(this).width()-container.left > 0 && + $(this).offset().left < container.width+container.left && + $(this).offset().top+$(this).height()-container.top > 0 && + $(this).offset().top < container.height+container.top + ) return true; + return false; + } +})(jQuery); diff --git a/orm/services/region_manager/htmlcov/jquery.min.js b/orm/services/region_manager/htmlcov/jquery.min.js new file mode 100644 index 00000000..e2efc335 --- /dev/null +++ b/orm/services/region_manager/htmlcov/jquery.min.js @@ -0,0 +1,9404 @@ +/*! + * jQuery JavaScript Library v1.7.2 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Fri Jul 5 14:07:58 UTC 2013 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.2", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, pass ) { + var exec, + bulk = key == null, + i = 0, + length = elems.length; + + // Sets many values + if ( key && typeof key === "object" ) { + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); + } + chainable = 1; + + // Sets one value + } else if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = pass === undefined && jQuery.isFunction( value ); + + if ( bulk ) { + // Bulk operations only iterate when executing function values + if ( exec ) { + exec = fn; + fn = function( elem, key, value ) { + return exec.call( jQuery( elem ), value ); + }; + + // Otherwise they run against the entire set + } else { + fn.call( elems, value ); + fn = null; + } + } + + if ( fn ) { + for (; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + } + + chainable = 1; + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + fired = true; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + pixelMargin: true + }; + + // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead + jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for ( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, + paddingMarginBorderVisibility, paddingMarginBorder, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + paddingMarginBorder = "padding:0;margin:0;border:"; + positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; + paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; + style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; + html = "
" + + "" + + "
"; + + container = document.createElement("div"); + container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
t
"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + div.innerHTML = ""; + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.innerHTML = ""; + div.style.width = div.style.padding = "1px"; + div.style.border = 0; + div.style.overflow = "hidden"; + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = "block"; + div.style.overflow = "visible"; + div.innerHTML = "
"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + } + + div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + if ( window.getComputedStyle ) { + div.style.marginTop = "1%"; + support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; + } + + if ( typeof container.style.zoom !== "undefined" ) { + container.style.zoom = 1; + } + + body.removeChild( container ); + marginDiv = div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, part, attr, name, l, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attr = elem.attributes; + for ( l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split( ".", 2 ); + parts[1] = parts[1] ? "." + parts[1] : ""; + part = parts[1] + "!"; + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + data = this.triggerHandler( "getData" + part, [ parts[0] ] ); + + // Try to fetch any internally stored data first + if ( data === undefined && elem ) { + data = jQuery.data( elem, key ); + data = dataAttr( elem, key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } + + parts[1] = value; + this.each(function() { + var self = jQuery( this ); + + self.triggerHandler( "setData" + part, parts ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + part, parts ); + }); + }, null, value, arguments.length > 1, null, false ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise( object ); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, isBool, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + isBool = rboolean.test( name ); + + // See #9699 for explanation of this approach (setting first, then removal) + // Do not do this for boolean attributes (see #10870) + if ( !isBool ) { + jQuery.attr( elem, name, "" ); + } + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( isBool && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true, + coords: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: selector && quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + special = jQuery.event.special[ event.type ] || {}, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers that should run if there are delegated events + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + + // Don't process events on disabled elements (#6911, #8165) + if ( cur.disabled !== true ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { // && selector != null + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} +// Expose origPOS +// "global" as in regardless of relation to brackets/parens +Expr.match.globalPOS = origPOS; + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.globalPOS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /]", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*", "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers___init___py.html b/orm/services/region_manager/htmlcov/rms_controllers___init___py.html new file mode 100644 index 00000000..e9a4bb7f --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/controllers/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_configuration_py.html b/orm/services/region_manager/htmlcov/rms_controllers_configuration_py.html new file mode 100644 index 00000000..5fd6bb6f --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_configuration_py.html @@ -0,0 +1,157 @@ + + + + + + + + + + + Coverage for rms/controllers/configuration.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+ +
+

"""Configuration rest API input module.""" 

+

 

+

import logging 

+

 

+

from orm_common.utils import utils 

+

 

+

from pecan import conf 

+

from pecan import request 

+

from pecan import rest 

+

from wsmeext.pecan import wsexpose 

+

 

+

from rms.utils import authentication 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class ConfigurationController(rest.RestController): 

+

"""Configuration controller.""" 

+

 

+

@wsexpose(str, str, status_code=200) 

+

def get(self, dump_to_log='false'): 

+

"""get method. 

+

 

+

:param dump_to_log: A boolean string that says whether the 

+

configuration should be written to log 

+

:return: A pretty string that contains the service's configuration 

+

""" 

+

logger.info("Get configuration...") 

+

authentication.authorize(request, 'configuration:get') 

+

 

+

dump = dump_to_log.lower() == 'true' 

+

utils.set_utils_conf(conf) 

+

result = utils.report_config(conf, dump, logger) 

+

return result 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_lcp_controller_py.html b/orm/services/region_manager/htmlcov/rms_controllers_lcp_controller_py.html new file mode 100644 index 00000000..90c29c78 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_lcp_controller_py.html @@ -0,0 +1,329 @@ + + + + + + + + + + + Coverage for rms/controllers/lcp_controller.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+ +
+

import logging 

+

 

+

from pecan import rest, request 

+

from pecan import conf 

+

 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

from rms.model import url_parm 

+

from rms.services.error_base import ErrorStatus 

+

from rms.services import services 

+

from rms.utils import authentication 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class LcpController(rest.RestController): 

+

 

+

@wsexpose(wtypes.text, rest_content_types='json') 

+

def get_all(self): 

+

""" 

+

This function is called when receiving /lcp without a parameter. 

+

parameter: 

+

None. 

+

return: entire list of lcp. 

+

""" 

+

logger.info('Received a GET request for all LCPs') 

+

authentication.authorize(request, 'lcp:get_all') 

+

 

+

zones = [] 

+

 

+

try: 

+

zones = get_zones() 

+

logger.debug('Returning LCP list: %s' % (zones,)) 

+

return zones 

+

 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(wtypes.text, str, rest_content_types='json') 

+

def get_one(self, lcp_id): 

+

 

+

logger.info('Received a GET request for LCP %s' % (id,)) 

+

authentication.authorize(request, 'lcp:get_one') 

+

 

+

zones = [] 

+

try: 

+

 

+

zones = get_zones() 

+

 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

for zone in zones: 

+

if zone["id"] == lcp_id: 

+

logger.debug('Returning: %s' % (zone,)) 

+

return zone 

+

 

+

error_msg = 'LCP %s not found' % (lcp_id,) 

+

logger.info(error_msg) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=error_msg, 

+

status_code=404) 

+

 

+

 

+

def get_zones(): 

+

""" 

+

This function returns the lcp list from CSV file. 

+

parameter: 

+

None. 

+

return: 

+

zone list in json format. 

+

""" 

+

logger.debug('Enter get_zones function') 

+

result = [] 

+

 

+

try: 

+

url_args = url_parm.UrlParms() 

+

zones = services.get_regions_data(url_args) 

+

 

+

for zone in zones.regions: 

+

result.append(build_zone_response(zone)) 

+

 

+

logger.debug("Available regions: {}".format(', '.join( 

+

[region["zone_name"] for region in result]))) 

+

 

+

except ErrorStatus as e: 

+

logger.debug(e.message) 

+

finally: 

+

return result 

+

 

+

 

+

def build_zone_response(zone): 

+

 

+

end_points_dict = {"identity": "", 

+

"dashboard": "", 

+

"ord": ""} 

+

for end_point in zone.endpoints: 

+

end_points_dict[end_point.type] = end_point.publicurl 

+

 

+

return dict( 

+

zone_name=zone.name, 

+

id=zone.id, 

+

status="1" if zone.status == "functional" else "0", 

+

design_type=zone.design_type, 

+

location_type=zone.location_type, 

+

vLCP_name=zone.vlcp_name, 

+

AIC_version=zone.ranger_agent_version, 

+

OS_version=zone.open_stack_version, 

+

keystone_EP=end_points_dict["identity"], 

+

horizon_EP=end_points_dict["dashboard"], 

+

ORD_EP=end_points_dict["ord"] 

+

) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_logs_py.html b/orm/services/region_manager/htmlcov/rms_controllers_logs_py.html new file mode 100644 index 00000000..51ddc2a1 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_logs_py.html @@ -0,0 +1,237 @@ + + + + + + + + + + + Coverage for rms/controllers/logs.py: 94% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+ +
+

import logging 

+

from pecan import rest, request 

+

import wsme 

+

from wsmeext.pecan import wsexpose 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

from rms.utils import authentication 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class LogChangeResultWSME(wsme.types.DynamicBase): 

+

"""log change result wsme type.""" 

+

 

+

result = wsme.wsattr(str, mandatory=True, default=None) 

+

 

+

def __init__(self, **kwargs): 

+

""""init method.""" 

+

super(LogChangeResult, self).__init__(**kwargs) 

+

 

+

 

+

class LogChangeResult(object): 

+

"""log change result type.""" 

+

 

+

def __init__(self, result): 

+

""""init method.""" 

+

self.result = result 

+

 

+

 

+

class LogsController(rest.RestController): 

+

"""Logs Audit controller.""" 

+

 

+

@wsexpose(LogChangeResultWSME, str, status_code=201, 

+

rest_content_types='json') 

+

def put(self, level): 

+

"""update log level. 

+

 

+

:param level: the log level text name 

+

:return: 

+

""" 

+

 

+

logger.info("Changing log level to [{}]".format(level)) 

+

authentication.authorize(request, 'log:update') 

+

 

+

try: 

+

log_level = logging._levelNames.get(level.upper()) 

+

if log_level is not None: 

+

self._change_log_level(log_level) 

+

result = "Log level changed to {}.".format(level) 

+

logger.info(result) 

+

else: 

+

raise Exception( 

+

"The given log level [{}] doesn't exist.".format(level)) 

+

 

+

return LogChangeResult(result) 

+

 

+

except Exception as exception: 

+

logger.error("Fail to change log_level. Reason: {}".format( 

+

exception.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@staticmethod 

+

def _change_log_level(log_level): 

+

path = __name__.split('.') 

+

if len(path) > 0: 

+

root = path[0] 

+

root_logger = logging.getLogger(root) 

+

root_logger.setLevel(log_level) 

+

else: 

+

logger.info("Fail to change log_level to [{}]. " 

+

"the given log level doesn't exist.".format(log_level)) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_root_py.html b/orm/services/region_manager/htmlcov/rms_controllers_root_py.html new file mode 100644 index 00000000..9a9cdf4b --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_root_py.html @@ -0,0 +1,159 @@ + + + + + + + + + + + Coverage for rms/controllers/root.py: 92% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+ +
+

from pecan import expose 

+

from lcp_controller import LcpController 

+

from logs import LogsController 

+

from configuration import ConfigurationController 

+

from rms.controllers.v2 import root 

+

 

+

 

+

class RootController(object): 

+

lcp = LcpController() 

+

logs = LogsController() 

+

configuration = ConfigurationController() 

+

v2 = root.V2Controller() 

+

 

+

@expose(template='json') 

+

def _default(self): 

+

""" 

+

Method to handle GET / 

+

parameters: None 

+

return: dict describing lcp rest version information 

+

""" 

+

return { 

+

"versions": { 

+

"values": [ 

+

{ 

+

"status": "stable", 

+

"id": "v2", 

+

"links": [ 

+

{ 

+

"href": "http://localhost:8789/" 

+

} 

+

] 

+

} 

+

] 

+

} 

+

} 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2___init___py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2___init___py.html new file mode 100644 index 00000000..bf5cfb53 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""orm package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_orm___init___py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm___init___py.html new file mode 100644 index 00000000..40a61780 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""resource package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources___init___py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources___init___py.html new file mode 100644 index 00000000..80509395 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""orm package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_groups_py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_groups_py.html new file mode 100644 index 00000000..76366806 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_groups_py.html @@ -0,0 +1,597 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/groups.py: 82% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+ +
+

"""rest module.""" 

+

import logging 

+

import time 

+

import wsme 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

from rms.services import error_base 

+

from rms.services import services as GroupService 

+

from rms.utils import authentication 

+

from pecan import rest, request 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

from rms.model import model as PythonModel 

+

 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class Groups(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

id = wsme.wsattr(wtypes.text, mandatory=True) 

+

name = wsme.wsattr(wtypes.text, mandatory=True) 

+

description = wsme.wsattr(wtypes.text, mandatory=True) 

+

regions = wsme.wsattr([str], mandatory=True) 

+

 

+

def __init__(self, id=None, name=None, description=None, regions=[]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.id = id 

+

self.name = name 

+

self.description = description 

+

self.regions = regions 

+

 

+

def _to_python_obj(self): 

+

obj = PythonModel.Groups() 

+

obj.id = self.id 

+

obj.name = self.name 

+

obj.description = self.description 

+

obj.regions = self.regions 

+

return obj 

+

 

+

 

+

class GroupWrapper(wtypes.DynamicBase): 

+

"""main cotain lis of groups.""" 

+

 

+

groups = wsme.wsattr([Groups], mandatory=True) 

+

 

+

def __init__(self, groups=[]): 

+

""" 

+

 

+

:param group: 

+

""" 

+

self.groups = groups 

+

 

+

 

+

class OutputResource(wtypes.DynamicBase): 

+

"""class method returned json body.""" 

+

 

+

id = wsme.wsattr(wtypes.text, mandatory=True) 

+

name = wsme.wsattr(wtypes.text, mandatory=True) 

+

created = wsme.wsattr(wtypes.text, mandatory=True) 

+

links = wsme.wsattr({str: str}, mandatory=True) 

+

 

+

def __init__(self, id=None, name=None, created=None, links={}): 

+

"""init function. 

+

 

+

:param id: 

+

:param created: 

+

:param links: 

+

""" 

+

self.id = id 

+

self.name = name 

+

self.created = created 

+

self.links = links 

+

 

+

 

+

class Result(wtypes.DynamicBase): 

+

"""class method json headers.""" 

+

 

+

group = wsme.wsattr(OutputResource, mandatory=True) 

+

 

+

def __init__(self, group=OutputResource()): 

+

"""init dunction. 

+

 

+

:param group: The created group 

+

""" 

+

self.group = group 

+

 

+

 

+

class GroupsController(rest.RestController): 

+

"""controller get resource.""" 

+

 

+

@wsexpose(Groups, str, status_code=200, 

+

rest_content_types='json') 

+

def get(self, id=None): 

+

"""Handle get request. 

+

 

+

:param id: Group ID 

+

:return: 200 OK on success, 404 Not Found otherwise. 

+

""" 

+

logger.info("Entered Get Group: id = {}".format(id)) 

+

authentication.authorize(request, 'group:get_one') 

+

 

+

try: 

+

 

+

result = GroupService.get_groups_data(id) 

+

logger.debug('Returning group, regions: {}'.format(result.regions)) 

+

return result 

+

 

+

except error_base.NotFoundError as e: 

+

logger.error("GroupsController - Group not found") 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=404) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(GroupWrapper, status_code=200, rest_content_types='json') 

+

def get_all(self): 

+

logger.info("gett all groups") 

+

authentication.authorize(request, 'group:get_all') 

+

try: 

+

 

+

logger.debug("api-get all groups") 

+

groups_wrraper = GroupService.get_all_groups() 

+

logger.debug("got groups {}".format(groups_wrraper)) 

+

 

+

except Exception as exp: 

+

logger.error("api--fail to get all groups") 

+

logger.exception(exp) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

return groups_wrraper 

+

 

+

@wsexpose(Result, body=Groups, status_code=201, rest_content_types='json') 

+

def post(self, group_input): 

+

"""Handle post request. 

+

 

+

:param group_input: json data 

+

:return: 201 created on success, 409 otherwise. 

+

""" 

+

logger.info("Entered Create Group") 

+

logger.debug("id = {}, name = {}, description = {}, regions = {}".format( 

+

group_input.id, 

+

group_input.name, 

+

group_input.description, 

+

group_input.regions)) 

+

authentication.authorize(request, 'group:create') 

+

 

+

try: 

+

# May raise an exception which will return status code 400 

+

GroupService.create_group_in_db(group_input.id, 

+

group_input.name, 

+

group_input.description, 

+

group_input.regions) 

+

logger.debug("Group created successfully in DB") 

+

 

+

# Create the group output data with the correct timestamp and link 

+

group = OutputResource(group_input.id, 

+

group_input.name, 

+

repr(int(time.time() * 1000)), 

+

{'self': '{}/v2/orm/groups/{}'.format( 

+

request.application_url, 

+

group_input.id)}) 

+

 

+

event_details = 'Region group {} {} created with regions: {}'.format( 

+

group_input.id, group_input.name, group_input.regions) 

+

utils.audit_trail('create group', request.transaction_id, 

+

request.headers, group_input.id, 

+

event_details=event_details) 

+

return Result(group) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error("GroupsController - {}".format(e.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(None, str, status_code=204, rest_content_types='json') 

+

def delete(self, group_id): 

+

logger.info("delete group") 

+

authentication.authorize(request, 'group:delete') 

+

 

+

try: 

+

 

+

logger.debug("delete group with id {}".format(group_id)) 

+

GroupService.delete_group(group_id) 

+

logger.debug("done") 

+

 

+

event_details = 'Region group {} deleted'.format(group_id) 

+

utils.audit_trail('delete group', request.transaction_id, 

+

request.headers, group_id, 

+

event_details=event_details) 

+

 

+

except Exception as exp: 

+

 

+

logger.exception("fail to delete group :- {}".format(exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exp.message) 

+

return 

+

 

+

@wsexpose(Result, str, body=Groups, status_code=201, 

+

rest_content_types='json') 

+

def put(self, group_id, group): 

+

logger.info("update group") 

+

authentication.authorize(request, 'group:update') 

+

 

+

try: 

+

logger.debug("update group - id {}".format(group_id)) 

+

result = GroupService.update_group(group, group_id) 

+

logger.debug("group updated to :- {}".format(result)) 

+

 

+

# build result 

+

group_result = OutputResource(result.id, result.name, 

+

repr(int(time.time() * 1000)), { 

+

'self': '{}/v2/orm/groups/{}'.format( 

+

request.application_url, 

+

result.id)}) 

+

 

+

event_details = 'Region group {} {} updated with regions: {}'.format( 

+

group_id, group.name, group.regions) 

+

utils.audit_trail('update group', request.transaction_id, 

+

request.headers, group_id, 

+

event_details=event_details) 

+

 

+

except error_base.ErrorStatus as exp: 

+

logger.error("group to update not found {}".format(exp)) 

+

logger.exception(exp) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

except Exception as exp: 

+

logger.error("fail to update groupt -- id {}".format(group_id)) 

+

logger.exception(exp) 

+

raise 

+

 

+

return Result(group_result) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_metadata_py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_metadata_py.html new file mode 100644 index 00000000..fcb8e1ed --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_metadata_py.html @@ -0,0 +1,437 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/metadata.py: 78% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+ +
+

import json 

+

 

+

import logging 

+

 

+

from pecan import rest, request 

+

import wsme 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

 

+

from rms.services import error_base 

+

from rms.services import services as RegionService 

+

 

+

from rms.utils import authentication 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class MetaData(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

metadata = wsme.wsattr({str: [str]}, mandatory=True) 

+

 

+

def __init__(self, metadata={}): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.metadata = metadata 

+

 

+

 

+

class RegionMetadataController(rest.RestController): 

+

 

+

@wsexpose(MetaData, str, status_code=200, rest_content_types='json') 

+

def get(self, region_id): 

+

logger.info("Get metadata for region id: {}".format(region_id)) 

+

authentication.authorize(request, 'metadata:get') 

+

 

+

try: 

+

region = RegionService.get_region_by_id_or_name(region_id) 

+

logger.debug("Got region metadata: {}".format(region.metadata)) 

+

return MetaData(region.metadata) 

+

 

+

except error_base.ErrorStatus as exp: 

+

logger.error("RegionsController - {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

except Exception as exp: 

+

logger.exception(exp.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exp.message) 

+

 

+

@wsexpose(MetaData, str, body=MetaData, status_code=201, 

+

rest_content_types='json') 

+

def post(self, region_id, metadata_input): 

+

"""Handle post request. 

+

:param region_id: region_id to add metadata to. 

+

:param metadata_input: json data 

+

:return: 201 created on success, 409 duplicate entry, 404 not found 

+

""" 

+

logger.info("Entered Create region metadata") 

+

logger.debug("Got metadata: {}".format(metadata_input)) 

+

authentication.authorize(request, 'metadata:create') 

+

 

+

try: 

+

self._validate_request_input() 

+

# May raise an exception which will return status code 400 

+

result = RegionService.add_region_metadata(region_id, 

+

metadata_input.metadata) 

+

logger.debug("Metadata was successfully added to " 

+

"region: {}. New metadata: {}".format(region_id, result)) 

+

 

+

event_details = 'Region {} metadata added'.format(region_id) 

+

utils.audit_trail('create metadata', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

return MetaData(result) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(MetaData, str, body=MetaData, status_code=201, 

+

rest_content_types='json') 

+

def put(self, region_id, metadata_input): 

+

"""Handle put request. 

+

:param region_id: region_id to update metadata to. 

+

:param metadata_input: json data 

+

:return: 201 created on success, 404 not found 

+

""" 

+

logger.info("Entered update region metadata") 

+

logger.debug("Got metadata: {}".format(metadata_input)) 

+

authentication.authorize(request, 'metadata:update') 

+

 

+

try: 

+

self._validate_request_input() 

+

# May raise an exception which will return status code 400 

+

result = RegionService.update_region_metadata(region_id, 

+

metadata_input.metadata) 

+

logger.debug("Metadata was successfully added to " 

+

"region: {}. New metadata: {}".format(region_id, result)) 

+

 

+

event_details = 'Region {} metadata updated'.format(region_id) 

+

utils.audit_trail('update metadata', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

return MetaData(result) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(None, str, str, status_code=204, rest_content_types='json') 

+

def delete(self, region_id, metadata_key): 

+

"""Handle delete request. 

+

:param region_id: region_id to update metadata to. 

+

:param metadata_key: metadata key to be deleted 

+

:return: 204 deleted 

+

""" 

+

logger.info("Entered delete region metadata with " 

+

"key: {}".format(metadata_key)) 

+

authentication.authorize(request, 'metadata:delete') 

+

 

+

try: 

+

# May raise an exception which will return status code 400 

+

result = RegionService.delete_metadata_from_region(region_id, 

+

metadata_key) 

+

logger.debug("Metadata was successfully deleted.") 

+

 

+

event_details = 'Region {} metadata {} deleted'.format( 

+

region_id, metadata_key) 

+

utils.audit_trail('delete metadata', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

def _validate_request_input(self): 

+

data_dict = json.loads(request.body) 

+

logger.debug("Got {}".format(data_dict)) 

+

for k, v in data_dict['metadata'].iteritems(): 

+

if isinstance(v, basestring): 

+

logger.error("Invalid json. value type list is expected, " 

+

"received string, for metadata key {}".format(k)) 

+

raise error_base.ErrorStatus(400, "Invalid json. Expecting list " 

+

"of metadata values, got string") 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_regions_py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_regions_py.html new file mode 100644 index 00000000..02014699 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_regions_py.html @@ -0,0 +1,819 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/regions.py: 97% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+ +
+

"""rest module.""" 

+

import logging 

+

 

+

from pecan import rest, request 

+

import wsme 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

 

+

from rms.model import url_parm 

+

from rms.model import model as PythonModel 

+

from rms.services import error_base 

+

from rms.services import services as RegionService 

+

 

+

from rms.controllers.v2.orm.resources.metadata import RegionMetadataController 

+

from rms.controllers.v2.orm.resources.status import RegionStatusController 

+

 

+

from rms.utils import authentication 

+

 

+

from orm_common.policy import policy 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class Address(wtypes.DynamicBase): 

+

"""wsme class for address json.""" 

+

 

+

country = wsme.wsattr(wtypes.text, mandatory=True) 

+

state = wsme.wsattr(wtypes.text, mandatory=True) 

+

city = wsme.wsattr(wtypes.text, mandatory=True) 

+

street = wsme.wsattr(wtypes.text, mandatory=True) 

+

zip = wsme.wsattr(wtypes.text, mandatory=True) 

+

 

+

def __init__(self, country=None, state=None, city=None, 

+

street=None, zip=None): 

+

""" 

+

 

+

:param country: 

+

:param state: 

+

:param city: 

+

:param street: 

+

:param zip: 

+

""" 

+

self.country = country 

+

self.state = state 

+

self.city = city 

+

self.street = street 

+

self.zip = zip 

+

 

+

def _to_clean_python_obj(self): 

+

obj = PythonModel.Address() 

+

obj.country = self.country 

+

obj.state = self.state 

+

obj.city = self.city 

+

obj.street = self.street 

+

obj.zip = self.zip 

+

return obj 

+

 

+

 

+

class EndPoint(wtypes.DynamicBase): 

+

"""class method endpoints body.""" 

+

 

+

publicurl = wsme.wsattr(wtypes.text, mandatory=True, name="publicURL") 

+

type = wsme.wsattr(wtypes.text, mandatory=True) 

+

 

+

def __init__(self, publicurl=None, type=None): 

+

"""init function. 

+

 

+

:param publicURL: field 

+

:param typee: field 

+

:return: 

+

""" 

+

self.type = type 

+

self.publicurl = publicurl 

+

 

+

def _to_clean_python_obj(self): 

+

obj = PythonModel.EndPoint() 

+

obj.publicurl = self.publicurl 

+

obj.type = self.type 

+

return obj 

+

 

+

 

+

class RegionsData(wtypes.DynamicBase): 

+

"""class method json header.""" 

+

 

+

status = wsme.wsattr(wtypes.text, mandatory=True) 

+

id = wsme.wsattr(wtypes.text, mandatory=True) 

+

name = wsme.wsattr(wtypes.text, mandatory=False) 

+

ranger_agent_version = wsme.wsattr(wtypes.text, mandatory=True, name="rangerAgentVersion") 

+

open_stack_version = wsme.wsattr(wtypes.text, mandatory=True, name="OSVersion") 

+

clli = wsme.wsattr(wtypes.text, mandatory=True, name="CLLI") 

+

metadata = wsme.wsattr({str: [str]}, mandatory=True) 

+

endpoints = wsme.wsattr([EndPoint], mandatory=True) 

+

address = wsme.wsattr(Address, mandatory=True) 

+

design_type = wsme.wsattr(wtypes.text, mandatory=True, name="designType") 

+

location_type = wsme.wsattr(wtypes.text, mandatory=True, name="locationType") 

+

vlcp_name = wsme.wsattr(wtypes.text, mandatory=True, name="vlcpName") 

+

is_ecomp = wsme.wsattr(bool, mandatory=False, name="is_ecomp", 

+

default=False) 

+

is_ssp = wsme.wsattr(bool, mandatory=False, name="is_ssp", 

+

default=False) 

+

purpose_of_region = wsme.wsattr(wtypes.text, mandatory=True, name="purpose_of_region") 

+

 

+

def __init__(self, status=None, id=None, name=None, clli=None, design_type=None, 

+

location_type=None, vlcp_name=None, open_stack_version=None, 

+

address=Address(), ranger_agent_version=None, metadata={}, 

+

endpoint=[EndPoint()], is_ecomp=False, is_ssp=False, 

+

purpose_of_region=None): 

+

""" 

+

 

+

:param status: 

+

:param id: 

+

:param name: 

+

:param clli: 

+

:param design_type: 

+

:param location_type: 

+

:param vlcp_name: 

+

:param open_stack_version: 

+

:param address: 

+

:param ranger_agent_version: 

+

:param metadata: 

+

:param endpoint: 

+

:param is_ecomp: 

+

:param is_ssp: 

+

:param purpose_of_region 

+

""" 

+

self.status = status 

+

self.id = id 

+

self.name = self.id 

+

self.clli = clli 

+

self.ranger_agent_version = ranger_agent_version 

+

self.metadata = metadata 

+

self.endpoint = endpoint 

+

self.design_type = design_type 

+

self.location_type = location_type 

+

self.vlcp_name = vlcp_name 

+

self.address = address 

+

self.open_stack_version = open_stack_version 

+

self.is_ecomp = is_ecomp 

+

self.is_ssp = is_ssp 

+

self.purpose_of_region = purpose_of_region 

+

 

+

def _to_clean_python_obj(self): 

+

obj = PythonModel.RegionData() 

+

obj.endpoints = [] 

+

obj.status = self.status 

+

obj.id = self.id 

+

obj.name = self.name 

+

obj.ranger_agent_version = self.ranger_agent_version 

+

obj.clli = self.clli 

+

obj.metadata = self.metadata 

+

for endpoint in self.endpoints: 

+

obj.endpoints.append(endpoint._to_clean_python_obj()) 

+

obj.address = self.address._to_clean_python_obj() 

+

obj.design_type = self.design_type 

+

obj.location_type = self.location_type 

+

obj.vlcp_name = self.vlcp_name 

+

obj.open_stack_version = self.open_stack_version 

+

obj.is_ecomp = self.is_ecomp 

+

obj.is_ssp = self.is_ssp 

+

obj.purpose_of_region = self.purpose_of_region 

+

return obj 

+

 

+

 

+

class Regions(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

regions = wsme.wsattr([RegionsData], mandatory=True) 

+

 

+

def __init__(self, regions=[RegionsData()]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.regions = regions 

+

 

+

 

+

class RegionsController(rest.RestController): 

+

"""controller get resource.""" 

+

metadata = RegionMetadataController() 

+

status = RegionStatusController() 

+

 

+

@wsexpose(Regions, str, str, [str], str, str, str, str, str, str, str, 

+

str, str, str, str, str, str, status_code=200, rest_content_types='json') 

+

def get_all(self, type=None, status=None, metadata=None, rangerAgentVersion=None, 

+

clli=None, regionname=None, osversion=None, valet=None, 

+

state=None, country=None, city=None, street=None, zip=None, 

+

is_ecomp=False, is_ssp=False, purpose_of_region=None): 

+

"""get regions. 

+

 

+

:param type: query field 

+

:param status: query field 

+

:param metadata: query field 

+

:param rangerAgentVersion: query field 

+

:param clli: query field 

+

:param regionname: query field 

+

:param osversion: query field 

+

:param valet: query field 

+

:param state: query field 

+

:param country: query field 

+

:param city: query field 

+

:param street: query field 

+

:param zip: query field 

+

:param is_ecomp: query field 

+

:param is_ssp: query field 

+

:param purpose_of_region: query field 

+

:return: json from db 

+

:exception: EntityNotFoundError 404 

+

""" 

+

logger.info("Entered Get Regions") 

+

authentication.authorize(request, 'region:get_all') 

+

 

+

url_args = {'type': type, 'status': status, 'metadata': metadata, 

+

'rangerAgentVersion': rangerAgentVersion, 'clli': clli, 'regionname': regionname, 

+

'osversion': osversion, 'valet': valet, 'state': state, 

+

'country': country, 'city': city, 'street': street, 'zip': zip, 

+

'is_ecomp': is_ecomp, 'is_ssp': is_ssp, 

+

'purpose_of_region': purpose_of_region} 

+

logger.debug("Parameters: {}".format(str(url_args))) 

+

 

+

try: 

+

url_args = url_parm.UrlParms(**url_args) 

+

 

+

result = RegionService.get_regions_data(url_args) 

+

 

+

logger.debug("Returning regions: {}".format(', '.join( 

+

[region.name for region in result.regions]))) 

+

 

+

return result 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error("RegionsController {}".format(e.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exception.message) 

+

 

+

@wsexpose(RegionsData, str, status_code=200, rest_content_types='json') 

+

def get_one(self, id_or_name): 

+

logger.info("API: Entered get region by id or name: {}".format(id_or_name)) 

+

authentication.authorize(request, 'region:get_one') 

+

 

+

try: 

+

result = RegionService.get_region_by_id_or_name(id_or_name) 

+

logger.debug("API: Got region {} success: {}".format(id_or_name, result)) 

+

except error_base.ErrorStatus as exp: 

+

logger.error("RegionsController {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

except Exception as exp: 

+

logger.exception(exp.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exp.message) 

+

 

+

return result 

+

 

+

@wsexpose(RegionsData, body=RegionsData, status_code=201, rest_content_types='json') 

+

def post(self, full_region_input): 

+

logger.info("API: CreateRegion") 

+

authentication.authorize(request, 'region:create') 

+

 

+

try: 

+

logger.debug("API: create region .. data = : {}".format(full_region_input)) 

+

result = RegionService.create_full_region(full_region_input) 

+

logger.debug("API: region created : {}".format(result)) 

+

 

+

event_details = 'Region {} {} created: AICversion {}, OSversion {}, CLLI {}'.format( 

+

full_region_input.name, full_region_input.design_type, 

+

full_region_input.ranger_agent_version, 

+

full_region_input.open_stack_version, full_region_input.clli) 

+

utils.audit_trail('create region', request.transaction_id, 

+

request.headers, full_region_input.id, 

+

event_details=event_details) 

+

except error_base.InputValueError as exp: 

+

logger.exception("Error in save region {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exp.status_code, 

+

message=exp.message) 

+

 

+

except error_base.ConflictError as exp: 

+

logger.exception("Conflict error {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=exp.message, 

+

status_code=exp.status_code) 

+

 

+

except Exception as exp: 

+

logger.exception("Error in creating region .. reason:- {}".format(exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exp.message) 

+

 

+

return result 

+

 

+

@wsexpose(None, str, rest_content_types='json', status_code=204) 

+

def delete(self, region_id): 

+

logger.info("Delete Region") 

+

authentication.authorize(request, 'region:delete') 

+

 

+

try: 

+

 

+

logger.debug("delete region {}".format(region_id)) 

+

result = RegionService.delete_region(region_id) 

+

logger.debug("region deleted") 

+

 

+

event_details = 'Region {} deleted'.format(region_id) 

+

utils.audit_trail('delete region', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

except Exception as exp: 

+

logger.exception( 

+

"error in deleting region .. reason:- {}".format(exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exp.message) 

+

return 

+

 

+

@wsexpose(RegionsData, str, body=RegionsData, status_code=201, 

+

rest_content_types='json') 

+

def put(self, region_id, region): 

+

logger.info("API: update region") 

+

authentication.authorize(request, 'region:update') 

+

 

+

try: 

+

 

+

logger.debug( 

+

"region to update {} with{}".format(region_id, region)) 

+

result = RegionService.update_region(region_id, region) 

+

logger.debug("API: region {} updated".format(region_id)) 

+

 

+

event_details = 'Region {} {} modified: AICversion {}, OSversion {}, CLLI {}'.format( 

+

region.name, region.design_type, region.ranger_agent_version, 

+

region.open_stack_version, region.clli) 

+

utils.audit_trail('update region', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

except error_base.NotFoundError as exp: 

+

logger.exception("region {} not found".format(region_id)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exp.status_code, 

+

message=exp.message) 

+

 

+

except error_base.InputValueError as exp: 

+

logger.exception("not valid input {}".format(exp.message)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=exp.status_code, 

+

message=exp.message) 

+

except Exception as exp: 

+

logger.exception( 

+

"API: error in updating region {}.. reason:- {}".format(region_id, 

+

exp)) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

message=exp.message) 

+

return result 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_status_py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_status_py.html new file mode 100644 index 00000000..f719798a --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_resources_status_py.html @@ -0,0 +1,299 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/resources/status.py: 94% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+ +
+

import logging 

+

 

+

import pecan 

+

from pecan import rest, request, conf 

+

 

+

import wsme 

+

from wsme import types as wtypes 

+

from wsmeext.pecan import wsexpose 

+

 

+

from orm_common.utils import api_error_utils as err_utils 

+

from orm_common.utils import utils 

+

 

+

from rms.services import error_base 

+

from rms.services import services as RegionService 

+

from rms.utils import authentication 

+

 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class RegionStatus(wtypes.DynamicBase): 

+

"""main json header.""" 

+

 

+

status = wsme.wsattr(str, mandatory=True) 

+

links = wsme.wsattr({str: str}, mandatory=False) 

+

 

+

def __init__(self, status=None, links=None): 

+

""" 

+

RegionStatus wrapper 

+

:param status: 

+

""" 

+

self.status = status 

+

self.links = links 

+

 

+

 

+

class RegionStatusController(rest.RestController): 

+

 

+

@wsexpose(RegionStatus, str, body=RegionStatus, status_code=201, 

+

rest_content_types='json') 

+

def put(self, region_id, new_status): 

+

""" 

+

Handle put request to modify region status 

+

:param region_id: 

+

:param new_status: 

+

:return: 200 for updated, 404 for region not found 

+

400 invalid status 

+

""" 

+

logger.info("Entered update region status") 

+

logger.debug("Got status: {}".format(new_status.status)) 

+

 

+

authentication.authorize(request, 'status:put') 

+

 

+

try: 

+

allowed_status = conf.region_options.allowed_status_values[:] 

+

 

+

if new_status.status not in allowed_status: 

+

logger.error("Invalid status. Region status " 

+

"must be one of {}".format(allowed_status)) 

+

raise error_base.InputValueError( 

+

message="Invalid status. Region status " 

+

"must be one of {}".format(allowed_status)) 

+

 

+

# May raise an exception which will return status code 400 

+

status = RegionService.update_region_status(region_id, new_status.status) 

+

base_link = 'https://{0}:{1}{2}'.format(conf.server.host, conf.server.port, 

+

pecan.request.path) 

+

link = {'self': base_link} 

+

 

+

logger.debug("Region status for region id {}, was successfully " 

+

"changed to: {}.".format(region_id, new_status.status)) 

+

 

+

event_details = 'Region {} status updated to {}'.format( 

+

region_id, new_status.status) 

+

utils.audit_trail('Update status', request.transaction_id, 

+

request.headers, region_id, 

+

event_details=event_details) 

+

 

+

return RegionStatus(status, link) 

+

 

+

except error_base.ErrorStatus as e: 

+

logger.error(e.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

message=e.message, 

+

status_code=e.status_code) 

+

except Exception as exception: 

+

logger.error(exception.message) 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=500, 

+

error_details=exception.message) 

+

 

+

@wsexpose(str, str, rest_content_types='json') 

+

def get(self, region_id): 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=405) 

+

 

+

@wsexpose(RegionStatus, str, body=RegionStatus, status_code=200, 

+

rest_content_types='json') 

+

def post(self, region_id, status): 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=405) 

+

 

+

@wsexpose(str, str, rest_content_types='json') 

+

def delete(self, region_id): 

+

raise err_utils.get_error(request.transaction_id, 

+

status_code=405) 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_root_py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_root_py.html new file mode 100644 index 00000000..df7c358c --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_orm_root_py.html @@ -0,0 +1,109 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/orm/root.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+ +
+

"""ORM controller module.""" 

+

from rms.controllers.v2.orm.resources import groups 

+

from rms.controllers.v2.orm.resources import regions 

+

 

+

 

+

class OrmController(object): 

+

"""ORM controller class.""" 

+

 

+

regions = regions.RegionsController() 

+

groups = groups.GroupsController() 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_controllers_v2_root_py.html b/orm/services/region_manager/htmlcov/rms_controllers_v2_root_py.html new file mode 100644 index 00000000..addd8179 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_controllers_v2_root_py.html @@ -0,0 +1,105 @@ + + + + + + + + + + + Coverage for rms/controllers/v2/root.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+ +
+

"""V2 root controller module.""" 

+

from rms.controllers.v2.orm import root 

+

 

+

 

+

class V2Controller(object): 

+

"""V2 root controller class.""" 

+

 

+

orm = root.OrmController() 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock___init___py.html b/orm/services/region_manager/htmlcov/rms_external_mock___init___py.html new file mode 100644 index 00000000..301a20c8 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_audit_client___init___py.html b/orm/services/region_manager/htmlcov/rms_external_mock_audit_client___init___py.html new file mode 100644 index 00000000..0ba1d066 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_audit_client___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/audit_client/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api___init___py.html b/orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api___init___py.html new file mode 100644 index 00000000..9415cc4a --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/audit_client/api/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api_audit_py.html b/orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api_audit_py.html new file mode 100644 index 00000000..92fe9996 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_audit_client_api_audit_py.html @@ -0,0 +1,101 @@ + + + + + + + + + + + Coverage for rms/external_mock/audit_client/api/audit.py: 0% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+ +
+

def audit(*args, **kwargs): 

+

pass 

+

 

+

 

+

def init(*args, **kwargs): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils___init___py.html b/orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils___init___py.html new file mode 100644 index 00000000..c91da3f1 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/keystone_utils/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils_tokens_py.html b/orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils_tokens_py.html new file mode 100644 index 00000000..b833593b --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_keystone_utils_tokens_py.html @@ -0,0 +1,103 @@ + + + + + + + + + + + Coverage for rms/external_mock/keystone_utils/tokens.py: 80% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+ +
+

def get_token_user(*a, **k): 

+

pass 

+

 

+

 

+

class TokenConf(object): 

+

def __init__(self, *a, **kw): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_orm_common___init___py.html b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common___init___py.html new file mode 100644 index 00000000..9d0575ed --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy___init___py.html b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy___init___py.html new file mode 100644 index 00000000..4367b302 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/policy/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy_policy_py.html b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy_policy_py.html new file mode 100644 index 00000000..972d1dcf --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_policy_policy_py.html @@ -0,0 +1,109 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/policy/policy.py: 67% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+ +
+

def init(*a, **kw): 

+

pass 

+

 

+

 

+

def enforce(*a, **kw): 

+

pass 

+

 

+

 

+

def authorize(*a, **kw): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils___init___py.html b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils___init___py.html new file mode 100644 index 00000000..e339ed1a --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/utils/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_api_error_utils_py.html b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_api_error_utils_py.html new file mode 100644 index 00000000..9979bae3 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_api_error_utils_py.html @@ -0,0 +1,93 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/utils/api_error_utils.py: 50% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+ +
+

def get_error(*args, **kwargs): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_utils_py.html b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_utils_py.html new file mode 100644 index 00000000..fa2be4b5 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_external_mock_orm_common_utils_utils_py.html @@ -0,0 +1,119 @@ + + + + + + + + + + + Coverage for rms/external_mock/orm_common/utils/utils.py: 83% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+ +
+

"""Utils module mock.""" 

+

 

+

 

+

def report_config(conf, dump=False): 

+

"""Mock report_config function.""" 

+

pass 

+

 

+

 

+

def set_utils_conf(conf): 

+

"""Mock set_utils_conf function.""" 

+

pass 

+

 

+

 

+

def audit_trail(*args, **kwargs): 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_model___init___py.html b/orm/services/region_manager/htmlcov/rms_model___init___py.html new file mode 100644 index 00000000..dd0a069f --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_model___init___py.html @@ -0,0 +1,119 @@ + + + + + + + + + + + Coverage for rms/model/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+ +
+

from pecan import conf # noqa 

+

 

+

 

+

def init_model(): 

+

""" 

+

This is a stub method which is called at application startup time. 

+

 

+

If you need to bind to a parsed database configuration, set up tables or 

+

ORM classes, or perform any database initialization, this is the 

+

recommended place to do it. 

+

 

+

For more information working with databases, and some common recipes, 

+

see http://pecan.readthedocs.org/en/latest/databases.html 

+

""" 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_model_model_py.html b/orm/services/region_manager/htmlcov/rms_model_model_py.html new file mode 100644 index 00000000..d4a4be2a --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_model_model_py.html @@ -0,0 +1,475 @@ + + + + + + + + + + + Coverage for rms/model/model.py: 93% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+ +
+

"""model module.""" 

+

from rms.services import error_base 

+

from pecan import conf 

+

 

+

 

+

class Address(object): 

+

"""address class.""" 

+

 

+

def __init__(self, country=None, state=None, city=None, 

+

street=None, zip=None): 

+

""" 

+

 

+

:param country: 

+

:param state: 

+

:param city: 

+

:param street: 

+

:param zip: 

+

""" 

+

self.country = country 

+

self.state = state 

+

self.city = city 

+

self.street = street 

+

self.zip = zip 

+

 

+

 

+

class EndPoint(object): 

+

"""class method endpoints body.""" 

+

 

+

def __init__(self, publicurl=None, type=None): 

+

"""init function. 

+

 

+

:param public_url: field 

+

:param type: field 

+

:return: 

+

""" 

+

self.type = type 

+

self.publicurl = publicurl 

+

 

+

 

+

class RegionData(object): 

+

"""class method json header.""" 

+

 

+

def __init__(self, status=None, id=None, name=None, clli=None, 

+

ranger_agent_version=None, design_type=None, location_type=None, 

+

vlcp_name=None, open_stack_version=None, 

+

address=Address(), is_ecomp=None, is_ssp=None, 

+

purpose_of_region=None, metadata={}, endpoints=[EndPoint()]): 

+

""" 

+

 

+

:param status: 

+

:param id: 

+

:param name: 

+

:param clli: 

+

:param ranger_agent_version: 

+

:param design_type: 

+

:param location_type: 

+

:param vlcp_name: 

+

:param open_stack_version: 

+

:param address: 

+

:param is_ecomp 

+

:param is_ssp 

+

:param purpose_of_region 

+

:param metadata: 

+

:param endpoints: 

+

""" 

+

self.status = status 

+

self.id = id 

+

# make id and name always the same 

+

self.name = self.id 

+

self.clli = clli 

+

self.ranger_agent_version = ranger_agent_version 

+

self.metadata = metadata 

+

self.endpoints = endpoints 

+

self.design_type = design_type 

+

self.location_type = location_type 

+

self.vlcp_name = vlcp_name 

+

self.open_stack_version = open_stack_version 

+

self.address = address 

+

self.is_ecomp = is_ecomp 

+

self.is_ssp = is_ssp 

+

self.purpose_of_region = purpose_of_region 

+

 

+

def _validate_end_points(self, endpoints_types_must_have): 

+

ep_duplicate = [] 

+

for endpoint in self.endpoints: 

+

if endpoint.type not in ep_duplicate: 

+

ep_duplicate.append(endpoint.type) 

+

else: 

+

raise error_base.InputValueError( 

+

message="Invalid endpoints. Duplicate endpoint " 

+

"type {}".format(endpoint.type)) 

+

try: 

+

endpoints_types_must_have.remove(endpoint.type) 

+

except: 

+

pass 

+

if len(endpoints_types_must_have) > 0: 

+

raise error_base.InputValueError( 

+

message="Invalid endpoints. Endpoint type '{}' " 

+

"is missing".format(endpoints_types_must_have)) 

+

 

+

def _validate_status(self, allowed_status): 

+

if self.status not in allowed_status: 

+

raise error_base.InputValueError( 

+

message="Invalid status. Region status must be " 

+

"one of {}".format(allowed_status)) 

+

return 

+

 

+

def _validate_model(self): 

+

allowed_status = conf.region_options.allowed_status_values[:] 

+

endpoints_types_must_have = conf.region_options.endpoints_types_must_have[:] 

+

self._validate_status(allowed_status) 

+

self._validate_end_points(endpoints_types_must_have) 

+

return 

+

 

+

def _to_db_model_dict(self): 

+

end_points = [] 

+

 

+

for endpoint in self.endpoints: 

+

ep = {} 

+

ep['type'] = endpoint.type 

+

ep['url'] = endpoint.publicurl 

+

end_points.append(ep) 

+

 

+

db_model_dict = {} 

+

db_model_dict['region_id'] = self.id 

+

db_model_dict['name'] = self.name 

+

db_model_dict['address_state'] = self.address.state 

+

db_model_dict['address_country'] = self.address.country 

+

db_model_dict['address_city'] = self.address.city 

+

db_model_dict['address_street'] = self.address.street 

+

db_model_dict['address_zip'] = self.address.zip 

+

db_model_dict['region_status'] = self.status 

+

db_model_dict['ranger_agent_version'] = self.ranger_agent_version 

+

db_model_dict['open_stack_version'] = self.open_stack_version 

+

db_model_dict['design_type'] = self.design_type 

+

db_model_dict['location_type'] = self.location_type 

+

db_model_dict['vlcp_name'] = self.location_type 

+

db_model_dict['clli'] = self.clli 

+

db_model_dict['is_ecomp'] = self.is_ecomp * 1 

+

db_model_dict['is_ssp'] = self.is_ssp * 1 

+

db_model_dict['purpose_of_region'] = self.purpose_of_region 

+

db_model_dict['end_point_list'] = end_points 

+

db_model_dict['meta_data_dict'] = self.metadata 

+

return db_model_dict 

+

 

+

 

+

class Regions(object): 

+

"""main json header.""" 

+

 

+

def __init__(self, regions=[RegionData()]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.regions = regions 

+

 

+

 

+

class Groups(object): 

+

"""main json header.""" 

+

 

+

def __init__(self, id=None, name=None, 

+

description=None, regions=[]): 

+

"""init function. 

+

 

+

:param regions: 

+

:return: 

+

""" 

+

self.id = id 

+

self.name = name 

+

self.description = description 

+

self.regions = regions 

+

 

+

def _to_db_model_dict(self): 

+

db_dict = {} 

+

db_dict['group_name'] = self.name 

+

db_dict['group_description'] = self.description 

+

db_dict['group_regions'] = self.regions 

+

return db_dict 

+

 

+

 

+

class GroupsWrraper(object): 

+

"""list of groups.""" 

+

 

+

def __init__(self, groups=None): 

+

""" 

+

 

+

:param groups: 

+

""" 

+

if groups is None: 

+

self.groups = [] 

+

else: 

+

self.groups = groups 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_model_url_parm_py.html b/orm/services/region_manager/htmlcov/rms_model_url_parm_py.html new file mode 100644 index 00000000..c8234f07 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_model_url_parm_py.html @@ -0,0 +1,307 @@ + + + + + + + + + + + Coverage for rms/model/url_parm.py: 94% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+ +
+

"""module.""" 

+

 

+

 

+

class UrlParms(object): 

+

"""class method.""" 

+

 

+

def __init__(self, type=None, status=None, metadata=None, rangerAgentVersion=None, 

+

clli=None, regionname=None, osversion=None, valet=None, 

+

state=None, country=None, city=None, street=None, zip=None, 

+

is_ecomp=None, is_ssp=None, purpose_of_region=None): 

+

"""init method. 

+

 

+

:param type: 

+

:param status: 

+

:param metadata: 

+

:param rangerAgentVersion: 

+

:param clli: 

+

:param regionname: 

+

:param osversion: 

+

:param valet: 

+

:param state: 

+

:param country: 

+

:param city: 

+

:param street: 

+

:param zip: 

+

""" 

+

if type: 

+

self.location_type = type 

+

if status: 

+

self.region_status = status 

+

if metadata: 

+

self.metadata = metadata 

+

if rangerAgentVersion: 

+

self.ranger_agent_version = rangerAgentVersion 

+

if clli: 

+

self.clli = clli 

+

if regionname: 

+

self.name = regionname 

+

if osversion: 

+

self.open_stack_version = osversion 

+

if valet: 

+

self.valet = valet 

+

if state: 

+

self.address_state = state 

+

if country: 

+

self.address_country = country 

+

if city: 

+

self.address_city = city 

+

if street: 

+

self.address_street = street 

+

if zip: 

+

self.address_zip = zip 

+

if is_ecomp: 

+

self.is_ecomp = is_ecomp 

+

if is_ssp: 

+

self.is_ssp = is_ssp 

+

if purpose_of_region: 

+

self.purpose_of_region 

+

 

+

def _build_query(self): 

+

"""nuild db query. 

+

 

+

:return: 

+

""" 

+

metadatadict = None 

+

regiondict = None 

+

if self.__dict__: 

+

metadatadict = self._build_metadata_dict() 

+

regiondict = self._build_region_dict() 

+

return regiondict, metadatadict, None 

+

 

+

def _build_metadata_dict(self): 

+

"""meta_data dict. 

+

 

+

:return: metadata dict 

+

""" 

+

metadata = None 

+

if 'metadata' in self.__dict__: 

+

metadata = {'ref_keys': [], 'meta_data_pairs': [], 

+

'meta_data_keys': []} 

+

for metadata_item in self.metadata: 

+

if ':' in metadata_item: 

+

key = metadata_item.split(':')[0] 

+

metadata['ref_keys'].append(key) 

+

metadata['meta_data_pairs'].\ 

+

append({'metadata_key': key, 

+

'metadata_value': metadata_item.split(':')[1]}) 

+

else: 

+

metadata['meta_data_keys'].append(metadata_item) 

+

# Now clean irrelevant values 

+

keys_list = [] 

+

for item in metadata['meta_data_keys']: 

+

if item not in metadata['ref_keys']: 

+

keys_list.append(item) 

+

 

+

metadata['meta_data_keys'] = keys_list 

+

 

+

return metadata 

+

 

+

def _build_region_dict(self): 

+

"""region dict. 

+

 

+

:return:regin dict 

+

""" 

+

regiondict = {} 

+

for key, value in self.__dict__.items(): 

+

if key != 'metadata': 

+

regiondict[key] = value 

+

return regiondict 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_services___init___py.html b/orm/services/region_manager/htmlcov/rms_services___init___py.html new file mode 100644 index 00000000..430566d0 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_services___init___py.html @@ -0,0 +1,91 @@ + + + + + + + + + + + Coverage for rms/services/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

"""services package.""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_services_error_base_py.html b/orm/services/region_manager/htmlcov/rms_services_error_base_py.html new file mode 100644 index 00000000..3488d3a4 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_services_error_base_py.html @@ -0,0 +1,155 @@ + + + + + + + + + + + Coverage for rms/services/error_base.py: 89% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+ +
+

"""Exceptions module.""" 

+

 

+

 

+

class Error(Exception): 

+

pass 

+

 

+

 

+

class ErrorStatus(Error): 

+

 

+

def __init__(self, status_code, message=""): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class NotFoundError(ErrorStatus): 

+

 

+

def __init__(self, status_code=404, message="Not found"): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class ConflictError(ErrorStatus): 

+

 

+

def __init__(self, status_code=409, message="Conflict error"): 

+

self.status_code = status_code 

+

self.message = message 

+

 

+

 

+

class InputValueError(ErrorStatus): 

+

 

+

def __init__(self, status_code=400, message="value not allowed"): 

+

self.status_code = status_code 

+

self.message = message 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_services_services_py.html b/orm/services/region_manager/htmlcov/rms_services_services_py.html new file mode 100644 index 00000000..0192f62f --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_services_services_py.html @@ -0,0 +1,661 @@ + + + + + + + + + + + Coverage for rms/services/services.py: 68% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+ +
+

"""DB actions wrapper module.""" 

+

import logging 

+

from rms.model.model import Groups 

+

from rms.model.model import Regions 

+

from rms.services import error_base 

+

from rms.storage import base_data_manager 

+

from rms.storage import data_manager_factory 

+

 

+

LOG = logging.getLogger(__name__) 

+

 

+

 

+

def get_regions_data(url_parms): 

+

"""get region from db. 

+

 

+

:param url_parms: the parameters got in the url to make the query 

+

:return: region model for json output 

+

:raise: NoContentError( status code 404) 

+

""" 

+

region_dict, metadata_dict, end_point = url_parms._build_query() 

+

db = data_manager_factory.get_data_manager() 

+

regions = db.get_regions(region_dict, metadata_dict, end_point) 

+

if not regions: 

+

raise error_base.NotFoundError(message="No regions found for the given search parameters") 

+

return Regions(regions) 

+

 

+

 

+

def get_region_by_id_or_name(region_id_or_name): 

+

""" 

+

 

+

:param region_id_or_name: 

+

:return: region object (wsme format) 

+

""" 

+

LOG.debug("LOGIC:- get region data by id or name {}".format(region_id_or_name)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

region = db.get_region_by_id_or_name(region_id_or_name) 

+

 

+

if not region: 

+

raise error_base.NotFoundError(message="Region {} not found".format(region_id_or_name)) 

+

 

+

except Exception as exp: 

+

LOG.exception("error in get region by id/name") 

+

raise 

+

 

+

return region 

+

 

+

 

+

def update_region(region_id, region): 

+

""" 

+

:param region: 

+

:return: 

+

""" 

+

LOG.debug("logic:- update region {}".format(region)) 

+

try: 

+

 

+

region = region._to_clean_python_obj() 

+

region._validate_model() 

+

region_dict = region._to_db_model_dict() 

+

 

+

db = data_manager_factory.get_data_manager() 

+

db.update_region(region_to_update=region_id, **region_dict) 

+

LOG.debug("region {} updated".format(region_id)) 

+

result = get_region_by_id_or_name(region_id) 

+

 

+

except error_base.NotFoundError as exp: 

+

LOG.exception("fail to update region {}".format(exp.message)) 

+

raise 

+

except Exception as exp: 

+

LOG.exception("fail to update region {}".format(exp)) 

+

raise 

+

return result 

+

 

+

 

+

def delete_region(region_id): 

+

""" 

+

 

+

:param region_id: 

+

:return: 

+

""" 

+

LOG.debug("logic:- delete region {}".format(region_id)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

db.delete_region(region_id) 

+

LOG.debug("region deleted") 

+

except Exception as exp: 

+

LOG.exception("fail to delete region {}".format(exp)) 

+

raise 

+

return 

+

 

+

 

+

def create_full_region(full_region): 

+

"""create region logic. 

+

 

+

:param full_region obj: 

+

:return: 

+

:raise: input value error(status code 400) 

+

""" 

+

LOG.debug("logic:- save region ") 

+

try: 

+

 

+

full_region = full_region._to_clean_python_obj() 

+

full_region._validate_model() 

+

 

+

full_region_db_dict = full_region._to_db_model_dict() 

+

LOG.debug("region to save {}".format(full_region_db_dict)) 

+

db = data_manager_factory.get_data_manager() 

+

db.add_region(**full_region_db_dict) 

+

LOG.debug("region added") 

+

result = get_region_by_id_or_name(full_region.id) 

+

 

+

except error_base.InputValueError as exp: 

+

LOG.exception("error in save region {}".format(exp.message)) 

+

raise 

+

except base_data_manager.DuplicateEntryError as exp: 

+

LOG.exception("error in save region {}".format(exp.message)) 

+

raise error_base.ConflictError(message=exp.message) 

+

except Exception as exp: 

+

LOG.exception("error in save region {}".format(exp.message)) 

+

raise 

+

 

+

return result 

+

 

+

 

+

def add_region_metadata(region_id, metadata_dict): 

+

LOG.debug("Add metadata: {} to region id : {}".format(metadata_dict, 

+

region_id)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

result = db.add_meta_data_to_region(region_id, metadata_dict) 

+

if not result: 

+

raise error_base.NotFoundError(message="Region {} not found".format(region_id)) 

+

else: 

+

return result.metadata 

+

 

+

except Exception as exp: 

+

LOG.exception("Error getting metadata for region id:".format(region_id)) 

+

raise 

+

 

+

 

+

def update_region_metadata(region_id, metadata_dict): 

+

LOG.debug("Update metadata to region id : {}. " 

+

"New metadata: {}".format(region_id, metadata_dict)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

result = db.update_region_meta_data(region_id, metadata_dict) 

+

if not result: 

+

raise error_base.NotFoundError(message="Region {} not " 

+

"found".format(region_id)) 

+

else: 

+

return result.metadata 

+

 

+

except Exception as exp: 

+

LOG.exception("Error getting metadata for region id:".format(region_id)) 

+

raise 

+

 

+

 

+

def delete_metadata_from_region(region_id, metadata_key): 

+

LOG.info("Delete metadata key: {} from region id : {}." 

+

.format(metadata_key, region_id)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

db.delete_region_metadata(region_id, metadata_key) 

+

 

+

except Exception as exp: 

+

LOG.exception("Error getting metadata for region id:".format(region_id)) 

+

raise 

+

 

+

 

+

def get_groups_data(name): 

+

"""get group from db. 

+

 

+

:param name: groupe name 

+

:return: groupe object with its regions 

+

:raise: NoContentError( status code 404) 

+

""" 

+

db = data_manager_factory.get_data_manager() 

+

groups = db.get_group(name) 

+

if not groups: 

+

raise error_base.NotFoundError(message="Group {} not found".format(name)) 

+

return Groups(**groups) 

+

 

+

 

+

def get_all_groups(): 

+

""" 

+

 

+

:return: 

+

""" 

+

try: 

+

LOG.debug("logic - get all groups") 

+

db = data_manager_factory.get_data_manager() 

+

all_groups = db.get_all_groups() 

+

LOG.debug("logic - got all groups {}".format(all_groups)) 

+

 

+

except Exception as exp: 

+

LOG.error("fail to get all groups") 

+

LOG.exception(exp) 

+

raise 

+

 

+

return all_groups 

+

 

+

 

+

def delete_group(group_id): 

+

""" 

+

 

+

:param group_id: 

+

:return: 

+

""" 

+

LOG.debug("delete group logic") 

+

try: 

+

 

+

db = data_manager_factory.get_data_manager() 

+

LOG.debug("delete group id {} from db".format(group_id)) 

+

db.delete_group(group_id) 

+

 

+

except Exception as exp: 

+

LOG.exception(exp) 

+

raise 

+

return 

+

 

+

 

+

def create_group_in_db(group_id, group_name, description, regions): 

+

"""Create a region group in the database. 

+

 

+

:param group_id: The ID of the group to create 

+

:param group_name: The name of the group to create 

+

:param description: The group description 

+

:param regions: A list of regions inside the group 

+

:raise: GroupExistsError (status code 400) if the group already exists 

+

""" 

+

try: 

+

manager = data_manager_factory.get_data_manager() 

+

manager.add_group(group_id, group_name, description, regions) 

+

except error_base.ConflictError: 

+

LOG.exception("Group {} already exists".format(group_id)) 

+

raise error_base.ConflictError( 

+

message="Group {} already exists".format(group_id)) 

+

except error_base.InputValueError: 

+

LOG.exception("Some of the regions not found") 

+

raise error_base.NotFoundError( 

+

message="Some of the regions not found") 

+

 

+

 

+

def update_group(group, group_id): 

+

result = None 

+

LOG.debug("update group logic") 

+

try: 

+

group = group._to_python_obj() 

+

db_manager = data_manager_factory.get_data_manager() 

+

LOG.debug("update group to {}".format(group._to_db_model_dict())) 

+

db_manager.update_group(group_id=group_id, **group._to_db_model_dict()) 

+

LOG.debug("group updated") 

+

# make sure it updated 

+

groups = db_manager.get_group(group_id) 

+

 

+

except error_base.NotFoundError: 

+

LOG.error("Group {} not found") 

+

raise 

+

except error_base.InputValueError: 

+

LOG.exception("Some of the regions not found") 

+

raise error_base.NotFoundError( 

+

message="Some of the regions not found") 

+

except Exception as exp: 

+

LOG.error("Failed to update group {}".format(group.group_id)) 

+

LOG.exception(exp) 

+

raise 

+

 

+

return Groups(**groups) 

+

 

+

 

+

def update_region_status(region_id, new_status): 

+

"""Update region. 

+

 

+

:param region_id: 

+

:param new_status: 

+

:return: 

+

""" 

+

LOG.debug("Update region id: {} status to: {}".format(region_id, 

+

new_status)) 

+

try: 

+

db = data_manager_factory.get_data_manager() 

+

result = db.update_region_status(region_id, new_status) 

+

return result 

+

 

+

except Exception as exp: 

+

LOG.exception("Error updating status for region id:".format(region_id)) 

+

raise 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_storage___init___py.html b/orm/services/region_manager/htmlcov/rms_storage___init___py.html new file mode 100644 index 00000000..b9e43969 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_storage___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/storage/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_storage_base_data_manager_py.html b/orm/services/region_manager/htmlcov/rms_storage_base_data_manager_py.html new file mode 100644 index 00000000..c3b0d045 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_storage_base_data_manager_py.html @@ -0,0 +1,331 @@ + + + + + + + + + + + Coverage for rms/storage/base_data_manager.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+ +
+

 

+

class BaseDataManager(object): 

+

 

+

def __init__(self, url, 

+

max_retries, 

+

retry_interval): 

+

pass 

+

 

+

def add_region(self, 

+

region_id, 

+

name, 

+

address_state, 

+

address_country, 

+

address_city, 

+

address_street, 

+

address_zip, 

+

region_status, 

+

ranger_agent_version, 

+

open_stack_version, 

+

design_type, 

+

location_type, 

+

vlcp_name, 

+

clli, 

+

description, 

+

meta_data_list, 

+

end_point_list, 

+

is_ecomp, 

+

is_ssp, 

+

purpose_of_region): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def delete_region(self, 

+

region_id): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

def get_regions(self, 

+

region_filters_dict, 

+

meta_data_dict, 

+

end_point_dict): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def get_all_regions(self): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def add_meta_data_to_region(self, 

+

region_id, 

+

key, 

+

value, 

+

description): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def remove_meta_data_from_region(self, 

+

region_id, 

+

key): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def add_end_point_to_region(self, 

+

region_id, 

+

end_point_type, 

+

end_point_url, 

+

description): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def remove_end_point_from_region(self, 

+

region_id, 

+

end_point_type): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

def add_group(self, 

+

group_id, 

+

group_name, 

+

group_description, 

+

region_ids_list): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def delete_group(self, 

+

group_name): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

def get_group(self, group_id): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def get_all_groups(self): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

""" 

+

def add_region_to_group(self, 

+

group_id, 

+

region_id): 

+

raise NotImplementedError("Please Implement this method") 

+

 

+

def remove_region_from_group(self, 

+

group_id, 

+

region_id): 

+

raise NotImplementedError("Please Implement this method") 

+

""" 

+

 

+

 

+

class SQLDBError(Exception): 

+

pass 

+

 

+

 

+

class EntityNotFound(Exception): 

+

"""if item not found in DB.""" 

+

pass 

+

 

+

 

+

class DuplicateEntryError(Exception): 

+

"""A group already exists.""" 

+

pass 

+

 

+

 

+

class InputValueError(Exception): 

+

""" unvalid input from user""" 

+

pass 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_storage_data_manager_factory_py.html b/orm/services/region_manager/htmlcov/rms_storage_data_manager_factory_py.html new file mode 100644 index 00000000..87d1ef0d --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_storage_data_manager_factory_py.html @@ -0,0 +1,129 @@ + + + + + + + + + + + Coverage for rms/storage/data_manager_factory.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+ +
+

import logging 

+

 

+

from pecan import conf 

+

 

+

from rms.storage.my_sql.data_manager import DataManager 

+

 

+

LOG = logging.getLogger(__name__) 

+

 

+

 

+

def get_data_manager(): 

+

try: 

+

dm = DataManager(url=conf.database.url, 

+

max_retries=conf.database.max_retries, 

+

retries_interval=conf.database.retries_interval) 

+

return dm 

+

except Exception: 

+

nagios_message = "CRITICAL|CONDB001 - Could not establish " \ 

+

"database connection" 

+

LOG.error(nagios_message) 

+

raise Exception("Could not establish database connection") 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_storage_my_sql___init___py.html b/orm/services/region_manager/htmlcov/rms_storage_my_sql___init___py.html new file mode 100644 index 00000000..a6e95539 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_storage_my_sql___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/storage/my_sql/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_storage_my_sql_data_manager_py.html b/orm/services/region_manager/htmlcov/rms_storage_my_sql_data_manager_py.html new file mode 100644 index 00000000..589a433b --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_storage_my_sql_data_manager_py.html @@ -0,0 +1,1147 @@ + + + + + + + + + + + Coverage for rms/storage/my_sql/data_manager.py: 89% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+

500

+

501

+

502

+

503

+

504

+

505

+

506

+

507

+

508

+

509

+

510

+

511

+

512

+

513

+

514

+

515

+

516

+

517

+

518

+

519

+

520

+

521

+

522

+

523

+

524

+

525

+

526

+

527

+

528

+

529

+ +
+

import logging 

+

 

+

import oslo_db 

+

from oslo_db.sqlalchemy import session as db_session 

+

from sqlalchemy.ext.declarative.api import declarative_base 

+

from sqlalchemy.sql import or_ 

+

 

+

from rms.services import error_base as ServiceBase 

+

from data_models import Region, RegionEndPoint, Group 

+

from data_models import RegionMetaData, GroupRegion 

+

from rms.services import error_base 

+

from rms.storage.base_data_manager import BaseDataManager, DuplicateEntryError, EntityNotFound 

+

from rms.model import model as PythonModels 

+

 

+

Base = declarative_base() 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

class DataManager(BaseDataManager): 

+

 

+

def __init__(self, url, max_retries, retries_interval): 

+

self._engine_facade = db_session.EngineFacade(url, 

+

max_retries=max_retries, 

+

retry_interval=retries_interval) 

+

 

+

def add_region(self, 

+

region_id, 

+

name, 

+

address_state, 

+

address_country, 

+

address_city, 

+

address_street, 

+

address_zip, 

+

region_status, 

+

ranger_agent_version, 

+

open_stack_version, 

+

design_type, 

+

location_type, 

+

vlcp_name, 

+

clli, 

+

# a list of dictionaries of format 

+

# {"type":"", "url":"", "description":"" 

+

end_point_list, 

+

# a dictionary of key,value pairs 

+

# {"key":"value", } 

+

meta_data_dict, 

+

is_ecomp, 

+

is_ssp, 

+

purpose_of_region="", 

+

description=""): 

+

""" add a new region to the `region` table 

+

add also the regions give meta_data and end_points to the `region_end_point` and 

+

`region_meta_data` tables if given. 

+

handle duplicate errors if raised""" 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

region = Region(region_id=region_id, 

+

name=name, 

+

address_state=address_state, 

+

address_country=address_country, 

+

address_city=address_city, 

+

address_street=address_street, 

+

address_zip=address_zip, 

+

region_status=region_status, 

+

ranger_agent_version=ranger_agent_version, 

+

open_stack_version=open_stack_version, 

+

design_type=design_type, 

+

location_type=location_type, 

+

vlcp_name=vlcp_name, 

+

clli=clli, 

+

description=description, 

+

is_ecomp=is_ecomp * 1, 

+

is_ssp=is_ssp * 1, 

+

purpose_of_region=purpose_of_region) 

+

 

+

if end_point_list is not None: 

+

for end_point in end_point_list: 

+

region_end_point = RegionEndPoint( 

+

end_point_type=end_point["type"], 

+

public_url=end_point["url"]) 

+

region.end_points.append(region_end_point) 

+

 

+

if meta_data_dict is not None: 

+

for k, v in meta_data_dict.iteritems(): 

+

for list_item in v: 

+

region.meta_data.append( 

+

RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

 

+

session.add(region) 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.warning("Duplicate entry: {}".format(str(e))) 

+

raise DuplicateEntryError("Region {} already " 

+

"exist".format(region_id)) 

+

 

+

def update_region(self, 

+

region_to_update, 

+

region_id, 

+

name, 

+

address_state, 

+

address_country, 

+

address_city, 

+

address_street, 

+

address_zip, 

+

region_status, 

+

ranger_agent_version, 

+

open_stack_version, 

+

design_type, 

+

location_type, 

+

vlcp_name, 

+

clli, 

+

# a list of dictionaries of format 

+

# {"type":"", "url":"", "description":"" 

+

end_point_list, 

+

# a list of dictionaries of format 

+

# {"key":"", "value":"", "description":"" 

+

meta_data_dict, 

+

is_ecomp, 

+

is_ssp, 

+

purpose_of_region="", 

+

description=""): 

+

""" add a new region to the `region` table 

+

add also the regions give meta_data and end_points to the `region_end_point` and 

+

`region_meta_data` tables if given. 

+

handle duplicate errors if raised""" 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

# remove all childs as with update need to replace them 

+

session.query(RegionMetaData).filter_by(region_id=region_to_update).delete() 

+

session.query(RegionEndPoint).filter_by(region_id=region_to_update).delete() 

+

 

+

record = session.query(Region).filter_by(region_id=region_to_update).first() 

+

if record is not None: 

+

# record.region_id = region_id # ignore id and name when update 

+

# record.name = name 

+

record.address_state = address_state 

+

record.address_country = address_country 

+

record.address_city = address_city 

+

record.address_street = address_street 

+

record.address_zip = address_zip 

+

record.region_status = region_status 

+

record.ranger_agent_version = ranger_agent_version 

+

record.open_stack_version = open_stack_version 

+

record.design_type = design_type 

+

record.location_type = location_type 

+

record.vlcp_name = vlcp_name 

+

record.clli = clli 

+

record.description = description 

+

record.is_ecomp = is_ecomp * 1 

+

record.is_ssp = is_ssp * 1 

+

record.purpose_of_region = purpose_of_region 

+

 

+

if end_point_list is not None: 

+

for end_point in end_point_list: 

+

region_end_point = RegionEndPoint( 

+

end_point_type=end_point["type"], 

+

public_url=end_point["url"] 

+

) 

+

record.end_points.append(region_end_point) 

+

 

+

if meta_data_dict is not None: 

+

for k, v in meta_data_dict.iteritems(): 

+

for list_item in v: 

+

record.meta_data.append( 

+

RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

else: 

+

raise EntityNotFound("Region {} not found".format( 

+

region_to_update)) 

+

except EntityNotFound as exp: 

+

logger.exception( 

+

"fail to update entity with id {} not found".format( 

+

region_to_update)) 

+

raise ServiceBase.NotFoundError(message=exp.message) 

+

except Exception as exp: 

+

logger.exception("fail to update region {}".format(str(exp))) 

+

raise 

+

 

+

def delete_region(self, region_id): 

+

# delete a region from `region` table and also the region's 

+

# entries from `region_meta_data` and `region_end_points` tables 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(Region).filter_by(region_id=region_id).delete() 

+

 

+

def get_all_regions(self): 

+

return self.get_regions(None, None, None) 

+

 

+

def get_regions(self, 

+

region_filters_dict, 

+

meta_data_dict, 

+

end_point_dict): 

+

logger.debug("Get regions") 

+

records_model = [] 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

records = session.query(Region) 

+

if region_filters_dict is not None: 

+

records = records.filter_by(**region_filters_dict) 

+

 

+

if meta_data_dict is not None: 

+

regions = self._get_regions_for_meta_data_dict(meta_data_dict, 

+

session) 

+

query = [] 

+

query.append((Region.region_id.in_(regions))) 

+

records = records.filter(*query) 

+

 

+

if end_point_dict is not None: 

+

records = records.join(RegionEndPoint).\ 

+

filter_by(**end_point_dict) 

+

if records is not None: 

+

for record in records: 

+

records_model.append(record.to_wsme()) 

+

return records_model 

+

 

+

def _get_regions_for_meta_data_dict(self, meta_data_dict, session): 

+

result_lists = [] 

+

for key in meta_data_dict['meta_data_keys']: 

+

md_q = session.query(RegionMetaData). \ 

+

filter(RegionMetaData.meta_data_key == key).all() 

+

temp_result_list = [] 

+

if md_q is not None: 

+

for record in md_q: 

+

temp_result_list.append(record.region_id) 

+

result_lists.append(set(temp_result_list)) 

+

logger.debug(set(temp_result_list)) 

+

for item in meta_data_dict['meta_data_pairs']: 

+

md_q = session.query(RegionMetaData). \ 

+

filter(RegionMetaData.meta_data_key == item['metadata_key'], 

+

RegionMetaData.meta_data_value == item['metadata_value']).all() 

+

temp_result_list = [] 

+

if md_q is not None: 

+

for record in md_q: 

+

temp_result_list.append(record.region_id) 

+

result_lists.append(set(temp_result_list)) 

+

logger.debug(set(temp_result_list)) 

+

 

+

result = [] 

+

if result_lists: 

+

result = result_lists[0] 

+

for l in result_lists: 

+

result = result.intersection(l) 

+

else: 

+

result = None 

+

logger.debug(result) 

+

return result 

+

 

+

def get_region_by_id_or_name(self, region_id_or_name): 

+

logger.debug("Get region by id or name: {}".format(region_id_or_name)) 

+

try: 

+

 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

record = session.query(Region) 

+

record = record.filter(or_(Region.region_id == region_id_or_name, 

+

Region.name == region_id_or_name)) 

+

if record.first(): 

+

return record.first().to_wsme() 

+

return None 

+

 

+

except Exception as exp: 

+

logger.exception("DB error filtering by id/name") 

+

raise 

+

 

+

def add_meta_data_to_region(self, region_id, 

+

metadata_dict): 

+

""" 

+

:param region_id: 

+

:param metadata_dict: 

+

:return: 

+

""" 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

record = session.query(Region).\ 

+

filter_by(region_id=region_id).first() 

+

 

+

if record is not None: 

+

region_metadata = [] 

+

for k, v in metadata_dict.iteritems(): 

+

for list_item in v: 

+

region_metadata.append(RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

session.add_all(region_metadata) 

+

return record.to_wsme() 

+

else: 

+

logger.error("Region {} does not exist. " 

+

"Meta Data was not added!".format(region_id)) 

+

return None 

+

 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.warning("Duplicate entry: {}".format(str(e))) 

+

raise error_base.ConflictError(message="Duplicate metadata value " 

+

"in region {}".format(region_id)) 

+

 

+

def update_region_meta_data(self, region_id, 

+

metadata_dict): 

+

""" 

+

Replace existing metadata for given region_id 

+

:param region_id: 

+

:param metadata_dict: 

+

:return: 

+

""" 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

 

+

record = session.query(Region). \ 

+

filter_by(region_id=region_id).first() 

+

if not record: 

+

msg = "Region {} not found".format(region_id) 

+

logger.info(msg) 

+

raise error_base.NotFoundError(message=msg) 

+

 

+

session.query(RegionMetaData).\ 

+

filter_by(region_id=region_id).delete() 

+

 

+

region_metadata = [] 

+

for k, v in metadata_dict.iteritems(): 

+

for list_item in v: 

+

region_metadata.append(RegionMetaData(region_id=region_id, 

+

meta_data_key=k, 

+

meta_data_value=list_item)) 

+

 

+

session.add_all(region_metadata) 

+

return record.to_wsme() 

+

 

+

def delete_region_metadata(self, region_id, key): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

record = session.query(Region). \ 

+

filter_by(region_id=region_id).first() 

+

 

+

if not record: 

+

msg = "Region {} not found".format(region_id) 

+

logger.info(msg) 

+

raise error_base.NotFoundError(message=msg) 

+

 

+

session.query(RegionMetaData).filter_by(region_id=region_id, 

+

meta_data_key=key).delete() 

+

 

+

def update_region_status(self, region_id, region_status): 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

 

+

record = session.query(Region).filter_by(region_id=region_id).first() 

+

if record is not None: 

+

record.region_status = region_status 

+

else: 

+

msg = "Region {} not found".format(region_id) 

+

logger.info(msg) 

+

raise error_base.NotFoundError(message=msg) 

+

return record.region_status 

+

 

+

except Exception as exp: 

+

logger.exception("failed to update region {}".format(str(exp))) 

+

raise 

+

""" 

+

def add_end_point_to_region(self, 

+

region_id, 

+

end_point_type, 

+

end_point_url, 

+

description): 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

record = session.query(Region).filter_by(region_id=region_id).\ 

+

first() 

+

if record is not None: 

+

session.add( 

+

RegionEndPoint(region_id=region_id, 

+

end_point_type=end_point_type, 

+

public_url=end_point_url, 

+

description=description)) 

+

else: 

+

logger.error("Region {} does not exist. " 

+

"End point was not added !".format(region_id)) 

+

 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.warning("Duplicate entry: {}".format(str(e))) 

+

raise SQLDBError("Duplicate entry error") 

+

 

+

def remove_end_point_from_region(self, 

+

region_id, 

+

end_point_type): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(Region).filter_by(region_id=region_id, 

+

end_point_type=end_point_type).\ 

+

delete() 

+

""" 

+

 

+

# Handle group management operations 

+

def add_group(self, 

+

group_id, 

+

group_name, 

+

group_description, 

+

region_ids_list): 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

session.add(Group(group_id=group_id, 

+

name=group_name, 

+

description=group_description)) 

+

 

+

session.flush() # add the groupe if not rollback 

+

 

+

if region_ids_list is not None: 

+

group_regions = [] 

+

for region_id in region_ids_list: 

+

group_regions.append(GroupRegion(group_id=group_id, 

+

region_id=region_id)) 

+

session.add_all(group_regions) 

+

except oslo_db.exception.DBReferenceError as e: 

+

logger.error("Reference error: {}".format(str(e))) 

+

raise error_base.InputValueError("Reference error") 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.error("Duplicate entry: {}".format(str(e))) 

+

raise error_base.ConflictError("Duplicate entry error") 

+

 

+

def delete_group(self, group_id): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(Group).filter_by(group_id=group_id).delete() 

+

 

+

def get_all_groups(self): 

+

logger.debug("DB- Get all groups") 

+

records_model = PythonModels.GroupsWrraper() 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

groups = session.query(Group) 

+

for a_group in groups: 

+

group_model = PythonModels.Groups() 

+

group_model.id = a_group.group_id 

+

group_model.name = a_group.name 

+

group_model.description = a_group.description 

+

regions = [] 

+

group_regions = session.query(GroupRegion).\ 

+

filter_by(group_id=a_group.group_id) 

+

for group_region in group_regions: 

+

regions.append(group_region.region_id) 

+

 

+

group_model.regions = regions 

+

records_model.groups.append(group_model) 

+

return records_model 

+

 

+

def update_group(self, group_id, group_name, group_description, 

+

group_regions): 

+

try: 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

# in update scenario delete all child records 

+

session.query(GroupRegion).filter_by( 

+

group_id=group_id).delete() 

+

 

+

group_record = session.query(Group).filter_by( 

+

group_id=group_id).first() 

+

if group_record is None: 

+

raise error_base.NotFoundError( 

+

message="Group {} not found".format(group_id)) 

+

# only desc and regions can be changed 

+

group_record.description = group_description 

+

group_record.name = group_name 

+

regions = [] 

+

for region_id in group_regions: 

+

regions.append(GroupRegion(region_id=region_id, 

+

group_id=group_id)) 

+

session.add_all(regions) 

+

 

+

except error_base.NotFoundError as exp: 

+

logger.error(exp.message) 

+

raise 

+

except oslo_db.exception.DBReferenceError as e: 

+

logger.error("Reference error: {}".format(str(e))) 

+

raise error_base.InputValueError("Reference error") 

+

except Exception as exp: 

+

logger.error("failed to update group {}".format(group_id)) 

+

logger.exception(exp) 

+

raise 

+

return 

+

 

+

def get_group(self, group_id): 

+

logger.debug("Get group by name") 

+

group_model = None 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

a_group = session.query(Group).filter_by(group_id=group_id)\ 

+

.first() 

+

if a_group is not None: 

+

group_model = {"id": a_group.group_id, 

+

"name": a_group.name, 

+

"description": a_group.description} 

+

regions = [] 

+

group_regions = session.query(GroupRegion). \ 

+

filter_by(group_id=a_group.group_id) 

+

for group_region in group_regions: 

+

regions.append(group_region.region_id) 

+

group_model["regions"] = regions 

+

return group_model 

+

 

+

""" 

+

def add_region_to_group(self, 

+

group_id, 

+

region_id): 

+

session = self._engine_facade.get_session() 

+

try: 

+

with session.begin(): 

+

session.add(GroupRegion(group_id=group_id, 

+

region_id=region_id)) 

+

except oslo_db.exception.DBReferenceError as e: 

+

logger.error("Refernce error: {}".format(str(e))) 

+

raise SQLDBError("Reference error") 

+

except oslo_db.exception.DBDuplicateEntry as e: 

+

logger.error("Duplicate entry: {}".format(str(e))) 

+

raise SQLDBError("Duplicate entry error") 

+

 

+

def remove_region_from_group(self, 

+

group_id, 

+

region_id): 

+

session = self._engine_facade.get_session() 

+

with session.begin(): 

+

session.query(GroupRegion).filter_by(group_id=group_id, 

+

region_id=region_id).delete() 

+

""" 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_utils___init___py.html b/orm/services/region_manager/htmlcov/rms_utils___init___py.html new file mode 100644 index 00000000..e9506082 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_utils___init___py.html @@ -0,0 +1,89 @@ + + + + + + + + + + + Coverage for rms/utils/__init__.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/rms_utils_authentication_py.html b/orm/services/region_manager/htmlcov/rms_utils_authentication_py.html new file mode 100644 index 00000000..70124848 --- /dev/null +++ b/orm/services/region_manager/htmlcov/rms_utils_authentication_py.html @@ -0,0 +1,195 @@ + + + + + + + + + + + Coverage for rms/utils/authentication.py: 100% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+ +
+

import logging 

+

 

+

from keystone_utils import tokens 

+

from orm_common.policy import policy 

+

from orm_common.utils import api_error_utils as err_utils 

+

 

+

from pecan import conf 

+

 

+

from rms.services import services as RegionService 

+

 

+

 

+

logger = logging.getLogger(__name__) 

+

 

+

 

+

def _get_keystone_ep(auth_region): 

+

result = RegionService.get_region_by_id_or_name(auth_region) 

+

for ep in result.endpoints: 

+

if ep.type == 'identity': 

+

return ep.publicurl 

+

 

+

# Keystone EP not found 

+

return None 

+

 

+

 

+

def authorize(request, action): 

+

if not _is_authorization_enabled(conf): 

+

return 

+

 

+

auth_region = request.headers.get('X-Auth-Region') 

+

try: 

+

keystone_ep = _get_keystone_ep(auth_region) 

+

except Exception: 

+

# Failed to find Keystone EP - we'll set it to None instead of failing 

+

# because the rule might be to let everyone pass 

+

keystone_ep = None 

+

 

+

policy.authorize(action, request, conf, keystone_ep=keystone_ep) 

+

 

+

 

+

def _is_authorization_enabled(app_conf): 

+

return app_conf.authentication.enabled 

+

 

+

 

+

def get_token_conf(app_conf): 

+

mech_id = app_conf.authentication.mech_id 

+

mech_password = app_conf.authentication.mech_pass 

+

# RMS URL is not necessary since this service is RMS 

+

rms_url = '' 

+

tenant_name = app_conf.authentication.tenant_name 

+

keystone_version = app_conf.authentication.keystone_version 

+

conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name, 

+

keystone_version) 

+

return conf 

+ +
+
+ + + + + diff --git a/orm/services/region_manager/htmlcov/status.json b/orm/services/region_manager/htmlcov/status.json new file mode 100644 index 00000000..1ce63b3c --- /dev/null +++ b/orm/services/region_manager/htmlcov/status.json @@ -0,0 +1 @@ +{"files":{"rms_controllers_v2_orm_resources___init___py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/__init__.py","html_filename":"rms_controllers_v2_orm_resources___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"103b3a1826780f920626a4f622bcee7c"},"rms___init___py":{"index":{"relative_filename":"rms/__init__.py","html_filename":"rms___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_external_mock_audit_client_api___init___py":{"index":{"relative_filename":"rms/external_mock/audit_client/api/__init__.py","html_filename":"rms_external_mock_audit_client_api___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"19a064df3967067a2ec86c467e92d140"},"rms_controllers_lcp_controller_py":{"index":{"relative_filename":"rms/controllers/lcp_controller.py","html_filename":"rms_controllers_lcp_controller_py.html","nums":[1,56,0,0,0,0,0]},"hash":"a032e4765d0af628116fb4a525003601"},"rms_utils_authentication_py":{"index":{"relative_filename":"rms/utils/authentication.py","html_filename":"rms_utils_authentication_py.html","nums":[1,32,0,0,0,0,0]},"hash":"9816983ff8e57e8d1a9ed68b7c30ff50"},"rms_external_mock_orm_common___init___py":{"index":{"relative_filename":"rms/external_mock/orm_common/__init__.py","html_filename":"rms_external_mock_orm_common___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_model___init___py":{"index":{"relative_filename":"rms/model/__init__.py","html_filename":"rms_model___init___py.html","nums":[1,3,0,0,0,0,0]},"hash":"cf7982d8674fcb1858c46e0ed829f1e2"},"rms_storage_my_sql___init___py":{"index":{"relative_filename":"rms/storage/my_sql/__init__.py","html_filename":"rms_storage_my_sql___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_model_url_parm_py":{"index":{"relative_filename":"rms/model/url_parm.py","html_filename":"rms_model_url_parm_py.html","nums":[1,63,0,4,0,0,0]},"hash":"554a5d26967309c0321bb2d5ca4de2d0"},"rms_controllers_root_py":{"index":{"relative_filename":"rms/controllers/root.py","html_filename":"rms_controllers_root_py.html","nums":[1,12,0,1,0,0,0]},"hash":"22b13e705390f352b9e7a78c2f83d7b9"},"rms_external_mock_keystone_utils___init___py":{"index":{"relative_filename":"rms/external_mock/keystone_utils/__init__.py","html_filename":"rms_external_mock_keystone_utils___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_controllers_v2_orm_resources_groups_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/groups.py","html_filename":"rms_controllers_v2_orm_resources_groups_py.html","nums":[1,124,0,22,0,0,0]},"hash":"38e755d11e73209da31ce4c4faff8d49"},"rms_controllers_v2_orm_resources_status_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/status.py","html_filename":"rms_controllers_v2_orm_resources_status_py.html","nums":[1,47,0,3,0,0,0]},"hash":"afc1e6ecd339095ff57e6b9c7c9c819d"},"rms_services_error_base_py":{"index":{"relative_filename":"rms/services/error_base.py","html_filename":"rms_services_error_base_py.html","nums":[1,18,0,2,0,0,0]},"hash":"9cf5cf89d59dd8f73527b7bec0f6f11d"},"rms_external_mock_orm_common_policy_policy_py":{"index":{"relative_filename":"rms/external_mock/orm_common/policy/policy.py","html_filename":"rms_external_mock_orm_common_policy_policy_py.html","nums":[1,6,0,2,0,0,0]},"hash":"7ea56b6475a95231ec2271c5fb347853"},"rms_storage___init___py":{"index":{"relative_filename":"rms/storage/__init__.py","html_filename":"rms_storage___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_external_mock_orm_common_policy___init___py":{"index":{"relative_filename":"rms/external_mock/orm_common/policy/__init__.py","html_filename":"rms_external_mock_orm_common_policy___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_controllers_v2_orm___init___py":{"index":{"relative_filename":"rms/controllers/v2/orm/__init__.py","html_filename":"rms_controllers_v2_orm___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"f820fd7cab97fc8512bec0556f3f002c"},"rms_controllers_v2___init___py":{"index":{"relative_filename":"rms/controllers/v2/__init__.py","html_filename":"rms_controllers_v2___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"103b3a1826780f920626a4f622bcee7c"},"rms_controllers_v2_orm_resources_regions_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/regions.py","html_filename":"rms_controllers_v2_orm_resources_regions_py.html","nums":[1,187,0,5,0,0,0]},"hash":"31c3b8e26317be7b0c19896d4f008617"},"rms_model_model_py":{"index":{"relative_filename":"rms/model/model.py","html_filename":"rms_model_model_py.html","nums":[1,100,0,7,0,0,0]},"hash":"29463d225fbe8d7a76bd2c96fc854ea9"},"rms_external_mock_orm_common_utils_utils_py":{"index":{"relative_filename":"rms/external_mock/orm_common/utils/utils.py","html_filename":"rms_external_mock_orm_common_utils_utils_py.html","nums":[1,6,0,1,0,0,0]},"hash":"3fd1f8e4472126ea5ccdbf31e97d61ea"},"rms_services___init___py":{"index":{"relative_filename":"rms/services/__init__.py","html_filename":"rms_services___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"8ac99c36cc7a0f004e5b32bd37a07666"},"rms_controllers_configuration_py":{"index":{"relative_filename":"rms/controllers/configuration.py","html_filename":"rms_controllers_configuration_py.html","nums":[1,17,0,0,0,0,0]},"hash":"b26d7e4d8662a3399ee6bfcca7910f6a"},"rms_storage_data_manager_factory_py":{"index":{"relative_filename":"rms/storage/data_manager_factory.py","html_filename":"rms_storage_data_manager_factory_py.html","nums":[1,12,0,0,0,0,0]},"hash":"187c41f6ce3b54925ae87043fbd31c9d"},"rms_storage_base_data_manager_py":{"index":{"relative_filename":"rms/storage/base_data_manager.py","html_filename":"rms_storage_base_data_manager_py.html","nums":[1,24,0,0,0,0,0]},"hash":"161f32081cdb26e50f07e76b8b1a9a5d"},"rms_controllers_v2_orm_resources_metadata_py":{"index":{"relative_filename":"rms/controllers/v2/orm/resources/metadata.py","html_filename":"rms_controllers_v2_orm_resources_metadata_py.html","nums":[1,85,0,19,0,0,0]},"hash":"7bb73ee1f834e6f8998371ce19f77ade"},"rms_external_mock___init___py":{"index":{"relative_filename":"rms/external_mock/__init__.py","html_filename":"rms_external_mock___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"19a064df3967067a2ec86c467e92d140"},"rms_storage_my_sql_data_manager_py":{"index":{"relative_filename":"rms/storage/my_sql/data_manager.py","html_filename":"rms_storage_my_sql_data_manager_py.html","nums":[1,270,0,31,0,0,0]},"hash":"5a1cbfe3ec7b9c95011d5586e27057cb"},"rms_external_mock_keystone_utils_tokens_py":{"index":{"relative_filename":"rms/external_mock/keystone_utils/tokens.py","html_filename":"rms_external_mock_keystone_utils_tokens_py.html","nums":[1,5,0,1,0,0,0]},"hash":"9e3b83a68d18e91870ada743cc6e8aaf"},"rms_controllers_v2_root_py":{"index":{"relative_filename":"rms/controllers/v2/root.py","html_filename":"rms_controllers_v2_root_py.html","nums":[1,3,0,0,0,0,0]},"hash":"a6ab4c4b9cc373374c0cdf71e05f92b2"},"rms_external_mock_orm_common_utils_api_error_utils_py":{"index":{"relative_filename":"rms/external_mock/orm_common/utils/api_error_utils.py","html_filename":"rms_external_mock_orm_common_utils_api_error_utils_py.html","nums":[1,2,0,1,0,0,0]},"hash":"5b097d36789f1b7dea3d91b3b8d3718a"},"rms_services_services_py":{"index":{"relative_filename":"rms/services/services.py","html_filename":"rms_services_services_py.html","nums":[1,170,0,55,0,0,0]},"hash":"ee882c9e5fa6c871e813a62a7126e7f8"},"rms_external_mock_audit_client_api_audit_py":{"index":{"relative_filename":"rms/external_mock/audit_client/api/audit.py","html_filename":"rms_external_mock_audit_client_api_audit_py.html","nums":[1,4,0,4,0,0,0]},"hash":"b0c363258cf77b3628c98037bd768ab4"},"rms_controllers___init___py":{"index":{"relative_filename":"rms/controllers/__init__.py","html_filename":"rms_controllers___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_controllers_logs_py":{"index":{"relative_filename":"rms/controllers/logs.py","html_filename":"rms_controllers_logs_py.html","nums":[1,36,0,2,0,0,0]},"hash":"8e5e06cf27f60958af7f39bc4c28a5b9"},"rms_external_mock_audit_client___init___py":{"index":{"relative_filename":"rms/external_mock/audit_client/__init__.py","html_filename":"rms_external_mock_audit_client___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"19a064df3967067a2ec86c467e92d140"},"rms_controllers_v2_orm_root_py":{"index":{"relative_filename":"rms/controllers/v2/orm/root.py","html_filename":"rms_controllers_v2_orm_root_py.html","nums":[1,5,0,0,0,0,0]},"hash":"7d9f45649dfaa9de7107f13e60452b2e"},"rms_external_mock_orm_common_utils___init___py":{"index":{"relative_filename":"rms/external_mock/orm_common/utils/__init__.py","html_filename":"rms_external_mock_orm_common_utils___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"},"rms_utils___init___py":{"index":{"relative_filename":"rms/utils/__init__.py","html_filename":"rms_utils___init___py.html","nums":[1,0,0,0,0,0,0]},"hash":"eef1bc6a4630e20d1fc10b7474fc01f8"}},"version":"4.3.4","settings":"38064b61f52503ec7d8e083087b27b2f","format":1} \ No newline at end of file diff --git a/orm/services/region_manager/htmlcov/style.css b/orm/services/region_manager/htmlcov/style.css new file mode 100644 index 00000000..86b82091 --- /dev/null +++ b/orm/services/region_manager/htmlcov/style.css @@ -0,0 +1,375 @@ +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ + +/* CSS styles for coverage.py. */ + +/* Page-wide styles */ +html, body, h1, h2, h3, p, table, td, th { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; + } + +/* Set baseline grid to 16 pt. */ +body { + font-family: georgia, serif; + font-size: 1em; + } + +html>body { + font-size: 16px; + } + +/* Set base font size to 12/16 */ +p { + font-size: .75em; /* 12/16 */ + line-height: 1.33333333em; /* 16/12 */ + } + +table { + border-collapse: collapse; + } +td { + vertical-align: top; +} +table tr.hidden { + display: none !important; + } + +p#no_rows { + display: none; + font-size: 1.2em; + } + +a.nav { + text-decoration: none; + color: inherit; + } +a.nav:hover { + text-decoration: underline; + color: inherit; + } + +/* Page structure */ +#header { + background: #f8f8f8; + width: 100%; + border-bottom: 1px solid #eee; + } + +#source { + padding: 1em; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + } + +.indexfile #footer { + margin: 1em 3em; + } + +.pyfile #footer { + margin: 1em 1em; + } + +#footer .content { + padding: 0; + font-size: 85%; + font-family: verdana, sans-serif; + color: #666666; + font-style: italic; + } + +#index { + margin: 1em 0 0 3em; + } + +/* Header styles */ +#header .content { + padding: 1em 3em; + } + +h1 { + font-size: 1.25em; + display: inline-block; +} + +#filter_container { + display: inline-block; + float: right; + margin: 0 2em 0 0; +} +#filter_container input { + width: 10em; +} + +h2.stats { + margin-top: .5em; + font-size: 1em; +} +.stats span { + border: 1px solid; + padding: .1em .25em; + margin: 0 .1em; + cursor: pointer; + border-color: #999 #ccc #ccc #999; +} +.stats span.hide_run, .stats span.hide_exc, +.stats span.hide_mis, .stats span.hide_par, +.stats span.par.hide_run.hide_par { + border-color: #ccc #999 #999 #ccc; +} +.stats span.par.hide_run { + border-color: #999 #ccc #ccc #999; +} + +.stats span.run { + background: #ddffdd; +} +.stats span.exc { + background: #eeeeee; +} +.stats span.mis { + background: #ffdddd; +} +.stats span.hide_run { + background: #eeffee; +} +.stats span.hide_exc { + background: #f5f5f5; +} +.stats span.hide_mis { + background: #ffeeee; +} +.stats span.par { + background: #ffffaa; +} +.stats span.hide_par { + background: #ffffcc; +} + +/* Help panel */ +#keyboard_icon { + float: right; + margin: 5px; + cursor: pointer; +} + +.help_panel { + position: absolute; + background: #ffffcc; + padding: .5em; + border: 1px solid #883; + display: none; +} + +.indexfile .help_panel { + width: 20em; height: 4em; +} + +.pyfile .help_panel { + width: 16em; height: 8em; +} + +.help_panel .legend { + font-style: italic; + margin-bottom: 1em; +} + +#panel_icon { + float: right; + cursor: pointer; +} + +.keyhelp { + margin: .75em; +} + +.keyhelp .key { + border: 1px solid black; + border-color: #888 #333 #333 #888; + padding: .1em .35em; + font-family: monospace; + font-weight: bold; + background: #eee; +} + +/* Source file styles */ +.linenos p { + text-align: right; + margin: 0; + padding: 0 .5em; + color: #999999; + font-family: verdana, sans-serif; + font-size: .625em; /* 10/16 */ + line-height: 1.6em; /* 16/10 */ + } +.linenos p.highlight { + background: #ffdd00; + } +.linenos p a { + text-decoration: none; + color: #999999; + } +.linenos p a:hover { + text-decoration: underline; + color: #999999; + } + +td.text { + width: 100%; + } +.text p { + margin: 0; + padding: 0 0 0 .5em; + border-left: 2px solid #ffffff; + white-space: pre; + position: relative; + } + +.text p.mis { + background: #ffdddd; + border-left: 2px solid #ff0000; + } +.text p.run, .text p.run.hide_par { + background: #ddffdd; + border-left: 2px solid #00ff00; + } +.text p.exc { + background: #eeeeee; + border-left: 2px solid #808080; + } +.text p.par, .text p.par.hide_run { + background: #ffffaa; + border-left: 2px solid #eeee99; + } +.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, +.text p.hide_run.hide_par { + background: inherit; + } + +.text span.annotate { + font-family: georgia; + color: #666; + float: right; + padding-right: .5em; + } +.text p.hide_par span.annotate { + display: none; + } +.text span.annotate.long { + display: none; + } +.text p:hover span.annotate.long { + display: block; + max-width: 50%; + white-space: normal; + float: right; + position: absolute; + top: 1.75em; + right: 1em; + width: 30em; + height: auto; + color: #333; + background: #ffffcc; + border: 1px solid #888; + padding: .25em .5em; + z-index: 999; + border-radius: .2em; + box-shadow: #cccccc .2em .2em .2em; + } + +/* Syntax coloring */ +.text .com { + color: green; + font-style: italic; + line-height: 1px; + } +.text .key { + font-weight: bold; + line-height: 1px; + } +.text .str { + color: #000080; + } + +/* index styles */ +#index td, #index th { + text-align: right; + width: 5em; + padding: .25em .5em; + border-bottom: 1px solid #eee; + } +#index th { + font-style: italic; + color: #333; + border-bottom: 1px solid #ccc; + cursor: pointer; + } +#index th:hover { + background: #eee; + border-bottom: 1px solid #999; + } +#index td.left, #index th.left { + padding-left: 0; + } +#index td.right, #index th.right { + padding-right: 0; + } +#index th.headerSortDown, #index th.headerSortUp { + border-bottom: 1px solid #000; + white-space: nowrap; + background: #eee; + } +#index th.headerSortDown:after { + content: " ↓"; +} +#index th.headerSortUp:after { + content: " ↑"; +} +#index td.name, #index th.name { + text-align: left; + width: auto; + } +#index td.name a { + text-decoration: none; + color: #000; + } +#index tr.total, +#index tr.total_dynamic { + } +#index tr.total td, +#index tr.total_dynamic td { + font-weight: bold; + border-top: 1px solid #ccc; + border-bottom: none; + } +#index tr.file:hover { + background: #eeeeee; + } +#index tr.file:hover td.name { + text-decoration: underline; + color: #000; + } + +/* scroll marker styles */ +#scroll_marker { + position: fixed; + right: 0; + top: 0; + width: 16px; + height: 100%; + background: white; + border-left: 1px solid #eee; + } + +#scroll_marker .marker { + background: #eedddd; + position: absolute; + min-height: 3px; + width: 100%; + } diff --git a/orm/services/region_manager/public/css/style.css b/orm/services/region_manager/public/css/style.css new file mode 100755 index 00000000..55c9db54 --- /dev/null +++ b/orm/services/region_manager/public/css/style.css @@ -0,0 +1,43 @@ +body { + background: #311F00; + color: white; + font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif; + padding: 1em 2em; +} + +a { + color: #FAFF78; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div#content { + width: 800px; + margin: 0 auto; +} + +form { + margin: 0; + padding: 0; + border: 0; +} + +fieldset { + border: 0; +} + +input.error { + background: #FAFF78; +} + +header { + text-align: center; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Futura-CondensedExtraBold', 'Futura', 'Helvetica', sans-serif; + text-transform: uppercase; +} diff --git a/orm/services/region_manager/public/images/logo.png b/orm/services/region_manager/public/images/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..a8f403e4a4f3ce69a4577a46ae37f5633abb79fa GIT binary patch literal 20596 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 ZD6?|u)*zSG3-Sw<9?1AXo%Yig{{dC~0l)wN literal 0 HcmV?d00001 diff --git a/orm/services/region_manager/readme.md b/orm/services/region_manager/readme.md new file mode 100644 index 00000000..b8a2af36 --- /dev/null +++ b/orm/services/region_manager/readme.md @@ -0,0 +1,35 @@ +# ORM discovery readme file + +This document includes the following topics +1. Dev environment installation process +1. Code style guide +1. Python tips and tricks + + +## Dev Environment installation process +This project is python based so you need to have python 2.7 installed before continuing + + +_TBD_ + +## Code style guide +First of all please read two of the following documents: +1. [Pythons PEP8](https://www.python.org/dev/peps/pep-0008/) +1. [Openstack hacking](http://docs.openstack.org/developer/hacking/) + +__After reading this please read it 3 more times__ + +### Folders +Folders names should be lowercase, if needed delimited by underscore + +do: +``` +business_logic +``` +not +``` +BusinessLogic +``` +## Python tips and tricks + +# test header \ No newline at end of file diff --git a/orm/services/region_manager/revert_csv2db.py b/orm/services/region_manager/revert_csv2db.py new file mode 100644 index 00000000..7f25849a --- /dev/null +++ b/orm/services/region_manager/revert_csv2db.py @@ -0,0 +1,32 @@ +import logging +import csv + +from rms.storage.base_data_manager import SQLDBError + +from rms.storage.my_sql.data_manager import DataManager +import config + +logger = logging.getLogger(__name__) + + +def revert_csv2db(data_manager): + logger.info('revert csv to db..') + + try: + + with open('rms/resources/regions.csv') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + data_manager.delete_region(row["region_id"]) + + except SQLDBError as e: + logger.error("SQL error raised {}".format(e.message)) + + +def main(): + db_url = config.database['url'] + data_manager = DataManager(db_url) + revert_csv2db(data_manager) + +if __name__ == "__main__": + main() diff --git a/orm/services/region_manager/rms.conf b/orm/services/region_manager/rms.conf new file mode 100644 index 00000000..7912de6a --- /dev/null +++ b/orm/services/region_manager/rms.conf @@ -0,0 +1,26 @@ +Listen 8080 + + + + WSGIDaemonProcess rms user=orm group=orm threads=5 + WSGIScriptAlias / /opt/app/orm/rms/rms.wsgi + + + Order deny,allow + Deny from all + Allow from localhost + + + + Order deny,allow + Deny from all + Allow from localhost + + + + WSGIProcessGroup rms + WSGIApplicationGroup %{GLOBAL} + Require all granted + Allow from all + + diff --git a/orm/services/region_manager/rms.wsgi b/orm/services/region_manager/rms.wsgi new file mode 100644 index 00000000..dc62cf7e --- /dev/null +++ b/orm/services/region_manager/rms.wsgi @@ -0,0 +1,2 @@ +from pecan.deploy import deploy +application = deploy('/opt/app/orm/rms/config.py') diff --git a/orm/services/region_manager/rms/__init__.py b/orm/services/region_manager/rms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/app.py b/orm/services/region_manager/rms/app.py new file mode 100755 index 00000000..a03c6ae9 --- /dev/null +++ b/orm/services/region_manager/rms/app.py @@ -0,0 +1,40 @@ +import logging +import os + +from pecan import make_app +from pecan.commands import CommandRunner + +from orm_common.utils import utils +from orm_common.policy import policy + +from rms import model +from rms.utils import authentication + + +logger = logging.getLogger(__name__) + + +def setup_app(config): + model.init_model() + token_conf = authentication.get_token_conf(config) + policy.init(config.authentication.policy_file, token_conf) + app_conf = dict(config.app) + + utils.set_utils_conf(config) + + app = make_app( + app_conf.pop('root'), + logging=getattr(config, 'logging', {}), + **app_conf + ) + + logger.info('Starting RMS...') + return app + + +def main(): + dir_name = os.path.dirname(__file__) + drive, path_and_file = os.path.splitdrive(dir_name) + path, filename = os.path.split(path_and_file) + runner = CommandRunner() + runner.run(['serve', path+'/config.py']) diff --git a/orm/services/region_manager/rms/controllers/__init__.py b/orm/services/region_manager/rms/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/controllers/configuration.py b/orm/services/region_manager/rms/controllers/configuration.py new file mode 100755 index 00000000..e570a282 --- /dev/null +++ b/orm/services/region_manager/rms/controllers/configuration.py @@ -0,0 +1,34 @@ +"""Configuration rest API input module.""" + +import logging + +from orm_common.utils import utils + +from pecan import conf +from pecan import request +from pecan import rest +from wsmeext.pecan import wsexpose + +from rms.utils import authentication + +logger = logging.getLogger(__name__) + + +class ConfigurationController(rest.RestController): + """Configuration controller.""" + + @wsexpose(str, str, status_code=200) + def get(self, dump_to_log='false'): + """get method. + + :param dump_to_log: A boolean string that says whether the + configuration should be written to log + :return: A pretty string that contains the service's configuration + """ + logger.info("Get configuration...") + authentication.authorize(request, 'configuration:get') + + dump = dump_to_log.lower() == 'true' + utils.set_utils_conf(conf) + result = utils.report_config(conf, dump, logger) + return result diff --git a/orm/services/region_manager/rms/controllers/lcp_controller.py b/orm/services/region_manager/rms/controllers/lcp_controller.py new file mode 100755 index 00000000..dacb8a57 --- /dev/null +++ b/orm/services/region_manager/rms/controllers/lcp_controller.py @@ -0,0 +1,120 @@ +import logging + +from pecan import rest, request +from pecan import conf + +from wsme import types as wtypes +from wsmeext.pecan import wsexpose +from rms.model import url_parm +from rms.services.error_base import ErrorStatus +from rms.services import services +from rms.utils import authentication + +from orm_common.utils import api_error_utils as err_utils + +logger = logging.getLogger(__name__) + + +class LcpController(rest.RestController): + + @wsexpose(wtypes.text, rest_content_types='json') + def get_all(self): + """ + This function is called when receiving /lcp without a parameter. + parameter: + None. + return: entire list of lcp. + """ + logger.info('Received a GET request for all LCPs') + authentication.authorize(request, 'lcp:get_all') + + zones = [] + + try: + zones = get_zones() + logger.debug('Returning LCP list: %s' % (zones,)) + return zones + + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(wtypes.text, str, rest_content_types='json') + def get_one(self, lcp_id): + + logger.info('Received a GET request for LCP %s' % (id,)) + authentication.authorize(request, 'lcp:get_one') + + zones = [] + try: + + zones = get_zones() + + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + for zone in zones: + if zone["id"] == lcp_id: + logger.debug('Returning: %s' % (zone,)) + return zone + + error_msg = 'LCP %s not found' % (lcp_id,) + logger.info(error_msg) + raise err_utils.get_error(request.transaction_id, + message=error_msg, + status_code=404) + + +def get_zones(): + """ + This function returns the lcp list from CSV file. + parameter: + None. + return: + zone list in json format. + """ + logger.debug('Enter get_zones function') + result = [] + + try: + url_args = url_parm.UrlParms() + zones = services.get_regions_data(url_args) + + for zone in zones.regions: + result.append(build_zone_response(zone)) + + logger.debug("Available regions: {}".format(', '.join( + [region["zone_name"] for region in result]))) + + except ErrorStatus as e: + logger.debug(e.message) + finally: + return result + + +def build_zone_response(zone): + + end_points_dict = {"identity": "", + "dashboard": "", + "ord": ""} + for end_point in zone.endpoints: + end_points_dict[end_point.type] = end_point.publicurl + + return dict( + zone_name=zone.name, + id=zone.id, + status="1" if zone.status == "functional" else "0", + design_type=zone.design_type, + location_type=zone.location_type, + vLCP_name=zone.vlcp_name, + AIC_version=zone.ranger_agent_version, + OS_version=zone.open_stack_version, + keystone_EP=end_points_dict["identity"], + horizon_EP=end_points_dict["dashboard"], + ORD_EP=end_points_dict["ord"] + ) diff --git a/orm/services/region_manager/rms/controllers/logs.py b/orm/services/region_manager/rms/controllers/logs.py new file mode 100755 index 00000000..7cd4dafe --- /dev/null +++ b/orm/services/region_manager/rms/controllers/logs.py @@ -0,0 +1,74 @@ +import logging +from pecan import rest, request +import wsme +from wsmeext.pecan import wsexpose + +from orm_common.utils import api_error_utils as err_utils + +from rms.utils import authentication + +logger = logging.getLogger(__name__) + + +class LogChangeResultWSME(wsme.types.DynamicBase): + """log change result wsme type.""" + + result = wsme.wsattr(str, mandatory=True, default=None) + + def __init__(self, **kwargs): + """"init method.""" + super(LogChangeResult, self).__init__(**kwargs) + + +class LogChangeResult(object): + """log change result type.""" + + def __init__(self, result): + """"init method.""" + self.result = result + + +class LogsController(rest.RestController): + """Logs Audit controller.""" + + @wsexpose(LogChangeResultWSME, str, status_code=201, + rest_content_types='json') + def put(self, level): + """update log level. + + :param level: the log level text name + :return: + """ + + logger.info("Changing log level to [{}]".format(level)) + authentication.authorize(request, 'log:update') + + try: + log_level = logging._levelNames.get(level.upper()) + if log_level is not None: + self._change_log_level(log_level) + result = "Log level changed to {}.".format(level) + logger.info(result) + else: + raise Exception( + "The given log level [{}] doesn't exist.".format(level)) + + return LogChangeResult(result) + + except Exception as exception: + logger.error("Fail to change log_level. Reason: {}".format( + exception.message)) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @staticmethod + def _change_log_level(log_level): + path = __name__.split('.') + if len(path) > 0: + root = path[0] + root_logger = logging.getLogger(root) + root_logger.setLevel(log_level) + else: + logger.info("Fail to change log_level to [{}]. " + "the given log level doesn't exist.".format(log_level)) diff --git a/orm/services/region_manager/rms/controllers/root.py b/orm/services/region_manager/rms/controllers/root.py new file mode 100755 index 00000000..863ac35e --- /dev/null +++ b/orm/services/region_manager/rms/controllers/root.py @@ -0,0 +1,35 @@ +from pecan import expose +from lcp_controller import LcpController +from logs import LogsController +from configuration import ConfigurationController +from rms.controllers.v2 import root + + +class RootController(object): + lcp = LcpController() + logs = LogsController() + configuration = ConfigurationController() + v2 = root.V2Controller() + + @expose(template='json') + def _default(self): + """ + Method to handle GET / + parameters: None + return: dict describing lcp rest version information + """ + return { + "versions": { + "values": [ + { + "status": "stable", + "id": "v2", + "links": [ + { + "href": "http://localhost:8080/" + } + ] + } + ] + } + } diff --git a/orm/services/region_manager/rms/controllers/v2/__init__.py b/orm/services/region_manager/rms/controllers/v2/__init__.py new file mode 100644 index 00000000..a53d8cf9 --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/__init__.py @@ -0,0 +1 @@ +"""orm package.""" diff --git a/orm/services/region_manager/rms/controllers/v2/orm/__init__.py b/orm/services/region_manager/rms/controllers/v2/orm/__init__.py new file mode 100644 index 00000000..1e27d7e7 --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/orm/__init__.py @@ -0,0 +1 @@ +"""resource package.""" diff --git a/orm/services/region_manager/rms/controllers/v2/orm/resources/__init__.py b/orm/services/region_manager/rms/controllers/v2/orm/resources/__init__.py new file mode 100644 index 00000000..6d88728d --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/orm/resources/__init__.py @@ -0,0 +1 @@ +"""orm package.""" diff --git a/orm/services/region_manager/rms/controllers/v2/orm/resources/groups.py b/orm/services/region_manager/rms/controllers/v2/orm/resources/groups.py new file mode 100755 index 00000000..b3a74d2d --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/orm/resources/groups.py @@ -0,0 +1,254 @@ +"""rest module.""" +import logging +import time +import wsme + +from orm_common.utils import api_error_utils as err_utils +from orm_common.utils import utils + +from rms.services import error_base +from rms.services import services as GroupService +from rms.utils import authentication +from pecan import rest, request +from wsme import types as wtypes +from wsmeext.pecan import wsexpose +from rms.model import model as PythonModel + + +logger = logging.getLogger(__name__) + + +class Groups(wtypes.DynamicBase): + """main json header.""" + + id = wsme.wsattr(wtypes.text, mandatory=True) + name = wsme.wsattr(wtypes.text, mandatory=True) + description = wsme.wsattr(wtypes.text, mandatory=True) + regions = wsme.wsattr([str], mandatory=True) + + def __init__(self, id=None, name=None, description=None, regions=[]): + """init function. + + :param regions: + :return: + """ + self.id = id + self.name = name + self.description = description + self.regions = regions + + def _to_python_obj(self): + obj = PythonModel.Groups() + obj.id = self.id + obj.name = self.name + obj.description = self.description + obj.regions = self.regions + return obj + + +class GroupWrapper(wtypes.DynamicBase): + """main cotain lis of groups.""" + + groups = wsme.wsattr([Groups], mandatory=True) + + def __init__(self, groups=[]): + """ + + :param group: + """ + self.groups = groups + + +class OutputResource(wtypes.DynamicBase): + """class method returned json body.""" + + id = wsme.wsattr(wtypes.text, mandatory=True) + name = wsme.wsattr(wtypes.text, mandatory=True) + created = wsme.wsattr(wtypes.text, mandatory=True) + links = wsme.wsattr({str: str}, mandatory=True) + + def __init__(self, id=None, name=None, created=None, links={}): + """init function. + + :param id: + :param created: + :param links: + """ + self.id = id + self.name = name + self.created = created + self.links = links + + +class Result(wtypes.DynamicBase): + """class method json headers.""" + + group = wsme.wsattr(OutputResource, mandatory=True) + + def __init__(self, group=OutputResource()): + """init dunction. + + :param group: The created group + """ + self.group = group + + +class GroupsController(rest.RestController): + """controller get resource.""" + + @wsexpose(Groups, str, status_code=200, + rest_content_types='json') + def get(self, id=None): + """Handle get request. + + :param id: Group ID + :return: 200 OK on success, 404 Not Found otherwise. + """ + logger.info("Entered Get Group: id = {}".format(id)) + authentication.authorize(request, 'group:get_one') + + try: + + result = GroupService.get_groups_data(id) + logger.debug('Returning group, regions: {}'.format(result.regions)) + return result + + except error_base.NotFoundError as e: + logger.error("GroupsController - Group not found") + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=404) + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(GroupWrapper, status_code=200, rest_content_types='json') + def get_all(self): + logger.info("gett all groups") + authentication.authorize(request, 'group:get_all') + try: + + logger.debug("api-get all groups") + groups_wrraper = GroupService.get_all_groups() + logger.debug("got groups {}".format(groups_wrraper)) + + except Exception as exp: + logger.error("api--fail to get all groups") + logger.exception(exp) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + return groups_wrraper + + @wsexpose(Result, body=Groups, status_code=201, rest_content_types='json') + def post(self, group_input): + """Handle post request. + + :param group_input: json data + :return: 201 created on success, 409 otherwise. + """ + logger.info("Entered Create Group") + logger.debug("id = {}, name = {}, description = {}, regions = {}".format( + group_input.id, + group_input.name, + group_input.description, + group_input.regions)) + authentication.authorize(request, 'group:create') + + try: + # May raise an exception which will return status code 400 + GroupService.create_group_in_db(group_input.id, + group_input.name, + group_input.description, + group_input.regions) + logger.debug("Group created successfully in DB") + + # Create the group output data with the correct timestamp and link + group = OutputResource(group_input.id, + group_input.name, + repr(int(time.time() * 1000)), + {'self': '{}/v2/orm/groups/{}'.format( + request.application_url, + group_input.id)}) + + event_details = 'Region group {} {} created with regions: {}'.format( + group_input.id, group_input.name, group_input.regions) + utils.audit_trail('create group', request.transaction_id, + request.headers, group_input.id, + event_details=event_details) + return Result(group) + + except error_base.ErrorStatus as e: + logger.error("GroupsController - {}".format(e.message)) + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=e.status_code) + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(None, str, status_code=204, rest_content_types='json') + def delete(self, group_id): + logger.info("delete group") + authentication.authorize(request, 'group:delete') + + try: + + logger.debug("delete group with id {}".format(group_id)) + GroupService.delete_group(group_id) + logger.debug("done") + + event_details = 'Region group {} deleted'.format(group_id) + utils.audit_trail('delete group', request.transaction_id, + request.headers, group_id, + event_details=event_details) + + except Exception as exp: + + logger.exception("fail to delete group :- {}".format(exp)) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exp.message) + return + + @wsexpose(Result, str, body=Groups, status_code=201, + rest_content_types='json') + def put(self, group_id, group): + logger.info("update group") + authentication.authorize(request, 'group:update') + + try: + logger.debug("update group - id {}".format(group_id)) + result = GroupService.update_group(group, group_id) + logger.debug("group updated to :- {}".format(result)) + + # build result + group_result = OutputResource(result.id, result.name, + repr(int(time.time() * 1000)), { + 'self': '{}/v2/orm/groups/{}'.format( + request.application_url, + result.id)}) + + event_details = 'Region group {} {} updated with regions: {}'.format( + group_id, group.name, group.regions) + utils.audit_trail('update group', request.transaction_id, + request.headers, group_id, + event_details=event_details) + + except error_base.ErrorStatus as exp: + logger.error("group to update not found {}".format(exp)) + logger.exception(exp) + raise err_utils.get_error(request.transaction_id, + message=exp.message, + status_code=exp.status_code) + except Exception as exp: + logger.error("fail to update groupt -- id {}".format(group_id)) + logger.exception(exp) + raise + + return Result(group_result) diff --git a/orm/services/region_manager/rms/controllers/v2/orm/resources/metadata.py b/orm/services/region_manager/rms/controllers/v2/orm/resources/metadata.py new file mode 100755 index 00000000..604ffcfc --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/orm/resources/metadata.py @@ -0,0 +1,174 @@ +import json + +import logging + +from pecan import rest, request +import wsme +from wsme import types as wtypes +from wsmeext.pecan import wsexpose + +from rms.services import error_base +from rms.services import services as RegionService + +from rms.utils import authentication + +from orm_common.utils import api_error_utils as err_utils +from orm_common.utils import utils + +logger = logging.getLogger(__name__) + + +class MetaData(wtypes.DynamicBase): + """main json header.""" + + metadata = wsme.wsattr({str: [str]}, mandatory=True) + + def __init__(self, metadata={}): + """init function. + + :param regions: + :return: + """ + self.metadata = metadata + + +class RegionMetadataController(rest.RestController): + + @wsexpose(MetaData, str, status_code=200, rest_content_types='json') + def get(self, region_id): + logger.info("Get metadata for region id: {}".format(region_id)) + authentication.authorize(request, 'metadata:get') + + try: + region = RegionService.get_region_by_id_or_name(region_id) + logger.debug("Got region metadata: {}".format(region.metadata)) + return MetaData(region.metadata) + + except error_base.ErrorStatus as exp: + logger.error("RegionsController - {}".format(exp.message)) + raise err_utils.get_error(request.transaction_id, + message=exp.message, + status_code=exp.status_code) + except Exception as exp: + logger.exception(exp.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exp.message) + + @wsexpose(MetaData, str, body=MetaData, status_code=201, + rest_content_types='json') + def post(self, region_id, metadata_input): + """Handle post request. + :param region_id: region_id to add metadata to. + :param metadata_input: json data + :return: 201 created on success, 409 duplicate entry, 404 not found + """ + logger.info("Entered Create region metadata") + logger.debug("Got metadata: {}".format(metadata_input)) + authentication.authorize(request, 'metadata:create') + + try: + self._validate_request_input() + # May raise an exception which will return status code 400 + result = RegionService.add_region_metadata(region_id, + metadata_input.metadata) + logger.debug("Metadata was successfully added to " + "region: {}. New metadata: {}".format(region_id, result)) + + event_details = 'Region {} metadata added'.format(region_id) + utils.audit_trail('create metadata', request.transaction_id, + request.headers, region_id, + event_details=event_details) + return MetaData(result) + + except error_base.ErrorStatus as e: + logger.error(e.message) + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=e.status_code) + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(MetaData, str, body=MetaData, status_code=201, + rest_content_types='json') + def put(self, region_id, metadata_input): + """Handle put request. + :param region_id: region_id to update metadata to. + :param metadata_input: json data + :return: 201 created on success, 404 not found + """ + logger.info("Entered update region metadata") + logger.debug("Got metadata: {}".format(metadata_input)) + authentication.authorize(request, 'metadata:update') + + try: + self._validate_request_input() + # May raise an exception which will return status code 400 + result = RegionService.update_region_metadata(region_id, + metadata_input.metadata) + logger.debug("Metadata was successfully added to " + "region: {}. New metadata: {}".format(region_id, result)) + + event_details = 'Region {} metadata updated'.format(region_id) + utils.audit_trail('update metadata', request.transaction_id, + request.headers, region_id, + event_details=event_details) + return MetaData(result) + + except error_base.ErrorStatus as e: + logger.error(e.message) + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=e.status_code) + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(None, str, str, status_code=204, rest_content_types='json') + def delete(self, region_id, metadata_key): + """Handle delete request. + :param region_id: region_id to update metadata to. + :param metadata_key: metadata key to be deleted + :return: 204 deleted + """ + logger.info("Entered delete region metadata with " + "key: {}".format(metadata_key)) + authentication.authorize(request, 'metadata:delete') + + try: + # May raise an exception which will return status code 400 + result = RegionService.delete_metadata_from_region(region_id, + metadata_key) + logger.debug("Metadata was successfully deleted.") + + event_details = 'Region {} metadata {} deleted'.format( + region_id, metadata_key) + utils.audit_trail('delete metadata', request.transaction_id, + request.headers, region_id, + event_details=event_details) + + except error_base.ErrorStatus as e: + logger.error(e.message) + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=e.status_code) + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + def _validate_request_input(self): + data_dict = json.loads(request.body) + logger.debug("Got {}".format(data_dict)) + for k, v in data_dict['metadata'].iteritems(): + if isinstance(v, basestring): + logger.error("Invalid json. value type list is expected, " + "received string, for metadata key {}".format(k)) + raise error_base.ErrorStatus(400, "Invalid json. Expecting list " + "of metadata values, got string") diff --git a/orm/services/region_manager/rms/controllers/v2/orm/resources/regions.py b/orm/services/region_manager/rms/controllers/v2/orm/resources/regions.py new file mode 100755 index 00000000..15f57e6e --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/orm/resources/regions.py @@ -0,0 +1,344 @@ +"""rest module.""" +import logging + +from pecan import rest, request +import wsme +from wsme import types as wtypes +from wsmeext.pecan import wsexpose + +from rms.model import url_parm +from rms.model import model as PythonModel +from rms.services import error_base +from rms.services import services as RegionService + +from rms.controllers.v2.orm.resources.metadata import RegionMetadataController +from rms.controllers.v2.orm.resources.status import RegionStatusController + +from rms.utils import authentication + +from orm_common.policy import policy +from orm_common.utils import api_error_utils as err_utils +from orm_common.utils import utils + +logger = logging.getLogger(__name__) + + +class Address(wtypes.DynamicBase): + """wsme class for address json.""" + + country = wsme.wsattr(wtypes.text, mandatory=True) + state = wsme.wsattr(wtypes.text, mandatory=True) + city = wsme.wsattr(wtypes.text, mandatory=True) + street = wsme.wsattr(wtypes.text, mandatory=True) + zip = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, country=None, state=None, city=None, + street=None, zip=None): + """ + + :param country: + :param state: + :param city: + :param street: + :param zip: + """ + self.country = country + self.state = state + self.city = city + self.street = street + self.zip = zip + + def _to_clean_python_obj(self): + obj = PythonModel.Address() + obj.country = self.country + obj.state = self.state + obj.city = self.city + obj.street = self.street + obj.zip = self.zip + return obj + + +class EndPoint(wtypes.DynamicBase): + """class method endpoints body.""" + + publicurl = wsme.wsattr(wtypes.text, mandatory=True, name="publicURL") + type = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, publicurl=None, type=None): + """init function. + + :param publicURL: field + :param typee: field + :return: + """ + self.type = type + self.publicurl = publicurl + + def _to_clean_python_obj(self): + obj = PythonModel.EndPoint() + obj.publicurl = self.publicurl + obj.type = self.type + return obj + + +class RegionsData(wtypes.DynamicBase): + """class method json header.""" + + status = wsme.wsattr(wtypes.text, mandatory=True) + id = wsme.wsattr(wtypes.text, mandatory=True) + name = wsme.wsattr(wtypes.text, mandatory=False) + ranger_agent_version = wsme.wsattr(wtypes.text, mandatory=True, name="rangerAgentVersion") + open_stack_version = wsme.wsattr(wtypes.text, mandatory=True, name="OSVersion") + clli = wsme.wsattr(wtypes.text, mandatory=True, name="CLLI") + metadata = wsme.wsattr({str: [str]}, mandatory=True) + endpoints = wsme.wsattr([EndPoint], mandatory=True) + address = wsme.wsattr(Address, mandatory=True) + design_type = wsme.wsattr(wtypes.text, mandatory=True, name="designType") + location_type = wsme.wsattr(wtypes.text, mandatory=True, name="locationType") + vlcp_name = wsme.wsattr(wtypes.text, mandatory=True, name="vlcpName") + + def __init__(self, status=None, id=None, name=None, clli=None, design_type=None, + location_type=None, vlcp_name=None, open_stack_version=None, + address=Address(), ranger_agent_version=None, metadata={}, + endpoint=[EndPoint()]): + """ + + :param status: + :param id: + :param name: + :param clli: + :param design_type: + :param location_type: + :param vlcp_name: + :param open_stack_version: + :param address: + :param ranger_agent_version: + :param metadata: + :param endpoint: + """ + self.status = status + self.id = id + self.name = self.id + self.clli = clli + self.ranger_agent_version = ranger_agent_version + self.metadata = metadata + self.endpoint = endpoint + self.design_type = design_type + self.location_type = location_type + self.vlcp_name = vlcp_name + self.address = address + self.open_stack_version = open_stack_version + + def _to_clean_python_obj(self): + obj = PythonModel.RegionData() + obj.endpoints = [] + obj.status = self.status + obj.id = self.id + obj.name = self.id + obj.ranger_agent_version = self.ranger_agent_version + obj.clli = self.clli + obj.metadata = self.metadata + for endpoint in self.endpoints: + obj.endpoints.append(endpoint._to_clean_python_obj()) + obj.address = self.address._to_clean_python_obj() + obj.design_type = self.design_type + obj.location_type = self.location_type + obj.vlcp_name = self.vlcp_name + obj.open_stack_version = self.open_stack_version + return obj + + +class Regions(wtypes.DynamicBase): + """main json header.""" + + regions = wsme.wsattr([RegionsData], mandatory=True) + + def __init__(self, regions=[RegionsData()]): + """init function. + + :param regions: + :return: + """ + self.regions = regions + + +class RegionsController(rest.RestController): + """controller get resource.""" + metadata = RegionMetadataController() + status = RegionStatusController() + + @wsexpose(Regions, str, str, [str], str, str, str, str, str, str, str, + str, str, str, status_code=200, rest_content_types='json') + def get_all(self, type=None, status=None, metadata=None, rangerAgentVersion=None, + clli=None, regionname=None, osversion=None, valet=None, + state=None, country=None, city=None, street=None, zip=None): + """get regions. + + :param type: query field + :param status: query field + :param metadata: query field + :param rangerAgentVersion: query field + :param clli: query field + :param regionname: query field + :param osversion: query field + :param valet: query field + :param state: query field + :param country: query field + :param city: query field + :param street: query field + :param zip: query field + :return: json from db + :exception: EntityNotFoundError 404 + """ + logger.info("Entered Get Regions") + authentication.authorize(request, 'region:get_all') + + url_args = {'type': type, 'status': status, 'metadata': metadata, + 'rangerAgentVersion': rangerAgentVersion, 'clli': clli, 'regionname': regionname, + 'osversion': osversion, 'valet': valet, 'state': state, + 'country': country, 'city': city, 'street': street, 'zip': zip} + logger.debug("Parameters: {}".format(str(url_args))) + + try: + url_args = url_parm.UrlParms(**url_args) + + result = RegionService.get_regions_data(url_args) + + logger.debug("Returning regions: {}".format(', '.join( + [region.name for region in result.regions]))) + + return result + + except error_base.ErrorStatus as e: + logger.error("RegionsController {}".format(e.message)) + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=e.status_code) + + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + message=exception.message) + + @wsexpose(RegionsData, str, status_code=200, rest_content_types='json') + def get_one(self, id_or_name): + logger.info("API: Entered get region by id or name: {}".format(id_or_name)) + authentication.authorize(request, 'region:get_one') + + try: + result = RegionService.get_region_by_id_or_name(id_or_name) + logger.debug("API: Got region {} success: {}".format(id_or_name, result)) + except error_base.ErrorStatus as exp: + logger.error("RegionsController {}".format(exp.message)) + raise err_utils.get_error(request.transaction_id, + message=exp.message, + status_code=exp.status_code) + except Exception as exp: + logger.exception(exp.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exp.message) + + return result + + @wsexpose(RegionsData, body=RegionsData, status_code=201, rest_content_types='json') + def post(self, full_region_input): + logger.info("API: CreateRegion") + authentication.authorize(request, 'region:create') + + try: + logger.debug("API: create region .. data = : {}".format(full_region_input)) + result = RegionService.create_full_region(full_region_input) + logger.debug("API: region created : {}".format(result)) + + event_details = 'Region {} {} created: AICversion {}, OSversion {}, CLLI {}'.format( + full_region_input.name, full_region_input.design_type, + full_region_input.ranger_agent_version, + full_region_input.open_stack_version, full_region_input.clli) + utils.audit_trail('create region', request.transaction_id, + request.headers, full_region_input.id, + event_details=event_details) + except error_base.InputValueError as exp: + logger.exception("Error in save region {}".format(exp.message)) + raise err_utils.get_error(request.transaction_id, + status_code=exp.status_code, + message=exp.message) + + except error_base.ConflictError as exp: + logger.exception("Conflict error {}".format(exp.message)) + raise err_utils.get_error(request.transaction_id, + message=exp.message, + status_code=exp.status_code) + + except Exception as exp: + logger.exception("Error in creating region .. reason:- {}".format(exp)) + raise err_utils.get_error(request.transaction_id, + status_code=500, + message=exp.message) + + return result + + @wsexpose(None, str, rest_content_types='json', status_code=204) + def delete(self, region_id): + logger.info("Delete Region") + authentication.authorize(request, 'region:delete') + + try: + + logger.debug("delete region {}".format(region_id)) + result = RegionService.delete_region(region_id) + logger.debug("region deleted") + + event_details = 'Region {} deleted'.format(region_id) + utils.audit_trail('delete region', request.transaction_id, + request.headers, region_id, + event_details=event_details) + + except Exception as exp: + logger.exception( + "error in deleting region .. reason:- {}".format(exp)) + raise err_utils.get_error(request.transaction_id, + status_code=500, + message=exp.message) + return + + @wsexpose(RegionsData, str, body=RegionsData, status_code=201, + rest_content_types='json') + def put(self, region_id, region): + logger.info("API: update region") + authentication.authorize(request, 'region:update') + + try: + + logger.debug( + "region to update {} with{}".format(region_id, region)) + result = RegionService.update_region(region_id, region) + logger.debug("API: region {} updated".format(region_id)) + + event_details = 'Region {} {} modified: AICversion {}, OSversion {}, CLLI {}'.format( + region.name, region.design_type, region.ranger_agent_version, + region.open_stack_version, region.clli) + utils.audit_trail('update region', request.transaction_id, + request.headers, region_id, + event_details=event_details) + + except error_base.NotFoundError as exp: + logger.exception("region {} not found".format(region_id)) + raise err_utils.get_error(request.transaction_id, + status_code=exp.status_code, + message=exp.message) + + except error_base.InputValueError as exp: + logger.exception("not valid input {}".format(exp.message)) + raise err_utils.get_error(request.transaction_id, + status_code=exp.status_code, + message=exp.message) + except Exception as exp: + logger.exception( + "API: error in updating region {}.. reason:- {}".format(region_id, + exp)) + raise err_utils.get_error(request.transaction_id, + status_code=500, + message=exp.message) + return result diff --git a/orm/services/region_manager/rms/controllers/v2/orm/resources/status.py b/orm/services/region_manager/rms/controllers/v2/orm/resources/status.py new file mode 100755 index 00000000..85a877c4 --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/orm/resources/status.py @@ -0,0 +1,105 @@ +import logging + +import pecan +from pecan import rest, request, conf + +import wsme +from wsme import types as wtypes +from wsmeext.pecan import wsexpose + +from orm_common.utils import api_error_utils as err_utils +from orm_common.utils import utils + +from rms.services import error_base +from rms.services import services as RegionService +from rms.utils import authentication + + +logger = logging.getLogger(__name__) + + +class RegionStatus(wtypes.DynamicBase): + """main json header.""" + + status = wsme.wsattr(str, mandatory=True) + links = wsme.wsattr({str: str}, mandatory=False) + + def __init__(self, status=None, links=None): + """ + RegionStatus wrapper + :param status: + """ + self.status = status + self.links = links + + +class RegionStatusController(rest.RestController): + + @wsexpose(RegionStatus, str, body=RegionStatus, status_code=201, + rest_content_types='json') + def put(self, region_id, new_status): + """ + Handle put request to modify region status + :param region_id: + :param new_status: + :return: 200 for updated, 404 for region not found + 400 invalid status + """ + logger.info("Entered update region status") + logger.debug("Got status: {}".format(new_status.status)) + + authentication.authorize(request, 'status:put') + + try: + allowed_status = conf.region_options.allowed_status_values[:] + + if new_status.status not in allowed_status: + logger.error("Invalid status. Region status " + "must be one of {}".format(allowed_status)) + raise error_base.InputValueError( + message="Invalid status. Region status " + "must be one of {}".format(allowed_status)) + + # May raise an exception which will return status code 400 + status = RegionService.update_region_status(region_id, new_status.status) + base_link = 'https://{0}:{1}{2}'.format(conf.server.host, conf.server.port, + pecan.request.path) + link = {'self': base_link} + + logger.debug("Region status for region id {}, was successfully " + "changed to: {}.".format(region_id, new_status.status)) + + event_details = 'Region {} status updated to {}'.format( + region_id, new_status.status) + utils.audit_trail('Update status', request.transaction_id, + request.headers, region_id, + event_details=event_details) + + return RegionStatus(status, link) + + except error_base.ErrorStatus as e: + logger.error(e.message) + raise err_utils.get_error(request.transaction_id, + message=e.message, + status_code=e.status_code) + except Exception as exception: + logger.error(exception.message) + raise err_utils.get_error(request.transaction_id, + status_code=500, + error_details=exception.message) + + @wsexpose(str, str, rest_content_types='json') + def get(self, region_id): + raise err_utils.get_error(request.transaction_id, + status_code=405) + + @wsexpose(RegionStatus, str, body=RegionStatus, status_code=200, + rest_content_types='json') + def post(self, region_id, status): + raise err_utils.get_error(request.transaction_id, + status_code=405) + + @wsexpose(str, str, rest_content_types='json') + def delete(self, region_id): + raise err_utils.get_error(request.transaction_id, + status_code=405) diff --git a/orm/services/region_manager/rms/controllers/v2/orm/root.py b/orm/services/region_manager/rms/controllers/v2/orm/root.py new file mode 100755 index 00000000..c23aee09 --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/orm/root.py @@ -0,0 +1,10 @@ +"""ORM controller module.""" +from rms.controllers.v2.orm.resources import groups +from rms.controllers.v2.orm.resources import regions + + +class OrmController(object): + """ORM controller class.""" + + regions = regions.RegionsController() + groups = groups.GroupsController() diff --git a/orm/services/region_manager/rms/controllers/v2/root.py b/orm/services/region_manager/rms/controllers/v2/root.py new file mode 100755 index 00000000..45108cd1 --- /dev/null +++ b/orm/services/region_manager/rms/controllers/v2/root.py @@ -0,0 +1,8 @@ +"""V2 root controller module.""" +from rms.controllers.v2.orm import root + + +class V2Controller(object): + """V2 root controller class.""" + + orm = root.OrmController() diff --git a/orm/services/region_manager/rms/etc/policy.json b/orm/services/region_manager/rms/etc/policy.json new file mode 100755 index 00000000..753964ee --- /dev/null +++ b/orm/services/region_manager/rms/etc/policy.json @@ -0,0 +1,24 @@ +{ + "default": "!", + "orm_admin": "role:admin and tenant:admin", + + "lcp:get_one": "", + "lcp:get_all": "", + "region:get_one": "", + "region:get_all": "", + "region:create": "rule:orm_admin", + "region:update": "rule:orm_admin", + "region:delete": "rule:orm_admin", + "group:get_one": "", + "group:get_all": "", + "group:create": "rule:orm_admin", + "group:update": "rule:orm_admin", + "group:delete": "rule:orm_admin", + "configuration:get": "rule:orm_admin", + "log:update": "rule:orm_admin", + "metadata:get": "rule:orm_admin", + "metadata:create": "rule:orm_admin", + "metadata:update": "rule:orm_admin", + "metadata:delete": "rule:orm_admin", + "status:put": "rule:orm_admin" +} \ No newline at end of file diff --git a/orm/services/region_manager/rms/external_mock/__init__.py b/orm/services/region_manager/rms/external_mock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/external_mock/audit_client/__init__.py b/orm/services/region_manager/rms/external_mock/audit_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/external_mock/audit_client/api/__init__.py b/orm/services/region_manager/rms/external_mock/audit_client/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/external_mock/audit_client/api/audit.py b/orm/services/region_manager/rms/external_mock/audit_client/api/audit.py new file mode 100644 index 00000000..ec483bdd --- /dev/null +++ b/orm/services/region_manager/rms/external_mock/audit_client/api/audit.py @@ -0,0 +1,6 @@ +def audit(*args, **kwargs): + pass + + +def init(*args, **kwargs): + pass diff --git a/orm/services/region_manager/rms/external_mock/keystone_utils/__init__.py b/orm/services/region_manager/rms/external_mock/keystone_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/external_mock/keystone_utils/tokens.py b/orm/services/region_manager/rms/external_mock/keystone_utils/tokens.py new file mode 100644 index 00000000..52ef992f --- /dev/null +++ b/orm/services/region_manager/rms/external_mock/keystone_utils/tokens.py @@ -0,0 +1,7 @@ +def get_token_user(*a, **k): + pass + + +class TokenConf(object): + def __init__(self, *a, **kw): + pass diff --git a/orm/services/region_manager/rms/external_mock/orm_common/__init__.py b/orm/services/region_manager/rms/external_mock/orm_common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/external_mock/orm_common/policy/__init__.py b/orm/services/region_manager/rms/external_mock/orm_common/policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/external_mock/orm_common/policy/policy.py b/orm/services/region_manager/rms/external_mock/orm_common/policy/policy.py new file mode 100644 index 00000000..2953f204 --- /dev/null +++ b/orm/services/region_manager/rms/external_mock/orm_common/policy/policy.py @@ -0,0 +1,10 @@ +def init(*a, **kw): + pass + + +def enforce(*a, **kw): + pass + + +def authorize(*a, **kw): + pass diff --git a/orm/services/region_manager/rms/external_mock/orm_common/utils/__init__.py b/orm/services/region_manager/rms/external_mock/orm_common/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/external_mock/orm_common/utils/api_error_utils.py b/orm/services/region_manager/rms/external_mock/orm_common/utils/api_error_utils.py new file mode 100644 index 00000000..653221e2 --- /dev/null +++ b/orm/services/region_manager/rms/external_mock/orm_common/utils/api_error_utils.py @@ -0,0 +1,2 @@ +def get_error(*args, **kwargs): + pass diff --git a/orm/services/region_manager/rms/external_mock/orm_common/utils/utils.py b/orm/services/region_manager/rms/external_mock/orm_common/utils/utils.py new file mode 100755 index 00000000..bf615fea --- /dev/null +++ b/orm/services/region_manager/rms/external_mock/orm_common/utils/utils.py @@ -0,0 +1,15 @@ +"""Utils module mock.""" + + +def report_config(conf, dump=False): + """Mock report_config function.""" + pass + + +def set_utils_conf(conf): + """Mock set_utils_conf function.""" + pass + + +def audit_trail(*args, **kwargs): + pass diff --git a/orm/services/region_manager/rms/logger/__init__.py b/orm/services/region_manager/rms/logger/__init__.py new file mode 100644 index 00000000..1894cbbe --- /dev/null +++ b/orm/services/region_manager/rms/logger/__init__.py @@ -0,0 +1,10 @@ +import logging + + +def get_logger(name): + logger = logging.getLogger(name) + logger.log_exception = lambda msg, exception: logger.exception(msg + " Exception: " + str(exception)) + + return logger + +__all__ = ['get_logger'] diff --git a/orm/services/region_manager/rms/model/__init__.py b/orm/services/region_manager/rms/model/__init__.py new file mode 100644 index 00000000..d983f7bc --- /dev/null +++ b/orm/services/region_manager/rms/model/__init__.py @@ -0,0 +1,15 @@ +from pecan import conf # noqa + + +def init_model(): + """ + This is a stub method which is called at application startup time. + + If you need to bind to a parsed database configuration, set up tables or + ORM classes, or perform any database initialization, this is the + recommended place to do it. + + For more information working with databases, and some common recipes, + see http://pecan.readthedocs.org/en/latest/databases.html + """ + pass diff --git a/orm/services/region_manager/rms/model/model.py b/orm/services/region_manager/rms/model/model.py new file mode 100755 index 00000000..b0875966 --- /dev/null +++ b/orm/services/region_manager/rms/model/model.py @@ -0,0 +1,183 @@ +"""model module.""" +from rms.services import error_base +from pecan import conf + + +class Address(object): + """address class.""" + + def __init__(self, country=None, state=None, city=None, + street=None, zip=None): + """ + + :param country: + :param state: + :param city: + :param street: + :param zip: + """ + self.country = country + self.state = state + self.city = city + self.street = street + self.zip = zip + + +class EndPoint(object): + """class method endpoints body.""" + + def __init__(self, publicurl=None, type=None): + """init function. + + :param public_url: field + :param type: field + :return: + """ + self.type = type + self.publicurl = publicurl + + +class RegionData(object): + """class method json header.""" + + def __init__(self, status=None, id=None, name=None, clli=None, + ranger_agent_version=None, design_type=None, location_type=None, + vlcp_name=None, open_stack_version=None, + address=Address(), metadata={}, endpoints=[EndPoint()]): + """ + + :param status: + :param id: + :param name: + :param clli: + :param ranger_agent_version: + :param design_type: + :param location_type: + :param vlcp_name: + :param open_stack_version: + :param address: + :param metadata: + :param endpoints: + """ + self.status = status + self.id = id + # make id and name always the same + self.name = self.id + self.clli = clli + self.ranger_agent_version = ranger_agent_version + self.metadata = metadata + self.endpoints = endpoints + self.design_type = design_type + self.location_type = location_type + self.vlcp_name = vlcp_name + self.open_stack_version = open_stack_version + self.address = address + + def _validate_end_points(self, endpoints_types_must_have): + ep_duplicate = [] + for endpoint in self.endpoints: + if endpoint.type not in ep_duplicate: + ep_duplicate.append(endpoint.type) + else: + raise error_base.InputValueError( + message="Invalid endpoints. Duplicate endpoint " + "type {}".format(endpoint.type)) + try: + endpoints_types_must_have.remove(endpoint.type) + except: + pass + if len(endpoints_types_must_have) > 0: + raise error_base.InputValueError( + message="Invalid endpoints. Endpoint type '{}' " + "is missing".format(endpoints_types_must_have)) + + def _validate_status(self, allowed_status): + if self.status not in allowed_status: + raise error_base.InputValueError( + message="Invalid status. Region status must be " + "one of {}".format(allowed_status)) + return + + def _validate_model(self): + allowed_status = conf.region_options.allowed_status_values[:] + endpoints_types_must_have = conf.region_options.endpoints_types_must_have[:] + self._validate_status(allowed_status) + self._validate_end_points(endpoints_types_must_have) + return + + def _to_db_model_dict(self): + end_points = [] + + for endpoint in self.endpoints: + ep = {} + ep['type'] = endpoint.type + ep['url'] = endpoint.publicurl + end_points.append(ep) + + db_model_dict = {} + db_model_dict['region_id'] = self.id + db_model_dict['name'] = self.name + db_model_dict['address_state'] = self.address.state + db_model_dict['address_country'] = self.address.country + db_model_dict['address_city'] = self.address.city + db_model_dict['address_street'] = self.address.street + db_model_dict['address_zip'] = self.address.zip + db_model_dict['region_status'] = self.status + db_model_dict['ranger_agent_version'] = self.ranger_agent_version + db_model_dict['open_stack_version'] = self.open_stack_version + db_model_dict['design_type'] = self.design_type + db_model_dict['location_type'] = self.location_type + db_model_dict['vlcp_name'] = self.vlcp_name + db_model_dict['clli'] = self.clli + db_model_dict['end_point_list'] = end_points + db_model_dict['meta_data_dict'] = self.metadata + return db_model_dict + + +class Regions(object): + """main json header.""" + + def __init__(self, regions=[RegionData()]): + """init function. + + :param regions: + :return: + """ + self.regions = regions + + +class Groups(object): + """main json header.""" + + def __init__(self, id=None, name=None, + description=None, regions=[]): + """init function. + + :param regions: + :return: + """ + self.id = id + self.name = name + self.description = description + self.regions = regions + + def _to_db_model_dict(self): + db_dict = {} + db_dict['group_name'] = self.name + db_dict['group_description'] = self.description + db_dict['group_regions'] = self.regions + return db_dict + + +class GroupsWrraper(object): + """list of groups.""" + + def __init__(self, groups=None): + """ + + :param groups: + """ + if groups is None: + self.groups = [] + else: + self.groups = groups diff --git a/orm/services/region_manager/rms/model/url_parm.py b/orm/services/region_manager/rms/model/url_parm.py new file mode 100755 index 00000000..1bfe86cf --- /dev/null +++ b/orm/services/region_manager/rms/model/url_parm.py @@ -0,0 +1,102 @@ +"""module.""" + + +class UrlParms(object): + """class method.""" + + def __init__(self, type=None, status=None, metadata=None, rangerAgentVersion=None, + clli=None, regionname=None, osversion=None, valet=None, + state=None, country=None, city=None, street=None, zip=None): + """init method. + + :param type: + :param status: + :param metadata: + :param rangerAgentVersion: + :param clli: + :param regionname: + :param osversion: + :param valet: + :param state: + :param country: + :param city: + :param street: + :param zip: + """ + if type: + self.location_type = type + if status: + self.region_status = status + if metadata: + self.metadata = metadata + if rangerAgentVersion: + self.ranger_agent_version = rangerAgentVersion + if clli: + self.clli = clli + if regionname: + self.name = regionname + if osversion: + self.open_stack_version = osversion + if valet: + self.valet = valet + if state: + self.address_state = state + if country: + self.address_country = country + if city: + self.address_city = city + if street: + self.address_street = street + if zip: + self.address_zip = zip + + def _build_query(self): + """nuild db query. + + :return: + """ + metadatadict = None + regiondict = None + if self.__dict__: + metadatadict = self._build_metadata_dict() + regiondict = self._build_region_dict() + return regiondict, metadatadict, None + + def _build_metadata_dict(self): + """meta_data dict. + + :return: metadata dict + """ + metadata = None + if 'metadata' in self.__dict__: + metadata = {'ref_keys': [], 'meta_data_pairs': [], + 'meta_data_keys': []} + for metadata_item in self.metadata: + if ':' in metadata_item: + key = metadata_item.split(':')[0] + metadata['ref_keys'].append(key) + metadata['meta_data_pairs'].\ + append({'metadata_key': key, + 'metadata_value': metadata_item.split(':')[1]}) + else: + metadata['meta_data_keys'].append(metadata_item) + # Now clean irrelevant values + keys_list = [] + for item in metadata['meta_data_keys']: + if item not in metadata['ref_keys']: + keys_list.append(item) + + metadata['meta_data_keys'] = keys_list + + return metadata + + def _build_region_dict(self): + """region dict. + + :return:regin dict + """ + regiondict = {} + for key, value in self.__dict__.items(): + if key != 'metadata': + regiondict[key] = value + return regiondict diff --git a/orm/services/region_manager/rms/resources/regions.csv b/orm/services/region_manager/rms/resources/regions.csv new file mode 100644 index 00000000..b63a9e01 --- /dev/null +++ b/orm/services/region_manager/rms/resources/regions.csv @@ -0,0 +1,4 @@ +region_id,region_name,address_state,address_country,address_city,address_street,address_zip,region_status,ranger_agent_version,open_stack_version,design_type,location_type,vlcp_name,clli,description,keystone_url,horizon_url,ord_url +SNA1,SNA 1,CAL,US,LA,n/a,1111,functional,ranger_agent1.0,kilo,n/a,n/a,n/a,n/a,SNA1 lcp in CAL,http://identity1.com,http://horizon1.com,http://ord1.com +SNA2,SNA 2,NY,US,NY City,n/a,2222,functional,ranger_agent1.5,kilo,n/a,n/a,n/a,n/a,SNA2 lcp in NYC,http://identity2.com,http://horizon2.com,http://ord2.com +DPK,DPK,NY,US,FIL,n/a,3333,functional,ranger_agent1.0,kilo,n/a,n/a,n/a,n/a,DPK lcp in FIL,http://identity3.com,http://horizon3.com,http://ord3.com \ No newline at end of file diff --git a/orm/services/region_manager/rms/services/__init__.py b/orm/services/region_manager/rms/services/__init__.py new file mode 100644 index 00000000..b57327ba --- /dev/null +++ b/orm/services/region_manager/rms/services/__init__.py @@ -0,0 +1 @@ +"""services package.""" diff --git a/orm/services/region_manager/rms/services/error_base.py b/orm/services/region_manager/rms/services/error_base.py new file mode 100755 index 00000000..162ecba5 --- /dev/null +++ b/orm/services/region_manager/rms/services/error_base.py @@ -0,0 +1,33 @@ +"""Exceptions module.""" + + +class Error(Exception): + pass + + +class ErrorStatus(Error): + + def __init__(self, status_code, message=""): + self.status_code = status_code + self.message = message + + +class NotFoundError(ErrorStatus): + + def __init__(self, status_code=404, message="Not found"): + self.status_code = status_code + self.message = message + + +class ConflictError(ErrorStatus): + + def __init__(self, status_code=409, message="Conflict error"): + self.status_code = status_code + self.message = message + + +class InputValueError(ErrorStatus): + + def __init__(self, status_code=400, message="value not allowed"): + self.status_code = status_code + self.message = message diff --git a/orm/services/region_manager/rms/services/services.py b/orm/services/region_manager/rms/services/services.py new file mode 100755 index 00000000..2ba2cd92 --- /dev/null +++ b/orm/services/region_manager/rms/services/services.py @@ -0,0 +1,286 @@ +"""DB actions wrapper module.""" +import logging +from rms.model.model import Groups +from rms.model.model import Regions +from rms.services import error_base +from rms.storage import base_data_manager +from rms.storage import data_manager_factory + +LOG = logging.getLogger(__name__) + + +def get_regions_data(url_parms): + """get region from db. + + :param url_parms: the parameters got in the url to make the query + :return: region model for json output + :raise: NoContentError( status code 404) + """ + region_dict, metadata_dict, end_point = url_parms._build_query() + db = data_manager_factory.get_data_manager() + regions = db.get_regions(region_dict, metadata_dict, end_point) + if not regions: + raise error_base.NotFoundError(message="No regions found for the given search parameters") + return Regions(regions) + + +def get_region_by_id_or_name(region_id_or_name): + """ + + :param region_id_or_name: + :return: region object (wsme format) + """ + LOG.debug("LOGIC:- get region data by id or name {}".format(region_id_or_name)) + try: + db = data_manager_factory.get_data_manager() + region = db.get_region_by_id_or_name(region_id_or_name) + + if not region: + raise error_base.NotFoundError(message="Region {} not found".format(region_id_or_name)) + + except Exception as exp: + LOG.exception("error in get region by id/name") + raise + + return region + + +def update_region(region_id, region): + """ + :param region: + :return: + """ + LOG.debug("logic:- update region {}".format(region)) + try: + + region = region._to_clean_python_obj() + region._validate_model() + region_dict = region._to_db_model_dict() + + db = data_manager_factory.get_data_manager() + db.update_region(region_to_update=region_id, **region_dict) + LOG.debug("region {} updated".format(region_id)) + result = get_region_by_id_or_name(region_id) + + except error_base.NotFoundError as exp: + LOG.exception("fail to update region {}".format(exp.message)) + raise + except Exception as exp: + LOG.exception("fail to update region {}".format(exp)) + raise + return result + + +def delete_region(region_id): + """ + + :param region_id: + :return: + """ + LOG.debug("logic:- delete region {}".format(region_id)) + try: + db = data_manager_factory.get_data_manager() + db.delete_region(region_id) + LOG.debug("region deleted") + except Exception as exp: + LOG.exception("fail to delete region {}".format(exp)) + raise + return + + +def create_full_region(full_region): + """create region logic. + + :param full_region obj: + :return: + :raise: input value error(status code 400) + """ + LOG.debug("logic:- save region ") + try: + + full_region = full_region._to_clean_python_obj() + full_region._validate_model() + + full_region_db_dict = full_region._to_db_model_dict() + LOG.debug("region to save {}".format(full_region_db_dict)) + db = data_manager_factory.get_data_manager() + db.add_region(**full_region_db_dict) + LOG.debug("region added") + result = get_region_by_id_or_name(full_region.id) + + except error_base.InputValueError as exp: + LOG.exception("error in save region {}".format(exp.message)) + raise + except base_data_manager.DuplicateEntryError as exp: + LOG.exception("error in save region {}".format(exp.message)) + raise error_base.ConflictError(message=exp.message) + except Exception as exp: + LOG.exception("error in save region {}".format(exp.message)) + raise + + return result + + +def add_region_metadata(region_id, metadata_dict): + LOG.debug("Add metadata: {} to region id : {}".format(metadata_dict, + region_id)) + try: + db = data_manager_factory.get_data_manager() + result = db.add_meta_data_to_region(region_id, metadata_dict) + if not result: + raise error_base.NotFoundError(message="Region {} not found".format(region_id)) + else: + return result.metadata + + except Exception as exp: + LOG.exception("Error getting metadata for region id:".format(region_id)) + raise + + +def update_region_metadata(region_id, metadata_dict): + LOG.debug("Update metadata to region id : {}. " + "New metadata: {}".format(region_id, metadata_dict)) + try: + db = data_manager_factory.get_data_manager() + result = db.update_region_meta_data(region_id, metadata_dict) + if not result: + raise error_base.NotFoundError(message="Region {} not " + "found".format(region_id)) + else: + return result.metadata + + except Exception as exp: + LOG.exception("Error getting metadata for region id:".format(region_id)) + raise + + +def delete_metadata_from_region(region_id, metadata_key): + LOG.info("Delete metadata key: {} from region id : {}." + .format(metadata_key, region_id)) + try: + db = data_manager_factory.get_data_manager() + db.delete_region_metadata(region_id, metadata_key) + + except Exception as exp: + LOG.exception("Error getting metadata for region id:".format(region_id)) + raise + + +def get_groups_data(name): + """get group from db. + + :param name: groupe name + :return: groupe object with its regions + :raise: NoContentError( status code 404) + """ + db = data_manager_factory.get_data_manager() + groups = db.get_group(name) + if not groups: + raise error_base.NotFoundError(message="Group {} not found".format(name)) + return Groups(**groups) + + +def get_all_groups(): + """ + + :return: + """ + try: + LOG.debug("logic - get all groups") + db = data_manager_factory.get_data_manager() + all_groups = db.get_all_groups() + LOG.debug("logic - got all groups {}".format(all_groups)) + + except Exception as exp: + LOG.error("fail to get all groups") + LOG.exception(exp) + raise + + return all_groups + + +def delete_group(group_id): + """ + + :param group_id: + :return: + """ + LOG.debug("delete group logic") + try: + + db = data_manager_factory.get_data_manager() + LOG.debug("delete group id {} from db".format(group_id)) + db.delete_group(group_id) + + except Exception as exp: + LOG.exception(exp) + raise + return + + +def create_group_in_db(group_id, group_name, description, regions): + """Create a region group in the database. + + :param group_id: The ID of the group to create + :param group_name: The name of the group to create + :param description: The group description + :param regions: A list of regions inside the group + :raise: GroupExistsError (status code 400) if the group already exists + """ + try: + manager = data_manager_factory.get_data_manager() + manager.add_group(group_id, group_name, description, regions) + except error_base.ConflictError: + LOG.exception("Group {} already exists".format(group_id)) + raise error_base.ConflictError( + message="Group {} already exists".format(group_id)) + except error_base.InputValueError: + LOG.exception("Some of the regions not found") + raise error_base.NotFoundError( + message="Some of the regions not found") + + +def update_group(group, group_id): + result = None + LOG.debug("update group logic") + try: + group = group._to_python_obj() + db_manager = data_manager_factory.get_data_manager() + LOG.debug("update group to {}".format(group._to_db_model_dict())) + db_manager.update_group(group_id=group_id, **group._to_db_model_dict()) + LOG.debug("group updated") + # make sure it updated + groups = db_manager.get_group(group_id) + + except error_base.NotFoundError: + LOG.error("Group {} not found") + raise + except error_base.InputValueError: + LOG.exception("Some of the regions not found") + raise error_base.NotFoundError( + message="Some of the regions not found") + except Exception as exp: + LOG.error("Failed to update group {}".format(group.group_id)) + LOG.exception(exp) + raise + + return Groups(**groups) + + +def update_region_status(region_id, new_status): + """Update region. + + :param region_id: + :param new_status: + :return: + """ + LOG.debug("Update region id: {} status to: {}".format(region_id, + new_status)) + try: + db = data_manager_factory.get_data_manager() + result = db.update_region_status(region_id, new_status) + return result + + except Exception as exp: + LOG.exception("Error updating status for region id:".format(region_id)) + raise diff --git a/orm/services/region_manager/rms/storage/__init__.py b/orm/services/region_manager/rms/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/storage/base_data_manager.py b/orm/services/region_manager/rms/storage/base_data_manager.py new file mode 100755 index 00000000..c1928854 --- /dev/null +++ b/orm/services/region_manager/rms/storage/base_data_manager.py @@ -0,0 +1,118 @@ + +class BaseDataManager(object): + + def __init__(self, url, + max_retries, + retry_interval): + pass + + def add_region(self, + region_id, + name, + address_state, + address_country, + address_city, + address_street, + address_zip, + region_status, + ranger_agent_version, + open_stack_version, + design_type, + location_type, + vlcp_name, + clli, + description, + meta_data_list, + end_point_list): + raise NotImplementedError("Please Implement this method") + + """ + def delete_region(self, + region_id): + raise NotImplementedError("Please Implement this method") + """ + + def get_regions(self, + region_filters_dict, + meta_data_dict, + end_point_dict): + raise NotImplementedError("Please Implement this method") + + def get_all_regions(self): + raise NotImplementedError("Please Implement this method") + + """ + def add_meta_data_to_region(self, + region_id, + key, + value, + description): + raise NotImplementedError("Please Implement this method") + + def remove_meta_data_from_region(self, + region_id, + key): + raise NotImplementedError("Please Implement this method") + + def add_end_point_to_region(self, + region_id, + end_point_type, + end_point_url, + description): + raise NotImplementedError("Please Implement this method") + + def remove_end_point_from_region(self, + region_id, + end_point_type): + raise NotImplementedError("Please Implement this method") + """ + + def add_group(self, + group_id, + group_name, + group_description, + region_ids_list): + raise NotImplementedError("Please Implement this method") + + """ + def delete_group(self, + group_name): + raise NotImplementedError("Please Implement this method") + """ + + def get_group(self, group_id): + raise NotImplementedError("Please Implement this method") + + def get_all_groups(self): + raise NotImplementedError("Please Implement this method") + + """ + def add_region_to_group(self, + group_id, + region_id): + raise NotImplementedError("Please Implement this method") + + def remove_region_from_group(self, + group_id, + region_id): + raise NotImplementedError("Please Implement this method") + """ + + +class SQLDBError(Exception): + pass + + +class EntityNotFound(Exception): + """if item not found in DB.""" + pass + + +class DuplicateEntryError(Exception): + """A group already exists.""" + pass + + +class InputValueError(Exception): + """ unvalid input from user""" + pass diff --git a/orm/services/region_manager/rms/storage/data_manager_factory.py b/orm/services/region_manager/rms/storage/data_manager_factory.py new file mode 100644 index 00000000..4e6e0477 --- /dev/null +++ b/orm/services/region_manager/rms/storage/data_manager_factory.py @@ -0,0 +1,20 @@ +import logging + +from pecan import conf + +from rms.storage.my_sql.data_manager import DataManager + +LOG = logging.getLogger(__name__) + + +def get_data_manager(): + try: + dm = DataManager(url=conf.database.url, + max_retries=conf.database.max_retries, + retries_interval=conf.database.retries_interval) + return dm + except Exception: + nagios_message = "CRITICAL|CONDB001 - Could not establish " \ + "database connection" + LOG.error(nagios_message) + raise Exception("Could not establish database connection") diff --git a/orm/services/region_manager/rms/storage/my_sql/__init__.py b/orm/services/region_manager/rms/storage/my_sql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/storage/my_sql/data_manager.py b/orm/services/region_manager/rms/storage/my_sql/data_manager.py new file mode 100755 index 00000000..1286697c --- /dev/null +++ b/orm/services/region_manager/rms/storage/my_sql/data_manager.py @@ -0,0 +1,517 @@ +import logging + +import oslo_db +from oslo_db.sqlalchemy import session as db_session +from sqlalchemy.ext.declarative.api import declarative_base +from sqlalchemy.sql import or_ + +from rms.services import error_base as ServiceBase +from data_models import Region, RegionEndPoint, Group +from data_models import RegionMetaData, GroupRegion +from rms.services import error_base +from rms.storage.base_data_manager import BaseDataManager, DuplicateEntryError, EntityNotFound +from rms.model import model as PythonModels + +Base = declarative_base() +logger = logging.getLogger(__name__) + + +class DataManager(BaseDataManager): + + def __init__(self, url, max_retries, retries_interval): + self._engine_facade = db_session.EngineFacade(url, + max_retries=max_retries, + retry_interval=retries_interval) + + def add_region(self, + region_id, + name, + address_state, + address_country, + address_city, + address_street, + address_zip, + region_status, + ranger_agent_version, + open_stack_version, + design_type, + location_type, + vlcp_name, + clli, + # a list of dictionaries of format + # {"type":"", "url":"", "description":"" + end_point_list, + # a dictionary of key,value pairs + # {"key":"value", } + meta_data_dict, + description=""): + """ add a new region to the `region` table + add also the regions give meta_data and end_points to the `region_end_point` and + `region_meta_data` tables if given. + handle duplicate errors if raised""" + try: + session = self._engine_facade.get_session() + with session.begin(): + region = Region(region_id=region_id, + name=name, + address_state=address_state, + address_country=address_country, + address_city=address_city, + address_street=address_street, + address_zip=address_zip, + region_status=region_status, + ranger_agent_version=ranger_agent_version, + open_stack_version=open_stack_version, + design_type=design_type, + location_type=location_type, + vlcp_name=vlcp_name, + clli=clli, + description=description) + + if end_point_list is not None: + for end_point in end_point_list: + region_end_point = RegionEndPoint( + end_point_type=end_point["type"], + public_url=end_point["url"]) + region.end_points.append(region_end_point) + + if meta_data_dict is not None: + for k, v in meta_data_dict.iteritems(): + for list_item in v: + region.meta_data.append( + RegionMetaData(region_id=region_id, + meta_data_key=k, + meta_data_value=list_item)) + + session.add(region) + except oslo_db.exception.DBDuplicateEntry as e: + logger.warning("Duplicate entry: {}".format(str(e))) + raise DuplicateEntryError("Region {} already " + "exist".format(region_id)) + + def update_region(self, + region_to_update, + region_id, + name, + address_state, + address_country, + address_city, + address_street, + address_zip, + region_status, + ranger_agent_version, + open_stack_version, + design_type, + location_type, + vlcp_name, + clli, + # a list of dictionaries of format + # {"type":"", "url":"", "description":"" + end_point_list, + # a list of dictionaries of format + # {"key":"", "value":"", "description":"" + meta_data_dict, + description=""): + """ add a new region to the `region` table + add also the regions give meta_data and end_points to the `region_end_point` and + `region_meta_data` tables if given. + handle duplicate errors if raised""" + try: + session = self._engine_facade.get_session() + with session.begin(): + # remove all childs as with update need to replace them + session.query(RegionMetaData).filter_by(region_id=region_to_update).delete() + session.query(RegionEndPoint).filter_by(region_id=region_to_update).delete() + + record = session.query(Region).filter_by(region_id=region_to_update).first() + if record is not None: + # record.region_id = region_id # ignore id and name when update + # record.name = name + record.address_state = address_state + record.address_country = address_country + record.address_city = address_city + record.address_street = address_street + record.address_zip = address_zip + record.region_status = region_status + record.ranger_agent_version = ranger_agent_version + record.open_stack_version = open_stack_version + record.design_type = design_type + record.location_type = location_type + record.vlcp_name = vlcp_name + record.clli = clli + record.description = description + + if end_point_list is not None: + for end_point in end_point_list: + region_end_point = RegionEndPoint( + end_point_type=end_point["type"], + public_url=end_point["url"] + ) + record.end_points.append(region_end_point) + + if meta_data_dict is not None: + for k, v in meta_data_dict.iteritems(): + for list_item in v: + record.meta_data.append( + RegionMetaData(region_id=region_id, + meta_data_key=k, + meta_data_value=list_item)) + else: + raise EntityNotFound("Region {} not found".format( + region_to_update)) + except EntityNotFound as exp: + logger.exception( + "fail to update entity with id {} not found".format( + region_to_update)) + raise ServiceBase.NotFoundError(message=exp.message) + except Exception as exp: + logger.exception("fail to update region {}".format(str(exp))) + raise + + def delete_region(self, region_id): + # delete a region from `region` table and also the region's + # entries from `region_meta_data` and `region_end_points` tables + session = self._engine_facade.get_session() + with session.begin(): + session.query(Region).filter_by(region_id=region_id).delete() + + def get_all_regions(self): + return self.get_regions(None, None, None) + + def get_regions(self, + region_filters_dict, + meta_data_dict, + end_point_dict): + logger.debug("Get regions") + records_model = [] + session = self._engine_facade.get_session() + with session.begin(): + records = session.query(Region) + if region_filters_dict is not None: + records = records.filter_by(**region_filters_dict) + + if meta_data_dict is not None: + regions = self._get_regions_for_meta_data_dict(meta_data_dict, + session) + query = [] + query.append((Region.region_id.in_(regions))) + records = records.filter(*query) + + if end_point_dict is not None: + records = records.join(RegionEndPoint).\ + filter_by(**end_point_dict) + if records is not None: + for record in records: + records_model.append(record.to_wsme()) + return records_model + + def _get_regions_for_meta_data_dict(self, meta_data_dict, session): + result_lists = [] + for key in meta_data_dict['meta_data_keys']: + md_q = session.query(RegionMetaData). \ + filter(RegionMetaData.meta_data_key == key).all() + temp_result_list = [] + if md_q is not None: + for record in md_q: + temp_result_list.append(record.region_id) + result_lists.append(set(temp_result_list)) + logger.debug(set(temp_result_list)) + for item in meta_data_dict['meta_data_pairs']: + md_q = session.query(RegionMetaData). \ + filter(RegionMetaData.meta_data_key == item['metadata_key'], + RegionMetaData.meta_data_value == item['metadata_value']).all() + temp_result_list = [] + if md_q is not None: + for record in md_q: + temp_result_list.append(record.region_id) + result_lists.append(set(temp_result_list)) + logger.debug(set(temp_result_list)) + + result = [] + if result_lists: + result = result_lists[0] + for l in result_lists: + result = result.intersection(l) + else: + result = None + logger.debug(result) + return result + + def get_region_by_id_or_name(self, region_id_or_name): + logger.debug("Get region by id or name: {}".format(region_id_or_name)) + try: + + session = self._engine_facade.get_session() + with session.begin(): + record = session.query(Region) + record = record.filter(or_(Region.region_id == region_id_or_name, + Region.name == region_id_or_name)) + if record.first(): + return record.first().to_wsme() + return None + + except Exception as exp: + logger.exception("DB error filtering by id/name") + raise + + def add_meta_data_to_region(self, region_id, + metadata_dict): + """ + :param region_id: + :param metadata_dict: + :return: + """ + session = self._engine_facade.get_session() + try: + with session.begin(): + record = session.query(Region).\ + filter_by(region_id=region_id).first() + + if record is not None: + region_metadata = [] + for k, v in metadata_dict.iteritems(): + for list_item in v: + region_metadata.append(RegionMetaData(region_id=region_id, + meta_data_key=k, + meta_data_value=list_item)) + session.add_all(region_metadata) + return record.to_wsme() + else: + logger.error("Region {} does not exist. " + "Meta Data was not added!".format(region_id)) + return None + + except oslo_db.exception.DBDuplicateEntry as e: + logger.warning("Duplicate entry: {}".format(str(e))) + raise error_base.ConflictError(message="Duplicate metadata value " + "in region {}".format(region_id)) + + def update_region_meta_data(self, region_id, + metadata_dict): + """ + Replace existing metadata for given region_id + :param region_id: + :param metadata_dict: + :return: + """ + session = self._engine_facade.get_session() + with session.begin(): + + record = session.query(Region). \ + filter_by(region_id=region_id).first() + if not record: + msg = "Region {} not found".format(region_id) + logger.info(msg) + raise error_base.NotFoundError(message=msg) + + session.query(RegionMetaData).\ + filter_by(region_id=region_id).delete() + + region_metadata = [] + for k, v in metadata_dict.iteritems(): + for list_item in v: + region_metadata.append(RegionMetaData(region_id=region_id, + meta_data_key=k, + meta_data_value=list_item)) + + session.add_all(region_metadata) + return record.to_wsme() + + def delete_region_metadata(self, region_id, key): + session = self._engine_facade.get_session() + with session.begin(): + record = session.query(Region). \ + filter_by(region_id=region_id).first() + + if not record: + msg = "Region {} not found".format(region_id) + logger.info(msg) + raise error_base.NotFoundError(message=msg) + + session.query(RegionMetaData).filter_by(region_id=region_id, + meta_data_key=key).delete() + + def update_region_status(self, region_id, region_status): + try: + session = self._engine_facade.get_session() + with session.begin(): + + record = session.query(Region).filter_by(region_id=region_id).first() + if record is not None: + record.region_status = region_status + else: + msg = "Region {} not found".format(region_id) + logger.info(msg) + raise error_base.NotFoundError(message=msg) + return record.region_status + + except Exception as exp: + logger.exception("failed to update region {}".format(str(exp))) + raise + """ + def add_end_point_to_region(self, + region_id, + end_point_type, + end_point_url, + description): + session = self._engine_facade.get_session() + try: + with session.begin(): + record = session.query(Region).filter_by(region_id=region_id).\ + first() + if record is not None: + session.add( + RegionEndPoint(region_id=region_id, + end_point_type=end_point_type, + public_url=end_point_url, + description=description)) + else: + logger.error("Region {} does not exist. " + "End point was not added !".format(region_id)) + + except oslo_db.exception.DBDuplicateEntry as e: + logger.warning("Duplicate entry: {}".format(str(e))) + raise SQLDBError("Duplicate entry error") + + def remove_end_point_from_region(self, + region_id, + end_point_type): + session = self._engine_facade.get_session() + with session.begin(): + session.query(Region).filter_by(region_id=region_id, + end_point_type=end_point_type).\ + delete() + """ + + # Handle group management operations + def add_group(self, + group_id, + group_name, + group_description, + region_ids_list): + session = self._engine_facade.get_session() + try: + with session.begin(): + session.add(Group(group_id=group_id, + name=group_name, + description=group_description)) + + session.flush() # add the groupe if not rollback + + if region_ids_list is not None: + group_regions = [] + for region_id in region_ids_list: + group_regions.append(GroupRegion(group_id=group_id, + region_id=region_id)) + session.add_all(group_regions) + except oslo_db.exception.DBReferenceError as e: + logger.error("Reference error: {}".format(str(e))) + raise error_base.InputValueError("Reference error") + except oslo_db.exception.DBDuplicateEntry as e: + logger.error("Duplicate entry: {}".format(str(e))) + raise error_base.ConflictError("Duplicate entry error") + + def delete_group(self, group_id): + session = self._engine_facade.get_session() + with session.begin(): + session.query(Group).filter_by(group_id=group_id).delete() + + def get_all_groups(self): + logger.debug("DB- Get all groups") + records_model = PythonModels.GroupsWrraper() + session = self._engine_facade.get_session() + with session.begin(): + groups = session.query(Group) + for a_group in groups: + group_model = PythonModels.Groups() + group_model.id = a_group.group_id + group_model.name = a_group.name + group_model.description = a_group.description + regions = [] + group_regions = session.query(GroupRegion).\ + filter_by(group_id=a_group.group_id) + for group_region in group_regions: + regions.append(group_region.region_id) + + group_model.regions = regions + records_model.groups.append(group_model) + return records_model + + def update_group(self, group_id, group_name, group_description, + group_regions): + try: + session = self._engine_facade.get_session() + with session.begin(): + # in update scenario delete all child records + session.query(GroupRegion).filter_by( + group_id=group_id).delete() + + group_record = session.query(Group).filter_by( + group_id=group_id).first() + if group_record is None: + raise error_base.NotFoundError( + message="Group {} not found".format(group_id)) + # only desc and regions can be changed + group_record.description = group_description + group_record.name = group_name + regions = [] + for region_id in group_regions: + regions.append(GroupRegion(region_id=region_id, + group_id=group_id)) + session.add_all(regions) + + except error_base.NotFoundError as exp: + logger.error(exp.message) + raise + except oslo_db.exception.DBReferenceError as e: + logger.error("Reference error: {}".format(str(e))) + raise error_base.InputValueError("Reference error") + except Exception as exp: + logger.error("failed to update group {}".format(group_id)) + logger.exception(exp) + raise + return + + def get_group(self, group_id): + logger.debug("Get group by name") + group_model = None + session = self._engine_facade.get_session() + with session.begin(): + a_group = session.query(Group).filter_by(group_id=group_id)\ + .first() + if a_group is not None: + group_model = {"id": a_group.group_id, + "name": a_group.name, + "description": a_group.description} + regions = [] + group_regions = session.query(GroupRegion). \ + filter_by(group_id=a_group.group_id) + for group_region in group_regions: + regions.append(group_region.region_id) + group_model["regions"] = regions + return group_model + + """ + def add_region_to_group(self, + group_id, + region_id): + session = self._engine_facade.get_session() + try: + with session.begin(): + session.add(GroupRegion(group_id=group_id, + region_id=region_id)) + except oslo_db.exception.DBReferenceError as e: + logger.error("Refernce error: {}".format(str(e))) + raise SQLDBError("Reference error") + except oslo_db.exception.DBDuplicateEntry as e: + logger.error("Duplicate entry: {}".format(str(e))) + raise SQLDBError("Duplicate entry error") + + def remove_region_from_group(self, + group_id, + region_id): + session = self._engine_facade.get_session() + with session.begin(): + session.query(GroupRegion).filter_by(group_id=group_id, + region_id=region_id).delete() + """ diff --git a/orm/services/region_manager/rms/storage/my_sql/data_models.py b/orm/services/region_manager/rms/storage/my_sql/data_models.py new file mode 100755 index 00000000..b5b17561 --- /dev/null +++ b/orm/services/region_manager/rms/storage/my_sql/data_models.py @@ -0,0 +1,141 @@ +# coding: utf-8 +from sqlalchemy import Column, ForeignKey, Index, Integer, String, Table +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declarative_base + +from rms.model.model import EndPoint, RegionData, Regions, Address + +Base = declarative_base() +metadata = Base.metadata + + +class Region(Base): + __tablename__ = 'region' + + id = Column(Integer, primary_key=True) + region_id = Column(String(64), nullable=False, unique=True) + name = Column(String(64), nullable=False, unique=True) + address_state = Column(String(64), nullable=False) + address_country = Column(String(64), nullable=False) + address_city = Column(String(64), nullable=False) + address_street = Column(String(64), nullable=False) + address_zip = Column(String(64), nullable=False) + region_status = Column(String(64), nullable=False) + ranger_agent_version = Column(String(64), nullable=False) + open_stack_version = Column(String(64), nullable=False) + design_type = Column(String(64), nullable=False) + location_type = Column(String(64), nullable=False) + vlcp_name = Column(String(64), nullable=False) + clli = Column(String(64), nullable=False) + description = Column(String(255), nullable=False) + + end_points = relationship(u'RegionEndPoint') + meta_data = relationship(u'RegionMetaData') + + def __json__(self): + pass + + def address_to_wsme(self): + return Address( + country=self.address_country, + state=self.address_state, + city=self.address_city, + street=self.address_street, + zip=self.address_zip + ) + + def to_wsme(self): + wsme_end_points = [end_point.to_wsme() for end_point in self.end_points] + + wsme_meta_data = {} + for meta_data in self.meta_data: + # wsme_meta_data[meta_data.meta_data_key] = meta_data.meta_data_value + if meta_data.meta_data_key not in wsme_meta_data: + wsme_meta_data[meta_data.meta_data_key] = [] + wsme_meta_data[meta_data.meta_data_key].append(meta_data.meta_data_value) + + id = self.region_id + name = self.name + status = self.region_status + clli = self.clli + ranger_agent_version = self.ranger_agent_version + design_type = self.design_type + location_type = self.location_type + vlcp_name = self.vlcp_name + open_stack_version = self.open_stack_version + address = self.address_to_wsme() + + return RegionData(status, id, name, clli, ranger_agent_version, + design_type, location_type, vlcp_name, + open_stack_version, address, + metadata=wsme_meta_data, + endpoints=wsme_end_points) + + +class Group(Base): + __tablename__ = 'rms_groups' + + id = Column(Integer, primary_key=True) + group_id = Column(String(64), nullable=False, unique=True) + name = Column(String(64), nullable=False, unique=True) + description = Column(String(255), nullable=False) + + def __json__(self): + return dict( + group_id=self.group_id, + name=self.name, + description=self.description + ) + + def to_wsme(self): + pass + + +class GroupRegion(Base): + __tablename__ = 'group_region' + + group_id = Column(ForeignKey(u'rms_groups.group_id'), primary_key=True) + region_id = Column(ForeignKey(u'region.region_id'), primary_key=True) + + def __json__(self): + return dict( + group_id=self.group_id, + region_id=self.region_id + ) + + def to_wsme(self): + pass + + +class RegionEndPoint(Base): + __tablename__ = 'region_end_point' + + region_id = Column(ForeignKey(u'region.region_id'), primary_key=True) + end_point_type = Column(String(64), primary_key=True) + public_url = Column(String(64), nullable=False) + + def __json__(self): + return dict( + end_point_type=self.end_point_type, + public_url=self.public_url + ) + + def to_wsme(self): + url = self.public_url + atype = self.end_point_type + return EndPoint(url, atype) + + +class RegionMetaData(Base): + __tablename__ = 'region_meta_data' + + id = Column(Integer, primary_key=True) + region_id = Column(ForeignKey(u'region.region_id'), nullable=False) + meta_data_key = Column(String(64), nullable=False) + meta_data_value = Column(String(255), nullable=False) + + def __json__(self): + return {self.meta_data_key: self.meta_data_value} + + def to_wsme(self): + pass diff --git a/orm/services/region_manager/rms/tests/__init__.py b/orm/services/region_manager/rms/tests/__init__.py new file mode 100644 index 00000000..78ea5274 --- /dev/null +++ b/orm/services/region_manager/rms/tests/__init__.py @@ -0,0 +1,22 @@ +import os +from unittest import TestCase +from pecan import set_config +from pecan.testing import load_test_app + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """ + Used for functional tests where you need to test your + literal application and its integration with the framework. + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/orm/services/region_manager/rms/tests/config.py b/orm/services/region_manager/rms/tests/config.py new file mode 100755 index 00000000..e2793a15 --- /dev/null +++ b/orm/services/region_manager/rms/tests/config.py @@ -0,0 +1,46 @@ +# Server Specific Configurations +server = { + 'port': '8080', + 'host': '0.0.0.0' +} + +# Pecan Application Configurations +app = { + 'root': 'rms.controllers.root.RootController', + 'modules': ['rms'], + 'static_root': '%(confdir)s/../../public', + 'template_path': '%(confdir)s/../templates', + 'debug': True, + 'errors': { + '404': '/error/404', + '__force_dict__': True + } +} + +endpoints = { + 'lcp': 'http://127.0.0.1:8082/lcp' +} + +# user input validations +region_options = { + 'allowed_status_values': [ + 'functional', + 'maintenance', + 'down' + ], + 'endpoints_types_must_have': [ + 'dashboard', + 'identity', + 'ord' + ] +} + +authentication = { + "enabled": True, + "mech_id": "admin", + "mech_pass": "stack", + "tenant_name": "admin", + # The Keystone version currently in use. Can be either "2.0" or "3" + "keystone_version": "2.0", + "policy_file": "/opt/app/orm/rms/rms/etc/policy.json" +} diff --git a/orm/services/region_manager/rms/tests/controllers/__init__.py b/orm/services/region_manager/rms/tests/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/controllers/v1/__init__.py b/orm/services/region_manager/rms/tests/controllers/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/controllers/v1/orm/__init__.py b/orm/services/region_manager/rms/tests/controllers/v1/orm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/__init__.py b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_groups.py b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_groups.py new file mode 100755 index 00000000..95049555 --- /dev/null +++ b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_groups.py @@ -0,0 +1,213 @@ +"""get_groups unittests module.""" +import json + +from mock import patch, MagicMock +from rms.controllers.v2.orm.resources import groups +from rms.services import error_base + +from rms.tests import FunctionalTest + +from wsme.exc import ClientSideError + +res = {"regions": ["aaaa", "bbbb", "ccccc"], + "name": "mygroup", "id": "any", + "description": "this is my only for testing"} + + +group_dict = {'id': 'noq', 'name': 'poq', 'description': 'b', 'regions': ['c']} + + +class Groups(object): + """class method.""" + + def __init__(self, id=None, name=None, description=None, + regions=[], any=None): + """init function. + + :param regions: + :return: + """ + self.id = id + self.name = name + self.description = description + self.regions = regions + if any: + self.any = any + + +class GroupsList(object): + def __init__(self, groups): + self.groups = [] + for group in groups: + self.groups.append(Groups(**group)) + + +class TestGetGroups(FunctionalTest): + + # all success + @patch.object(groups.GroupService, 'get_groups_data', return_value=Groups(**res)) + @patch.object(groups, 'authentication') + def test_get_success(self, mock_authentication, result): + response = self.app.get('/v2/orm/groups/1') + self.assertEqual(dict(response.json), res) + + # raise exception no content + @patch.object(groups.GroupService, 'get_groups_data', + side_effect=groups.error_base.NotFoundError("no content !!!?")) + @patch.object(groups.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 404, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '444', + 'message': 'test', + 'details': 'test' + }), status_code=404)) + @patch.object(groups, 'authentication') + def test_get_groups_not_found(self, mock_auth, get_err, result): + temp_request = groups.request + groups.request = MagicMock() + + response = self.app.get('/v2/orm/groups/1', expect_errors=True) + + groups.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('444', result_json['transaction_id']) + self.assertEqual(404, result_json['code']) + + # raise general exception + @patch.object(groups.GroupService, 'get_groups_data', side_effect=Exception("unknown error")) + @patch.object(groups.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '555', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + @patch.object(groups, 'authentication') + def test_get_groups_unknown_exception(self, mock_auth, get_err, result): + temp_request = groups.request + groups.request = MagicMock() + + response = self.app.get('/v2/orm/groups/1', expect_errors=True) + + groups.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('555', result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + +class TestCreateGroup(FunctionalTest): + """Main create_group test case.""" + + @patch.object(groups, 'request') + @patch.object(groups.GroupService, 'create_group_in_db') + @patch.object(groups, 'authentication') + def test_post_success(self, mock_authentication, mock_create_group, + mock_request): + """Test successful group creation.""" + mock_request.application_url = 'http://localhost' + response = self.app.post_json('/v2/orm/groups', + {'id': 'd', 'name': 'a', + 'description': 'b', + 'regions': ['c']}) + # Make sure all keys are in place + self.assertTrue(all([c in response.json['group'] for c in ( + 'created', 'id', 'links')])) + + self.assertEqual(response.json['group']['id'], 'd') + self.assertEqual(response.json['group']['name'], 'a') + self.assertEqual(response.json['group']['links']['self'], + 'http://localhost/v2/orm/groups/d') + + @patch.object(groups.GroupService, 'create_group_in_db', side_effect=groups.error_base.ConflictError) + @patch.object(groups.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 409, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '333', + 'message': 'test', + 'details': 'test' + }), status_code=409)) + @patch.object(groups, 'authentication') + def test_post_group_already_exists(self, mock_auth, get_err, + mock_create_group): + """Make sure the function returns status code 409 if group exists.""" + temp_request = groups.request + groups.request = MagicMock() + + response = self.app.post_json('/v2/orm/groups', + {'id': 'noq', 'name': 'poq', + 'description': 'b', + 'regions': ['c']}, expect_errors=True) + + groups.request = temp_request + self.assertEqual(response.status_code, 409) + + +class TestDeleteGroup(FunctionalTest): + """Main delete group.""" + + @patch.object(groups, 'request') + @patch.object(groups.GroupService, 'delete_group') + @patch.object(groups, 'authentication') + def test_delete_group_success(self, auth_mock, mock_delete_group, + mock_request): + response = self.app.delete('/v2/orm/groups/{id}') + self.assertEqual(response.status_code, 204) + + @patch.object(groups.GroupService, 'delete_group', side_effect=Exception("any")) + @patch.object(groups, 'authentication') + def test_delete_group_error(self, auth_mock, mock_delete_group): + response = self.app.delete('/v2/orm/groups/{id}', expect_errors=True) + self.assertEqual(response.status_code, 500) + + +class TestUpdateGroup(FunctionalTest): + """Main delete group.""" + + def get_error(self, transaction_id, status_code, error_details=None, + message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) + + @patch.object(groups, 'request') + @patch.object(groups.GroupService, 'update_group', + return_value=Groups(**group_dict)) + @patch.object(groups, 'authentication') + def test_update_group_success(self, auth_mock, mock_delete_group, + mock_request): + response = self.app.put_json('/v2/orm/groups/id', group_dict) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json['group']['id'], group_dict['id']) + + # @patch.object(groups, 'err_utils') + # @patch.object(groups.GroupService, 'update_group', + # side_effect=error_base.NotFoundError(message="any")) + # @patch.object(groups, 'authentication') + # def test_update_group_error(self, auth_mock, mock_delete_group, + # mock_err_utils): + # mock_err_utils.get_error = self.get_error + # response = self.app.put_json('/v2/orm/groups/{id}', group_dict, + # expect_errors=True) + # self.assertEqual(response.status_code, 404) + + @patch.object(groups.GroupService, 'get_all_groups', + return_value=GroupsList([res])) + @patch.object(groups, 'authentication') + def test_get_all_success(self, mock_authentication, result): + response = self.app.get('/v2/orm/groups') + self.assertEqual(dict(response.json), {'groups': [res]}) diff --git a/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_metadata.py b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_metadata.py new file mode 100755 index 00000000..ffb0c0b8 --- /dev/null +++ b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_metadata.py @@ -0,0 +1,336 @@ +import json + +from mock import patch, MagicMock + +from rms.controllers.v2.orm.resources import metadata +from rms.model.model import * +from rms.tests import FunctionalTest + +from wsme.exc import ClientSideError + +result_inst = RegionData("1", "2", "3", "4", "5", "6", + endpoints=[ + EndPoint("http://www.example.co.il", "url") + ], + address=Address("US", "NY", "HANEGEV", "AIRPORT_CITY", "5"), + metadata={"key1": ["value1"], "key2": ["value2"]}) + +result_dict = {u'status': u'2', u'vlcpName': None, u'clli': u'5', + u'name': u'4', u'designType': None, + u'AicVersion': u'6', u'OSVersion': None, u'id': u'3', + u'address': {u'country': u'US', u'state': u'NY', + u'street': u'AIRPORT_CITY', + u'zip': u'5', u'city': u'HANEGEV'}, + u'endpoints': [ + {u'type': u'url', + u'publicURL': u'http://www.example.co.il'}], + u'locationType': None, + u'metadata': {u'key1': [u'value1'], + u'key2': [u'value2']} + } + +metadata_input_dict = { + "metadata": { + "key1": ["value1"], + "key2": ["value2"] + } +} + + +metadata_result_dict = {u'metadata': {u'key1': [u'value1'], + u'key2': [u'value2'] + } + } + +metadata_result_empty_dict = {u'metadata': {}} + + +class TestMetadataController(FunctionalTest): + + ############### + # Test DELETE api + @patch.object(metadata, 'request') + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'delete_metadata_from_region') + def test_delete_success(self, result, mock_auth, mock_request): + response = self.app.delete('/v2/orm/regions/my_region/metadata/mykey', + expect_errors=True) + self.assertEqual(response.status_int, 204) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'delete_metadata_from_region', + side_effect=metadata.error_base.NotFoundError("region not found !!!?")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 404, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '774', + 'message': 'test', + 'details': 'test' + }), status_code=404)) + def test_delete_with_region_not_exist(self, get_err, result, + mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.delete('/v2/orm/regions/my_region/metadata/key', + expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('774', result_json['transaction_id']) + self.assertEqual(404, result_json['code']) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'delete_metadata_from_region', + side_effect=Exception("unknown error")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '771', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + # @patch.object(metadata, 'authentication') + def test_delete_region_metadata_unknown_exception(self, err, result, + mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.delete('/v2/orm/regions/my_region/metadata/key', + expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('771', result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + ############### + # Test PUT api + # @patch.object(metadata, 'request') + # @patch.object(metadata, 'authentication') + # @patch.object(metadata.RegionService, 'update_region_metadata', + # return_value=result_inst.metadata) + # def test_put_success(self, result, mock_auth, mock_request): + # response = self.app.put_json('/v2/orm/regions/my_region/metadata', + # metadata_input_dict) + # self.assertEqual(dict(response.json), metadata_result_dict) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'update_region_metadata', + side_effect=metadata.error_base.NotFoundError("region not found !!!?")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 404, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '888', + 'message': 'test', + 'details': 'test' + }), status_code=404)) + def test_put_update_region_metadata_with_region_not_exist(self, get_err, + result, + mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.put_json('/v2/orm/regions/my_region/metadata', + metadata_input_dict, expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('888', result_json['transaction_id']) + self.assertEqual(404, result_json['code']) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'update_region_metadata', + side_effect=Exception("unknown error")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '777', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + # @patch.object(metadata, 'authentication') + def test_put_update_region_metadata_unknown_exception(self, err, result, + mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.put_json('/v2/orm/regions/my_region/metadata', + metadata_input_dict, expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('777', result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + ############### + # Test POST api + # @patch.object(metadata, 'request') + # @patch.object(metadata, 'authentication') + # @patch.object(metadata.RegionService, 'add_region_metadata', + # return_value=result_inst.metadata) + # def test_post_success(self, result, mock_auth, mock_request): + # response = self.app.post_json('/v2/orm/regions/my_region/metadata', + # metadata_input_dict) + # self.assertEqual(dict(response.json), metadata_result_dict) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'add_region_metadata', + side_effect=metadata.error_base.NotFoundError("region not found !!!?")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 404, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '333', + 'message': 'test', + 'details': 'test' + }), status_code=404)) + def test_post_add_region_metadata_with_region_not_exist(self, get_err, + result, mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.post_json('/v2/orm/regions/my_region/metadata', + metadata_input_dict, expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('333', result_json['transaction_id']) + self.assertEqual(404, result_json['code']) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'add_region_metadata', + side_effect=metadata.error_base.ConflictError("unknown error")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 409, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '999', + 'message': 'test', + 'details': 'test' + }), status_code=409)) + # @patch.object(metadata, 'authentication') + def test_post_add_region_metadata_with_duplicate(self, err, result, + mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.post_json('/v2/orm/regions/my_region/metadata', + metadata_input_dict, expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('999', result_json['transaction_id']) + self.assertEqual(409, result_json['code']) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'add_region_metadata', + side_effect=Exception("unknown error")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '444', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + # @patch.object(metadata, 'authentication') + def test_post_add_region_metadata_unknown_exception(self, err, result, + mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.post_json('/v2/orm/regions/my_region/metadata', + metadata_input_dict, expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('444', result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + ############## + # Test GET api + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'get_region_by_id_or_name', + return_value=result_inst) + def test_get_success(self, result, mock_auth): + response = self.app.get('/v2/orm/regions/my_region/metadata') + self.assertEqual(dict(response.json), metadata_result_dict) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'get_region_by_id_or_name', + side_effect=Exception("unknown error")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '111', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + # @patch.object(metadata, 'authentication') + def test_get_get_region_by_id_or_name_throws_exception(self, err, result, + mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.get('/v2/orm/regions/my_region/metadata', expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('111', result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + @patch.object(metadata, 'authentication') + @patch.object(metadata.RegionService, 'get_region_by_id_or_name', + side_effect=metadata.error_base.NotFoundError("no content !!!?")) + @patch.object(metadata.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 404, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '222', + 'message': 'test', + 'details': 'test' + }), status_code=404)) + def test_get_get_region_by_id_or_name_region_not_found(self, get_err, + result, mock_auth): + temp_request = metadata.request + metadata.request = MagicMock() + + response = self.app.get('/v2/orm/regions/my_region/metadata', expect_errors=True) + + metadata.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('222', result_json['transaction_id']) + self.assertEqual(404, result_json['code']) diff --git a/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_region.py b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_region.py new file mode 100755 index 00000000..82e65b1c --- /dev/null +++ b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_region.py @@ -0,0 +1,414 @@ +import json +from mock import patch, MagicMock + +from rms.controllers.v2.orm.resources import regions +from rms.model import model as PyModels +from rms.tests import FunctionalTest + +from wsme.exc import ClientSideError + + +result_inst = PyModels.Regions([PyModels.RegionData("2", "3", "4", "5", "6", + address=PyModels.Address("US", "NY", "HANEGEV", "AIRPORT_CITY", "5"), + endpoints=[ + PyModels.EndPoint("http://www.example.co.il", "url") + ], + metadata={"key1": ["value1"], "key2": ["value2"]}), + PyModels.RegionData("2", "3", "4", "5", "6", endpoints=[ + PyModels.EndPoint("http://www.example.co.il", "url")], + address=PyModels.Address("US", "NY", "HANEGEV", "AIRPORT_CITY", "5"), + metadata={"key3": ["value3"], "key4": ["value4"]})]) + + +result_dict = {u'regions': [{u'status': u'2', u'vlcpName': None, u'CLLI': u'5', + u'name': u'3', u'designType': None, + u'rangerAgentVersion': u'6', u'OSVersion': None, u'id': u'3', + u'address': {u'country': u'US', u'state': u'NY', + u'street': u'AIRPORT_CITY', + u'zip': u'5', u'city': u'HANEGEV'}, + u'endpoints': [ + {u'type': u'url', + u'publicURL': u'http://www.example.co.il'}], + u'locationType': None, + u'metadata': {u'key1': [u'value1'], + u'key2': [u'value2']}}, + {u'status': u'2', u'vlcpName': None, u'CLLI': u'5', + u'name': u'3', u'designType': None, + u'rangerAgentVersion': u'6', u'OSVersion': None, + u'id': u'3', + u'address': {u'country': u'US', + u'state': u'NY', + u'street': u'AIRPORT_CITY', + u'zip': u'5', u'city': u'HANEGEV'}, + u'endpoints': [{u'type': u'url', + u'publicURL': u'http://www.example.co.il'}], + u'locationType': None, + u'metadata': {u'key3': [u'value3'], + u'key4': [u'value4']}}]} + + +db_full_region = { + 'region_status': 'functional', + 'address_city': 'LAb', + 'CLLI': 'nn/a', + 'region_id': 'SNA20', + 'open_stack_version': 'kilo', + 'address_country': 'US', + 'design_type': 'n/a', + 'ranger_agent_version': 'ranger_agent1.0', + 'vlcp_name': 'n/a', + 'end_point_list': [{ + 'url': 'http://horizon1.com', + 'type': 'dashboard' + }, { + 'url': 'http://identity1.com', + 'type': 'identity' + }, { + 'url': 'http://identity1.com', + 'type': 'identity222333' + }, { + 'url': 'http://ord1.com', + 'type': 'ord' + }], + 'meta_data_dict': { + 'A': ['b'] + }, + 'address_state': 'CAL', + 'address_zip': '1111', + 'address_street': 'n/a', + 'location_type': 'n/a', + 'name': 'SNA 18' +} + +full_region = { + "status": "functional", + "endpoints": + [ + { + "type": "dashboard", + "publicURL": "http://horizon1.com" + }, + + { + "type": "identity", + "publicURL": "http://identity1.com" + }, + { + "type": "identity222333", + "publicURL": "http://identity1.com" + }, + { + "type": "ord", + "publicURL": "http://ord1.com" + } + ], + "CLLI": "nn/a", + "name": "SNA20", + "designType": "n/a", + "locationType": "n/a", + "vlcpName": "n/a", + "address": + { + "country": "US", + "state": "CAL", + "street": "n/a", + "zip": "1111", + "city": "LAb"}, + "rangerAgentVersion": "ranger_agent1.0", + "OSVersion": "kilo", + "id": "SNA20", + "metadata": + {"A": ["b"]} +} + + +class TestAddRegion(FunctionalTest): + + def get_error(self, transaction_id, status_code, error_details=None, message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) + + def _create_result_from_input(self, input): + obj = PyModels.RegionData() + obj.clli = full_region["CLLI"] + obj.name = full_region["id"] # need to be same as id + obj.design_type = full_region["designType"] + obj.location_type = full_region["locationType"] + obj.vlcp_name = full_region["vlcpName"] + obj.id = full_region["id"] + obj.address.country = full_region["address"]["country"] + obj.address.city = full_region["address"]["city"] + obj.address.state = full_region["address"]["state"] + obj.address.street = full_region["address"]["street"] + obj.address.zip = full_region["address"]["zip"] + obj.ranger_agent_version = full_region["rangerAgentVersion"] + obj.open_stack_version = full_region["OSVersion"] + obj.metadata = full_region["metadata"] + obj.status = full_region["status"] + obj.endpoints = [] + for endpoint in full_region["endpoints"]: + obj.endpoints.append(PyModels.EndPoint(type=endpoint["type"], + publicurl=endpoint[ + "publicURL"])) + return obj + + @patch.object(regions, 'request') + @patch.object(regions.RegionService, 'create_full_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_add_region_success(self, mock_auth, mock_create_logic, + mock_request): + self.maxDiff = None + mock_create_logic.return_value = self._create_result_from_input( + full_region) + response = self.app.post_json('/v2/orm/regions', full_region) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json, full_region) + + @patch.object(regions.RegionService, 'create_full_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_add_region_any_error(self, mock_auth, mock_create_logic): + self.maxDiff = None + mock_create_logic.side_effect = Exception("unknown error") + response = self.app.post_json('/v2/orm/regions', full_region, + expect_errors=True) + self.assertEqual(response.status_code, 500) + + @patch.object(regions, 'request') + @patch.object(regions, 'err_utils') + @patch.object(regions.RegionService, 'create_full_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_add_region_value_error(self, mock_auth, mock_create_logic, + mock_get_error, request_mock): + mock_get_error.get_error = self.get_error + request_mock.transaction_id = "555" + mock_create_logic.side_effect = regions.error_base.InputValueError(message="value error") + response = self.app.post_json('/v2/orm/regions', full_region, + expect_errors=True) + self.assertEqual(response.status_code, 400) + self.assertEqual(json.loads(response.json['faultstring'])['message'], 'value error') + + @patch.object(regions.RegionService, 'get_region_by_id_or_name') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_get_region_success(self, mock_auth, mock_create_logic): + self.maxDiff = None + mock_create_logic.return_value = self._create_result_from_input( + full_region) + response = self.app.get('/v2/orm/regions/id') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json, full_region) + + @patch.object(regions, 'request') + @patch.object(regions, 'err_utils') + @patch.object(regions.RegionService, 'get_region_by_id_or_name') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_get_region_not_found(self, mock_auth, mock_get_logic, + mock_get_error, mock_request): + mock_get_error.get_error = self.get_error + mock_request.transaction_id = "555" + mock_get_logic.side_effect = regions.error_base.NotFoundError(message="not found", status_code=404) + response = self.app.get('/v2/orm/regions/id', expect_errors=True) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + 'not found') + self.assertEqual(response.status_code, 404) + + @patch.object(regions, 'request') + @patch.object(regions, 'err_utils') + @patch.object(regions.RegionService, 'delete_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_delete_region(self, mock_auth, mock_delete_logic, + mock_get_error, mock_request): + mock_get_error.get_error = self.get_error + mock_request.transaction_id = "555" + mock_delete_logic.return_value = True + response = self.app.delete('/v2/orm/regions/id') + self.assertEqual(response.status_code, 204) + + @patch.object(regions, 'request') + @patch.object(regions, 'err_utils') + @patch.object(regions.RegionService, 'delete_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_delete_region_error(self, mock_auth, mock_delete_logic, + mock_get_error, mock_request): + mock_get_error.get_error = self.get_error + mock_request.transaction_id = "555" + mock_delete_logic.side_effect = Exception("unknown error") + response = self.app.delete('/v2/orm/regions/id', expect_errors=True) + self.assertEqual(response.status_code, 500) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + 'unknown error') + + @patch.object(regions, 'request') + @patch.object(regions.RegionService, 'update_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_update_region_success(self, mock_auth, mock_update_logic, + mock_request): + mock_update_logic.return_value = self._create_result_from_input( + full_region) + response = self.app.put_json('/v2/orm/regions/id', full_region) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json, full_region) + + @patch.object(regions, 'request') + @patch.object(regions, 'err_utils') + @patch.object(regions.RegionService, 'update_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_update_region_error(self, mock_auth, mock_update_logic, + mock_get_error, mock_request): + mock_get_error.get_error = self.get_error + mock_request.transaction_id = "555" + mock_update_logic.side_effect = Exception("unknown error2") + response = self.app.put_json('/v2/orm/regions/id', full_region, + expect_errors=True) + self.assertEqual(response.status_code, 500) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + 'unknown error2') + + @patch.object(regions, 'request') + @patch.object(regions, 'err_utils') + @patch.object(regions.RegionService, 'update_region') + @patch.object(regions.authentication, 'authorize', return_value=True) + def test_update_region_not_found_error(self, mock_auth, mock_update_logic, + mock_get_error, mock_request): + mock_get_error.get_error = self.get_error + mock_request.transaction_id = "555" + mock_update_logic.side_effect = regions.error_base.NotFoundError( + message="not found", status_code=404) + response = self.app.put_json('/v2/orm/regions/id', full_region, + expect_errors=True) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + 'not found') + self.assertEqual(response.status_code, 404) + + +class TestWsmeModelFunctions(TestAddRegion): + + def _to_wsme_from_input(self, input): + obj = regions.RegionsData() + obj.clli = full_region["CLLI"] + obj.name = full_region["name"] + obj.design_type = full_region["designType"] + obj.location_type = full_region["locationType"] + obj.vlcp_name = full_region["vlcpName"] + obj.id = full_region["id"] + obj.address.country = full_region["address"]["country"] + obj.address.city = full_region["address"]["city"] + obj.address.state = full_region["address"]["state"] + obj.address.street = full_region["address"]["street"] + obj.address.zip = full_region["address"]["zip"] + obj.ranger_agent_version = full_region["rangerAgentVersion"] + obj.open_stack_version = full_region["OSVersion"] + obj.metadata = full_region["metadata"] + obj.status = full_region["status"] + obj.endpoints = [] + for endpoint in full_region["endpoints"]: + obj.endpoints.append(regions.EndPoint(type=endpoint["type"], + publicurl=endpoint[ + "publicURL"])) + return obj + + def test_region_data_model(self): + self.maxDiff = None + wsme_to_python = self._to_wsme_from_input(full_region)._to_clean_python_obj() + python_obj_input = self._create_result_from_input(full_region) + self.assertEqual(wsme_to_python.__dict__.pop('address').__dict__, + python_obj_input.__dict__.pop('address').__dict__) + self.assertEqual(wsme_to_python.__dict__.pop('endpoints')[0].__dict__, + python_obj_input.__dict__.pop('endpoints')[0].__dict__) + self.assertEqual(wsme_to_python.__dict__, python_obj_input.__dict__) + + +class TestGetRegionsController(FunctionalTest): + + @patch.object(regions.RegionService, 'get_regions_data', return_value=result_inst) + @patch.object(regions, 'authentication') + def test_get_success(self, mock_authentication, result): + self.maxDiff = None + response = self.app.get('/v2/orm/regions') + self.assertEqual(dict(response.json), result_dict) + + @patch.object(regions.RegionService, 'get_regions_data', side_effect=Exception("unknown error")) + @patch.object(regions.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '111', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + @patch.object(regions, 'authentication') + def test_get_unknown_error(self, mock_auth, get_err, result): + temp_request = regions.request + regions.request = MagicMock() + + response = self.app.get('/v2/orm/regions', expect_errors=True) + + regions.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('111', result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + @patch.object(regions.RegionService, 'get_regions_data', + side_effect=regions.error_base.NotFoundError("no content !!!?")) + @patch.object(regions.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 404, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '222', + 'message': 'test', + 'details': 'test' + }), status_code=404)) + @patch.object(regions, 'authentication') + def test_get_region_not_found(self, mock_auth, get_err, result): + temp_request = regions.request + regions.request = MagicMock() + + response = self.app.get('/v2/orm/regions', expect_errors=True) + + regions.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('222', result_json['transaction_id']) + self.assertEqual(404, result_json['code']) + + @patch.object(regions.RegionService, 'get_region_by_id_or_name', + return_value=result_inst.regions[0]) + @patch.object(regions, 'authentication') + def test_get_one_success(self, mock_authentication, result): + response = self.app.get('/v2/orm/regions/id') + self.assertEqual(dict(response.json), result_dict['regions'][0]) + + @patch.object(regions.RegionService, 'get_regions_data', + side_effect=Exception("unknown error")) + @patch.object(regions.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '111', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + @patch.object(regions, 'authentication') + def test_get_one_unknown_error(self, mock_auth, get_err, result): + temp_request = regions.request + regions.request = MagicMock() + + response = self.app.get('/v2/orm/regions/id', expect_errors=True) + + regions.request = temp_request + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual('111', result_json['transaction_id']) + self.assertEqual(500, result_json['code']) diff --git a/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_status.py b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_status.py new file mode 100755 index 00000000..fb818711 --- /dev/null +++ b/orm/services/region_manager/rms/tests/controllers/v1/orm/resources/test_status.py @@ -0,0 +1,81 @@ +import json +from mock import patch + +from rms.controllers.v2.orm.resources import status +from rms.tests import FunctionalTest + +from wsme.exc import ClientSideError + + +def get_region_status_data(status): + return { + "status": status, + "links": { + "self": "https://0.0.0.0:8080/v2/orm/regions/my_region/status", + } + } + + +class TestRegionStatus(FunctionalTest): + @patch.object(status, 'request') + @patch.object(status.utils, 'audit_trail') + @patch.object(status.RegionService, 'update_region_status') + @patch.object(status.authentication, 'authorize', return_value=True) + def test_update_region_status_success(self, + auth, + mock_update_status_logic, + mock_audit_trail, + mock_request): + self.maxDiff = None + mock_update_status_logic.return_value = "functional" + response = self.app.put_json('/v2/orm/regions/my_region/status', + get_region_status_data("functional")) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json, get_region_status_data("functional")) + + @patch.object(status, 'request') + @patch.object(status, 'err_utils') + @patch.object(status.RegionService, 'update_region_status') + @patch.object(status.authentication, 'authorize', return_value=True) + def test_update_region_status_invalid_status(self, auth, + update_region_status, + mock_err_util, + request_mock): + mock_err_util.get_error = get_error + request_mock.transaction_id = "555" + response = self.app.put_json('/v2/orm/regions/my_region/status', + get_region_status_data("invalid_status"), + expect_errors=True) + self.assertEqual(response.status_code, 400) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + "Invalid status. Region status must be one " + "of ['functional', 'maintenance', 'down']") + + @patch.object(status, 'request') + @patch.object(status, 'err_utils') + @patch.object(status.RegionService, 'update_region_status') + @patch.object(status.authentication, 'authorize', return_value=True) + def test_update_region_status_unknown_error(self, auth, + update_region_status, + mock_err_util, + request_mock): + mock_err_util.get_error = get_error + request_mock.transaction_id = "555" + update_region_status.side_effect = Exception("unknown error") + response = self.app.put_json('/v2/orm/regions/my_region/status', + get_region_status_data("functional"), + expect_errors=True) + self.assertEqual(response.status_code, 500) + self.assertEqual(json.loads(response.json['faultstring'])['message'], + "unknown error") + + +def get_error(transaction_id, status_code, error_details=None, message=None): + return ClientSideError(json.dumps({ + 'code': status_code, + 'type': 'test', + 'created': '0.0', + 'transaction_id': transaction_id, + 'message': message if message else error_details, + 'details': 'test' + }), status_code=status_code) diff --git a/orm/services/region_manager/rms/tests/db_testing.py b/orm/services/region_manager/rms/tests/db_testing.py new file mode 100644 index 00000000..7ff4ff7e --- /dev/null +++ b/orm/services/region_manager/rms/tests/db_testing.py @@ -0,0 +1,90 @@ +import logging + +from rms.storage.base_data_manager import SQLDBError +from rms.storage.my_sql.data_manager import DataManager + +logger = logging.getLogger(__name__) + + +def run_db_tests(data_manager): + logger.info('In db testing') + + try: + # add regions with meta_data and end_points + + # end_point_list = [{"type": "ord", "url": "http://ord.com", "description": "ord url"}, + # {"type": "identity", "url": "http://identity.com", "description": "keystone url"}, + # {"type": "image", "url": "http://image.com", "description": "image api url"}] + # + # meta_data_list = [{"key": "key_1", "value": "value_1", "description": "meta data key 1"}, + # {"key": "key_2", "value": "value_2", "description": "meta data key 2"}, + # {"key": "key_3", "value": "value_3", "description": "meta data key 3"}] + # + # data_manager.add_region("region_1","region 1", "US", "Cal", "LA", "blv_1", "12345", 1, + # "functional", "ranger_agent 1.0", "kilo", "dt_1", "lt_1", + # "vlcp_1", "clli_1", "test test test", end_point_list, meta_data_list) + # + # data_manager.add_region("region_2","region 2", "IL", "IL", "TelAviv", "bazel 1", "12345", 0, + # "functional", "ranger_agent 1.0", "kilo", "dt_1", "lt_1", + # "vlcp_1", "clli_1", "test2 test2 test2", end_point_list, meta_data_list) + # + # # get all regions + # regions = data_manager.get_all_regions() + # logger.info(regions) + + # region_dict = {"address_country":"Cal"} + # meta_data_dict = {"meta_data_key": "key_1", "meta_data_value": "value_1"} + # end_point_dict = None#{"end_point_type": "type_1"} + # x = data_manager.get_regions(region_dict, + # meta_data_dict, + # end_point_dict) + + # delete exist region + # data_manager.delete_region("region_1") + + # delete a region that does not exist + # data_manager.delete_region("region_25") + + # remove valid meta_data entry from a region + # data_manager.remove_meta_data_from_region("region_2","key_1") + + # remove invalid meta_data entry from a region + # data_manager.remove_meta_data_from_region("region_6", "key_999") + + # add meta_data to valid region + # data_manager.add_meta_data_to_region("region_2","b_key", "b_value", "bla bla") + + # add meta_data to invalid region + # data_manager.add_meta_data_to_region("region_7", "c_key", "c_value", "cla cla") + + # add end_point to valid region + # data_manager.add_end_point_to_region("region_2","type_c", "url_c", "cla cla") + # data_manager.add_end_point_to_region("region_6", "type_c", "url_ccc", "cla cla") + + # add end_point to invalid region + # data_manager.add_end_point_to_region("region_7", "type_7", "url_7", "cla7 cla7") + + # x = data_manager.get_all_regions() + # logger.info(x) + + # data_manager.add_group("group_0","group 0 description",["lcp_1", "lcp_2"]) + # data_manager.delete_group("group_0") + # data_manager.remove_region_from_group("group_0","lcp_1") + # data_manager.add_region_to_group("group_0","lcp_0") + # data_manager.add_group("group_1","group 1","group 1 description",["SNA1","SNA2"]) + # data_manager.get_all_groups() + x = data_manager.get_group("group_1") + logger.info(x) + except SQLDBError as e: + logger.error("SQL error raised {}".format(e.message)) + + +def main(): + db_url = 'mysql://root:stack@127.0.0.1/orm_rms_db?charset=utf8' + + data_manager = DataManager(db_url) + + run_db_tests(data_manager) + +if __name__ == "__main__": + main() diff --git a/orm/services/region_manager/rms/tests/model/__init__.py b/orm/services/region_manager/rms/tests/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/model/test_url_parms.py b/orm/services/region_manager/rms/tests/model/test_url_parms.py new file mode 100755 index 00000000..3c8ac6ca --- /dev/null +++ b/orm/services/region_manager/rms/tests/model/test_url_parms.py @@ -0,0 +1,66 @@ +"""url parms unittests module.""" +import unittest + +from rms.model import url_parm + +parms = {'status': 'functional', 'city': 'Los Angeles', 'clli': 'clli_0', + 'zip': '012345', 'country': 'US', 'metadata': ['key_1:value_1', + 'key_2:value_2'], + 'valet': 'true', 'state': 'Cal', 'street': 'Blv st', + 'rangerAgentVersion': 'ranger_agent 1.0', 'osversion': 'kilo', + 'type': 'location_type_0', 'regionname': 'lcp 0'} + +parms_meta_none = {'status': 'functional', 'city': 'Los Angeles', + 'clli': 'clli_0', + 'zip': '012345', 'country': 'US', + 'metadata': None, + 'valet': 'true', 'state': 'Cal', 'street': 'Blv st', + 'rangerAgentVersion': 'ranger_agent 1.0', 'osversion': 'kilo', + 'type': 'location_type_0', 'regionname': 'lcp 0'} + +output_parms = {'address_city': 'Los Angeles', 'clli': 'clli_0', + 'name': 'lcp 0', 'open_stack_version': 'kilo', + 'address_street': 'Blv st', 'address_state': 'Cal', + 'region_status': 'functional', 'valet': 'true', + 'ranger_agent_version': 'ranger_agent 1.0', 'address_zip': '012345', + 'address_country': 'US', 'location_type': 'location_type_0', + 'metadata': ['key_1:value_1', 'key_2:value_2']} + +regiondict_output = {'address_city': 'Los Angeles', 'clli': 'clli_0', + 'name': 'lcp 0', 'valet': 'true', + 'open_stack_version': 'kilo', 'address_country': 'US', + 'ranger_agent_version': 'ranger_agent 1.0', 'region_status': 'functional', + 'address_state': 'Cal', 'address_street': 'Blv st', + 'location_type': 'location_type_0', + 'address_zip': '012345'} +metadata_output = {'meta_data_keys': [], + 'meta_data_pairs': [{'metadata_key': 'key_1', 'metadata_value': 'value_1'}, + {'metadata_key': 'key_2', 'metadata_value': 'value_2'}], + 'ref_keys': ['key_1', 'key_2']} + + +class TestUrlParms(unittest.TestCase): + # parms init + def test_init_all(self): + obj = url_parm.UrlParms(**parms) + self.assertEqual(obj.__dict__, output_parms) + + # test build query + def test_build_query(self): + obj = url_parm.UrlParms(**parms) + regiondict, metadatadict, none = obj._build_query() + self.assertEqual(regiondict_output, regiondict) + self.assertEqual(metadata_output, metadatadict) + + # test build query metadat None + def test_build_query_meta_none(self): + obj = url_parm.UrlParms(**parms_meta_none) + regiondict, metadatadict, none = obj._build_query() + self.assertEqual(metadatadict, None) + + # test build query metadat None + def test_build_query_all_none(self): + obj = url_parm.UrlParms() + regiondict, metadatadict, none = obj._build_query() + self.assertEqual(metadatadict, None) + self.assertEqual(regiondict, None) diff --git a/orm/services/region_manager/rms/tests/services/__init__.py b/orm/services/region_manager/rms/tests/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/services/test_services.py b/orm/services/region_manager/rms/tests/services/test_services.py new file mode 100755 index 00000000..3f10f1d7 --- /dev/null +++ b/orm/services/region_manager/rms/tests/services/test_services.py @@ -0,0 +1,327 @@ +"""Services module unittests.""" +import mock +from mock import patch +from rms.services import services +# from rms.model import url_parm as parms + +from rms.tests import FunctionalTest +from rms.tests.controllers.v1.orm.resources.test_region import full_region +from rms.controllers.v2.orm.resources import regions +from pecan import conf +from rms.model import model as PyModels + + +class db(object): + def __init__(self, name=None, exp=None): + self.name = name + self.exp = exp + + def get_group(self, name=None): + if name: + return {'regions': [u'lcp_1'], + 'name': u'ccplz', + 'description': u'b'} + else: + return None + + def get_all_groups(self): + if self.exp: + raise Exception("any") + return [{'regions': [u'lcp_1'], 'name': u'ccplz', + 'description': u'b'}, {'regions': [u'lcp_1'], 'name': u'ccplz', + 'description': u'b'}] + + def add_group(self, *items): + if items[3] and "bad_region" in items[3]: + raise services.error_base.InputValueError() + + def get_regions(self, region_dict=None, metadata_dict=None, + end_point=None): + if region_dict: + return {'regions': [u'lcp_1'], + 'name': u'ccplz', + 'description': u'b'} + else: + return None + + def delete_group(self, id): + if self.exp: + raise Exception("any") + return None + + def get_region_by_id_or_name(self, id_name): + return id_name + + def add_region(self, **kw): + if self.exp: + raise Exception("any") + return True + + def update_region(self, id=None, **kw): + if self.exp == "not found": + raise services.error_base.NotFoundError(message="id not found") + elif self.exp: + raise Exception("error") + return True + + def delete_region(self, id=None, **kw): + if self.exp: + raise Exception("not deleted") + return True + + +class URlParm(object): + + def __init__(self, metadata=None, clli=None): + self.metadata = metadata + self.clli = clli + + def _build_query(self): + if self.metadata: + return (self.metadata, self.clli, None) + return (None, None, None) + + +class TestServices(FunctionalTest): + """Main test case for the Services module.""" + + def _to_wsme_from_input(self, input): + full_region = input + obj = regions.RegionsData() + obj.clli = full_region["CLLI"] + obj.name = full_region["name"] + obj.design_type = full_region["designType"] + obj.location_type = full_region["locationType"] + obj.vlcp_name = full_region["vlcpName"] + obj.id = full_region["id"] + obj.address.country = full_region["address"]["country"] + obj.address.city = full_region["address"]["city"] + obj.address.state = full_region["address"]["state"] + obj.address.street = full_region["address"]["street"] + obj.address.zip = full_region["address"]["zip"] + obj.ranger_agent_version = full_region["rangerAgentVersion"] + obj.open_stack_version = full_region["OSVersion"] + obj.metadata = full_region["metadata"] + obj.status = full_region["status"] + obj.endpoints = [] + for endpoint in full_region["endpoints"]: + obj.endpoints.append(regions.EndPoint(type=endpoint["type"], + publicurl=endpoint[ + "publicURL"])) + return obj + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_get_groups_data(self, mock_db_get_group): + services.get_groups_data('ccplz') + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db(exp=True)) + def test_get_all_groups_data_err(self, mock_db_get_group): + with self.assertRaises(Exception) as exp: + services.get_all_groups() + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_get_all_groups_data(self, mock_db_get_group): + services.get_all_groups() + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_delete_group(self, mock_db_get_group): + services.delete_group('id') + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db(exp=True)) + def test_delete_group_err(self, mock_db_get_group): + with self.assertRaises(Exception) as exp: + services.delete_group('id') + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_get_groups_empty_data(self, mock_db_get_group): + self.assertRaises(services.error_base.NotFoundError, + services.get_groups_data, None) + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_get_regions_empty_data(self, mock_db_get_group): + url_parm = URlParm() + self.assertRaises(services.error_base.NotFoundError, + services.get_regions_data, url_parm) + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_get_regions_data(self, mock_db_get_group): + url_parm = URlParm(metadata="key,value", clli="any") + services.get_regions_data(url_parm) + + @patch.object(services.data_manager_factory, 'get_data_manager') + def test_create_group_in_db_success(self, mock_get_data_manager): + """Make sure that no exception is raised.""" + services.create_group_in_db('d', 'a', 'b', ['c']) + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_create_group_in_db_not_valid_regions(self, mock_get_data_manager): + """Make sure that no exception is raised.""" + with self.assertRaises(services.error_base.NotFoundError) as exp: + services.create_group_in_db('d', 'a', 'b', ['bad_region']) + + @patch.object(services.data_manager_factory, 'get_data_manager') + def test_create_group_in_db_duplicate_entry(self, mock_get_data_manager): + """Make sure that the expected exception is raised if group exists.""" + my_manager = mock.MagicMock() + my_manager.add_group = mock.MagicMock( + side_effect=services.error_base.ConflictError( + 'test')) + mock_get_data_manager.return_value = my_manager + self.assertRaises(services.error_base.ConflictError, + services.create_group_in_db, 'd', 'a', 'b', ['c']) + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_get_region_by_id_or_name(self, mock_data_manager_factory): + result = services.get_region_by_id_or_name({"test1": "test1"}) + self.assertEqual(result, {"test1": "test1"}) + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_get_region_by_id_or_name_no_content(self, + mock_data_manager_factory): + self.assertRaises(services.error_base.NotFoundError, + services.get_region_by_id_or_name, None) + + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=Exception("any")) + def test_get_region_by_id_or_name_500(self, mock_data_manager_factory): + self.assertRaises(Exception, services.get_region_by_id_or_name, "id") + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_create_region_success(self, mock_db_get_group, + mock_get_region_id_name): + result = services.create_full_region(self._to_wsme_from_input(full_region)) + self.assertEqual(result, {"a": "b"}) + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_create_region_duplicate(self, mock_db_create_region, + mock_get_region_id_name): + duplicate = mock.MagicMock() + duplicate.side_effect = services.base_data_manager.DuplicateEntryError() + mock_db_create_region.return_value.add_region = duplicate + with self.assertRaises(services.error_base.ConflictError) as exp: + result = services.create_full_region( + self._to_wsme_from_input(full_region)) + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_create_region_validate_status_error(self, mock_db_get_group, + mock_get_region_id_name): + orig_status = full_region['status'] + full_region['status'] = "123" + allowed_status = conf.region_options.allowed_status_values[:] + with self.assertRaises(services.error_base.InputValueError) as exp: + result = services.create_full_region(self._to_wsme_from_input(full_region)) + test_ok = str(allowed_status) in exp.expected.message + self.assertEqual(test_ok, True) + full_region['status'] = orig_status + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_create_region_validate_endpoints_error(self, mock_db_get_group, + mock_get_region_id_name): + message = "" + endpoints_types_must_have = conf.region_options.endpoints_types_must_have[:] + orig_endpoint = full_region['endpoints'] + full_region['endpoints'] = [ + { + "type": "dashboards", + "publicURL": "http://horizon1.com" + }] + try: + result = services.create_full_region( + self._to_wsme_from_input(full_region)) + except services.error_base.InputValueError as exp: + message = exp.message + full_region['endpoints'] = orig_endpoint + self.assertEqual(str(endpoints_types_must_have) in str(message), True) + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db(exp=True)) + def test_create_region_validate_any_error(self, mock_db_get_group, + mock_get_region_id_name): + message = None + try: + result = services.create_full_region( + self._to_wsme_from_input(full_region)) + except Exception as exp: + message = exp.message + self.assertEqual(message, "any") + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_update_region_success(self, mock_db_get_group, + mock_get_region_id_name): + result = services.update_region('id', + self._to_wsme_from_input(full_region)) + self.assertEqual(result, {"a": "b"}) + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db(exp=True)) + def test_update_region_error(self, mock_db_get_group, + mock_get_region_id_name): + try: + result = services.update_region('id', + self._to_wsme_from_input(full_region)) + except Exception as exp: + message = exp.message + self.assertEqual(message, "error") + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db(exp="not found")) + def test_update_region_notfound_error(self, mock_db_get_group, + mock_get_region_id_name): + try: + result = services.update_region('id', + self._to_wsme_from_input(full_region)) + except services.error_base.NotFoundError as exp: + message = exp.message + self.assertEqual(message, "id not found") + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db(exp=True)) + def test_delete_region_error(self, mock_db_get_group, + mock_get_region_id_name): + try: + result = services.delete_region(self._to_wsme_from_input(full_region)) + except Exception as exp: + message = exp.message + self.assertEqual(message, "not deleted") + + @patch.object(services, 'get_region_by_id_or_name', + return_value={"a": "b"}) + @patch.object(services.data_manager_factory, 'get_data_manager', + return_value=db()) + def test_delete_region_success(self, mock_db_get_group, + mock_get_region_id_name): + result = services.delete_region(self._to_wsme_from_input(full_region)) diff --git a/orm/services/region_manager/rms/tests/storage/__init__.py b/orm/services/region_manager/rms/tests/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/storage/my_sql/__init__.py b/orm/services/region_manager/rms/tests/storage/my_sql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/storage/my_sql/test_data_manager.py b/orm/services/region_manager/rms/tests/storage/my_sql/test_data_manager.py new file mode 100755 index 00000000..7f3603d9 --- /dev/null +++ b/orm/services/region_manager/rms/tests/storage/my_sql/test_data_manager.py @@ -0,0 +1,293 @@ +import unittest + +import mock + +from rms.services import error_base +from rms.storage.my_sql import data_manager, data_models +from rms.storage.base_data_manager import DuplicateEntryError + +end_point_list = [{"type": "ord", + "url": "http://ord.com"}] + +meta_data_dict = {"key_1": ["value_1"]} + + +return_region = data_models.Region(region_id='SNA1') + + +class QueryObject(): + + def __init__(self, ret=None): + self.ret = ret + + def filter(self, query=None): + ret = mock.MagicMock() + if self.ret: + ret.first.return_value = return_region + return ret + else: + ret.first.return_value = None + return ret + + def filter_by(self, *args, **kwargs): + ret = mock.MagicMock() + if self.ret: + ret.first.return_value = return_region + return ret + else: + ret.first.return_value = None + return ret + + +class MyFacade(object): + """Mock EngineFacade class.""" + + def __init__(self, filter_by=None, query=None, is_ref_err=False): + """Initialize the object.""" + self._filter_by = filter_by + self._query = query + self._is_ref_err = is_ref_err + + def get_session(self): + """Make add() Raise a duplicate entry exception.""" + session = mock.MagicMock() + dup_ent = data_manager.oslo_db.exception.DBDuplicateEntry + session.add = mock.MagicMock(side_effect=dup_ent('test')) + if self._is_ref_err: + dup_ent = data_manager.oslo_db.exception.DBReferenceError + session.add = mock.MagicMock(side_effect=dup_ent('test', "", "", "")) + my_filter = mock.MagicMock() + my_filter.filter_by = mock.MagicMock(return_value=self._filter_by) + if self._query is not None: + ret = self._query + else: + ret = my_filter + session.query = mock.MagicMock(return_value=ret) + + return session + + +class TestDataManager(unittest.TestCase): + + @mock.patch.object(data_manager, 'db_session') + def test_add_region_sanity(self, mock_db_session): + """Test that no exception is raised when calling add_status_record.""" + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.add_region("reg1", "region1", "a_state", "a_country", + "a_city", "a_street", "a_zip", "a_status", "ranger_agent_ver", + "os_ver", "design_type", "loc_type", "vlcp", "clli", + end_point_list, meta_data_dict, "a_desc") + + @mock.patch.object(data_manager.db_session, 'EngineFacade', return_value=MyFacade()) + def test_add_region_duplicate_error(self, mock_db_session): + """Test that duplicate exception is raised when calling add_status_record.""" + my_data_manager = data_manager.DataManager("url", "", "") + with self.assertRaises(DuplicateEntryError): + my_data_manager.add_region("reg1", "region1", "a_state", "a_country", + "a_city", "a_street", "a_zip", "a_status", "ranger_agent_ver", + "os_ver", "design_type", "loc_type", "vlcp", "clli", + [], {}, "a_desc") + + @mock.patch.object(data_manager, 'db_session') + def test_add_group_sanity(self, mock_db_session): + """Test that no exception is raised when calling add_group.""" + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.add_group("group1", "group 1", "bla bla", ["region1"]) + + @mock.patch.object(data_manager.db_session, 'EngineFacade', return_value=MyFacade()) + def test_add_group_duplicate_error(self, mock_db_session): + """Test that ConflictError is raised when calling add_group.""" + my_data_manager = data_manager.DataManager("url", "", "") + with self.assertRaises(error_base.ConflictError): + my_data_manager.add_group("group1", "group 1", "bla bla", ["region1"]) + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade(is_ref_err=True)) + def test_add_group_reference_error(self, mock_db_session): + """Test that reference exception is raised when calling add_group.""" + my_data_manager = data_manager.DataManager("url", "", "") + with self.assertRaises(error_base.InputValueError): + my_data_manager.add_group("group1", "group 1", "bla bla", ["region1"]) + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade(query=QueryObject(ret=return_region))) + def test_get_region_id_or_name_success(self, mock_db_session): + my_data_manager = data_manager.DataManager('url', "", "") + result = my_data_manager.get_region_by_id_or_name("id") + self.assertEqual(result.id, 'SNA1') + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade( + query=QueryObject(ret=None))) + def test_get_region_id_or_name_None(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + result = my_data_manager.get_region_by_id_or_name("id") + self.assertEqual(result, None) + + @mock.patch.object(data_manager.db_session, 'EngineFacade') + def test_get_region_id_or_name_error(self, mock_db_session): + mock_get_session = mock.MagicMock() + mock_get_session.get_session.side_effect = ValueError('test') + mock_db_session.return_value = mock_get_session + + my_data_manager = data_manager.DataManager("url", "", "") + self.assertRaises(ValueError, my_data_manager.get_region_by_id_or_name, + "id") + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade( + filter_by=[return_region])) + def test_get_regions(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + result = my_data_manager.get_regions( + region_filters_dict={"meta_data_key": "1"}, + meta_data_dict=None, end_point_dict=None) + self.assertEqual(result[0].id, return_region.region_id) + + # Test that no exception is raised on the other successful flow + mock_db_session.return_value = mock.MagicMock() + my_data_manager.get_regions( + region_filters_dict=None, + meta_data_dict={"meta_data_keys": ["1"], "meta_data_pairs": []}, + end_point_dict=end_point_list[0]) + + @mock.patch.object(data_manager.db_session, 'EngineFacade') + def test_get_all_regions(self, mock_db_session): + all_regions = [return_region] + mock_db_session.return_value = MyFacade(query=all_regions) + my_data_manager = data_manager.DataManager("url", "", "") + + result = my_data_manager.get_all_regions() + self.assertEqual(len(result), len(all_regions)) + self.assertEqual(result[0].id, all_regions[0].region_id) + + @mock.patch.object(data_manager, 'db_session') + def test_update_region_sanity(self, mock_db_session): + """Test that no exception is raised when calling update_region.""" + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.update_region("reg1", "region1", "region_name", + "a_state", "a_country", + "a_city", "a_street", "a_zip", "a_status", + "ranger_agent_ver", + "os_ver", "design_type", "loc_type", + "vlcp", "clli", + end_point_list, meta_data_dict, "a_desc") + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade( + query=QueryObject(ret=None))) + def test_update_region_region_not_found(self, mock_engine_facade): + """Test that NotFoundError is raised when calling update_region.""" + my_data_manager = data_manager.DataManager("url", "", "") + self.assertRaises(data_manager.ServiceBase.NotFoundError, + my_data_manager.update_region, "reg1", "region1", + "region_name", + "a_state", "a_country", + "a_city", "a_street", "a_zip", "a_status", + "ranger_agent_ver", + "os_ver", "design_type", "loc_type", + "vlcp", "clli", + end_point_list, meta_data_dict, "a_desc") + + @mock.patch.object(data_manager.db_session, 'EngineFacade') + def test_update_region_other_error(self, mock_engine_facade): + """Test that ValueError is raised when calling update_region.""" + mock_session = mock.MagicMock() + mock_session.get_session.side_effect = ValueError('test') + mock_engine_facade.return_value = mock_session + my_data_manager = data_manager.DataManager("url", "", "") + self.assertRaises(ValueError, + my_data_manager.update_region, "reg1", "region1", + "region_name", + "a_state", "a_country", + "a_city", "a_street", "a_zip", "a_status", + "ranger_agent_ver", + "os_ver", "design_type", "loc_type", + "vlcp", "clli", + end_point_list, meta_data_dict, "a_desc") + + @mock.patch.object(data_manager, 'db_session') + def test_delete_region_sanity(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.delete_region("region") + + @mock.patch.object(data_manager, 'db_session') + def test_add_meta_data_to_region_sanity(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.add_meta_data_to_region('region', {'meta': 'data'}) + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade( + query=QueryObject(ret=None))) + def test_add_meta_data_to_region_region_not_found(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + result = my_data_manager.add_meta_data_to_region('region', + {'meta': 'data'}) + self.assertIsNone(result) + + @mock.patch.object(data_manager.db_session, 'EngineFacade') + def test_add_meta_data_to_region_error(self, mock_db_session): + mock_begin = mock.MagicMock() + mock_begin.begin.side_effect = data_manager.oslo_db.exception.DBDuplicateEntry( + 'test') + mock_get_session = mock.MagicMock() + mock_get_session.get_session.return_value = mock_begin + mock_db_session.return_value = mock_get_session + + my_data_manager = data_manager.DataManager("url", "", "") + + self.assertRaises(data_manager.error_base.ConflictError, + my_data_manager.add_meta_data_to_region, + 'region', {'meta': 'data'}) + + @mock.patch.object(data_manager, 'db_session') + def test_update_region_meta_data_sanity(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.update_region_meta_data('region', {'meta': 'data'}) + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade( + query=QueryObject(ret=None))) + def test_update_region_meta_data_region_not_found(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + self.assertRaises(data_manager.error_base.NotFoundError, + my_data_manager.update_region_meta_data, + 'region', {'meta': 'data'}) + + @mock.patch.object(data_manager, 'db_session') + def test_delete_region_metadata_sanity(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.delete_region_metadata('region', {'meta': 'data'}) + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade( + query=QueryObject(ret=None))) + def test_delete_region_metadata_region_not_found(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + self.assertRaises(data_manager.error_base.NotFoundError, + my_data_manager.delete_region_metadata, + 'region', {'meta': 'data'}) + + @mock.patch.object(data_manager, 'db_session') + def test_update_region_status_sanity(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + my_data_manager.update_region_status('region', 'status') + + @mock.patch.object(data_manager.db_session, 'EngineFacade', + return_value=MyFacade( + query=QueryObject(ret=None))) + def test_update_region_status_region_not_found(self, mock_db_session): + my_data_manager = data_manager.DataManager("url", "", "") + self.assertRaises(data_manager.error_base.NotFoundError, + my_data_manager.update_region_status, + 'region', 'status') + + @mock.patch.object(data_manager, 'db_session') + def test_group_functions_sanity(self, mock_db_session): + """Test that no exception is raised when calling group functions.""" + my_data_manager = data_manager.DataManager("url", "", "") + + my_data_manager.delete_group('group') + my_data_manager.get_group('group') + my_data_manager.update_group('id', 'name', 'description', ['region']) + my_data_manager.get_all_groups() diff --git a/orm/services/region_manager/rms/tests/storage/test_base_data_manager.py b/orm/services/region_manager/rms/tests/storage/test_base_data_manager.py new file mode 100644 index 00000000..c8726996 --- /dev/null +++ b/orm/services/region_manager/rms/tests/storage/test_base_data_manager.py @@ -0,0 +1,44 @@ +import unittest + +from rms.storage.base_data_manager import BaseDataManager + + +class BaseDataManagerTests(unittest.TestCase): + + def test_base_data_manager_add_region_not_implemented(self): + """ Check if creating an instance and calling add_region + method fail""" + with self.assertRaises(NotImplementedError): + BaseDataManager("", "", "").add_region('1', '2', '3', '4', '5', '6', '7', + '8', '9', '10', '11', '12', '13', + '14', '15', '16', '17') + + def test_base_data_manager_get_regions_not_implemented(self): + """ Check if creating an instance and calling get_regions + method fail""" + with self.assertRaises(NotImplementedError): + BaseDataManager("", "", "").get_regions('1', '2', '3') + + def test_base_data_manager_get_all_regions_not_implemented(self): + """ Check if creating an instance and calling get_all_regions + method fail""" + with self.assertRaises(NotImplementedError): + BaseDataManager("", "", "").get_all_regions() + + def test_base_data_manager_add_group_not_implemented(self): + """ Check if creating an instance and calling add_group + method fail""" + with self.assertRaises(NotImplementedError): + BaseDataManager("", "", "").add_group("1", "2", "3", "4") + + def test_base_data_manager_get_group_not_implemented(self): + """ Check if creating an instance and calling get_group + method fail""" + with self.assertRaises(NotImplementedError): + BaseDataManager("", "", "").get_group("1") + + def test_base_data_manager_get_all_groups_not_implemented(self): + """ Check if creating an instance and calling get_all_groups + method fail""" + with self.assertRaises(NotImplementedError): + BaseDataManager("", "", "").get_all_groups() diff --git a/orm/services/region_manager/rms/tests/storage/test_data_manager_factory.py b/orm/services/region_manager/rms/tests/storage/test_data_manager_factory.py new file mode 100644 index 00000000..3b2f73ae --- /dev/null +++ b/orm/services/region_manager/rms/tests/storage/test_data_manager_factory.py @@ -0,0 +1,17 @@ +from mock import patch +import unittest + +from rms.storage import data_manager_factory +from rms.storage.my_sql.data_manager import DataManager +from rms.storage.my_sql import data_manager + + +class StorageFactoryTests(unittest.TestCase): + + @patch.object(data_manager_factory, 'conf') + @patch.object(data_manager, 'db_session') + def test_get_data_manager(self, conf_mock, db_session_mock): + """ Check the returned object from get_region_resource_id_status_connection + is instance of DataManager""" + obj = data_manager_factory.get_data_manager() + self.assertIsInstance(obj, DataManager) diff --git a/orm/services/region_manager/rms/tests/test_configuration.py b/orm/services/region_manager/rms/tests/test_configuration.py new file mode 100755 index 00000000..e2f4c454 --- /dev/null +++ b/orm/services/region_manager/rms/tests/test_configuration.py @@ -0,0 +1,15 @@ +"""Get configuration module unittests.""" +from mock import patch +from rms.controllers import configuration as root +from rms.tests import FunctionalTest + + +class TestGetConfiguration(FunctionalTest): + """Main get configuration test case.""" + + @patch.object(root.utils, 'report_config', return_value='12345') + @patch.object(root, 'authentication') + def test_get_configuration_success(self, mock_authentication, input): + """Test get_configuration returns the expected value on success.""" + response = self.app.get('/configuration') + self.assertEqual(response.json, '12345') diff --git a/orm/services/region_manager/rms/tests/test_logs.py b/orm/services/region_manager/rms/tests/test_logs.py new file mode 100755 index 00000000..24f122e4 --- /dev/null +++ b/orm/services/region_manager/rms/tests/test_logs.py @@ -0,0 +1,47 @@ +import json +import logging + +from mock import patch, MagicMock +from wsme.exc import ClientSideError + +import rms.controllers.logs as logs +from rms.controllers.logs import LogsController as logs_controller +from rms.tests import FunctionalTest + + +class TestGetConfiguration(FunctionalTest): + + @patch.object(logging, 'getLogger') + def test_change_log_level_success(self, input): + logs_controller._change_log_level(50) + + @patch.object(logs_controller, '_change_log_level') + @patch.object(logs, 'authentication') + def test_put_success(self, mock_authentication, err): + response = self.app.put('/logs/info', expect_errors=True) + self.assertEqual(response.status_int, 201) + + @patch.object(logs_controller, '_change_log_level') + @patch.object(logs.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '333', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + @patch.object(logs, 'authentication') + def test_put_failed_wrong_log_level(self, mock_auth, err, err2): + temp_request = logs.request + logs.request = MagicMock() + + response = self.app.put('/logs/info000', expect_errors=True) + + logs.request = temp_request + + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual("333", result_json['transaction_id']) + self.assertEqual(500, result_json['code']) diff --git a/orm/services/region_manager/rms/tests/tests_lcp_controller.py b/orm/services/region_manager/rms/tests/tests_lcp_controller.py new file mode 100755 index 00000000..fff36de2 --- /dev/null +++ b/orm/services/region_manager/rms/tests/tests_lcp_controller.py @@ -0,0 +1,195 @@ +from mock import patch, MagicMock +from wsme.exc import ClientSideError + +from rms.services import services +from rms.controllers import lcp_controller + +from rms.model.model import RegionData, Regions, EndPoint +from rms.services.error_base import NotFoundError + +import json + +from rms.controllers import lcp_controller as lcps + +from rms.tests import FunctionalTest + + +TEST_REGIONS_DATA = [ + { + "status": "1", + "vLCP_name": "n/a", + "ORD_EP": "http://ord1.com", + "horizon_EP": "http://horizon1.com", + "design_type": "n/a", + "AIC_version": "ranger_agent1.0", + "id": "SNA1", + "OS_version": "kilo", + "keystone_EP": "http://identity1.com", + "zone_name": "SNA1", + "location_type": "n/a" + }, + { + "status": "0", + "vLCP_name": "n/a", + "ORD_EP": "http://ord2.com", + "horizon_EP": "http://horizon2.com", + "design_type": "n/a", + "AIC_version": "ranger_agent1.5", + "id": "SNA2", + "OS_version": "kilo", + "keystone_EP": "http://identity2.com", + "zone_name": "SNA2", + "location_type": "n/a" + }, +] + +end_point_ord_1 = EndPoint(publicurl="http://ord1.com", + type="ord") +end_point_identity_1 = EndPoint(publicurl="http://identity1.com", + type="identity") +end_point_horizon_1 = EndPoint(publicurl="http://horizon1.com", + type="dashboard") + +end_point_ord_2 = EndPoint(publicurl="http://ord2.com", + type="ord") +end_point_identity_2 = EndPoint(publicurl="http://identity2.com", + type="identity") +end_point_horizon_2 = EndPoint(publicurl="http://horizon2.com", + type="dashboard") +end_points_1 = [end_point_ord_1, end_point_identity_1, end_point_horizon_1] +end_points_2 = [end_point_ord_2, end_point_identity_2, end_point_horizon_2] + +region_data_sna1 = RegionData(status="functional", id="SNA1", name="SNA 1", + clli="n/a", ranger_agent_version="ranger_agent1.0", design_type="n/a", + location_type="n/a", vlcp_name="n/a", open_stack_version="kilo", + endpoints=end_points_1) +region_data_sna2 = RegionData(status="down", id="SNA2", name="SNA 2", + clli="n/a", ranger_agent_version="ranger_agent1.5", design_type="n/a", + location_type="n/a", vlcp_name="n/a", open_stack_version="kilo", + endpoints=end_points_2) +region_data_no_endpoints = RegionData(status="functional", id="SNA2", name="SNA 2", + clli="n/a", ranger_agent_version="ranger_agent1.5", design_type="n/a", + location_type="n/a", vlcp_name="n/a", open_stack_version="kilo") + +regions_mock = Regions([region_data_sna1, region_data_sna2]) + + +class TestLcpController(FunctionalTest): + + @patch.object(services, 'get_regions_data', return_value=regions_mock) + def test_get_zones_success(self, regions_data): + zones = lcps.get_zones() + self.assertEqual(zones, TEST_REGIONS_DATA) + + @patch.object(services, 'get_regions_data', + side_effect=NotFoundError(message="No regions found!")) + def test_get_zones_get_regions_data_error(self, regions_data): + zones = lcps.get_zones() + self.assertEqual(zones, []) + + # Test get_all in lcp_controller + @patch.object(lcp_controller, 'get_zones', return_value=TEST_REGIONS_DATA) + @patch.object(lcp_controller, 'authentication') + def test_get_all_success(self, mock_authentication, get_zones): + + response = self.app.get('/lcp', expect_errors=True) + response_json = json.loads(response.body) + + self.assertEqual(response_json, TEST_REGIONS_DATA) + self.assertEqual(response.status_int, 200) + + @patch.object(lcp_controller, 'get_zones', + side_effect=Exception("unknown error")) + @patch.object(lcp_controller.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '999', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + @patch.object(lcp_controller, 'authentication') + def test_get_all_get_zones_error(self, mock_auth, err, get_zones): + temp_request = lcp_controller.request + lcp_controller.request = MagicMock() + + response = self.app.get('/lcp', expect_errors=True) + + lcp_controller.request = temp_request + + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual("999", result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + # Test get_one in lcp_controller + @patch.object(lcp_controller, 'get_zones', return_value=TEST_REGIONS_DATA) + @patch.object(lcp_controller, 'authentication') + def test_get_one_success(self, mock_authentication, get_zones): + + response = self.app.get('/lcp/SNA1/', expect_errors=True) + response_json = json.loads(response.body) + + self.assertEqual(response_json["zone_name"], "SNA1") + self.assertEqual(response_json["id"], "SNA1") + self.assertEqual(response.status_int, 200) + + @patch.object(lcp_controller, 'get_zones', + side_effect=Exception("unknown error")) + @patch.object(lcp_controller.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 500, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '555', + 'message': 'test', + 'details': 'test' + }), status_code=500)) + @patch.object(lcp_controller, 'authentication') + def test_get_one_get_zones_error(self, mock_auth, err, get_zones): + temp_request = lcp_controller.request + lcp_controller.request = MagicMock() + + response = self.app.get('/lcp/1234', expect_errors=True) + + lcp_controller.request = temp_request + + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual("555", result_json['transaction_id']) + self.assertEqual(500, result_json['code']) + + @patch.object(lcp_controller, 'get_zones', return_value=[]) + @patch.object(lcp_controller.err_utils, 'get_error', + return_value=ClientSideError(json.dumps({ + 'code': 404, + 'type': 'test', + 'created': '0.0', + 'transaction_id': '444', + 'message': 'test', + 'details': 'test' + }), status_code=404)) + @patch.object(lcp_controller, 'authentication') + def test_get_one_not_found(self, mock_auth, err, get_zones): + temp_request = lcp_controller.request + lcp_controller.request = MagicMock() + + response = self.app.get('/lcp/1234', expect_errors=True) + + lcp_controller.request = temp_request + + dict_body = json.loads(response.body) + result_json = json.loads(dict_body['faultstring']) + + self.assertEqual("444", result_json['transaction_id']) + self.assertEqual(404, result_json['code']) + + # Test get_one in lcp_controller + def test_build_zone_response_with_missing_endpoints(self,): + result = lcps.build_zone_response(region_data_no_endpoints) + self.assertEqual("", result['keystone_EP']) + self.assertEqual("", result['horizon_EP']) + self.assertEqual("", result['ORD_EP']) diff --git a/orm/services/region_manager/rms/tests/utils/__init__.py b/orm/services/region_manager/rms/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/tests/utils/test_authentication.py b/orm/services/region_manager/rms/tests/utils/test_authentication.py new file mode 100755 index 00000000..d0dc925c --- /dev/null +++ b/orm/services/region_manager/rms/tests/utils/test_authentication.py @@ -0,0 +1,80 @@ +"""Authentication utilities module unittests.""" +import mock +from rms.utils import authentication +from rms.tests import FunctionalTest + + +class TestGetConfiguration(FunctionalTest): + """Main authentication test case.""" + + @mock.patch.object(authentication.policy, 'authorize') + @mock.patch.object(authentication, '_get_keystone_ep') + @mock.patch.object(authentication, '_is_authorization_enabled') + def test_authorize_success(self, mock_iae, mock_gke, mock_authorize): + request = mock.MagicMock() + action = 'test:test' + + # Success when authentication is disabled + mock_iae.return_value = False + authentication.authorize(request, action) + + # Success when authentication is enabled + mock_iae.return_value = True + authentication.authorize(request, action) + + def mock_authorize_no_keystone(self, *args, **kwargs): + self.assertIsNone(kwargs['keystone_ep']) + + @mock.patch.object(authentication, 'policy') + @mock.patch.object(authentication, '_get_keystone_ep') + @mock.patch.object(authentication, '_is_authorization_enabled') + def test_authorize_gke_failed(self, mock_iae, mock_gke, mock_policy): + request = mock.MagicMock() + action = 'test:test' + + # Success when authentication is disabled + mock_iae.return_value = False + authentication.authorize(request, action) + + # Success when authentication is enabled + mock_iae.return_value = True + authentication.authorize(request, action) + + @mock.patch.object(authentication, 'policy') + @mock.patch.object(authentication, '_get_keystone_ep', + side_effect=ValueError('test')) + @mock.patch.object(authentication, '_is_authorization_enabled', + return_value=True) + def test_authorize_gke_failed(self, mock_iae, mock_gke, mock_policy): + request = mock.MagicMock() + action = 'test:test' + + mock_policy.authorize = self.mock_authorize_no_keystone + authentication.authorize(request, action) + + def test_is_authorization_enabled(self): + app_conf = mock.MagicMock() + + app_conf.authentication.enabled = True + self.assertTrue(authentication._is_authorization_enabled(app_conf)) + + app_conf.authentication.enabled = False + self.assertFalse(authentication._is_authorization_enabled(app_conf)) + + @mock.patch.object(authentication.RegionService, + 'get_region_by_id_or_name') + def test_get_keystone_ep_success(self, mock_grbion): + region = mock.MagicMock() + keystone_ep = mock.MagicMock() + keystone_ep.type = 'identity' + keystone_ep.publicurl = 'test' + region.endpoints = [keystone_ep] + mock_grbion.return_value = region + + self.assertEqual(authentication._get_keystone_ep('region'), + keystone_ep.publicurl) + + @mock.patch.object(authentication.RegionService, + 'get_region_by_id_or_name') + def test_get_keystone_ep_no_keystone_ep(self, mock_grbion): + self.assertIsNone(authentication._get_keystone_ep('region')) diff --git a/orm/services/region_manager/rms/utils/__init__.py b/orm/services/region_manager/rms/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms/utils/authentication.py b/orm/services/region_manager/rms/utils/authentication.py new file mode 100755 index 00000000..ae93d2ac --- /dev/null +++ b/orm/services/region_manager/rms/utils/authentication.py @@ -0,0 +1,53 @@ +import logging + +from keystone_utils import tokens +from orm_common.policy import policy +from orm_common.utils import api_error_utils as err_utils + +from pecan import conf + +from rms.services import services as RegionService + + +logger = logging.getLogger(__name__) + + +def _get_keystone_ep(auth_region): + result = RegionService.get_region_by_id_or_name(auth_region) + for ep in result.endpoints: + if ep.type == 'identity': + return ep.publicurl + + # Keystone EP not found + return None + + +def authorize(request, action): + if not _is_authorization_enabled(conf): + return + + auth_region = request.headers.get('X-Auth-Region') + try: + keystone_ep = _get_keystone_ep(auth_region) + except Exception: + # Failed to find Keystone EP - we'll set it to None instead of failing + # because the rule might be to let everyone pass + keystone_ep = None + + policy.authorize(action, request, conf, keystone_ep=keystone_ep) + + +def _is_authorization_enabled(app_conf): + return app_conf.authentication.enabled + + +def get_token_conf(app_conf): + mech_id = app_conf.authentication.mech_id + mech_password = app_conf.authentication.mech_pass + # RMS URL is not necessary since this service is RMS + rms_url = '' + tenant_name = app_conf.authentication.tenant_name + keystone_version = app_conf.authentication.keystone_version + conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name, + keystone_version) + return conf diff --git a/orm/services/region_manager/rms_mock/MANIFEST.in b/orm/services/region_manager/rms_mock/MANIFEST.in new file mode 100644 index 00000000..c922f11a --- /dev/null +++ b/orm/services/region_manager/rms_mock/MANIFEST.in @@ -0,0 +1 @@ +recursive-include public * diff --git a/orm/services/region_manager/rms_mock/__init__.py b/orm/services/region_manager/rms_mock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms_mock/config.py b/orm/services/region_manager/rms_mock/config.py new file mode 100644 index 00000000..eef79b91 --- /dev/null +++ b/orm/services/region_manager/rms_mock/config.py @@ -0,0 +1,56 @@ +# Server Specific Configurations +server = { + 'port': '8082', + 'host': '0.0.0.0' +} + +# Pecan Application Configurations +app = { + 'root': 'rms_mock.controllers.root.RootController', + 'modules': ['rms_mock'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/rms_mock/templates', + 'debug': True, + 'errors': { + 404: '/error/404', + '__force_dict__': True + } +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'mock': {'level': 'DEBUG', 'handlers': ['console'], + 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], + 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +# Custom Configurations must be in Python dictionary format:: +# +# foo = {'bar':'baz'} +# +# All configurations are accessible at:: +# pecan.conf diff --git a/orm/services/region_manager/rms_mock/data/zones.json b/orm/services/region_manager/rms_mock/data/zones.json new file mode 100644 index 00000000..fa83cdef --- /dev/null +++ b/orm/services/region_manager/rms_mock/data/zones.json @@ -0,0 +1,4002 @@ +[ + { + "site": { + "location_type": "Kaka", + "id": 0 + }, + "zonePurpose": { + "name": "56d560c0fdf7f8f3d54e0975", + "id": 1809 + }, + "powerType": { + "name": "56d560c0beb6b59b6971e4af", + "id": 5591 + }, + "designType": { + "name": "56d560c0e888eae4b4addf00", + "id": 7554 + }, + "zoneType": { + "name": "56d560c0b8cc195e24fc71f1", + "id": 5290 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 65, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-09-21" + }, + "created": { + "date": "2015-07-17" + }, + "name": "56d560c0077bed9896419fed", + "id": 0 + }, + { + "site": { + "location_type": "Gerber", + "id": 1 + }, + "zonePurpose": { + "name": "56d560c00be07167daaca128", + "id": 9905 + }, + "powerType": { + "name": "56d560c0df982f8d8b9a31bc", + "id": 1502 + }, + "designType": { + "name": "56d560c0444b508f593e5212", + "id": 5758 + }, + "zoneType": { + "name": "56d560c0b34cab98f98d7810", + "id": 3283 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 87, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-07-13" + }, + "created": { + "date": "2014-12-31" + }, + "name": "56d560c0fb8c3a1e604ea38a", + "id": 1 + }, + { + "site": { + "location_type": "Darlington", + "id": 2 + }, + "zonePurpose": { + "name": "56d560c07c31e09908d572a2", + "id": 4989 + }, + "powerType": { + "name": "56d560c0bacb2d27d3478ecb", + "id": 7287 + }, + "designType": { + "name": "56d560c0671119915dd2bfa3", + "id": 8755 + }, + "zoneType": { + "name": "56d560c08b54a95277893bec", + "id": 44 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 31, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-07-28" + }, + "created": { + "date": "2015-01-02" + }, + "name": "56d560c0ad15ee6584ea7f6a", + "id": 2 + }, + { + "site": { + "location_type": "Grapeview", + "id": 3 + }, + "zonePurpose": { + "name": "56d560c0e9ee854d68341aeb", + "id": 1559 + }, + "powerType": { + "name": "56d560c07241c8733f4930cf", + "id": 2162 + }, + "designType": { + "name": "56d560c0f8faf5af9e3ebe31", + "id": 3085 + }, + "zoneType": { + "name": "56d560c077c836936e75abb2", + "id": 1721 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 52, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-07-27" + }, + "created": { + "date": "2015-06-01" + }, + "name": "56d560c0287fb8c701fc9e38", + "id": 3 + }, + { + "site": { + "location_type": "Harborton", + "id": 4 + }, + "zonePurpose": { + "name": "56d560c0208f030996c82344", + "id": 1738 + }, + "powerType": { + "name": "56d560c08c42f6f945f944f1", + "id": 9090 + }, + "designType": { + "name": "56d560c06b7def38de502709", + "id": 8537 + }, + "zoneType": { + "name": "56d560c079b0fc1cef6d2222", + "id": 5724 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 40, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-07-22" + }, + "created": { + "date": "2016-01-02" + }, + "name": "56d560c0b77f19e071949de8", + "id": 4 + }, + { + "site": { + "location_type": "Loma", + "id": 5 + }, + "zonePurpose": { + "name": "56d560c0ec3e2ac7b2c0f152", + "id": 8000 + }, + "powerType": { + "name": "56d560c0d0ec627f037015bb", + "id": 9791 + }, + "designType": { + "name": "56d560c0bd04e377a0dd0f2c", + "id": 2792 + }, + "zoneType": { + "name": "56d560c0f6d83b30489af6f5", + "id": 3254 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 69, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-09-10" + }, + "created": { + "date": "2015-06-28" + }, + "name": "56d560c0f1fa91683d500f95", + "id": 5 + }, + { + "site": { + "location_type": "Conestoga", + "id": 6 + }, + "zonePurpose": { + "name": "56d560c053d0ef55f8ea6d97", + "id": 9496 + }, + "powerType": { + "name": "56d560c079ffaf8bce92e04d", + "id": 3434 + }, + "designType": { + "name": "56d560c02416964249a44183", + "id": 3247 + }, + "zoneType": { + "name": "56d560c0186ca3357e09d971", + "id": 2673 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 64, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-11-06" + }, + "created": { + "date": "2015-04-07" + }, + "name": "56d560c0661fa2553b08bf58", + "id": 6 + }, + { + "site": { + "location_type": "Robbins", + "id": 7 + }, + "zonePurpose": { + "name": "56d560c02705578ca9d2a544", + "id": 3572 + }, + "powerType": { + "name": "56d560c08afb35dcb4059ea2", + "id": 4462 + }, + "designType": { + "name": "56d560c0d56b34acf3db716e", + "id": 8982 + }, + "zoneType": { + "name": "56d560c033901a4be0ac0f75", + "id": 3828 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 48, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-05-13" + }, + "created": { + "date": "2015-11-14" + }, + "name": "56d560c097454dbe336965d4", + "id": 7 + }, + { + "site": { + "location_type": "Levant", + "id": 8 + }, + "zonePurpose": { + "name": "56d560c036b795db4f296524", + "id": 8868 + }, + "powerType": { + "name": "56d560c0960ffb1caa91bc60", + "id": 6739 + }, + "designType": { + "name": "56d560c0e7171703cc080421", + "id": 6038 + }, + "zoneType": { + "name": "56d560c0daa037ee05426ba2", + "id": 5118 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 75, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-01-25" + }, + "created": { + "date": "2014-12-20" + }, + "name": "56d560c008fa52b023b29fc5", + "id": 8 + }, + { + "site": { + "location_type": "Ruckersville", + "id": 9 + }, + "zonePurpose": { + "name": "56d560c0b4c0c5f87861b820", + "id": 2198 + }, + "powerType": { + "name": "56d560c0dd9c2d8249119d7d", + "id": 6208 + }, + "designType": { + "name": "56d560c00ae96c94d4e5130c", + "id": 6494 + }, + "zoneType": { + "name": "56d560c04e9cd43ba9b82a99", + "id": 8232 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 60, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-05-20" + }, + "created": { + "date": "2014-09-12" + }, + "name": "56d560c0ef468199f4ce9fbe", + "id": 9 + }, + { + "site": { + "location_type": "Darbydale", + "id": 10 + }, + "zonePurpose": { + "name": "56d560c0568a1bc602c2a516", + "id": 4225 + }, + "powerType": { + "name": "56d560c016040e62ccfb0d91", + "id": 3783 + }, + "designType": { + "name": "56d560c0f3ba7341a17d8185", + "id": 2087 + }, + "zoneType": { + "name": "56d560c0d5b6fd0898c2e906", + "id": 836 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 12, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-12-22" + }, + "created": { + "date": "2014-10-21" + }, + "name": "56d560c03585ccd60db30317", + "id": 10 + }, + { + "site": { + "location_type": "Russellville", + "id": 11 + }, + "zonePurpose": { + "name": "56d560c07db2ad46c813de9a", + "id": 6894 + }, + "powerType": { + "name": "56d560c0a0caca1bcd68427e", + "id": 9151 + }, + "designType": { + "name": "56d560c00203072ed32a04de", + "id": 6937 + }, + "zoneType": { + "name": "56d560c0e70b8710fa947299", + "id": 2205 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 67, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-01-22" + }, + "created": { + "date": "2014-12-21" + }, + "name": "56d560c0e4749a49c3aa5c1a", + "id": 11 + }, + { + "site": { + "location_type": "Torboy", + "id": 12 + }, + "zonePurpose": { + "name": "56d560c0d2dfe463240d2aec", + "id": 6800 + }, + "powerType": { + "name": "56d560c0e9dfc4d729a79d88", + "id": 5345 + }, + "designType": { + "name": "56d560c0269658c50beef835", + "id": 2533 + }, + "zoneType": { + "name": "56d560c0cc3b33209a39b847", + "id": 6875 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 2, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-10-05" + }, + "created": { + "date": "2014-07-08" + }, + "name": "56d560c053960ab24ca0805e", + "id": 12 + }, + { + "site": { + "location_type": "Sunbury", + "id": 13 + }, + "zonePurpose": { + "name": "56d560c080bee9492ac82b1d", + "id": 4413 + }, + "powerType": { + "name": "56d560c02dc6868b70818fa2", + "id": 8623 + }, + "designType": { + "name": "56d560c07f9ccb7b84fad7bb", + "id": 2265 + }, + "zoneType": { + "name": "56d560c0ca5601d3f78203aa", + "id": 7699 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 71, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-08-15" + }, + "created": { + "date": "2014-10-25" + }, + "name": "56d560c03d488b2cc84a1dda", + "id": 13 + }, + { + "site": { + "location_type": "Lynn", + "id": 14 + }, + "zonePurpose": { + "name": "56d560c09a23c4ecd0ed752c", + "id": 9861 + }, + "powerType": { + "name": "56d560c00f300e4c7e8b1cd0", + "id": 8714 + }, + "designType": { + "name": "56d560c08aa0f77bd0795e90", + "id": 2862 + }, + "zoneType": { + "name": "56d560c0aad9980362e9a1b6", + "id": 6874 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 18, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-10-18" + }, + "created": { + "date": "2014-04-30" + }, + "name": "56d560c051688c89f3de807c", + "id": 14 + }, + { + "site": { + "location_type": "Nash", + "id": 15 + }, + "zonePurpose": { + "name": "56d560c0b7308257f2b028c0", + "id": 4799 + }, + "powerType": { + "name": "56d560c0d03dd4e556b6f928", + "id": 2694 + }, + "designType": { + "name": "56d560c05c8de483d88ba5a8", + "id": 73 + }, + "zoneType": { + "name": "56d560c07a4a13778f1e48ac", + "id": 8245 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 23, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-09-01" + }, + "created": { + "date": "2015-11-22" + }, + "name": "56d560c0a29e37498b71e70d", + "id": 15 + }, + { + "site": { + "location_type": "Cascades", + "id": 16 + }, + "zonePurpose": { + "name": "56d560c045a7ed95d8ad744b", + "id": 6161 + }, + "powerType": { + "name": "56d560c051cf90347cbad974", + "id": 6327 + }, + "designType": { + "name": "56d560c01f480e20a6cc9ad0", + "id": 6763 + }, + "zoneType": { + "name": "56d560c0b111b46a36a74504", + "id": 6658 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 99, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-05-05" + }, + "created": { + "date": "2016-02-09" + }, + "name": "56d560c057f556e9c3c1c6b6", + "id": 16 + }, + { + "site": { + "location_type": "Alamo", + "id": 17 + }, + "zonePurpose": { + "name": "56d560c0fe44e205be539cf3", + "id": 6946 + }, + "powerType": { + "name": "56d560c09cc6b3e45ff59c07", + "id": 4407 + }, + "designType": { + "name": "56d560c0a980159b631c9e5b", + "id": 3171 + }, + "zoneType": { + "name": "56d560c09d49a4761a48ffe3", + "id": 4357 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 99, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2016-02-01" + }, + "created": { + "date": "2014-07-31" + }, + "name": "56d560c00cf5a2760cf08e41", + "id": 17 + }, + { + "site": { + "location_type": "Frystown", + "id": 18 + }, + "zonePurpose": { + "name": "56d560c05952a93c02b6b576", + "id": 7733 + }, + "powerType": { + "name": "56d560c056fdce251c91a768", + "id": 7114 + }, + "designType": { + "name": "56d560c0ef82a3e2338ea7d0", + "id": 5849 + }, + "zoneType": { + "name": "56d560c0a65759eb60f29950", + "id": 8347 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 10, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-10-04" + }, + "created": { + "date": "2014-04-02" + }, + "name": "56d560c0241cde7ce6c58188", + "id": 18 + }, + { + "site": { + "location_type": "Brantleyville", + "id": 19 + }, + "zonePurpose": { + "name": "56d560c06a25d6dc604f01e0", + "id": 1082 + }, + "powerType": { + "name": "56d560c03d5d6ad44a79d1a1", + "id": 7015 + }, + "designType": { + "name": "56d560c0151053ff69026777", + "id": 3352 + }, + "zoneType": { + "name": "56d560c0fd123e68b408ed90", + "id": 6766 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 12, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-06-30" + }, + "created": { + "date": "2016-01-25" + }, + "name": "56d560c03c73665ebdc6056b", + "id": 19 + }, + { + "site": { + "location_type": "Cannondale", + "id": 20 + }, + "zonePurpose": { + "name": "56d560c09478dd6f647504dd", + "id": 1005 + }, + "powerType": { + "name": "56d560c0bc4043e8c6deb18a", + "id": 9489 + }, + "designType": { + "name": "56d560c04be3e637fa5c092f", + "id": 9217 + }, + "zoneType": { + "name": "56d560c0edcd5d451ffc6426", + "id": 4048 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 60, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-07-08" + }, + "created": { + "date": "2015-04-09" + }, + "name": "56d560c016415f96723501d1", + "id": 20 + }, + { + "site": { + "location_type": "Leyner", + "id": 21 + }, + "zonePurpose": { + "name": "56d560c04d8e23fd99da120b", + "id": 5456 + }, + "powerType": { + "name": "56d560c0df29852762a05de6", + "id": 7454 + }, + "designType": { + "name": "56d560c0758cd1219562ffb6", + "id": 4913 + }, + "zoneType": { + "name": "56d560c056dfdc9fc398dbed", + "id": 4009 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 59, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-07-02" + }, + "created": { + "date": "2014-10-01" + }, + "name": "56d560c0ce9b895539518fe3", + "id": 21 + }, + { + "site": { + "location_type": "Gorham", + "id": 22 + }, + "zonePurpose": { + "name": "56d560c0adebb96d11f6f791", + "id": 3097 + }, + "powerType": { + "name": "56d560c013eb6c1f0105810c", + "id": 8159 + }, + "designType": { + "name": "56d560c0c4863be25cc9dfa4", + "id": 1517 + }, + "zoneType": { + "name": "56d560c06b0ca411cb84a07e", + "id": 6974 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 92, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-10-08" + }, + "created": { + "date": "2015-07-08" + }, + "name": "56d560c0e24d04e0e61880dc", + "id": 22 + }, + { + "site": { + "location_type": "Fillmore", + "id": 23 + }, + "zonePurpose": { + "name": "56d560c066c130964a8f58d6", + "id": 5072 + }, + "powerType": { + "name": "56d560c0acf54c89ed29b3b2", + "id": 8943 + }, + "designType": { + "name": "56d560c0fb24b2f45d1dc3d9", + "id": 7884 + }, + "zoneType": { + "name": "56d560c054d47b34cb47f35d", + "id": 4668 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 5, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-07-15" + }, + "created": { + "date": "2014-10-26" + }, + "name": "56d560c0126cddd5f1a987bd", + "id": 23 + }, + { + "site": { + "location_type": "Greer", + "id": 24 + }, + "zonePurpose": { + "name": "56d560c05349527b410d1e7e", + "id": 3018 + }, + "powerType": { + "name": "56d560c0da936cb67de27921", + "id": 9559 + }, + "designType": { + "name": "56d560c0724f86e6a6a6afb2", + "id": 119 + }, + "zoneType": { + "name": "56d560c001633e4d0d567b48", + "id": 6613 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 18, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-10-30" + }, + "created": { + "date": "2014-04-26" + }, + "name": "56d560c0fc34077ac5f8babc", + "id": 24 + }, + { + "site": { + "location_type": "Bison", + "id": 25 + }, + "zonePurpose": { + "name": "56d560c0247024acf62455c3", + "id": 2622 + }, + "powerType": { + "name": "56d560c0ddb7f3280470d846", + "id": 7570 + }, + "designType": { + "name": "56d560c0c9ae1912a40456ce", + "id": 1433 + }, + "zoneType": { + "name": "56d560c0bce9c1f9c3b920f5", + "id": 2768 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 60, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2016-01-23" + }, + "created": { + "date": "2016-01-21" + }, + "name": "56d560c0c79b04a794c6c887", + "id": 25 + }, + { + "site": { + "location_type": "Smeltertown", + "id": 26 + }, + "zonePurpose": { + "name": "56d560c069b4a5fddb8d7d4c", + "id": 4235 + }, + "powerType": { + "name": "56d560c05a31f12df87e86b8", + "id": 422 + }, + "designType": { + "name": "56d560c022ef9593330519b0", + "id": 9307 + }, + "zoneType": { + "name": "56d560c0a8f766c6482cd548", + "id": 3376 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 33, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-03-01" + }, + "created": { + "date": "2015-12-03" + }, + "name": "56d560c033ae93c228322b30", + "id": 26 + }, + { + "site": { + "location_type": "Lodoga", + "id": 27 + }, + "zonePurpose": { + "name": "56d560c085d1df36b4cfc240", + "id": 3966 + }, + "powerType": { + "name": "56d560c0ae10ed5eaf5d4f1b", + "id": 6883 + }, + "designType": { + "name": "56d560c099ed12e73d38d06f", + "id": 5884 + }, + "zoneType": { + "name": "56d560c06cbb0fb0ed806eb4", + "id": 6026 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 98, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-04-29" + }, + "created": { + "date": "2015-04-16" + }, + "name": "56d560c029fddda29ae94099", + "id": 27 + }, + { + "site": { + "location_type": "Castleton", + "id": 28 + }, + "zonePurpose": { + "name": "56d560c005d5523efa88f11b", + "id": 9223 + }, + "powerType": { + "name": "56d560c066631aa8ec5c366e", + "id": 1442 + }, + "designType": { + "name": "56d560c00fc0fdad15c53ca2", + "id": 2050 + }, + "zoneType": { + "name": "56d560c024dd7d85839c6fba", + "id": 3735 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 39, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-02-19" + }, + "created": { + "date": "2015-11-16" + }, + "name": "56d560c0ce0e3a71cf4fd6bb", + "id": 28 + }, + { + "site": { + "location_type": "Tilleda", + "id": 29 + }, + "zonePurpose": { + "name": "56d560c020be752d13e4f0cd", + "id": 7834 + }, + "powerType": { + "name": "56d560c03cecf2671bb9ee2c", + "id": 3227 + }, + "designType": { + "name": "56d560c08837945128d96159", + "id": 1698 + }, + "zoneType": { + "name": "56d560c04fdf224ad8d0a998", + "id": 6490 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 79, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-03-02" + }, + "created": { + "date": "2015-04-14" + }, + "name": "56d560c0a78a2aea1e5df5b5", + "id": 29 + }, + { + "site": { + "location_type": "Valmy", + "id": 30 + }, + "zonePurpose": { + "name": "56d560c0b788c77b8c16e01c", + "id": 6296 + }, + "powerType": { + "name": "56d560c08ef889d6a7e215dd", + "id": 8792 + }, + "designType": { + "name": "56d560c041bb436f46172e7b", + "id": 9819 + }, + "zoneType": { + "name": "56d560c061fac9825e323af2", + "id": 465 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 53, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-03-09" + }, + "created": { + "date": "2014-09-16" + }, + "name": "56d560c03191b8f63969fd4e", + "id": 30 + }, + { + "site": { + "location_type": "Blue", + "id": 31 + }, + "zonePurpose": { + "name": "56d560c0b51a91dbdac3fc36", + "id": 1343 + }, + "powerType": { + "name": "56d560c046c5c6fa155f9ecf", + "id": 7517 + }, + "designType": { + "name": "56d560c06ef3ee967b7f5ccb", + "id": 8951 + }, + "zoneType": { + "name": "56d560c0286719c68089524b", + "id": 2042 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 87, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-11-16" + }, + "created": { + "date": "2014-02-26" + }, + "name": "56d560c03bb51163ed1e028e", + "id": 31 + }, + { + "site": { + "location_type": "Herlong", + "id": 32 + }, + "zonePurpose": { + "name": "56d560c0a69f976fe08f03af", + "id": 7471 + }, + "powerType": { + "name": "56d560c0aac0198e0745d8ba", + "id": 6107 + }, + "designType": { + "name": "56d560c0eed43fd9c0a94abb", + "id": 3396 + }, + "zoneType": { + "name": "56d560c01e08dd515acd4d45", + "id": 8826 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 34, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-10-16" + }, + "created": { + "date": "2014-05-11" + }, + "name": "56d560c049ca7f5030e09cb6", + "id": 32 + }, + { + "site": { + "location_type": "Rodanthe", + "id": 33 + }, + "zonePurpose": { + "name": "56d560c0c968144ed07e180a", + "id": 7716 + }, + "powerType": { + "name": "56d560c0e9c14ccda611a97b", + "id": 2584 + }, + "designType": { + "name": "56d560c090b97312fabcceba", + "id": 5558 + }, + "zoneType": { + "name": "56d560c01157ab8f0bb3e7cb", + "id": 5173 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 38, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-11-23" + }, + "created": { + "date": "2015-10-29" + }, + "name": "56d560c0326a70418fa07b91", + "id": 33 + }, + { + "site": { + "location_type": "Sanders", + "id": 34 + }, + "zonePurpose": { + "name": "56d560c01cea86b9060ac9bb", + "id": 3675 + }, + "powerType": { + "name": "56d560c082b8301ac84dc675", + "id": 5335 + }, + "designType": { + "name": "56d560c0d5f62bcd51abea28", + "id": 5121 + }, + "zoneType": { + "name": "56d560c066c5e191a879b52f", + "id": 2578 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 91, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-01-08" + }, + "created": { + "date": "2014-10-18" + }, + "name": "56d560c00f6e15f9503ea384", + "id": 34 + }, + { + "site": { + "location_type": "Calverton", + "id": 35 + }, + "zonePurpose": { + "name": "56d560c030cfa637f749326e", + "id": 5236 + }, + "powerType": { + "name": "56d560c05e23eb35e0b7432e", + "id": 5964 + }, + "designType": { + "name": "56d560c0df297a2eb168afcb", + "id": 5343 + }, + "zoneType": { + "name": "56d560c04cb3698141129144", + "id": 5191 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 27, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-12-15" + }, + "created": { + "date": "2014-01-09" + }, + "name": "56d560c0a2642bbad9f0180b", + "id": 35 + }, + { + "site": { + "location_type": "Rote", + "id": 36 + }, + "zonePurpose": { + "name": "56d560c08a8518cf01863a4d", + "id": 1290 + }, + "powerType": { + "name": "56d560c0832ecb1823e069a8", + "id": 1429 + }, + "designType": { + "name": "56d560c009191b5429961c39", + "id": 4446 + }, + "zoneType": { + "name": "56d560c05b4413606834951e", + "id": 3055 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 93, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-03-02" + }, + "created": { + "date": "2014-01-03" + }, + "name": "56d560c07ee2080ff4bcd900", + "id": 36 + }, + { + "site": { + "location_type": "Gloucester", + "id": 37 + }, + "zonePurpose": { + "name": "56d560c0100e69fbd25caecf", + "id": 934 + }, + "powerType": { + "name": "56d560c0b83644ed71029eb5", + "id": 7837 + }, + "designType": { + "name": "56d560c0b27c1d8d3ac70e4b", + "id": 1098 + }, + "zoneType": { + "name": "56d560c0620d33989ab375e2", + "id": 8544 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 22, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-07-09" + }, + "created": { + "date": "2015-12-17" + }, + "name": "56d560c071b95693ff3def45", + "id": 37 + }, + { + "site": { + "location_type": "Libertytown", + "id": 38 + }, + "zonePurpose": { + "name": "56d560c07d322c6dda1f0d61", + "id": 8516 + }, + "powerType": { + "name": "56d560c049c685a3f3a2bc3f", + "id": 7844 + }, + "designType": { + "name": "56d560c0f5fc4a8172d02db2", + "id": 2358 + }, + "zoneType": { + "name": "56d560c0bb27ffeb81c307b4", + "id": 1292 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 3, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-11-17" + }, + "created": { + "date": "2016-02-06" + }, + "name": "56d560c0397247302b0703bc", + "id": 38 + }, + { + "site": { + "location_type": "Madrid", + "id": 39 + }, + "zonePurpose": { + "name": "56d560c081d6850fca450f46", + "id": 9765 + }, + "powerType": { + "name": "56d560c082e495ba33e3ace3", + "id": 9109 + }, + "designType": { + "name": "56d560c00f831dfdd97f4aea", + "id": 1050 + }, + "zoneType": { + "name": "56d560c058b8cf043e0ddefc", + "id": 7071 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 3, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-05-07" + }, + "created": { + "date": "2016-01-16" + }, + "name": "56d560c065e33d4a7a97d6dc", + "id": 39 + }, + { + "site": { + "location_type": "Magnolia", + "id": 40 + }, + "zonePurpose": { + "name": "56d560c045ac9fed80740a73", + "id": 2020 + }, + "powerType": { + "name": "56d560c03c71f014cb6f41dd", + "id": 538 + }, + "designType": { + "name": "56d560c0dda73cacfaf7f70a", + "id": 3698 + }, + "zoneType": { + "name": "56d560c028d3df2a8a5a7097", + "id": 5185 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 42, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-05-30" + }, + "created": { + "date": "2014-10-07" + }, + "name": "56d560c079539139c836d585", + "id": 40 + }, + { + "site": { + "location_type": "Gilmore", + "id": 41 + }, + "zonePurpose": { + "name": "56d560c086d55b3215b897c1", + "id": 2761 + }, + "powerType": { + "name": "56d560c0af644b576afd4591", + "id": 9758 + }, + "designType": { + "name": "56d560c02e4e7a560f5b1c9e", + "id": 3327 + }, + "zoneType": { + "name": "56d560c01afef659bbd2a63e", + "id": 8009 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 15, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-05-05" + }, + "created": { + "date": "2015-06-09" + }, + "name": "56d560c052f661383788f819", + "id": 41 + }, + { + "site": { + "location_type": "Wheaton", + "id": 42 + }, + "zonePurpose": { + "name": "56d560c091897930e4a321ac", + "id": 7099 + }, + "powerType": { + "name": "56d560c0144e5a67a1c23005", + "id": 1417 + }, + "designType": { + "name": "56d560c0ec1399e14c3831c7", + "id": 7711 + }, + "zoneType": { + "name": "56d560c038f7cb1be99a077c", + "id": 8038 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 35, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-08-07" + }, + "created": { + "date": "2014-04-30" + }, + "name": "56d560c0107734f625b3c7e0", + "id": 42 + }, + { + "site": { + "location_type": "Delwood", + "id": 43 + }, + "zonePurpose": { + "name": "56d560c041eec1fca5437a00", + "id": 2300 + }, + "powerType": { + "name": "56d560c06285c6b65af88f7a", + "id": 6106 + }, + "designType": { + "name": "56d560c01ccd49801ba9cfbd", + "id": 7534 + }, + "zoneType": { + "name": "56d560c0626dafd003dc0ba1", + "id": 6481 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 41, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-08-12" + }, + "created": { + "date": "2015-12-28" + }, + "name": "56d560c00fd3ffc699a9a9f5", + "id": 43 + }, + { + "site": { + "location_type": "Chumuckla", + "id": 44 + }, + "zonePurpose": { + "name": "56d560c04b034f9b2f39a1a2", + "id": 7511 + }, + "powerType": { + "name": "56d560c00d6710729c93866d", + "id": 4002 + }, + "designType": { + "name": "56d560c00251b1d8f74040db", + "id": 1474 + }, + "zoneType": { + "name": "56d560c0d20f69bdb0f8e8ac", + "id": 5392 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 43, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-01-01" + }, + "created": { + "date": "2015-01-10" + }, + "name": "56d560c0f4a03df16f4d6081", + "id": 44 + }, + { + "site": { + "location_type": "Blanford", + "id": 45 + }, + "zonePurpose": { + "name": "56d560c07ebae1008545f665", + "id": 5638 + }, + "powerType": { + "name": "56d560c0f77e9bbc9460232e", + "id": 76 + }, + "designType": { + "name": "56d560c0497eb03d1071195c", + "id": 6700 + }, + "zoneType": { + "name": "56d560c066046abba79d8dcb", + "id": 2329 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 25, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-09-04" + }, + "created": { + "date": "2015-11-23" + }, + "name": "56d560c09b6527b949303ffb", + "id": 45 + }, + { + "site": { + "location_type": "Bascom", + "id": 46 + }, + "zonePurpose": { + "name": "56d560c0ee8d4ea5a1dd0243", + "id": 2815 + }, + "powerType": { + "name": "56d560c01adc0eeaceed92fe", + "id": 224 + }, + "designType": { + "name": "56d560c008e6c030928e09e4", + "id": 338 + }, + "zoneType": { + "name": "56d560c0aabadfa119131e0a", + "id": 6656 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 59, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-10-29" + }, + "created": { + "date": "2014-01-06" + }, + "name": "56d560c0289c2a2965c76497", + "id": 46 + }, + { + "site": { + "location_type": "Harold", + "id": 47 + }, + "zonePurpose": { + "name": "56d560c0731d5299d3122f2b", + "id": 4243 + }, + "powerType": { + "name": "56d560c00e9f70ce6b329a26", + "id": 2150 + }, + "designType": { + "name": "56d560c0a8f6e4445bdd6169", + "id": 7391 + }, + "zoneType": { + "name": "56d560c0bf5adf58ffffb14d", + "id": 2719 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 41, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-08-29" + }, + "created": { + "date": "2014-12-30" + }, + "name": "56d560c01f75f39e09e3c9f3", + "id": 47 + }, + { + "site": { + "location_type": "Roosevelt", + "id": 48 + }, + "zonePurpose": { + "name": "56d560c0988d063eb71729a7", + "id": 6667 + }, + "powerType": { + "name": "56d560c0ad31768ea1ddca09", + "id": 6991 + }, + "designType": { + "name": "56d560c0c28c4c6758652c3c", + "id": 5256 + }, + "zoneType": { + "name": "56d560c01bebe9061aa47d00", + "id": 6970 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 57, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2016-01-29" + }, + "created": { + "date": "2015-09-30" + }, + "name": "56d560c0d1b8adfa554b6423", + "id": 48 + }, + { + "site": { + "location_type": "Leland", + "id": 49 + }, + "zonePurpose": { + "name": "56d560c00423da2f23b6dcb1", + "id": 8098 + }, + "powerType": { + "name": "56d560c01792c147706146cd", + "id": 8653 + }, + "designType": { + "name": "56d560c0c789b9e3a0770593", + "id": 2149 + }, + "zoneType": { + "name": "56d560c0b5f828802b16e82a", + "id": 7818 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 98, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-07-06" + }, + "created": { + "date": "2014-12-30" + }, + "name": "56d560c01eef9e6dff0eeb5d", + "id": 49 + }, + { + "site": { + "location_type": "Jardine", + "id": 50 + }, + "zonePurpose": { + "name": "56d560c0cb9345d9d38fede8", + "id": 2308 + }, + "powerType": { + "name": "56d560c0e9fd1b050d95ae66", + "id": 3012 + }, + "designType": { + "name": "56d560c02d1e2244a85d7baf", + "id": 9549 + }, + "zoneType": { + "name": "56d560c09f247522857fc0f6", + "id": 6134 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 48, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-10-10" + }, + "created": { + "date": "2014-11-25" + }, + "name": "56d560c09f8a40e4324a4280", + "id": 50 + }, + { + "site": { + "location_type": "Trona", + "id": 51 + }, + "zonePurpose": { + "name": "56d560c0d7077ede587d4592", + "id": 3824 + }, + "powerType": { + "name": "56d560c058dd0e17896b96a9", + "id": 5409 + }, + "designType": { + "name": "56d560c0f36bfb970b33ffe6", + "id": 7274 + }, + "zoneType": { + "name": "56d560c0a4860ed240407db0", + "id": 4302 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 15, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-05-30" + }, + "created": { + "date": "2014-02-18" + }, + "name": "56d560c06d460d876cb74409", + "id": 51 + }, + { + "site": { + "location_type": "Whitestone", + "id": 52 + }, + "zonePurpose": { + "name": "56d560c0006314920e70e82c", + "id": 7292 + }, + "powerType": { + "name": "56d560c0ebee34abaaa4204d", + "id": 3853 + }, + "designType": { + "name": "56d560c038c35079789da1de", + "id": 5965 + }, + "zoneType": { + "name": "56d560c0013d9ded4cfbc59f", + "id": 8513 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 99, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-10-05" + }, + "created": { + "date": "2015-01-02" + }, + "name": "56d560c0f5386dfbbfd4039b", + "id": 52 + }, + { + "site": { + "location_type": "Lupton", + "id": 53 + }, + "zonePurpose": { + "name": "56d560c0410f97fbd39bb188", + "id": 6797 + }, + "powerType": { + "name": "56d560c06eafb4db025f1106", + "id": 2265 + }, + "designType": { + "name": "56d560c0a16a7371f3b818d5", + "id": 8569 + }, + "zoneType": { + "name": "56d560c08f19993f2e6e26a0", + "id": 1021 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 14, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-06-19" + }, + "created": { + "date": "2014-04-06" + }, + "name": "56d560c080ca44dd12438645", + "id": 53 + }, + { + "site": { + "location_type": "Rivera", + "id": 54 + }, + "zonePurpose": { + "name": "56d560c0832400a19d27c71a", + "id": 2428 + }, + "powerType": { + "name": "56d560c01caf295194dab3db", + "id": 3595 + }, + "designType": { + "name": "56d560c0d81865e32cfd5015", + "id": 1995 + }, + "zoneType": { + "name": "56d560c034c0c62dec180146", + "id": 1222 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 91, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-04-04" + }, + "created": { + "date": "2014-03-02" + }, + "name": "56d560c008b9be11f9ea2af3", + "id": 54 + }, + { + "site": { + "location_type": "Bayview", + "id": 55 + }, + "zonePurpose": { + "name": "56d560c0b8c00af24a57e1dc", + "id": 9190 + }, + "powerType": { + "name": "56d560c02d84a656157756c1", + "id": 2364 + }, + "designType": { + "name": "56d560c039f53fe3347386e2", + "id": 7347 + }, + "zoneType": { + "name": "56d560c00238f9a29b650121", + "id": 6711 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 67, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-03-30" + }, + "created": { + "date": "2016-02-02" + }, + "name": "56d560c08ae02ad02c962e6d", + "id": 55 + }, + { + "site": { + "location_type": "Craig", + "id": 56 + }, + "zonePurpose": { + "name": "56d560c0cdbd720e7d620f14", + "id": 5433 + }, + "powerType": { + "name": "56d560c0013a4ee2d8618fce", + "id": 1942 + }, + "designType": { + "name": "56d560c09e1a56046e1f8ba9", + "id": 6283 + }, + "zoneType": { + "name": "56d560c0e92d4b5c624e36d9", + "id": 1646 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 0, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-10-11" + }, + "created": { + "date": "2014-06-20" + }, + "name": "56d560c0d6ca45a9f0125296", + "id": 56 + }, + { + "site": { + "location_type": "Titanic", + "id": 57 + }, + "zonePurpose": { + "name": "56d560c0fbb05edc14762095", + "id": 5966 + }, + "powerType": { + "name": "56d560c0f60aa44c28712cbc", + "id": 456 + }, + "designType": { + "name": "56d560c0e160905a6e575615", + "id": 8400 + }, + "zoneType": { + "name": "56d560c08023b61f49493343", + "id": 6948 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 14, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-11-23" + }, + "created": { + "date": "2015-11-08" + }, + "name": "56d560c0761fab2939b9ed3d", + "id": 57 + }, + { + "site": { + "location_type": "Rockingham", + "id": 58 + }, + "zonePurpose": { + "name": "56d560c0f5b34a73b9cf891f", + "id": 8830 + }, + "powerType": { + "name": "56d560c012bec7571a69a7fc", + "id": 4413 + }, + "designType": { + "name": "56d560c091c8a6795879c234", + "id": 112 + }, + "zoneType": { + "name": "56d560c00f1fcc2699896dfd", + "id": 5779 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 93, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-01-24" + }, + "created": { + "date": "2015-03-25" + }, + "name": "56d560c05cbfa15ec7bfb3e4", + "id": 58 + }, + { + "site": { + "location_type": "Tuttle", + "id": 59 + }, + "zonePurpose": { + "name": "56d560c04169d60c304d1284", + "id": 6779 + }, + "powerType": { + "name": "56d560c05b5e6657f4622c48", + "id": 5927 + }, + "designType": { + "name": "56d560c031f9a0a8860e410c", + "id": 3470 + }, + "zoneType": { + "name": "56d560c0286d2374c3247255", + "id": 596 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 39, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-01-09" + }, + "created": { + "date": "2014-04-30" + }, + "name": "56d560c07121f35302543176", + "id": 59 + }, + { + "site": { + "location_type": "Katonah", + "id": 60 + }, + "zonePurpose": { + "name": "56d560c0d542bd664b5d93c3", + "id": 8153 + }, + "powerType": { + "name": "56d560c06e0536e61c9a0e98", + "id": 203 + }, + "designType": { + "name": "56d560c01c26486c1398d7cc", + "id": 7477 + }, + "zoneType": { + "name": "56d560c016519009f54f8df1", + "id": 9112 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 29, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-11-04" + }, + "created": { + "date": "2014-08-26" + }, + "name": "56d560c08e6f1b72a957cdf2", + "id": 60 + }, + { + "site": { + "location_type": "Unionville", + "id": 61 + }, + "zonePurpose": { + "name": "56d560c06cde4ee178611ed4", + "id": 6050 + }, + "powerType": { + "name": "56d560c054fd0bd4b212be14", + "id": 8759 + }, + "designType": { + "name": "56d560c0095871981b7901a8", + "id": 226 + }, + "zoneType": { + "name": "56d560c05dd527ac1fd78c0a", + "id": 8845 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 97, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-02-10" + }, + "created": { + "date": "2014-11-23" + }, + "name": "56d560c0ed72fec6e592add2", + "id": 61 + }, + { + "site": { + "location_type": "Coalmont", + "id": 62 + }, + "zonePurpose": { + "name": "56d560c090418fd4da3d2aab", + "id": 3669 + }, + "powerType": { + "name": "56d560c059f6dc824d76c767", + "id": 3525 + }, + "designType": { + "name": "56d560c0ffce94eea11ba508", + "id": 3046 + }, + "zoneType": { + "name": "56d560c093df5084d7f0ad5f", + "id": 834 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 51, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-06-04" + }, + "created": { + "date": "2015-06-21" + }, + "name": "56d560c092d30a559b40ba2d", + "id": 62 + }, + { + "site": { + "location_type": "Irwin", + "id": 63 + }, + "zonePurpose": { + "name": "56d560c087b64651dbcb1361", + "id": 3262 + }, + "powerType": { + "name": "56d560c000579241be30407c", + "id": 1335 + }, + "designType": { + "name": "56d560c062da2f0bbde27b37", + "id": 5320 + }, + "zoneType": { + "name": "56d560c0822dee52cba698ba", + "id": 417 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 61, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-11-02" + }, + "created": { + "date": "2014-02-02" + }, + "name": "56d560c03acc16df6b47dcc6", + "id": 63 + }, + { + "site": { + "location_type": "Crenshaw", + "id": 64 + }, + "zonePurpose": { + "name": "56d560c0bf1e2852cd4925c1", + "id": 5006 + }, + "powerType": { + "name": "56d560c0a93b6643c073b480", + "id": 6467 + }, + "designType": { + "name": "56d560c04787261cb236edd2", + "id": 3597 + }, + "zoneType": { + "name": "56d560c0c4c83a2bbb12763b", + "id": 3791 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 93, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-05-05" + }, + "created": { + "date": "2015-12-19" + }, + "name": "56d560c0c5ecc5e808265326", + "id": 64 + }, + { + "site": { + "location_type": "Belva", + "id": 65 + }, + "zonePurpose": { + "name": "56d560c0e9196a00a007e857", + "id": 4471 + }, + "powerType": { + "name": "56d560c09c9727544cfe1ee1", + "id": 4969 + }, + "designType": { + "name": "56d560c0127968d2b6a5a977", + "id": 6874 + }, + "zoneType": { + "name": "56d560c027fe345cf5530f2d", + "id": 1997 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 21, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-06-06" + }, + "created": { + "date": "2015-07-14" + }, + "name": "56d560c05b9c5abca8adebd6", + "id": 65 + }, + { + "site": { + "location_type": "Rockbridge", + "id": 66 + }, + "zonePurpose": { + "name": "56d560c157dad375db38b27e", + "id": 9853 + }, + "powerType": { + "name": "56d560c1e3ffb1af13368243", + "id": 9357 + }, + "designType": { + "name": "56d560c19a814a81cf725bae", + "id": 5757 + }, + "zoneType": { + "name": "56d560c1b56f90ed745f3c22", + "id": 8718 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 59, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-12-18" + }, + "created": { + "date": "2015-08-08" + }, + "name": "56d560c1c982f7aa14aaa418", + "id": 66 + }, + { + "site": { + "location_type": "Oley", + "id": 67 + }, + "zonePurpose": { + "name": "56d560c135814cd334320d3b", + "id": 207 + }, + "powerType": { + "name": "56d560c1398ccee865c3baaa", + "id": 9699 + }, + "designType": { + "name": "56d560c1e88dba86fec92df9", + "id": 5170 + }, + "zoneType": { + "name": "56d560c1c356cadfad0425c2", + "id": 7402 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 43, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-05-07" + }, + "created": { + "date": "2014-11-27" + }, + "name": "56d560c1f1ac9edd50fae484", + "id": 67 + }, + { + "site": { + "location_type": "Leeper", + "id": 68 + }, + "zonePurpose": { + "name": "56d560c166cd1db8dbacf7f0", + "id": 2697 + }, + "powerType": { + "name": "56d560c1aa5754dede90ff32", + "id": 7930 + }, + "designType": { + "name": "56d560c171b5a8f8c67fef7c", + "id": 2335 + }, + "zoneType": { + "name": "56d560c184e8a7b907e7673f", + "id": 3897 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 100, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-07-24" + }, + "created": { + "date": "2015-08-23" + }, + "name": "56d560c18b9b3aa0288cb26f", + "id": 68 + }, + { + "site": { + "location_type": "Detroit", + "id": 69 + }, + "zonePurpose": { + "name": "56d560c1994ce1159c0625b3", + "id": 2652 + }, + "powerType": { + "name": "56d560c17edf282edb081071", + "id": 2448 + }, + "designType": { + "name": "56d560c1b44f04bb41dd6dd2", + "id": 5040 + }, + "zoneType": { + "name": "56d560c1c61abf60958d1db0", + "id": 3496 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 44, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-07-04" + }, + "created": { + "date": "2015-10-19" + }, + "name": "56d560c138231bc7f6844803", + "id": 69 + }, + { + "site": { + "location_type": "Leroy", + "id": 70 + }, + "zonePurpose": { + "name": "56d560c172ced1c936869d12", + "id": 7284 + }, + "powerType": { + "name": "56d560c1650048a5d48e8dd7", + "id": 1132 + }, + "designType": { + "name": "56d560c19699f4ef4c6a4cfb", + "id": 1519 + }, + "zoneType": { + "name": "56d560c1e8d5b714d5893558", + "id": 4905 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 83, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-05-08" + }, + "created": { + "date": "2015-02-21" + }, + "name": "56d560c17cbc1d569ecdd225", + "id": 70 + }, + { + "site": { + "location_type": "Dupuyer", + "id": 71 + }, + "zonePurpose": { + "name": "56d560c13eb8af5955ed91a8", + "id": 4368 + }, + "powerType": { + "name": "56d560c128e5d7459fa1367e", + "id": 7564 + }, + "designType": { + "name": "56d560c1e4ba9e890872dc62", + "id": 3930 + }, + "zoneType": { + "name": "56d560c133179db7652a5e34", + "id": 6196 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 91, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-07-30" + }, + "created": { + "date": "2015-06-10" + }, + "name": "56d560c165534d12641f1d15", + "id": 71 + }, + { + "site": { + "location_type": "Caspar", + "id": 72 + }, + "zonePurpose": { + "name": "56d560c17fdf00d790acb5af", + "id": 8494 + }, + "powerType": { + "name": "56d560c12be5b3910451809b", + "id": 5521 + }, + "designType": { + "name": "56d560c1563801541feae38a", + "id": 6 + }, + "zoneType": { + "name": "56d560c10d702c675c6b1fe9", + "id": 9217 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 66, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-12-10" + }, + "created": { + "date": "2014-03-28" + }, + "name": "56d560c1cc08dc1c0e26b4b6", + "id": 72 + }, + { + "site": { + "location_type": "Drytown", + "id": 73 + }, + "zonePurpose": { + "name": "56d560c1dae28a1b5ae267ec", + "id": 2477 + }, + "powerType": { + "name": "56d560c1dfc10bf928b9c9f3", + "id": 3228 + }, + "designType": { + "name": "56d560c1287c90f1ccd20fb0", + "id": 9864 + }, + "zoneType": { + "name": "56d560c1a6eae1d900584625", + "id": 1583 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 81, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-05-29" + }, + "created": { + "date": "2015-04-09" + }, + "name": "56d560c1492afbc7c1427d77", + "id": 73 + }, + { + "site": { + "location_type": "Durham", + "id": 74 + }, + "zonePurpose": { + "name": "56d560c1ef65302934f5e6dc", + "id": 3898 + }, + "powerType": { + "name": "56d560c12b636879fc9ac848", + "id": 3383 + }, + "designType": { + "name": "56d560c1db8604849fee5fd4", + "id": 8929 + }, + "zoneType": { + "name": "56d560c13cdc8e1a01de7d8d", + "id": 877 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 65, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-12-15" + }, + "created": { + "date": "2015-07-10" + }, + "name": "56d560c1fa90866d656a6004", + "id": 74 + }, + { + "site": { + "location_type": "Jenkinsville", + "id": 75 + }, + "zonePurpose": { + "name": "56d560c1603c6b2be9de5e4d", + "id": 4698 + }, + "powerType": { + "name": "56d560c1fa18a8c0d656a422", + "id": 9998 + }, + "designType": { + "name": "56d560c12d9f261309cddf96", + "id": 8088 + }, + "zoneType": { + "name": "56d560c137336e7289f9fd36", + "id": 6565 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 48, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-09-18" + }, + "created": { + "date": "2015-12-12" + }, + "name": "56d560c1c3a8051dea22d46d", + "id": 75 + }, + { + "site": { + "location_type": "Gasquet", + "id": 76 + }, + "zonePurpose": { + "name": "56d560c13c9816037480ffe4", + "id": 7599 + }, + "powerType": { + "name": "56d560c1a6cad875a215ad89", + "id": 5056 + }, + "designType": { + "name": "56d560c1d6abc9b84a8cb846", + "id": 319 + }, + "zoneType": { + "name": "56d560c1e6d4b25b58eab937", + "id": 3990 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 66, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-12-20" + }, + "created": { + "date": "2015-08-01" + }, + "name": "56d560c1e4d63f2f6af4200f", + "id": 76 + }, + { + "site": { + "location_type": "Dowling", + "id": 77 + }, + "zonePurpose": { + "name": "56d560c1d0a538762b59e02f", + "id": 1615 + }, + "powerType": { + "name": "56d560c16ea1f0b739a39d2c", + "id": 4555 + }, + "designType": { + "name": "56d560c1f794ff62afd4274b", + "id": 9755 + }, + "zoneType": { + "name": "56d560c19345cad1efcafa14", + "id": 4699 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 59, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-04-30" + }, + "created": { + "date": "2014-07-23" + }, + "name": "56d560c1f6a875453d430369", + "id": 77 + }, + { + "site": { + "location_type": "Balm", + "id": 78 + }, + "zonePurpose": { + "name": "56d560c10227da3e57ebce4a", + "id": 6345 + }, + "powerType": { + "name": "56d560c10a3341aaf5466396", + "id": 6294 + }, + "designType": { + "name": "56d560c1e4e14c514a6abe2b", + "id": 212 + }, + "zoneType": { + "name": "56d560c15249489062d07d7a", + "id": 2414 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 19, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-06-05" + }, + "created": { + "date": "2015-11-30" + }, + "name": "56d560c1a58b9572c1ce12f1", + "id": 78 + }, + { + "site": { + "location_type": "Macdona", + "id": 79 + }, + "zonePurpose": { + "name": "56d560c17e4def1d50cc9d9e", + "id": 1376 + }, + "powerType": { + "name": "56d560c14a89ad37ec57d535", + "id": 8573 + }, + "designType": { + "name": "56d560c1d55c3d19bb9166db", + "id": 3172 + }, + "zoneType": { + "name": "56d560c16739ffe0dfee5149", + "id": 408 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 46, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-11-28" + }, + "created": { + "date": "2014-12-05" + }, + "name": "56d560c19de149e3bd30c972", + "id": 79 + }, + { + "site": { + "location_type": "Wattsville", + "id": 80 + }, + "zonePurpose": { + "name": "56d560c119fc710821e3f495", + "id": 2092 + }, + "powerType": { + "name": "56d560c189b822790b799165", + "id": 7339 + }, + "designType": { + "name": "56d560c164b56c4099dbff29", + "id": 4075 + }, + "zoneType": { + "name": "56d560c13bde6d70c6da0d1d", + "id": 1304 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 19, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-10-26" + }, + "created": { + "date": "2014-12-03" + }, + "name": "56d560c1b86d7ce12797f618", + "id": 80 + }, + { + "site": { + "location_type": "Buxton", + "id": 81 + }, + "zonePurpose": { + "name": "56d560c1f69903bc85c090e8", + "id": 7934 + }, + "powerType": { + "name": "56d560c1cae0dd4d94754853", + "id": 2180 + }, + "designType": { + "name": "56d560c11ba0e8fa84f55d1a", + "id": 8607 + }, + "zoneType": { + "name": "56d560c14ae48add0172593b", + "id": 1304 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 94, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-09-07" + }, + "created": { + "date": "2015-11-21" + }, + "name": "56d560c18e1c0dd8719b9401", + "id": 81 + }, + { + "site": { + "location_type": "Conway", + "id": 82 + }, + "zonePurpose": { + "name": "56d560c1d840cf24e9296df0", + "id": 6151 + }, + "powerType": { + "name": "56d560c188074d3dec302d02", + "id": 482 + }, + "designType": { + "name": "56d560c17b60a2e9c54b8608", + "id": 5809 + }, + "zoneType": { + "name": "56d560c12365e48fcf1694d6", + "id": 4354 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 98, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-11-05" + }, + "created": { + "date": "2014-12-07" + }, + "name": "56d560c1dd5675ae340efb8b", + "id": 82 + }, + { + "site": { + "location_type": "Jackpot", + "id": 83 + }, + "zonePurpose": { + "name": "56d560c11f00e6c29f78b357", + "id": 1744 + }, + "powerType": { + "name": "56d560c18df238018caa26ca", + "id": 5287 + }, + "designType": { + "name": "56d560c1b35bc61d3201bb7c", + "id": 8121 + }, + "zoneType": { + "name": "56d560c1edf03c238d559997", + "id": 1720 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 59, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-06-29" + }, + "created": { + "date": "2015-05-19" + }, + "name": "56d560c1d4b55352966eed84", + "id": 83 + }, + { + "site": { + "location_type": "Flintville", + "id": 84 + }, + "zonePurpose": { + "name": "56d560c1d428f161c6454acd", + "id": 2440 + }, + "powerType": { + "name": "56d560c1cd181d518199ce04", + "id": 8665 + }, + "designType": { + "name": "56d560c12bfc2ac757eb098b", + "id": 9819 + }, + "zoneType": { + "name": "56d560c12bcb1d075747ddc0", + "id": 2338 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 41, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-07-27" + }, + "created": { + "date": "2015-01-08" + }, + "name": "56d560c12fe463b676b7e647", + "id": 84 + }, + { + "site": { + "location_type": "Deercroft", + "id": 85 + }, + "zonePurpose": { + "name": "56d560c1fa9c119a979058ff", + "id": 8 + }, + "powerType": { + "name": "56d560c1ba9d11096c5769e6", + "id": 1345 + }, + "designType": { + "name": "56d560c1ab418371a415b8ba", + "id": 4950 + }, + "zoneType": { + "name": "56d560c1d5bed9c9f15e9efe", + "id": 9636 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 23, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-11-15" + }, + "created": { + "date": "2014-04-08" + }, + "name": "56d560c127f1c4d3708558f9", + "id": 85 + }, + { + "site": { + "location_type": "Cawood", + "id": 86 + }, + "zonePurpose": { + "name": "56d560c159b143aab9ef1e99", + "id": 4495 + }, + "powerType": { + "name": "56d560c1e7537e1bbf7d5648", + "id": 2554 + }, + "designType": { + "name": "56d560c1197293b7d414d3bb", + "id": 939 + }, + "zoneType": { + "name": "56d560c194d5d0d6e8dcf3ee", + "id": 974 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 52, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-09-15" + }, + "created": { + "date": "2015-08-11" + }, + "name": "56d560c12fb4759369c8f96d", + "id": 86 + }, + { + "site": { + "location_type": "Inkerman", + "id": 87 + }, + "zonePurpose": { + "name": "56d560c112d9498c1d3fcaa8", + "id": 1548 + }, + "powerType": { + "name": "56d560c1e0104f5e9d4f8d8a", + "id": 3361 + }, + "designType": { + "name": "56d560c133df234e47d6dca1", + "id": 8099 + }, + "zoneType": { + "name": "56d560c11c718ac4304c0502", + "id": 7261 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 92, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-09-05" + }, + "created": { + "date": "2015-03-18" + }, + "name": "56d560c1f71f5112ae54405d", + "id": 87 + }, + { + "site": { + "location_type": "Blodgett", + "id": 88 + }, + "zonePurpose": { + "name": "56d560c1150f38e8dc7a4334", + "id": 3995 + }, + "powerType": { + "name": "56d560c1d491b5fdda008ea3", + "id": 4677 + }, + "designType": { + "name": "56d560c179a359491ca8dd08", + "id": 9489 + }, + "zoneType": { + "name": "56d560c17c50b926dec1b1f5", + "id": 7173 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 4, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-04-25" + }, + "created": { + "date": "2014-06-07" + }, + "name": "56d560c1f39bec5d9efdbe06", + "id": 88 + }, + { + "site": { + "location_type": "Fidelis", + "id": 89 + }, + "zonePurpose": { + "name": "56d560c17535831df443ec51", + "id": 5998 + }, + "powerType": { + "name": "56d560c16f0040a27ea2506c", + "id": 2008 + }, + "designType": { + "name": "56d560c1ed6495cbebbe7930", + "id": 3526 + }, + "zoneType": { + "name": "56d560c1ead71c6d1e5b2174", + "id": 9417 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 15, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-01-31" + }, + "created": { + "date": "2016-01-14" + }, + "name": "56d560c1b9e56f19abd6d0bd", + "id": 89 + }, + { + "site": { + "location_type": "Hobucken", + "id": 90 + }, + "zonePurpose": { + "name": "56d560c15f5447d4be24a4a2", + "id": 1225 + }, + "powerType": { + "name": "56d560c16fd746187141fa5a", + "id": 7639 + }, + "designType": { + "name": "56d560c16162bc7f1465c827", + "id": 8999 + }, + "zoneType": { + "name": "56d560c1a75b14bd612a54c0", + "id": 446 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 30, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-03-13" + }, + "created": { + "date": "2015-09-15" + }, + "name": "56d560c111d62a18cd25e389", + "id": 90 + }, + { + "site": { + "location_type": "Coinjock", + "id": 91 + }, + "zonePurpose": { + "name": "56d560c137d70331834fa47c", + "id": 3158 + }, + "powerType": { + "name": "56d560c1eb63a51e75f1ef33", + "id": 8092 + }, + "designType": { + "name": "56d560c18f68172f95bb0412", + "id": 7822 + }, + "zoneType": { + "name": "56d560c1ec7e2d44ff7c8e48", + "id": 8128 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 57, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-09-03" + }, + "created": { + "date": "2014-01-01" + }, + "name": "56d560c13685873e32faf2fe", + "id": 91 + }, + { + "site": { + "location_type": "Ballico", + "id": 92 + }, + "zonePurpose": { + "name": "56d560c14d131b8fd1afd72c", + "id": 157 + }, + "powerType": { + "name": "56d560c1d8d3a94f20d11c97", + "id": 533 + }, + "designType": { + "name": "56d560c194c02371bb234868", + "id": 8345 + }, + "zoneType": { + "name": "56d560c1dc9ebbde0c28355a", + "id": 1564 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 23, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-06-30" + }, + "created": { + "date": "2014-09-07" + }, + "name": "56d560c107c932a3e08e44d9", + "id": 92 + }, + { + "site": { + "location_type": "Columbus", + "id": 93 + }, + "zonePurpose": { + "name": "56d560c11871178266078ed7", + "id": 8963 + }, + "powerType": { + "name": "56d560c11d43eb1b8d44e390", + "id": 1006 + }, + "designType": { + "name": "56d560c144ef61f5dc7c709f", + "id": 4945 + }, + "zoneType": { + "name": "56d560c1d2f6ce761b4328c5", + "id": 9895 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 99, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2015-03-05" + }, + "created": { + "date": "2015-07-19" + }, + "name": "56d560c1c37f519b199b12eb", + "id": 93 + }, + { + "site": { + "location_type": "Kirk", + "id": 94 + }, + "zonePurpose": { + "name": "56d560c1c71242e1f36e3b59", + "id": 6990 + }, + "powerType": { + "name": "56d560c14c3c992705e5fe68", + "id": 3348 + }, + "designType": { + "name": "56d560c175fcda9b3798923d", + "id": 3914 + }, + "zoneType": { + "name": "56d560c1c7be25f9c7150eed", + "id": 7459 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 79, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-08-15" + }, + "created": { + "date": "2015-09-26" + }, + "name": "56d560c13978259efa633fa5", + "id": 94 + }, + { + "site": { + "location_type": "Logan", + "id": 95 + }, + "zonePurpose": { + "name": "56d560c1254dae383a8dd86e", + "id": 3477 + }, + "powerType": { + "name": "56d560c12604efdfa865d9b1", + "id": 560 + }, + "designType": { + "name": "56d560c14c3615bcc6554586", + "id": 2663 + }, + "zoneType": { + "name": "56d560c11154f37c39cca7d4", + "id": 472 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 98, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-06-22" + }, + "created": { + "date": "2014-07-05" + }, + "name": "56d560c123c59b5e54271039", + "id": 95 + }, + { + "site": { + "location_type": "Vaughn", + "id": 96 + }, + "zonePurpose": { + "name": "56d560c16e6e5741fed83023", + "id": 1079 + }, + "powerType": { + "name": "56d560c1553a5d9fc6f9a1c3", + "id": 6041 + }, + "designType": { + "name": "56d560c1ade2d650edac5b3a", + "id": 9487 + }, + "zoneType": { + "name": "56d560c18607f77902613a1e", + "id": 9476 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 15, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-08-01" + }, + "created": { + "date": "2014-08-03" + }, + "name": "56d560c128bc0e550ee7cd9d", + "id": 96 + }, + { + "site": { + "location_type": "Hasty", + "id": 97 + }, + "zonePurpose": { + "name": "56d560c17ca2c51bca21b5ed", + "id": 7746 + }, + "powerType": { + "name": "56d560c1de7e5f37d960863f", + "id": 7781 + }, + "designType": { + "name": "56d560c1b35c5951047223bc", + "id": 2835 + }, + "zoneType": { + "name": "56d560c1110f2de2aec1cdc7", + "id": 1459 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 85, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-09-27" + }, + "created": { + "date": "2014-06-16" + }, + "name": "56d560c1428d7cc61a8ad82b", + "id": 97 + }, + { + "site": { + "location_type": "Worton", + "id": 98 + }, + "zonePurpose": { + "name": "56d560c15aa7e8436d56c023", + "id": 7942 + }, + "powerType": { + "name": "56d560c113f9f7cef8466bdb", + "id": 2035 + }, + "designType": { + "name": "56d560c19f900b07adaf74a5", + "id": 6988 + }, + "zoneType": { + "name": "56d560c1d3ba5cff37e5f0ff", + "id": 2242 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 65, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-09-29" + }, + "created": { + "date": "2014-02-10" + }, + "name": "56d560c113ca121ec83c9c94", + "id": 98 + }, + { + "site": { + "location_type": "Adelino", + "id": 99 + }, + "zonePurpose": { + "name": "56d560c1fd3213b0e9f937c4", + "id": 7382 + }, + "powerType": { + "name": "56d560c1496181a2fb68ee85", + "id": 6767 + }, + "designType": { + "name": "56d560c138bf22f6e8a98efa", + "id": 87 + }, + "zoneType": { + "name": "56d560c16ceffaa72387ccaa", + "id": 6987 + }, + "zoneState": "string", + "useableRackUnits": 0, + "openStackVersion": "string", + "numberOfComputeRack": 0, + "distributedControlPlane": "string", + "avialableKwPerRack": 23, + "secondaryDcp": "string", + "primaryDcp": "string", + "lastUpdateBy": "string", + "locked_by": "string", + "last_updated": { + "date": "2014-11-22" + }, + "created": { + "date": "2016-01-22" + }, + "name": "56d560c1edd271f6d12b707c", + "id": 99 + } +] \ No newline at end of file diff --git a/orm/services/region_manager/rms_mock/public/css/style.css b/orm/services/region_manager/rms_mock/public/css/style.css new file mode 100644 index 00000000..55c9db54 --- /dev/null +++ b/orm/services/region_manager/rms_mock/public/css/style.css @@ -0,0 +1,43 @@ +body { + background: #311F00; + color: white; + font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif; + padding: 1em 2em; +} + +a { + color: #FAFF78; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div#content { + width: 800px; + margin: 0 auto; +} + +form { + margin: 0; + padding: 0; + border: 0; +} + +fieldset { + border: 0; +} + +input.error { + background: #FAFF78; +} + +header { + text-align: center; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Futura-CondensedExtraBold', 'Futura', 'Helvetica', sans-serif; + text-transform: uppercase; +} diff --git a/orm/services/region_manager/rms_mock/public/images/logo.png b/orm/services/region_manager/rms_mock/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f403e4a4f3ce69a4577a46ae37f5633abb79fa GIT binary patch literal 20596 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 ZD6?|u)*zSG3-Sw<9?1AXo%Yig{{dC~0l)wN literal 0 HcmV?d00001 diff --git a/orm/services/region_manager/rms_mock/rms_mock/__init__.py b/orm/services/region_manager/rms_mock/rms_mock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms_mock/rms_mock/app.py b/orm/services/region_manager/rms_mock/rms_mock/app.py new file mode 100644 index 00000000..f53f79bb --- /dev/null +++ b/orm/services/region_manager/rms_mock/rms_mock/app.py @@ -0,0 +1,26 @@ +from pecan import make_app +from rms_mock import model +from pecan import make_app, conf +from pecan.commands import CommandRunner + +from rms import model + + +def setup_app(config): + + model.init_model() + app_conf = dict(config.app) + + return make_app( + app_conf.pop('root'), + logging=getattr(config, 'logging', {}), + **app_conf + ) + + +def main(): + runner = CommandRunner() + runner.run(['serve', '../config.py']) + +if __name__ == "__main__": + main() diff --git a/orm/services/region_manager/rms_mock/rms_mock/controllers/__init__.py b/orm/services/region_manager/rms_mock/rms_mock/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/region_manager/rms_mock/rms_mock/controllers/lcp_controller.py b/orm/services/region_manager/rms_mock/rms_mock/controllers/lcp_controller.py new file mode 100644 index 00000000..96e4cdb9 --- /dev/null +++ b/orm/services/region_manager/rms_mock/rms_mock/controllers/lcp_controller.py @@ -0,0 +1,26 @@ +import pecan +import logging + +from pecan import rest + +LOG = logging.getLogger(__name__) + + +class LcpController(rest.RestController): + """ + this class is for getting the lcp list from AIC Formation. + """ + + @pecan.expose() + def get(self): + """ + when a get call is received in RestAPI this function return a + list of lcp + """ + LOG.info('int get function') + + file = open('data/zones.json', 'r') + zones = file.read() + + # return the lcp's as dictionary so they can be converted to Json + return zones diff --git a/orm/services/region_manager/rms_mock/rms_mock/controllers/root.py b/orm/services/region_manager/rms_mock/rms_mock/controllers/root.py new file mode 100644 index 00000000..3366e46a --- /dev/null +++ b/orm/services/region_manager/rms_mock/rms_mock/controllers/root.py @@ -0,0 +1,19 @@ +from pecan import expose, redirect +from webob.exc import status_map +from lcp_controller import LcpController + + +class RootController(object): + """ + in charge of RestAPI in the root directory + """ + lcp = LcpController() + + @expose('error.html') + def error(self, status): + try: + status = int(status) + except ValueError: # pragma: no cover + status = 500 + message = getattr(status_map.get(status), 'explanation', '') + return dict(status=status, message=message) diff --git a/orm/services/region_manager/rms_mock/rms_mock/model/__init__.py b/orm/services/region_manager/rms_mock/rms_mock/model/__init__.py new file mode 100644 index 00000000..d983f7bc --- /dev/null +++ b/orm/services/region_manager/rms_mock/rms_mock/model/__init__.py @@ -0,0 +1,15 @@ +from pecan import conf # noqa + + +def init_model(): + """ + This is a stub method which is called at application startup time. + + If you need to bind to a parsed database configuration, set up tables or + ORM classes, or perform any database initialization, this is the + recommended place to do it. + + For more information working with databases, and some common recipes, + see http://pecan.readthedocs.org/en/latest/databases.html + """ + pass diff --git a/orm/services/region_manager/rms_mock/rms_mock/templates/error.html b/orm/services/region_manager/rms_mock/rms_mock/templates/error.html new file mode 100644 index 00000000..323bc562 --- /dev/null +++ b/orm/services/region_manager/rms_mock/rms_mock/templates/error.html @@ -0,0 +1,12 @@ +<%inherit file="../../../lcp_core/templates/layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Server Error ${status} + + +## now define the body of the template +

+

Server Error ${status}

+
+

${message}

diff --git a/orm/services/region_manager/rms_mock/rms_mock/templates/index.html b/orm/services/region_manager/rms_mock/rms_mock/templates/index.html new file mode 100644 index 00000000..c5624a2e --- /dev/null +++ b/orm/services/region_manager/rms_mock/rms_mock/templates/index.html @@ -0,0 +1,34 @@ +<%inherit file="../../../mock/templates/layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Welcome to Pecan! + + +## now define the body of the template +
+

+
+ +
+ +

This is a sample Pecan project.

+ +

+ Instructions for getting started can be found online at pecanpy.org +

+ +

+ ...or you can search the documentation here: +

+ +
+
+ + +
+ Enter search terms or a module, class or function name. +
+ +
diff --git a/orm/services/region_manager/rms_mock/rms_mock/templates/layout.html b/orm/services/region_manager/rms_mock/rms_mock/templates/layout.html new file mode 100644 index 00000000..40908591 --- /dev/null +++ b/orm/services/region_manager/rms_mock/rms_mock/templates/layout.html @@ -0,0 +1,22 @@ + + + ${self.title()} + ${self.style()} + ${self.javascript()} + + + ${self.body()} + + + +<%def name="title()"> + Default Title + + +<%def name="style()"> + + + +<%def name="javascript()"> + + diff --git a/orm/services/region_manager/rms_mock/setup.cfg b/orm/services/region_manager/rms_mock/setup.cfg new file mode 100644 index 00000000..00ca2206 --- /dev/null +++ b/orm/services/region_manager/rms_mock/setup.cfg @@ -0,0 +1,6 @@ +[nosetests] +match=^test +where=test +nocapture=1 +cover-package=test +cover-erase=1 diff --git a/orm/services/region_manager/rms_mock/setup.py b/orm/services/region_manager/rms_mock/setup.py new file mode 100644 index 00000000..0d17d5b6 --- /dev/null +++ b/orm/services/region_manager/rms_mock/setup.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages + +setup( + name='mock', + version='0.1', + description='', + author='', + author_email='', + install_requires=[ + "pecan", + ], + test_suite='mock', + zip_safe=False, + include_package_data=True, + packages=find_packages(exclude=['ez_setup']) +) diff --git a/orm/services/region_manager/scripts/db_scripts/create_db.sql b/orm/services/region_manager/scripts/db_scripts/create_db.sql new file mode 100644 index 00000000..b37c821a --- /dev/null +++ b/orm/services/region_manager/scripts/db_scripts/create_db.sql @@ -0,0 +1,63 @@ +create database if not exists orm_rms_db DEFAULT CHARACTER SET utf8 COLLATE utf8_bin; +use orm_rms_db; + +create table if not exists rms_groups + ( + id integer auto_increment not null, + group_id varchar(64) not null, + name varchar(64) not null, + description varchar(255) not null, + primary key (id), + unique group_idx (group_id)); + + +create table if not exists region + ( + id integer auto_increment not null, + region_id varchar(64) not null, + name varchar(64) not null, + address_state varchar(64) not null, + address_country varchar(64) not null, + address_city varchar(64) not null, + address_street varchar(64) not null, + address_zip varchar(64) not null, + region_status enum('building', 'functional', 'maintenance', 'down') not null, + ranger_agent_version varchar(64) not null, + open_stack_version varchar(64) not null, + design_type Varchar(64) not null, + location_type varchar(64) not null, + vlcp_name varchar(64) not null, + clli varchar(64) not null, + description varchar(255) not null, + primary key (id), + unique region_idx (region_id)); + + +create table if not exists group_region + ( + group_id varchar(64) not null, + region_id varchar(64) not null, + primary key (group_id, region_id), + foreign key (group_id) REFERENCES `rms_groups` (`group_id`) ON DELETE CASCADE, + foreign key (region_id) REFERENCES `region` (`region_id`) ON DELETE CASCADE); + + +create table if not exists region_end_point + ( + region_id varchar(64) not null, + end_point_type varchar(64) not null, + public_url varchar(255) not null, + primary key (region_id, end_point_type), + foreign key (region_id) REFERENCES `region` (`region_id`) ON DELETE CASCADE, + unique region_end_point_type(region_id, end_point_type)); + + +create table if not exists region_meta_data + ( + id integer auto_increment not null, + region_id varchar(64) not null, + meta_data_key varchar(64) not null, + meta_data_value varchar(255) not null, + primary key (id), + foreign key (region_id) REFERENCES `region` (`region_id`) ON DELETE CASCADE, + unique region_meta_data_key_value(region_id, meta_data_key, meta_data_value)); diff --git a/orm/services/region_manager/scripts/db_scripts/insert_test_values.sql b/orm/services/region_manager/scripts/db_scripts/insert_test_values.sql new file mode 100644 index 00000000..aa364ade --- /dev/null +++ b/orm/services/region_manager/scripts/db_scripts/insert_test_values.sql @@ -0,0 +1,59 @@ +use orm_rms_db; + +insert into region (region_id, + name, + address_state, + address_country, + address_city, + address_street, + address_zip, + region_status, + ranger_agent_version, + open_stack_version, + design_type, + location_type, + vlcp_name, + clli , + description) + values + ("lcp_0","lcp 0", "Cal", "US", "Los Angeles", "Blv st", "012345", "functional", "ranger_agent 1.0", "kilo", "design_type_0", "location_type_0", "vlcp_0", "clli_0", "lcp_0 in LA"), + ("lcp_1","lcp 1", "NY", "US", "New York", "5th avn", "112345", "functional", "ranger_agent 1.9", "kilo", "design_type_1", "location_type_1", "vlcp_1", "clli_1", "lcp_1 in NY"), + ("lcp_2","lcp 2", "", "IL", "Tel Aviv", "Bazel 4", "212345", "functional", "ranger_agent 1.0", "kilo", "design_type_2", "location_type_2", "vlcp_2", "clli_2", "lcp_2 in Tel Aviv"); + +insert into rms_groups (group_id, + name, + description) + values + ("group_0", "group 0", "test group 0"), + ("group_1", "group 1", "test group 1"); + +insert into group_region (group_id, + region_id) + values + ("group_0","lcp_0"), + ("group_0","lcp_1"), + ("group_1","lcp_2"); + +insert into region_meta_data (region_id, + meta_data_key, + meta_data_value) + values + ("lcp_0", "key_0", "value_0"), + ("lcp_0", "key_1", "value_1"), + ("lcp_1", "key_0", "value_0"), + ("lcp_1", "key_1", "value_1"), + ("lcp_2", "key_0", "value_0"); + +insert into region_end_point (region_id, + end_point_type, + public_url) + values + ("lcp_0", "ord", "http://ord_0.com"), + ("lcp_0", "identity", "http://identity_0.com"), + ("lcp_0", "dashboard", "http://image_0.com"), + ("lcp_1", "ord", "http://ord_1.com"), + ("lcp_1", "identity", "http://identity_1.com"), + ("lcp_1", "dashboard", "http://image_1.com"), + ("lcp_2", "ord", "http://ord_2.com"), + ("lcp_2", "identity", "http://identity_2.com"), + ("lcp_2", "dashboard", "http://image_2.com"); diff --git a/orm/services/region_manager/scripts/db_scripts/update_db.sql b/orm/services/region_manager/scripts/db_scripts/update_db.sql new file mode 100644 index 00000000..73f8c851 --- /dev/null +++ b/orm/services/region_manager/scripts/db_scripts/update_db.sql @@ -0,0 +1,80 @@ +use orm_rms_db; + +# This SQL script is used for upgrading ORM_RMS_DB. + +# PROCEDURE Update_Region_Status() +# The following defines and then calls a stored procedure that updates and replaces +# region_status from 'commissioning' to 'building'. + +DROP PROCEDURE IF EXISTS Rename_group_table; + +DELIMITER $$ + +CREATE PROCEDURE Rename_group_table() +BEGIN + DECLARE _table_exist INT; +-- Check if table 'group' exists even if no rows in table + SET _table_exist = ( SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = 'orm_rms_db' + AND table_name like 'group'); + IF _table_exist > 0 THEN + RENAME TABLE `group` TO rms_groups; + END IF; +END $$ +DELIMITER ; + +CALL Rename_group_table; + +DROP PROCEDURE IF EXISTS Update_Region_Status; + +DELIMITER $$ + +CREATE PROCEDURE Update_Region_Status() +BEGIN +-- Add a new enum value 'building' to the end of the enum list + ALTER TABLE region CHANGE region_status + region_status ENUM('commissioning','functional','maintenance','down','building'); + +-- Update the table to change the old to the new value + UPDATE region set region_status = 'building' where region_status = 'commissioning'; + +-- after changing to the new values, safe to remove the old "commissioning" value from the ENUM list + ALTER TABLE region CHANGE region_status + region_status ENUM('building','functional','maintenance','down'); + +END $$ +DELIMITER ; + +CALL Update_Region_Status; + +# PROCEDURE Upgrade_Region_Meta_Data; +# The following defines and then calls a stored procedure that does the following for the region_meta_data table: +# 1. Check if a column named 'id' not exist, if exist the db already up to date. +# 2. Remove old fk, pk and unique constraint. +# 3. Add a new column to the region_meta_data table named 'id' set it as auto increment and primary key. +# 4. Add a new constraint to define unique values. + +DROP PROCEDURE IF EXISTS Upgrade_Region_Meta_Data; + +DELIMITER $$ +CREATE PROCEDURE Upgrade_Region_Meta_Data() +BEGIN + DECLARE _count INT; + SET _count = ( SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'region_meta_data' AND + COLUMN_NAME = 'id'); + IF _count = 0 THEN + ALTER TABLE region_meta_data DROP FOREIGN KEY region_meta_data_ibfk_1; + ALTER TABLE region_meta_data DROP PRIMARY KEY; + ALTER TABLE region_meta_data DROP index region_meta_data_key; + + ALTER TABLE region_meta_data ADD COLUMN id int NOT NULL AUTO_INCREMENT primary key FIRST; + ALTER TABLE region_meta_data ADD CONSTRAINT region_meta_data_key_value UNIQUE (region_id, meta_data_key, meta_data_value); + END IF; +END $$ +DELIMITER ; + +CALL Upgrade_Region_Meta_Data; + diff --git a/orm/services/region_manager/scripts/shell_scripts/create_db.sh b/orm/services/region_manager/scripts/shell_scripts/create_db.sh new file mode 100644 index 00000000..5dee4fcb --- /dev/null +++ b/orm/services/region_manager/scripts/shell_scripts/create_db.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +echo Creating database: orm_rms_db +echo Creating tables: rms_groups, region, group_region, region_end_point, region_meta_data + +mysql -uroot -pstack < ../db_scripts/create_db.sql + +echo Done ! + + + + + + + diff --git a/orm/services/region_manager/scripts/shell_scripts/csv_2_db_loader.sh b/orm/services/region_manager/scripts/shell_scripts/csv_2_db_loader.sh new file mode 100644 index 00000000..b79094ce --- /dev/null +++ b/orm/services/region_manager/scripts/shell_scripts/csv_2_db_loader.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +echo Loading region data from csv to rms db + +cd ../.. +python csv2db.py + +echo Done ! + + + + + + + diff --git a/orm/services/region_manager/scripts/shell_scripts/update_db.sh b/orm/services/region_manager/scripts/shell_scripts/update_db.sh new file mode 100644 index 00000000..024a1287 --- /dev/null +++ b/orm/services/region_manager/scripts/shell_scripts/update_db.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +mysql -uroot -pstack < ../db_scripts/update_db.sql + +echo Done ! diff --git a/orm/services/region_manager/swagger/swagger.yaml b/orm/services/region_manager/swagger/swagger.yaml new file mode 100644 index 00000000..03180aaa --- /dev/null +++ b/orm/services/region_manager/swagger/swagger.yaml @@ -0,0 +1,713 @@ +swagger: '2.0' +info: + version: 3.5.0 + title: RMS API + +# the domain of the service +host: 135.76.2.229 +# array of all schemes that your API supports +schemes: + - https + +# will be prefixed to all paths +basePath: /v2/orm +produces: + - application/json + +paths: + /regions: + parameters: + + - $ref: "#/parameters/Client" + post: + summary: Create a new region + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - name: full region + in: body + description: input body to create full region + schema: + $ref: '#/definitions/RegionsData' + required: true + + tags: + - Region + + responses: + 201: + description: Region created successfully + schema: + $ref: '#/definitions/RegionsData' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + 409: + description: Duplicate Error + schema: + $ref: '#/definitions/409' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + get: + summary: get a list of regions by criteria (or all regions, if no criterion is specified) + parameters: + - name: type + in: query + type: "string" + required: false + - name: status + in: query + type: "string" + required: false + - name: metadata + in: query + type: "array" + items: + type: "string" + required: false + - name: rangerAgentVersion + in: query + type: "string" + required: false + - name: clli + in: query + type: "string" + required: false + - name: regionname + in: query + type: "string" + required: false + - name: osversion + in: query + type: "string" + required: false + - name: valet + in: query + type: "string" + required: false + - name: country + in: query + type: "string" + required: false + - name: state + in: query + type: "string" + required: false + - name: city + in: query + type: "string" + required: false + - name: street + in: query + type: "string" + required: false + - name: zip + in: query + type: "string" + required: false + tags: + - Region + responses: + 200: + description: list of regions by criteria + schema: + $ref: '#/definitions/RegionsWrapper' + 404: + description: No regions found for the specified criteria + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /regions/{region_id}: + parameters: + - $ref: "#/parameters/Client" + get: + summary: Get a region by id or name + parameters: + - name: region_id + in: path + type: string + description: ID or name of the requested region + required: true + tags: + - Region + responses: + 200: + description: The requested region + schema: + $ref: '#/definitions/RegionsData' + 404: + description: Region not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + delete: + summary: Delete a region by ID + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - name: region_id + in: path + type: string + description: ID of the region to delete + required: true + tags: + - Region + responses: + 204: + description: No content + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + put: + summary: Update a region by ID + parameters: + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + - name: region_id + in: path + type: string + description: ID or name of the requested region + required: true + - name: full region + in: body + description: input body to create full region + schema: + $ref: '#/definitions/RegionsData' + required: true + tags: + - Region + responses: + 200: + description: The updated region + schema: + $ref: '#/definitions/RegionsData' + 404: + description: Region not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /groups: + parameters: + - $ref: "#/parameters/Client" + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + post: + summary: Create a new group + parameters: + - name: full group + in: body + description: input body to create full group + schema: + $ref: '#/definitions/Groups' + required: true + + tags: + - Group + + responses: + 201: + description: Group created successfully + schema: + $ref: '#/definitions/Result' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/400' + 409: + description: Duplicate Error + schema: + $ref: '#/definitions/409' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + get: + summary: Get all groups + tags: + - Group + responses: + 200: + description: list of groups + schema: + $ref: '#/definitions/GroupsWrapper' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /groups/{group_id}: + parameters: + - $ref: "#/parameters/Client" + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + get: + summary: Get a single group by ID + parameters: + - name: group_id + in: path + type: string + description: ID of the requested group + required: true + tags: + - Group + responses: + 200: + description: The requested group + schema: + $ref: '#/definitions/Groups' + 404: + description: Group not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + delete: + summary: Delete a group by ID + parameters: + - name: group_id + in: path + type: string + description: ID of the group to delete + required: true + tags: + - Group + responses: + 204: + description: No content' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + put: + summary: Update a group by ID + parameters: + - name: group_id + in: path + type: string + description: ID of the requested group + required: true + - name: full group + in: body + description: input body to update full group + schema: + $ref: '#/definitions/Groups' + required: true + tags: + - Group + responses: + 200: + description: The updated group + schema: + $ref: '#/definitions/Result' + 404: + description: Group not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /regions/{region_id}/status: + parameters: + - $ref: "#/parameters/Client" + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + put: + summary: Update a region status + parameters: + - name: region_id + in: path + type: string + description: ID of the requested region + required: true + - name: status + in: body + description: status JSON + schema: + $ref: '#/definitions/RegionStatus' + required: true + tags: + - Status + responses: + 200: + description: The updated status + schema: + $ref: '#/definitions/RegionStatus' + 400: + description: Invalid status + schema: + $ref: '#/definitions/400' + 404: + description: Region not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /regions/{region_id}/metadata: + parameters: + - $ref: "#/parameters/Client" + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + post: + summary: Add metadata to a region + parameters: + - name: region_id + in: path + type: string + description: ID of the requested region + required: true + - name: metadata + in: body + description: metadata JSON + schema: + $ref: '#/definitions/MetadataWrapper' + required: true + tags: + - Metadata + responses: + 201: + description: Metadata successfully added + schema: + $ref: '#/definitions/MetadataWrapper' + 400: + description: Invalid JSON body + schema: + $ref: '#/definitions/400' + 404: + description: Region not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + put: + summary: Replace region metadata + parameters: + - name: region_id + in: path + type: string + description: ID of the requested region + required: true + - name: metadata + in: body + description: metadata JSON + schema: + $ref: '#/definitions/MetadataWrapper' + required: true + tags: + - Metadata + responses: + 200: + description: Metadata successfully replaced + schema: + $ref: '#/definitions/MetadataWrapper' + 400: + description: Invalid JSON body + schema: + $ref: '#/definitions/400' + 404: + description: Region not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + get: + summary: Get region metadata + parameters: + - name: region_id + in: path + type: string + description: ID of the requested region + required: true + tags: + - Metadata + responses: + 200: + description: Region metadata + schema: + $ref: '#/definitions/MetadataWrapper' + 404: + description: Region not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /regions/{region_id}/metadata/{metadata_key}: + parameters: + - $ref: "#/parameters/Client" + - $ref: "#/parameters/Token" + - $ref: "#/parameters/Region" + delete: + summary: Delete metadata from a region + parameters: + - name: region_id + in: path + type: string + description: ID of the requested region + required: true + - name: metadata_key + in: path + type: string + description: Metadata key to delete + required: true + tags: + - Metadata + responses: + 204: + description: No content + 404: + description: Region not found + schema: + $ref: '#/definitions/404' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + +definitions: + Address: + type: object + properties: + country: + type: string + state: + type: string + city: + type: string + street: + type: string + zip: + type: string + + EndPoint: + type: object + properties: + publicURL: + type: string + type: + type: string + + RegionsData: + type: object + properties: + status: + type: string + enum: [ + "building", + "functional", + "maintenance", + "down" + ] + id: + type: string + description: Region ID + name: + type: string + description: deprecated, this field is ignored + ranger_agent_version: + type: string + example: "AIC3.5" + open_stack_version: + type: string + clli: + type: string + metadata: + $ref: '#/definitions/ListDictionary' + endpoints: + type: array + description: Region endpoints. Must include "identity", "ord" and "dashboard" + items: + $ref: '#/definitions/EndPoint' + address: + $ref: '#/definitions/Address' + design_type: + type: string + example: "medium" + location_type: + type: string + vlcp_name: + type: string + + Groups: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + regions: + type: array + items: + type: string + + GroupsWrapper: + type: object + properties: + groups: + type: array + items: + $ref: '#/definitions/Groups' + + Result: + type: object + properties: + group: + $ref: '#/definitions/Groups' + + RegionStatus: + type: object + properties: + status: + type: string + enum: [ + "functional", + "maintenance", + "building", + "down" + ] + links: + type: object + description: Status link, for output only + example: {"property1": "value1"} + + MetadataWrapper: + type: object + properties: + metadata: + $ref: '#/definitions/ListDictionary' + + RegionsWrapper: + type: object + properties: + regions: + type: array + items: + $ref: '#/definitions/RegionsData' + + Error: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + 409: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + 400: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + 404: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + transaction_id: + type: string + message: + type: string + details: + type: string + + ListDictionary: + type: object + additionalProperties: + type: "string" + example: {"property1": ["value1", "value2"]} + + +parameters: + Token: + name: X-Auth-Token + in: header + description: Token from keystone + required: true + type: string + + Region: + name: X-Auth-Region + in: header + description: Region + required: true + type: string + + Client: + name: X-RANGER-Client + in: header + description: Client name + required: false + type: string + diff --git a/orm/services/region_manager/tox.ini b/orm/services/region_manager/tox.ini new file mode 100755 index 00000000..d1ea4254 --- /dev/null +++ b/orm/services/region_manager/tox.ini @@ -0,0 +1,27 @@ +[tox] +envlist = py27 + +[testenv] +setenv = PYTHONPATH = {toxinidir}:{toxinidir}/rms/external_mock/ +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +commands = + python setup.py testr --coverage --slowest --omit=rms/storage/my_sql/data_models.py,rms/tests/*,.tox/*,external_mock/*,setup.py,rms/logger/*,rms/app.py + coverage report --omit=rms/storage/my_sql/data_models.py,rms/tests/*,.tox/*,external_mock/*,setup.py,rms/logger/*,rms/app.py + coverage html --omit=rms/storage/my_sql/data_models.py,rms/tests/*,.tox/*,external_mock/*,setup.py,rms/logger/*,rms/app.py + +[testenv:cover] +setenv = PYTHONPATH = {toxinidir}:{toxinidir}/rms/external_mock/ +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +commands = + python setup.py testr --coverage --omit=rms/storage/my_sql/data_models.py,rms/tests/*,.tox/*,external_mock/*,setup.py,rms/logger/*,rms/app.py + coverage report --omit=rms/storage/my_sql/data_models.py,rms/tests/*,.tox/*,external_mock/*,setup.py,rms/logger/*,rms/app.py + coverage html --omit=rms/storage/my_sql/data_models.py,rms/tests/*,.tox/*,external_mock/*,setup.py,rms/logger/*,rms/app.py + +[testenv:pep8] +commands = + + py.test --pep8 -m pep8 diff --git a/orm/services/resource_distributor/_-init__.py b/orm/services/resource_distributor/_-init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/config.py b/orm/services/resource_distributor/config.py new file mode 100755 index 00000000..1b43ada6 --- /dev/null +++ b/orm/services/resource_distributor/config.py @@ -0,0 +1,176 @@ +# Pecan Application configurations +app = { + 'root': 'rds.controllers.root.RootController', + 'modules': ['rds'], + 'service_name': 'RDS' +} + +server = { + 'port': '8777', + 'host': '0.0.0.0' +} + +# DB configurations +database = { + 'url': 'mysql://root:stack@127.0.0.1/orm_rds?charset=utf8' +} + +sot = { + 'type': 'git', +} + +git = { + # possible values : 'native', 'gittle' + 'type': 'native', + 'local_repository_path': '/opt/app/orm/ORM', + 'file_name_format': 's_{}.yml', + 'relative_path_format': '/{}/hot/{}/{}', + 'commit_message_format': 'File was added to repository: {}', + 'commit_user': 'orm_rds', + 'commit_email': 'orm_rds@att.com', + 'git_server_url': 'orm_rds@172.20.90.218:~/SoT/ORM.git', + 'git_cmd_timeout': 45 +} + +audit = { + 'audit_server_url': 'http://127.0.0.1:8776/v1/audit/transaction', + 'num_of_send_retries': 3, + 'time_wait_between_retries': 1 +} + +ims = { + 'base_url': 'http://127.0.0.1:8084/', + 'metadata_path': 'v1/orm/images/{0}/regions/{1}/metadata' +} + +rms = { + 'base_url': 'http://127.0.0.1:8080/', + 'all_regions_path': 'v2/orm/regions' +} + +ordupdate = { + 'discovery_url': 'http://127.0.0.1', + 'discovery_port': 8080, + 'template_type': 'hot', + # This flag should be false only in case the ord does not support https. + 'https_enabled': True, + # ORD supports HTTPS and you don't need a certificate? set 'cert_path': '' + 'cert_path': '../resources/ord.crt' +} + +verify = False + +UUID_URL = 'http://172.20.90.232:8090/v1/uuids' + +# yaml configurations +yaml_configs = { + 'customer_yaml': { + 'yaml_version': '2014-10-16', + 'yaml_options': { + 'quotas': True, + 'type': 'ldap' + }, + 'yaml_keys': { + 'quotas_keys': { + 'keypairs': 'key_pairs', + 'network': 'networks', + 'port': 'ports', + 'router': 'routers', + 'subnet': 'subnets', + 'floatingip': 'floating_ips' + } + } + }, + 'flavor_yaml':{ + 'yaml_version': '2013-05-23', + 'yaml_args': { + 'rxtx_factor': 1 + } + }, + 'image_yaml': { + 'yaml_version': '2014-10-16' + } +} + +# value of status to be blocked before creating any resource +block_by_status = "Submitted" + +# this tells which values to allow resource submit the region +allow_region_statuses = ['functional'] + +# region_resource_id_status configurations +region_resource_id_status = { + # interval_time_validation in minutes + 'max_interval_time': { + 'images': 60, + 'tenants': 60, + 'flavors': 60, + 'users': 60, + 'default': 60 + }, + 'allowed_status_values': { + 'Success', + 'Error', + 'Submitted' + }, + 'allowed_operation_type': + { + 'create', + 'modify', + 'delete' + }, + 'allowed_resource_type': + { + 'customer', + 'image', + 'flavor' + } +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'rds': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'orm_common': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'audit_client': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + }, + 'Logfile': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'maxBytes': 50000000, + 'backupCount': 10, + 'filename': '/tmp/rds.log', + 'formatter': 'simple' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format':'%(asctime)s [%(padded_color_levelname)s] [%(name)s] [%(threadName)s] %(message)s', + '__force_dict__': True + } + } +} + + +authentication = { + "enabled": True, + "mech_id": "admin", + "mech_pass": "stack", + "tenant_name": "admin", + # The Keystone version currently in use. Can be either "2.0" or "3" + "keystone_version": "2.0" +} diff --git a/orm/services/resource_distributor/doc/RDS_Status_Service_Design.docx b/orm/services/resource_distributor/doc/RDS_Status_Service_Design.docx new file mode 100644 index 0000000000000000000000000000000000000000..e4bd69a3ab5d9755cdec39d31b823e081a3e69c9 GIT binary patch literal 466037 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�PBQ3PDXFIagSw=**FUA~@k%J{Kx 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%0DG4myg+Ju4NbA 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�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 j@Ide&0R`hwf+@SvfozC@G literal 0 HcmV?d00001 diff --git a/orm/services/resource_distributor/doc/source/conf.py b/orm/services/resource_distributor/doc/source/conf.py new file mode 100644 index 00000000..5db310ee --- /dev/null +++ b/orm/services/resource_distributor/doc/source/conf.py @@ -0,0 +1,75 @@ +# -*- 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 os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'orm_rds' +copyright = u'2013, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/orm/services/resource_distributor/doc/source/contributing.rst b/orm/services/resource_distributor/doc/source/contributing.rst new file mode 100644 index 00000000..30c78c8f --- /dev/null +++ b/orm/services/resource_distributor/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst diff --git a/orm/services/resource_distributor/doc/source/index.rst b/orm/services/resource_distributor/doc/source/index.rst new file mode 100644 index 00000000..8cbbcddf --- /dev/null +++ b/orm/services/resource_distributor/doc/source/index.rst @@ -0,0 +1,25 @@ +.. rds documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to rds's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/orm/services/resource_distributor/doc/source/installation.rst b/orm/services/resource_distributor/doc/source/installation.rst new file mode 100644 index 00000000..8a6f5e5b --- /dev/null +++ b/orm/services/resource_distributor/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install rds + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv rds + $ pip install rds diff --git a/orm/services/resource_distributor/doc/source/readme.rst b/orm/services/resource_distributor/doc/source/readme.rst new file mode 100644 index 00000000..0517a6bb --- /dev/null +++ b/orm/services/resource_distributor/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst diff --git a/orm/services/resource_distributor/doc/source/usage.rst b/orm/services/resource_distributor/doc/source/usage.rst new file mode 100644 index 00000000..d1854c14 --- /dev/null +++ b/orm/services/resource_distributor/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use rds in a project:: + + import rds diff --git a/orm/services/resource_distributor/ordmockserver/MANIFEST.in b/orm/services/resource_distributor/ordmockserver/MANIFEST.in new file mode 100644 index 00000000..c922f11a --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/MANIFEST.in @@ -0,0 +1 @@ +recursive-include public * diff --git a/orm/services/resource_distributor/ordmockserver/config.py b/orm/services/resource_distributor/ordmockserver/config.py new file mode 100755 index 00000000..7909c0e0 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/config.py @@ -0,0 +1,114 @@ +# Server Specific Configurations +server = { + 'port': '1337', + 'host': '0.0.0.0' +} + +UUID_URL = 'http://127.0.0.1:8090/v1/uuids' +RDS_STATUS_URL = 'http://127.0.0.1:8777/v1/rds/status' +SECONDS_BEFORE_STATUS_UPDATE = 5 + +status_data = { + 'ord_notifier_id': '1', + 'region': 'mtn6', + 'status': 'Success', + 'error_code': '', + 'error_msg': '' +} + +image_extra_metadata = { + 'checksum': 'd4ea426817a742328da91438e3a3208b', + # Size should be int and virtual_size should be real None once our + # database supports these values + 'size': '1337', + 'virtual_size': 'None' +} + +# Pecan Application Configurations +app = { + 'root': 'ordmockserver.controllers.root.RootController', + 'modules': ['ordmockserver'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/restapi/templates', + 'debug': True, + 'errors': { + 404: '/error/404', + '__force_dict__': True + } +} + +verify = False + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'restapi': {'level': 'DEBUG', 'handlers': ['console','logfile'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + }, + 'logfile' : { + 'class': 'logging.FileHandler', + 'filename' : '/home/pecanlogs.log', + 'level' : 'DEBUG', + 'formatter' : 'simple' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + + +""" +# orign logging +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'restapi': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} +""" +# Custom Configurations must be in Python dictionary format:: +# +# foo = {'bar':'baz'} +# +# All configurations are accessible at:: +# pecan.conf diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/__init__.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/app.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/app.py new file mode 100644 index 00000000..6e5140dd --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/ordmockserver/app.py @@ -0,0 +1,14 @@ +from pecan import make_app +from ordmockserver import model + + +def setup_app(config): + + model.init_model() + app_conf = dict(config.app) + + return make_app( + app_conf.pop('root'), + logging=getattr(config, 'logging', {}), + **app_conf + ) diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/__init__.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/models/__init__.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/models/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/root.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/root.py new file mode 100755 index 00000000..a14c7df8 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/OrdNotifier/root.py @@ -0,0 +1,90 @@ +import json +import logging.handlers +from pecan import conf +from pecan import request +import pecan.rest +import requests +import threading +import time +import wsme +from wsme import types as wtypes +from wsmeext.pecan import wsexpose + +my_logger = logging.getLogger(__name__) + + +class Result(wtypes.DynamicBase): + haha = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, haha): + self.haha = haha + + +class OrdNotifierWrapper(wtypes.DynamicBase): + ord_notifier = wsme.wsattr( + {str: str, str: str, str: str, str: str, str: str}, mandatory=False, + name='ord-notifier') + + def __init__(self, ord_notifier=None): + self.ord_notifier = ord_notifier + + +def send_status_update(ord_notifier_wrapper): + # Wait before sending the status update, to make sure RDS updates the + # status to Submitted + time.sleep(conf.SECONDS_BEFORE_STATUS_UPDATE) + + json_to_send = {"rds-listener": {}} + for key in ('ord-notifier-id', 'region', 'status', 'error-code', + 'error-msg',): + # Take the keys from the configuration + json_to_send['rds-listener'][key] = conf.status_data[ + key.replace('-', '_')] + + for key in ('request-id', 'resource-id', 'resource-type', + 'resource-template-version', 'resource-template-type', + 'region',): + # Take the keys from the input json + json_to_send['rds-listener'][key] = ord_notifier_wrapper.ord_notifier[ + key] + + json_to_send['rds-listener']['resource-operation'] = \ + ord_notifier_wrapper.ord_notifier['operation'] + + if ord_notifier_wrapper.ord_notifier['resource-type'] == 'image': + json_to_send['rds-listener'][ + 'resource_extra_metadata'] = dict(conf.image_extra_metadata) + + result = requests.post(conf.RDS_STATUS_URL, + headers={'Content-Type': 'application/json'}, + data=json.dumps(json_to_send), + verify=conf.verify) + my_logger.debug( + 'Status update status code: {}, content: {}'.format(result.status_code, + result.content)) + return result + + +class OrdNotifier(pecan.rest.RestController): + def _send_status_update(self, ord_notifier_wrapper): + thread = threading.Thread(target=send_status_update, + args=(ord_notifier_wrapper,)) + thread.start() + + @wsexpose(Result, body=OrdNotifierWrapper, status_code=200, + rest_content_types='json') + def post(self, ord_notifier_wrapper): + try: + my_logger.debug('Entered post, ord_notifier: {}'.format( + ord_notifier_wrapper.ord_notifier)) + mandatory_keys = ['resource-type'] + if not all( + [key in ord_notifier_wrapper.ord_notifier for key in + mandatory_keys]): + raise ValueError('A mandatory key is missing') + + self._send_status_update(ord_notifier_wrapper) + except Exception as exc: + my_logger.error(str(exc)) + + return Result('Success') diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/__init__.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/root.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/root.py new file mode 100755 index 00000000..5dfe7670 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/ordmockserver/controllers/root.py @@ -0,0 +1,48 @@ +from pecan import expose, redirect, response +from pecan import * +from webob.exc import status_map +from OrdNotifier import root + + +class CatalogController(object): + @expose() + def index(self): + return "Welcome to the catalog." + + +class ORD(object): + @expose() + def index(self): + return dict() + ord_notifier=root.OrdNotifier() + + +class RootOne(object): + @expose() + def index(self): + return dict() + ord=ORD() + + +class RootController(object): + + @expose(generic=True, template='index.html') + def index(self): + return dict() + + @index.when(method='GET') + def index_get(self): + return 'hi' + + + def error(self, status): + try: + status = int(status) + except ValueError: # pragma: no cover + status = 500 + message = getattr(status_map.get(status), 'explanation', '') + return dict(status=status, message=message) + + cat=CatalogController() + #customer=root.CreateNewCustomer() + v1=RootOne() diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/model/__init__.py b/orm/services/resource_distributor/ordmockserver/ordmockserver/model/__init__.py new file mode 100644 index 00000000..d983f7bc --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/ordmockserver/model/__init__.py @@ -0,0 +1,15 @@ +from pecan import conf # noqa + + +def init_model(): + """ + This is a stub method which is called at application startup time. + + If you need to bind to a parsed database configuration, set up tables or + ORM classes, or perform any database initialization, this is the + recommended place to do it. + + For more information working with databases, and some common recipes, + see http://pecan.readthedocs.org/en/latest/databases.html + """ + pass diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/error.html b/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/error.html new file mode 100644 index 00000000..f2d97961 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/error.html @@ -0,0 +1,12 @@ +<%inherit file="layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Server Error ${status} + + +## now define the body of the template +
+

Server Error ${status}

+
+

${message}

diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/index.html b/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/index.html new file mode 100644 index 00000000..f17c3862 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/index.html @@ -0,0 +1,34 @@ +<%inherit file="layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Welcome to Pecan! + + +## now define the body of the template +
+

+
+ +
diff --git a/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/layout.html b/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/layout.html new file mode 100644 index 00000000..40908591 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/ordmockserver/templates/layout.html @@ -0,0 +1,22 @@ + + + ${self.title()} + ${self.style()} + ${self.javascript()} + + + ${self.body()} + + + +<%def name="title()"> + Default Title + + +<%def name="style()"> + + + +<%def name="javascript()"> + + diff --git a/orm/services/resource_distributor/ordmockserver/public/css/style.css b/orm/services/resource_distributor/ordmockserver/public/css/style.css new file mode 100644 index 00000000..55c9db54 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/public/css/style.css @@ -0,0 +1,43 @@ +body { + background: #311F00; + color: white; + font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif; + padding: 1em 2em; +} + +a { + color: #FAFF78; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div#content { + width: 800px; + margin: 0 auto; +} + +form { + margin: 0; + padding: 0; + border: 0; +} + +fieldset { + border: 0; +} + +input.error { + background: #FAFF78; +} + +header { + text-align: center; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Futura-CondensedExtraBold', 'Futura', 'Helvetica', sans-serif; + text-transform: uppercase; +} diff --git a/orm/services/resource_distributor/ordmockserver/public/images/logo.png b/orm/services/resource_distributor/ordmockserver/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f403e4a4f3ce69a4577a46ae37f5633abb79fa GIT binary patch literal 20596 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 ZD6?|u)*zSG3-Sw<9?1AXo%Yig{{dC~0l)wN literal 0 HcmV?d00001 diff --git a/orm/services/resource_distributor/ordmockserver/setup.cfg b/orm/services/resource_distributor/ordmockserver/setup.cfg new file mode 100644 index 00000000..4c9b8495 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/setup.cfg @@ -0,0 +1,6 @@ +[nosetests] +match=^test +where=ordmockserver +nocapture=1 +cover-package=ordmockserver +cover-erase=1 diff --git a/orm/services/resource_distributor/ordmockserver/setup.py b/orm/services/resource_distributor/ordmockserver/setup.py new file mode 100644 index 00000000..c4d11ee5 --- /dev/null +++ b/orm/services/resource_distributor/ordmockserver/setup.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages + +setup( + name='ordmockserver', + version='0.1', + description='', + author='', + author_email='', + install_requires=[ + "pecan", + ], + test_suite='ordmockserver', + zip_safe=False, + include_package_data=True, + packages=find_packages(exclude=['ez_setup']) +) diff --git a/orm/services/resource_distributor/rds.conf b/orm/services/resource_distributor/rds.conf new file mode 100644 index 00000000..a7bba7d1 --- /dev/null +++ b/orm/services/resource_distributor/rds.conf @@ -0,0 +1,26 @@ +Listen 8777 + + + + WSGIDaemonProcess rds user=orm group=orm threads=5 + WSGIScriptAlias / /opt/app/orm/rds/rds.wsgi + + + Order deny,allow + Deny from all + Allow from localhost + + + + Order deny,allow + Deny from all + Allow from localhost + + + + WSGIProcessGroup rds + WSGIApplicationGroup %{GLOBAL} + Require all granted + Allow from all + + diff --git a/orm/services/resource_distributor/rds.wsgi b/orm/services/resource_distributor/rds.wsgi new file mode 100644 index 00000000..f5259e20 --- /dev/null +++ b/orm/services/resource_distributor/rds.wsgi @@ -0,0 +1,2 @@ +from pecan.deploy import deploy +application = deploy('/opt/app/orm/rds/config.py') diff --git a/orm/services/resource_distributor/rds/__init__.py b/orm/services/resource_distributor/rds/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/app.py b/orm/services/resource_distributor/rds/app.py new file mode 100755 index 00000000..8f9a1e9c --- /dev/null +++ b/orm/services/resource_distributor/rds/app.py @@ -0,0 +1,75 @@ +import logging +import os + +from pecan import make_app, conf +from pecan.commands import CommandRunner + +from services import region_resource_id_status +from storage import factory +from sot import sot_factory + +from audit_client.api import audit + + +logger = logging.getLogger(__name__) + + +def setup_app(pecan_config): + """This method is the starting point of the application. + The application can be started either by running pecan + and pass it the config.py, + or by running this file with python, + then the main method is called and starting pecan. + + The method initializes components and return a WSGI application""" + + init_sot() + init_audit() + + factory.database = conf.database + region_resource_id_status.config = conf.region_resource_id_status + + app = make_app(conf.app.root, logging=conf.logging) + logger.info('Starting RDS...') + + validate_sot() + + return app + + +def init_sot(): + """Initialize SoT module + """ + sot_factory.sot_type = conf.sot.type + sot_factory.local_repository_path = conf.git.local_repository_path + sot_factory.relative_path_format = conf.git.relative_path_format + sot_factory.file_name_format = conf.git.file_name_format + sot_factory.commit_message_format = conf.git.commit_message_format + sot_factory.commit_user = conf.git.commit_user + sot_factory.commit_email = conf.git.commit_email + sot_factory.git_server_url = conf.git.git_server_url + sot_factory.git_type = conf.git.type + + +def init_audit(): + """Initialize audit client module + """ + audit.init(conf.audit.audit_server_url, + conf.audit.num_of_send_retries, + conf.audit.time_wait_between_retries, + conf.app.service_name) + + +def validate_sot(): + sot_factory.get_sot().validate_sot_state() + + +def main(): + dir_name = os.path.dirname(__file__) + drive, path_and_file = os.path.splitdrive(dir_name) + path, filename = os.path.split(path_and_file) + runner = CommandRunner() + runner.run(['serve', path+'/config.py']) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/orm/services/resource_distributor/rds/controllers/__init__.py b/orm/services/resource_distributor/rds/controllers/__init__.py new file mode 100644 index 00000000..e1a527df --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/__init__.py @@ -0,0 +1 @@ +"""v1 package.""" diff --git a/orm/services/resource_distributor/rds/controllers/root.py b/orm/services/resource_distributor/rds/controllers/root.py new file mode 100644 index 00000000..5f687358 --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/root.py @@ -0,0 +1,8 @@ +"""controller moudle.""" +from rds.controllers.v1 import root as v1 + + +class RootController(object): + """api controller.""" + + v1 = v1.V1Controller() diff --git a/orm/services/resource_distributor/rds/controllers/v1/__init__.py b/orm/services/resource_distributor/rds/controllers/v1/__init__.py new file mode 100644 index 00000000..b933650e --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/__init__.py @@ -0,0 +1 @@ +"""v1 package.""" diff --git a/orm/services/resource_distributor/rds/controllers/v1/base.py b/orm/services/resource_distributor/rds/controllers/v1/base.py new file mode 100644 index 00000000..15cb5ad2 --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/base.py @@ -0,0 +1,100 @@ +"""Exceptions.""" +import wsme +from wsme import types as wtypes + + +class ClientSideError(wsme.exc.ClientSideError): + """return 400 with error message.""" + + def __init__(self, error, status_code=400): + """init function.. + + :param error: error message + :param status_code: returned code + """ + super(ClientSideError, self).__init__(error, status_code) + + +class InputValueError(ClientSideError): + """return 400 for invalid input.""" + + def __init__(self, name, value, status_code=400): + """init function. + + :param name: inavlid input field name + :param value: invalid value + :param status_code: returned code + """ + super(InputValueError, self).__init__("Invalid " + "value for input {} : " + "{}".format(name, value), + status_code) + + +class EntityNotFoundError(ClientSideError): + """return 404 entity not found.""" + + def __init__(self, id): + """init func. + + :param id: Entity id + """ + super(EntityNotFoundError, self).__init__("Entity not found " + "for {}".format(id), + status_code=404) + + +class LockedEntity(ClientSideError): + """return 409 locked.""" + + def __init__(self, name): + """init func. + + :param name: locked message + """ + super(LockedEntity, self).__init__("Entity {} is " + "locked".format(name), + status_code=409) + + +class NotAllowedError(ClientSideError): + """return 405 not allowed operation.""" + + def __init__(self, name): + """init func. + + :param name: name of method + """ + super(NotAllowedError, self).__init__("not allowed : " + "{}".format(name), + status_code=405) + + +class Base(wtypes.DynamicBase): + """not implemented.""" + + pass + + ''' + @classmethod + def from_model(cls, m): + return cls(**(m.as_dict())) + + def as_dict(self, model): + valid_keys = inspect.getargspec(model.__init__)[0] + if 'self' in valid_keys: + valid_keys.remove('self') + return self.as_dict_from_keys(valid_keys) + + + def as_dict_from_keys(self, keys): + return dict((k, getattr(self, k)) + for k in keys + if hasattr(self, k) and + getattr(self, k) != wsme.Unset) + + @classmethod + def from_db_and_links(cls, m, links): + return cls(links=links, **(m.as_dict())) + + ''' diff --git a/orm/services/resource_distributor/rds/controllers/v1/configuration/__init__.py b/orm/services/resource_distributor/rds/controllers/v1/configuration/__init__.py new file mode 100644 index 00000000..e1a527df --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/configuration/__init__.py @@ -0,0 +1 @@ +"""v1 package.""" diff --git a/orm/services/resource_distributor/rds/controllers/v1/configuration/root.py b/orm/services/resource_distributor/rds/controllers/v1/configuration/root.py new file mode 100644 index 00000000..f8be0ffa --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/configuration/root.py @@ -0,0 +1,28 @@ +"""Configuration rest API input module.""" + +import logging +from orm_common.utils import utils +from pecan import conf +from pecan import rest +from wsmeext.pecan import wsexpose + +logger = logging.getLogger(__name__) + + +class Configuration(rest.RestController): + """Configuration controller.""" + + @wsexpose(str, str, status_code=200) + def get(self, dump_to_log='false'): + """get method. + + :param dump_to_log: A boolean string that says whether the + configuration should be written to log + :return: A pretty string that contains the service's configuration + """ + logger.info("Get configuration...") + + dump = dump_to_log.lower() == 'true' + utils.set_utils_conf(conf) + result = utils.report_config(conf, dump, logger) + return result diff --git a/orm/services/resource_distributor/rds/controllers/v1/logs.py b/orm/services/resource_distributor/rds/controllers/v1/logs.py new file mode 100644 index 00000000..e8d7b1bb --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/logs.py @@ -0,0 +1,65 @@ +import logging + +from pecan import rest +import wsme +from wsmeext.pecan import wsexpose + +logger = logging.getLogger(__name__) + + +class LogChangeResultWSME(wsme.types.DynamicBase): + """log change result wsme type.""" + + result = wsme.wsattr(str, mandatory=True, default=None) + + def __init__(self, **kwargs): + """"init method.""" + super(LogChangeResult, self).__init__(**kwargs) + + +class LogChangeResult(object): + """log change result type.""" + + def __init__(self, result): + """"init method.""" + self.result = result + + +class LogsController(rest.RestController): + """Logs Audit controller.""" + + @wsexpose(LogChangeResultWSME, str, status_code=201, + rest_content_types='json') + def put(self, level): + """update log level. + + :param level: the log level text name + :return: + """ + + logger.info("Changing log level to [{}]".format(level)) + try: + log_level = logging._levelNames.get(level.upper()) + if log_level is not None: + self._change_log_level(log_level) + result = "Log level changed to {}.".format(level) + logger.info(result) + else: + raise Exception( + "The given log level [{}] doesn't exist.".format(level)) + except Exception as e: + result = "Fail to change log_level. Reason: {}".format( + e.message) + logger.error(result) + return LogChangeResult(result) + + @staticmethod + def _change_log_level(log_level): + path = __name__.split('.') + if len(path) > 0: + root = path[0] + root_logger = logging.getLogger(root) + root_logger.setLevel(log_level) + else: + logger.info("Fail to change log_level to [{}]. " + "the given log level doesn't exist.".format(log_level)) diff --git a/orm/services/resource_distributor/rds/controllers/v1/resources/__init__.py b/orm/services/resource_distributor/rds/controllers/v1/resources/__init__.py new file mode 100644 index 00000000..a3c7f303 --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/resources/__init__.py @@ -0,0 +1 @@ +"""resource package.""" diff --git a/orm/services/resource_distributor/rds/controllers/v1/resources/root.py b/orm/services/resource_distributor/rds/controllers/v1/resources/root.py new file mode 100755 index 00000000..ab202b0b --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/resources/root.py @@ -0,0 +1,283 @@ +"""handle rest api input module.""" + +import ast +import logging.handlers +import time + +import pecan +import wsme +from pecan import rest +from wsme import types as wtypes +from wsmeext.pecan import wsexpose + +from rds.controllers.v1.base import ClientSideError +from rds.controllers.v1.base import LockedEntity +from rds.controllers.v1.base import NotAllowedError +from rds.services import resource as ResourceService +from rds.services.base import ConflictValue + +my_logger = logging.getLogger(__name__) + +resources_operation_list = { + "flavor": ['delete', 'create', 'modify'], + "image": ['delete', 'create', 'modify'] +} + + +class Links(wtypes.DynamicBase): + """class method.""" + + self = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(own, self=""): + """init function. + + :param self: self link + """ + own.self = self + + +class CreatedResource(wtypes.DynamicBase): + """class method for returned json.""" + + id = wsme.wsattr(wtypes.text, mandatory=True) + created = wsme.wsattr(wtypes.text, mandatory=False) + links = wsme.wsattr(Links, mandatory=True) + updated = wsme.wsattr(wtypes.text, mandatory=False) + err = wsme.wsattr(wtypes.text, mandatory=False) + message = wsme.wsattr(wtypes.text, mandatory=False) + + def __init__(self, id="", err=None, message=None, + created=None, updated=None, links=Links()): + """init function. + + :param id: resource id + :param err: error if any + :param message: error message + :param created: timestamp + :param updated: timestamp when put request + :param links: + """ + self.id = id + self.links = links + if created is not None: + self.created = created + if updated is not None: + self.updated = updated + if err is not None: + self.err = err # pragma: no cover + if message is not None: + self.message = message # pragma: no cover + + +class Result(wtypes.DynamicBase): + """class method, json header.""" + + customer = wsme.wsattr(CreatedResource, mandatory=False) + flavor = wsme.wsattr(CreatedResource, mandatory=False) + image = wsme.wsattr(CreatedResource, mandatory=False) + + def __init__(self, customer=None, + flavor=None, image=None): + """init function. + + :param customer: json header + :param flavor: json header + """ + if customer is not None: + self.customer = customer + if flavor is not None: + self.flavor = flavor + if image is not None: + self.image = image + + +class TrackingData(wtypes.DynamicBase): + """class method to handle json input.""" + + external_id = wsme.wsattr(wtypes.text, mandatory=True) + tracking_id = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, external_id="", tracking_id=""): + """init function. + + :param external_id: full flow traking id + :param tracking_id: enternal traking id + """ + self.external_id = external_id + self.tracking_id = tracking_id + + +class ResourceTypeData(wtypes.DynamicBase): + """class method, handle json input.""" + + resource_type = wsme.wsattr(wtypes.text, mandatory=True) + resource_id = wsme.wsattr(wtypes.text, mandatory=False) + + def __init__(self, resource_type="", resource_id=None): + """init function. + + :param resource_type: type of the resource eg.customer, flavor.. + :param resource_id: id of the resource + """ + self.resource_type = resource_type + if resource_id is not None: + self.resource_id = resource_id # pragma: no cover + + +class ResourceData(wtypes.DynamicBase): + """class method to handle resource data json.""" + + resource = wsme.wsattr(ResourceTypeData, mandatory=True) + model = wsme.wsattr(wtypes.text, mandatory=True) + # model = wsme.wsattr(FullJson, mandatory=True) + tracking = wsme.wsattr(TrackingData, mandatory=True) + + def __init__(self, model="", resource=ResourceTypeData(), + tracking=TrackingData(),): + """init function. + + :param model: input json (resource data) + :param resource: resource type, resource id + :param tracking: transaction id + """ + self.resource = resource + self.tracking = tracking + self.model = model + + +class Resource(wtypes.DynamicBase): + """main class first key json.""" + + service_template = wsme.wsattr(ResourceData, mandatory=True) + + def __init__(self, service_template=ResourceData()): + """init function. + + :param service_template: + """ + self.service_template = service_template + + +class CreateNewResource(rest.RestController): + """creatin new resource controller.""" + + @wsexpose(Result, body=Resource, status_code=201, + rest_content_types='json') + def post(self, resource): + """Handle HTTP POST request. + + :param Customer (json in request body): + :return: result (json format ... {'Cusetomer':{'id':'', + 'links':{'own':'how host url'},'created':'1234567890'}} + the response will be 201 created if success + :return 409 for conflict + :return 400 bad request + handle json input + """ + my_logger.info("create resource") + jsondata = resource.service_template.model + my_logger.debug("parse json & get yaml file!!! {}".format(jsondata)) + uuid = resource.service_template.tracking.tracking_id + resource_type = resource.service_template.resource.resource_type + base_url = pecan.request.application_url + jsondata = ast.literal_eval(jsondata) + + try: + resource_id = ResourceService.main(jsondata, + uuid, + resource_type, + 'create') + site_link = "%s/v1/rds/%s/%s" % (base_url, + resource_type, + resource_id) + res = Result(**{resource_type: CreatedResource(id=resource_id, + created='%d' % (time.time()*1000), + links=Links(site_link))}) + return res + except ConflictValue as e: + my_logger.error("the request blocked need to wait " + "for previous operation to be done ") + raise LockedEntity(e.message) + except Exception as e: + my_logger.error("error :- %s " % str(e.message)) + raise ClientSideError(e.message) + + @wsexpose(Result, body=Resource, status_code=201, + rest_content_types='json') + def put(self, resource): + """Handle HTTP POST request. + + :param Customer (json in request body): + :return: result (json format ... {'Cusetomer':{'id':'', + 'links':{'own':'how host url'},'created':'1234567890'}} + the response will be 201 created if success + :return 409 for conflict + :return 400 bad request + handle json input + """ + my_logger.info("modify resource") + jsondata = resource.service_template.model + my_logger.debug("parse json & get yaml file!!! {}".format(jsondata)) + uuid = resource.service_template.tracking.tracking_id + resource_type = resource.service_template.resource.resource_type + base_url = pecan.request.application_url + jsondata = ast.literal_eval(jsondata) + + try: + resource_id = ResourceService.main(jsondata, + uuid, + resource_type, + 'modify') + my_logger.debug("data sent!.") + site_link = "%s/v1/rds/%s/%s" % (base_url, + resource_type, + resource_id) + res = Result(**{resource_type: CreatedResource(id=resource_id, + updated='%d' % ( + time.time() * 1000), + links=Links( + site_link))}) + return res + except ConflictValue as e: + my_logger.error("the request blocked need to wait " + "for previous operation to be done ") + raise LockedEntity(e.message) + except Exception as e: + my_logger.error("error :- %s " % str(e.message)) + raise ClientSideError(e.message) + + @wsexpose(str, body=Resource, status_code=200, + rest_content_types='json') + def delete(self, resource): + """handle json input. + + :param resource: input json + :return: 200 if valid json + :return: 405 not allowed for not valid resource to delete + :return: 400 for bad request + """ + operation = 'delete' + my_logger.info("delete resource ") + jsondata = resource.service_template.model + my_logger.debug("parse json & get yaml file!!! {}".format(jsondata)) + jsondata = ast.literal_eval(jsondata) + resource_uuid = resource.service_template.tracking.tracking_id + resource_type = resource.service_template.resource.resource_type + if resource_type not in resources_operation_list or operation not in \ + resources_operation_list[resource_type]: + raise NotAllowedError("delete Not allowed for this" + " resource %s" % resource_type) + try: + resource_id = ResourceService.main(jsondata, + resource_uuid, + resource_type, + operation) + return resource_id + except ConflictValue as e: + my_logger.error("the request blocked need to wait" + " for previous operation to be done ") + raise LockedEntity(e.message) + except Exception as e: + my_logger.error("error :- %s " % str(e.message)) + raise ClientSideError(e.message) diff --git a/orm/services/resource_distributor/rds/controllers/v1/root.py b/orm/services/resource_distributor/rds/controllers/v1/root.py new file mode 100755 index 00000000..3f395838 --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/root.py @@ -0,0 +1,21 @@ +"""v1 controller.""" +from rds.controllers.v1 import logs +from rds.controllers.v1.configuration import root as config_root +from rds.controllers.v1.resources import root as Rds + +from rds.controllers.v1.status import resource_status + + +class RDS(object): + """RDS controller.""" + + resources = Rds.CreateNewResource() + status = resource_status.Status() + configuration = config_root.Configuration() + logs = logs.LogsController() + + +class V1Controller(object): + """v1 controller.""" + + rds = RDS diff --git a/orm/services/resource_distributor/rds/controllers/v1/status/__init__.py b/orm/services/resource_distributor/rds/controllers/v1/status/__init__.py new file mode 100644 index 00000000..1c127ddb --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/status/__init__.py @@ -0,0 +1 @@ +"""status module.""" diff --git a/orm/services/resource_distributor/rds/controllers/v1/status/get_resource.py b/orm/services/resource_distributor/rds/controllers/v1/status/get_resource.py new file mode 100755 index 00000000..592c8a16 --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/status/get_resource.py @@ -0,0 +1,111 @@ +"""handle get resource module.""" +import logging + +import wsme +from pecan import rest +from wsme import types as wtypes +from wsmeext.pecan import wsexpose + +from rds.controllers.v1.base import EntityNotFoundError +from rds.services import region_resource_id_status as regionResourceIdStatus + +logger = logging.getLogger(__name__) + + +class ResourceMetaData(wtypes.DynamicBase): + """class method.""" + + checksum = wsme.wsattr(wtypes.text, mandatory=True) + virtual_size = wsme.wsattr(wtypes.text, mandatory=True) + size = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, size='', virtual_size='', checksum=''): + """ + + :param size: + :param virtual_size: + :param checksum: + """ + self.checksum = checksum + self.virtual_size = virtual_size + self.size = size + + +class OutputResource(wtypes.DynamicBase): + """class method returned json body.""" + + region = wsme.wsattr(wtypes.text, mandatory=True) + timestamp = wsme.wsattr(wtypes.text, mandatory=True) + ord_transaction_id = wsme.wsattr(wtypes.text, mandatory=True) + resource_id = wsme.wsattr(wtypes.text, mandatory=True) + ord_notifier_id = wsme.wsattr(wtypes.text, mandatory=True) + status = wsme.wsattr(wtypes.text, mandatory=True) + error_code = wsme.wsattr(wtypes.text, mandatory=True) + error_msg = wsme.wsattr(wtypes.text, mandatory=True) + resource_extra_metadata = wsme.wsattr(ResourceMetaData, mandatory=False) + operation = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, region="", timestamp="", ord_transaction_id="", + resource_id="", ord_notifier_id="", status="", + error_code="", error_msg="", operation="", + resource_meta_data=ResourceMetaData()): + """init function. + + :param region: targets : list of lcp's + :param timestamp: + :param ord_transaction_id: + :param resource_id: + :param ord_notifier_id: + :param status: success, error, submitted + :param error_code: + :param error_msg: error message + """ + self.region = region + self.timestamp = timestamp + self.ord_notifier_id = ord_notifier_id + self.ord_transaction_id = ord_transaction_id + self.resource_id = resource_id + self.status = status + self.error_code = error_code + self.error_msg = error_msg + self.operation = operation + if resource_meta_data: + self.resource_extra_metadata = resource_meta_data + + +class Result(wtypes.DynamicBase): + """class method json headers.""" + + regions = wsme.wsattr([OutputResource], mandatory=True) + status = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, status=[OutputResource()]): + """init dunction. + + :param status: mian status: success, error, submitted + """ + self.status = status # pragma: no cover + + +class GetResource(rest.RestController): + """controller get resource.""" + + @wsexpose(Result, str, status_code=200, rest_content_types='json') + def get(self, id): + """get method. + + :param id: resource id + :return: json output by resource id + if no data for this resource id 404 will be returned + :description: the function will get resource id check the DB for + all resource status and return list of json data + """ + logger.info("get status") + logger.debug("get status data by resource id : %s" % id) + result = regionResourceIdStatus.get_status_by_resource_id(id) + + if result is None or not result.regions: + logger.error("no content for id %s " % id) + raise EntityNotFoundError("resourceid %s" % id) + logger.debug("items number : %s" % len(result.status)) + return result diff --git a/orm/services/resource_distributor/rds/controllers/v1/status/resource_status.py b/orm/services/resource_distributor/rds/controllers/v1/status/resource_status.py new file mode 100755 index 00000000..b4b71b11 --- /dev/null +++ b/orm/services/resource_distributor/rds/controllers/v1/status/resource_status.py @@ -0,0 +1,155 @@ +"""handle post request module.""" +import logging +import time + +import wsme +from pecan import rest +from rds.controllers.v1.base import InputValueError, ClientSideError +from wsme import types as wtypes +from wsmeext.pecan import wsexpose + +from rds.controllers.v1.status import get_resource +from rds.services import region_resource_id_status as regionResourceIdStatus +from rds.services.base import InputError, ErrorMesage +from rds.utils import utils + +logger = logging.getLogger(__name__) + + +class MetaData(wtypes.DynamicBase): + """class method metadata input.""" + checksum = wsme.wsattr(wtypes.text, mandatory=True) + virtual_size = wsme.wsattr(wtypes.text, mandatory=True) + size = wsme.wsattr(wtypes.text, mandatory=True) + + def __init__(self, checksum=None, virtual_size=None, size=None): + """ + + :param checksum: + :param virtual_size: + :param size: + """ + self.size = size + self.checksum = checksum + self.virtual_size = virtual_size + + def to_dict(self): + return dict(size=self.size, + checksum=self.checksum, + virtual_size=self.virtual_size) + + +class ResourceData(wtypes.DynamicBase): + """class method, handle json input.""" + + resource_id = wsme.wsattr(wtypes.text, mandatory=True, name='resource-id') + request_id = wsme.wsattr(wtypes.text, mandatory=True, name='request-id') + resource_type = wsme.wsattr(wtypes.text, mandatory=True, + name='resource-type') + resource_template_version = wsme.wsattr(wtypes.text, mandatory=True, + name='resource-template-version') + resource_template_type = wsme.wsattr(wtypes.text, mandatory=True, + name='resource-template-type') + resource_operation = wsme.wsattr(wtypes.text, mandatory=True, + name='resource-operation') + ord_notifier_id = wsme.wsattr(wtypes.text, mandatory=True, + name='ord-notifier-id') + region = wsme.wsattr(wtypes.text, mandatory=True) + status = wsme.wsattr(wtypes.text, mandatory=True) + error_code = wsme.wsattr(wtypes.text, mandatory=True, name='error-code') + error_msg = wsme.wsattr(wtypes.text, mandatory=True, name='error-msg') + resource_extra_metadata = wsme.wsattr(MetaData, mandatory=False) + + def __init__(self, resource_id="", request_id="", resource_type="", + resource_template_version="", resource_template_type="", + resource_operation="", ord_notifier_id="", region="", + status="", error_code="", error_msg="", + resource_extra_metadata=None): + """init function. + + :param resource_id: uuid + :param request_id: + :param resource_type: customer, flavor, image... + :param resource_template_version: version of heat + :param resource_template_type: + :param resource_operation: create, delete.. + :param ord_notifier_id: + :param region: lcp's + :param status: success, error, submitted + :param error_code: + :param error_msg: error message + """ + self.resource_id = resource_id + self.request_id = request_id + self.resource_type = resource_type + self.resource_template_version = resource_template_version + self.resource_template_type = resource_template_type + self.resource_operation = resource_operation + self.ord_notifier_id = ord_notifier_id + self.region = region + self.status = status + self.error_code = error_code + self.error_msg = error_msg + if resource_extra_metadata: + self.resource_extra_metadata = resource_extra_metadata + + +class StatusInput(wtypes.DynamicBase): + """class method, input json header.""" + + rds_listener = wsme.wsattr(ResourceData, mandatory=True, + name='rds-listener') + + def __init__(self, rds_listener=ResourceData()): + """init function. + + :param rds_listener: json header + """ + self.rds_listener = rds_listener + + +class Status(rest.RestController): + """post status controller.""" + + resource = get_resource.GetResource() + + @wsexpose(None, body=StatusInput, status_code=201, + rest_content_types='json') + def post(self, status_input): + """handle post request. + + :param status_input: json data + :return: 201 created + :description: get input json create dict and save dict to the DB + if any validation fields fail will return input value error 400 + """ + logger.info("post status") + logger.debug("parse json!") + data_to_save = dict( + timestamp=int(time.time())*1000, + region=status_input.rds_listener.region, + resource_id=status_input.rds_listener.resource_id, + status=status_input.rds_listener.status, + transaction_id=status_input.rds_listener.request_id, + error_code=status_input.rds_listener.error_code, + error_msg=status_input.rds_listener.error_msg, + resource_operation=status_input.rds_listener.resource_operation, + resource_type=status_input.rds_listener.resource_type, + ord_notifier_id=status_input.rds_listener.ord_notifier_id) + + if status_input.rds_listener.resource_type == 'image' and status_input.rds_listener.resource_extra_metadata != wsme.Unset: + data_to_save['resource_extra_metadata'] =\ + status_input.rds_listener.resource_extra_metadata.to_dict() + + logger.debug("save data to database.. data :- %s" % data_to_save) + try: + regionResourceIdStatus.add_status(data_to_save) + # send data to ims + utils.post_data_to_image(data_to_save) + except ErrorMesage as exp: + logger.error(exp.message) + # raise ClientSideError(status_code=400, error=exp.message) + except InputError as e: + logger.error("Invalid value for input {}: {}".format(str(e.name), + str(e.value))) + raise InputValueError(e.name, e.value) diff --git a/orm/services/resource_distributor/rds/ordupdate/__init__.py b/orm/services/resource_distributor/rds/ordupdate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/ordupdate/ord_notifier.py b/orm/services/resource_distributor/rds/ordupdate/ord_notifier.py new file mode 100755 index 00000000..3e5bd92d --- /dev/null +++ b/orm/services/resource_distributor/rds/ordupdate/ord_notifier.py @@ -0,0 +1,287 @@ +"""ORD trigger main module.""" + +import json +import time + +import logging +import requests + +from pecan import conf + +from audit_client.api import audit + +from rds.services import region_resource_id_status as regionResourceIdStatus + +# REST API constants +OK_CODE = 200 +ACK_CODE = 200 + +logger = logging.getLogger(__name__) + + +class OrdNotFoundError(Exception): + """Indicates that the correct ORD to notify was not found.""" + + pass + + +class NotifyNotAcknowledgedError(Exception): + """Indicates that the ORD did not respond correctly to our notification.""" + + pass + + +class ConfigFileError(Exception): + """Indicates that the configuration file could not be found.""" + + pass + + +def _find_correct_ord(url, lcp_name): + """Use the Discover API to get the ORD URL. + + :param url: Discovery server URL + :param lcp_name: The name of the LCP whose ORD is to be found + :return: The ORD URL, or None if it wasn't found + """ + logger.info('Getting the ORD URL of LCP %s...' % (lcp_name,)) + # Get the LCP record from RMS + response = requests.get('%s/v2/orm/regions?regionname=%s' % (url, + lcp_name,), + verify=conf.verify) + if response.status_code != OK_CODE: + return None + + lcp = response.json() + try: + for endpoint in lcp['regions'][0]['endpoints']: + if endpoint['type'] == 'ord': + return endpoint['publicURL'] + except KeyError: + return None + + # Invalid LCP record (does not contain an ORD) + return None + + +def _notify(ord_url, + transaction_id, + resource_id, + resource_type, + resource_template_version, + resource_template_name, + operation, + region_id): + """Send the notification message to the ORD. + + :param ord_url: + :param transaction_id: + :param resource_id: + :param resource_type: + :param resource_template_version: + :param resource_template_name: + :param operation: + :param region_id: + :raise: requests.exceptions.ConnectionError when the POST request + cannot be sent, + NotifyNotAcknowledgedError when the ORD did not respond to the notification + as expected + InvalidJsonError if the payload is missing one of the expected values + :return: + """ + # Prepare the request body + data_to_send = {'ord-notifier': { + 'request-id': transaction_id, + 'resource-id': resource_id, + 'resource-type': resource_type, + 'resource-template-version': resource_template_version, + 'resource-template-name': resource_template_name, + 'resource-template-type': conf.ordupdate.template_type, + 'operation': operation, + 'region': region_id + } + } + + is_ord_url_https = ord_url.startswith('https') + https_enabled = conf.ordupdate.https_enabled + logger.debug('notify: ord_url: %s, https_enabled: %s, JSON: %s' % ( + ord_url, str(https_enabled), data_to_send,)) + + logger.info('Notifying ORD...') + if https_enabled: + if conf.ordupdate.cert_path == '': + extra_message = '(not using certificate)' + else: + extra_message = '' + + logger.debug('Certificate path: \'%s\' %s' % ( + conf.ordupdate.cert_path, extra_message, )) + + if not is_ord_url_https: + ord_url = 'https%s' % ord_url[4:] + logger.debug('switch to https, notifying ord_url: %s' % ( + ord_url)) + try: + # Added the header to support the older version of requests + headers = {'Content-Type': 'application/json'} + response = requests.post('%s/v1/ord/ord_notifier' % (ord_url,), + data=json.dumps(data_to_send), + headers=headers, + cert=conf.ordupdate.cert_path) + except requests.exceptions.SSLError: + logger.debug('Received an SSL error (is the certificate valid?)') + raise + else: + if is_ord_url_https: + ord_url = 'http%s' % ord_url[5:] + logger.debug('https not supported, notifying ord_url: %s' % ( + ord_url)) + headers = {'Content-Type': 'application/json'} + response = requests.post('%s/v1/ord/ord_notifier' % (ord_url,), + headers=headers, + data=json.dumps(data_to_send)) + + # Make sure the ORD sent an ACK + if response.status_code != ACK_CODE: + message = 'Did not receive an ACK from ORD %s, status code: %d' % ( + ord_url, response.status_code, ) + encoded_message = message.replace('\n', '_').replace('\r', '_') + if encoded_message != message: + encoded_message = encoded_message + "(encoded)" + logger.error(encoded_message) + raise NotifyNotAcknowledgedError(message) + + +def _update_audit(lcp_name, application_id, tracking_id, transaction_id, + transaction_type, resource_id, user_id=None, + external_id=None, event_details=None, status=None): + """Update the Audit repository with the action status.""" + timestamp = int(time.time() * 1000) + audit.audit(timestamp, application_id, tracking_id, transaction_id, + transaction_type, resource_id, conf.app.service_name, + user_id, external_id, event_details) + logger.info('LCP %s: %s (%s)' % (lcp_name, event_details, status, )) + + +def _update_resource_status(region, resource_id, status, transaction_id, + error_code, error_msg, resource_operation, + resource_type): + """Update the resource status db with the status.""" + if status == 'Success': + status = 'Submitted' + else: + status = 'Error' + + data_to_save = dict( + timestamp=int(time.time() * 1000), + region=region, + resource_id=resource_id, + status=status, + transaction_id=transaction_id, + error_code=error_code, + error_msg=error_msg, + resource_operation=resource_operation, + resource_type=resource_type, + ord_notifier_id="") + + regionResourceIdStatus.add_status(data_to_save) + + +def notify_ord(transaction_id, + tracking_id, + resource_type, + resource_template_version, + resource_name, + resource_id, + operation, + region_id, + application_id, + user_id, + external_id=None, + error=False): + """Notify ORD of the changes. + + This function should be called after a resource has changed in SoT + (created, modified or deleted). + + :param transaction_id: The transaction id under which the resource was + updated + :param tracking_id: The tracking ID of the whole operation + :param resource_type: The resource type ("customer" | "image" | "flavor") + :param resource_template_version: The version id of the change in git + :param resource_name: The updated resource name + :param resource_id: The updated resource ID + :param operation: Operation made on resource ("create" | "modify" | + "delete") + :param region_id: This is the LCP name (not ID!). + :param application_id: The running application ID (RDS, CMS, etc.) + :param user_id: The calling user ID + :param external_id: An external tracking ID (optional) + :param error: A boolean that says whether an error has occurred during the + upload operation + :return: + :raise: ConfigFileError - when the configuration file was not found, + OrdNotFoundError - when the ORD was not found, + requests.exceptions.ConnectionError when the POST request + cannot be sent, + NotifyNotAcknowledgedError - when the ORD did not respond to the + notification as expected + """ + logger.debug('Entered notify_ord with transaction_id: %s, ' + 'tracking_id: %s, resource_type: %s, ' + 'resource_template_version: %s, resource_name: %s, ' + 'resource_id: %s, operation: %s, region_id: %s, ' + 'application_id: %s, user_id: %s, external_id: %s, ' + 'error: %s' % (transaction_id, tracking_id, resource_type, + resource_template_version, resource_name, + resource_id, operation, region_id, + application_id, user_id, external_id, error,)) + + error_msg = '' + transaction_type = '%s %s' % (operation, resource_type, ) + try: + if error: + event_details = 'upload failed' + status = 'SoT_Error' + error_msg = 'Upload to SoT Git repository failed' + else: + # Discover the correct ORD + discover_url = '%s:%d' % (conf.ordupdate.discovery_url, + conf.ordupdate.discovery_port,) + ord_to_update = _find_correct_ord(discover_url, region_id) + + if ord_to_update is None: + message = 'ORD of LCP %s not found' % (region_id, ) + logger.error(message) + raise OrdNotFoundError(message) + + _notify(ord_to_update, + transaction_id, + resource_id, + resource_type, + resource_template_version, + resource_name, + operation, + region_id) + + # All OK + event_details = '%s notified' % (region_id, ) + status = 'Success' + except Exception: + event_details = '%s notification failed' % (region_id, ) + status = 'ORD_Error' + error_msg = 'Notification to ORD failed' + raise + finally: + # Update resource_status db with status + _update_resource_status(region_id, resource_id, status, transaction_id, + 0, error_msg, operation, resource_type) + + # Write a record to Audit repository. Note that I assigned the + # appropriate values to event_details and status in every flow, so + # these variables won't be referenced before assignment + _update_audit(region_id, application_id, tracking_id, transaction_id, + transaction_type, resource_id, user_id, external_id, + event_details, status) + logger.debug("Create Resource Requested to ORD: region=%s resource_id=%s status=%s" + % (region_id, resource_id, status)) diff --git a/orm/services/resource_distributor/rds/proxies/__init__.py b/orm/services/resource_distributor/rds/proxies/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/proxies/ims_proxy.py b/orm/services/resource_distributor/rds/proxies/ims_proxy.py new file mode 100755 index 00000000..a62dddb2 --- /dev/null +++ b/orm/services/resource_distributor/rds/proxies/ims_proxy.py @@ -0,0 +1,61 @@ +import requests +import json +import logging + +from pecan import conf + +from rds.utils import authentication as AuthService +from rds.services.base import ErrorMesage + + +logger = logging.getLogger(__name__) + + +headers = {'content-type': 'application/json'} + + +def _set_headers(): + try: + region, token_id = AuthService.get_token() + if token_id: + headers['X-Auth-Token'] = token_id + headers['X-Auth-Region'] = region + except: + logger.error("no token") + + +def send_image_metadata(meta_data, region, resource_id, action='post'): + logger.debug( + "IMS PROXY - send metadata to ims {} for region {}".format(meta_data, + region)) + data_to_send = { + "metadata": { + "checksum": meta_data['checksum'], + "virtual_size": meta_data['virtual_size'], + "size": meta_data['size'] + } + } + + _set_headers() + data_to_send_as_json = json.dumps(data_to_send) + logger.debug("sending the data to ims server post method ") + logger.debug("ims server {0} path = {1}".format(conf.ims.base_url, + conf.ims.metadata_path).format( + resource_id, region)) + + if action == 'post': + try: + response = requests.post( + conf.ims.base_url + (conf.ims.metadata_path).format(resource_id, region), + data=data_to_send_as_json, headers=headers, verify=conf.verify) + logger.debug("got response from ims {}".format(response)) + except requests.ConnectionError as exp: + logger.error(exp) + logger.exception(exp) + raise ErrorMesage("fail to connect to server {}".format(exp.message)) + + if response.status_code != 200: + raise ErrorMesage( + "Got error from rds server, code: {0} message: {1}".format( + response.status_code, response.content)) + return diff --git a/orm/services/resource_distributor/rds/proxies/rms_proxy.py b/orm/services/resource_distributor/rds/proxies/rms_proxy.py new file mode 100755 index 00000000..e237ab60 --- /dev/null +++ b/orm/services/resource_distributor/rds/proxies/rms_proxy.py @@ -0,0 +1,31 @@ +"""python module.""" + +import json +import logging +import requests + +from pecan import conf +from rds.services.base import ErrorMesage + + +logger = logging.getLogger(__name__) + + +headers = {'content-type': 'application/json'} + + +def get_regions(): + logger.debug("get list of regions from rms") + logger.debug("rms server {0} path = {1}".format(conf.rms.base_url, + conf.rms.all_regions_path)) + + response = requests.get(conf.rms.base_url + conf.rms.all_regions_path, + headers=headers, verify=conf.verify) + + if response.status_code != 200: + log_message = "not able to get regions {}".format(response) + log_message = log_message.replace('\n', '_').replace('\r', '_') + logger.error(log_message) + return + + return response.json() diff --git a/orm/services/resource_distributor/rds/resources/ord.crt b/orm/services/resource_distributor/rds/resources/ord.crt new file mode 100644 index 00000000..3efb7eb2 --- /dev/null +++ b/orm/services/resource_distributor/rds/resources/ord.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgIJAM1SFBUUEOLCMA0GCSqGSIb3DQEBCwUAMGcxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxIDAeBgNVBAMMF29yZC5sb2NhbC5hdHQuaW5mcmEubmV0 +MB4XDTE2MDUwMjE2MDQyMVoXDTE3MDUwMjE2MDQyMVowZzELMAkGA1UEBhMCQVUx +EzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMg +UHR5IEx0ZDEgMB4GA1UEAwwXb3JkLmxvY2FsLmF0dC5pbmZyYS5uZXQwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAGKa75pvFsWqbdnFif7a+85krdjgF +RZZ2lnRVCdjiwyAI5KEk8awr79vr+3/xOMmEiuMYBkS3EbjnEILlGme+MHepIysB +OHMcLmqVqOZwd/9kiNBViIBX4Ma05zYlCyMjwipgawuwUc1riJPMPTAXuXsjuDVH +0AZQHX22S56vx/y4PtoAW61qcbSaKxCiNPrYb9wom/YMacI4kkL5yA0w0J4UBbWw +95dY8x0bUWjqo+pDUGwaHzSNsy4gRdrV/uHRmNEdnw3rVsRhjSxKoL0QDlnmTfFS +g0qnEafVMZewcL/H9+5hbnsW1xC8mSj0aNGOwYi3WQPF6lfrpRAH73nhAgMBAAGj +UDBOMB0GA1UdDgQWBBR0F2IlR9nD9ahVa+bxhzoJbhAvxTAfBgNVHSMEGDAWgBR0 +F2IlR9nD9ahVa+bxhzoJbhAvxTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4IBAQChB/YXQLcSI91ClDgn66OKIBrUvOdCi1s7oxphT8URY6Wj1nEUa0I7g3Rj +kUzyBTrUgHn36+R7exo1Zw0LhVyEAphsI0bMSJPJTLwi6qXwHN025/ZAEpeCvMsi +aMB6GiOO+oKWlU3aUGme5+y/u4TIbWdxI3TQPZs+2sRGKZ1mKEPOzdF/25Ao2xXg +eEmg71CST9QhyS+eAfKaM3Tujd/2uyfzZdGlmYYrOjs7cWOx+AF9RcUaeXnheEWz +YTB5xL05CcQlUf/Ahj2bvfBcy/Lu8sASNz9ESJQOnupzcv3tYS/pn7g788Z5TL2P +anwmSyLi6hyiYKP6RE2Rj6kD/jOX +-----END CERTIFICATE----- diff --git a/orm/services/resource_distributor/rds/services/__init__.py b/orm/services/resource_distributor/rds/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/services/base.py b/orm/services/resource_distributor/rds/services/base.py new file mode 100644 index 00000000..199f60dc --- /dev/null +++ b/orm/services/resource_distributor/rds/services/base.py @@ -0,0 +1,22 @@ + +class Error(Exception): + pass + + +class InputError(Error): + def __init__(self, name, value): + self.name = name + self.value = value + + +class ErrorMesage(Error): + def __init__(self, message=None): + self.message = message + + +class ConflictValue(Error): + """ + block values if operation still in progress + """ + pass + diff --git a/orm/services/resource_distributor/rds/services/etc/audit.conf b/orm/services/resource_distributor/rds/services/etc/audit.conf new file mode 100644 index 00000000..a5168dd7 --- /dev/null +++ b/orm/services/resource_distributor/rds/services/etc/audit.conf @@ -0,0 +1,4 @@ +[DEFAULT] +audit_server_url=http://127.0.0.1:8776/v1/audit/transaction +num_of_send_retries = 3 +time_wait_between_retries = 1 diff --git a/orm/services/resource_distributor/rds/services/model/__init__.py b/orm/services/resource_distributor/rds/services/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/services/model/region_resource_id_status.py b/orm/services/resource_distributor/rds/services/model/region_resource_id_status.py new file mode 100755 index 00000000..d312db1a --- /dev/null +++ b/orm/services/resource_distributor/rds/services/model/region_resource_id_status.py @@ -0,0 +1,69 @@ + + +class ResourceMetaData(object): + def __init__(self, checksum, virtual_size, size): + self.size = size + self.virtual_size = virtual_size + self.checksum = checksum + + def as_dict(self): + return self.__dict__ + + +class Model(object): + def __init__(self, + timestamp, + region, + status, + transaction_id, + resource_id, + ord_notifier, + err_msg, + err_code, + operation, + resource_extra_metadata=None): + self.timestamp = timestamp + self.region = region + self.status = status + self.ord_transaction_id = transaction_id + self.resource_id = resource_id + self.ord_notifier_id = ord_notifier + self.error_msg = err_msg + self.error_code = err_code + self.operation = operation + + if resource_extra_metadata: + self.resource_extra_metadata = ResourceMetaData( + checksum=resource_extra_metadata[0].checksum, + virtual_size=resource_extra_metadata[0].virtual_size, + size=resource_extra_metadata[0].size + ) + else: + self.resource_extra_metadata = None + + def as_dict(self): + return self.__dict__ + + +class StatusModel(object): + def __init__(self, status): + self.regions = status + self.status = self._get_aggregated_status() + + def _get_aggregated_status(self): + is_pending = False + for region in self.regions: + if region.status == 'Error' and region.operation.strip() != 'delete': + # If a region had an error, the aggregated status is 'Error' + return 'Error' + elif region.status == 'Submitted': + # Just set the flag but don't return, because there might be + # an error in any of the next iterations + is_pending = True + + if is_pending: + return 'Pending' + else: + # If self.regions is empty, the result will still be 'Success' but the + # server returns 404 Not Found + return 'Success' diff --git a/orm/services/resource_distributor/rds/services/model/resource_input.py b/orm/services/resource_distributor/rds/services/model/resource_input.py new file mode 100644 index 00000000..a4d62929 --- /dev/null +++ b/orm/services/resource_distributor/rds/services/model/resource_input.py @@ -0,0 +1,13 @@ + +class ResourceData(object): + def __init__(self, resource_id, resource_type, + targets, operation="create", + transaction_id="", model="", + external_transaction_id=""): + self.resource_id = resource_id + self.targets = targets + self.resource_type = resource_type + self.operation = operation + self.transaction_id = transaction_id + self.model = model + self.external_transaction_id = external_transaction_id \ No newline at end of file diff --git a/orm/services/resource_distributor/rds/services/region_resource_id_status.py b/orm/services/resource_distributor/rds/services/region_resource_id_status.py new file mode 100755 index 00000000..d9238572 --- /dev/null +++ b/orm/services/resource_distributor/rds/services/region_resource_id_status.py @@ -0,0 +1,96 @@ +import logging +import sys +import time + +from rds.services.base import Error, InputError +from rds.storage import factory + +logger = logging.getLogger(__name__) +config = { + 'max_interval_time': { + }, + 'allowed_status_values': { + } +} + +num_of_seconds_in_minute = 60 +num_of_miliseconds_in_seconds = 1000 + + +def add_status(data): + logger.debug("add resource status timestamp [{}], region [{}], status [{}] " + ", transaction_id [{}] and resource_id [{}], ord_notifier_id [{}], " + "error message [{}], error code [{}] and " + "resource_extra_metadata [{}]".format(data['timestamp'], + data['region'], + data['status'], + data['transaction_id'], + data['resource_id'], + data['ord_notifier_id'], + data['error_msg'], + data['error_code'], + data.get('resource_extra_metadata', None))) + + try: + validate_status_value(data['status']) + validate_operation_type(data['resource_operation']) + validate_resource_type(data['resource_type']) + + conn = factory.get_region_resource_id_status_connection() + conn.add_update_status_record(data['timestamp'], data['region'], data['status'], + data['transaction_id'], data['resource_id'], + data['ord_notifier_id'], data['error_msg'], + data['error_code'], + data['resource_operation'], + data.get('resource_extra_metadata')) + # post_data_to_image(data) + except Error as e: + logger.exception("invalid inputs error") + raise + except: + logger.exception("Unexpected error: {}".format(sys.exc_info()[0])) + raise + + +def get_status_by_resource_id(resource_id): + logger.debug("get status by resource id %s " % resource_id) + conn = factory.get_region_resource_id_status_connection() + result = conn.get_records_by_resource_id(resource_id) + return result + + +def get_regions_by_status_resource_id(status, resource_id): + logger.debug("get regions by status %s for resource %s" % (status, resource_id)) + conn = factory.get_region_resource_id_status_connection() + result = conn.get_records_by_resource_id_and_status(resource_id, + status) + return result + + +def validate_resource_type(resource_type): + allowed_resource_type = config['allowed_resource_type'] + if resource_type not in allowed_resource_type: + logger.exception("status value is not valid: {}".format(resource_type)) + raise InputError("operation_type", resource_type) + + +def validate_operation_type(operation_type): + allowed_operation_type = config['allowed_operation_type'] + if operation_type not in allowed_operation_type: + logger.exception("status value is not valid: {}".format(operation_type)) + raise InputError("operation_type", operation_type) + + +def validate_status_value(status): + allowed_status_values = config['allowed_status_values'] + if status not in allowed_status_values: + logger.exception("status value is not valid: {}".format(status)) + raise InputError("status", status) + + +# def post_data_to_image(data): +# if data['resource_type'] == "image": +# logger.debug("send metadata {} to ims :- {} for region {}".format( +# data['resource_extra_metadata'], data['resource_id'], data['region'])) +# # ims_proxy.send_image_metadata(data['resource_extra_metadata'], data['resource_id'], data['region']) +# return \ No newline at end of file diff --git a/orm/services/resource_distributor/rds/services/resource.py b/orm/services/resource_distributor/rds/services/resource.py new file mode 100755 index 00000000..a7442447 --- /dev/null +++ b/orm/services/resource_distributor/rds/services/resource.py @@ -0,0 +1,183 @@ +"""create resource moudle.""" +import logging +import time + +from pecan import conf +from pecan import request +from rds.services import region_resource_id_status as regionResourceIdStatus +from rds.services import yaml_customer_builder +from rds.services import yaml_flavor_bulder +from rds.services import yaml_image_builder +from rds.services.base import ConflictValue +from rds.services.base import ErrorMesage +from rds.services.model.resource_input import ResourceData as InputData +from rds.sot import sot_factory +from rds.utils import uuid_utils +from rds.utils import utils + + +my_logger = logging.getLogger(__name__) + + +def _get_inputs_from_resource_type(jsondata, + resource_type, + external_transaction_id, + operation="create"): + if resource_type == "customer": + input_data = InputData(resource_id=jsondata['uuid'], + resource_type=resource_type, + operation=operation, + targets=jsondata['regions'], + model=jsondata, + external_transaction_id=external_transaction_id) + elif resource_type == "flavor" or resource_type == "image": + input_data = InputData(resource_id=jsondata['id'], + resource_type=resource_type, + operation=operation, + targets=jsondata['regions'], + model=jsondata, + external_transaction_id=external_transaction_id) + else: + raise ErrorMesage("no support for resource %s" % resource_type) + return input_data + + +def _region_valid(region): + if 'rms_status' in region and region[ + 'rms_status'] not in conf.allow_region_statuses: + return False + return True + + +def _create_or_update_resource_status(input_data, target, error_msg='', + status="Submitted"): + # check rms region status + if not _region_valid(target): + status = 'Error' + error_msg = "Not sent to ord as status equal to " + target['rms_status'] + + my_logger.debug("save status as %s" % status) + data_to_save = dict( + timestamp=int(time.time() * 1000), + region=target['name'], + resource_id=input_data.resource_id, + status=status, + transaction_id=input_data.transaction_id, + error_code='', + error_msg=error_msg, + resource_operation=target['action'], + resource_type=input_data.resource_type, + ord_notifier_id='') + regionResourceIdStatus.add_status(data_to_save) + my_logger.debug("status %s saved" % status) + + +def _set_all_statuses_to_error(input_data, message=None): + targets = input_data.targets + for target in targets: + _create_or_update_resource_status(input_data=input_data, target=target, + error_msg=message or 'system error', + status="Error") + + +def _create_data_to_sot(input_data): + """create data. + + : build yaml string + :param jsondata: full json in request body + :param resource_type: eg... Customer + :return: return list of dictionaries with yaml string + """ + jsondata = input_data.model + targetslist = [] + targets = input_data.targets + for target in targets: + # save start status to submitted for each region + _create_or_update_resource_status(input_data, target) + if not _region_valid(target): + continue + if target['action'] == "delete": + yamldata = "delete" + elif input_data.resource_type == "customer": + yamldata = yaml_customer_builder.yamlbuilder(jsondata, target) + elif input_data.resource_type == "flavor": + yamldata = yaml_flavor_bulder.yamlbuilder(jsondata, target) + elif input_data.resource_type == "image": + yamldata = yaml_image_builder.yamlbuilder(jsondata, target) + targetslist.append({"region_id": target['name'], + "resource_type": input_data.resource_type, + "resource_name": input_data.resource_id, + "template_data": yamldata, + "operation": target['action']}) + return targetslist + + +def _upload_to_sot(uuid, tranid, targetslist): + application_id = request.headers[ + 'X-RANGER-Client'] if 'X-RANGER-Client' in request.headers else \ + 'NA' + user_id = request.headers[ + 'X-RANGER-Requester'] if 'X-RANGER-Requester' in request.headers else \ + '' + sot = sot_factory.get_sot() + sot.save_resource_to_sot(uuid, + tranid, + targetslist, + application_id, + user_id) + + +def _check_resource_status(input_data): + resource_id = input_data.resource_id + status = conf.block_by_status + # check if any of the region creation in pending + regions_by_resource = \ + regionResourceIdStatus.get_regions_by_status_resource_id(status, + resource_id) + # if any not ready return 409 + if regions_by_resource is not None and regions_by_resource.regions: + raise ConflictValue([region.region for region in regions_by_resource.regions]) + + +def update_sot(input_data): + """create resource.""" + my_logger.debug("build yaml file for %s id: %s" % (input_data.resource_type, + input_data.resource_id)) + targetslist = _create_data_to_sot(input_data) + my_logger.debug("upload yaml to SoT") + _upload_to_sot(input_data.resource_id, + input_data.transaction_id, + targetslist) + + +def main(jsondata, external_transaction_id, resource_type, operation): + """main function handle resource operation.""" + my_logger.info("got %s for %s resource" % (operation, resource_type)) + try: + input_data = _get_inputs_from_resource_type( + jsondata=jsondata, + resource_type=resource_type, + operation=operation, + external_transaction_id=external_transaction_id + ) + my_logger.debug("iterate through the regions see if none in submitted") + _check_resource_status(input_data) + my_logger.debug("get uuid from uuid generator") + input_data.transaction_id = uuid_utils.get_random_uuid() + my_logger.debug("uuid ={}".format(input_data.transaction_id)) + # add regions status from rms (to check if it down) + input_data.targets = utils.add_rms_status_to_regions( + input_data.targets, input_data.resource_type) + update_sot(input_data) + except ConflictValue: + raise + except ErrorMesage as exp: + my_logger.error(exp.message) + my_logger.exception(exp) + raise + except Exception as e: + my_logger.exception(e) + _set_all_statuses_to_error(input_data) + my_logger.error("deleting fails ,Error : {}".format(str(e.message))) + raise ErrorMesage(str(e.message)) + return input_data.resource_id diff --git a/orm/services/resource_distributor/rds/services/yaml_customer_builder.py b/orm/services/resource_distributor/rds/services/yaml_customer_builder.py new file mode 100755 index 00000000..c4273cfb --- /dev/null +++ b/orm/services/resource_distributor/rds/services/yaml_customer_builder.py @@ -0,0 +1,171 @@ +"""yaml build build yaml from json input.""" +import logging +import re +import yaml + +from pecan import conf + +logger = logging.getLogger(__name__) + +def get_users_quotas(data, region): + """get default or own region. + + get users and quotas from default or actual region + :param data: + :param region: + :return: + """ + users = region['users'] + quotas = region['quotas'] + if not users: + users = data['default_region']['users'] + if not quotas: + quotas = data['default_region']['quotas'] + return users, quotas + + +def creat_final_yaml(title, description, resources, outputs): + """put all yaml strings together. + + :param title: ther version of yaml + :param description: file description + :param resources: body of the yaml file + :param outputs: the output of the yaml + :return: the full string of yaml file + """ + title_yaml = re.sub("'", "", yaml.dump(title, default_flow_style=False)) + description_yaml = yaml.dump(description, default_flow_style=False) + resourcesyaml = re.sub("''", '', yaml.dump(resources, + default_flow_style=False)) + resources_yaml = re.sub("'", '', resourcesyaml) + yamldata = title_yaml + yamldata = yamldata + "\n"+description_yaml + yamldata = yamldata + "\n"+resources_yaml + yamldata = yamldata + "\n"+yaml.dump(outputs) + return yamldata + + +def _create_metadata_yaml(alldata): + metadata ={} + metadata_items={} + for item in alldata['metadata']: + metadata_items.update(item) + metadata['tenant_metadata'] = {'type': 'OS::Keystone::Metadata\n', + 'properties': { + 'TENANT_ID': "{'get_resource': '%s'}" % + alldata['uuid'], + 'METADATA': { + 'metadata': metadata_items}}} + return metadata + + +def yamlbuilder(alldata, region): + logger.info("building customer yaml") + logger.debug("start building flavor yaml for region %s" % region['name']) + """build cstomer yaml. + + build yaml file from json + :param alldata: full json data + :param region: data per region + :return: the full string of yaml file + """ + outputs = {} + resources = {} + yaml_version = conf.yaml_configs.customer_yaml.yaml_version + yaml_type = conf.yaml_configs.customer_yaml.yaml_options.type + title = {'heat_template_version': yaml_version} + description = {'description': 'yaml file for region - %s' % region['name']} + jsondata = alldata + project_name = jsondata['name'] + project_description = jsondata['description'] + # TODO(amar): remove these lines when using objects instead of string json + status = {"0": False, "1": True}[str(jsondata['enabled'])] + outputs['outputs'] = {} + resources['resources'] = {} + resources['resources']["%s" % alldata['uuid']] =\ + {'type': 'OS::Keystone::Project2\n', + 'properties': {'name': "%s" % project_name, + 'project_id': alldata['uuid'], + 'description': project_description, + 'enabled': status}} + # create the output + outputs['outputs']["%s_id" % alldata['uuid']] =\ + {"value": {"get_resource": "%s" % alldata['uuid']}} + + users, quotas = get_users_quotas(alldata, region) + for user in users: + user_roles = [] + any_role = [] + for role in user['roles']: + role_format = "%s" + user_roles.append( + {"role": role_format % role, + 'project': "{'get_resource': '%s'}" % alldata['uuid']} + ) + # create the output for roles + # outputs['outputs']["%s_id" % role] =\ + # {"value": {"get_resource": "%s" % role}} + + # no support for group when type is ldap + if yaml_type != 'ldap': + # create one group for user + # not real group its only from heat to be able to delete the user + any_role = [ + {'project': "{'get_resource': '%s'}" % alldata['uuid'], + 'role': "{'get_resource': '%s'}" % role} + ] + group_name = "%s_%s_group" % (alldata['uuid'], user['id']) + resources['resources'][group_name] = \ + {'type': 'OS::Keystone::Group\n', + 'properties': {'name': "%s" % group_name, + 'domain': 'default', + 'description': 'dummy', + 'roles': any_role}} + + # remove groupe section when type is ldap + # create users :: added the hard coded groupe + user_group = ["{'get_resource': '%s'}" % group_name] + resources['resources'][user['id']] = \ + {'type': 'OS::Keystone::User\n', + 'properties': {'name': user['id'], + 'groups': user_group, + 'roles': user_roles}} + else: + resources['resources'][user['id']] = \ + {'type': 'OS::Keystone::UserRoleAssignment\n', + 'properties': {'user': user['id'], + 'roles': user_roles}} + + # create the output for users + outputs['outputs']["%s_id" % user['id']] = \ + {"value": {"get_resource": user['id']}} + + options = {"compute": ["nova_quota", "OS::Nova::Quota\n"], + "network": ["neutron_quota", "OS::Neutron::Quota\n"], + "storage": ["cinder_quota", "OS::Cinder::Quota\n"]} + + # create quotas if quotas + if conf.yaml_configs.customer_yaml.yaml_options.quotas: + quotas_keys = dict(conf.yaml_configs.customer_yaml.yaml_keys.quotas_keys) + for items in quotas: + for item in items: + + # these lines added to check if got excpected keys if not they will be replaced + for ite in items[item].keys()[:]: + if ite in quotas_keys: + items[item][quotas_keys[ite]] = items[item][ite] + del items[item][ite] + #------------------------------------ + + # adding tenant to each quota + items[item]['tenant'] = \ + "{'get_resource': %s}" % alldata['uuid'] + resources['resources'][options[item][0]] = \ + {"type": options[item][1], "properties": items[item]} + metadata = _create_metadata_yaml(alldata) + resources['resources'].update(metadata) + # putting all parts together for full yaml + yamldata = creat_final_yaml(title, description, resources, outputs) + logger.debug( + "done building customer yaml for region %s " % region['name']) + return yamldata diff --git a/orm/services/resource_distributor/rds/services/yaml_flavor_bulder.py b/orm/services/resource_distributor/rds/services/yaml_flavor_bulder.py new file mode 100755 index 00000000..847eca52 --- /dev/null +++ b/orm/services/resource_distributor/rds/services/yaml_flavor_bulder.py @@ -0,0 +1,78 @@ +"""flavor builder module.""" +import logging +import re + +import yaml +from pecan import conf + +my_logger = logging.getLogger(__name__) + + +def create_final_yaml(title, resources, description, outputs): + """connect yaml strings together.""" + title_yaml = re.sub("'", "", yaml.dump(title, default_flow_style=False)) + description_yaml = yaml.dump(description, default_flow_style=False) + resources_yaml = yaml.dump(resources) + outputs_yaml = yaml.dump(outputs) + yamldata = title_yaml + "\n" + description_yaml + yamldata = yamldata + "\n" + resources_yaml + "\n" + outputs_yaml + return yamldata + + +def yamlbuilder(alldata, region): + """build yaml.""" + my_logger.info("building flavor yaml") + my_logger.debug("start building flavor yaml for region %s" % region['name']) + resources = {} + extra_specs = {} + outputs = {} + tags = {} + options = {} + tenants = [] + flavor_type = 'nova_flavor' + rxtx_factor = conf.yaml_configs.flavor_yaml.yaml_args.rxtx_factor + if 'rxtx_factor' in alldata: + rxtx_factor = int(alldata['rxtx_factor']) + yaml_version = conf.yaml_configs.flavor_yaml.yaml_version + public = {'public': True, 'private': False}[alldata['visibility']] + title = {'heat_template_version': yaml_version} + description = {'description': 'yaml file for region - %s' % region['name']} + ram = int(alldata['ram']) + swap = int(alldata['swap']) + for key, value in alldata['extra_specs'].items(): + extra_specs[key] = value + # Handle tags + if 'tag' in alldata: + for key, value in alldata['tag'].items(): + extra_specs[key] = value + # Handle options + if 'options' in alldata: + for key, value in alldata['options'].items(): + extra_specs[key] = value + # Handle tenants + for tenant in alldata['tenants']: + tenants.append(tenant['tenant_id']) + + # Generate the output + resources['resources'] = {} + resources['resources'][flavor_type] = \ + {'type': 'OS::Nova::Flavor', + 'properties': {'disk': alldata['disk'], + 'ephemeral': alldata['ephemeral'], + 'extra_specs': extra_specs, + 'flavorid': alldata['id'], + 'is_public': public, + 'name': alldata['name'], + 'ram': ram, + 'rxtx_factor': rxtx_factor, + 'swap': swap, + 'tenants': tenants, + 'vcpus': alldata['vcpus']}} + # gen the output + outputs['outputs'] = {} + outputs['outputs']['%s_id' % flavor_type] =\ + {'value': {"get_resource": flavor_type}} + flavor_yaml = create_final_yaml(title, resources, description, outputs) + my_logger.debug( + "done!!! building flavor yaml for region %s " % region['name']) + return flavor_yaml diff --git a/orm/services/resource_distributor/rds/services/yaml_image_builder.py b/orm/services/resource_distributor/rds/services/yaml_image_builder.py new file mode 100755 index 00000000..37fcdaa8 --- /dev/null +++ b/orm/services/resource_distributor/rds/services/yaml_image_builder.py @@ -0,0 +1,56 @@ +import logging +import re + +import yaml +from pecan import conf + +my_logger = logging.getLogger(__name__) + + +def create_full_yaml(title, resources, description, outputs): + title_yaml = re.sub("'", "", yaml.dump(title, default_flow_style=False)) + description_yaml = yaml.dump(description, default_flow_style=False) + resources_yaml = re.sub("'", '', re.sub("''", '', yaml.dump(resources, default_flow_style=False))) + outputs_yaml = re.sub("'", '', re.sub("''", '', yaml.dump(outputs))) + full_yaml = title_yaml + "\n" + description_yaml + full_yaml = full_yaml + "\n" + resources_yaml + "\n" + outputs_yaml + return full_yaml + + +def _properties(alldata, region): + public = True if alldata['visibility'] == "public" else False + protected = {0: False, 1: True}[alldata['protected']] + tenants = [tenant['customer_id'] for tenant in alldata['customers']] + return dict( + name = alldata['name'], + container_format = alldata["container_format"], + min_ram = alldata['min_ram'], + disk_format = alldata['disk_format'], + min_disk = alldata['min_disk'], + protected = protected, + copy_from = alldata["url"], + owner = alldata["owner"], + is_public = public, + tenants = str(tenants) + ) + + +def _glanceimage(alldata, region): + return dict( + type = "OS::Glance::Image2", + properties = _properties(alldata, region) + ) + + +def yamlbuilder(alldata, region): + resources = {} + outputs = {} + image_type = "glance_image" + yaml_version = conf.yaml_configs.image_yaml.yaml_version + title = {'heat_template_version': yaml_version} + description = {'description': 'yaml file for region - %s' % region['name']} + resources['resources'] = {"glance_image": _glanceimage(alldata, region)} + outputs['outputs'] = { + '%s_id' % image_type: {"value": {"get_resource": "%s" % image_type}}} + full_yaml = create_full_yaml(title, resources, description, outputs) + return full_yaml diff --git a/orm/services/resource_distributor/rds/sot/__init__.py b/orm/services/resource_distributor/rds/sot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/sot/base_sot.py b/orm/services/resource_distributor/rds/sot/base_sot.py new file mode 100644 index 00000000..6a440cee --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/base_sot.py @@ -0,0 +1,18 @@ +""" SoT interface definition +""" + + +class BaseSoT(object): + + def save_resource_to_sot(self, + tracking_id, + transaction_id, + resource_list): + raise NotImplementedError("Please Implement this method") + + def validate_sot_state(self): + raise NotImplementedError("Please Implement this method") + + +class SoTError(Exception): + pass diff --git a/orm/services/resource_distributor/rds/sot/git_sot/__init__.py b/orm/services/resource_distributor/rds/sot/git_sot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/sot/git_sot/git_base.py b/orm/services/resource_distributor/rds/sot/git_sot/git_base.py new file mode 100644 index 00000000..17fd8ad7 --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/git_sot/git_base.py @@ -0,0 +1,35 @@ +import subprocess + +from pecan import conf + + +class BaseGit(object): + + def git_init(self): + raise NotImplementedError("Please Implement this method") + + def git_upload_changes(self): + raise NotImplementedError("Please Implement this method") + + def git_reset_changes(self): + raise NotImplementedError("Please Implement this method") + + def validate_git(self): + raise NotImplementedError("Please Implement this method") + + +class GitInitError(Exception): + pass + + +class GitUploadError(Exception): + pass + + +class GitResetError(Exception): + pass + + +class GitValidateError(Exception): + pass + diff --git a/orm/services/resource_distributor/rds/sot/git_sot/git_factory.py b/orm/services/resource_distributor/rds/sot/git_sot/git_factory.py new file mode 100644 index 00000000..542c66e8 --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/git_sot/git_factory.py @@ -0,0 +1,15 @@ +from git_gittle import GitGittle +from git_native import GitNative + + +def get_git_impl(git_type): + """Return the correct Git implementation according to git_type""" + git_impl = None + if git_type == 'gittle': + git_impl = GitGittle() + elif git_type == 'native': + git_impl = GitNative() + else: + raise RuntimeError("Invalid Git implementation!!") + + return git_impl diff --git a/orm/services/resource_distributor/rds/sot/git_sot/git_gittle.py b/orm/services/resource_distributor/rds/sot/git_sot/git_gittle.py new file mode 100755 index 00000000..c7c0a93f --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/git_sot/git_gittle.py @@ -0,0 +1,63 @@ +import logging + +from pecan import conf +from gittle import Gittle + +from git_base import BaseGit, GitInitError, GitUploadError + +logger = logging.getLogger(__name__) + + +class GitGittle(BaseGit): + + def __init__(self): + self.repo = None + + def git_init(self): + try: + # Init repository + logger.debug("Local repository path:{}, Git server url: {}".format(conf.git.local_repository_path, + conf.git.git_server_url)) + self.repo = Gittle(conf.git.local_repository_path, origin_uri=conf.git.git_server_url) + + logger.info("Pulling from git..") + # Update local working copy + self.repo.pull() + + logger.info("GitGittle - Git is up to date !") + + except Exception as exc: + logger.error("GitGittle - Failed to initialize Git. Reason: {}".format(exc.message)) + raise GitInitError(exc.message) + + def git_upload_changes(self): + commit_id = "" + try: + + logger.info("Commit changes in progress ..") + # Stage modified files + self.repo.stage(self.repo.pending_files) + + commit_message = conf.git.commit_message_format.format(self.repo.added_files) + # Commit the change + commit_id = self.repo.commit(conf.git.commit_user, + conf.git.commit_email, + commit_message) + + logger.info("Commit details: commit_user:{}, commit_email:{}, " + "commit_message:{}, commit_id:{}".format(conf.git.commit_user, + conf.git.commit_email, + commit_message, + commit_id)) + + # Push to repository + self.repo.push() + + except Exception as exc: + logger.error("GitGittle - Filed to upload file to git.") + raise GitUploadError("Failed to upload file to Git") + + return commit_id + + def validate_git(self): + pass diff --git a/orm/services/resource_distributor/rds/sot/git_sot/git_native.py b/orm/services/resource_distributor/rds/sot/git_sot/git_native.py new file mode 100644 index 00000000..587b869b --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/git_sot/git_native.py @@ -0,0 +1,271 @@ +"""Native (bash commands) Git module.""" +import logging +import subprocess, shlex +from threading import Timer + +import time +from pecan import conf + +from git_base import BaseGit +from git_base import GitUploadError, GitInitError, GitResetError +from git_base import GitValidateError + +logger = logging.getLogger(__name__) + + +class GitNative(BaseGit): + """The native Git implementation.""" + + def git_init(self): + """Initialize Git.""" + try: + logger.info("Local repository path:{}, " + "Git server url: {}, " + "Git command timeout: " + "{} seconds".format(conf.git.local_repository_path, + conf.git.git_server_url, + conf.git.git_cmd_timeout)) + + out, error = self._git_pull(conf.git.local_repository_path) + if self._is_conflict(out) or self._is_error(error): + logger.error("Git pull result:\nerror:" + " {}\nout: {}".format(error, out)) + self.git_reset_changes("Reset all changes due " + "to git pull conflict or error.") + + except GitResetError as exc: + msg = "Failed to initialize git repository. " \ + "Reason: {}".format(exc.message) + logger.error(msg) + raise GitInitError(msg) + + def git_upload_changes(self): + """Upload (commit and push) the changes to Git.""" + commit_id = "" + try: + logger.info("Upload changes in progress ..") + + self._git_add(conf.git.local_repository_path) + + commit_message = conf.git.commit_message_format.format("") + + logger.info("Committing with the following parameters: " + "user: {}, email: {}, message: {}". + format(conf.git.commit_user, + conf.git.commit_email, + commit_message)) + self._git_commit(conf.git.commit_user, + conf.git.commit_email, + commit_message, + conf.git.local_repository_path) + + commit_id, error = self._git_get_commit_id( + conf.git.local_repository_path) + logger.info("Commit id : {}".format(commit_id)) + + out, error = self._git_pull(conf.git.local_repository_path) + # This check is needed only for Pull before Push. + if self._is_error(error): + raise GitNativeError("Git pull error! [{}]".format(error)) + + # Push to repository + self._git_push(conf.git.local_repository_path) + + except GitNativeError as exc: + msg = "Failed to upload file to git. " \ + "Reason: {}".format(exc.message) + logger.error(msg) + raise GitUploadError(msg) + + return commit_id + + def validate_git(self): + logger.info("Git repository validation started...\n" + "Git commands timeout: {} " + "seconds".format(conf.git.git_cmd_timeout)) + try: + self._git_config(conf.git.local_repository_path) + + out, error = self._git_pull(conf.git.local_repository_path) + if self._is_conflict(out) or self._is_error(error): + logger.info("Git pull error, reset ...") + self._git_commit(conf.git.commit_user, + conf.git.commit_email, + "Git pull error found !!!", + conf.git.local_repository_path) + + self._git_reset(conf.git.local_repository_path) + logger.info("Git was reset to server's state.") + else: + self._git_add(conf.git.local_repository_path) + + self._git_commit(conf.git.commit_user, + conf.git.commit_email, + "Git validation commit", + conf.git.local_repository_path) + + out, error = self._git_pull(conf.git.local_repository_path) + # This check is needed only for pull before push. + if self._is_error(error): + raise GitNativeError("Git pull error! [{}]".format(error)) + + self._git_push(conf.git.local_repository_path) + + logger.info("Git repository state validation check done !") + + except GitNativeError as exc: + logger.error("Git state invalid. Reason: [{}]".format( + exc.message)) + raise GitValidateError("Git state invalid !") + + def git_reset_changes(self, msg): + logger.info("Reset local repository to Git server started.") + try: + self._git_commit(conf.git.commit_user, + conf.git.commit_email, + msg, + conf.git.local_repository_path) + + self._git_reset(conf.git.local_repository_path) + logger.info("Local repository is now up to date " + "with Git server.") + + except GitNativeError as exc: + msg = "Git reset changes failed. " \ + "Reason: {}".format(exc.message) + logger.error(msg) + raise GitResetError(msg) + + def _git_config(self, repo_dir): + logger.info("Set git configuration params started.") + cmds = ['git config --global user.name {}'.format(conf.git.commit_user), + 'git config --global user.email {}'.format(conf.git.commit_email)] + + for cmd in cmds: + self._execute_git_cmd(cmd, repo_dir) + logger.info("Set git configuration params done.") + + def _git_add(self, repo_dir): + logger.info("Git add started.") + cmd = 'git add --all' + out, error = self._execute_git_cmd(cmd, repo_dir) + logger.info("Git add done.") + return out, error + + def _git_commit(self, user, email, commit_message, repo_dir): + logger.info("Git commit started.") + cmd = 'git commit --author="%s <%s>" -am "%s"' % (user, + email, + commit_message) + out, error = self._execute_git_cmd(cmd, repo_dir) + logger.info("Git commit done.") + return out, error + + def _git_get_commit_id(self, repo_dir): + logger.info("Git get commit id started.") + cmd = 'git rev-parse HEAD' + out, error = self._execute_git_cmd(cmd, repo_dir) + # we need to clean \n and whitespaces before returning the commit id + out = out.strip().split('\n')[0] + logger.info("Git get commit id done. commit id: {}".format(out)) + return out, error + + def _git_reset(self, repo_dir): + logger.info("Git reset started.") + cmd = 'git reset --hard origin/master ' + out, error = self._execute_git_cmd(cmd, repo_dir) + logger.info("Git reset done.") + return out, error + + def _git_push(self, repo_dir): + logger.info("Git push started.") + cmd = 'git push ' + start_time = time.time() + out, error = self._execute_git_cmd(cmd, repo_dir) + logger.info("Git push done " + "(%.3f seconds)" % (time.time() - start_time)) + return out, error + + def _execute_git_cmd(self, cmd, repo_dir): + error = "" + proc = subprocess.Popen(shlex.split(cmd), cwd=repo_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + timeout = conf.git.git_cmd_timeout + timer = Timer(timeout, on_subprocess_timeout, [cmd, proc]) + try: + timer.start() + (out, error) = proc.communicate() + logger.debug("Cmd proc id: {}".format(proc.pid)) + proc.wait() + finally: + if not timer.is_alive(): + msg = "Git command '{}' timed out !!".format(cmd) + logger.error(msg) + # the word error must be in message + error = "error:" + msg + timer.cancel() + + if self._is_error(error): + raise GitNativeError("Git error! [{}]".format(error)) + return out, error + + def _git_pull(self, repo_dir): + logger.info("Git pull started.") + cmd = 'git pull ' + error = "" + start_time = time.time() + proc = subprocess.Popen(shlex.split(cmd), cwd=repo_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + timeout = conf.git.git_cmd_timeout + timer = Timer(timeout, on_subprocess_timeout, [cmd, proc]) + try: + timer.start() + (out, error) = proc.communicate() + logger.debug("Cmd proc id: {}".format(proc.pid)) + proc.wait() + finally: + if not timer.is_alive(): + msg = "Git command '{}' timed out !!".format(cmd) + logger.error(msg) + # the word error must be in message + error = "error:" + msg + timer.cancel() + + # Special case for pull caller method will check the output + if not self._is_error(error): + logger.info("Git pull done " + "(%.3f seconds)" % (time.time() - start_time)) + + return out, error + + def _is_error(self, error): + if error: + l_error = error.lower() + if 'error' in l_error or 'fatal' in l_error: + logger.error("Git operation returned with " + "error:\n{}".format(error)) + return True + return False + + def _is_conflict(self, out): + if out: + l_out = out.lower() + if 'conflict' in l_out: + logger.info("Git operation returned with " + "conflict:\n{}".format(out)) + return True + return False + + +def on_subprocess_timeout(cmd, proc): + logger.error("Subprocess for command : {}, timed out!".format(cmd)) + logger.info("Terminating subprocess id: {}".format(proc.pid)) + proc.kill() + + +class GitNativeError(Exception): + """Describes a generic error in a Git operation.""" + + pass diff --git a/orm/services/resource_distributor/rds/sot/git_sot/git_sot.py b/orm/services/resource_distributor/rds/sot/git_sot/git_sot.py new file mode 100755 index 00000000..ad5b9882 --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/git_sot/git_sot.py @@ -0,0 +1,233 @@ +import logging +import os +import threading + +from rds.ordupdate.ord_notifier import notify_ord +from rds.sot import base_sot +from rds.sot.base_sot import SoTError + +import git_factory +from git_base import GitUploadError, GitInitError, GitResetError +from git_base import GitValidateError + +logger = logging.getLogger(__name__) +lock = threading.Lock() + + +class GitSoT(base_sot.BaseSoT): + + local_repository_path = "" + relative_path_format = "" + file_name_format = "" + commit_message_format = "" + commit_user = "" + commit_email = "" + git_server_url = "" + git_type = "" + + def __init__(self): + logger.debug("In Git based SoT") + self.git_impl = git_factory.get_git_impl(GitSoT.git_type) + + def save_resource_to_sot(self, tracking_id, transaction_id, + resource_list, application_id, user_id): + thread = threading.Thread(target=update_sot, + args=(self.git_impl, + lock, + tracking_id, + transaction_id, + resource_list, + application_id, + user_id)) + thread.start() + + def validate_sot_state(self): + thread = threading.Thread(target=validate_git, + args=(self.git_impl, lock)) + + thread.start() + + +def update_sot(git_impl, my_lock, tracking_id, transaction_id, resource_list, + application_id, user_id): + logger.info("Save resource to SoT. start ...") + commit_id = "" + result = False + logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + logger.info("Acquire Git lock...") + # Lock the entire git operations, so that no other threads change local + # files. + my_lock.acquire() + logger.info("Git lock acquired !!!!") + try: + init_git(git_impl) + + handle_file_operations(resource_list) + + commit_id = update_git(git_impl) + + logger.info("All files were successfully updated in Git server :-)\n") + + result = True + + except SoTError as exc: + logger.error("Save resource to SoT Git repository failed. " + "Reason: {}.". + format(exc.message)) + except GitInitError as init_exc: + logger.error("Initializing Git repository Failed. Reason: {}.". + format(init_exc.message)) + except GitUploadError as upload_exc: + logger.error("Uploading to Git repository Failed. Reason: {}.". + format(upload_exc.message)) + cleanup(git_impl) + finally: + logger.info("Release Git lock...") + my_lock.release() + logger.info("Git lock released !!!!") + logger.info("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + + # This method is called also in case exception raised. + # Notification to ords will not be sent but status db and audit + # will be updated. + for resource in resource_list: + try: + notify_ord(transaction_id, + tracking_id, + resource["resource_type"], + commit_id, # This is the resource-template-version + GitSoT.file_name_format.format( + resource["resource_name"]), + resource["resource_name"], # This is the resource_id + resource["operation"], + resource["region_id"], + application_id, # application_id is not available + user_id, # user_id is not available + "NA", # external_id is not available + not result) + except Exception as e: + logger.error("Error in updating ORD! Error: {}".format( + e.message + )) + + +def handle_file_operations(resource_list): + for resource in resource_list: + file_path = get_resource_file_path(resource) + operation = resource["operation"] + logger.debug("Operation: {}".format(operation)) + if operation == "delete": + logger.info("Deleting file: {}".format(file_path)) + if os.path.exists(file_path): + try: + os.remove(file_path) + logger.info("File successfully deleted!") + except OSError as ex: + msg = "Could not delete file. " \ + "Reason: {}".format(ex.message) + logger.error(msg) + raise SoTError(msg) + else: + logger.info("File does not exist, nothing to delete..") + + else: # for all other operations "modify", "create" + logger.info("Adding file: {}".format(file_path)) + create_file_in_path(file_path, resource["template_data"]) + logger.info("File was successfully added!") + + +def get_resource_file_path(resource): + file_name = GitSoT.file_name_format.format(resource["resource_name"]) + relative_path = GitSoT.relative_path_format. \ + format(resource["region_id"], + resource["resource_type"], + file_name) + file_path = GitSoT.local_repository_path + relative_path + return file_path + + +def create_file_in_path(file_path, file_data): + logger.info("Creating file : {}".format(file_path)) + + create_dir(file_path) + logger.debug("Directory path created..") + + write_data_to_file(file_path, file_data) + logger.info("Data written to file.") + + +def create_dir(file_path): + # Create actual directory path if not exist + f_path = os.path.dirname(file_path) + if not os.path.exists(f_path): + try: + os.makedirs(f_path) + except OSError as ex: + msg = "Failed to create directory path. " \ + "Reason: {}".format(ex.message) + logger.error(msg) + raise SoTError(msg) + + +def write_data_to_file(file_path, file_data): + # Create and write data to file (If file exists it is overwritten) + try: + with open(file_path, 'w') as fo: + fo.write(file_data) + except IOError as ex: + msg = "Could not write data to file. " \ + "Reason: {}".format(ex.message) + logger.error(msg) + raise SoTError(msg) + else: + fo.close() + + +def init_git(git_impl): + try: + git_impl.git_init() + except GitInitError as exc: + logger.error("Failed to initialize Git. " + "Reason: {}".format(exc.message)) + raise + + +def update_git(git_impl): + commit_id = "" + try: + commit_id = git_impl.git_upload_changes() + except GitUploadError as exc: + logger.error(exc.message) + raise + return commit_id + + +def validate_git(git_impl, my_lock): + logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>") + logger.info("Acquire Git lock...") + my_lock.acquire() + logger.info("Git lock acquired !!!!") + try: + git_impl.validate_git() + except GitValidateError as exc: + logger.error("Git validation error. Reason: {}.". + format(exc.message)) + finally: + logger.info("Release Git lock...") + my_lock.release() + logger.info("Git lock released !!!!") + logger.info("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + + +def cleanup(git_impl): + logger.info("Cleanup started...") + try: + git_impl.git_reset_changes("Clean up changes due to upload error.") + except GitResetError as exc: + logger.error(exc.message) + raise SoTError(exc.message) + + + + + diff --git a/orm/services/resource_distributor/rds/sot/sot_factory.py b/orm/services/resource_distributor/rds/sot/sot_factory.py new file mode 100644 index 00000000..1c2fea4c --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/sot_factory.py @@ -0,0 +1,29 @@ +from rds.sot.git_sot import git_sot + +sot_type = "" +local_repository_path = "" +relative_path_format = "" +file_name_format = "" +commit_message_format = "" +commit_user = "" +commit_email = "" +git_server_url = "" +git_type = "" + + +def get_sot(): + """Return the correct SoT implementation according to sot_type""" + + if sot_type == 'git': + git_sot.GitSoT.local_repository_path = local_repository_path + git_sot.GitSoT.relative_path_format = relative_path_format + git_sot.GitSoT.file_name_format = file_name_format + git_sot.GitSoT.commit_message_format = commit_message_format + git_sot.GitSoT.commit_user = commit_user + git_sot.GitSoT.commit_email = commit_email + git_sot.GitSoT.git_server_url = git_server_url + git_sot.GitSoT.git_type = git_type + sot = git_sot.GitSoT() + return sot + else: + raise RuntimeError("Invalid SoT implementation!!") diff --git a/orm/services/resource_distributor/rds/sot/sot_utils.py b/orm/services/resource_distributor/rds/sot/sot_utils.py new file mode 100644 index 00000000..758e3bc7 --- /dev/null +++ b/orm/services/resource_distributor/rds/sot/sot_utils.py @@ -0,0 +1,43 @@ +import yaml + + +def merge_yamls(document, section): + document_dict = yaml.load(document) + section_dict = yaml.load(section) + merge_dict(section_dict, document_dict) + new_document = yaml.dump(document_dict) + return new_document + + +# source is being merged into destiantion +def merge_dict(source, destination): + for key, value in source.items(): + if isinstance(value, dict): + # get node or create one + node = destination.setdefault(key, {}) + merge_dict(value, node) + else: + destination[key] = value + + return destination + +document = """ + a: 1 + b: + c: 3 + d: 4 + f: + h: h1 +""" + +section = """ + b: + d: 6 + e: 5 + f: + g: g1 + h: + h1: h2 +""" + +print(merge_yamls(document, section)) diff --git a/orm/services/resource_distributor/rds/storage/__init__.py b/orm/services/resource_distributor/rds/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/storage/factory.py b/orm/services/resource_distributor/rds/storage/factory.py new file mode 100644 index 00000000..a959c7fb --- /dev/null +++ b/orm/services/resource_distributor/rds/storage/factory.py @@ -0,0 +1,10 @@ +from rds.storage.mysql.region_resource_id_status import Connection as RegionResourceIdStatusConnection + +database = { + 'url' : 'na' +} + + +def get_region_resource_id_status_connection(): + return RegionResourceIdStatusConnection(database['url']) + diff --git a/orm/services/resource_distributor/rds/storage/mysql/__init__.py b/orm/services/resource_distributor/rds/storage/mysql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/storage/mysql/region_resource_id_status.py b/orm/services/resource_distributor/rds/storage/mysql/region_resource_id_status.py new file mode 100755 index 00000000..6fb07c06 --- /dev/null +++ b/orm/services/resource_distributor/rds/storage/mysql/region_resource_id_status.py @@ -0,0 +1,210 @@ +import time + +from oslo_db.sqlalchemy import session as db_session +from sqlalchemy import Column, Integer, Text, BigInteger, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative.api import declarative_base + +from rds.services.model.region_resource_id_status import Model, StatusModel +from rds.storage import region_resource_id_status +import logging +import oslo_db + +from pecan import conf + +Base = declarative_base() +logger = logging.getLogger(__name__) + + +class ResourceStatusRecord(Base): + __tablename__ = 'resource_status' + + id = Column(Integer, autoincrement=True, primary_key=True) + timestamp = Column(BigInteger, primary_key=False) + region = Column(Text, primary_key=False) + status = Column(Text, primary_key=False) + transaction_id = Column(Text, primary_key=False) + resource_id = Column(Text, primary_key=False) + ord_notifier = Column(Text, primary_key=False) + err_code = Column(Text, primary_key=False) + err_msg = Column(Text, primary_key=False) + operation = Column(Text, primary_key=False) + resource_extra_metadata = relationship("ImageMetadData", + cascade="all, delete, delete-orphan") + +class ImageMetadData(Base): + __tablename__ = 'image_metadata' + + image_meta_data_id = Column(ForeignKey(u'resource_status.id'), + primary_key=True) + checksum = Column(Text, primary_key=False) + virtual_size = Column(Text, primary_key=False) + size = Column(Text, primary_key=False) + + +class Connection(region_resource_id_status.Base): + """ Implements mysql DB """ + + def __init__(self, url): + self._engine_facade = db_session.EngineFacade(url) + + def add_update_status_record(self, + timestamp, + region, + status, + transaction_id, + resource_id, + ord_notifier, + err_msg, + err_code, + operation, + resource_extra_metadata=None): + logger.debug("Add/Update status record:\ntimestamp [{}]\nregion [{}]" + "\nstatus [{}]\ntransaction_id [{}]\nresource_id [{}]\n" + "ord_notifier [{}]\nerr_code [{}]\n" + "err_msg [{}] operation [{}] resource_extra_metadata" + " [{}]".format(timestamp, + region, + status, + transaction_id, + resource_id, + ord_notifier, + err_code, + err_msg, + operation, + resource_extra_metadata)) + try: + session = self._engine_facade.get_session() + with session.begin(): + image_metadata = None + record = session.query(ResourceStatusRecord).\ + filter_by(resource_id=resource_id, region=region).first() + if resource_extra_metadata: + image_metadata = ImageMetadData( + checksum=resource_extra_metadata['checksum'], + virtual_size=resource_extra_metadata['virtual_size'], + size=resource_extra_metadata['size']) + + if record is not None: + logger.debug("Update record") + record.timestamp = timestamp + record.region = region + record.status = status + record.transaction_id = transaction_id + record.resource_id = resource_id + record.ord_notifier = ord_notifier + record.err_msg = err_msg + record.err_code = err_code + record.operation = operation + if record.resource_extra_metadata and image_metadata: + record.resource_extra_metadata[0] = image_metadata + elif image_metadata: + record.resource_extra_metadata.append(image_metadata) + else: + # remove child if not given + session.query(ImageMetadData).filter_by( + image_meta_data_id=record.id).delete() + else: + logger.debug("Add record") + resource_status = ResourceStatusRecord(timestamp=timestamp, + region=region, + status=status, + transaction_id=transaction_id, + resource_id=resource_id, + ord_notifier=ord_notifier, + err_msg=err_msg, + err_code=err_code, + operation=operation) + if resource_extra_metadata: + resource_status.resource_extra_metadata.append(image_metadata) + + session.add(resource_status) + + except oslo_db.exception.DBDuplicateEntry as e: + logger.warning("Duplicate entry: {}".format(str(e))) + + def get_records_by_resource_id(self, resource_id): + return self.get_records_by_filter_args(resource_id=resource_id) + + def get_records_by_filter_args(self, **filter_args): + logger.debug("Get records filtered by [{}]".format(filter_args)) + (timestamp, ref_timestamp) = self.get_timstamp_pair() + logger.debug("timestamp=%s, ref_timestamp=%s" % (timestamp, ref_timestamp)) + records_model = [] + session = self._engine_facade.get_session() + with session.begin(): + records = session.query(ResourceStatusRecord).filter_by(**filter_args) + # if found records return these records + if records is not None: + for record in records: + if record.status == "Submitted" and record.timestamp < ref_timestamp: + record.timestamp = timestamp + record.status = "Error" + record.err_msg = "Status updated to 'Error'. Too long 'Submitted' status" + + status = Model(record.timestamp, + record.region, + record.status, + record.transaction_id, + record.resource_id, + record.ord_notifier, + record.err_msg, + record.err_code, + record.operation, + record.resource_extra_metadata) + records_model.append(status) + return StatusModel(records_model) + else: + logger.debug("No records found") + return None + + def get_records_by_resource_id_and_status(self, + resource_id, + status): + """ This method filters all the records where resource_id is the given + resource_id and status is the given status. + for the matching records check if a time period elapsed and if so, + change the status to 'Error' and the timestamp to the given timestamp.""" + logger.debug("Get records filtered by resource_id={} " + "and status={}".format(resource_id, + status)) + (timestamp, ref_timestamp) = self.get_timstamp_pair() + logger.debug("timestamp=%s, ref_timestamp=%s" % (timestamp, ref_timestamp)) + session = self._engine_facade.get_session() + records_model = [] + with session.begin(): + records = session.query(ResourceStatusRecord).\ + filter_by(resource_id=resource_id, + status=status) + if records is not None: + for record in records: + if record.status == "Submitted" and record.timestamp < ref_timestamp: + record.timestamp = timestamp + record.status = "Error" + record.err_msg = "Status updated to 'Error'. Too long 'Submitted' status" + else: + status = Model(record.timestamp, + record.region, + record.status, + record.transaction_id, + record.resource_id, + record.ord_notifier, + record.err_msg, + record.err_code, + record.operation, + record.resource_extra_metadata) + records_model.append(status) + if len(records_model): + return StatusModel(records_model) + else: + logger.debug("No records found") + return None + + def get_timstamp_pair(self): + timestamp = int(time.time())*1000 + # assume same time period for all resource types + max_interval_time_in_seconds = conf.region_resource_id_status.max_interval_time.default * 60 + ref_timestamp = (int(time.time()) - max_interval_time_in_seconds) * 1000 + return timestamp, ref_timestamp + + diff --git a/orm/services/resource_distributor/rds/storage/region_resource_id_status.py b/orm/services/resource_distributor/rds/storage/region_resource_id_status.py new file mode 100644 index 00000000..43856d0d --- /dev/null +++ b/orm/services/resource_distributor/rds/storage/region_resource_id_status.py @@ -0,0 +1,24 @@ +""" Storage base backend +""" + + +class Base(object): + def __init__(self, url): + pass + + def add_update_status_record(self, + timestamp, + region, + status, + transaction_id, + resource_id, + ord_notifier, + err_msg, + err_code): + raise NotImplementedError("Please Implement this method") + + def get_records_by_resource_id(self, resource_id): + raise NotImplementedError("Please Implement this method") + + def get_records_by_filter_args(self, **filter_args): + raise NotImplementedError("Please Implement this method") \ No newline at end of file diff --git a/orm/services/resource_distributor/rds/tests/__init__.py b/orm/services/resource_distributor/rds/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/base.py b/orm/services/resource_distributor/rds/tests/base.py new file mode 100644 index 00000000..23bd24c6 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/base.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# 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. + +from oslotest import base + + +class TestCase(base.BaseTestCase): + + """Test case base class for all unit tests.""" diff --git a/orm/services/resource_distributor/rds/tests/config.py b/orm/services/resource_distributor/rds/tests/config.py new file mode 100755 index 00000000..b7314bfd --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/config.py @@ -0,0 +1,170 @@ +# Pecan Application configurations +app = { + 'root': 'rds.controllers.root.RootController', + 'modules': ['rds'], + 'service_name': 'RDS' +} + +server = { + 'port': '8777', + 'host': '0.0.0.0' +} + +# DB configurations +database = { + 'url': 'mysql://root:stack@127.0.0.1/orm_rds?charset=utf8' +} + +sot = { + 'type': 'git', +} + +git = { + # possible values : 'native', 'gittle' + 'type': 'gittle', + 'local_repository_path': '/home/orm/SoT/ORM', + 'file_name_format': 's_{}.yml', + 'relative_path_format': '/Document_Store/LCP/{}/{}/{}', + 'commit_message_format': 'File was added to repository: {}', + 'commit_user': 'orm_rds', + 'commit_email': 'orm_rds@att.com', + 'git_server_url': 'orm_rds@172.20.90.218:~/SoT/ORM.git' + +} + +audit = { + 'audit_server_url': 'http://127.0.0.1:8776/v1/audit/transaction', + 'num_of_send_retries': 3, + 'time_wait_between_retries': 1 +} + +authentication = { + 'enabled': False, + 'mech_id': 'admin', + 'mech_pass': 'stack', + 'rms_url': 'http://172.20.90.174:8080', + 'tenant_name': 'admin' +} + +ordupdate = { + 'discovery_url': '127.0.0.1', + 'discovery_port': '8080', + 'template_type': 'hot' +} + +verify = False + +UUID_URL = 'http://172.20.90.232:8090/v1/uuids' + +yaml_configs = { + 'customer_yaml': { + 'yaml_version': '2014-10-16', + 'yaml_options': { + 'quotas': True, + 'type': 'ldap' + }, + 'yaml_keys': { + 'quotas_keys': { + 'keypairs': 'key_pairs', + 'network': 'networks', + 'port': 'ports', + 'router': 'routers', + 'subnet': 'subnets', + 'floatingip': 'floating_ips' + } + } + }, + 'flavor_yaml':{ + 'yaml_version': '2013-05-23', + 'yaml_args': { + 'rxtx_factor': 1 + } + }, + 'image_yaml': { + 'yaml_version': '2014-10-16' + } +} + +# yaml configuration for create flavor +yaml_flavor_version='2014-10-16' + +# value of status to be blocked before creating any resource +block_by_status = "Submitted" + +# this tells which values to allow resource submit the region +allow_region_statuses = ['functional'] + +keystone_role_list = { + 'member': '68cddd1a64eb4eae9c5d82581bc55426', + 'reselleradmin': '2f358be4320a401cb7517c5938d93003', + 'wwiftoperator': '852113b8aeba420eb6176f896e85d1fb', + '_member_': '6b29638c65de4df09b4d3ee0bee3ca39', + 'admin': '084103f31503413a93d4e3b3383ca954' +} + +# region_resource_id_status configurations +region_resource_id_status = { + # interval_time_validation in minutes + 'max_interval_time': { + 'images': 60, + 'tenants': 60, + 'flavors': 60, + 'users': 60, + 'default': 60 + }, + 'allowed_status_values': { + 'Success', + 'Error', + 'Submitted' + }, + 'allowed_operation_type': + { + 'create', + 'modify', + 'delete' + }, + 'allowed_resource_type': + { + 'customer', + 'image', + 'flavor' + } +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'rds': {'level': 'DEBUG', 'handlers': ['console', 'Logfile'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'CRITICAL', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + }, + 'Logfile': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'maxBytes': 50000000, + 'backupCount': 10, + 'filename': '/tmp/rds.log', + 'formatter': 'simple' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format':'%(asctime)s [%(padded_color_levelname)s] [%(name)s] [%(threadName)s] %(message)s', + '__force_dict__': True + } + } +} + + diff --git a/orm/services/resource_distributor/rds/tests/controllers/__init__.py b/orm/services/resource_distributor/rds/tests/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/__init__.py b/orm/services/resource_distributor/rds/tests/controllers/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/configuration/__init__.py b/orm/services/resource_distributor/rds/tests/controllers/v1/configuration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/configuration/test_get_configuration.py b/orm/services/resource_distributor/rds/tests/controllers/v1/configuration/test_get_configuration.py new file mode 100755 index 00000000..53a12452 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/controllers/v1/configuration/test_get_configuration.py @@ -0,0 +1,21 @@ +"""Get configuration module unittests.""" +from rds.tests.controllers.v1.functional_test import FunctionalTest +from rds.controllers.v1.configuration import root +from mock import patch + + +class TestGetConfiguration(FunctionalTest): + """Main get configuration test case.""" + @patch.object(root, 'utils') + def test_get_configuration_success(self, mock_utils): + """test get config success.""" + mock_utils.set_utils_conf.return_value = True + mock_utils.report_config.return_value = "1234" + response = self.app.get('/v1/rds/configuration') + self.assertEqual(response.json, '1234') + + # @patch.object(root.utils, 'report_config', return_value='12345') + # def test_get_configuration_success(self, input): + # """Test get_configuration returns the expected value on success.""" + # response = self.app.get('/v1/rds/configuration') + # self.assertEqual(response.json, '12345') diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/functional_test.py b/orm/services/resource_distributor/rds/tests/controllers/v1/functional_test.py new file mode 100644 index 00000000..5b2b64dc --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/controllers/v1/functional_test.py @@ -0,0 +1,5 @@ +from rds.tests.functional_test import FunctionalTest + + +class FunctionalTest(FunctionalTest): + PATH_PREFIX = '/v1' diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/resources/__init__.py b/orm/services/resource_distributor/rds/tests/controllers/v1/resources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/resources/test_create_resource.py b/orm/services/resource_distributor/rds/tests/controllers/v1/resources/test_create_resource.py new file mode 100755 index 00000000..bb1a173b --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/controllers/v1/resources/test_create_resource.py @@ -0,0 +1,242 @@ +"""unittest get resource.""" +from mock import patch + +import rds.controllers.v1.resources.root as root +from rds.tests.controllers.v1.functional_test import FunctionalTest + + +class TestCreateResource(FunctionalTest): + """tests for only for api handler.""" + + @patch.object(root.ResourceService, 'main', return_value="12345") + def test_create_resource_success(self, input): + """test create resource as it succeed.""" + response = self.app.post_json('/v1/rds/resources', good_data) + assert response.json['customer']['id'] == '12345' + assert response.status_int == 201 + + @patch.object(root.ResourceService, 'main', return_value="12345") + def test_create_resource_success_flavor(self, input): + """test create flavor as it succeed.""" + response = self.app.post_json('/v1/rds/resources', flavor_data) + assert response.json['flavor']['id'] == '12345' + assert response.status_int == 201 + + @patch.object(root.ResourceService, 'main', return_value="12345") + def test_create_resource_success_image(self, input): + """test create flavor as it succeed.""" + response = self.app.post_json('/v1/rds/resources', image_data) + assert response.json['image']['id'] == '12345' + assert response.status_int == 201 + + @patch.object(root.ResourceService, 'main', + side_effect=Exception("general exception")) + def test_create_resource_gen_except(self, input): + """test creatte resource to catch general exception.""" + response = self.app.post_json('/v1/rds/resources', + good_data, expect_errors=True) + assert response.status_int == 400 + + @patch.object(root.ResourceService, 'main', + side_effect=root.ConflictValue("region")) + def test_create_resource_conflict_except(self, input): + """test creatte resource to catch ConflictValue exception.""" + response = self.app.post_json('/v1/rds/resources', + good_data, expect_errors=True) + assert response.status_int == 409 + + @patch.object(root.ResourceService, 'main', return_value="12345") + def test_delete_resource_flavor(self, input): + """test delete flavor.""" + response = self.app.delete_json('/v1/rds/resources', flavor_data) + assert response.status_int == 200 + + @patch.object(root.ResourceService, 'main', return_value="12345") + def test_delete_resource_any(self, input): + """test delete resource not flavor.""" + flavor_data["service_template"]["resource"]['resource_type'] = \ + "customer" + response = self.app.delete_json('/v1/rds/resources', flavor_data, + expect_errors=True) + assert response.status_int == 405 + flavor_data["service_template"]["resource"]['resource_type'] = "flavor" + + @patch.object(root.ResourceService, 'main', + side_effect=root.ConflictValue("region")) + def test_delete_resource_flavor_con(self, input): + """test delete flavor while previous proccess still in progress.""" + try: + response = self.app.delete_json('/v1/rds/resources', flavor_data) + except Exception as e: + if '409 Conflict' not in str(e.message): + self.fail('error') + + @patch.object(root.ResourceService, 'main', + side_effect=Exception("unknown error")) + def test_delete_resource_flavor_exce(self, input): + """test delete flavor with general; exception.""" + try: + response = self.app.delete_json('/v1/rds/resources', flavor_data) + except Exception as e: + if 'unknown error' not in str(e.message): + self.fail('error') + + @patch.object(root.ResourceService, 'main', return_value="12345") + def test_update_resource_success(self, input): + updated =False + """test update resource as it succeed.""" + response = self.app.put_json('/v1/rds/resources', good_data) + if 'updated' in response.json['customer']: + updated = True + assert response.json['customer']['id'] == '12345' + assert response.status_int == 201 + assert updated == True + + @patch.object(root.ResourceService, 'main', + side_effect=Exception("unknown error")) + def test_put_resource_gen_exce(self, input): + """test customer put with general; exception.""" + try: + response = self.app.put_json('/v1/rds/resources', good_data) + except Exception as e: + if 'unknown error' not in str(e.message): + self.fail('error') + + @patch.object(root.ResourceService, 'main', + side_effect=root.ConflictValue("region")) + def test_modify_resource_conflict_except(self, input): + """test modify resource to catch ConflictValue exception.""" + response = self.app.put_json('/v1/rds/resources', + good_data, expect_errors=True) + assert response.status_int == 409 + +good_data = { + "service_template": { + "resource": { + "resource_type": "customer" + }, + "model": "{\n \"uuid\": \"1e24981a-fa51-11e5-86aa-5e5517507c6" + "6\",\n \"description\": \"this is a description\",\n \"nam" + "e\": \"testname\",\n \"enabled\": 1,\n \"default_regio" + "n\": {\n \"name\": \"regionnamezzzz\",\n \"quota" + "s\":[\n {\n \"compute\": \n " + " {\n \"instances\": \"10\",\n " + "\"injected_files\": \"10\",\n \"keypair" + "s\": \"10\",\n \"ram\": \"10\"\n " + " },\n \"storage\":\n {\n " + " \"gigabytes\": \"10\",\n \"snapsho" + "ts\": \"10\",\n \"volumes\": \"10\"\n " + " },\n \"network\": \n {\n " + " \"floatingip\": \"10\",\n \"ne" + "twork\": \"10\",\n \"port\": \"10\",\n " + " \"router\": \"10\",\n \"subnet\":" + " \"10\"\n }\n }\n ],\n " + "\"users\": [\n {\n \"id\": \"userId1zzz" + "z\",\n \"roles\": [\n \"admi" + "nzzzz\",\n\t\t\t \"otherzzzzz\"\n ]\n " + "},\n\t\t{\n \"id\": \"userId2zzz\",\n \"ro" + "les\": [\n\t\t\t \"storagezzzzz\"\n ]\n " + "}\n ]\n },\n \"regions\": [\n {\n \"nam" + "e\": \"regionname\",\n \"quotas\":[\n {\n " + " \"compute\": \n {\n \"ins" + "tances\": \"10\",\n \"injected_file" + "s\": \"10\",\n \"keypairs\": \"10\",\n " + " \"ram\": \"10\"\n },\n \"st" + "orage\":\n {\n \"gigabyte" + "s\": \"10\",\n \"snapshots\": \"10\",\n " + " \"volumes\": \"10\"\n },\n " + " \"network\": \n {\n \"floati" + "ngip\": \"10\",\n \"network\": \"10\",\n " + " \"port\": \"10\",\n \"route" + "r\": \"10\",\n \"subnet\": \"10\"\n " + " }\n }\n ],\n \"users\": [\n " + " {\n \"id\": \"userId1\",\n \"role" + "s\": [\n \"admin\",\n\t\t\t \"other\"\n " + " ]\n },\n\t\t{\n \"id\": \"userId2\",\n " + " \"roles\": [\n\t\t\t \"storage\"\n ]\n " + " }\n ]\n },\n\t{\n \"name\": \"regionname" + "test\",\n \"quotas\":[\n {\n \"com" + "pute\": \n {\n \"instanc" + "es\": \"10\",\n \"injected_files\": \"10\",\n" + " \"keypairs\": \"10\",\n \"ra" + "m\": \"10\"\n },\n \"storage\":\n " + " {\n \"gigabytes\": \"10\",\n " + " \"snapshots\": \"10\",\n \"volum" + "es\": \"10\"\n },\n \"network\": \n" + " {\n \"floatingip\": \"10\",\n " + " \"network\": \"10\",\n \"por" + "t\": \"10\",\n \"router\": \"10\",\n " + " \"subnet\": \"10\"\n }\n " + "}\n ],\n \"users\": [\n {\n \"i" + "d\": \"userId1test\",\n \"roles\": [\n " + " \"admintest\",\n\t\t\t \"othertest\"\n ]\n " + " },\n\t\t{\n \"id\": \"userId2test\",\n " + " \"roles\": [\n\t\t\t \"storagetest\"\n ]\n " + " }\n ]\n }\n ]\n}", + "tracking": { + "external_id": "SSP-session1234", + "tracking_id": "uuid-12345" + } + } + } + +flavor_data = { + "service_template": { + "resource": { + "resource_type": "flavor" + }, + "model": "{\n \"status\": \"complete\",\n \"pr" + "ofile\": \"P2\",\n \"regions\": [\n " + " {\n \"name\": \"0\"\n " + " },\n {\n \"nam" + "e\": \"1\"\n }\n ],\n " + "\"description\": \"First flavor for AMAR\",\n \"r" + "am\": 64,\n \"visibility\": \"public\",\n " + " \"extra_specs\": {\n \"key1\": \"value" + "1\",\n \"key2\": \"value2\",\n " + " \"keyx\": \"valuex\"\n },\n \"vcpu" + "s\": 2,\n \"swap\": 51231,\n \"tenan" + "ts\": [\n {\n \"tenant_" + "id\": \"abcd-efgh-ijkl-4567\"\n },\n " + " {\n \"tenant_id\": \"abcd-efgh-" + "ijkl-4567\"\n }\n ],\n " + "\"disk\": 512,\n \"empheral\": 1,\n " + "\"id\": \"uuid-uuid-uuid-uuid\",\n \"name\": \"N" + "ice Flavor\"\n }", + "tracking": { + "external_id": "SSP-session1234", + "tracking_id": "uuid-12345" + } + } + } + +image_data = { + "service_template": { + "resource": { + "resource_type": "image" + }, + "model": "{ \r\n \"internal_id\":1,\r\n \"id\":\"uuu1id12-uuid-uuid-uuid\",\r\n " + " \"name\":\"Ubuntu\",\r\n \"enabled\": 1,\r\n \"protected\": 1,\r\n " + " \"url\": \"https:\/\/mirrors.it.att.com\/images\/image-name\",\r\n " + " \"visibility\": \"public\",\r\n \"disk_format\": \"raw\",\r\n " + " \"container_format\": \"bare\",\r\n \"min_disk\":2,\r\n \"min_ram\":0,\r\n " + " \"regions\":[ \r\n { \r\n \"name\":\"North\",\r\n " + " \"type\":\"single\",\r\n \"action\": \"delete\",\r\n " + "\"image_internal_id\":1\r\n },\r\n { \r\n \"name\":\"North\",\r\n " + " \"action\": \"create\",\r\n \"type\":\"single\",\r\n " + " \"image_internal_id\":1\r\n }\r\n ],\r\n \"image_properties\":[ \r\n { \r\n " + " \"key_name\":\"Key1\",\r\n \"key_value\":\"Key1.value\",\r\n " + "\"image_internal_id\":1\r\n },\r\n { \r\n \"key_name\":\"Key2\",\r\n " + " \"key_value\":\"Key2.value\",\r\n \"image_internal_id\":1\r\n " + " }\r\n ],\r\n \"image_tenant\":[ \r\n { \r\n \"tenant_id\":\"abcd-efgh-ijkl-4567\",\r\n " + " \"image_internal_id\":1\r\n },\r\n { \r\n " + " \"tenant_id\":\"abcd-efgh-ijkl-4567\",\r\n \"image_internal_id\":1\r\n }\r\n ],\r\n " + " \"image_tags\":[ \r\n { \r\n \"tag\":\"abcd-efgh-ijkl-4567\",\r\n " + " \"image_internal_id\":1\r\n },\r\n { \r\n \"tag\":\"abcd-efgh-ijkl-4567\",\r\n " + " \"image_internal_id\":1\r\n }\r\n ],\r\n \"status\":\"complete\",\r\n}", + "tracking": { + "external_id": "SSP-session1234", + "tracking_id": "uuid-12345" + } + } +} diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/status/__init__.py b/orm/services/resource_distributor/rds/tests/controllers/v1/status/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_base.py b/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_base.py new file mode 100644 index 00000000..93e1d313 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_base.py @@ -0,0 +1,13 @@ +import unittest + +from rds.controllers.v1.base import ClientSideError + + +class Test(unittest.TestCase): + + #Test the creation of ClientSideError + def test_ClientSideError(self): + error_str = "This is an error message" + clientSideError = ClientSideError(error=error_str) + self.assertEqual(clientSideError.msg, error_str) + self.assertEqual(clientSideError.code, 400) \ No newline at end of file diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_get_resource_status.py b/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_get_resource_status.py new file mode 100755 index 00000000..6aa30d4b --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_get_resource_status.py @@ -0,0 +1,45 @@ +"""unittest get resource status.""" +from mock import MagicMock + +import rds.controllers.v1.status.get_resource as resource +from rds.services.model.region_resource_id_status import Model +from rds.services.model.region_resource_id_status import StatusModel +from rds.tests.controllers.v1.functional_test import FunctionalTest + + +class EmptyModel(object): + """mock class.""" + + status = None + + def __init__(self, regions=None): + """init function. + + :param regions: + """ + self.regions = regions + + +class GetResourceStatus(FunctionalTest): + """tests for get status api.""" + + def test_get_not_found_resource(self): + """get not found.""" + resource.regionResourceIdStatus.get_status_by_resource_id = \ + MagicMock(return_value=EmptyModel()) + response = self.app.get('/v1/rds/status/resource/1', + expect_errors=True) + assert response.status_int == 404 + + def test_get_valid_resource(self): + """get valid resource.""" + result = Model( + status="200", timestamp="123456789", region="name", + transaction_id=5, resource_id="1", + ord_notifier="", err_msg="123", err_code="12", operation="create" + ) + status_model = StatusModel(status=[result]) + resource.regionResourceIdStatus.get_status_by_resource_id = \ + MagicMock(return_value=status_model) + response = self.app.get('/v1/rds/status/resource/1') + assert response.status_int == 200 diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_resource_status.py b/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_resource_status.py new file mode 100644 index 00000000..e6edcc89 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/controllers/v1/status/test_resource_status.py @@ -0,0 +1,64 @@ +"""unittest for post resource.""" +from mock import patch + +import rds.controllers.v1.status.resource_status as resource +from rds.tests.controllers.v1.functional_test import FunctionalTest + + +class PostResourceStatus(FunctionalTest): + """tests for only for api handler.""" + + @patch.object(resource.regionResourceIdStatus, 'add_status', + return_value=None) + def test_valid_Post_status(self, input): + """Post json valid json.""" + response = self.app.post_json('/v1/rds/status/', data) + assert response.status_int == 201 + + @patch.object(resource.regionResourceIdStatus, 'add_status', + side_effect=resource.InputError("no input", 'request_id')) + def test_valid_Post_status_database_error(self, input): + """Post valid json return database error.""" + response = self.app.post_json('/v1/rds/status/', data, + expect_errors=True) + assert response.status_int == 400 + + @patch.object(resource.regionResourceIdStatus, 'add_status', + return_value=None) + def test_not_valid_json_Post(self, input): + """Post valid json return database error.""" + response = self.app.post_json('/v1/rds/status/', data_not_valid, + expect_errors=True) + assert response.status_int == 400 + + +data = { + "rds-listener": { + "request-id": "0649c5be323f4792", + "resource-id": "12fde398643", + "resource-type": "customer", + "resource-template-version": "1", + "resource-template-type": "HOT", + "resource-operation": "create", + "ord-notifier-id": "1", + "region": "dla1", + "status": "Success", + "error-code": "200", + "error-msg": "OK" + } + } + +data_not_valid = { + "rds_listener": { + "resource_id": "12fde398643", + "resource_type": "customer", + "resource_template_version": "1", + "resource_template_type": "HOT", + "resource_operation": "create", + "ord_notifier_id": "1", + "region": "dla1", + "status": "Success", + "error_code": "200", + "error_msg": "OK" + } + } diff --git a/orm/services/resource_distributor/rds/tests/controllers/v1/test_logs.py b/orm/services/resource_distributor/rds/tests/controllers/v1/test_logs.py new file mode 100755 index 00000000..61cde64f --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/controllers/v1/test_logs.py @@ -0,0 +1,26 @@ +"""Logs module unittests.""" +import logging +from rds.tests.controllers.v1.functional_test import FunctionalTest +from rds.controllers.v1.configuration import root +from mock import patch + + +class TestLogs(FunctionalTest): + """logs tests.""" + + def test_change_log_level_fail(self): + response = self.app.put('/v1/rds/logs/1') + expected_result = {"result": "Fail to change log_level. Reason: The given log level [1] doesn't exist."} + self.assertEqual(expected_result, response.json) + + def test_change_log_level_none(self): + response = self.app.put('/v1/rds/logs/', expect_errors=True) + expexted_result = 'Missing argument: "level"' + self.assertEqual(response.json["faultstring"], expexted_result) + self.assertEqual(response.status_code, 400) + + def test_change_log_level_success(self): + response = self.app.put('/v1/rds/logs/debug') + expexted_result = {'result': 'Log level changed to debug.'} + self.assertEqual(response.json, expexted_result) + self.assertEqual(response.status_code, 201) diff --git a/orm/services/resource_distributor/rds/tests/functional_test.py b/orm/services/resource_distributor/rds/tests/functional_test.py new file mode 100644 index 00000000..9684b586 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/functional_test.py @@ -0,0 +1,140 @@ +"""Base classes for API tests. +""" + +import pecan +import pecan.testing +import unittest +from pecan.testing import load_test_app +import os + + +class FunctionalTest(unittest.TestCase): + """Used for functional tests of Pecan controllers. + + Used in case when you need to test your literal application and its + integration with the framework. + """ + + PATH_PREFIX = '' + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + super(FunctionalTest, self).tearDown() + pecan.set_config({}, overwrite=True) + + def put_json(self, path, params, expect_errors=False, headers=None, + extra_environ=None, status=None): + """Sends simulated HTTP PUT request to Pecan test app. + + :param path: url path of target service + :param params: content for wsgi.input of request + :param expect_errors: boolean value whether an error is expected based + on request + :param headers: A dictionary of headers to send along with the request + :param extra_environ: A dictionary of environ variables to send along + with the request + :param status: Expected status code of response + """ + return self.post_json(path=path, params=params, + expect_errors=expect_errors, + headers=headers, extra_environ=extra_environ, + status=status, method="put") + + def post_json(self, path, params, expect_errors=False, headers=None, + method="post", extra_environ=None, status=None): + """Sends simulated HTTP POST request to Pecan test app. + + :param path: url path of target service + :param params: content for wsgi.input of request + :param expect_errors: boolean value whether an error is expected based + on request + :param headers: A dictionary of headers to send along with the request + :param method: Request method type. Appropriate method function call + should be used rather than passing attribute in. + :param extra_environ: A dictionary of environ variables to send along + with the request + :param status: Expected status code of response + """ + full_path = self.PATH_PREFIX + path + response = getattr(self.app, "%s_json" % method)( + str(full_path), + params=params, + headers=headers, + status=status, + extra_environ=extra_environ, + expect_errors=expect_errors + ) + return response + + def delete(self, path, expect_errors=False, headers=None, + extra_environ=None, status=None): + """Sends simulated HTTP DELETE request to Pecan test app. + + :param path: url path of target service + :param expect_errors: boolean value whether an error is expected based + on request + :param headers: A dictionary of headers to send along with the request + :param extra_environ: A dictionary of environ variables to send along + with the request + :param status: Expected status code of response + """ + full_path = self.PATH_PREFIX + path + response = self.app.delete(str(full_path), + headers=headers, + status=status, + extra_environ=extra_environ, + expect_errors=expect_errors) + return response + + def get_json(self, path, expect_errors=False, headers=None, + extra_environ=None, q=None, groupby=None, status=None, + override_params=None, **params): + """Sends simulated HTTP GET request to Pecan test app. + + :param path: url path of target service + :param expect_errors: boolean value whether an error is expected based + on request + :param headers: A dictionary of headers to send along with the request + :param extra_environ: A dictionary of environ variables to send along + with the request + :param q: list of queries consisting of: field, value, op, and type + keys + :param groupby: list of fields to group by + :param status: Expected status code of response + :param override_params: literally encoded query param string + :param params: content for wsgi.input of request + """ + q = q or [] + groupby = groupby or [] + full_path = self.PATH_PREFIX + path + if override_params: + all_params = override_params + else: + query_params = {'q.field': [], + 'q.value': [], + 'q.op': [], + 'q.type': [], + } + for query in q: + for name in ['field', 'op', 'value', 'type']: + query_params['q.%s' % name].append(query.get(name, '')) + all_params = {} + all_params.update(params) + if q: + all_params.update(query_params) + if groupby: + all_params.update({'groupby': groupby}) + response = self.app.get(full_path, + params=all_params, + headers=headers, + extra_environ=extra_environ, + expect_errors=expect_errors, + status=status) + if not expect_errors: + response = response.json + return response diff --git a/orm/services/resource_distributor/rds/tests/ordupdate/__init__.py b/orm/services/resource_distributor/rds/tests/ordupdate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/ordupdate/test_ord_notifier.py b/orm/services/resource_distributor/rds/tests/ordupdate/test_ord_notifier.py new file mode 100755 index 00000000..384ff833 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/ordupdate/test_ord_notifier.py @@ -0,0 +1,234 @@ +import mock +from mock import patch + +from rds.ordupdate import ord_notifier +import unittest + + +class MyResponse(object): + def __init__(self, status_code, json_result): + self.status_code = status_code + self.json_result = json_result + + def json(self): + return self.json_result + + +def validate_http_post(addr, **kwargs): + if addr.startswith('https'): + raise ValueError('Received an HTTPS address!') + else: + return MyResponse(ord_notifier.ACK_CODE, 'OK') + + +def validate_https_post(addr, **kwargs): + if not addr.startswith('https'): + raise ValueError('Received an HTTPS address!') + else: + return MyResponse(ord_notifier.ACK_CODE, 'OK') + + +class MyOrdupdate(object): + def __init__(self): + self.discovery_url = '3' + self.discovery_port = 3 + self.template_type = '3' + + +class MyDefault(object): + def __init__(self): + self.application_name = 'RDS' + + +class MyConf(object): + def __init__(self): + self.ordupdate = MyOrdupdate() + self.DEFAULT = MyDefault() + + def __call__(self, *args, **kwargs): + pass + + def register_group(self, param): + pass + + def register_opts(self, param1, param2): + pass + + +class MainTest(unittest.TestCase): + def setUp(self): + super(MainTest, self).setUp() + self.addCleanup(mock.patch.stopall) + ord_notifier.CONF = MyConf() + + @mock.patch.object(ord_notifier, 'conf') + def test_find_correct_ord_get_failure(self, mock_conf): + ord_notifier.requests.get = mock.MagicMock( + return_value=MyResponse(404, 'test')) + result = ord_notifier._find_correct_ord(None, None) + self.assertIsNone(result) + + @mock.patch.object(ord_notifier, 'conf') + def test_find_correct_ord_bad_response(self, mock_conf): + ord_notifier.requests.get = mock.MagicMock( + return_value=MyResponse(ord_notifier.OK_CODE, + {'regions': [{'endpoints': [ + {'publicurl': 'test', + 'type': 'test'}]}]})) + result = ord_notifier._find_correct_ord(None, None) + self.assertIsNone(result) + ord_notifier.requests.get = mock.MagicMock( + return_value=MyResponse(ord_notifier.OK_CODE, + {'regions': [{'endqoints': [ + {'publicurl': 'test', + 'type': 'test'}]}]})) + result = ord_notifier._find_correct_ord(None, None) + self.assertIsNone(result) + + @mock.patch.object(ord_notifier, 'conf') + def test_find_correct_ord_sanity(self, mock_conf): + ord_notifier.requests.get = mock.MagicMock( + return_value=MyResponse(ord_notifier.OK_CODE, + {'regions': [{'endpoints': [ + {'publicURL': 'test', + 'type': 'ord'}]}]})) + result = ord_notifier._find_correct_ord(None, 'gigi') + self.assertEqual('test', result) + + @mock.patch.object(ord_notifier, 'conf') + @mock.patch.object(ord_notifier.json, 'dumps') + def test_notify_sanity(self, mock_dumps, mock_conf): + ord_notifier.requests.post = mock.MagicMock( + return_value=MyResponse(ord_notifier.ACK_CODE, None)) + ord_notifier._notify(*("1",) * 8) + + @mock.patch.object(ord_notifier, 'conf') + @mock.patch.object(ord_notifier.json, 'dumps') + def test_notify_not_acknowledged(self, mock_dumps, mock_conf): + ord_notifier.requests.post = mock.MagicMock( + return_value=MyResponse(404, None)) + + try: + ord_notifier._notify(*("1",) * 8) + self.fail('notify() passed successfully' + '(expected NotifyNotAcknowledgedError)') + except ord_notifier.NotifyNotAcknowledgedError: + pass + # @mock.patch.object(ord_notifier, 'conf') + # def test_notify_sanity(self, mock_conf): + # ord_notifier.requests.post = mock.MagicMock( + # return_value=MyResponse(ord_notifier.ACK_CODE, None)) + # ord_notifier._notify(*("1", )*8) + +# @mock.patch.object(ord_notifier, 'conf') + # def test_notify_not_acknowledged(self, mock_conf): + # ord_notifier.requests.post = mock.MagicMock( + # return_value=MyResponse(404, None)) +# + # try: + # ord_notifier._notify(*("1", )*8) + # self.fail('notify() passed successfully' + # '(expected NotifyNotAcknowledgedError)') + # except ord_notifier.NotifyNotAcknowledgedError: + # pass + + @mock.patch.object(ord_notifier, 'conf') + def test_notify_https_disabled_but_received(self, mock_conf): + ord_notifier.requests.post = validate_http_post + mock_conf.ordupdate.https_enabled = False + mock_conf.ordupdate.template_type = 'a' + ord_notifier._notify('https://127.0.0.1:1337', *("1", )*7) + + @mock.patch.object(ord_notifier, 'conf') + @mock.patch.object(ord_notifier.json, 'dumps') + def test_notify_https_enabled_and_no_certificate(self, mock_dumps, + mock_conf): + ord_notifier.requests.post = validate_https_post + mock_conf.ordupdate.https_enabled = True + mock_conf.ordupdate.cert_path = '' + ord_notifier._notify('https://127.0.0.1:1337', *("1",) * 7) + + @mock.patch.object(ord_notifier, 'conf') + @mock.patch.object(ord_notifier.json, 'dumps') + def test_notify_https_enabled_and_ssl_error(self, mock_dumps, mock_conf): + ord_notifier.requests.post = mock.MagicMock( + side_effect=ord_notifier.requests.exceptions.SSLError('test')) + mock_conf.ordupdate.https_enabled = True + mock_conf.ordupdate.cert_path = '' + self.assertRaises(ord_notifier.requests.exceptions.SSLError, + ord_notifier._notify, 'https://127.0.0.1:1337', + *("1",) * 7) +# @mock.patch.object(ord_notifier, 'conf') + # def test_notify_https_enabled_and_no_certificate(self, mock_conf): + # ord_notifier.requests.post = validate_https_post + # mock_conf.ordupdate.https_enabled = True + # mock_conf.ordupdate.cert_path = '' + # ord_notifier._notify('https://127.0.0.1:1337', *("1", )*7) + +# @mock.patch.object(ord_notifier, 'conf') + # def test_notify_https_enabled_and_ssl_error(self, mock_conf): + # ord_notifier.requests.post = mock.MagicMock( + # side_effect=ord_notifier.requests.exceptions.SSLError('test')) + # mock_conf.ordupdate.https_enabled = True + # mock_conf.ordupdate.cert_path = '' + # self.assertRaises(ord_notifier.requests.exceptions.SSLError, + # ord_notifier._notify, 'https://127.0.0.1:1337', + # *("1", )*7) + + @patch.object(ord_notifier.audit, 'audit') + @patch.object(ord_notifier, 'regionResourceIdStatus') + @mock.patch.object(ord_notifier, 'conf') + def test_main_ord_not_found(self, mock_audit, mock_region, mock_conf): + ord_notifier.requests.get = mock.MagicMock( + return_value=MyResponse(404, 'test')) + try: + ord_notifier.notify_ord('test', '1', '2', '3', '4', '5', '6', + 'gigi', '7', '') + self.fail('notify_ord() passed successfully (expected OrdNotFoundError)') + except ord_notifier.OrdNotFoundError as e: + self.assertEquals(e.message, 'ORD of LCP %s not found' % ( + 'gigi', )) + + #@patch.object(ord_notifier.audit, 'audit') + #@patch.object(ord_notifier, 'regionResourceIdStatus') + # @mock.patch.object(ord_notifier, 'conf') + # @mock.patch.object(ord_notifier.json, 'dumps') + #def test_main_sanity(self, mock_dumps, mock_conf, mock_region, mock_audit): + # ord_notifier.requests.get = mock.MagicMock( + # return_value=MyResponse(ord_notifier.OK_CODE, + # {ord_notifier.LCP_ID: 'gigi', + # ord_notifier.ORD_URL: 'test'})) + # ord_notifier.requests.post = mock.MagicMock( + # return_value=MyResponse(ord_notifier.ACK_CODE, None)) + + # ord_notifier.notify_ord('test', '1', '2', '3', '4', '5', '6', '7', + # '8', '') + # @patch.object(ord_notifier.audit, 'audit') + # @patch.object(ord_notifier, 'regionResourceIdStatus') +# @mock.patch.object(ord_notifier, 'conf') +# def test_main_sanity(self, mock_audit, mock_region, mock_conf): + # ord_notifier.requests.get = mock.MagicMock( + # return_value=MyResponse(ord_notifier.OK_CODE, + # {'regions': [{'endpoints': [ + # {'publicurl': 'test', + # 'type': 'ord'}]}]})) + # ord_notifier.requests.post = mock.MagicMock( + # return_value=MyResponse(ord_notifier.ACK_CODE, None)) + + #ord_notifier.notify_ord('test', '1', '2', '3', '4', '5', '6', '7', + # '8', '') + + @patch.object(ord_notifier.audit, 'audit') + @patch.object(ord_notifier, 'regionResourceIdStatus') + @mock.patch.object(ord_notifier, 'conf') + def test_main_error(self, mock_audit, mock_region, mock_conf): + ord_notifier.requests.get = mock.MagicMock( + return_value=MyResponse(ord_notifier.OK_CODE, + {'regions': [{'endpoints': [ + {'publicurl': 'test', + 'type': 'ord'}]}]})) + ord_notifier.requests.post = mock.MagicMock( + return_value=MyResponse(ord_notifier.ACK_CODE, None)) + + ord_notifier.notify_ord('test', '1', '2', '3', '4', '5', '6', '7', + '8', '9', '10', True) diff --git a/orm/services/resource_distributor/rds/tests/services/__init__.py b/orm/services/resource_distributor/rds/tests/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/services/model/__init__.py b/orm/services/resource_distributor/rds/tests/services/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/services/model/test_region_resource_id_status.py b/orm/services/resource_distributor/rds/tests/services/model/test_region_resource_id_status.py new file mode 100755 index 00000000..ae3be098 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/services/model/test_region_resource_id_status.py @@ -0,0 +1,44 @@ +import unittest + +from rds.services.model import region_resource_id_status + + +class TestModel(unittest.TestCase): + def test_model_as_dict(self): + model = region_resource_id_status.Model(1, 2, 3, 4, 5, 6, 7, 8, + 'create') + expected_dict = { + 'timestamp': 1, + 'region': 2, + 'status': 3, + 'ord_transaction_id': 4, + 'resource_id': 5, + 'ord_notifier_id': 6, + 'error_msg': 7, + 'error_code': 8, + 'operation': 'create', + 'resource_extra_metadata': None + } + + test_dict = model.as_dict() + self.assertEqual(test_dict, expected_dict) + + +class TestStatusModel(unittest.TestCase): + def test_get_aggregated_status_error(self): + model = region_resource_id_status.Model(1, 2, 'Error', 4, 5, 6, 7, 8, + 'create') + status_model = region_resource_id_status.StatusModel([model]) + self.assertEqual(status_model.status, 'Error') + + def test_get_aggregated_status_pending(self): + model = region_resource_id_status.Model(1, 2, 'Submitted', 4, 5, 6, 7, + 8, 'create') + status_model = region_resource_id_status.StatusModel([model]) + self.assertEqual(status_model.status, 'Pending') + + def test_get_aggregated_status_success(self): + model = region_resource_id_status.Model(1, 2, 'Success', 4, 5, 6, 7, 8, + 'create') + status_model = region_resource_id_status.StatusModel([model]) + self.assertEqual(status_model.status, 'Success') diff --git a/orm/services/resource_distributor/rds/tests/services/test_create_resource.py b/orm/services/resource_distributor/rds/tests/services/test_create_resource.py new file mode 100755 index 00000000..9a02c5fd --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/services/test_create_resource.py @@ -0,0 +1,671 @@ +"""create resource unittest module.""" +import unittest + +from mock import patch + +from rds.services import resource as ResourceService +from rds.services.model.region_resource_id_status import Model, ResourceMetaData +from rds.services.model.region_resource_id_status import StatusModel + +result = Model( + status="success", timestamp="123456789", region="name", + transaction_id=5, resource_id="1", + ord_notifier="", err_msg="123", err_code="12", operation="create", + resource_extra_metadata=[ResourceMetaData(checksum=1,virtual_size=2,size=3)] +) + +uuid = "uuid-12345" + + +class InputData(object): + """mock class.""" + + def __init__(self, resource_id, resource_type, + targets, operation="create", + transaction_id="", model="", + external_transaction_id=""): + """init function. + + :param resource_id: + :param resource_type: + :param targets: + :param operation: + :param transaction_id: + :param model: + :param external_transaction_id: + """ + self.resource_id = resource_id + self.targets = targets + self.resource_type = resource_type + self.operation = operation + self.transaction_id = transaction_id + self.model = model + self.external_transaction_id = external_transaction_id + + +class SoT(object): + """mock class.""" + + def save_resource_to_sot(*args): + """mock function.""" + return None + + def delete_resource_from_sot(*args): + """mock function.""" + return None + + +class CreateResource(unittest.TestCase): + """create resource test.""" + + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', + # return_value=StatusModel(status=[result])) + # def test_create_customer_conflict_rise(self, result): + # """check raise conflict.""" + # with self.assertRaises(ResourceService.ConflictValue): + # ResourceService.main(jsondata, uuid, 'customer', 'create') + + # @patch.object(ResourceService, '_upload_to_sot', return_value=[1, 2]) + # @patch.object(ResourceService, '_create_data_to_sot', return_value=[1, 2]) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', return_value=None) + # @patch.object(ResourceService.uuid_utils, + # 'get_random_uuid', return_value='uuid-gen-123456') + # def test_create_customer_valid_uuid_gen(self, tranid, result, + # sotdata, sotupload): + # """check flow with uuid gen.""" + # status_model = StatusModel(status=[result]) + # status_model.regions = None + # result.return_value = status_model + # resource_id = ResourceService.main(jsondata, uuid, + # 'customer', 'create') + # self.assertEqual(resource_id, jsondata['uuid']) + + @patch.object(ResourceService.regionResourceIdStatus, 'add_status', + return_value=None) + @patch.object(ResourceService, '_upload_to_sot', return_value=[1, 2]) + @patch.object(ResourceService, '_create_data_to_sot', return_value=[1, 2]) + @patch.object(ResourceService.regionResourceIdStatus, + 'get_regions_by_status_resource_id', return_value=None) + @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + side_effect=Exception("uuid general exception")) + def test_create_customer_not_valid_uuid_gen(self, tranid, result, sotdata, + sotupload, database): + """uuid gen raise an error.""" + status_model = StatusModel(status=[result]) + status_model.regions = None + result.return_value = status_model + with self.assertRaises(ResourceService.ErrorMesage): + resource_id = ResourceService.main(jsondata, uuid, + 'customer', 'create') + + # @patch.object(ResourceService.regionResourceIdStatus, 'add_status', + # return_value=None) + # @patch.object(ResourceService.yaml_customer_builder, 'yamlbuilder', + # return_value=["anystring"]) + # @patch.object(ResourceService, '_upload_to_sot', return_value=[1, 2]) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', return_value=None) + # @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + # return_value='uuid-gen-123456') + # def test_create_customer_sot_data(self, tranid, result, sotupload, + # yamlbuilder, database): + # """check sot data build for customer.""" + # status_model = StatusModel(status=[result]) + # status_model.regions = None + # result.return_value = status_model + # resource_id = ResourceService.main(jsondata, uuid, + # 'customer', 'create') + + # @patch.object(ResourceService.regionResourceIdStatus, 'add_status', + # return_value=None) + # @patch.object(ResourceService.yaml_customer_builder, 'yamlbuilder', + # return_value=["anystring"]) + # @patch.object(ResourceService.sot_factory, 'get_sot', + # return_value=SoT()) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', return_value=None) + # @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + # return_value='uuid-gen-123456') + # def test_create_resource_upload_sot(self, tranid, result, sotupload, + # yamlbuilder, database): + # """check upload to sot.""" + # status_model = StatusModel(status=[result]) + # status_model.regions = None + # result.return_value = status_model + # resource_id = ResourceService.main(jsondata, uuid, + # 'customer', 'create') + + # @patch.object(ResourceService.regionResourceIdStatus, 'add_status', + # return_value=None) + # @patch.object(ResourceService.yaml_flavor_bulder, 'yamlbuilder', + # return_value=["anystring"]) + # @patch.object(ResourceService.sot_factory, 'get_sot', return_value=SoT()) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', return_value=None) + # @patch.object(ResourceService.uuid_utils, + # 'get_random_uuid', return_value='uuid-gen-123456') + # def test_create_flavor_sot_data(self, tranid, result, sotupload, + # yamlbuilder, database): + # """check flavor data create.""" + # status_model = StatusModel(status=[result]) + # status_model.regions = None + # result.return_value = status_model + # resource_id = ResourceService.main(flavorjsondata, uuid, + # 'flavor', 'create') + + @patch.object(ResourceService.regionResourceIdStatus, + 'add_status', return_value=None) + @patch.object(ResourceService.yaml_customer_builder, + 'yamlbuilder', return_value=["anystring"]) + @patch.object(ResourceService.sot_factory, 'get_sot', return_value=SoT()) + @patch.object(ResourceService.regionResourceIdStatus, + 'get_regions_by_status_resource_id', return_value=None) + @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + return_value='uuid-gen-123456') + def test_create_flavor_sot_data_check(self, tranid, result, sotupload, + yamlbuilder, database): + """check list creating.""" + input_data = InputData( + transaction_id='497ab942-1ac0-11e6-82f3-005056a5129b', + resource_type='customer', + resource_id='1e24981a-fa51-11e5-86aa-5e5517507c66', + operation='create', + targets=targets + ) + status_model = StatusModel(status=[result]) + status_model.regions = None + result.return_value = status_model + result = ResourceService._create_data_to_sot(input_data) + self.assertEqual(result, target_list) + + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', + # return_value=StatusModel(status=[result])) + # def test_delete_flavor_conflict(self, databasemock): + # """check delete flavor with conflict.""" + # with self.assertRaises(ResourceService.ConflictValue): + # ResourceService.main(flavorjsondata, uuid, 'flavor', 'delete') + + @patch.object(ResourceService.regionResourceIdStatus, + 'add_status', return_value=None) + @patch.object(ResourceService, '_upload_to_sot', return_value=[1, 2]) + @patch.object(ResourceService, '_create_data_to_sot', return_value=[1, 2]) + @patch.object(ResourceService.regionResourceIdStatus, + 'get_regions_by_status_resource_id', return_value=None) + @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + side_effect=Exception("uuid general exception")) + def test_delete_flavor_not_valid_uuid_gen(self, tranid, result, sotdata, + sotupload, database): + """delete flavor uuid gen raise an error.""" + status_model = StatusModel(status=[result]) + status_model.regions = None + result.return_value = status_model + with self.assertRaises(ResourceService.ErrorMesage): + resource_id = ResourceService.main(flavorjsondata, uuid, + 'flavor', 'delete') + + # @patch.object(ResourceService.yaml_flavor_bulder, + # 'yamlbuilder', return_value=["anystring"]) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'add_status', return_value=None) + # # @patch.object(ResourceService, '_delete_from_sot', return_value = None) + # @patch.object(ResourceService.sot_factory, 'get_sot', return_value=SoT()) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', return_value=None) + # @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + # return_value='uuid-gen-123456') + # def test_delete_flavor_not_valid_all(self, tranid, result, + # sotdata, sotupload, yaml_mock): + # """delete flavor uuid gen raise an error.""" + # status_model = StatusModel(status=[result]) + # status_model.regions = None + # result.return_value = status_model + # resource_id = ResourceService.main(flavorjsondata, uuid, + # 'flavor', 'delete') + # self.assertEqual('uuid-uuid-uuid-uuid', resource_id) + + + # @patch.object(ResourceService.regionResourceIdStatus, 'add_status', + # return_value=None) + # @patch.object(ResourceService.yaml_customer_builder, 'yamlbuilder', + # return_value=["anystring"]) + # @patch.object(ResourceService.sot_factory, 'get_sot', + # return_value=SoT()) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', return_value=None) + # @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + # return_value='uuid-gen-123456') + # def test_create_resource_up2load_sot_put(self, moc_get_random_uuid, + # moc_get_regions_by_status_resource_id, + # moc_get_sot, + # moc_yamlbuilder, moc_add_status): + # """check upload to sot.""" + # status_model = StatusModel(status=[result]) + # status_model.regions = None + # moc_get_regions_by_status_resource_id.return_value = status_model + # resource_id = ResourceService.main(jsondata, uuid, + # 'customer', 'modify') + + + # @patch.object(ResourceService.regionResourceIdStatus, 'add_status', + # return_value=None) + # @patch.object(ResourceService.yaml_image_builder, 'yamlbuilder', + # return_value=["anystring"]) + # @patch.object(ResourceService.sot_factory, 'get_sot', + # return_value=SoT()) + # @patch.object(ResourceService.regionResourceIdStatus, + # 'get_regions_by_status_resource_id', return_value=None) + # @patch.object(ResourceService.uuid_utils, 'get_random_uuid', + # return_value='uuid-gen-123456') + # def test_create_resource_up2load_sot_put_image(self, moc_get_random_uuid, + # moc_get_regions_by_status_resource_id, + # moc_get_sot, + # moc_yamlbuilder, moc_add_status): + # """check upload to sot.""" + # status_model = StatusModel(status=[result]) + # status_model.regions = None + # moc_get_regions_by_status_resource_id.return_value = status_model + # resource_id = ResourceService.main(json_data_image, uuid, + # 'image', 'modify') + + + def test_get_inputs_from_resource_type(self): + input_data = ResourceService._get_inputs_from_resource_type(jsondata, + 'customer', + 'uuid-12345') + assert ( input_data.__dict__ == input_data_resource ) + + + def test_get_inputs_from_resource_type_image(self): + input_data = ResourceService._get_inputs_from_resource_type(json_data_image, + 'image', + 'uuid-12345') + assert (input_data.__dict__ == expected_image_input_data) + + + def test_unknown_resource_type(self): + with self.assertRaises(ResourceService.ErrorMesage): + input_data = ResourceService._get_inputs_from_resource_type(jsondata, + 'unknown', + 'uuid-12345') + + + +jsondata = { + "uuid": "1e24981a-fa51-11e5-86aa-5e5517507c66", "default_region": + { + "quotas": + [ + { + "compute": { + "instances": "10", + "ram": "10", + "keypairs": "10", + "injected_files": "10" + }, + "storage": {"gigabytes": "10", + "snapshots": "10", + "volumes": "10" + }, + "network": + { + "router": "10", + "floatingip": "10", + "port": "10", + "network": "10", + "subnet": "10" + }}], + "users": + [ + { + "id": "userId1zzzz", + "roles": + [ + "adminzzzz", + "otherzzzzz" + ] + }, + {"id": "userId2zzz", + "roles": + [ + "storagezzzzz" + ] + } + ], + "name": "regionnamezzzz", + "action": "delete", + }, + "description": "this is a description", + "enabled": 1, + "regions": + [ + { + "quotas": + [], + "users": + [ + { + "id": "userId1", + "roles": + [ + "admin", + "other" + ] + }, + {"id": "userId2", + "roles": + [ + "storage" + ] + } + ], + "name": "regionname", + "action": "create" + }, + { + "quotas": + [ + { + "compute": + { + "instances": "10", + "ram": "10", + "keypairs": "10", + "injected_files": "10" + }, + "storage": + { + "gigabytes": "10", + "snapshots": "10", + "volumes": "10" + }, + "network": + { + "router": "10", + "floatingip": "10", + "port": "10", + "network": "10", + "subnet": "10" + } + } + ], + "users": + [], + "name": "regionnametest", + "action": "delete" + } + ], + "name": "welcome_man" +} + +flavorjsondata = {"status": "complete", "profile": "P2", "regions": + [{"name": "North1","action": "create"}, {"name": "North2","action": "delete" + }], "description": "First flavor for AMAR", + "ram": 64, "visibility": "public", "extra_specs": { + "key1": "value1", "key2": "value2", "keyx": "valuex"}, + "vcpus": 2, + "swap": 0, "tenants": [{"tenant_id": "abcd-efgh-ijkl-4567"}, + {"tenant_id": "abcd-efgh-ijkl-4567" + }], + "disk": 512, "empheral": 1, "id": "uuid-uuid-uuid-uuid", + "name": "Nice Flavor"} + +json_data = {'uuid': '1e24981a-fa51-11e5-86aa-5e5517507c66', + 'default_region': {'users': [{'id': 'userId1zzzz', + 'roles': ['adminzzzz', + 'otherzzzzz' + ] + }, + {'id': 'userId2zzz', + 'roles': ['storagezzzzz' + ] + } + ], + 'name': 'regionnamezzzz', + "action": "create", + 'quotas': [{'storage': { + 'gigabytes': '111', + 'volumes': '111', + 'snapshots': '111'}, + 'compute': {'instances': '111', + 'ram': '111', + 'keypairs': '111', + 'injected_files': '111' + }, + 'network': {'port': '111', + 'router': '111', + 'subnet': '111', + 'network': '111', + 'floatingip': '111'}}]}, + 'description': 'this is a description', 'enabled': 1, + 'regions': [{'users': [{'id': 'userId1', + 'roles': ['admin', 'other']}, + {'id': 'userId2', + 'roles': ['storage']}], + 'name': 'regionname', "action": "delete", + 'quotas': []}, + {'users': [], 'name': 'regionnametest', + "action": "modify", + 'quotas': [{'storage': {'gigabytes': '10', + 'volumes': '10', + 'snapshots': '10'}, + 'compute': {'instances': '10', + 'ram': '10', + 'keypairs': '10', + 'injected_files': '10'}, + 'network': {'port': '10', + 'router': '10', + 'subnet': '10', + 'network': '10', + 'floatingip': '10'}}]}], + 'name': 'welcome_man'} + + +target_list = [{'template_data': ['anystring'], + 'operation': 'create', + 'resource_name': '1e24981a-fa51-11e5-86aa-5e5517507c66', + 'region_id': 'regionname', 'resource_type': u'customer'}, + {'template_data': 'delete', 'operation': 'delete', + 'resource_name': '1e24981a-fa51-11e5-86aa-5e5517507c66', + 'region_id': 'regionnametest', 'resource_type': u'customer'}] + +targets = [{'users': [{'id': 'userId1', 'roles': ['admin', 'other']}, + {'id': 'userId2', 'roles': ['storage']}], + 'name': 'regionname', "action": "create", 'quotas': []}, + {'users': [], + 'name': 'regionnametest', + "action": "delete", + 'quotas': [{'storage': {'gigabytes': '10', 'volumes': '10', + 'snapshots': '10'}, + 'compute': {'instances': '10', 'ram': '10', + 'keypairs': '10', 'injected_files': '10'}, + 'network': {'port': '10', + 'router': '10', + 'subnet': '10', + 'network': '10', + 'floatingip': '10'}}]}] + +json_data_image = { + "internal_id":1, + "id":"uuu1id12-uuid-uuid-uuid", + "name":"Ubuntu", + "enabled": 1, + "protected": 1, + "url": "https://mirrors.it.att.com/images/image-name", + "visibility": "public", + "disk_format": "raw", + "container_format": "bare", + "min_disk":2, + "min_ram":0, + "regions":[ + { + "name":"North", + "type":"single", + "action": "delete", + "image_internal_id":1 + }, + { + "name":"North", + "action": "create", + "type":"single", + "image_internal_id":1 + } + ], + "image_properties":[ + { + "key_name":"Key1", + "key_value":"Key1.value", + "image_internal_id":1 + }, + { + "key_name":"Key2", + "key_value":"Key2.value", + "image_internal_id":1 + } + ], + "image_tenant":[ + { + "tenant_id":"abcd-efgh-ijkl-4567", + "image_internal_id":1 + }, + { + "tenant_id":"abcd-efgh-ijkl-4567", + "image_internal_id":1 + } + ], + "image_tags":[ + { + "tag":"abcd-efgh-ijkl-4567", + "image_internal_id":1 + }, + { + "tag":"abcd-efgh-ijkl-4567", + "image_internal_id":1 + } + ], + "status":"complete", +} + +input_data_resource = {'resource_id': '1e24981a-fa51-11e5-86aa-5e5517507c66', + 'targets': [ + {'action': 'create', 'quotas': [], + 'name': 'regionname', + 'users': [ + {'id': 'userId1', 'roles': ['admin', 'other']}, + {'id': 'userId2', 'roles': ['storage']}]}, + {'action': 'delete', + 'quotas': [{ + 'storage': { + 'gigabytes': '10', + 'volumes': '10', + 'snapshots': '10'}, + 'compute': { + 'instances': '10', + 'ram': '10', + 'keypairs': '10', + 'injected_files': '10'}, + 'network': { + 'subnet': '10', + 'router': '10', + 'port': '10', + 'network': '10', + 'floatingip': '10'}}], + 'name': 'regionnametest', + 'users': []}], + 'resource_type': 'customer', + 'model': { + 'uuid': '1e24981a-fa51-11e5-86aa-5e5517507c66', + 'default_region': {'action': 'delete', + 'quotas': [{'storage': { + 'gigabytes': '10', + 'volumes': '10', + 'snapshots': '10'}, + 'compute': { + 'instances': '10', + 'ram': '10', + 'keypairs': '10', + 'injected_files': '10'}, + 'network': { + 'subnet': '10', + 'router': '10', + 'port': '10', + 'network': '10', + 'floatingip': '10'}}], + 'name': 'regionnamezzzz', + 'users': [ + {'id': 'userId1zzzz', + 'roles': ['adminzzzz', + 'otherzzzzz']}, + {'id': 'userId2zzz', + 'roles': [ + 'storagezzzzz']}]}, + 'description': 'this is a description', + 'enabled': 1, 'regions': [ + {'action': 'create', 'quotas': [], + 'name': 'regionname', + 'users': [{'id': 'userId1', + 'roles': ['admin', 'other']}, + {'id': 'userId2', + 'roles': ['storage']}]}, + {'action': 'delete', + 'quotas': [{'storage': {'gigabytes': '10', + 'volumes': '10', + 'snapshots': '10'}, + 'compute': {'instances': '10', + 'ram': '10', + 'keypairs': '10', + 'injected_files': '10'}, + 'network': {'subnet': '10', + 'router': '10', + 'port': '10', + 'network': '10', + 'floatingip': '10'}}], + 'name': 'regionnametest', 'users': []}], + 'name': 'welcome_man'}, + 'external_transaction_id': 'uuid-12345', + 'operation': 'create', + 'transaction_id': ''} + +expected_image_input_data = {'resource_id': 'uuu1id12-uuid-uuid-uuid', + 'targets': [ + {'action': 'delete', 'image_internal_id': 1, + 'type': 'single', 'name': 'North'}, + {'action': 'create', 'image_internal_id': 1, + 'type': 'single', 'name': 'North'}], + 'resource_type': 'image', + 'model': {'status': 'complete', 'name': 'Ubuntu', + 'internal_id': 1, + 'url': 'https://mirrors.it.att.com/images/image-name', + 'disk_format': 'raw', 'min_ram': 0, + 'enabled': 1, 'visibility': 'public', + 'image_tags': [{'image_internal_id': 1, + 'tag': 'abcd-efgh-ijkl-4567'}, + {'image_internal_id': 1, + 'tag': 'abcd-efgh-ijkl-4567'}], + 'regions': [{'action': 'delete', + 'image_internal_id': 1, + 'type': 'single', + 'name': 'North'}, + {'action': 'create', + 'image_internal_id': 1, + 'type': 'single', + 'name': 'North'}], + 'image_properties': [ + {'key_name': 'Key1', + 'key_value': 'Key1.value', + 'image_internal_id': 1}, + {'key_name': 'Key2', + 'key_value': 'Key2.value', + 'image_internal_id': 1}], + 'protected': 1, 'image_tenant': [ + {'tenant_id': 'abcd-efgh-ijkl-4567', + 'image_internal_id': 1}, + {'tenant_id': 'abcd-efgh-ijkl-4567', + 'image_internal_id': 1}], + 'container_format': 'bare', + 'min_disk': 2, + 'id': 'uuu1id12-uuid-uuid-uuid'}, + 'external_transaction_id': 'uuid-12345', + 'operation': 'create', 'transaction_id': ''} diff --git a/orm/services/resource_distributor/rds/tests/services/test_customer_yaml.py b/orm/services/resource_distributor/rds/tests/services/test_customer_yaml.py new file mode 100755 index 00000000..3ca34827 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/services/test_customer_yaml.py @@ -0,0 +1,293 @@ +"""unittests create customer yaml module.""" +import unittest + +import yaml +from mock import patch + +from rds.services import yaml_customer_builder as CustomerBuild + +alldata = { + 'uuid': '1e24981a-fa51-11e5-86aa-5e5517507c66', + 'metadata': [{'my_server_name': 'Apache1'},{'ocx_cust': '123456889'}], + 'default_region': {'users': [{'id': 'userId1zzzz', + 'roles': ['adminzzzz', 'otherzzzzz']}, + {'id': 'userId2zzz', + 'roles': ['storagezzzzz']}], + 'name': 'regionnamezzzz', + 'quotas': [{'storage': {'gigabytes': '111', + 'volumes': '111', + 'snapshots': '111'}, + 'compute': {'instances': '111', + 'ram': '111', + 'keypairs': '111', + 'injected_files': '111'}, + 'network': {'port': '111', + 'router': '111', + 'subnet': '111', + 'network': '111', + 'floatingip': '111'}}]}, + 'description': 'this is a description', 'enabled': 1, + 'regions': [{'users': [{'id': 'userId1', 'roles': ['admin', 'other']}, + {'id': 'userId2', 'roles': ['storage']}], + 'name': 'regionname', 'quotas': []}, + {'users': [], 'name': 'regionnametest', + 'quotas': [{'storage': {'gigabytes': '10', + 'volumes': '10', + 'snapshots': '10'}, + 'compute': {'instances': '10', 'ram': '10', + 'keypairs': '10', + 'injected_files': '10'}, + 'network': {'port': '10', 'router': '10', + 'subnet': '10', 'network': '10', + 'floatingip': '10'}}]}], + 'name': 'welcome_man'} + +region_quotas = {'users': + [], + 'name': 'regionnametest', + 'quotas': [{'storage': {'gigabytes': '10', + 'volumes': '10', 'snapshots': '10'}, + 'compute': {'instances': '10', 'ram': '10', + 'keypairs': '10', + 'injected_files': '10'}, + 'network': {'port': '10', + 'router': '10', + 'subnet': '10', + 'network': '10', + 'floatingip': '10'}}]} + +region_users = {'users': [{'id': 'userId1', 'roles': ['admin', 'other']}, + {'id': 'userId2', 'roles': ['storage']}], + 'name': 'regionname', 'quotas': []} + +full_region = {'users': [{'id': 'userId1', 'roles': ['admin', 'other']}, + {'id': 'userId2', 'roles': ['storage']}], + 'name': 'regionnametest', + 'quotas': [{'storage': {'gigabytes': '10', + 'volumes': '10', 'snapshots': '10'}, + 'compute': {'instances': '10', 'ram': '10', + 'keypairs': '10', + 'injected_files': '10'}, + 'network': {'port': '10', 'router': '10', + 'subnet': '10', + 'network': '10', 'floatingip': '10'}}]} + + +fullyaml_with_users_quotasoff = \ + 'heat_template_version: 2015-1-2\n\ndescription: yaml file for region - ' \ + 'regionname\n\nresources:\n tenant_metadata:\n' \ + ' properties:\n METADATA:\n metadata:\n my_server_name: Apache1\n ' \ + ' ocx_cust: 123456889\n TENANT_ID: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' type: OS::Keystone::Metadata\n\n \n userId1:\n ' \ + 'properties:\n groups:\n - {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1_group}\n ' \ + 'name: userId1\n roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' role: admin\n - project: {get_resource: ' \ + '1e24981a-fa51-11e5-86aa-5e5517507c66}\n role: other\n type: OS::Keystone::User\n\n' \ + ' \n userId2:\n properties:\n groups:\n - ' \ + '{get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2_group}\n name: userId2\n roles:\n ' \ + '- project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n role: storage\n' \ + ' type: OS::Keystone::User\n\n \n 1e24981a-fa51-11e5-86aa-5e5517507c66:\n properties:\n ' \ + 'description: this is a description\n enabled: true\n ' \ + 'name: welcome_man\n project_id: 1e24981a-fa51-11e5-86aa-5e5517507c66\n type: OS::Keystone::Project2\n\n ' \ + '\n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1_group:\n properties:\n description: dummy\n ' \ + 'domain: default\n name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1_group\n roles:\n - ' \ + 'project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n role: {get_resource: other}\n ' \ + 'type: OS::Keystone::Group\n\n \n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2_group:\n properties:\n ' \ + ' description: dummy\n domain: default\n name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2_group\n ' \ + 'roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: {get_resource: storage}\n type: OS::Keystone::Group\n\n ' \ + '\n\noutputs:\n userId1_id:\n value: {get_resource: userId1}\n' \ + ' userId2_id:\n value: {get_resource: userId2}\n ' \ + '1e24981a-fa51-11e5-86aa-5e5517507c66_id:\n value: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' + + +fullyaml_no_users_quotasoff = \ + 'heat_template_version: 2015-1-1\n\ndescription: yaml file for region ' \ + '- regionnametest\n\nresources:\n tenant_metadata:\n properties:\n' \ + ' METADATA:\n metadata:\n my_server_name: Apache1\n ocx_cust: 123456889\n' \ + ' TENANT_ID: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'type: OS::Keystone::Metadata\n\n \n userId1zzzz:\n properties:\n ' \ + 'groups:\n - {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1zzzz_group}\n ' \ + 'name: userId1zzzz\n roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' role: adminzzzz\n - ' \ + 'project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n role: otherzzzzz\n' \ + ' type: OS::Keystone::User\n\n \n userId2zzz:\n properties:\n ' \ + 'groups:\n - {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2zzz_group}\n ' \ + 'name: userId2zzz\n roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' role: storagezzzzz\n type: OS::Keystone::User\n\n ' \ + '\n 1e24981a-fa51-11e5-86aa-5e5517507c66:\n properties:\n description: this is a description\n' \ + ' enabled: true\n name: welcome_man\n ' \ + ' project_id: 1e24981a-fa51-11e5-86aa-5e5517507c66\n type: OS::Keystone::Project2\n\n \n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1zzzz_group:\n ' \ + 'properties:\n description: dummy\n domain: default\n ' \ + 'name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1zzzz_group\n roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' role: {get_resource: otherzzzzz}\n type: OS::Keystone::Group\n\n' \ + ' \n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2zzz_group:\n properties:\n description: dummy\n ' \ + 'domain: default\n name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2zzz_group\n roles:\n ' \ + '- project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n role: {get_resource: storagezzzzz}\n' \ + ' type: OS::Keystone::Group\n\n \n\noutputs:\n userId1zzzz_id:\n' \ + ' value: {get_resource: userId1zzzz}\n userId2zzz_id:\n ' \ + 'value: {get_resource: userId2zzz}\n 1e24981a-fa51-11e5-86aa-5e5517507c66_id:\n ' \ + 'value: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' + +full_yaml_default_quotas = 'heat_template_version: 2015-1-1\n\ndescription: yaml file for region ' \ + '- regionname\n\nresources:\n cinder_quota:\n properties:\n ' \ + 'gigabytes: 111\n snapshots: 111\n tenant: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' volumes: 111\n type: OS::Cinder::Quota\n\n ' \ + ' \n neutron_quota:\n properties:\n floatingip: 111\n' \ + ' network: 111\n port: 111\n router: 111\n subnet: 111\n' \ + ' tenant: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n type: OS::Neutron::Quota\n\n' \ + ' \n nova_quota:\n properties:\n injected_files: 111\n ' \ + 'instances: 111\n keypairs: 111\n ram: 111\n ' \ + 'tenant: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'type: OS::Nova::Quota\n\n \n tenant_metadata:\n properties:\n METADATA:\n ' \ + ' metadata:\n my_server_name: Apache1\n ocx_cust: 123456889\n ' \ + 'TENANT_ID: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'type: OS::Keystone::Metadata\n\n \n userId1:\n' \ + ' properties:\n groups:\n ' \ + '- {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1_group}\n name: userId1\n' \ + ' roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' role: admin\n ' \ + '- project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: other\n type: OS::Keystone::User\n\n' \ + ' \n userId2:\n properties:\n groups:\n' \ + ' - {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2_group}\n ' \ + 'name: userId2\n roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' role: storage\n type: OS::Keystone::User\n\n ' \ + '\n 1e24981a-fa51-11e5-86aa-5e5517507c66:\n properties:\n description: this is a description\n' \ + ' enabled: true\n name: welcome_man\n' \ + ' project_id: 1e24981a-fa51-11e5-86aa-5e5517507c66\n ' \ + 'type: OS::Keystone::Project2\n\n \n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1_group:\n' \ + ' properties:\n description: dummy\n domain: default\n' \ + ' name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1_group\n roles:\n' \ + ' - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: {get_resource: other}\n type: OS::Keystone::Group\n\n ' \ + '\n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2_group:\n properties:\n ' \ + 'description: dummy\n domain: default\n name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2_group\n ' \ + 'roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: {get_resource: storage}\n type: OS::Keystone::Group\n\n ' \ + '\n\noutputs:\n userId1_id:\n ' \ + 'value: {get_resource: userId1}\n userId2_id:\n ' \ + 'value: {get_resource: userId2}\n 1e24981a-fa51-11e5-86aa-5e5517507c66_id:\n ' \ + 'value: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' + +full_yaml_quotas = 'heat_template_version: 2015-1-1\n\ndescription: yaml file for region - ' \ + 'regionnametest\n\nresources:\n cinder_quota:\n ' \ + 'properties:\n gigabytes: 10\n snapshots: 10\n ' \ + 'tenant: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n volumes: 10\n ' \ + 'type: OS::Cinder::Quota\n\n \n neutron_quota:\n ' \ + 'properties:\n floatingip: 10\n network: 10\n ' \ + 'port: 10\n router: 10\n subnet: 10\n ' \ + 'tenant: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'type: OS::Neutron::Quota\n\n \n nova_quota:\n ' \ + 'properties:\n injected_files: 10\n instances: 10\n ' \ + 'keypairs: 10\n ram: 10\n tenant: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'type: OS::Nova::Quota\n\n \n tenant_metadata:\n ' \ + 'properties:\n METADATA:\n metadata:\n my_server_name: Apache1\n' \ + ' ocx_cust: 123456889\n TENANT_ID: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' type: OS::Keystone::Metadata\n\n \n userId1zzzz:\n properties:\n ' \ + 'groups:\n - {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1zzzz_group}\n ' \ + 'name: userId1zzzz\n roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' role: adminzzzz\n - project: ' \ + '{get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n role: ' \ + 'otherzzzzz\n type: OS::Keystone::User\n\n \n ' \ + 'userId2zzz:\n properties:\n groups:\n - {get_resource:' \ + ' 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2zzz_group}\n name: userId2zzz\n roles:\n' \ + ' - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: storagezzzzz\n type: OS::Keystone::User\n\n' \ + ' \n 1e24981a-fa51-11e5-86aa-5e5517507c66:\n properties:\n ' \ + 'description: this is a description\n ' \ + 'enabled: true\n name: welcome_man\n ' \ + 'project_id: 1e24981a-fa51-11e5-86aa-5e5517507c66\n ' \ + 'type: OS::Keystone::Project2\n\n \n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1zzzz_group:\n' \ + ' properties:\n description: dummy\n ' \ + 'domain: default\n name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId1zzzz_group\n roles:\n ' \ + '- project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: {get_resource: otherzzzzz}\n type: OS::Keystone::Group\n\n' \ + ' \n 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2zzz_group:\n properties:\n ' \ + 'description: dummy\n domain: default\n name: 1e24981a-fa51-11e5-86aa-5e5517507c66_userId2zzz_group\n' \ + ' roles:\n - project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: {get_resource: storagezzzzz}\n type: OS::Keystone::Group\n\n' \ + ' \n\noutputs:\n userId1zzzz_id:\n ' \ + 'value: {get_resource: userId1zzzz}\n userId2zzz_id:\n ' \ + 'value: {get_resource: userId2zzz}\n 1e24981a-fa51-11e5-86aa-5e5517507c66_id:\n ' \ + 'value: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' + +full_yaml_ldap = 'heat_template_version: 2015-1-2\n\ndescription: yaml file' \ + ' for region - regionname\n\nresources:\n tenant_metadata:\n ' \ + 'properties:\n METADATA:\n metadata:\n my_server_name: Apache1\n' \ + ' ocx_cust: 123456889\n TENANT_ID: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' \ + ' type: OS::Keystone::Metadata\n\n \n userId1:\n ' \ + 'properties:\n roles:\n ' \ + '- project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: admin\n - project: ' \ + '{get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: other\n user: userId1\n ' \ + 'type: OS::Keystone::UserRoleAssignment\n\n \n ' \ + 'userId2:\n properties:\n roles:\n ' \ + '- project: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n ' \ + 'role: storage\n user: userId2\n ' \ + 'type: OS::Keystone::UserRoleAssignment\n\n \n ' \ + '1e24981a-fa51-11e5-86aa-5e5517507c66:\n properties:\n ' \ + 'description: this is a description\n ' \ + 'enabled: true\n name: welcome_man\n ' \ + 'project_id: 1e24981a-fa51-11e5-86aa-5e5517507c66\n ' \ + 'type: OS::Keystone::Project2\n\n \n\noutputs:\n ' \ + 'userId1_id:\n ' \ + 'value: {get_resource: userId1}\n userId2_id:\n ' \ + 'value: {get_resource: userId2}\n 1e24981a-fa51-11e5-86aa-5e5517507c66_id:\n ' \ + 'value: {get_resource: 1e24981a-fa51-11e5-86aa-5e5517507c66}\n' + + +class CreateResource(unittest.TestCase): + """class metohd.""" + + @patch.object(CustomerBuild, 'conf') + def test_create_customer_yaml_nousers(self, mock_conf): + """test valid dict to yaml output as expected without users.""" + ver = mock_conf.yaml_configs.customer_yaml.yaml_version = '2015-1-1' + mock_conf.yaml_configs.customer_yaml.yaml_options.quotas = False + yamlfile = CustomerBuild.yamlbuilder(alldata, region_quotas) + yamlfile_as_json = yaml.load(yamlfile) + self.assertEqual(yamlfile_as_json['heat_template_version'], ver) + self.assertEqual(yaml.load(yamlfile), yaml.load(fullyaml_no_users_quotasoff)) + + @patch.object(CustomerBuild, 'conf') + def test_create_flavor_yaml_noquotas(self, mock_conf): + """test valid dict to yaml output as expected with users.""" + ver = mock_conf.yaml_configs.customer_yaml.yaml_version = '2015-1-2' + mock_conf.yaml_configs.customer_yaml.yaml_options.quotas = False + yamlfile = CustomerBuild.yamlbuilder(alldata, region_users) + yamlfile_as_json = yaml.load(yamlfile) + self.assertEqual(yamlfile_as_json['heat_template_version'], ver) + self.assertEqual(yaml.load(yamlfile), yaml.load(fullyaml_with_users_quotasoff)) + + @patch.object(CustomerBuild, 'conf') + def test_create_customer_yaml_noquotas_on(self, mock_conf): + """test valid dict to yaml output as expected with default regions.""" + ver = mock_conf.yaml_configs.customer_yaml.yaml_version = '2015-1-1' + mock_conf.yaml_configs.customer_yaml.yaml_options.quotas = True + yamlfile = CustomerBuild.yamlbuilder(alldata, region_users) + yamlfile_as_json = yaml.load(yamlfile) + self.assertEqual(yamlfile_as_json['heat_template_version'], ver) + self.assertEqual(yaml.load(yamlfile), yaml.load(full_yaml_default_quotas)) + + @patch.object(CustomerBuild, 'conf') + def test_create_customer_yaml_withquotas_on(self, mock_conf): + """valid dict to yaml output as expect with regions default users.""" + ver = mock_conf.yaml_configs.customer_yaml.yaml_version = '2015-1-1' + mock_conf.yaml_configs.customer_yaml.yaml_options.quotas = True + yamlfile = CustomerBuild.yamlbuilder(alldata, region_quotas) + yamlfile_as_json = yaml.load(yamlfile) + self.assertEqual(yamlfile_as_json['heat_template_version'], ver) + self.assertEqual(yaml.load(yamlfile), yaml.load(full_yaml_quotas)) + + @patch.object(CustomerBuild, 'conf') + def test_create_flavor_yaml_ldap(self, mock_conf): + """test valid dict to yaml output as expected with ldap system.""" + ver = mock_conf.yaml_configs.customer_yaml.yaml_version = '2015-1-2' + mock_conf.yaml_configs.customer_yaml.yaml_options.quotas = False + mock_conf.yaml_configs.customer_yaml.yaml_options.type = "ldap" + yamlfile = CustomerBuild.yamlbuilder(alldata, region_users) + yamlfile_as_json = yaml.load(yamlfile) + self.assertEqual(yamlfile_as_json['heat_template_version'], ver) + self.assertEqual(yaml.load(yamlfile), yaml.load(full_yaml_ldap)) diff --git a/orm/services/resource_distributor/rds/tests/services/test_flavor_yaml.py b/orm/services/resource_distributor/rds/tests/services/test_flavor_yaml.py new file mode 100755 index 00000000..e1de0e3f --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/services/test_flavor_yaml.py @@ -0,0 +1,87 @@ +"""flavor unittest module.""" +from mock import patch +from rds.services import yaml_flavor_bulder as FlavorBuild +import unittest +import yaml + + +alldata = {'status': 'complete', 'series': 'P2', + 'description': 'First flavor for AMAR', + 'ephemeral': 1, 'ram': 64, 'visibility': 'public', + 'regions': [{'name': 'North1'}, {'name': 'North2'}], 'vcpus': 2, + 'extra_specs': {'key2:aa': 'value2', 'key1': 'value1', + 'keyx': 'valuex'}, + 'tag': {'tagkey2': 'tagvalue2', 'tagkey1': 'tagvalue1'}, + 'options': {'optkey2': 'optvalue2', 'optkey1': 'optvalue1'}, + 'swap': 51231, 'disk': 512, + 'tenants': [{'tenant_id': 'abcd-efgh-ijkl-4567'}, + {'tenant_id': 'abcd-efgh-ijkl-4567'}], + 'id': 'uuid-uuid-uuid-uuid', + 'name': 'Nice Flavor'} + +region = {'name': '0'} + + +fullyaml = 'heat_template_version: 2015-1-1\n\ndescription: yaml file for region - 0\n\nresources:\n' \ + ' nova_flavor:\n properties:\n disk: 512\n ephemeral: 1\n' \ + ' extra_specs: {key1: value1, "key2:aa": value2, keyx: valuex, tagkey1: tagvalue1, ' \ + 'tagkey2: tagvalue2, optkey1: optvalue1, optkey2: optvalue2}\n' \ + ' flavorid: uuid-uuid-uuid-uuid\n' \ + ' is_public: true\n name: Nice Flavor\n ram: 64\n rxtx_factor: 1\n' \ + ' swap: 51231\n tenants: [abcd-efgh-ijkl-4567, abcd-efgh-ijkl-4567]\n vcpus: 2\n' \ + ' type: OS::Nova::Flavor\n\n \n\noutputs:\n nova_flavor_id:\n' \ + ' value: {get_resource: nova_flavor}\n' + +alldata_rxtffactor = {'status': 'complete', 'series': 'P2', + 'description': 'First flavor for AMAR', + 'ephemeral': 1, 'ram': 64, 'visibility': 'public', + 'regions': [{'name': 'North1'}, {'name': 'North2'}], + 'vcpus': 2, + 'extra_specs': {'key2': 'value2', 'key1': 'value1', + 'keyx': 'valuex'}, + 'tag': {'tagkey2': 'tagvalue2', 'tagkey1': 'tagvalue1'}, + 'options': {'optkey2': 'optvalue2', 'optkey1': 'optvalue1'}, + 'swap': 51231, 'disk': 512, + 'tenants': [{'tenant_id': 'abcd-efgh-ijkl-4567'}, + {'tenant_id': 'abcd-efgh-ijkl-4567'}], + 'id': 'uuid-uuid-uuid-uuid', + 'rxtx_factor': 10, + 'name': 'Nice Flavor'} + +fullyaml_rxtx = 'heat_template_version: 2015-1-1\n\ndescription: yaml file for region - 0\n\nresources:\n' \ + ' nova_flavor:\n properties:\n disk: 512\n ephemeral: 1\n' \ + ' extra_specs: {key1: value1, key2: value2, keyx: valuex, tagkey1: tagvalue1, ' \ + 'tagkey2: tagvalue2, optkey1: optvalue1, optkey2: optvalue2}\n' \ + ' flavorid: uuid-uuid-uuid-uuid\n' \ + ' is_public: true\n name: Nice Flavor\n ram: 64\n rxtx_factor: 10\n' \ + ' swap: 51231\n tenants: [abcd-efgh-ijkl-4567, abcd-efgh-ijkl-4567]\n vcpus: 2\n' \ + ' type: OS::Nova::Flavor\n\n \n\noutputs:\n nova_flavor_id:\n' \ + ' value: {get_resource: nova_flavor}\n' + + +class CreateResource(unittest.TestCase): + """class method flavor tests.""" + + @patch.object(FlavorBuild, 'conf') + def test_create_flavor_yaml(self, mock_conf): + self.maxDiff=None + """test valid dict to yaml output as expected.""" + mock_conf.yaml_configs.flavor_yaml.yaml_version = '2015-1-1' + mock_conf.yaml_configs.flavor_yaml.yaml_args.rxtx_factor = 1 + yamlfile = FlavorBuild.yamlbuilder(alldata, region) + yamlfile_as_json = yaml.load(yamlfile) + self.assertEqual(yamlfile_as_json['heat_template_version'], + mock_conf.yaml_configs.flavor_yaml.yaml_version) + self.assertEqual(yaml.load(fullyaml), yamlfile_as_json) + + @patch.object(FlavorBuild, 'conf') + def test_create_flavor_yaml_(self, mock_conf): + self.maxDiff = None + """test when extx ioncluded in the input.""" + mock_conf.yaml_configs.flavor_yaml.yaml_version = '2015-1-1' + mock_conf.yaml_configs.flavor_yaml.yaml_args.rxtx_factor = 1 + yamlfile = FlavorBuild.yamlbuilder(alldata_rxtffactor, region) + yamlfile_as_json = yaml.load(yamlfile) + self.assertEqual(yamlfile_as_json['heat_template_version'], + mock_conf.yaml_configs.flavor_yaml.yaml_version) + self.assertEqual(yaml.load(fullyaml_rxtx), yamlfile_as_json) diff --git a/orm/services/resource_distributor/rds/tests/services/test_image_yaml.py b/orm/services/resource_distributor/rds/tests/services/test_image_yaml.py new file mode 100755 index 00000000..daebc399 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/services/test_image_yaml.py @@ -0,0 +1,52 @@ +import unittest +from mock import patch +from rds.services import yaml_image_builder as ImageBuild +import yaml +import datetime + +json_input = {'status': 'complete', 'name': 'Ubuntu', 'internal_id': 1, + 'url': 'https://mirrors.it.att.com/images/image-name', + 'disk_format': 'raw', 'min_ram': 0, 'enabled': 1, + 'visibility': 'public', 'owner': 'unknown', 'image_tags': [ + {'image_internal_id': 1, 'tag': 'abcd-efgh-ijkl-4567'}, + {'image_internal_id': 1, 'tag': 'abcd-efgh-ijkl-4567'}], 'regions': [ + {'action': 'delete', 'image_internal_id': 1, 'type': 'single', + 'name': 'North'}, + {'action': 'create', 'image_internal_id': 1, 'type': 'single', + 'name': 'North'}], 'image_properties': [ + {'key_name': 'Key1', 'key_value': 'Key1.value', + 'image_internal_id': 1}, + {'key_name': 'Key2', 'key_value': 'Key2.value', + 'image_internal_id': 1}], 'protected': 1, 'customers': [ + {'customer_id': 'abcd-efgh-ijkl-4567', 'image_id': 1}, + {'customer_id': 'abcd-efgh-ijkl-4567', 'image_id': 1}], + 'container_format': 'bare', 'min_disk': 2, + 'id': 'uuu1id12-uuid-uuid-uuid'} + +region = {'action': 'delete', 'image_internal_id': 1, 'type': 'single', + 'name': 'North'} + +yaml_output = {'description': 'yaml file for region - North', + 'resources': {'glance_image': {'properties': {'container_format': 'bare', + 'disk_format': 'raw', + 'is_public': True, + 'copy_from': 'https://mirrors.it.att.com/images/image-name', + 'min_disk': 2, + 'min_ram': 0, + 'name': 'North', + 'owner': 'unknown', + 'protected': True, + 'tenants': ['abcd-efgh-ijkl-4567', 'abcd-efgh-ijkl-4567']}, + 'type': 'OS::Glance::Image2'}}, + 'heat_template_version': '2015-1-1', + 'outputs': {'glance_image_id': {'value': {'get_resource': 'glance_image'}}}} + +class CreateImage(unittest.TestCase): + """class method image test.""" + + @patch.object(ImageBuild, 'conf') + def test_create_image(self, mock_conf): + self.maxDiff = None + mock_conf.yaml_configs.image_yaml.yaml_version = '2015-1-1' + response = ImageBuild.yamlbuilder(json_input, region) + self.assertEqual(yaml.load(response), yaml_output) diff --git a/orm/services/resource_distributor/rds/tests/services/test_region_resource_id_status.py b/orm/services/resource_distributor/rds/tests/services/test_region_resource_id_status.py new file mode 100755 index 00000000..623623b4 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/services/test_region_resource_id_status.py @@ -0,0 +1,170 @@ +from rds.tests import config as conf +import mock +import time +import unittest + +from rds.services import region_resource_id_status + + +class MyResult(object): + def __init__(self, resource_type, status, timestamp): + self.resource_type = resource_type + self.status = status + self.timestamp = timestamp + + +class MockClass(object): + def __init__(self, regions): + self.regions = regions + self.done = False + + def __call__(self, *args, **kwargs): + return self + + def get_records_by_filter_args(self, **kw): + return self + + def add_update_status_record(self, *args): + self.done = True + + +class TestModel(unittest.TestCase): + def setUp(self): + region_resource_id_status.config = conf.region_resource_id_status + + self.temp_connection = region_resource_id_status.factory.get_region_resource_id_status_connection + + # Save the original config + self.temp_config = region_resource_id_status.config + + def tearDown(self): + # Restore the original config + region_resource_id_status.config = self.temp_config + + region_resource_id_status.factory.get_region_resource_id_status_connection = self.temp_connection + + def test_validate_status_value_sanity(self): + test_status = 'test' + region_resource_id_status.config['allowed_status_values'].add(test_status) + # Make sure that no exception is raised + region_resource_id_status.validate_status_value(test_status) + + def test_validate_status_value_invalid_status(self): + test_status = 'test' + if test_status in region_resource_id_status.config['allowed_status_values']: + region_resource_id_status.config['allowed_status_values'].remove(test_status) + + self.assertRaises(region_resource_id_status.InputError, + region_resource_id_status.validate_status_value, + test_status) + + def test_validate_operation_type_sanity(self): + test_operation = 'test' + region_resource_id_status.config['allowed_operation_type'] = {test_operation: 'A'} + # Make sure that no exception is raised + region_resource_id_status.validate_operation_type(test_operation) + + def test_validate_operation_type_invalid_operation(self): + test_operation = 'test' + region_resource_id_status.config['allowed_operation_type'] = {} + + self.assertRaises(region_resource_id_status.InputError, + region_resource_id_status.validate_operation_type, + test_operation) + + def test_validate_resource_type_sanity(self): + test_resource = 'test' + region_resource_id_status.config['allowed_resource_type'] = {test_resource: 'A'} + # Make sure that no exception is raised + region_resource_id_status.validate_resource_type(test_resource) + + def test_validate_resource_type_invalid_resource(self): + test_resource = 'test' + region_resource_id_status.config['allowed_resource_type'] = {} + + self.assertRaises(region_resource_id_status.InputError, + region_resource_id_status.validate_resource_type, + test_resource) + + @mock.patch.object(region_resource_id_status.factory, 'get_region_resource_id_status_connection') + def test_get_regions_by_status_resource_id_sanity(self, mock_factory): + # Make sure that no exception is raised + region_resource_id_status.get_regions_by_status_resource_id(1, 2) + + @mock.patch.object(region_resource_id_status.factory, 'get_region_resource_id_status_connection') + def test_get_status_by_resource_id_sanity(self, mock_factory): + # Make sure that no exception is raised + region_resource_id_status.get_status_by_resource_id(1) + + def test_add_status_sanity(self): + test_resource = 'test' + region_resource_id_status.config['allowed_resource_type'] = {test_resource: 'A'} + test_status = 'test' + region_resource_id_status.config['allowed_status_values'].add(test_status) + test_operation = 'test' + region_resource_id_status.config['allowed_operation_type'] = {test_operation: 'A'} + + temp_mock = MockClass(['test']) + region_resource_id_status.factory.get_region_resource_id_status_connection = temp_mock + region_resource_id_status.add_status({'timestamp': 1, + 'region': 2, + 'status': test_status, + 'transaction_id': 4, + 'resource_id': 5, + 'ord_notifier_id': 6, + 'error_msg': 7, + 'error_code': 8, + 'resource_operation': test_operation, + 'resource_type': test_resource}) + self.assertTrue(temp_mock.done) + + def test_add_status_no_regions(self): + test_resource = 'test' + region_resource_id_status.config['allowed_resource_type'] = {test_resource: 'A'} + test_status = 'test' + region_resource_id_status.config['allowed_status_values'].add(test_status) + test_operation = 'test' + region_resource_id_status.config['allowed_operation_type'] = {test_operation: 'A'} + + temp_mock = MockClass([]) + region_resource_id_status.factory.get_region_resource_id_status_connection = temp_mock + region_resource_id_status.add_status({'timestamp': 1, + 'region': 2, + 'status': test_status, + 'transaction_id': 4, + 'resource_id': 5, + 'ord_notifier_id': 6, + 'error_msg': 7, + 'error_code': 8, + 'resource_operation': test_operation, + 'resource_type': test_resource}) + self.assertTrue(temp_mock.done) + + def test_add_status_input_error(self): + test_resource = 'test' + region_resource_id_status.config['allowed_resource_type'] = {test_resource: 'A'} + test_status = 'test' + region_resource_id_status.config['allowed_status_values'].add(test_status) + test_operation = 'test' + region_resource_id_status.config['allowed_operation_type'] = {test_operation: 'A'} + + temp_mock = MockClass([]) + region_resource_id_status.factory.get_region_resource_id_status_connection = temp_mock + self.assertRaises(region_resource_id_status.InputError, + region_resource_id_status.add_status, + {'timestamp': 1, 'region': 2, 'status': 3, + 'transaction_id': 4, 'resource_id': 5, + 'ord_notifier_id': 6, 'error_msg': 7, + 'error_code': 8, 'resource_operation': test_operation, + 'resource_type': test_resource}) + + def test_add_status_other_exception(self): + test_status = 'test' + region_resource_id_status.config['allowed_status_values'].add(test_status) + + temp_mock = MockClass([]) + region_resource_id_status.factory.get_region_resource_id_status_connection = temp_mock + self.assertRaises(KeyError, region_resource_id_status.add_status, + {'timestamp': 1, 'region': 2, 'status': test_status, + 'transaction_id': 4, 'resource_id': 5, 'ord_notifier_id': 6, + 'error_msg': 7, 'error_code': 8, 'resource_type': 9}) diff --git a/orm/services/resource_distributor/rds/tests/sot/__init__.py b/orm/services/resource_distributor/rds/tests/sot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/sot/git_sot/__init__.py b/orm/services/resource_distributor/rds/tests/sot/git_sot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_base.py b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_base.py new file mode 100644 index 00000000..34952062 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_base.py @@ -0,0 +1,62 @@ +import subprocess +import unittest +import mock +from mock import patch + +from rds.sot.git_sot import git_base +from rds.sot.git_sot.git_base import BaseGit, GitResetError + + +class BaseGitTests(unittest.TestCase): + + def test_git_base_no_method_git_init_implemented(self): + """ Check if creating an instance and calling git_init method fail""" + with self.assertRaises(NotImplementedError): + base_git = BaseGit() + base_git.git_init() + + def test_git_base_no_method_git_upload_changes_implemented(self): + """ Check if creating an instance and calling git_upload_changes method fail""" + with self.assertRaises(NotImplementedError): + base_git = BaseGit() + base_git.git_upload_changes() + + # @patch.object(git_base, 'conf') + # @patch.object(subprocess, 'Popen') + # def test_git_base_reset_error(self, mock_popen, conf_mock): + # """ Test that exception raised when stderr returns error.""" + # my_pipe = mock.MagicMock() + # my_pipe.communicate = mock.MagicMock(return_value=('1', 'error',)) + # mock_popen.return_value = my_pipe + # + # base_git = BaseGit() + # callback = base_git.git_reset_changes + # self.assertRaises(GitResetError, callback) + + # @patch.object(git_base, 'conf') + # @patch.object(subprocess, 'Popen') + # def test_git_base_reset_no_error(self, mock_popen, conf_mock): + # """ Test that no exception raised when no error returned.""" + # my_pipe = mock.MagicMock() + # my_pipe.communicate = mock.MagicMock(return_value=('1', 'bla bla',)) + # mock_popen.return_value = my_pipe + # + # base_git = BaseGit() + # try: + # base_git.git_reset_changes() + # except GitResetError: + # self.fail("No exception should be raised here") + + def test_git_base_no_method_git_reset_changes_implemented(self): + """ Check if creating an instance and calling + git_reset_changes method fail""" + with self.assertRaises(NotImplementedError): + base_git = BaseGit() + base_git.git_reset_changes() + + def test_git_base_no_method_validate_git_implemented(self): + """ Check if creating an instance and calling validate_git method fail""" + with self.assertRaises(NotImplementedError): + base_git = BaseGit() + base_git.validate_git() + diff --git a/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_factory.py b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_factory.py new file mode 100644 index 00000000..abcb5744 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_factory.py @@ -0,0 +1,25 @@ +import unittest + +from rds.sot.git_sot import git_factory +from rds.sot.git_sot.git_gittle import GitGittle +from rds.sot.git_sot.git_native import GitNative + + +class GitFactoryTests(unittest.TestCase): + def setUp(self): + super(GitFactoryTests, self).setUp() + + def test_get_git_impl_with_gittle(self): + """Test that when given gittle the GitGittle instance returned""" + obj = git_factory.get_git_impl("gittle") + self.assertIsInstance(obj, GitGittle) + + def test_get_git_impl_with_native(self): + """Test that when given native the GitNative instance returned""" + obj = git_factory.get_git_impl("native") + self.assertIsInstance(obj, GitNative) + + def test_get_sot_no_sot_type(self): + """Test that when given unknown type, exception raised""" + with self.assertRaises(RuntimeError): + git_factory.get_git_impl("unknown") \ No newline at end of file diff --git a/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_gittle.py b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_gittle.py new file mode 100644 index 00000000..709e1212 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_gittle.py @@ -0,0 +1,56 @@ +import mock +from mock import patch + +import unittest + +from rds.sot.git_sot import git_gittle +from rds.sot.git_sot.git_gittle import GitGittle +from rds.sot.git_sot.git_base import GitInitError, GitUploadError + + +class GitGittleTests(unittest.TestCase): + + def setUp(self): + super(GitGittleTests, self).setUp() + self.addCleanup(mock.patch.stopall) + self.my_git = GitGittle() + + def tearDown(self): + # Restore the original config + self.my_git.repo = None + + ################### + # git_init # + ################### + + @patch.object(git_gittle, 'Gittle', side_effect=Exception("Failed to delete file path")) + def test_git_gittle_init_git_create_gittle_failed(self, gittle_mock): + """Test that when Gittle fail to initialize exception is raised.""" + with self.assertRaises(GitInitError): + self.my_git.git_init() + + @patch.object(git_gittle, 'Gittle') + @patch.object(git_gittle, 'conf') + def test_git_gittle_init_git_create_gittle_success(self, gittle_mock, conf_mock): + """Test that when Gittle initialize success.""" + self.my_git.git_init() + + ###################### + # git_upload_changes # + ###################### + + @patch.object(git_gittle, 'conf') + def test_git_gittle_git_upload_changes_success(self, conf_mock): + """Test that when upload success commit id is returned.""" + self.my_git.repo = mock.MagicMock() + self.my_git.repo.commit = mock.MagicMock(return_value="123") + commit_id = self.my_git.git_upload_changes() + self.assertEqual(commit_id, "123") + + @patch.object(git_gittle, 'conf') + def test_git_gittle_git_upload_changes_commit_failed(self, conf_mock): + """Test that when upload failed exception raised.""" + self.my_git.repo = mock.MagicMock() + self.my_git.repo.commit = mock.MagicMock(side_effect=Exception("Failed to commit")) + self.assertRaises(GitUploadError, self.my_git.git_upload_changes) + diff --git a/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_native.py b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_native.py new file mode 100644 index 00000000..d1cf7938 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_native.py @@ -0,0 +1,92 @@ +"""Unittest module for git_native.""" +import mock +from mock import patch +import unittest + +from rds.sot.git_sot import git_native +from rds.sot.git_sot.git_native import GitNativeError, GitValidateError + + +class GitNativeTest(unittest.TestCase): + """The test case of GitNative.""" + + # @patch.object(git_native.subprocess, 'Popen') + # def test_git_operations_sanity(self, mock_popen): + # """Test that no exception is raised when performing git operations.""" + # my_pipe = mock.MagicMock() + # my_pipe.communicate = mock.MagicMock(return_value=('1', '2',)) + # mock_popen.return_value = my_pipe + # test_git = git_native.GitNative() + # for callback in [test_git._git_pull, test_git._git_add, + # test_git._git_push, test_git._git_get_commit_id]: + # callback('test') + # + # test_git._git_commit('test', 'test', 'test', 'test') + + # @patch.object(git_native.subprocess, 'Popen') + # def test_git_operations_error(self, mock_popen): + # """Test that an exception is raised when stderror returns error.""" + # my_pipe = mock.MagicMock() + # my_pipe.communicate = mock.MagicMock(return_value=('1', 'error',)) + # mock_popen.return_value = my_pipe + # test_git = git_native.GitNative() + # for callback in [test_git._git_pull, test_git._git_add, + # test_git._git_push, test_git._git_get_commit_id]: + # self.assertRaises(git_native.GitNativeError, callback, 'test') + # + # self.assertRaises(git_native.GitNativeError, + # test_git._git_commit, 'test', 'test', 'test', 'test') + + @patch.object(git_native, 'conf') + @patch.object(git_native.subprocess, 'Popen') + def test_git_init_sanity(self, mock_popen, mock_conf): + """Test that no exception is raised when calling git_init.""" + my_pipe = mock.MagicMock() + my_pipe.communicate = mock.MagicMock(return_value=('1', '2',)) + mock_popen.return_value = my_pipe + test_git = git_native.GitNative() + test_git.git_init() + + @patch.object(git_native, 'conf') + @patch.object(git_native.subprocess, 'Popen') + def test_git_upload_changes_sanity(self, mock_popen, mock_conf): + """Test that no exception is raised when calling git_upload_changes.""" + my_pipe = mock.MagicMock() + my_pipe.communicate = mock.MagicMock(return_value=('1', '2',)) + mock_popen.return_value = my_pipe + test_git = git_native.GitNative() + test_git.git_upload_changes() + + @patch.object(git_native, 'conf') + @patch.object(git_native.subprocess, 'Popen') + def test_git_upload_changes_error(self, mock_popen, mock_conf): + """Test that an exception is raised when stderror returns error.""" + my_pipe = mock.MagicMock() + my_pipe.communicate = mock.MagicMock(return_value=('1', 'error',)) + mock_popen.return_value = my_pipe + test_git = git_native.GitNative() + self.assertRaises(git_native.GitUploadError, + test_git.git_upload_changes) + + @patch.object(git_native, 'conf') + @patch.object(git_native.subprocess, 'Popen') + def test_git_validate_git_sanity(self, mock_popen, mock_conf): + """Test that no exception is raised when calling validate_git.""" + my_pipe = mock.MagicMock() + my_pipe.communicate = mock.MagicMock(return_value=('1', '2',)) + mock_popen.return_value = my_pipe + test_git = git_native.GitNative() + test_git.validate_git() + + @patch.object(git_native, 'conf') + @patch.object(git_native.subprocess, 'Popen') + @patch.object(git_native.GitNative, '_git_config', + side_effect=GitNativeError("Could not write to file")) + def test_git_native_validate_git_config_fail(self, conf,mock_popen, result): + """Test that no exception is raised when calling git_init.aein""" + my_pipe = mock.MagicMock() + my_pipe.communicate = mock.MagicMock(return_value=('1', '2',)) + mock_popen.return_value = my_pipe + test_git = git_native.GitNative() + with self.assertRaises(GitValidateError): + test_git.validate_git() \ No newline at end of file diff --git a/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_sot.py b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_sot.py new file mode 100755 index 00000000..764c46e5 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/sot/git_sot/test_git_sot.py @@ -0,0 +1,294 @@ +import os +import mock +import unittest +import threading +from mock import patch + +from rds.sot.git_sot import git_sot as sot +from rds.sot.base_sot import SoTError +from rds.sot.git_sot.git_base import GitUploadError, GitInitError, GitResetError +from rds.sot.git_sot.git_base import GitValidateError +from rds.sot import sot_factory + +from rds.tests import config as conf + +lock = mock.MagicMock() + +resource = { + "operation": "create", + "region_id": '1', + "resource_type": '2', + "resource_name": '3', + "template_data": '4' +} + +resource_delete= { + "operation": "delete", + "region_id": '1', + "resource_type": '2', + "resource_name": '3', + "template_data": '4' +} + + +def dummy_thread_method(): + pass + + +class GitSoTTest(unittest.TestCase): + + def setUp(self): + super(GitSoTTest, self).setUp() + self.addCleanup(mock.patch.stopall) + git_factory = mock.MagicMock() + git_factory.get_git_impl = mock.MagicMock() + + ################## + ### update_sot ### + ################## + + @patch.object(sot, 'init_git', side_effect=GitInitError("Failed to initialize Git")) + def test_git_sot_update_sot_init_git_fail(self, result): + """" init_git fails and raise exception""" + try: + sot.update_sot("", lock, '1', '2', ['3','5'], '4', '6') + except SoTError: + self.fail("Exception should have been handled inside method") + + @patch.object(sot, 'handle_file_operations', side_effect=SoTError("Failed to create file path")) + @patch.object(sot, 'init_git') + def test_git_sot_update_sot_create_files_fail(self, git_repo, result): + """" create_files fails and raise exception""" + try: + sot.update_sot("", lock, "a", "b", ['c', 'd'], 'e', 'f') + except SoTError: + self.fail("Exception should have been handled inside method") + + @patch.object(sot, 'update_git', side_effect=GitUploadError("Failed to upload file to Git")) + @patch.object(sot, 'init_git') + @patch.object(sot, 'handle_file_operations') + @patch.object(sot, 'cleanup') + def test_git_sot_update_sot_update_git_fail(self, git_repo, files_created, result, clean_result): + """" update git fails and raise exception""" + try: + sot.update_sot("",lock, 'a', 'b', ['c', 'd'], 'e', 'f') + except GitUploadError: + self.fail("Exception should have been handled inside method") + + @patch.object(sot, 'update_git') + @patch.object(sot, 'init_git') + @patch.object(sot, 'handle_file_operations') + @patch.object(sot, 'cleanup') + def test_git_sot_update_sot_success(self, git_repo, files_created, result, cleanup_result): + """"no exceptions raised""" + try: + sot.update_sot("", lock, "a", "b", ['c', 'd'], 'e', 'f') + except SoTError: + self.fail("Exception should have been handled inside method") + + ####################### + # create_dir # + ####################### + + @patch.object(os.path, 'dirname', return_value="File path") + @patch.object(os.path, 'exists', return_value=False) + @patch.object(os, 'makedirs') + def test_git_sot_create_dir_path_not_exist_success(self, dir_name, exists, makedirs): + """create directory path when path not exist and makedir success""" + sot.create_dir("my_file") + + @patch.object(os.path, 'dirname', return_value="File path") + @patch.object(os.path, 'exists', return_value=True) + def test_git_sot_create_dir_path_exists_success(self, dir_name, exists): + """create directory path when path not exist and makedir success""" + sot.create_dir("my_file") + + @patch.object(os.path, 'dirname', return_value="File path") + @patch.object(os.path, 'exists', return_value=False) + @patch.object(os, 'makedirs', side_effect=OSError("Could not make dir")) + def test_git_sot_create_dir_makedir_fails(self, dir_name, exists, makedirs): + """create direcory path makedir throws exception and the methos throws SoTError""" + with self.assertRaises(SoTError): + sot.create_dir("my_file") + + ############################ + # save_resource_to_sot # + ############################ + + @patch.object(threading, 'Thread', return_value=threading.Thread(target=dummy_thread_method)) + def test_git_sot_save_resource_to_sot(self, thread): + """Check the save runs in a new thread""" + sot_factory.sot_type = "git" + sot_factory.git_type = "gittle" + git_impl = mock.MagicMock() + sot = sot_factory.get_sot() + sot.save_resource_to_sot("t_id", "tk_id", [], "a_id", "u_id") + self.assertNotEqual(thread, threading.Thread.getName("main_thread")) + + + ################################ + # create_file_in_path # + ################################ + + @patch.object(sot, 'create_dir', return_value="File path") + @patch.object(sot, 'write_data_to_file', return_value=True) + def test_git_sot_create_file_in_path_success(self, aDir, aFile): + """create file in path success scenario no exception raised""" + try: + sot.create_file_in_path("path", "data") + except SoTError: + self.fail("No exceptions should be thrown in this case") + + @patch.object(sot, 'create_dir', side_effect=SoTError("Failed to create file path")) + def test_git_sot_create_file_in_path_create_dir_failed(self, aDir): + """create file in path fail, create file raise exception """ + with self.assertRaises(SoTError): + sot.create_file_in_path("path", "data") + + @patch.object(sot, 'create_dir', return_value="File path") + @patch.object(sot, 'write_data_to_file', side_effect=SoTError("Could not write to file")) + def test_git_sot_create_file_in_path_create_file_failed(self, aDir, aFile): + """create file in path fail,create dir success, writing data to file failed """ + with self.assertRaises(SoTError): + sot.create_file_in_path("path", "data") + + ############################# + # get_resource_file_path # + ############################# + + def test_git_sot_get_resource_file_path_failed(self): + """get_resource_file_path """ + sot_factory.sot_type = "git" + sot_factory.git_type = "native" + sot_factory.local_repository_path = conf.git["local_repository_path"] + sot_factory.relative_path_format = conf.git["relative_path_format"] + sot_factory.file_name_format = conf.git["file_name_format"] + sot_factory.get_sot() + + name = conf.git["file_name_format"].format(resource["resource_name"]) + path = conf.git["local_repository_path"] + \ + conf.git["relative_path_format"].format(resource["region_id"], + resource["resource_type"], + name) + + result_path = sot.get_resource_file_path(resource) + self.assertEqual(path, result_path) + + ############################# + # handle_file_operations # + ############################# + + # @patch.object(sot, 'create_file_in_path', return_value=True) + # def test_git_sot_handle_file_operations_success(self, result): + # """create files """ + # roll_list = [] + # sot.handle_file_operations([resource, ]) + # self.assertEqual(len(roll_list), 1) + # + # + # @patch.object(os, 'remove') + # @patch.object(os.path, 'exists', return_value=True) + # def test_git_sot_handle_file_operations_delete_success(self, + # result, + # result2): + # """delete files """ + # roll_list = [] + # sot.handle_file_operations([resource_delete, ], roll_list) + # self.assertEqual(len(roll_list), 1) + + + ############################# + # write_data_to_file # + ############################# + + @patch('__builtin__.open') + @patch.object(os, 'close') + @patch.object(os, 'write', return_value=True) + def test_git_sot_write_data_to_file_success(self, result1, result2, result3): + """Check create_file success """ + try: + sot.write_data_to_file("file_path", "data11") + except SoTError: + self.fail("No exceptions should be thrown in this case") + + @patch('__builtin__.open', side_effect=IOError("Failed writing data to file")) + def test_git_sot_write_data_to_file_failed(self, result1): + """Check create file failed , could not write to file """ + with self.assertRaises(SoTError): + sot.write_data_to_file("file_path", "data") + + ############################# + # cleanup # + ############################# + + def test_git_sot_cleanup_files_success(self): + """Check cleanup success """ + git_impl = mock.MagicMock() + git_impl.git_reset_changes = mock.MagicMock() + try: + sot.cleanup(git_impl) + except SoTError: + self.fail("No exceptions should be thrown in this case") + + def test_git_sot_cleanup_files_remove_failed(self): + """Check cleanup fail because reset git failed """ + git_impl = mock.MagicMock() + git_impl.git_reset_changes = mock.MagicMock(side_effect=GitResetError("failed to reset")) + with self.assertRaises(SoTError): + sot.cleanup(git_impl) + + ############################# + # init_git # + ############################# + + def test_git_sot_init_git_success(self): + """Check init_git success """ + git_impl = mock.MagicMock() + git_impl.git_init = mock.MagicMock() + try: + sot.init_git(git_impl) + except GitInitError: + self.fail("No exceptions should be thrown in this case") + + def test_git_sot_init_git_gittle_failed(self): + """Check init_git failed """ + git_impl = mock.MagicMock() + git_impl.git_init = mock.MagicMock(side_effect=GitInitError("failed to init")) + with self.assertRaises(GitInitError): + sot.init_git(git_impl) + + ############################# + # update_git # + ############################# + + def test_git_sot_update_git_success(self): + """Check update_git success""" + git_impl = mock.MagicMock() + git_impl.git_upload_changes = mock.MagicMock(return_value="123") + + commit_id = sot.update_git(git_impl) + self.assertEqual(commit_id, "123") + + def test_git_sot_update_git_commit_faild(self): + """Check update_git commit failed""" + git_impl = mock.MagicMock() + git_impl.git_upload_changes = mock.MagicMock(side_effect= + GitUploadError("Failed in upload")) + with self.assertRaises(GitUploadError): + sot.update_git(git_impl) + + ############################# + # validate_git # + ############################# + + def test_git_sot_validate_git_faild(self): + """Check validate_git failed""" + git_impl = mock.MagicMock() + git_impl.validate_git = mock.MagicMock(side_effect= + GitValidateError("Failed in upload")) + try: + sot.validate_git(git_impl, lock) + except GitInitError: + self.fail("No exceptions should be thrown in this case") + + diff --git a/orm/services/resource_distributor/rds/tests/sot/test_base_sot.py b/orm/services/resource_distributor/rds/tests/sot/test_base_sot.py new file mode 100644 index 00000000..bfad0531 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/sot/test_base_sot.py @@ -0,0 +1,20 @@ +import unittest + +from rds.sot.base_sot import BaseSoT + + +class BaseSoTTests(unittest.TestCase): + + def test_base_sot_no_method_save_implemented(self): + """ Check if creating an instance and calling save method fail""" + with self.assertRaises(Exception): + sot = BaseSoT() + sot.save_resource_to_sot('1','2',[]) + + def test_base_sot_no_method_validate_implemented(self): + """ Check if creating an instance and calling validate method fail""" + with self.assertRaises(Exception): + sot = BaseSoT() + sot.validate_sot_state() + + diff --git a/orm/services/resource_distributor/rds/tests/sot/test_sot_factory.py b/orm/services/resource_distributor/rds/tests/sot/test_sot_factory.py new file mode 100644 index 00000000..8c1e6ef5 --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/sot/test_sot_factory.py @@ -0,0 +1,54 @@ +import unittest +import mock + +from rds.sot import sot_factory +from rds.sot.git_sot.git_sot import GitSoT + + +class SoTFactoryTests(unittest.TestCase): + def setUp(self): + super(SoTFactoryTests, self).setUp() + self.addCleanup(mock.patch.stopall) + git_factory = mock.MagicMock() + git_factory.get_git_impl = mock.MagicMock() + + def test_get_sot_no_sot_type(self): + """Check that a runtime error is raised if no git type + is available from config""" + sot_factory.sot_type = "" + with self.assertRaises(RuntimeError): + sot_factory.get_sot() + + def test_get_sot_git_type(self): + """ Check that when 'git' type is provided the returned object + is instance of GiTSoT""" + sot_factory.sot_type = "git" + obj = sot_factory.get_sot() + self.assertIsInstance(obj, GitSoT) + + def test_get_sot_git_sot_params(self): + sot_factory.sot_type = "git" + sot_factory.local_repository_path = "2" + sot_factory.relative_path_format = "3" + sot_factory.commit_message_format = "4" + sot_factory.commit_user = "5" + sot_factory.commit_email = "6" + sot_factory.git_server_url = "7" + sot_factory.git_type = "gittle" + + obj = sot_factory.get_sot() + self.assertEqual(GitSoT.local_repository_path, "2", "local_repository_path not match") + self.assertEqual(GitSoT.relative_path_format, "3", "relative_path_format not match") + self.assertEqual(GitSoT.commit_message_format, "4", "commit_message_format not match") + self.assertEqual(GitSoT.commit_user, "5", "commit_user not match") + self.assertEqual(GitSoT.commit_email, "6", "commit_email not match") + self.assertEqual(GitSoT.git_server_url, "7", "git_server_url not match") + self.assertEqual(GitSoT.git_type, "gittle", "git_type not match") + + + + + + + + diff --git a/orm/services/resource_distributor/rds/tests/storage/__init__.py b/orm/services/resource_distributor/rds/tests/storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/storage/mysql/__init__.py b/orm/services/resource_distributor/rds/tests/storage/mysql/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/storage/mysql/test_region_resource_id_status.py b/orm/services/resource_distributor/rds/tests/storage/mysql/test_region_resource_id_status.py new file mode 100755 index 00000000..def02faa --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/storage/mysql/test_region_resource_id_status.py @@ -0,0 +1,215 @@ +"""Unittest module for mysql.region_resource_id_status.""" +import time + +import mock +from mock import patch + +from rds.storage.mysql import region_resource_id_status +import unittest + + +class RecordMock(object): + def __init__(self, record=None): + self._record = record + self.timestamp = 0 + self.status = "Submitted" + self.err_msg = "test" + self.region = "1" + self.transaction_id = "2" + self.resource_id = "3" + self.ord_notifier = "4" + self.err_code = 1 + self.operation = "create" + self.resource_extra_metadata = None + + def first(self): + return self._record + + def delete(self): + return + + +class MyFacade(object): + """Mock EngineFacade class.""" + + def __init__(self, dup_entry=False, + record_exist=False, + is_get_records=False): + """Initialize the object.""" + self._is_dup_entry = dup_entry + self._is_record_exist = record_exist + self._is_get_records = is_get_records + + def get_session(self): + + session = mock.MagicMock() + if self._is_dup_entry: + dup_ent = region_resource_id_status.oslo_db.exception.DBDuplicateEntry + session.add = mock.MagicMock(side_effect=dup_ent('test')) + + records = None + my_record = RecordMock() + if self._is_record_exist: + my_record = RecordMock(mock.MagicMock()) + records = [RecordMock()] + + my_filter = mock.MagicMock() + if not self._is_get_records: + my_filter.filter_by = mock.MagicMock(return_value=my_record) + else: + my_filter.filter_by = mock.MagicMock(return_value=records) + + session.query = mock.MagicMock(return_value=my_filter) + + return session + + +class MysqlRegionResourceIdStatusTest(unittest.TestCase): + """Main test case of this module.""" + + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(False, True)) + def test_add_update_status_record_record_exist_sanity(self, mock_db_session): + """Test that no exception is raised when calling add_update_status_record. + where record exist""" + my_connection = region_resource_id_status.Connection('url') + my_connection.add_update_status_record('timestamp', + 'region', + 'status', + 'transaction_id', + 'resource_id', + 'ord_notifier', + 'err_msg', + 'err_code', + {"checksum": "1", + "virtual_size": "2", + "size": "3"}) + + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade()) + def test_add_update_status_record_record_not_exist_sanity(self, mock_db_session): + """Test that no exception is raised when calling add_update_status_record. + where record does not exist""" + my_connection = region_resource_id_status.Connection('url') + my_connection.add_update_status_record('timestamp', + 'region', + 'status', + 'transaction_id', + 'resource_id', + 'ord_notifier', + 'err_msg', + 'create', + 'err_code') + + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(True,False)) + def test_add_update_status_record_duplicate_entry(self, mock_db_session): + """No exception is raised when trying to add a duplicate entry.""" + my_connection = region_resource_id_status.Connection('url') + my_connection.add_update_status_record('timestamp', + 'region', + 'status', + 'transaction_id', + 'resource_id', + 'ord_notifier', + 'err_msg', + 'delete', + 'err_code') + + @mock.patch.object(region_resource_id_status, 'StatusModel') + @patch.object(region_resource_id_status.Connection, + 'get_timstamp_pair', + return_value=(1,2)) + @mock.patch.object(region_resource_id_status, 'Model') + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(False,False,True)) + def test_get_records_by_filter_args_no_records(self, mock_db_session, + mock_get_timestamp, + mock_model, + mock_statusmodel): + """Test that the function returns None when it got no records.""" + my_connection = region_resource_id_status.Connection('url') + self.assertIsNone(my_connection.get_records_by_filter_args()) + + @mock.patch.object(region_resource_id_status, 'StatusModel') + @patch.object(region_resource_id_status.Connection, + 'get_timstamp_pair', + return_value=(1, 2)) + @mock.patch.object(region_resource_id_status, 'Model') + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(False, True, True)) + def test_get_records_by_filter_args_with_records(self, mock_db_session, + mock_get_timestamp, + mock_model, + mock_statusmodel): + """Test that the function returns None when it got records.""" + my_connection = region_resource_id_status.Connection('url') + my_connection.get_records_by_filter_args() + + @mock.patch.object(region_resource_id_status, 'StatusModel') + @patch.object(region_resource_id_status.Connection, + 'get_timstamp_pair', + return_value=(1, 2)) + @mock.patch.object(region_resource_id_status, 'Model') + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(False, False, True)) + def test_get_records_by_resource_id_sanity(self, mock_db_session, + mock_get_timestamp, + mock_model, + mock_statusmodel): + """No exception is raised when calling get_records_by_resource_id.""" + my_connection = region_resource_id_status.Connection('url') + my_connection.get_records_by_resource_id('test') + + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade()) + @patch.object(time, 'time', return_value=80) + @mock.patch.object(region_resource_id_status, 'conf') + def test_get_timstamp_pair_sanity(self, db_session, time_mock, conf_mock): + """Test get_timestamp_pair""" + conf_mock.region_resource_id_status.max_interval_time.default = 1 + my_connection = region_resource_id_status.Connection('url') + (timestamp, ref_timestamp) = my_connection.get_timstamp_pair() + self.assertEqual(timestamp, 80000) + + @mock.patch.object(region_resource_id_status, 'StatusModel') + @patch.object(region_resource_id_status.Connection, + 'get_timstamp_pair', + return_value=(1, 2)) + @mock.patch.object(region_resource_id_status, 'Model') + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(False, False, True)) + def test_get_records_by_resource_id_and_status_no_records(self, mock_db_session, + mock_get_timestamp, + mock_model, + mock_statusmodel): + """Test that the function returns None when it got no records.""" + my_connection = region_resource_id_status.Connection('url') + self.assertIsNone(my_connection.get_records_by_resource_id_and_status('1', '2')) + + @mock.patch.object(region_resource_id_status, 'StatusModel') + @patch.object(region_resource_id_status.Connection, 'get_timstamp_pair', + return_value=(1, 2)) + @mock.patch.object(region_resource_id_status, 'Model') + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(False, True, True)) + def test_get_records_by_resource_id_and_status_sanity(self, mock_db_session, + mock_get_timestamp, + mock_model, + mock_statusmodel): + my_connection = region_resource_id_status.Connection('url') + my_connection.get_records_by_resource_id_and_status('1', '2') + + @mock.patch.object(region_resource_id_status, 'StatusModel') + @patch.object(region_resource_id_status.Connection, 'get_timstamp_pair', + return_value=(1, 0)) + @mock.patch.object(region_resource_id_status, 'Model') + @mock.patch.object(region_resource_id_status.db_session, 'EngineFacade', + return_value=MyFacade(False, True, True)) + def test_get_records_by_resource_id_and_status_with_records(self, mock_db_session, + mock_get_timestamp, + mock_model, + mock_statusmodel): + my_connection = region_resource_id_status.Connection('url') + my_connection.get_records_by_resource_id_and_status('1', '2') + diff --git a/orm/services/resource_distributor/rds/tests/storage/test_region_resource_id_status.py b/orm/services/resource_distributor/rds/tests/storage/test_region_resource_id_status.py new file mode 100644 index 00000000..82245fea --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/storage/test_region_resource_id_status.py @@ -0,0 +1,21 @@ +import unittest + +from rds.storage.region_resource_id_status import Base + + +class BaseStorageTests(unittest.TestCase): + + def test_storage_add_status_record_not_implemented(self): + """ Check if creating an instance and calling add_update_status_record method fail""" + with self.assertRaises(Exception): + Base("").add_update_status_record('1','2','3','4','5','6','7','8') + + def test_storage_get_records_by_resource_id_implemented(self): + """ Check if creating an instance and calling get_records_by_resource_id method fail""" + with self.assertRaises(Exception): + Base("").get_records_by_resource_id('1') + + def test_storage_get_records_by_filter_args_implemented(self): + """ Check if creating an instance and calling get_records_by_filter_args method fail""" + with self.assertRaises(Exception): + Base("").get_records_by_filter_args(abc="def") diff --git a/orm/services/resource_distributor/rds/tests/utils/__init__.py b/orm/services/resource_distributor/rds/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/tests/utils/test_uuid_utils.py b/orm/services/resource_distributor/rds/tests/utils/test_uuid_utils.py new file mode 100755 index 00000000..b05f30fe --- /dev/null +++ b/orm/services/resource_distributor/rds/tests/utils/test_uuid_utils.py @@ -0,0 +1,30 @@ +"""UUID utils test module.""" + +import mock +from rds.utils import uuid_utils +import unittest + + +class MyResponse(object): + """A test response class.""" + + def json(self): + """Return the test dict.""" + return {'uuid': 3} + + +# class UuidUtilsTest(unittest.TestCase): +# """The main UUID utils test case.""" +# +# @mock.patch.object(uuid_utils, 'config') +# @mock.patch.object(uuid_utils.requests, 'post', return_value=MyResponse()) +# def test_get_random_uuid_sanity(self, mock_post, mock_config): +# """Test that the function returns the expected value.""" +# self.assertEqual(uuid_utils.get_random_uuid(), 3) +# +# @mock.patch.object(uuid_utils, 'config') +# @mock.patch.object(uuid_utils.requests, 'post', side_effect=ValueError( +# 'test')) +# def test_get_random_uuid_exception(self, mock_post, mock_config): +# """Test that the function lets exceptions propagate.""" +# self.assertRaises(ValueError, uuid_utils.get_random_uuid) diff --git a/orm/services/resource_distributor/rds/utils/__init__.py b/orm/services/resource_distributor/rds/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/utils/authentication.py b/orm/services/resource_distributor/rds/utils/authentication.py new file mode 100755 index 00000000..e2266a7b --- /dev/null +++ b/orm/services/resource_distributor/rds/utils/authentication.py @@ -0,0 +1,111 @@ +import logging +import requests +import json +from pecan import conf +from keystone_utils import tokens +from rds.proxies import rms_proxy as RmsService + + +enabled = False +mech_id = "" +mech_password = False +rms_url = "" +tenant_name = "" + + +headers = {'content-type': 'application/json'} + + +logger = logging.getLogger(__name__) + + +def _is_authorization_enabled(): + return conf.authentication.enabled + + +def _get_token_conf(): + conf = tokens.TokenConf(mech_id, mech_password, rms_url, tenant_name) + return conf + + +def get_keystone_ep_region_name(): + # get any region that hase keystone end point + logger.debug("get list of regions from rms") + regions = RmsService.get_regions() + if not regions: + logger.error("failto get regions from rms") + return None, None + logger.debug("got {} regions".format(len(regions))) + keystone_ep = None + region_name = None + for region in regions['regions']: + for endpoint in region['endpoints']: + if endpoint['type'] == 'identity': + keystone_ep = endpoint['publicURL'] + break + if keystone_ep: + region_name = region['name'] + break + logger.debug("Got keystone_ep {} for region name {}".format(keystone_ep, + region_name)) + return region_name, keystone_ep + + +def get_token(): + + logger.debug("create token") + if not _is_authorization_enabled(): + return + + region, keystone_ep = get_keystone_ep_region_name() + if not region or not keystone_ep: + log_message = "fail to create token reason -- fail to get region-- " \ + "region:{} keystone {}".format(region, keystone_ep) + log_message = log_message.replace('\n', '_').replace('\r', '_') + logger.error(log_message) + return + + url = keystone_ep + '/v2.0/tokens' + logger.debug("url :- {}".format(url)) + data = { + "auth": { + "tenantName": conf.authentication.tenant_name, + "passwordCredentials": { + "username": conf.authentication.mech_id, + "password": conf.authentication.mech_pass + } + } + } + try: + logger.debug("get token url- {} data= {}".format(url, data)) + respone = requests.post(url, data=json.dumps(data), headers=headers, + verify=conf.verify) + + if respone.status_code != 200: + logger.error("fail to get token from url") + logger.debug("got token for region {}".format(region)) + return region, respone.json()['access']['token']['id'] + + except Exception as exp: + logger.error(exp) + logger.exception(exp) + + +def check_permissions(token_to_validate, lcp_id): + logger.debug("Check permissions...start") + try: + if _is_authorization_enabled(): + token_conf = _get_token_conf() + logger.debug("Authorization: validating token=[{}] on lcp_id=[{}]".format(token_to_validate, lcp_id)) + is_permitted = tokens.is_token_valid(token_to_validate, lcp_id, token_conf) + logger.debug("Authorization: The token=[{}] on lcp_id=[{}] is [{}]".format(token_to_validate, lcp_id, "valid" if is_permitted else "invalid")) + else: + logger.debug("The authentication service is disabled. No authentication is needed.") + is_permitted = True + except Exception as e: + msg = "Fail to validate request. due to {}.".format(e.message) + logger.error(msg) + logger.exception(e) + is_permitted = False + logger.debug("Check permissions...end") + return is_permitted diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/README.txt b/orm/services/resource_distributor/rds/utils/module_mocks/README.txt new file mode 100644 index 00000000..87ddd695 --- /dev/null +++ b/orm/services/resource_distributor/rds/utils/module_mocks/README.txt @@ -0,0 +1,3 @@ +This directory contains mock modules for modules that are outside of this repository. +The relative path to this directory should be included in PYTHONPATH whenever running tests (in tox.ini, for example). + diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/__init__.py b/orm/services/resource_distributor/rds/utils/module_mocks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/audit_client/__init__.py b/orm/services/resource_distributor/rds/utils/module_mocks/audit_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/audit_client/api/__init__.py b/orm/services/resource_distributor/rds/utils/module_mocks/audit_client/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/audit_client/api/audit.py b/orm/services/resource_distributor/rds/utils/module_mocks/audit_client/api/audit.py new file mode 100644 index 00000000..ec483bdd --- /dev/null +++ b/orm/services/resource_distributor/rds/utils/module_mocks/audit_client/api/audit.py @@ -0,0 +1,6 @@ +def audit(*args, **kwargs): + pass + + +def init(*args, **kwargs): + pass diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/keystone_utils/__init__.py b/orm/services/resource_distributor/rds/utils/module_mocks/keystone_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/keystone_utils/tokens.py b/orm/services/resource_distributor/rds/utils/module_mocks/keystone_utils/tokens.py new file mode 100755 index 00000000..99708117 --- /dev/null +++ b/orm/services/resource_distributor/rds/utils/module_mocks/keystone_utils/tokens.py @@ -0,0 +1,6 @@ +def is_token_valid(token_to_validate, lcp_id, conf, token_role): + pass + + +def TokenConf(mech_id, mech_password, rms_url, tenant_name, keystone_version): + pass diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/orm_common/__init__.py b/orm/services/resource_distributor/rds/utils/module_mocks/orm_common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/orm_common/utils/__init__.py b/orm/services/resource_distributor/rds/utils/module_mocks/orm_common/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/services/resource_distributor/rds/utils/module_mocks/orm_common/utils/utils.py b/orm/services/resource_distributor/rds/utils/module_mocks/orm_common/utils/utils.py new file mode 100755 index 00000000..364e5d0a --- /dev/null +++ b/orm/services/resource_distributor/rds/utils/module_mocks/orm_common/utils/utils.py @@ -0,0 +1,14 @@ +"""Utils module mock.""" + + +def report_config(conf, dump=False): + """Mock report_config function.""" + + pass + + +def set_utils_conf(conf): + """Mock set_utils_conf function.""" + + pass + diff --git a/orm/services/resource_distributor/rds/utils/utils.py b/orm/services/resource_distributor/rds/utils/utils.py new file mode 100755 index 00000000..3529d9b1 --- /dev/null +++ b/orm/services/resource_distributor/rds/utils/utils.py @@ -0,0 +1,73 @@ +"""module""" +import logging +import requests +from pecan import conf +from rds.services.base import ErrorMesage +from rds.proxies import ims_proxy + +logger = logging.getLogger(__name__) + + +def post_data_to_image(data): + if data['resource_type'] == "image" and 'resource_extra_metadata' in data: + logger.debug("send metadata {} to ims :- {} for region {}".format( + data['resource_extra_metadata'], data['resource_id'], data['region'])) + + ims_proxy.send_image_metadata( + meta_data=data['resource_extra_metadata'], + resource_id=data['resource_id'], region=data['region']) + + return + + +def _get_all_rms_regions(): + # rms url + discover_url = '%s:%d' % (conf.ordupdate.discovery_url, + conf.ordupdate.discovery_port,) + # get all regions + response = requests.get('%s/v2/orm/regions' % (discover_url), + verify=conf.verify) + + if response.status_code != 200: + # fail to get regions + error = "got bad response from rms {}".format(response) + logger.error(error) + raise ErrorMesage(message="got bad response from rms ") + + return response.json() + + +def _validate_version(region, resource_type): + version = None + if 'ranger_agent' in region['version'].lower(): + version = region['version'].lower().split('aic')[1].strip().split('.') + version = version[0] + '.' + ''.join(version[1:]) + if not version or float(version) < 3: + return False + return True + + +def add_rms_status_to_regions(resource_regions, resource_type): + rms_regions = {} + all_regions = _get_all_rms_regions() + + # iterate through rms regions and gett regions status and version + for region in all_regions['regions']: + rms_regions[region['name']] = {'status': region['status'], + 'version': region['rangerAgentVersion']} + + # iterate through resource regions and add to them rms status + for region in resource_regions: + if region['name'] in rms_regions: + # check if version valid + if not _validate_version(rms_regions[region['name']], + resource_type): + raise ErrorMesage( + message="ranger_agent version for region {} must be >=1.0 ".format( + region['name'])) + + region['rms_status'] = rms_regions[region['name']]['status'] + continue + # if region not found in rms + region['rms_status'] = "region_not_found_in_rms" + return resource_regions diff --git a/orm/services/resource_distributor/rds/utils/uuid_utils.py b/orm/services/resource_distributor/rds/utils/uuid_utils.py new file mode 100755 index 00000000..66b45196 --- /dev/null +++ b/orm/services/resource_distributor/rds/utils/uuid_utils.py @@ -0,0 +1,8 @@ +import requests + +from pecan import conf + + +def get_random_uuid(): + response = requests.post(conf.UUID_URL, verify=conf.verify) + return response.json()['uuid'] diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/README.md b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/README.md new file mode 100644 index 00000000..d94aa385 --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/README.md @@ -0,0 +1,24 @@ +# Swagger generated server + +## Overview +This server was generated by the [swagger-codegen](https://github.com/swagger-api/swagger-codegen) project. By using the [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote server, you can easily generate a server stub. This is an example of building a node.js server. + +This example uses the [expressjs](http://expressjs.com/) framework. To see how to make this your own, look here: + +[README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md) + +### Running the server +To run the server, follow these simple steps: + +``` +npm install +node . +``` + +To view the Swagger UI interface: + +``` +open http://localhost:8080/docs +``` + +This project leverages the mega-awesome [swagger-tools](https://github.com/apigee-127/swagger-tools) middleware which does most all the work. diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/api/swagger.yaml b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/api/swagger.yaml new file mode 100644 index 00000000..31001e3a --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/api/swagger.yaml @@ -0,0 +1,296 @@ +--- +swagger: "2.0" +info: + description: "RDS API" + version: "1.0.0" + title: "RDS API" +host: "server" +basePath: "/v1" +schemes: +- "http" +consumes: +- "application/json" +produces: +- "application/json" +paths: + /resources: + post: + tags: + - "Resources" + summary: "New Resource" + description: "Creates new resource\n" + operationId: "resourcesPOST" + parameters: + - in: "body" + name: "resource" + description: "The new resource" + required: true + schema: + $ref: "#/definitions/Resource" + responses: + 201: + description: "Created" + schema: + $ref: "#/definitions/CreatedResource" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Resources" + delete: + tags: + - "Resources" + summary: "Update resource" + description: "Update resource\n" + operationId: "resourcesDELETE" + parameters: + - in: "body" + name: "resource" + description: "The new resource to update" + required: true + schema: + $ref: "#/definitions/Resource" + responses: + 200: + description: "OK" + schema: + type: "string" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Resources" + /status: + post: + tags: + - "Status" + summary: "New resource status" + description: "Creates new resource status\n" + operationId: "statusPOST" + parameters: + - in: "body" + name: "status_input" + description: "New resoure status" + required: true + schema: + $ref: "#/definitions/StatusInput" + responses: + 201: + description: "Created" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Status" + /status/resource/{id}: + get: + tags: + - "Status" + summary: "Get resource status" + description: "Get resource status\n" + operationId: "statusResourceIdGET" + parameters: + - name: "id" + in: "path" + description: "resource id" + required: true + type: "string" + responses: + 200: + description: "resource status" + schema: + $ref: "#/definitions/ResourceStatus" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Status" +definitions: + Resource: + type: "object" + required: + - "service_template" + properties: + service_template: + $ref: "#/definitions/ResourceData" + ResourceData: + type: "object" + required: + - "model" + - "resource" + - "tracking" + properties: + resource: + $ref: "#/definitions/ResourceTypeData" + model: + type: "string" + description: "model" + tracking: + $ref: "#/definitions/TrackingData" + ResourceTypeData: + type: "object" + required: + - "resource_type" + properties: + resource_type: + type: "string" + description: "resource type" + resource_id: + type: "string" + description: "resource id" + TrackingData: + type: "object" + required: + - "external_id" + - "tracking_id" + properties: + external_id: + type: "string" + description: "external id" + tracking_id: + type: "string" + description: "tracking id" + CreatedResource: + type: "object" + required: + - "id" + - "links" + properties: + id: + type: "string" + description: "id" + created: + type: "string" + description: "tracking id" + links: + $ref: "#/definitions/links" + updated: + type: "string" + description: "tracking id" + err: + type: "string" + description: "tracking id" + message: + type: "string" + description: "tracking id" + links: + type: "object" + required: + - "self" + properties: + self: + type: "string" + description: "self" + Error: + type: "object" + properties: + faultcode: + type: "string" + faultstring: + type: "string" + debuginfo: + type: "string" + StatusInput: + type: "object" + required: + - "rds_listener" + properties: + rds_listener: + $ref: "#/definitions/StatusResourceData" + StatusResourceData: + type: "object" + required: + - "error-code" + - "error-msg" + - "ord-notifier-id" + - "region" + - "request-id" + - "resource-id" + - "resource-operation" + - "resource-template-type" + - "resource-template-version" + - "resource-type" + - "status" + properties: + resource-id: + type: "string" + description: "resource id" + request-id: + type: "string" + description: "request id" + resource-type: + type: "string" + description: "resource type" + resource-template-version: + type: "string" + description: "resource template version" + resource-template-type: + type: "string" + description: "resource template type" + resource-operation: + type: "string" + description: "resource operation" + ord-notifier-id: + type: "string" + description: "ord notifier id" + region: + type: "string" + description: "region" + status: + type: "string" + description: "status" + error-code: + type: "string" + description: "error code" + error-msg: + type: "string" + description: "error msg" + ResourceStatus: + type: "object" + required: + - "regions" + - "status" + properties: + status: + type: "string" + description: "status" + regions: + type: "array" + items: + $ref: "#/definitions/OutputResource" + OutputResource: + type: "object" + required: + - "error_code" + - "error_msg" + - "ord_notifier_id" + - "ord_transaction_id" + - "region" + - "resource_id" + - "status" + - "timestamp" + properties: + region: + type: "string" + description: "region" + timestamp: + type: "string" + description: "timestamp" + ord_transaction_id: + type: "string" + description: "ord transaction id" + resource_id: + type: "string" + description: "resource id" + ord_notifier_id: + type: "string" + description: "ord notifier id" + status: + type: "string" + description: "status" + error_code: + type: "string" + description: "error code" + error_msg: + type: "string" + description: "error msg" diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Resources.js b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Resources.js new file mode 100644 index 00000000..bcb96cc5 --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Resources.js @@ -0,0 +1,15 @@ +'use strict'; + +var url = require('url'); + + +var Resources = require('./ResourcesService'); + + +module.exports.resourcesDELETE = function resourcesDELETE (req, res, next) { + Resources.resourcesDELETE(req.swagger.params, res, next); +}; + +module.exports.resourcesPOST = function resourcesPOST (req, res, next) { + Resources.resourcesPOST(req.swagger.params, res, next); +}; diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/ResourcesService.js b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/ResourcesService.js new file mode 100644 index 00000000..14f726ba --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/ResourcesService.js @@ -0,0 +1,53 @@ +'use strict'; + +exports.resourcesDELETE = function(args, res, next) { + /** + * parameters expected in the args: + * resource (Resource) + **/ + + + var examples = {}; + examples['application/json'] = "aeiou"; + + if(Object.keys(examples).length > 0) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2)); + } + else { + res.end(); + } + + +} + +exports.resourcesPOST = function(args, res, next) { + /** + * parameters expected in the args: + * resource (Resource) + **/ + + + var examples = {}; + examples['application/json'] = { + "err" : "aeiou", + "created" : "aeiou", + "links" : { + "self" : "aeiou" + }, + "id" : "aeiou", + "message" : "aeiou", + "updated" : "aeiou" +}; + + if(Object.keys(examples).length > 0) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2)); + } + else { + res.end(); + } + + +} + diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Status.js b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Status.js new file mode 100644 index 00000000..4bd1cc04 --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/Status.js @@ -0,0 +1,15 @@ +'use strict'; + +var url = require('url'); + + +var Status = require('./StatusService'); + + +module.exports.statusPOST = function statusPOST (req, res, next) { + Status.statusPOST(req.swagger.params, res, next); +}; + +module.exports.statusResourceIdGET = function statusResourceIdGET (req, res, next) { + Status.statusResourceIdGET(req.swagger.params, res, next); +}; diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/StatusService.js b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/StatusService.js new file mode 100644 index 00000000..4d7f33a9 --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/controllers/StatusService.js @@ -0,0 +1,46 @@ +'use strict'; + +exports.statusPOST = function(args, res, next) { + /** + * parameters expected in the args: + * statusInput (StatusInput) + **/ + // no response value expected for this operation + + + res.end(); +} + +exports.statusResourceIdGET = function(args, res, next) { + /** + * parameters expected in the args: + * id (String) + **/ + + + var examples = {}; + examples['application/json'] = { + "regions" : [ { + "ord_transaction_id" : "aeiou", + "error_msg" : "aeiou", + "resource_id" : "aeiou", + "error_code" : "aeiou", + "region" : "aeiou", + "timestamp" : "aeiou", + "ord_notifier_id" : "aeiou", + "status" : "aeiou" + } ], + "status" : "aeiou" +}; + + if(Object.keys(examples).length > 0) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2)); + } + else { + res.end(); + } + + +} + diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/index.js b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/index.js new file mode 100644 index 00000000..aaa873a1 --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/index.js @@ -0,0 +1,40 @@ +'use strict'; + +var app = require('connect')(); +var http = require('http'); +var swaggerTools = require('swagger-tools'); +var jsyaml = require('js-yaml'); +var fs = require('fs'); +var serverPort = 8080; + +// swaggerRouter configuration +var options = { + swaggerUi: '/swagger.json', + controllers: './controllers', + useStubs: process.env.NODE_ENV === 'development' ? true : false // Conditionally turn on stubs (mock mode) +}; + +// The Swagger document (require it, build it programmatically, fetch it from a URL, ...) +var spec = fs.readFileSync('./api/swagger.yaml', 'utf8'); +var swaggerDoc = jsyaml.safeLoad(spec); + +// Initialize the Swagger middleware +swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) { + // Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain + app.use(middleware.swaggerMetadata()); + + // Validate Swagger requests + app.use(middleware.swaggerValidator()); + + // Route validated requests to appropriate controller + app.use(middleware.swaggerRouter(options)); + + // Serve the Swagger documents and Swagger UI + app.use(middleware.swaggerUi()); + + // Start the server + http.createServer(app).listen(serverPort, function () { + console.log('Your server is listening on port %d (http://localhost:%d)', serverPort, serverPort); + console.log('Swagger-ui is available on http://localhost:%d/docs', serverPort); + }); +}); diff --git a/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/package.json b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/package.json new file mode 100644 index 00000000..02fd5509 --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/nodejs_rds_docs/package.json @@ -0,0 +1,16 @@ +{ + "name": "rds-api", + "version": "1.0.0", + "description": "RDS API", + "main": "index.js", + "keywords": [ + "swagger" + ], + "license": "MIT", + "private": true, + "dependencies": { + "connect": "^3.2.0", + "js-yaml": "^3.3.0", + "swagger-tools": "0.9.*" + } +} diff --git a/orm/services/resource_distributor/rds_docs/rds_swagger.yaml b/orm/services/resource_distributor/rds_docs/rds_swagger.yaml new file mode 100755 index 00000000..06b668db --- /dev/null +++ b/orm/services/resource_distributor/rds_docs/rds_swagger.yaml @@ -0,0 +1,295 @@ +# this is the RDS API +swagger: '2.0' +info: + title: RDS API + description: RDS API + version: "3.5" +# the domain of the service +host: server +# array of all schemes that your API supports +schemes: + - http +# will be prefixed to all paths +basePath: /v1/rds +consumes: + - application/json +produces: + - application/json +paths: + /resources: + post: + summary: New Resource + description: | + Creates new resource + parameters: + - name: resource + in: body + description: The new resource + required: true + schema: + $ref: "#/definitions/Resource" + tags: + - Resources + responses: + 201: + description: Created + schema: + $ref: '#/definitions/CreatedResource' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + delete: + summary: Update resource + description: | + Update resource + parameters: + - name: resource + in: body + description: The new resource to update + required: true + schema: + $ref: "#/definitions/Resource" + tags: + - Resources + responses: + 200: + description: OK + schema: + type: string + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /status: + post: + summary: New resource status + description: | + Creates new resource status + parameters: + - name: status_input + in: body + description: New resoure status + required: true + schema: + $ref: "#/definitions/StatusInput" + tags: + - Status + responses: + 201: + description: Created + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /status/resource/{id}: + get: + summary: Get resource status + description: | + Get resource status + parameters: + - name: id + in: path + description: resource id + required: true + type: string + tags: + - Status + responses: + 200: + description: resource status + schema: + $ref: '#/definitions/ResourceStatus' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' +definitions: + Resource: + type: object + required: + - service_template + properties: + service_template: + $ref: '#/definitions/ResourceData' + ResourceData: + type: object + required: + - resource + - model + - tracking + properties: + resource: + $ref: '#/definitions/ResourceTypeData' + model: + type: string + description: model + tracking: + $ref: '#/definitions/TrackingData' + ResourceTypeData: + type: object + required: + - resource_type + properties: + resource_type: + type: string + description: resource type + resource_id: + type: string + description: resource id + TrackingData: + type: object + required: + - external_id + - tracking_id + properties: + external_id: + type: string + description: external id + tracking_id: + type: string + description: tracking id + CreatedResource: + type: object + required: + - id + - links + properties: + id: + type: string + description: id + created: + type: string + description: tracking id + links: + $ref: '#/definitions/links' + updated: + type: string + description: tracking id + err: + type: string + description: tracking id + message: + type: string + description: tracking id + links: + type: object + required: + - self + properties: + self: + type: string + description: self + Error: + type: object + properties: + faultcode: + type: string + faultstring: + type: string + debuginfo: + type: string + StatusInput: + type: object + required: + - rds_listener + properties: + rds_listener: + $ref: '#/definitions/StatusResourceData' + StatusResourceData: + type: object + required: + - resource-id + - request-id + - resource-type + - resource-template-version + - resource-template-type + - resource-operation + - ord-notifier-id + - region + - status + - error-code + - error-msg + properties: + resource-id: + type: string + description: resource id + request-id: + type: string + description: request id (transaction ID received from RDS) + resource-type: + type: string + description: resource type (e.g., 'customer') + resource-template-version: + type: string + description: resource template version (Git commit ID) + resource-template-type: + type: string + description: resource template type + resource-operation: + type: string + description: resource operation + ord-notifier-id: + type: string + description: ord notifier id + region: + type: string + description: region + status: + type: string + description: status + error-code: + type: string + description: error code + error-msg: + type: string + description: error msg + ResourceStatus: + type: object + required: + - status + - regions + properties: + status: + type: string + description: status + regions: + type: array + items: + $ref: '#/definitions/OutputResource' + OutputResource: + type: object + required: + - region + - timestamp + - ord_transaction_id + - resource_id + - ord_notifier_id + - status + - error_code + - error_msg + properties: + region: + type: string + description: region + timestamp: + type: string + description: timestamp + ord_transaction_id: + type: string + description: ord transaction id + resource_id: + type: string + description: resource id + ord_notifier_id: + type: string + description: ord notifier id + status: + type: string + description: status + error_code: + type: string + description: error code + error_msg: + type: string + description: error msg diff --git a/orm/services/resource_distributor/scripts/db_scripts/create_db.sql b/orm/services/resource_distributor/scripts/db_scripts/create_db.sql new file mode 100755 index 00000000..9b58a5e5 --- /dev/null +++ b/orm/services/resource_distributor/scripts/db_scripts/create_db.sql @@ -0,0 +1,37 @@ +create database if not exists orm_rds DEFAULT CHARACTER SET utf8 COLLATE utf8_bin; +use orm_rds; + + +#***** +#* MySql script for Creating Table resource_status +#***** + +create table if not exists resource_status +( + id integer auto_increment not null, + timestamp bigint not null, + region varchar(64) not null, + resource_id varchar(64) not null, + status varchar(16) not null, + transaction_id varchar(64), + ord_notifier varchar(64) not null, + err_msg varchar(255), + err_code varchar(64), + operation varchar(64), + primary key (id), + unique(resource_id, region)); + +#***** +#* MySql script for Creating Table image_metadata +#***** + +create table if not exists image_metadata +( + image_meta_data_id integer not null, + checksum varchar(64) , + virtual_size varchar(64) , + size varchar(64) , + foreign key (image_meta_data_id) references resource_status(id) ON DELETE CASCADE ON UPDATE NO ACTION, + primary key (image_meta_data_id)); + +# diff --git a/orm/services/resource_distributor/scripts/db_scripts/update_db.sql b/orm/services/resource_distributor/scripts/db_scripts/update_db.sql new file mode 100755 index 00000000..05ba7230 --- /dev/null +++ b/orm/services/resource_distributor/scripts/db_scripts/update_db.sql @@ -0,0 +1,41 @@ +use orm_rds; + + +DELIMITER ;; + +#***** +#* MySql script for Creating Table image_metadata +#***** +DROP PROCEDURE IF EXISTS add_table_image_metadata ;; +CREATE PROCEDURE add_table_image_metadata() +BEGIN + + create table if not exists image_metadata + ( + image_meta_data_id integer not null, + checksum varchar(64) , + virtual_size varchar(64) , + size varchar(64) , + foreign key (image_meta_data_id) references resource_status(id) ON DELETE CASCADE ON UPDATE NO ACTION, + primary key (image_meta_data_id)); + + +END ;; +# +#*********** +#* add operation field to resource_status table +#*********** + +DROP PROCEDURE IF EXISTS add_operation_in_resource_status ;; +CREATE PROCEDURE add_operation_in_resource_status() +BEGIN + + IF NOT EXISTS( (SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() + AND COLUMN_NAME='operation' AND TABLE_NAME='resource_status') ) THEN + ALTER TABLE resource_status ADD operation varchar(64) DEFAULT ''; + END IF; + +END ;; +CALL add_operation_in_resource_status() ;; +CALL add_table_image_metadata();; +DELIMITER ; \ No newline at end of file diff --git a/orm/services/resource_distributor/scripts/shell_scripts/create_db.sh b/orm/services/resource_distributor/scripts/shell_scripts/create_db.sh new file mode 100755 index 00000000..904ab727 --- /dev/null +++ b/orm/services/resource_distributor/scripts/shell_scripts/create_db.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +echo Creating database: orm_rds +echo Creating table: resource_status + +mysql -uroot -pstack < ../db_scripts/create_db.sql + +echo Done ! + + + + + + + diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/README.md b/orm/services/resource_distributor/swagger/nodejs_rds_docs/README.md new file mode 100644 index 00000000..d94aa385 --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/README.md @@ -0,0 +1,24 @@ +# Swagger generated server + +## Overview +This server was generated by the [swagger-codegen](https://github.com/swagger-api/swagger-codegen) project. By using the [OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote server, you can easily generate a server stub. This is an example of building a node.js server. + +This example uses the [expressjs](http://expressjs.com/) framework. To see how to make this your own, look here: + +[README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md) + +### Running the server +To run the server, follow these simple steps: + +``` +npm install +node . +``` + +To view the Swagger UI interface: + +``` +open http://localhost:8080/docs +``` + +This project leverages the mega-awesome [swagger-tools](https://github.com/apigee-127/swagger-tools) middleware which does most all the work. diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/api/swagger.yaml b/orm/services/resource_distributor/swagger/nodejs_rds_docs/api/swagger.yaml new file mode 100644 index 00000000..31001e3a --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/api/swagger.yaml @@ -0,0 +1,296 @@ +--- +swagger: "2.0" +info: + description: "RDS API" + version: "1.0.0" + title: "RDS API" +host: "server" +basePath: "/v1" +schemes: +- "http" +consumes: +- "application/json" +produces: +- "application/json" +paths: + /resources: + post: + tags: + - "Resources" + summary: "New Resource" + description: "Creates new resource\n" + operationId: "resourcesPOST" + parameters: + - in: "body" + name: "resource" + description: "The new resource" + required: true + schema: + $ref: "#/definitions/Resource" + responses: + 201: + description: "Created" + schema: + $ref: "#/definitions/CreatedResource" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Resources" + delete: + tags: + - "Resources" + summary: "Update resource" + description: "Update resource\n" + operationId: "resourcesDELETE" + parameters: + - in: "body" + name: "resource" + description: "The new resource to update" + required: true + schema: + $ref: "#/definitions/Resource" + responses: + 200: + description: "OK" + schema: + type: "string" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Resources" + /status: + post: + tags: + - "Status" + summary: "New resource status" + description: "Creates new resource status\n" + operationId: "statusPOST" + parameters: + - in: "body" + name: "status_input" + description: "New resoure status" + required: true + schema: + $ref: "#/definitions/StatusInput" + responses: + 201: + description: "Created" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Status" + /status/resource/{id}: + get: + tags: + - "Status" + summary: "Get resource status" + description: "Get resource status\n" + operationId: "statusResourceIdGET" + parameters: + - name: "id" + in: "path" + description: "resource id" + required: true + type: "string" + responses: + 200: + description: "resource status" + schema: + $ref: "#/definitions/ResourceStatus" + default: + description: "Unexpected error" + schema: + $ref: "#/definitions/Error" + x-swagger-router-controller: "Status" +definitions: + Resource: + type: "object" + required: + - "service_template" + properties: + service_template: + $ref: "#/definitions/ResourceData" + ResourceData: + type: "object" + required: + - "model" + - "resource" + - "tracking" + properties: + resource: + $ref: "#/definitions/ResourceTypeData" + model: + type: "string" + description: "model" + tracking: + $ref: "#/definitions/TrackingData" + ResourceTypeData: + type: "object" + required: + - "resource_type" + properties: + resource_type: + type: "string" + description: "resource type" + resource_id: + type: "string" + description: "resource id" + TrackingData: + type: "object" + required: + - "external_id" + - "tracking_id" + properties: + external_id: + type: "string" + description: "external id" + tracking_id: + type: "string" + description: "tracking id" + CreatedResource: + type: "object" + required: + - "id" + - "links" + properties: + id: + type: "string" + description: "id" + created: + type: "string" + description: "tracking id" + links: + $ref: "#/definitions/links" + updated: + type: "string" + description: "tracking id" + err: + type: "string" + description: "tracking id" + message: + type: "string" + description: "tracking id" + links: + type: "object" + required: + - "self" + properties: + self: + type: "string" + description: "self" + Error: + type: "object" + properties: + faultcode: + type: "string" + faultstring: + type: "string" + debuginfo: + type: "string" + StatusInput: + type: "object" + required: + - "rds_listener" + properties: + rds_listener: + $ref: "#/definitions/StatusResourceData" + StatusResourceData: + type: "object" + required: + - "error-code" + - "error-msg" + - "ord-notifier-id" + - "region" + - "request-id" + - "resource-id" + - "resource-operation" + - "resource-template-type" + - "resource-template-version" + - "resource-type" + - "status" + properties: + resource-id: + type: "string" + description: "resource id" + request-id: + type: "string" + description: "request id" + resource-type: + type: "string" + description: "resource type" + resource-template-version: + type: "string" + description: "resource template version" + resource-template-type: + type: "string" + description: "resource template type" + resource-operation: + type: "string" + description: "resource operation" + ord-notifier-id: + type: "string" + description: "ord notifier id" + region: + type: "string" + description: "region" + status: + type: "string" + description: "status" + error-code: + type: "string" + description: "error code" + error-msg: + type: "string" + description: "error msg" + ResourceStatus: + type: "object" + required: + - "regions" + - "status" + properties: + status: + type: "string" + description: "status" + regions: + type: "array" + items: + $ref: "#/definitions/OutputResource" + OutputResource: + type: "object" + required: + - "error_code" + - "error_msg" + - "ord_notifier_id" + - "ord_transaction_id" + - "region" + - "resource_id" + - "status" + - "timestamp" + properties: + region: + type: "string" + description: "region" + timestamp: + type: "string" + description: "timestamp" + ord_transaction_id: + type: "string" + description: "ord transaction id" + resource_id: + type: "string" + description: "resource id" + ord_notifier_id: + type: "string" + description: "ord notifier id" + status: + type: "string" + description: "status" + error_code: + type: "string" + description: "error code" + error_msg: + type: "string" + description: "error msg" diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Resources.js b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Resources.js new file mode 100644 index 00000000..bcb96cc5 --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Resources.js @@ -0,0 +1,15 @@ +'use strict'; + +var url = require('url'); + + +var Resources = require('./ResourcesService'); + + +module.exports.resourcesDELETE = function resourcesDELETE (req, res, next) { + Resources.resourcesDELETE(req.swagger.params, res, next); +}; + +module.exports.resourcesPOST = function resourcesPOST (req, res, next) { + Resources.resourcesPOST(req.swagger.params, res, next); +}; diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/ResourcesService.js b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/ResourcesService.js new file mode 100644 index 00000000..14f726ba --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/ResourcesService.js @@ -0,0 +1,53 @@ +'use strict'; + +exports.resourcesDELETE = function(args, res, next) { + /** + * parameters expected in the args: + * resource (Resource) + **/ + + + var examples = {}; + examples['application/json'] = "aeiou"; + + if(Object.keys(examples).length > 0) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2)); + } + else { + res.end(); + } + + +} + +exports.resourcesPOST = function(args, res, next) { + /** + * parameters expected in the args: + * resource (Resource) + **/ + + + var examples = {}; + examples['application/json'] = { + "err" : "aeiou", + "created" : "aeiou", + "links" : { + "self" : "aeiou" + }, + "id" : "aeiou", + "message" : "aeiou", + "updated" : "aeiou" +}; + + if(Object.keys(examples).length > 0) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2)); + } + else { + res.end(); + } + + +} + diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Status.js b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Status.js new file mode 100644 index 00000000..4bd1cc04 --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/Status.js @@ -0,0 +1,15 @@ +'use strict'; + +var url = require('url'); + + +var Status = require('./StatusService'); + + +module.exports.statusPOST = function statusPOST (req, res, next) { + Status.statusPOST(req.swagger.params, res, next); +}; + +module.exports.statusResourceIdGET = function statusResourceIdGET (req, res, next) { + Status.statusResourceIdGET(req.swagger.params, res, next); +}; diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/StatusService.js b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/StatusService.js new file mode 100644 index 00000000..4d7f33a9 --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/controllers/StatusService.js @@ -0,0 +1,46 @@ +'use strict'; + +exports.statusPOST = function(args, res, next) { + /** + * parameters expected in the args: + * statusInput (StatusInput) + **/ + // no response value expected for this operation + + + res.end(); +} + +exports.statusResourceIdGET = function(args, res, next) { + /** + * parameters expected in the args: + * id (String) + **/ + + + var examples = {}; + examples['application/json'] = { + "regions" : [ { + "ord_transaction_id" : "aeiou", + "error_msg" : "aeiou", + "resource_id" : "aeiou", + "error_code" : "aeiou", + "region" : "aeiou", + "timestamp" : "aeiou", + "ord_notifier_id" : "aeiou", + "status" : "aeiou" + } ], + "status" : "aeiou" +}; + + if(Object.keys(examples).length > 0) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2)); + } + else { + res.end(); + } + + +} + diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/index.js b/orm/services/resource_distributor/swagger/nodejs_rds_docs/index.js new file mode 100644 index 00000000..aaa873a1 --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/index.js @@ -0,0 +1,40 @@ +'use strict'; + +var app = require('connect')(); +var http = require('http'); +var swaggerTools = require('swagger-tools'); +var jsyaml = require('js-yaml'); +var fs = require('fs'); +var serverPort = 8080; + +// swaggerRouter configuration +var options = { + swaggerUi: '/swagger.json', + controllers: './controllers', + useStubs: process.env.NODE_ENV === 'development' ? true : false // Conditionally turn on stubs (mock mode) +}; + +// The Swagger document (require it, build it programmatically, fetch it from a URL, ...) +var spec = fs.readFileSync('./api/swagger.yaml', 'utf8'); +var swaggerDoc = jsyaml.safeLoad(spec); + +// Initialize the Swagger middleware +swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) { + // Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain + app.use(middleware.swaggerMetadata()); + + // Validate Swagger requests + app.use(middleware.swaggerValidator()); + + // Route validated requests to appropriate controller + app.use(middleware.swaggerRouter(options)); + + // Serve the Swagger documents and Swagger UI + app.use(middleware.swaggerUi()); + + // Start the server + http.createServer(app).listen(serverPort, function () { + console.log('Your server is listening on port %d (http://localhost:%d)', serverPort, serverPort); + console.log('Swagger-ui is available on http://localhost:%d/docs', serverPort); + }); +}); diff --git a/orm/services/resource_distributor/swagger/nodejs_rds_docs/package.json b/orm/services/resource_distributor/swagger/nodejs_rds_docs/package.json new file mode 100644 index 00000000..02fd5509 --- /dev/null +++ b/orm/services/resource_distributor/swagger/nodejs_rds_docs/package.json @@ -0,0 +1,16 @@ +{ + "name": "rds-api", + "version": "1.0.0", + "description": "RDS API", + "main": "index.js", + "keywords": [ + "swagger" + ], + "license": "MIT", + "private": true, + "dependencies": { + "connect": "^3.2.0", + "js-yaml": "^3.3.0", + "swagger-tools": "0.9.*" + } +} diff --git a/orm/services/resource_distributor/swagger/swagger.yaml b/orm/services/resource_distributor/swagger/swagger.yaml new file mode 100755 index 00000000..06b668db --- /dev/null +++ b/orm/services/resource_distributor/swagger/swagger.yaml @@ -0,0 +1,295 @@ +# this is the RDS API +swagger: '2.0' +info: + title: RDS API + description: RDS API + version: "3.5" +# the domain of the service +host: server +# array of all schemes that your API supports +schemes: + - http +# will be prefixed to all paths +basePath: /v1/rds +consumes: + - application/json +produces: + - application/json +paths: + /resources: + post: + summary: New Resource + description: | + Creates new resource + parameters: + - name: resource + in: body + description: The new resource + required: true + schema: + $ref: "#/definitions/Resource" + tags: + - Resources + responses: + 201: + description: Created + schema: + $ref: '#/definitions/CreatedResource' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + delete: + summary: Update resource + description: | + Update resource + parameters: + - name: resource + in: body + description: The new resource to update + required: true + schema: + $ref: "#/definitions/Resource" + tags: + - Resources + responses: + 200: + description: OK + schema: + type: string + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /status: + post: + summary: New resource status + description: | + Creates new resource status + parameters: + - name: status_input + in: body + description: New resoure status + required: true + schema: + $ref: "#/definitions/StatusInput" + tags: + - Status + responses: + 201: + description: Created + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + /status/resource/{id}: + get: + summary: Get resource status + description: | + Get resource status + parameters: + - name: id + in: path + description: resource id + required: true + type: string + tags: + - Status + responses: + 200: + description: resource status + schema: + $ref: '#/definitions/ResourceStatus' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' +definitions: + Resource: + type: object + required: + - service_template + properties: + service_template: + $ref: '#/definitions/ResourceData' + ResourceData: + type: object + required: + - resource + - model + - tracking + properties: + resource: + $ref: '#/definitions/ResourceTypeData' + model: + type: string + description: model + tracking: + $ref: '#/definitions/TrackingData' + ResourceTypeData: + type: object + required: + - resource_type + properties: + resource_type: + type: string + description: resource type + resource_id: + type: string + description: resource id + TrackingData: + type: object + required: + - external_id + - tracking_id + properties: + external_id: + type: string + description: external id + tracking_id: + type: string + description: tracking id + CreatedResource: + type: object + required: + - id + - links + properties: + id: + type: string + description: id + created: + type: string + description: tracking id + links: + $ref: '#/definitions/links' + updated: + type: string + description: tracking id + err: + type: string + description: tracking id + message: + type: string + description: tracking id + links: + type: object + required: + - self + properties: + self: + type: string + description: self + Error: + type: object + properties: + faultcode: + type: string + faultstring: + type: string + debuginfo: + type: string + StatusInput: + type: object + required: + - rds_listener + properties: + rds_listener: + $ref: '#/definitions/StatusResourceData' + StatusResourceData: + type: object + required: + - resource-id + - request-id + - resource-type + - resource-template-version + - resource-template-type + - resource-operation + - ord-notifier-id + - region + - status + - error-code + - error-msg + properties: + resource-id: + type: string + description: resource id + request-id: + type: string + description: request id (transaction ID received from RDS) + resource-type: + type: string + description: resource type (e.g., 'customer') + resource-template-version: + type: string + description: resource template version (Git commit ID) + resource-template-type: + type: string + description: resource template type + resource-operation: + type: string + description: resource operation + ord-notifier-id: + type: string + description: ord notifier id + region: + type: string + description: region + status: + type: string + description: status + error-code: + type: string + description: error code + error-msg: + type: string + description: error msg + ResourceStatus: + type: object + required: + - status + - regions + properties: + status: + type: string + description: status + regions: + type: array + items: + $ref: '#/definitions/OutputResource' + OutputResource: + type: object + required: + - region + - timestamp + - ord_transaction_id + - resource_id + - ord_notifier_id + - status + - error_code + - error_msg + properties: + region: + type: string + description: region + timestamp: + type: string + description: timestamp + ord_transaction_id: + type: string + description: ord transaction id + resource_id: + type: string + description: resource id + ord_notifier_id: + type: string + description: ord notifier id + status: + type: string + description: status + error_code: + type: string + description: error code + error_msg: + type: string + description: error msg diff --git a/orm/services/resource_distributor/tox.ini b/orm/services/resource_distributor/tox.ini new file mode 100755 index 00000000..9891056a --- /dev/null +++ b/orm/services/resource_distributor/tox.ini @@ -0,0 +1,21 @@ +[tox] +envlist = py27, cover + +[testenv:cover] + +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +install_command = pip install -U {opts} {packages} + +setenv = PYTHONPATH = {toxinidir}:{toxinidir}/rds/utils/module_mocks/ + +commands = + python setup.py testr --slowest --coverage + coverage report --omit=rds/storage/factory.py,rds/api/app.py,rds/tests/*,rds/utils/authentication.py,rds/sot/sot_utils.py,rds/utils/module_mocks/* + coverage html --omit=rds/storage/factory.py,rds/api/app.py,rds/tests/*,rds/utils/authentication.py,rds/sot/sot_utils.py,rds/utils/module_mocks/* + +[testenv:pep8] +whitelist_externals=true +commands=true + diff --git a/orm/swagger/__init__.py b/orm/swagger/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/templates/error.html b/orm/templates/error.html new file mode 100644 index 00000000..f2d97961 --- /dev/null +++ b/orm/templates/error.html @@ -0,0 +1,12 @@ +<%inherit file="layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Server Error ${status} + + +## now define the body of the template +

+

Server Error ${status}

+
+

${message}

diff --git a/orm/templates/index.html b/orm/templates/index.html new file mode 100644 index 00000000..f17c3862 --- /dev/null +++ b/orm/templates/index.html @@ -0,0 +1,34 @@ +<%inherit file="layout.html" /> + +## provide definitions for blocks we want to redefine +<%def name="title()"> + Welcome to Pecan! + + +## now define the body of the template +
+

+
+ +
+ +

This is a sample Pecan project.

+ +

+ Instructions for getting started can be found online at pecanpy.org +

+ +

+ ...or you can search the documentation here: +

+ +
+
+ + +
+ Enter search terms or a module, class or function name. +
+ +
diff --git a/orm/templates/layout.html b/orm/templates/layout.html new file mode 100644 index 00000000..40908591 --- /dev/null +++ b/orm/templates/layout.html @@ -0,0 +1,22 @@ + + + ${self.title()} + ${self.style()} + ${self.javascript()} + + + ${self.body()} + + + +<%def name="title()"> + Default Title + + +<%def name="style()"> + + + +<%def name="javascript()"> + + diff --git a/orm/tests/__init__.py b/orm/tests/__init__.py new file mode 100644 index 00000000..78ea5274 --- /dev/null +++ b/orm/tests/__init__.py @@ -0,0 +1,22 @@ +import os +from unittest import TestCase +from pecan import set_config +from pecan.testing import load_test_app + +__all__ = ['FunctionalTest'] + + +class FunctionalTest(TestCase): + """ + Used for functional tests where you need to test your + literal application and its integration with the framework. + """ + + def setUp(self): + self.app = load_test_app(os.path.join( + os.path.dirname(__file__), + 'config.py' + )) + + def tearDown(self): + set_config({}, overwrite=True) diff --git a/orm/tests/config.py b/orm/tests/config.py new file mode 100644 index 00000000..c72b6987 --- /dev/null +++ b/orm/tests/config.py @@ -0,0 +1,137 @@ +from fms_rest.tests.simple_hook_mock import SimpleHookMock + +global SimpleHookMock + +# Server Specific Configurations +server = { + 'port': '8082', + 'host': '0.0.0.0', + 'name': 'fms' +} + +# Pecan Application Configurations +app = { + 'root': 'fms_rest.controllers.root.RootController', + 'modules': ['fms_rest'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/fms_rest/templates', + 'debug': True, + 'errors': { + '__force_dict__': True + }, + 'hooks': lambda: [SimpleHookMock()] +} + +logging = { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'loggers': { + 'fms_rest': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'color' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + }, + 'color': { + '()': 'pecan.log.ColorFormatter', + 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' + '[%(threadName)s] %(message)s'), + '__force_dict__': True + } + } +} + +database = { + 'host': 'localhost', + 'username': 'root', + 'password': 'root', + 'db_name': 'orm_fms_db', + +} + +# this table is for calculating default extra specs needed +extra_spec_needed_table = { + "ns": { + "aggregate_instance_extra_specs____ns": "true", + "hw____mem_page_size": "large" + }, + "nd": { + "aggregate_instance_extra_specs____nd": "true", + "hw____mem_page_size": "large" + }, + "nv": { + "aggregate_instance_extra_specs____nv": "true", + "hw____mem_page_size": "large" + }, + "gv": { + "aggregate_instance_extra_specs____gv": "true", + "aggregate_instance_extra_specs____c2": "true", + "hw____numa_nodes": "2" + }, + "ss": { + "aggregate_instance_extra_specs____ss": "true" + } +} + +# any key will be added to extra_spec_needed_table need to be added here +default_extra_spec_calculated_table = { + "aggregate_instance_extra_specs____ns": "", + "aggregate_instance_extra_specs____nd": "", + "aggregate_instance_extra_specs____nv": "", + "aggregate_instance_extra_specs____gv": "", + "aggregate_instance_extra_specs____c2": "", + "aggregate_instance_extra_specs____ss": "", + "aggregate_instance_extra_specs____c2": "", + "aggregate_instance_extra_specs____c4": "", + "aggregate_instance_extra_specs____v": "", + "hw____mem_page_size": "", + "hw____cpu_policy": "", + "hw____numa_nodes": "" +} + +database['connection_string'] = 'mysql://{0}:{1}@{2}:3306/{3}'.format(database['username'], + database['password'], + database['host'], + database['db_name']) + +application_root = 'http://localhost:{0}'.format(server['port']) + +api = { + 'uuid_server': { + 'base': 'http://127.0.0.1:8090/', + 'uuids': 'v1/uuids' + }, + 'rds_server': { + 'base': 'http://127.0.0.1:8777/', + 'resources': 'v1/rds/resources', + 'status': 'v1/rds/status/resource/' + }, + 'audit_server': { + 'base': 'http://127.0.0.1:8776/', + 'trans': 'v1/audit/transaction' + } + +} + +verify = False + +authentication = { + "enabled": False, + "mech_id": "admin", + "mech_pass": "stack", + "rms_url": "http://172.20.90.174:8080", + "tenant_name": "admin", + "keystone_version": "2.0", + # "policy_file": "/opt/app/orm/aic-orm-fms/fms_rest/etc/policy.json", + "policy_file": "/orm/aic-orm-fms/fms_rest/etc/policy.json" +} diff --git a/orm/tests/functional/__init__.py b/orm/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orm/tests/test_functional.py b/orm/tests/test_functional.py new file mode 100644 index 00000000..8ecb5357 --- /dev/null +++ b/orm/tests/test_functional.py @@ -0,0 +1,22 @@ +from unittest import TestCase +from webtest import TestApp +from orm.tests import FunctionalTest + + +class TestRootController(FunctionalTest): + + def test_get(self): + response = self.app.get('/') + assert response.status_int == 200 + + def test_search(self): + response = self.app.post('/', params={'q': 'RestController'}) + assert response.status_int == 302 + assert response.headers['Location'] == ( + 'http://pecan.readthedocs.org/en/latest/search.html' + '?q=RestController' + ) + + def test_get_not_found(self): + response = self.app.get('/a/bogus/url', expect_errors=True) + assert response.status_int == 404 diff --git a/orm/tests/test_units.py b/orm/tests/test_units.py new file mode 100644 index 00000000..573fb682 --- /dev/null +++ b/orm/tests/test_units.py @@ -0,0 +1,7 @@ +from unittest import TestCase + + +class TestUnits(TestCase): + + def test_units(self): + assert 5 * 5 == 25 diff --git a/orm/tests/unit/__init__.py b/orm/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 00000000..55c9db54 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,43 @@ +body { + background: #311F00; + color: white; + font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif; + padding: 1em 2em; +} + +a { + color: #FAFF78; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div#content { + width: 800px; + margin: 0 auto; +} + +form { + margin: 0; + padding: 0; + border: 0; +} + +fieldset { + border: 0; +} + +input.error { + background: #FAFF78; +} + +header { + text-align: center; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Futura-CondensedExtraBold', 'Futura', 'Helvetica', sans-serif; + text-transform: uppercase; +} diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f403e4a4f3ce69a4577a46ae37f5633abb79fa GIT binary patch literal 20596 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 ZD6?|u)*zSG3-Sw<9?1AXo%Yig{{dC~0l)wN literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c8d1bf5b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +# 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. + +pbr>=2.0 # Apache-2.0 +pecan==1.0.2 +netifaces==0.10.4 +SQLAlchemy==0.9.7 +WSME>=0.6 +MySQL-python==1.2.5 +requests==2.2.1 +oslo.db==1.7.2 +oslo.serialization +oslo.config +oslo.policy diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..fe935e9c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,77 @@ +[metadata] +name = ORM +version = 2016.3.0 +summary = AIC OpenStack Resource Manager +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[files] +packages = orm +data_files = + /etc/orm = + etc/orm.conf + + +[entry_points] +console_scripts= + orm-fms = orm.cmd.fms:main + orm-cms = orm.cmd.cms:main + orm-rms = orm.cmd.rms:main + orm-rds = orm.cmd.rds:main + orm-ims = orm.cmd.ims:main + orm-audit = orm.cmd.audit:main + orm-uuidgen = orm.cmd.uuidgen:main + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = orm/locale +domain = orm + +[update_catalog] +domain = orm +output_dir = orm/locale +input_file = orm/locale/orm.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = orm/locale/orm.pot + +[wheel] +universal = 1 + +[pbr] +autodoc_index_modules = 1 +warnerrors = true + + +[nosetests] +match=^test +where=orm +nocapture=1 +cover-package=orm +cover-erase=1 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..70aab5a0 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages + +setup( + name='orm', + version='0.1', + description='', + author='', + author_email='', + install_requires=[ + "pecan", + ], + test_suite='orm', + zip_safe=False, + packages=find_packages(), + include_package_data=True, + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..bedac3d2 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,16 @@ +# 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. + +hacking>=0.12.0,<0.13 # Apache-2.0 + +coverage>=4.0,!=4.4 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx>=1.6.2 # BSD +oslotest>=1.10.0 # Apache-2.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testtools>=1.4.0 # MIT +openstackdocstheme>=1.11.0 # Apache-2.0 +# releasenotes +reno>=1.8.0 # Apache-2.0 + diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..b36a1698 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +minversion = 2.0 +envlist = py34,py27,pypy,pep8 +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + PYTHONWARNINGS=default::DeprecationWarning +deps = -r{toxinidir}/test-requirements.txt + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:releasenotes] +commands = + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125,H101,H104,H238,H401,H404,H405,H306,E901,E128,E226,E501,F401,F841,F841,W191,W391,E101,E121,E122,E126,E231,H233,H301,H303,H304,F403,F811,E401,H201,E265,E111,W292,E201,E127,H202,E251,H403,F821,E303,E225,H234,E712,E124,E131,E203,E202,E221,E271,E302 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build

+ +

This is a sample Pecan project.

+ +

+ Instructions for getting started can be found online at pecanpy.org +

+ +

+ ...or you can search the documentation here: +

+ +
+
+ + +
+ Enter search terms or a module, class or function name. +
+ +