diff --git a/setup.cfg b/setup.cfg index 580cd25..c8119a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,3 +56,8 @@ oslo.config.opts = tempest.test_plugins = trio2o_tests = trio2o.tempestplugin.plugin:Trio2oTempestPlugin + +trio2o.common.schedulers = + pod_manager = trio2o.common.scheduler.pod_manager:PodManager + bottom_pod_filter = trio2o.common.scheduler.filters.bottom_pod_filter:BottomPodFilter + filter_scheduler = trio2o.common.scheduler.filter_scheduler:FilterScheduler \ No newline at end of file diff --git a/trio2o/api/controllers/pod.py b/trio2o/api/controllers/pod.py index bb22442..69e3c46 100644 --- a/trio2o/api/controllers/pod.py +++ b/trio2o/api/controllers/pod.py @@ -223,7 +223,6 @@ class BindingsController(rest.RestController): pod_b = kw['pod_binding'] tenant_id = pod_b.get('tenant_id', '').strip() pod_id = pod_b.get('pod_id', '').strip() - _uuid = uuidutils.generate_uuid() if tenant_id == '' or pod_id == '': return Response( @@ -249,11 +248,7 @@ class BindingsController(rest.RestController): return try: - with context.session.begin(): - pod_binding = core.create_resource(context, models.PodBinding, - {'id': _uuid, - 'tenant_id': tenant_id, - 'pod_id': pod_id}) + pod_binding = db_api.create_pod_binding(context, tenant_id, pod_id) except db_exc.DBDuplicateEntry: return Response(_('Pod binding already exists'), 409) except db_exc.DBConstraintError: diff --git a/trio2o/cinder_apigw/controllers/volume.py b/trio2o/cinder_apigw/controllers/volume.py index 5451c88..385181d 100644 --- a/trio2o/cinder_apigw/controllers/volume.py +++ b/trio2o/cinder_apigw/controllers/volume.py @@ -29,6 +29,7 @@ import trio2o.common.context as t_context from trio2o.common import httpclient as hclient from trio2o.common.i18n import _ from trio2o.common.i18n import _LE +from trio2o.common.scheduler import filter_scheduler from trio2o.common import utils import trio2o.db.api as db_api @@ -42,6 +43,7 @@ class VolumeController(rest.RestController): def __init__(self, tenant_id): self.tenant_id = tenant_id + self.filter_scheduler = filter_scheduler.FilterScheduler() @expose(generic=True, template='json') def post(self, **kw): @@ -52,10 +54,9 @@ class VolumeController(rest.RestController): 400, _("Missing required element 'volume' in request body.")) az = kw['volume'].get('availability_zone', '') - pod, pod_az = az_ag.get_pod_by_az_tenant( - context, - az_name=az, - tenant_id=self.tenant_id) + pod, pod_az = self.filter_scheduler.select_destination( + context, az, self.tenant_id, pod_group='') + if not pod: LOG.error(_LE("Pod not configured or scheduling failure")) return utils.format_cinder_error( diff --git a/trio2o/common/az_ag.py b/trio2o/common/az_ag.py index 3db6cc0..a9ea660 100644 --- a/trio2o/common/az_ag.py +++ b/trio2o/common/az_ag.py @@ -147,7 +147,8 @@ def get_pod_by_az_tenant(context, az_name, tenant_id): context, models.PodBinding, {'id': uuidutils.generate_uuid(), 'tenant_id': tenant_id, - 'pod_id': pod['pod_id']}) + 'pod_id': pod['pod_id'], + 'is_binding': True}) return pod, pod['pod_az_name'] except Exception as e: LOG.error(_LE('Fail to create pod binding: %(exception)s'), diff --git a/trio2o/common/scheduler/__init__.py b/trio2o/common/scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trio2o/common/scheduler/driver.py b/trio2o/common/scheduler/driver.py new file mode 100644 index 0000000..e1020dd --- /dev/null +++ b/trio2o/common/scheduler/driver.py @@ -0,0 +1,31 @@ +# 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 six + +from stevedore import driver + + +@six.add_metaclass(abc.ABCMeta) +class Scheduler(object): + + def __init__(self): + self.pod_manager = driver.DriverManager( + namespace='trio2o.common.schedulers', + name='pod_manager', + invoke_on_load=True + ).driver + + @abc.abstractmethod + def select_destination(self, context, az_name, tenant_id, spec_obj): + return None, None diff --git a/trio2o/common/scheduler/filter_scheduler.py b/trio2o/common/scheduler/filter_scheduler.py new file mode 100644 index 0000000..7f5f300 --- /dev/null +++ b/trio2o/common/scheduler/filter_scheduler.py @@ -0,0 +1,58 @@ +# 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 trio2o.common.scheduler import driver + + +class FilterScheduler(driver.Scheduler): + + def __init__(self, *args, **kwargs): + super(FilterScheduler, self).__init__(*args, **kwargs) + + def select_destination(self, context, az_name, tenant_id, pod_group): + current_binding, current_pod = \ + self.pod_manager.get_current_binding_and_pod( + context, az_name, tenant_id, pod_group) + + if current_binding and current_pod: + return current_pod, current_pod['pod_az_name'] + else: + pods = self.pod_manager.get_available_pods( + context, az_name, pod_group) + if not pods: + return None, None + # TODO(Yipei): Weigh pods and select one whose weight + # is the maximum. Here we chose one randomly. + is_current = False + best_pod = None + # select the pod by a circle in pods + for pod in pods: + if is_current: + best_pod = pod + break + if current_binding \ + and pod['pod_id'] == current_binding['pod_id']: + is_current = True + if is_current and len(pods) == 1: + return None, None + if not best_pod: + best_pod = pods[0] + + if current_binding: + is_successful = self.pod_manager.update_binding( + context, current_binding, best_pod['pod_id']) + else: + is_successful = self.pod_manager.create_binding( + context, tenant_id, best_pod['pod_id']) + if not is_successful: + return None, None + return best_pod, best_pod['pod_az_name'] diff --git a/trio2o/common/scheduler/filters/__init__.py b/trio2o/common/scheduler/filters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trio2o/common/scheduler/filters/base_filters.py b/trio2o/common/scheduler/filters/base_filters.py new file mode 100644 index 0000000..fbad0df --- /dev/null +++ b/trio2o/common/scheduler/filters/base_filters.py @@ -0,0 +1,31 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class BaseFilter(object): + """Base class for all pod filter classes.""" + def _filter_one(self, obj, pod_group): + return True + + def filter_all(self, filter_obj_list, pod_group): + for obj in filter_obj_list: + if self._filter_one(obj, pod_group): + yield obj + + +class BasePodFilter(BaseFilter): + + def _filter_one(self, obj, pod_group): + return self.is_pod_passed(obj, pod_group) + + def is_pod_passed(self, pod, pod_group): + raise NotImplementedError() diff --git a/trio2o/common/scheduler/filters/bottom_pod_filter.py b/trio2o/common/scheduler/filters/bottom_pod_filter.py new file mode 100644 index 0000000..8060a35 --- /dev/null +++ b/trio2o/common/scheduler/filters/bottom_pod_filter.py @@ -0,0 +1,23 @@ +# 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 trio2o.common.scheduler.filters import base_filters + + +class BottomPodFilter(base_filters.BasePodFilter): + """Returns all bottom pods.""" + + def is_pod_passed(self, pod, pod_group): + flag = False + if pod['az_name'] != '': + flag = True + return flag diff --git a/trio2o/common/scheduler/pod_manager.py b/trio2o/common/scheduler/pod_manager.py new file mode 100644 index 0000000..06ebe64 --- /dev/null +++ b/trio2o/common/scheduler/pod_manager.py @@ -0,0 +1,109 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from stevedore import driver + +from trio2o.common.i18n import _LE +from trio2o.db import api as db_api + +LOG = logging.getLogger(__name__) + + +class PodManager(object): + def __init__(self): + filter_names = ['bottom_pod_filter'] + self.default_filters = self._choose_pod_filters(filter_names) + + @staticmethod + def _choose_pod_filters(filter_names): + good_filters = [] + for filter_name in filter_names: + filter_ = driver.DriverManager( + 'trio2o.common.schedulers', + filter_name, + invoke_on_load=True + ).driver + good_filters.append(filter_) + return good_filters + + @staticmethod + def get_current_binding_and_pod(context, az_name, tenant_id, pod_group): + filter_b = [{'key': 'tenant_id', 'comparator': 'eq', + 'value': tenant_id}] + current_bindings = db_api.get_pod_binding_by_tenant_id( + context, filter_b) + if not current_bindings: + return None, None + + has_available_pods = False + for pod_b in current_bindings: + if pod_b['is_binding']: + pod = db_api.get_pod_by_pod_id(context, pod_b['pod_id']) + if az_name and pod['az_name'] == az_name: + has_available_pods = True + elif az_name == '' and pod['az_name'] != '': + # if the az_name is not specified, a default bottom + # pod will be selected + has_available_pods = True + + if has_available_pods: + # TODO(Yipei): check resource_affinity_tag + # if the resource utilization of the pod reaches the limit, + # return [], []. Considering the feature of checking + # resource utilization is not implemented, we use + # resource_affinity_tag to test the logic of updating + # a binding relationship. + if pod_group != '': + return pod_b, None + # TODO(Yipei): check resource utilization of the pod + # if the resource utilization of the pod reaches the limit, + # return pod_b, [] + + # If a pod passes the above checking, both the pod and its + # corresponding binding are returned. + return pod_b, pod + return None, None + + @staticmethod + def create_binding(context, tenant_id, pod_id): + try: + db_api.create_pod_binding(context, tenant_id, pod_id) + except Exception as e: + LOG.error(_LE('Fail to create pod binding: %(exception)s'), + {'exception': e}) + return False + return True + + @staticmethod + def update_binding(context, current_binding, pod_id): + current_binding['is_binding'] = False + try: + db_api.change_pod_binding( + context, current_binding, pod_id) + except Exception as e: + LOG.error(_LE('Fail to update pod binding: %(exception)s'), + {'exception': e}) + return False + return True + + def get_available_pods(self, context, az_name, pod_group): + if az_name != '': + filter_q = [{'key': 'az_name', + 'comparator': 'eq', 'value': az_name}] + else: + filter_q = None + pods = db_api.list_pods(context, filter_q) + for filter_ in self.default_filters: + objs_ = filter_.filter_all(pods, pod_group) + pods = list(objs_) + return pods diff --git a/trio2o/db/api.py b/trio2o/db/api.py index 9d246e6..3d33092 100644 --- a/trio2o/db/api.py +++ b/trio2o/db/api.py @@ -67,12 +67,42 @@ def update_pod(context, pod_id, update_dict): return core.update_resource(context, models.Pod, pod_id, update_dict) +def change_pod_binding(context, pod_binding, pod_id): + with context.session.begin(): + core.update_resource(context, models.PodBinding, + pod_binding['id'], pod_binding) + core.create_resource(context, models.PodBinding, + {'id': uuidutils.generate_uuid(), + 'tenant_id': pod_binding['tenant_id'], + 'pod_id': pod_id, + 'is_binding': True}) + + +def get_pod_binding_by_tenant_id(context, filter_): + with context.session.begin(): + return core.query_resource(context, models.PodBinding, filter_, []) + + +def get_pod_by_pod_id(context, pod_id): + with context.session.begin(): + return core.get_resource(context, models.Pod, pod_id) + + def create_pod_service_configuration(context, config_dict): with context.session.begin(): return core.create_resource(context, models.PodServiceConfiguration, config_dict) +def create_pod_binding(context, tenant_id, pod_id): + with context.session.begin(): + return core.create_resource(context, models.PodBinding, + {'id': uuidutils.generate_uuid(), + 'tenant_id': tenant_id, + 'pod_id': pod_id, + 'is_binding': True}) + + def delete_pod_service_configuration(context, config_id): with context.session.begin(): return core.delete_resource(context, models.PodServiceConfiguration, diff --git a/trio2o/db/migrate_repo/versions/001_init.py b/trio2o/db/migrate_repo/versions/001_init.py index 8c41e62..8859eb7 100644 --- a/trio2o/db/migrate_repo/versions/001_init.py +++ b/trio2o/db/migrate_repo/versions/001_init.py @@ -47,6 +47,7 @@ def upgrade(migrate_engine): sql.Column('id', sql.String(36), primary_key=True), sql.Column('tenant_id', sql.String(length=255), nullable=False), sql.Column('pod_id', sql.String(length=255), nullable=False), + sql.Column('is_binding', sql.Boolean, nullable=False), sql.Column('created_at', sql.DateTime), sql.Column('updated_at', sql.DateTime), migrate.UniqueConstraint( diff --git a/trio2o/db/models.py b/trio2o/db/models.py index 818ef0e..be2c315 100644 --- a/trio2o/db/models.py +++ b/trio2o/db/models.py @@ -420,7 +420,7 @@ class PodBinding(core.ModelBase, core.DictBase, models.TimestampMixin): 'tenant_id', 'pod_id', name='pod_binding0tenant_id0pod_id'), ) - attributes = ['id', 'tenant_id', 'pod_id', + attributes = ['id', 'tenant_id', 'pod_id', 'is_binding', 'created_at', 'updated_at'] id = sql.Column(sql.String(36), primary_key=True) @@ -428,6 +428,7 @@ class PodBinding(core.ModelBase, core.DictBase, models.TimestampMixin): pod_id = sql.Column('pod_id', sql.String(36), sql.ForeignKey('cascaded_pods.pod_id'), nullable=False) + is_binding = sql.Column('is_binding', sql.Boolean, nullable=False) # Routing Model diff --git a/trio2o/nova_apigw/controllers/server.py b/trio2o/nova_apigw/controllers/server.py index a9209a2..c0d6e39 100644 --- a/trio2o/nova_apigw/controllers/server.py +++ b/trio2o/nova_apigw/controllers/server.py @@ -20,7 +20,6 @@ import six import oslo_log.log as logging -from trio2o.common import az_ag import trio2o.common.client as t_client from trio2o.common import constants import trio2o.common.context as t_context @@ -29,12 +28,14 @@ from trio2o.common.i18n import _ from trio2o.common.i18n import _LE import trio2o.common.lock_handle as t_lock from trio2o.common.quota import QUOTAS +from trio2o.common.scheduler import filter_scheduler from trio2o.common import utils from trio2o.common import xrpcapi import trio2o.db.api as db_api from trio2o.db import core from trio2o.db import models + LOG = logging.getLogger(__name__) MAX_METADATA_KEY_LENGTH = 255 @@ -47,6 +48,7 @@ class ServerController(rest.RestController): self.project_id = project_id self.clients = {constants.TOP: t_client.Client()} self.xjob_handler = xrpcapi.XJobAPI() + self.filter_scheduler = filter_scheduler.FilterScheduler() def _get_client(self, pod_name=constants.TOP): if pod_name not in self.clients: @@ -112,9 +114,9 @@ class ServerController(rest.RestController): 400, _('server is not set')) az = kw['server'].get('availability_zone', '') + pod, b_az = self.filter_scheduler.select_destination( + context, az, self.project_id, pod_group='') - pod, b_az = az_ag.get_pod_by_az_tenant( - context, az, self.project_id) if not pod: return utils.format_nova_error( 500, _('Pod not configured or scheduling failure')) diff --git a/trio2o/tests/functional/api/controllers/test_pod.py b/trio2o/tests/functional/api/controllers/test_pod.py index d97bf83..9771ea5 100644 --- a/trio2o/tests/functional/api/controllers/test_pod.py +++ b/trio2o/tests/functional/api/controllers/test_pod.py @@ -613,7 +613,8 @@ class TestBindingController(API_FunctionalTest): "pod_binding": { "tenant_id": "dddddd", - "pod_id": "0ace0db2-ef33-43a6-a150-42703ffda643" + "pod_id": "0ace0db2-ef33-43a6-a150-42703ffda643", + "is_binding": "True" }, "expected_error": 200 }, @@ -622,7 +623,8 @@ class TestBindingController(API_FunctionalTest): "pod_binding": { "tenant_id": "aaaaa", - "pod_id": "0ace0db2-ef33-43a6-a150-42703ffda643" + "pod_id": "0ace0db2-ef33-43a6-a150-42703ffda643", + "is_binding": "True" }, "expected_error": 200 }, diff --git a/trio2o/tests/unit/common/scheduler/__init__.py b/trio2o/tests/unit/common/scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trio2o/tests/unit/common/scheduler/test_filter_scheduler.py b/trio2o/tests/unit/common/scheduler/test_filter_scheduler.py new file mode 100644 index 0000000..bfd22c1 --- /dev/null +++ b/trio2o/tests/unit/common/scheduler/test_filter_scheduler.py @@ -0,0 +1,141 @@ +# 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 trio2o.common import context +from trio2o.common.scheduler import filter_scheduler +from trio2o.db import api +from trio2o.db import core +from trio2o.db import models +import unittest + + +class FilterSchedulerTest(unittest.TestCase): + + def setUp(self): + core.initialize() + core.ModelBase.metadata.create_all(core.get_engine()) + self.context = context.Context() + self.project_id = 'test_fs_project' + self.az_name_1 = 'b_az_fs_1' + self.az_name_2 = 'b_az_fs_2' + self.filter_scheduler = filter_scheduler.FilterScheduler() + + def _prepare_binding(self, pod_id): + binding = {'tenant_id': self.project_id, + 'pod_id': pod_id, + 'is_binding': True} + api.create_pod_binding(self.context, self.project_id, pod_id) + return binding + + def test_select_destination(self): + b_pod_1 = {'pod_id': 'b_pod_fs_uuid_1', 'pod_name': 'b_region_fs_1', + 'az_name': self.az_name_1} + api.create_pod(self.context, b_pod_1) + b_pod_2 = {'pod_id': 'b_pod_fs_uuid_2', 'pod_name': 'b_region_fs_2', + 'az_name': self.az_name_2} + api.create_pod(self.context, b_pod_2) + b_pod_3 = {'pod_id': 'b_pod_fs_uuid_3', 'pod_name': 'b_region_fs_3', + 'az_name': self.az_name_2} + api.create_pod(self.context, b_pod_3) + + t_pod = {'pod_id': 'b_pod_fs_uuid_t_pod', + 'pod_name': 'b_region_fs_t_pod', + 'az_name': ''} + api.create_pod(self.context, t_pod) + self._prepare_binding(b_pod_1['pod_id']) + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(binding_q[0]['pod_id'], b_pod_1['pod_id']) + self.assertEqual(binding_q[0]['tenant_id'], self.project_id) + self.assertEqual(binding_q[0]['is_binding'], True) + + pod_1, _ = self.filter_scheduler.select_destination( + self.context, '', self.project_id, pod_group='') + self.assertEqual(pod_1['pod_id'], b_pod_1['pod_id']) + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(len(binding_q), 1) + self.assertEqual(binding_q[0]['pod_id'], pod_1['pod_id']) + self.assertEqual(binding_q[0]['tenant_id'], self.project_id) + self.assertEqual(binding_q[0]['is_binding'], True) + + pod_2, _ = self.filter_scheduler.select_destination( + self.context, '', 'new_project', pod_group='') + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': 'new_project'}], []) + self.assertEqual(len(binding_q), 1) + self.assertEqual(binding_q[0]['pod_id'], pod_2['pod_id']) + self.assertEqual(binding_q[0]['tenant_id'], 'new_project') + self.assertEqual(binding_q[0]['is_binding'], True) + + pod_3, _ = self.filter_scheduler.select_destination( + self.context, self.az_name_1, 'new_project', pod_group='') + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': 'new_project'}], []) + self.assertEqual(len(binding_q), 1) + self.assertEqual(binding_q[0]['pod_id'], pod_3['pod_id']) + self.assertEqual(binding_q[0]['tenant_id'], 'new_project') + self.assertEqual(binding_q[0]['is_binding'], True) + + pod_4, _ = self.filter_scheduler.select_destination( + self.context, self.az_name_2, 'new_project', pod_group='') + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': 'new_project'}], []) + self.assertEqual(len(binding_q), 2) + self.assertEqual(binding_q[1]['pod_id'], pod_4['pod_id']) + self.assertEqual(binding_q[1]['tenant_id'], 'new_project') + self.assertEqual(binding_q[1]['is_binding'], True) + + pod_5, _ = self.filter_scheduler.select_destination( + self.context, self.az_name_2, self.project_id, pod_group='') + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(len(binding_q), 2) + self.assertEqual(pod_5['az_name'], self.az_name_2) + self.assertEqual(binding_q[1]['pod_id'], pod_5['pod_id']) + self.assertEqual(binding_q[1]['tenant_id'], self.project_id) + self.assertEqual(binding_q[1]['is_binding'], True) + + pod_6, _ = self.filter_scheduler.select_destination( + self.context, self.az_name_1, self.project_id, pod_group='test') + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(len(binding_q), 2) + self.assertEqual(pod_6, None) + + pod_7, _ = self.filter_scheduler.select_destination( + self.context, self.az_name_2, self.project_id, pod_group='test') + binding_q = core.query_resource( + self.context, models.PodBinding, [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(len(binding_q), 3) + self.assertEqual(pod_7['az_name'], self.az_name_2) + self.assertEqual(binding_q[1]['tenant_id'], self.project_id) + self.assertEqual(binding_q[1]['is_binding'], False) + self.assertEqual(binding_q[2]['pod_id'], pod_7['pod_id']) + self.assertEqual(binding_q[2]['tenant_id'], self.project_id) + self.assertEqual(binding_q[2]['is_binding'], True) diff --git a/trio2o/tests/unit/common/scheduler/test_pod_manager.py b/trio2o/tests/unit/common/scheduler/test_pod_manager.py new file mode 100644 index 0000000..307430b --- /dev/null +++ b/trio2o/tests/unit/common/scheduler/test_pod_manager.py @@ -0,0 +1,141 @@ +# 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 stevedore import driver + +from trio2o.common import context +from trio2o.db import api +from trio2o.db import core +from trio2o.db import models + +import unittest + + +class PodManagerTest(unittest.TestCase): + def setUp(self): + core.initialize() + core.ModelBase.metadata.create_all(core.get_engine()) + self.context = context.Context() + self.project_id = 'test_pm_project' + self.az_name_2 = 'b_az_pm_2' + self.az_name_1 = 'b_az_pm_1' + self.pod_manager = driver.DriverManager( + namespace='trio2o.common.schedulers', + name='pod_manager', + invoke_on_load=True + ).driver + self.b_pod_1 = {'pod_id': 'b_pod_pm_uuid_1', + 'pod_name': 'b_region_pm_1', + 'az_name': self.az_name_1} + + self.b_pod_2 = {'pod_id': 'b_pod_pm_uuid_2', + 'pod_name': 'b_region_pm_2', + 'az_name': self.az_name_2} + + self.b_pod_3 = {'pod_id': 'b_pod_pm_uuid_3', + 'pod_name': 'b_region_pm_3', + 'az_name': self.az_name_2} + + self.b_pod_4 = {'pod_id': 'b_pod_pm_uuid_4', + 'pod_name': 'b_region_pm_4', + 'az_name': self.az_name_2} + + def test_get_current_binding_and_pod(self): + api.create_pod(self.context, self.b_pod_1) + api.create_pod_binding( + self.context, self.project_id, self.b_pod_1['pod_id']) + + pod_b_1, pod_1 = self.pod_manager.get_current_binding_and_pod( + self.context, self.az_name_1, self.project_id, pod_group='') + binding_q = core.query_resource( + self.context, models.PodBinding, + [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(len(binding_q), 1) + self.assertEqual(binding_q[0]['id'], pod_b_1['id']) + + pod_b_2, pod_2 = self.pod_manager.get_current_binding_and_pod( + self.context, self.az_name_1, 'new_project_pm_1', pod_group='') + binding_q = core.query_resource( + self.context, models.PodBinding, + [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': 'new_project_pm_1'}], []) + self.assertEqual(len(binding_q), 0) + self.assertEqual(pod_b_2, None) + self.assertEqual(pod_2, None) + + pod_b_3, pod_3 = self.pod_manager.get_current_binding_and_pod( + self.context, 'unknown_az', self.project_id, pod_group='') + binding_q = core.query_resource( + self.context, models.PodBinding, + [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(len(binding_q), 1) + self.assertEqual(pod_b_3, None) + self.assertEqual(pod_3, None) + + pod_b_4, pod_4 = self.pod_manager.get_current_binding_and_pod( + self.context, self.az_name_1, self.project_id, pod_group='test') + binding_q = core.query_resource( + self.context, models.PodBinding, + [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': self.project_id}], []) + self.assertEqual(len(binding_q), 1) + self.assertEqual(pod_b_4['id'], binding_q[0]['id']) + self.assertEqual(pod_4, None) + + def test_create_binding(self): + api.create_pod(self.context, self.b_pod_2) + flag = self.pod_manager.create_binding( + self.context, 'new_project_pm_2', self.b_pod_2['pod_id']) + self.assertEqual(flag, True) + binding_q = core.query_resource( + self.context, models.PodBinding, + [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': 'new_project_pm_2'}], []) + self.assertEqual(len(binding_q), 1) + self.assertEqual(binding_q[0]['pod_id'], self.b_pod_2['pod_id']) + self.assertEqual(binding_q[0]['tenant_id'], 'new_project_pm_2') + self.assertEqual(binding_q[0]['is_binding'], True) + + def test_update_binding(self): + api.create_pod(self.context, self.b_pod_4) + api.create_pod(self.context, self.b_pod_3) + flag = self.pod_manager.create_binding( + self.context, 'new_project_pm_3', self.b_pod_3['pod_id']) + self.assertEqual(flag, True) + current_binding = core.query_resource( + self.context, models.PodBinding, + [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': 'new_project_pm_3'}], []) + + flag = self.pod_manager.update_binding( + self.context, current_binding[0], self.b_pod_4['pod_id']) + self.assertEqual(flag, True) + binding_q = core.query_resource( + self.context, models.PodBinding, + [{'key': 'tenant_id', + 'comparator': 'eq', + 'value': 'new_project_pm_3'}], []) + self.assertEqual(len(binding_q), 2) + self.assertEqual(binding_q[0]['pod_id'], self.b_pod_3['pod_id']) + self.assertEqual(binding_q[0]['tenant_id'], 'new_project_pm_3') + self.assertEqual(binding_q[0]['is_binding'], False) + self.assertEqual(binding_q[1]['pod_id'], self.b_pod_4['pod_id']) + self.assertEqual(binding_q[1]['tenant_id'], 'new_project_pm_3') + self.assertEqual(binding_q[1]['is_binding'], True) diff --git a/trio2o/tests/unit/common/test_az_ag.py b/trio2o/tests/unit/common/test_az_ag.py index a93c9ed..9daeab5 100644 --- a/trio2o/tests/unit/common/test_az_ag.py +++ b/trio2o/tests/unit/common/test_az_ag.py @@ -133,6 +133,7 @@ class AZAGTest(unittest.TestCase): self.assertEqual(pod2['pod_name'], FAKE_SITE_NAME) self.assertEqual(pod2['pod_id'], FAKE_SITE_ID) self.assertEqual(pod2['az_name'], FAKE_AZ) + else: self.assertEqual(pod2['pod_name'], FAKE_SITE_NAME_2) self.assertEqual(pod2['pod_id'], FAKE_SITE_ID_2) diff --git a/trio2o/tests/unit/nova_apigw/controllers/test_server.py b/trio2o/tests/unit/nova_apigw/controllers/test_server.py index 1f9ad5c..8143f00 100644 --- a/trio2o/tests/unit/nova_apigw/controllers/test_server.py +++ b/trio2o/tests/unit/nova_apigw/controllers/test_server.py @@ -26,6 +26,7 @@ from trio2o.common import constants from trio2o.common import context import trio2o.common.exceptions as t_exceptions from trio2o.common import lock_handle +from trio2o.common.scheduler import filter_scheduler from trio2o.common import xrpcapi from trio2o.db import api from trio2o.db import core @@ -82,6 +83,7 @@ class FakeServerController(server.ServerController): self.clients = {'t_region': FakeClient('t_region')} self.project_id = project_id self.xjob_handler = xrpcapi.XJobAPI() + self.filter_scheduler = filter_scheduler.FilterScheduler() def _get_client(self, pod_name=None): if not pod_name: