diff options
-rw-r--r-- | starlingx/starlingx/resource/__init__.py | 13 | ||||
-rw-r--r-- | starlingx/starlingx/resource/tests/__init__.py | 13 | ||||
-rw-r--r-- | starlingx/starlingx/resource/tests/test_capacity.py | 277 | ||||
-rw-r--r-- | starlingx/starlingx/resource/tests/tests_infra_workload.py | 280 | ||||
-rw-r--r-- | starlingx/starlingx/urls.py | 8 |
5 files changed, 591 insertions, 0 deletions
diff --git a/starlingx/starlingx/resource/__init__.py b/starlingx/starlingx/resource/__init__.py new file mode 100644 index 00000000..81362a2b --- /dev/null +++ b/starlingx/starlingx/resource/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2019 Intel Corporation. +# +# 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/starlingx/starlingx/resource/tests/__init__.py b/starlingx/starlingx/resource/tests/__init__.py new file mode 100644 index 00000000..81362a2b --- /dev/null +++ b/starlingx/starlingx/resource/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2019 Intel Corporation. +# +# 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/starlingx/starlingx/resource/tests/test_capacity.py b/starlingx/starlingx/resource/tests/test_capacity.py new file mode 100644 index 00000000..c48f0eff --- /dev/null +++ b/starlingx/starlingx/resource/tests/test_capacity.py @@ -0,0 +1,277 @@ +# Copyright (c) 2019 Intel Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import json + +from rest_framework import status + +from newton_base.tests import mock_info +from newton_base.tests import test_base +from newton_base.util import VimDriverUtils + +MOCK_GET_TENANT_LIMIT_RESPONSE = { + "limits": { + "rate": [], + "absolute": { + "maxTotalRAMSize": 128 * 1024, + "totalRAMUsed": 8 * 1024, + "totalCoresUsed": 4, + "maxTotalCores": 20, + } + } +} + +MOCK_GET_HYPER_STATATICS_RESPONSE = { + "hypervisor_statistics": { + "vcpus_used": 4, + "free_ram_mb": 120 * 1024, + "vcpus": 10, + "free_disk_gb": 300 + } +} + +MOCK_GET_STORAGE_RESPONSE_OOS = { + "limits": { + "rate": [], + "absolute": { + "totalGigabytesUsed": 498, + "maxTotalVolumeGigabytes": 500, + } + } +} + +MOCK_GET_HYPER_STATATICS_RESPONSE_OUTOFVCPU = { + "hypervisor_statistics": { + "vcpus_used": 9, + "free_ram_mb": 120 * 1024, + "vcpus": 10, + "free_disk_gb": 300 + } +} + +MOCK_GET_TENANT_LIMIT_RESPONSE_OUTOFRAM = { + "limits": { + "rate": [], + "absolute": { + "maxTotalRAMSize": 12 * 1024, + "totalRAMUsed": 10 * 1024, + "totalCoresUsed": 4, + "maxTotalCores": 20, + } + } +} + +MOCK_GET_HYPER_STATATICS_RESPONSE_OUTOFRAM = { + "hypervisor_statistics": { + "vcpus_used": 4, + "free_ram_mb": 1 * 1024, + "vcpus": 10, + "free_disk_gb": 300 + } +} + +MOCK_GET_HYPER_STATATICS_RESPONSE_OUTOFSTORAGE = { + "hypervisor_statistics": { + "vcpus_used": 4, + "free_ram_mb": 120 * 1024, + "vcpus": 10, + "free_disk_gb": 3 + } +} + +MOCK_GET_STORAGE_RESPONSE = { + "limits": { + "rate": [], + "absolute": { + "totalGigabytesUsed": 200, + "maxTotalVolumeGigabytes": 500, + } + } +} + +TEST_REQ_SUCCESS_SOURCE = { + "vCPU": "4", + "Memory": "4096", + "Storage": "200" +} + +TEST_REQ_FAILED_SOURCE = { + "vCPU": "17", + "Memory": "4096", + "Storage": "200" +} + + +class TestCapacity(test_base.TestRequest): + def setUp(self): + super(TestCapacity, self).setUp() + + def _get_mock_response(self, return_value=None): + mock_response = mock.Mock(spec=test_base.MockResponse) + mock_response.status_code = status.HTTP_200_OK + mock_response.json.return_value = return_value + return mock_response + + @mock.patch.object(VimDriverUtils, 'get_session') + @mock.patch.object(VimDriverUtils, 'get_vim_info') + def test_capacity_check_success(self, mock_get_vim_info, mock_get_session): + mock_get_vim_info.return_value = mock_info.MOCK_VIM_INFO + mock_get_session.return_value = test_base.get_mock_session( + ["get"], { + "side_effect": [ + self._get_mock_response(MOCK_GET_TENANT_LIMIT_RESPONSE), + self._get_mock_response(MOCK_GET_HYPER_STATATICS_RESPONSE), + self._get_mock_response(MOCK_GET_STORAGE_RESPONSE), + ] + }) + + response = self.client.post( + "/api/multicloud-starlingx/v0/starlingx_RegionOne/capacity_check", + TEST_REQ_SUCCESS_SOURCE, + HTTP_X_AUTH_TOKEN=mock_info.MOCK_TOKEN_ID) + + self.assertEquals(status.HTTP_200_OK, response.status_code) + self.assertEqual({"result": True}, response.data) + + @mock.patch.object(VimDriverUtils, 'get_session') + @mock.patch.object(VimDriverUtils, 'get_vim_info') + def test_capacity_check_nova_limits_failed(self, mock_get_vim_info, mock_get_session): + mock_get_vim_info.return_value = mock_info.MOCK_VIM_INFO + mock_get_session.return_value = test_base.get_mock_session( + ["get"], { + "side_effect": [ + self._get_mock_response(MOCK_GET_TENANT_LIMIT_RESPONSE), + self._get_mock_response(MOCK_GET_HYPER_STATATICS_RESPONSE), + self._get_mock_response(MOCK_GET_STORAGE_RESPONSE), + ] + }) + + response = self.client.post( + "/api/multicloud-starlingx/v0/starlingx_RegionOne/capacity_check", + TEST_REQ_FAILED_SOURCE, + HTTP_X_AUTH_TOKEN=mock_info.MOCK_TOKEN_ID) + + self.assertEquals(status.HTTP_200_OK, response.status_code) + self.assertEqual({"result": False}, response.data) + + @mock.patch.object(VimDriverUtils, 'get_session') + @mock.patch.object(VimDriverUtils, 'get_vim_info') + def test_capacity_check_nova_hypervisor_outofram(self, mock_get_vim_info, mock_get_session): + mock_get_vim_info.return_value = mock_info.MOCK_VIM_INFO + mock_get_session.return_value = test_base.get_mock_session( + ["get"], { + "side_effect": [ + self._get_mock_response(MOCK_GET_TENANT_LIMIT_RESPONSE), + self._get_mock_response(MOCK_GET_HYPER_STATATICS_RESPONSE_OUTOFRAM), + self._get_mock_response(MOCK_GET_STORAGE_RESPONSE), + ] + }) + + response = self.client.post( + "/api/multicloud-starlingx/v0/starlingx_RegionOne/capacity_check", + data=json.dumps(TEST_REQ_SUCCESS_SOURCE), + content_type='application/json', + HTTP_X_AUTH_TOKEN=mock_info.MOCK_TOKEN_ID) + + self.assertEquals(status.HTTP_200_OK, response.status_code) + self.assertEqual({"result": False}, response.data) + + @mock.patch.object(VimDriverUtils, 'get_session') + @mock.patch.object(VimDriverUtils, 'get_vim_info') + def test_capacity_check_nova_hypervisor_outofstorage(self, mock_get_vim_info, mock_get_session): + mock_get_vim_info.return_value = mock_info.MOCK_VIM_INFO + mock_get_session.return_value = test_base.get_mock_session( + ["get"], { + "side_effect": [ + self._get_mock_response(MOCK_GET_TENANT_LIMIT_RESPONSE), + self._get_mock_response(MOCK_GET_HYPER_STATATICS_RESPONSE_OUTOFSTORAGE), + self._get_mock_response(MOCK_GET_STORAGE_RESPONSE), + ] + }) + + response = self.client.post( + "/api/multicloud-starlingx/v0/starlingx_RegionOne/capacity_check", + data=json.dumps(TEST_REQ_SUCCESS_SOURCE), + content_type='application/json', + HTTP_X_AUTH_TOKEN=mock_info.MOCK_TOKEN_ID) + + self.assertEquals(status.HTTP_200_OK, response.status_code) + self.assertEqual({"result": False}, response.data) + + @mock.patch.object(VimDriverUtils, 'get_session') + @mock.patch.object(VimDriverUtils, 'get_vim_info') + def test_capacity_check_nova_hypervisor_outofvcpu(self, mock_get_vim_info, mock_get_session): + mock_get_vim_info.return_value = mock_info.MOCK_VIM_INFO + mock_get_session.return_value = test_base.get_mock_session( + ["get"], { + "side_effect": [ + self._get_mock_response(MOCK_GET_TENANT_LIMIT_RESPONSE), + self._get_mock_response(MOCK_GET_HYPER_STATATICS_RESPONSE_OUTOFVCPU), + self._get_mock_response(MOCK_GET_STORAGE_RESPONSE), + ] + }) + + response = self.client.post( + "/api/multicloud-starlingx/v0/starlingx_RegionOne/capacity_check", + data=json.dumps(TEST_REQ_SUCCESS_SOURCE), + content_type='application/json', + HTTP_X_AUTH_TOKEN=mock_info.MOCK_TOKEN_ID) + + self.assertEquals(status.HTTP_200_OK, response.status_code) + self.assertEqual({"result": False}, response.data) + + @mock.patch.object(VimDriverUtils, 'get_session') + @mock.patch.object(VimDriverUtils, 'get_vim_info') + def test_capacity_check_nova_limits_outofram(self, mock_get_vim_info, mock_get_session): + mock_get_vim_info.return_value = mock_info.MOCK_VIM_INFO + mock_get_session.return_value = test_base.get_mock_session( + ["get"], { + "side_effect": [ + self._get_mock_response(MOCK_GET_TENANT_LIMIT_RESPONSE_OUTOFRAM), + self._get_mock_response(MOCK_GET_HYPER_STATATICS_RESPONSE), + self._get_mock_response(MOCK_GET_STORAGE_RESPONSE), + ] + }) + + response = self.client.post( + "/api/multicloud-starlingx/v0/starlingx_RegionOne/capacity_check", + data=json.dumps(TEST_REQ_SUCCESS_SOURCE), + content_type='application/json', + HTTP_X_AUTH_TOKEN=mock_info.MOCK_TOKEN_ID) + + self.assertEquals(status.HTTP_200_OK, response.status_code) + self.assertEqual({"result": False}, response.data) + + @mock.patch.object(VimDriverUtils, 'get_session') + @mock.patch.object(VimDriverUtils, 'get_vim_info') + def test_capacity_check_volume_limits_outofstorage(self, mock_get_vim_info, mock_get_session): + mock_get_vim_info.return_value = mock_info.MOCK_VIM_INFO + mock_get_session.return_value = test_base.get_mock_session( + ["get"], { + "side_effect": [ + self._get_mock_response(MOCK_GET_TENANT_LIMIT_RESPONSE), + self._get_mock_response(MOCK_GET_HYPER_STATATICS_RESPONSE), + self._get_mock_response(MOCK_GET_STORAGE_RESPONSE_OOS), + ] + }) + + response = self.client.post( + "/api/multicloud-starlingx/v0/starlingx_RegionOne/capacity_check", + data=json.dumps(TEST_REQ_SUCCESS_SOURCE), + content_type='application/json', + HTTP_X_AUTH_TOKEN=mock_info.MOCK_TOKEN_ID) + + self.assertEquals(status.HTTP_200_OK, response.status_code) + self.assertEqual({"result": False}, response.data) diff --git a/starlingx/starlingx/resource/tests/tests_infra_workload.py b/starlingx/starlingx/resource/tests/tests_infra_workload.py new file mode 100644 index 00000000..8e9f9d98 --- /dev/null +++ b/starlingx/starlingx/resource/tests/tests_infra_workload.py @@ -0,0 +1,280 @@ +# Copyright (c) 2019 Intel Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +import unittest +from rest_framework import status + +from common.msapi.helper import Helper as helper +from newton_base.resource.infra_workload import InfraWorkload +from newton_base.resource.infra_workload import APIv1InfraWorkload + +MOCK_TOKEN_RESPONSE = { + "access": + {"token": {"issued_at": "2018-05-10T16:56:56.000000Z", + "expires": "2018-05-10T17:56:56.000000Z", + "id": "4a832860dd744306b3f66452933f939e", + "tenant": {"domain": {"id": "default", + "name": "Default"}, + "enabled": "true", + "id": "0e148b76ee8c42f78d37013bf6b7b1ae", + "name": "VIM"}}, + "serviceCatalog": [], + "user": {"domain": {"id": "default", + "name": "Default"}, + "id": "ba76c94eb5e94bb7bec6980e5507aae2", + "name": "demo"} + } +} + +MOCK_HEAT_CREATE_BODY1 = { + "generic-vnf-id": "MOCK_GENERIF_VNF_ID1", + "vf-module-id": "MOCK_VF_MODULE_ID1", + "oof_directives": { + "directives": [ + { + "id": "MOCK_VNFC_ID1", + "type": "vnfc", + "directives": [{ + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "flavor1", + "attribute_value": "m1.hpa.medium" + } + ] + }, + {"type": "sriovNetNetwork_directives", + "attributes": [{ + "attribute_name": "physnetwork_label", + "attribute_value": "physnet1" + }] + } + ] + } + ] + }, + "sdnc_directives": {}, + "template_type": "HEAT", + "template_data": { + "files": {}, + "disable_rollback": True, + "parameters": { + "flavor1": "m1.heat" + }, + "stack_name": "teststack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "40be8d1a-3eb9-40de-8abd-43237517384f", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }, + "timeout_mins": 60 + } +} + +MOCK_HEAT_CREATE_RESPONSE1 = { + 'stack': { + 'id': "MOCKED_HEAT_STACK_ID1" + } +} + +MOCK_HEAT_LIST_RESPONSE1 = { + 'stacks': [ + { + 'stack_status': "CREATE_IN_PROCESS" + } + ] +} + +MOCK_HEAT_CREATE_BODY2 = { + "generic-vnf-id": "MOCK_GENERIF_VNF_ID1", + "vf-module-id": "MOCK_VF_MODULE_ID1", + "template_type": "HEAT", + "template_data": { + "files": {}, + "disable_rollback": True, + "parameters": { + "flavor1": "m1.heat" + }, + "stack_name": "teststack", + "template": { + "heat_template_version": "2013-05-23", + "description": "Simple template to test heat commands", + "parameters": { + "flavor": { + "default": "m1.tiny", + "type": "string" + } + }, + "resources": { + "hello_world": { + "type": "OS::Nova::Server", + "properties": { + "key_name": "heat_key", + "flavor": { + "get_param": "flavor" + }, + "image": "40be8d1a-3eb9-40de-8abd-43237517384f", + "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n" + } + } + } + }, + "timeout_mins": 60 + } +} + + +class InfraWorkloadTest(unittest.TestCase): + def setUp(self): + self._InfraWorkload = InfraWorkload() + pass + + def tearDown(self): + pass + + @mock.patch.object(helper, 'MultiCloudServiceHelper') + @mock.patch.object(helper, 'MultiCloudIdentityHelper') + def test_post(self, mock_MultiCloudIdentityHelper, mock_MultiCloudServiceHelper): + mock_request = mock.Mock() + mock_request.META = {"testkey": "testvalue"} + mock_request.data = MOCK_HEAT_CREATE_BODY1 + + mock_MultiCloudIdentityHelper.side_effect = [ + (0, MOCK_TOKEN_RESPONSE, status.HTTP_201_CREATED) + ] + + mock_MultiCloudServiceHelper.side_effect = [ + (0, MOCK_HEAT_CREATE_RESPONSE1, status.HTTP_201_CREATED) + ] + + vimid = "CloudOwner_Region1" + + response = self._InfraWorkload.post(mock_request, vimid) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + pass + + @mock.patch.object(helper, 'MultiCloudServiceHelper') + @mock.patch.object(helper, 'MultiCloudIdentityHelper') + def test_post_wo_oof_directive(self, mock_MultiCloudIdentityHelper, mock_MultiCloudServiceHelper): + mock_request = mock.Mock() + mock_request.META = {"testkey": "testvalue"} + mock_request.data = MOCK_HEAT_CREATE_BODY2 + + mock_MultiCloudIdentityHelper.side_effect = [ + (0, MOCK_TOKEN_RESPONSE, status.HTTP_201_CREATED) + ] + + mock_MultiCloudServiceHelper.side_effect = [ + (0, MOCK_HEAT_CREATE_RESPONSE1, status.HTTP_201_CREATED) + ] + + vimid = "CloudOwner_Region1" + + response = self._InfraWorkload.post(mock_request, vimid) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + pass + + @mock.patch.object(helper, 'MultiCloudServiceHelper') + @mock.patch.object(helper, 'MultiCloudIdentityHelper') + def test_get(self, mock_MultiCloudIdentityHelper, mock_MultiCloudServiceHelper): + mock_request = mock.Mock() + mock_request.META = {"testkey": "testvalue"} + + mock_MultiCloudIdentityHelper.side_effect = [ + (0, MOCK_TOKEN_RESPONSE, status.HTTP_201_CREATED) + ] + + mock_MultiCloudServiceHelper.side_effect = [ + (0, MOCK_HEAT_LIST_RESPONSE1, status.HTTP_200_OK) + ] + + vimid = "CloudOwner_Region1" + mock_stack_id = "MOCKED_HEAT_STACK_ID1" + + response = self._InfraWorkload.get(mock_request, vimid, mock_stack_id) + self.assertEqual(response.status_code, status.HTTP_200_OK) + pass + + +class APIv1InfraWorkloadTest(unittest.TestCase): + def setUp(self): + self._APIv1InfraWorkload = APIv1InfraWorkload() + pass + + def tearDown(self): + pass + + @mock.patch.object(helper, 'MultiCloudServiceHelper') + @mock.patch.object(helper, 'MultiCloudIdentityHelper') + def test_post(self, mock_MultiCloudIdentityHelper, mock_MultiCloudServiceHelper): + mock_request = mock.Mock() + mock_request.META = {"testkey": "testvalue"} + mock_request.data = MOCK_HEAT_CREATE_BODY1 + + mock_MultiCloudIdentityHelper.side_effect = [ + (0, MOCK_TOKEN_RESPONSE, status.HTTP_201_CREATED) + ] + + mock_MultiCloudServiceHelper.side_effect = [ + (0, MOCK_HEAT_CREATE_RESPONSE1, status.HTTP_201_CREATED) + ] + + cloud_owner = "CloudOwner" + cloud_region_id = "Region1" + + response = self._APIv1InfraWorkload.post(mock_request, cloud_owner, cloud_region_id) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + pass + + @mock.patch.object(helper, 'MultiCloudServiceHelper') + @mock.patch.object(helper, 'MultiCloudIdentityHelper') + def test_get(self, mock_MultiCloudIdentityHelper, mock_MultiCloudServiceHelper): + mock_request = mock.Mock() + mock_request.META = {"testkey": "testvalue"} + + mock_MultiCloudIdentityHelper.side_effect = [ + (0, MOCK_TOKEN_RESPONSE, status.HTTP_201_CREATED) + ] + + mock_MultiCloudServiceHelper.side_effect = [ + (0, MOCK_HEAT_LIST_RESPONSE1, status.HTTP_200_OK) + ] + + cloud_owner = "CloudOwner" + cloud_region_id = "Region1" + mock_stack_id = "MOCKED_HEAT_STACK_ID1" + + response = self._APIv1InfraWorkload.get(mock_request, cloud_owner, cloud_region_id, mock_stack_id) + self.assertEqual(response.status_code, status.HTTP_200_OK) + pass diff --git a/starlingx/starlingx/urls.py b/starlingx/starlingx/urls.py index d2a7c57f..5d597e46 100644 --- a/starlingx/starlingx/urls.py +++ b/starlingx/starlingx/urls.py @@ -15,6 +15,8 @@ from django.conf.urls import include, url from starlingx_base.registration import registration from newton_base.openoapi import tenants +from newton_base.resource import capacity +from newton_base.resource import infra_workload urlpatterns = [ url(r'^', include('starlingx.swagger.urls')), @@ -31,6 +33,8 @@ urlpatterns = [ tenants.Tenants.as_view()), url(r'^api/multicloud-starlingx/v0/(?P<vimid>[0-9a-zA-Z_-]+)/' '(?P<tenantid>[0-9a-zA-Z_-]{20,})/', include('starlingx.requests.urls')), + url(r'^api/multicloud-starlingx/v0/(?P<vimid>[0-9a-zA-Z_-]+)/capacity_check/?$', + capacity.CapacityCheck.as_view()), # API v1, depreciated due to MULTICLOUD-335 url(r'^api/multicloud-starlingx/v1/(?P<cloud_owner>[0-9a-zA-Z_-]+)/(?P<cloud_region_id>[0-9a-zA-Z_-]+)/registry/?$', @@ -43,4 +47,8 @@ urlpatterns = [ tenants.APIv1Tenants.as_view()), url(r'^api/multicloud-starlingx/v1/(?P<cloud_owner>[0-9a-zA-Z_-]+)/(?P<cloud_region_id>[0-9a-zA-Z_-]+)/' '(?P<tenantid>[0-9a-zA-Z_-]{20,})/', include('starlingx.requests.urlsV1')), + url(r'^api/multicloud-starlingx/v1/(?P<cloud_owner>[0-9a-zA-Z_-]+)/(?P<cloud_region_id>[0-9a-zA-Z_-]+)/capacity_check/?$', + capacity.APIv1CapacityCheck.as_view()), + url(r'^api/multicloud-starlingx/v1/(?P<cloud_owner>[0-9a-zA-Z_-]+)/(?P<cloud_region_id>[0-9a-zA-Z_-]+)/infra_workload/?$', + infra_workload.APIv1InfraWorkload.as_view()), ] |