From 51f6f7258b76b19962f748ff88936bb771cc5460 Mon Sep 17 00:00:00 2001 From: ZhaoBo Date: Wed, 5 Apr 2017 13:59:57 +0800 Subject: [PATCH] Extend api to accept qos_policy_id This patch extend Octavia v2 API to access qos_policy_id from neutron. Users can pass it as 'vip_qos_policy_id' to Octavia request body to create/update Loadbalancers, and the vrrp ports will have the qos abilities. This patch modifies the Loadbalancer Post/Put request body and response body. It also extends the 'vip' table with the new column named 'qos_policy_id' to store the qos_id from neutron. Co-Authored-By: Reedip Change-Id: I43aba9d2ae816b1498d16da077936d6bdb62e30a --- api-ref/source/parameters.yaml | 12 ++ .../v2/examples/loadbalancer-create-curl | 2 +- .../examples/loadbalancer-create-request.json | 3 +- .../loadbalancer-create-response.json | 3 +- .../loadbalancer-full-create-request.json | 3 +- .../loadbalancer-full-create-response.json | 3 +- .../examples/loadbalancer-show-response.json | 3 +- .../v2/examples/loadbalancer-update-curl | 2 +- .../examples/loadbalancer-update-request.json | 3 +- .../loadbalancer-update-response.json | 3 +- .../examples/loadbalancers-list-response.json | 3 +- api-ref/source/v2/loadbalancer.inc | 10 ++ octavia/api/v2/controllers/load_balancer.py | 18 ++- octavia/api/v2/types/load_balancer.py | 6 +- octavia/common/data_models.py | 3 +- octavia/common/validate.py | 10 ++ .../worker/flows/load_balancer_flows.py | 5 + octavia/controller/worker/task_utils.py | 13 ++ .../controller/worker/tasks/database_tasks.py | 6 + .../controller/worker/tasks/model_tasks.py | 17 +- .../controller/worker/tasks/network_tasks.py | 49 ++++++ ...0512_extend_api_to_accept_qos_policy_id.py | 37 +++++ octavia/db/models.py | 1 + octavia/network/base.py | 4 + octavia/network/data_models.py | 8 +- octavia/network/drivers/neutron/base.py | 15 ++ octavia/network/drivers/neutron/utils.py | 8 +- octavia/network/drivers/noop_driver/driver.py | 19 +++ octavia/tests/common/constants.py | 3 + .../functional/api/v2/test_load_balancer.py | 150 ++++++++++++++++++ .../tests/functional/db/test_repositories.py | 3 +- .../unit/api/v2/types/test_load_balancers.py | 8 +- octavia/tests/unit/common/test_validations.py | 15 ++ .../worker/flows/test_load_balancer_flows.py | 3 +- .../worker/tasks/test_database_tasks.py | 28 ++++ .../worker/tasks/test_model_tasks.py | 13 +- .../worker/tasks/test_network_tasks.py | 133 +++++++++++++++- .../unit/controller/worker/test_task_utils.py | 19 +++ .../unit/network/drivers/neutron/test_base.py | 36 +++++ .../drivers/test_network_noop_driver.py | 19 +++ ...accept_qos_policy_id-128ab592a735f3b8.yaml | 5 + 41 files changed, 681 insertions(+), 23 deletions(-) create mode 100644 octavia/db/migration/alembic_migrations/versions/0aee2b450512_extend_api_to_accept_qos_policy_id.py create mode 100644 releasenotes/notes/extend_api_to_accept_qos_policy_id-128ab592a735f3b8.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 2adfcce803..50e7eec28f 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1059,6 +1059,18 @@ vip_port_id-optional: in: body required: false type: string +vip_qos_policy_id: + description: | + The ID of the QoS Policy which will apply to the Virtual IP (VIP). + in: body + required: true + type: string +vip_qos_policy_id-optional: + description: | + The ID of the QoS Policy which will apply to the Virtual IP (VIP). + in: body + required: false + type: string vip_subnet_id: description: | The ID of the subnet for the Virtual IP (VIP). diff --git a/api-ref/source/v2/examples/loadbalancer-create-curl b/api-ref/source/v2/examples/loadbalancer-create-curl index 8247da247f..853065b7cd 100644 --- a/api-ref/source/v2/examples/loadbalancer-create-curl +++ b/api-ref/source/v2/examples/loadbalancer-create-curl @@ -1 +1 @@ -curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"loadbalancer": {"description": "My favorite load balancer", "admin_state_up": true, "project_id": "e3cd678b11784734bc366148aa37580e", "flavor": "a7ae5d5a-d855-4f9a-b187-af66b53f4d04", "vip_subnet_id": "d4af86e1-0051-488c-b7a0-527f97490c9a", "vip_address": "203.0.113.50", "provider": "octavia", "name": "best_load_balancer"}}' http://198.51.100.10:9876/v2.0/lbaas/loadbalancers +curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"loadbalancer": {"description": "My favorite load balancer", "admin_state_up": true, "project_id": "e3cd678b11784734bc366148aa37580e", "flavor": "a7ae5d5a-d855-4f9a-b187-af66b53f4d04", "vip_subnet_id": "d4af86e1-0051-488c-b7a0-527f97490c9a", "vip_address": "203.0.113.50", "provider": "octavia", "name": "best_load_balancer", "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3"}}' http://198.51.100.10:9876/v2.0/lbaas/loadbalancers diff --git a/api-ref/source/v2/examples/loadbalancer-create-request.json b/api-ref/source/v2/examples/loadbalancer-create-request.json index e13e995023..cf605c6b7d 100644 --- a/api-ref/source/v2/examples/loadbalancer-create-request.json +++ b/api-ref/source/v2/examples/loadbalancer-create-request.json @@ -6,6 +6,7 @@ "vip_subnet_id": "d4af86e1-0051-488c-b7a0-527f97490c9a", "vip_address": "203.0.113.50", "provider": "octavia", - "name": "best_load_balancer" + "name": "best_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } } diff --git a/api-ref/source/v2/examples/loadbalancer-create-response.json b/api-ref/source/v2/examples/loadbalancer-create-response.json index 4e57145cc2..fdc96246e4 100644 --- a/api-ref/source/v2/examples/loadbalancer-create-response.json +++ b/api-ref/source/v2/examples/loadbalancer-create-response.json @@ -14,6 +14,7 @@ "updated_at": "2017-02-28T00:43:30", "id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "operating_status": "OFFLINE", - "name": "best_load_balancer" + "name": "best_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } } diff --git a/api-ref/source/v2/examples/loadbalancer-full-create-request.json b/api-ref/source/v2/examples/loadbalancer-full-create-request.json index bc779e30a7..2f21071dee 100644 --- a/api-ref/source/v2/examples/loadbalancer-full-create-request.json +++ b/api-ref/source/v2/examples/loadbalancer-full-create-request.json @@ -82,6 +82,7 @@ "vip_subnet_id": "d4af86e1-0051-488c-b7a0-527f97490c9a", "vip_address": "203.0.113.50", "provider": "octavia", - "name": "best_load_balancer" + "name": "best_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } } diff --git a/api-ref/source/v2/examples/loadbalancer-full-create-response.json b/api-ref/source/v2/examples/loadbalancer-full-create-response.json index f6cd0faee0..a010a2bd21 100644 --- a/api-ref/source/v2/examples/loadbalancer-full-create-response.json +++ b/api-ref/source/v2/examples/loadbalancer-full-create-response.json @@ -172,6 +172,7 @@ "updated_at": "2017-02-28T00:43:30", "id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "operating_status": "ONLINE", - "name": "best_load_balancer" + "name": "best_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } } diff --git a/api-ref/source/v2/examples/loadbalancer-show-response.json b/api-ref/source/v2/examples/loadbalancer-show-response.json index 900a47eb42..81a5b1bb79 100644 --- a/api-ref/source/v2/examples/loadbalancer-show-response.json +++ b/api-ref/source/v2/examples/loadbalancer-show-response.json @@ -14,6 +14,7 @@ "updated_at": "2017-02-28T00:43:30", "id": "8a562351-f0fb-424c-a0af-513461424ea5", "operating_status": "ONLINE", - "name": "best_load_balancer" + "name": "best_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } } diff --git a/api-ref/source/v2/examples/loadbalancer-update-curl b/api-ref/source/v2/examples/loadbalancer-update-curl index c596b6b852..a3116c57e9 100644 --- a/api-ref/source/v2/examples/loadbalancer-update-curl +++ b/api-ref/source/v2/examples/loadbalancer-update-curl @@ -1 +1 @@ -curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"loadbalancer": {"description": "Temporarily disabled load balancer", "admin_state_up": false, "name": "disabled_load_balancer"}}' http://198.51.100.10:9876/v2.0/lbaas/loadbalancers/8b6fc468-07d5-4d8b-a0b9-695060e72c31 +curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: " -d '{"loadbalancer": {"description": "Temporarily disabled load balancer", "admin_state_up": false, "name": "disabled_load_balancer", "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3"}}' http://198.51.100.10:9876/v2.0/lbaas/loadbalancers/8b6fc468-07d5-4d8b-a0b9-695060e72c31 diff --git a/api-ref/source/v2/examples/loadbalancer-update-request.json b/api-ref/source/v2/examples/loadbalancer-update-request.json index eae4567bc8..f173ac9e3e 100644 --- a/api-ref/source/v2/examples/loadbalancer-update-request.json +++ b/api-ref/source/v2/examples/loadbalancer-update-request.json @@ -2,6 +2,7 @@ "loadbalancer": { "description": "Temporarily disabled load balancer", "admin_state_up": false, - "name": "disabled_load_balancer" + "name": "disabled_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } } diff --git a/api-ref/source/v2/examples/loadbalancer-update-response.json b/api-ref/source/v2/examples/loadbalancer-update-response.json index 6845b00407..c31fe28aba 100644 --- a/api-ref/source/v2/examples/loadbalancer-update-response.json +++ b/api-ref/source/v2/examples/loadbalancer-update-response.json @@ -14,6 +14,7 @@ "updated_at": "2017-02-28T00:43:30", "id": "8b6fc468-07d5-4d8b-a0b9-695060e72c31", "operating_status": "ONLINE", - "name": "disabled_load_balancer" + "name": "disabled_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } } diff --git a/api-ref/source/v2/examples/loadbalancers-list-response.json b/api-ref/source/v2/examples/loadbalancers-list-response.json index 4032fa2e81..23cd546482 100644 --- a/api-ref/source/v2/examples/loadbalancers-list-response.json +++ b/api-ref/source/v2/examples/loadbalancers-list-response.json @@ -25,7 +25,8 @@ "updated_at": "2017-02-28T00:43:30", "id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "operating_status": "ONLINE", - "name": "best_load_balancer" + "name": "best_load_balancer", + "vip_qos_policy_id": "ec4f78ca-8da8-4e99-8a1a-e3b94595a7a3" } ] } diff --git a/api-ref/source/v2/loadbalancer.inc b/api-ref/source/v2/loadbalancer.inc index 500e4b052d..f5da4d7117 100644 --- a/api-ref/source/v2/loadbalancer.inc +++ b/api-ref/source/v2/loadbalancer.inc @@ -63,6 +63,7 @@ Response Parameters - vip_network_id: vip_network_id - vip_port_id: vip_port_id - vip_subnet_id: vip_subnet_id + - vip_qos_policy_id: vip_qos_policy_id Response Example ---------------- @@ -106,6 +107,10 @@ using a pre-configured octavia flavor. Flavors are created by the operator to allow custom load balancer configurations, such as allocating more memory for the load balancer. +An optional ``vip_qos_policy_id`` attribute from Neutron can be used to +apply QoS policies on a loadbalancer VIP, also could pass a 'null' value to +remove QoS policies. + You can also specify the ``provider`` attribute when you create a load balancer. The ``provider`` attribute specifies which backend should be used to create the load balancer. This could be the default provider @@ -167,6 +172,7 @@ Request - vip_network_id: vip_network_id-optional - vip_port_id: vip_port_id-optional - vip_subnet_id: vip_subnet_id-optional + - vip_qos_policy_id: vip_qos_policy_id-optional Request Example ---------------- @@ -203,6 +209,7 @@ Response Parameters - vip_network_id: vip_network_id - vip_port_id: vip_port_id - vip_subnet_id: vip_subnet_id + - vip_qos_policy_id: vip_qos_policy_id Response Example ---------------- @@ -294,6 +301,7 @@ Response Parameters - vip_network_id: vip_network_id - vip_port_id: vip_port_id - vip_subnet_id: vip_subnet_id + - vip_qos_policy_id: vip_qos_policy_id Response Example ---------------- @@ -340,6 +348,7 @@ Request - loadbalancer: loadbalancer - loadbalancer_id: path-loadbalancer-id - name: name-optional + - vip_qos_policy_id: vip_qos_policy_id-optional Request Example --------------- @@ -376,6 +385,7 @@ Response Parameters - vip_network_id: vip_network_id - vip_port_id: vip_port_id - vip_subnet_id: vip_subnet_id + - vip_qos_policy_id: vip_qos_policy_id Response Example ---------------- diff --git a/octavia/api/v2/controllers/load_balancer.py b/octavia/api/v2/controllers/load_balancer.py index a1c9a392b6..ca9c19f081 100644 --- a/octavia/api/v2/controllers/load_balancer.py +++ b/octavia/api/v2/controllers/load_balancer.py @@ -132,6 +132,15 @@ class LoadBalancersController(base.BaseController): port = validate.port_exists(port_id=load_balancer.vip_port_id) load_balancer.vip_network_id = port.network_id + # validate the request vip port whether applied the qos_policy and + # store the port_qos_policy to loadbalancer obj if possible. The + # default behavior is that if 'vip_qos_policy_id' is specified in the + # request, it will override the qos_policy applied on vip_port. + port_qos_policy_id = port.qos_policy_id + if (port_qos_policy_id and + isinstance(load_balancer.vip_qos_policy_id, wtypes.UnsetType)): + load_balancer.vip_qos_policy_id = port_qos_policy_id + # Identify the subnet for this port if load_balancer.vip_subnet_id: validate.subnet_exists(subnet_id=load_balancer.vip_subnet_id) @@ -193,7 +202,9 @@ class LoadBalancersController(base.BaseController): subnet = validate.subnet_exists( subnet_id=load_balancer.vip_subnet_id) load_balancer.vip_network_id = subnet.network_id - + if load_balancer.vip_qos_policy_id: + validate.qos_policy_exists( + qos_policy_id=load_balancer.vip_qos_policy_id) validate.network_allowed_by_config(load_balancer.vip_network_id) def _create_vip_port_if_not_exist(self, load_balancer_db): @@ -418,6 +429,11 @@ class LoadBalancersController(base.BaseController): self._auth_validate_action(context, db_lb.project_id, constants.RBAC_PUT) + if (load_balancer.vip_qos_policy_id and + not isinstance(load_balancer.vip_qos_policy_id, + wtypes.UnsetType) and + db_lb.vip.qos_policy_id != load_balancer.vip_qos_policy_id): + validate.qos_policy_exists(load_balancer.vip_qos_policy_id) self._test_lb_status(context.session, id) try: LOG.info("Sending updated Load Balancer %s to the handler", id) diff --git a/octavia/api/v2/types/load_balancer.py b/octavia/api/v2/types/load_balancer.py index dfa8cd65bc..747c88840f 100644 --- a/octavia/api/v2/types/load_balancer.py +++ b/octavia/api/v2/types/load_balancer.py @@ -25,6 +25,7 @@ class BaseLoadBalancerType(types.BaseType): 'vip_subnet_id': 'vip.subnet_id', 'vip_port_id': 'vip.port_id', 'vip_network_id': 'vip.network_id', + 'vip_qos_policy_id': 'vip.qos_policy_id', 'admin_state_up': 'enabled'} _child_map = {'vip': { 'ip_address': 'vip_address', @@ -52,6 +53,7 @@ class LoadBalancerResponse(BaseLoadBalancerType): pools = wtypes.wsattr([types.IdOnlyType]) provider = wtypes.wsattr(wtypes.StringType()) flavor = wtypes.wsattr(wtypes.StringType()) + vip_qos_policy_id = wtypes.wsattr(wtypes.UuidType()) @classmethod def from_data_model(cls, data_model, children=False): @@ -62,7 +64,7 @@ class LoadBalancerResponse(BaseLoadBalancerType): result.vip_port_id = data_model.vip.port_id result.vip_address = data_model.vip.ip_address result.vip_network_id = data_model.vip.network_id - + result.vip_qos_policy_id = data_model.vip.qos_policy_id if cls._full_response(): listener_model = listener.ListenerFullResponse pool_model = pool.PoolFullResponse @@ -114,6 +116,7 @@ class LoadBalancerPOST(BaseLoadBalancerType): vip_port_id = wtypes.wsattr(wtypes.UuidType()) vip_subnet_id = wtypes.wsattr(wtypes.UuidType()) vip_network_id = wtypes.wsattr(wtypes.UuidType()) + vip_qos_policy_id = wtypes.wsattr(wtypes.UuidType()) project_id = wtypes.wsattr(wtypes.StringType(max_length=36)) listeners = wtypes.wsattr([listener.ListenerSingleCreate], default=[]) pools = wtypes.wsattr([pool.PoolSingleCreate], default=[]) @@ -134,6 +137,7 @@ class LoadBalancerPUT(BaseLoadBalancerType): name = wtypes.wsattr(wtypes.StringType(max_length=255)) description = wtypes.wsattr(wtypes.StringType(max_length=255)) + vip_qos_policy_id = wtypes.wsattr(wtypes.UuidType()) admin_state_up = wtypes.wsattr(bool) diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index 29f54e2a80..e7cbfc7955 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -450,13 +450,14 @@ class Vip(BaseDataModel): def __init__(self, load_balancer_id=None, ip_address=None, subnet_id=None, network_id=None, port_id=None, - load_balancer=None): + load_balancer=None, qos_policy_id=None): self.load_balancer_id = load_balancer_id self.ip_address = ip_address self.subnet_id = subnet_id self.network_id = network_id self.port_id = port_id self.load_balancer = load_balancer + self.qos_policy_id = qos_policy_id class SNI(BaseDataModel): diff --git a/octavia/common/validate.py b/octavia/common/validate.py index ba83b316d2..9d9d5a80c6 100644 --- a/octavia/common/validate.py +++ b/octavia/common/validate.py @@ -244,6 +244,16 @@ def subnet_exists(subnet_id): return subnet +def qos_policy_exists(qos_policy_id): + network_driver = utils.get_network_driver() + try: + qos_policy = network_driver.get_qos_policy(qos_policy_id) + except Exception: + raise exceptions.InvalidSubresource(resource='qos_policy', + id=qos_policy_id) + return qos_policy + + def network_exists_optionally_contains_subnet(network_id, subnet_id=None): """Raises an exception when a network does not exist. diff --git a/octavia/controller/worker/flows/load_balancer_flows.py b/octavia/controller/worker/flows/load_balancer_flows.py index a27a82dccf..38b201ed4c 100644 --- a/octavia/controller/worker/flows/load_balancer_flows.py +++ b/octavia/controller/worker/flows/load_balancer_flows.py @@ -311,6 +311,9 @@ class LoadBalancerFlows(object): new_LB_net_subflow.add(network_tasks.PlugVIP( requires=constants.LOADBALANCER, provides=constants.AMPS_DATA)) + new_LB_net_subflow.add(network_tasks.ApplyQos( + requires=(constants.LOADBALANCER, constants.AMPS_DATA, + constants.UPDATE_DICT))) new_LB_net_subflow.add(database_tasks.UpdateAmphoraVIPData( requires=constants.AMPS_DATA)) new_LB_net_subflow.add(database_tasks.ReloadLoadBalancer( @@ -339,6 +342,8 @@ class LoadBalancerFlows(object): rebind={constants.OBJECT: constants.LOADBALANCER}, requires=[constants.UPDATE_DICT])) + update_LB_flow.add(network_tasks.ApplyQos( + requires=(constants.LOADBALANCER, constants.UPDATE_DICT))) update_LB_flow.add(amphora_driver_tasks.ListenersUpdate( requires=[constants.LOADBALANCER, constants.LISTENERS])) update_LB_flow.add(database_tasks.UpdateLoadbalancerInDB( diff --git a/octavia/controller/worker/task_utils.py b/octavia/controller/worker/task_utils.py index affaf0b9c4..c5bc3f93be 100644 --- a/octavia/controller/worker/task_utils.py +++ b/octavia/controller/worker/task_utils.py @@ -233,3 +233,16 @@ class TaskUtils(object): LOG.error("Failed to update pool %(pool)s " "provisioning status to ERROR due to: " "%(except)s", {'pool': pool_id, 'except': e}) + + def get_current_loadbalancer_from_db(self, loadbalancer_id): + """Gets a Loadbalancer from db. + + :param: loadbalancer_id: Load balancer ID which to get from db + """ + try: + return self.loadbalancer_repo.get(db_apis.get_session(), + id=loadbalancer_id) + except Exception as e: + LOG.error("Failed to get loadbalancer &(loadbalancer)s " + "due to: %(except)s", + {'loadbalancer': loadbalancer_id, 'except': e}) diff --git a/octavia/controller/worker/tasks/database_tasks.py b/octavia/controller/worker/tasks/database_tasks.py index 3c04308021..b2ee0339cf 100644 --- a/octavia/controller/worker/tasks/database_tasks.py +++ b/octavia/controller/worker/tasks/database_tasks.py @@ -45,6 +45,7 @@ class BaseDatabaseTask(task.Task): self.health_mon_repo = repo.HealthMonitorRepository() self.listener_repo = repo.ListenerRepository() self.loadbalancer_repo = repo.LoadBalancerRepository() + self.vip_repo = repo.VipRepository() self.member_repo = repo.MemberRepository() self.pool_repo = repo.PoolRepository() self.amp_health_repo = repo.AmphoraHealthRepository() @@ -1272,6 +1273,11 @@ class UpdateLoadbalancerInDB(BaseDatabaseTask): """ LOG.debug("Update DB for loadbalancer id: %s ", loadbalancer.id) + if update_dict.get('vip'): + vip_dict = update_dict.pop('vip') + self.vip_repo.update(db_apis.get_session(), + loadbalancer.vip.load_balancer_id, + **vip_dict) self.loadbalancer_repo.update(db_apis.get_session(), loadbalancer.id, **update_dict) diff --git a/octavia/controller/worker/tasks/model_tasks.py b/octavia/controller/worker/tasks/model_tasks.py index d8a3fd67da..875ec22d68 100644 --- a/octavia/controller/worker/tasks/model_tasks.py +++ b/octavia/controller/worker/tasks/model_tasks.py @@ -13,6 +13,7 @@ # under the License. # +from octavia.common import data_models from taskflow import task @@ -28,5 +29,19 @@ class UpdateAttributes(task.Task): """Task to update an object for changes.""" def execute(self, object, update_dict): + """Update an object and its associated resources in nested way. - object.update(update_dict) + Such as LoadBalancer object, will nested update the Vip object if there + is any new field in PUT request. + :param object: The object will be updated. + :param update_dict: The PUT request body in dictionary type. + :returns: None + """ + for key, value in update_dict.items(): + if (hasattr(object, key) and + isinstance(getattr(object, key), + data_models.BaseDataModel) and + isinstance(value, dict)): + getattr(object, key).update(value) + else: + setattr(object, key, value) diff --git a/octavia/controller/worker/tasks/network_tasks.py b/octavia/controller/worker/tasks/network_tasks.py index 7fde9d9411..be08f1ad77 100644 --- a/octavia/controller/worker/tasks/network_tasks.py +++ b/octavia/controller/worker/tasks/network_tasks.py @@ -22,6 +22,7 @@ from taskflow.types import failure from octavia.common import constants from octavia.common import utils +from octavia.controller.worker import task_utils from octavia.network import base from octavia.network import data_models as n_data_models @@ -35,6 +36,7 @@ class BaseNetworkTask(task.Task): def __init__(self, **kwargs): super(BaseNetworkTask, self).__init__(**kwargs) self._network_driver = None + self.task_utils = task_utils.TaskUtils() @property def network_driver(self): @@ -452,3 +454,50 @@ class WaitForPortDetach(BaseNetworkTask): LOG.debug('Waiting for ports to detach from amphora: %(amp_id)s.', {'amp_id': amphora.id}) self.network_driver.wait_for_port_detach(amphora) + + +class ApplyQos(BaseNetworkTask): + """Apply Quality of Services to the VIP""" + + def _apply_qos_on_vrrp_ports(self, loadbalancer, amps_data, qos_policy_id, + is_revert=False, request_qos_id=None): + """Call network driver to apply QoS Policy on the vrrp ports.""" + if not amps_data: + amps_data = loadbalancer.amphorae + vrrp_port_ids = [amp.vrrp_port_id for amp in amps_data] + for port_id in vrrp_port_ids: + try: + self.network_driver.apply_qos_on_port(qos_policy_id, port_id) + except Exception: + if not is_revert: + raise + else: + LOG.warning('Failed to undo qos policy %(qos_id)s ' + 'on vrrp port: %(port)s from ' + 'amphorae: %(amp)s', + {'qos_id': request_qos_id, + 'port': vrrp_port_ids, + 'amp': [amp.id for amp in amps_data]}) + + def execute(self, loadbalancer, amps_data=None, update_dict=None): + """Apply qos policy on the vrrp ports which are related with vip.""" + qos_policy_id = loadbalancer.vip.qos_policy_id + if not qos_policy_id and ( + update_dict and ( + 'vip' not in update_dict or + 'qos_policy_id' not in update_dict['vip'])): + return + self._apply_qos_on_vrrp_ports(loadbalancer, amps_data, qos_policy_id) + + def revert(self, result, loadbalancer, amps_data=None, update_dict=None, + *args, **kwargs): + """Handle a failure to apply QoS to VIP""" + request_qos_id = loadbalancer.vip.qos_policy_id + orig_lb = self.task_utils.get_current_loadbalancer_from_db( + loadbalancer.id) + orig_qos_id = orig_lb.vip.qos_policy_id + if request_qos_id != orig_qos_id: + self._apply_qos_on_vrrp_ports(loadbalancer, amps_data, orig_qos_id, + is_revert=True, + request_qos_id=request_qos_id) + return diff --git a/octavia/db/migration/alembic_migrations/versions/0aee2b450512_extend_api_to_accept_qos_policy_id.py b/octavia/db/migration/alembic_migrations/versions/0aee2b450512_extend_api_to_accept_qos_policy_id.py new file mode 100644 index 0000000000..52d54bdbe2 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/0aee2b450512_extend_api_to_accept_qos_policy_id.py @@ -0,0 +1,37 @@ +# Copyright 2017 Huawei +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Add QoS Policy ID column to VIP table + +Revision ID: 0aee2b450512 +Revises: bf171d0d91c3 +Create Date: 2017-02-07 20:47:52.405865 + +""" + + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '0aee2b450512' +down_revision = 'bf171d0d91c3' + + +def upgrade(): + op.add_column('vip', + sa.Column('qos_policy_id', + sa.String(length=36), + nullable=True, server_default=None)) diff --git a/octavia/db/models.py b/octavia/db/models.py index bc0c5ecc5c..8986516b80 100644 --- a/octavia/db/models.py +++ b/octavia/db/models.py @@ -381,6 +381,7 @@ class Vip(base_models.BASE): load_balancer = orm.relationship("LoadBalancer", uselist=False, backref=orm.backref("vip", uselist=False, cascade="delete")) + qos_policy_id = sa.Column(sa.String(36), nullable=True) class Listener(base_models.BASE, base_models.IdMixin, diff --git a/octavia/network/base.py b/octavia/network/base.py index fa6f15f89f..b20a9c11d4 100644 --- a/octavia/network/base.py +++ b/octavia/network/base.py @@ -79,6 +79,10 @@ class TimeoutException(NetworkException): pass +class QosPolicyNotFound(NetworkException): + pass + + @six.add_metaclass(abc.ABCMeta) class AbstractNetworkDriver(object): """This class defines the methods for a fully functional network driver. diff --git a/octavia/network/data_models.py b/octavia/network/data_models.py index 915774decb..e4e274e761 100644 --- a/octavia/network/data_models.py +++ b/octavia/network/data_models.py @@ -76,7 +76,7 @@ class Port(data_models.BaseDataModel): def __init__(self, id=None, name=None, device_id=None, device_owner=None, mac_address=None, network_id=None, status=None, project_id=None, admin_state_up=None, fixed_ips=None, - network=None): + network=None, qos_policy_id=None): self.id = id self.name = name self.device_id = device_id @@ -88,6 +88,7 @@ class Port(data_models.BaseDataModel): self.admin_state_up = admin_state_up self.fixed_ips = fixed_ips or [] self.network = network + self.qos_policy_id = qos_policy_id def get_subnet_id(self, fixed_ip_address): for fixed_ip in self.fixed_ips: @@ -122,3 +123,8 @@ class HostRoute(data_models.BaseDataModel): def __init__(self, nexthop=None, destination=None): self.nexthop = nexthop self.destination = destination + + +class QosPolicy(data_models.BaseDataModel): + def __init__(self, id): + self.id = id diff --git a/octavia/network/drivers/neutron/base.py b/octavia/network/drivers/neutron/base.py index 13dfbc1c0b..d6651ad8ae 100644 --- a/octavia/network/drivers/neutron/base.py +++ b/octavia/network/drivers/neutron/base.py @@ -135,6 +135,18 @@ class BaseNeutronDriver(base.AbstractNetworkDriver): } self.neutron_client.create_security_group_rule(rule) + def apply_qos_on_port(self, qos_id, port_id): + body = { + 'port': + {'qos_policy_id': qos_id} + } + try: + self.neutron_client.update_port(port_id, body) + except neutron_client_exceptions.PortNotFoundClient as e: + raise base.PortNotFound(e.message) + except Exception as e: + raise base.NetworkException(str(e)) + def get_plugged_networks(self, compute_id): # List neutron ports associated with the Amphora try: @@ -223,3 +235,6 @@ class BaseNeutronDriver(base.AbstractNetworkDriver): return self._get_resources_by_filters( 'port', unique_item=True, network_id=network_id, device_id=device_id) + + def get_qos_policy(self, qos_policy_id): + return self._get_resource('qos_policy', qos_policy_id) diff --git a/octavia/network/drivers/neutron/utils.py b/octavia/network/drivers/neutron/utils.py index f1126d7b61..800529b6cf 100644 --- a/octavia/network/drivers/neutron/utils.py +++ b/octavia/network/drivers/neutron/utils.py @@ -47,7 +47,8 @@ def convert_port_dict_to_model(port_dict): status=port.get('status'), project_id=port.get('tenant_id'), admin_state_up=port.get('admin_state_up'), - fixed_ips=fixed_ips + fixed_ips=fixed_ips, + qos_policy_id=port.get('qos_policy_id') ) @@ -71,3 +72,8 @@ def convert_fixed_ip_dict_to_model(fixed_ip_dict): fixed_ip = fixed_ip_dict.get('fixed_ip', fixed_ip_dict) return network_models.FixedIP(subnet_id=fixed_ip.get('subnet_id'), ip_address=fixed_ip.get('ip_address')) + + +def convert_qos_policy_dict_to_model(qos_policy_dict): + qos_policy = qos_policy_dict.get('policy', qos_policy_dict) + return network_models.QosPolicy(id=qos_policy.get('id')) diff --git a/octavia/network/drivers/noop_driver/driver.py b/octavia/network/drivers/noop_driver/driver.py index dd5cf74420..51ba6d844d 100644 --- a/octavia/network/drivers/noop_driver/driver.py +++ b/octavia/network/drivers/noop_driver/driver.py @@ -190,6 +190,19 @@ class NoopManager(object): LOG.debug("failover %s no-op, wait_for_port_detach, amphora id %s", self.__class__.__name__, amphora.id) + def get_qos_policy(self, qos_policy_id): + LOG.debug("Qos Policy %s no-op, get_qos_policy qos_policy_id %s", + self.__class__.__name__, qos_policy_id) + self.networkconfigconfig[qos_policy_id] = (qos_policy_id, + 'get_qos_policy') + return qos_policy_id + + def apply_qos_on_port(self, qos_id, port_id): + LOG.debug("Network %s no-op, apply_qos_on_port qos_id %s, port_id " + "%s", self.__class__.__name__, qos_id, port_id) + self.networkconfigconfig[(qos_id, port_id)] = ( + qos_id, port_id, 'apply_qos_on_port') + class NoopNetworkDriver(driver_base.AbstractNetworkDriver): def __init__(self): @@ -230,6 +243,9 @@ class NoopNetworkDriver(driver_base.AbstractNetworkDriver): def get_port(self, port_id): return self.driver.get_port(port_id) + def get_qos_policy(self, qos_policy_id): + return self.driver.get_qos_policy(qos_policy_id) + def get_network_by_name(self, network_name): return self.driver.get_network_by_name(network_name) @@ -253,3 +269,6 @@ class NoopNetworkDriver(driver_base.AbstractNetworkDriver): def wait_for_port_detach(self, amphora): self.driver.wait_for_port_detach(amphora) + + def apply_qos_on_port(self, qos_id, port_id): + self.driver.apply_qos_on_port(qos_id, port_id) diff --git a/octavia/tests/common/constants.py b/octavia/tests/common/constants.py index 9fd229e72b..829836a7a3 100644 --- a/octavia/tests/common/constants.py +++ b/octavia/tests/common/constants.py @@ -75,6 +75,9 @@ MOCK_NEUTRON_PORT = {'port': {'network_id': MOCK_NETWORK_ID, 'mac_address': MOCK_MAC_ADDR, 'fixed_ips': [{'ip_address': MOCK_IP_ADDRESS, 'subnet_id': MOCK_SUBNET_ID}]}} +MOCK_NEUTRON_QOS_POLICY_ID = 'mock-qos-id' +MOCK_QOS_POLICY_ID1 = 'qos1-id' +MOCK_QOS_POLICY_ID2 = 'qos2-id' MOCK_NEUTRON_PORT2 = {'port': {'network_id': MOCK_NETWORK_ID2, 'device_id': MOCK_DEVICE_ID2, diff --git a/octavia/tests/functional/api/v2/test_load_balancer.py b/octavia/tests/functional/api/v2/test_load_balancer.py index fd1f6e47bd..2d33c46283 100644 --- a/octavia/tests/functional/api/v2/test_load_balancer.py +++ b/octavia/tests/functional/api/v2/test_load_balancer.py @@ -461,6 +461,129 @@ class TestLoadBalancer(base.BaseAPITest): # the octavia error message self.assertIn("neutron_msg", response.json.get("faultstring")) + def test_create_with_qos(self): + subnet = network_models.Subnet(id=uuidutils.generate_uuid(), + network_id=uuidutils.generate_uuid()) + qos_policy_id = uuidutils.generate_uuid() + # Test with specific vip_qos_policy_id + lb_json = {'vip_subnet_id': subnet.id, + 'project_id': self.project_id, + 'vip_qos_policy_id': qos_policy_id} + body = self._build_body(lb_json) + with mock.patch("octavia.network.drivers.noop_driver.driver" + ".NoopManager.get_subnet") as mock_get_subnet: + with mock.patch("octavia.common.validate." + "qos_policy_exists") as mock_get_qos: + mock_get_subnet.return_value = subnet + mock_get_qos.return_value = qos_policy_id + response = self.post(self.LBS_PATH, body) + api_lb = response.json.get(self.root_tag) + self._assert_request_matches_response(lb_json, api_lb) + self.assertEqual(subnet.id, api_lb.get('vip_subnet_id')) + self.assertEqual(qos_policy_id, api_lb.get('vip_qos_policy_id')) + + def test_create_with_qos_vip_port(self): + # Test with vip_port_id which applied qos_policy + subnet = network_models.Subnet(id=uuidutils.generate_uuid(), + network_id=uuidutils.generate_uuid()) + port_qos_policy_id = uuidutils.generate_uuid() + ip_address = '192.168.50.50' + network = network_models.Network(id=uuidutils.generate_uuid(), + subnets=[subnet]) + fixed_ip = network_models.FixedIP(subnet_id=subnet.id, + ip_address=ip_address) + port = network_models.Port(id=uuidutils.generate_uuid(), + fixed_ips=[fixed_ip], + network_id=network.id, + qos_policy_id=port_qos_policy_id) + lb_json = {'vip_port_id': port.id, + 'project_id': self.project_id} + body = self._build_body(lb_json) + with mock.patch( + "octavia.network.drivers.noop_driver.driver." + "NoopManager.get_network") as m_get_network, mock.patch( + "octavia.network.drivers.noop_driver.driver.NoopManager" + ".get_port") as mock_get_port, mock.patch( + "octavia.network.drivers.noop_driver.driver.NoopManager" + ".allocate_vip") as mock_allocate_vip, mock.patch( + "octavia.common.validate." + "qos_policy_exists") as m_get_qos: + m_get_qos.return_value = port_qos_policy_id + mock_allocate_vip.return_value = data_models.Vip( + ip_address=ip_address, subnet_id=subnet.id, + network_id=network.id, port_id=port.id) + m_get_network.return_value = network + mock_get_port.return_value = port + response = self.post(self.LBS_PATH, body) + api_lb = response.json.get(self.root_tag) + self._assert_request_matches_response(lb_json, api_lb) + self.assertEqual(port.id, api_lb.get('vip_port_id')) + self.assertEqual(subnet.id, api_lb.get('vip_subnet_id')) + self.assertEqual(network.id, api_lb.get('vip_network_id')) + self.assertEqual(port_qos_policy_id, api_lb.get( + 'vip_qos_policy_id')) + + def test_create_with_qos_vip_port_and_vip_qos(self): + subnet = network_models.Subnet(id=uuidutils.generate_uuid(), + network_id=uuidutils.generate_uuid()) + port_qos_policy_id = uuidutils.generate_uuid() + new_qos_policy_id = uuidutils.generate_uuid() + ip_address = '192.168.50.50' + network = network_models.Network(id=uuidutils.generate_uuid(), + subnets=[subnet]) + fixed_ip = network_models.FixedIP(subnet_id=subnet.id, + ip_address=ip_address) + port = network_models.Port(id=uuidutils.generate_uuid(), + fixed_ips=[fixed_ip], + network_id=network.id, + qos_policy_id=port_qos_policy_id) + lb_json = {'vip_port_id': port.id, + 'project_id': self.project_id, + 'vip_qos_policy_id': new_qos_policy_id} + body = self._build_body(lb_json) + with mock.patch( + "octavia.network.drivers.noop_driver.driver." + "NoopManager.get_network") as m_get_network, mock.patch( + "octavia.network.drivers.noop_driver.driver.NoopManager" + ".get_port") as mock_get_port, mock.patch( + "octavia.network.drivers.noop_driver.driver.NoopManager" + ".allocate_vip") as mock_allocate_vip, mock.patch( + "octavia.common.validate." + "qos_policy_exists") as m_get_qos: + m_get_qos.return_value = mock.ANY + mock_allocate_vip.return_value = data_models.Vip( + ip_address=ip_address, subnet_id=subnet.id, + network_id=network.id, port_id=port.id) + m_get_network.return_value = network + mock_get_port.return_value = port + response = self.post(self.LBS_PATH, body) + api_lb = response.json.get(self.root_tag) + self._assert_request_matches_response(lb_json, api_lb) + self.assertEqual(port.id, api_lb.get('vip_port_id')) + self.assertEqual(subnet.id, api_lb.get('vip_subnet_id')) + self.assertEqual(network.id, api_lb.get('vip_network_id')) + self.assertEqual(new_qos_policy_id, api_lb.get( + 'vip_qos_policy_id')) + + def test_create_with_non_exist_qos_policy_id(self): + subnet = network_models.Subnet(id=uuidutils.generate_uuid(), + network_id=uuidutils.generate_uuid()) + qos_policy_id = uuidutils.generate_uuid() + lb_json = {'vip_subnet_id': subnet.id, + 'project_id': self.project_id, + 'vip_qos_policy_id': qos_policy_id} + body = self._build_body(lb_json) + with mock.patch("octavia.network.drivers.noop_driver.driver" + ".NoopManager.get_subnet") as mock_get_subnet: + with mock.patch("octavia.network.drivers.noop_driver." + "driver.NoopManager." + "get_qos_policy") as mock_get_qos: + mock_get_subnet.return_value = subnet + mock_get_qos.side_effect = Exception() + response = self.post(self.LBS_PATH, body, status=400) + err_msg = "qos_policy %s not found." % qos_policy_id + self.assertEqual(err_msg, response.json.get('faultstring')) + def test_create_with_long_name(self): lb_json = {'name': 'n' * 256, 'vip_subnet_id': uuidutils.generate_uuid(), @@ -1113,6 +1236,32 @@ class TestLoadBalancer(base.BaseAPITest): self.put(self.LB_PATH.format(lb_id=lb_dict.get('id')), lb_json, status=400) + def test_update_with_qos(self): + project_id = uuidutils.generate_uuid() + lb = self.create_load_balancer( + uuidutils.generate_uuid(), name='lb1', + project_id=project_id, + vip_qos_policy_id=uuidutils.generate_uuid()) + lb_dict = lb.get(self.root_tag) + self.set_lb_status(lb_dict.get('id')) + lb_json = self._build_body( + {'vip_qos_policy_id': uuidutils.generate_uuid()}) + self.put(self.LB_PATH.format(lb_id=lb_dict.get('id')), + lb_json, status=200) + + def test_update_with_bad_qos(self): + project_id = uuidutils.generate_uuid() + vip_qos_policy_id = uuidutils.generate_uuid() + lb = self.create_load_balancer(uuidutils.generate_uuid(), + name='lb1', + project_id=project_id, + vip_qos_policy_id=vip_qos_policy_id) + lb_dict = lb.get(self.root_tag) + lb_json = self._build_body({'vip_qos_policy_id': 'BAD'}) + self.set_lb_status(lb_dict.get('id')) + self.put(self.LB_PATH.format(lb_id=lb_dict.get('id')), + lb_json, status=400) + def test_update_bad_lb_id(self): path = self.LB_PATH.format(lb_id='SEAN-CONNERY') self.put(path, body={}, status=404) @@ -1726,6 +1875,7 @@ class TestLoadBalancerGraph(base.BaseAPITest): # for this test without interfering with a ton of stuff, and it is # expected that this would be overwritten anyway, so 'ANY' is fine? 'vip_network_id': mock.ANY, + 'vip_qos_policy_id': None, 'flavor': '', 'provider': 'octavia' } diff --git a/octavia/tests/functional/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 0c6488bbfc..6f0651b937 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/octavia/tests/functional/db/test_repositories.py @@ -133,7 +133,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): vip = {'ip_address': '10.0.0.1', 'port_id': uuidutils.generate_uuid(), 'subnet_id': uuidutils.generate_uuid(), - 'network_id': uuidutils.generate_uuid()} + 'network_id': uuidutils.generate_uuid(), + 'qos_policy_id': None} lb_dm = self.repos.create_load_balancer_and_vip(self.session, lb, vip) lb_dm_dict = lb_dm.to_dict() del lb_dm_dict['vip'] diff --git a/octavia/tests/unit/api/v2/types/test_load_balancers.py b/octavia/tests/unit/api/v2/types/test_load_balancers.py index bddd0d29df..a6dc0d881d 100644 --- a/octavia/tests/unit/api/v2/types/test_load_balancers.py +++ b/octavia/tests/unit/api/v2/types/test_load_balancers.py @@ -56,6 +56,11 @@ class TestLoadBalancer(object): self.assertRaises(ValueError, wsme_json.fromjson, self._type, body) + def test_invalid_qos_policy_id(self): + body = {"vip_qos_policy_id": "invalid_uuid"} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type, + body) + class TestLoadBalancerPOST(base.BaseTypesTest, TestLoadBalancer): @@ -70,7 +75,8 @@ class TestLoadBalancerPOST(base.BaseTypesTest, TestLoadBalancer): def test_vip(self): body = {"vip_subnet_id": uuidutils.generate_uuid(), - "vip_port_id": uuidutils.generate_uuid()} + "vip_port_id": uuidutils.generate_uuid(), + "vip_qos_policy_id": uuidutils.generate_uuid()} wsme_json.fromjson(self._type, body) def test_invalid_ip_address(self): diff --git a/octavia/tests/unit/common/test_validations.py b/octavia/tests/unit/common/test_validations.py index 5be02928cb..743a70d4f4 100644 --- a/octavia/tests/unit/common/test_validations.py +++ b/octavia/tests/unit/common/test_validations.py @@ -347,3 +347,18 @@ class TestValidations(base.TestCase): self.assertRaises( exceptions.ValidationException, validate.network_allowed_by_config, net_id3) + + def test_qos_policy_exists(self): + qos_policy_id = uuidutils.generate_uuid() + qos_policy = network_models.QosPolicy(id=qos_policy_id) + with mock.patch( + 'octavia.common.utils.get_network_driver') as net_mock: + net_mock.return_value.get_qos_policy.return_value = qos_policy + self.assertEqual( + validate.qos_policy_exists(qos_policy_id), + qos_policy) + + net_mock.return_value.get_qos_policy.side_effect = Exception + self.assertRaises(exceptions.InvalidSubresource, + validate.qos_policy_exists, + qos_policy_id) diff --git a/octavia/tests/unit/controller/worker/flows/test_load_balancer_flows.py b/octavia/tests/unit/controller/worker/flows/test_load_balancer_flows.py index bef7501b66..9d302b6fcd 100644 --- a/octavia/tests/unit/controller/worker/flows/test_load_balancer_flows.py +++ b/octavia/tests/unit/controller/worker/flows/test_load_balancer_flows.py @@ -132,11 +132,12 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.AMPS_DATA, lb_flow.provides) self.assertIn(constants.LOADBALANCER, lb_flow.provides) + self.assertIn(constants.UPDATE_DICT, lb_flow.requires) self.assertIn(constants.LOADBALANCER, lb_flow.requires) self.assertIn(constants.LOADBALANCER_ID, lb_flow.requires) self.assertEqual(4, len(lb_flow.provides)) - self.assertEqual(2, len(lb_flow.requires)) + self.assertEqual(3, len(lb_flow.requires)) def test_get_update_load_balancer_flow(self, mock_get_net_driver): diff --git a/octavia/tests/unit/controller/worker/tasks/test_database_tasks.py b/octavia/tests/unit/controller/worker/tasks/test_database_tasks.py index 490c7b00d4..e8f5e74dca 100644 --- a/octavia/tests/unit/controller/worker/tasks/test_database_tasks.py +++ b/octavia/tests/unit/controller/worker/tasks/test_database_tasks.py @@ -1427,6 +1427,34 @@ class TestDatabaseTasks(base.TestCase): id=LB_ID, provisioning_status=constants.ERROR) + @mock.patch('octavia.db.repositories.LoadBalancerRepository.update') + @mock.patch('octavia.db.repositories.VipRepository.update') + def test_update_vip_in_db_during_update_loadbalancer(self, + mock_vip_update, + mock_listner_update, + mock_generate_uuid, + mock_LOG, + mock_get_session, + mock_lb_update, + mock_listener_update, + mock_amphora_update, + mock_amphora_delete): + + self.loadbalancer_mock.vip.load_balancer_id = LB_ID + update_load_balancer = database_tasks.UpdateLoadbalancerInDB() + update_load_balancer.execute(self.loadbalancer_mock, + {'name': 'test', + 'description': 'test2', + 'vip': {'qos_policy_id': 'fool'}}) + + repo.LoadBalancerRepository.update.assert_called_once_with( + 'TEST', + LB_ID, + name='test', description='test2') + + repo.VipRepository.update.assert_called_once_with('TEST', LB_ID, + qos_policy_id='fool') + @mock.patch('octavia.db.repositories.ListenerRepository.update') def test_update_listener_in_db(self, mock_listner_repo_update, diff --git a/octavia/tests/unit/controller/worker/tasks/test_model_tasks.py b/octavia/tests/unit/controller/worker/tasks/test_model_tasks.py index 7191e9e0f4..601035c7c0 100644 --- a/octavia/tests/unit/controller/worker/tasks/test_model_tasks.py +++ b/octavia/tests/unit/controller/worker/tasks/test_model_tasks.py @@ -15,6 +15,7 @@ import mock +from octavia.common import data_models as o_data_models from octavia.controller.worker.tasks import model_tasks import octavia.tests.unit.base as base @@ -41,4 +42,14 @@ class TestObjectUpdateTasks(base.TestCase): update_attr.execute(self.listener_mock, {'name': 'TEST2'}) - self.listener_mock.update.assert_called_once_with({'name': 'TEST2'}) + self.assertEqual('TEST2', getattr(self.listener_mock, 'name')) + + @mock.patch('octavia.common.data_models.Vip.update') + def test_update_vip_during_update_loadbalancer(self, mock_vip): + vip_object = o_data_models.Vip() + lb_object = o_data_models.LoadBalancer(vip=vip_object) + update_attr = model_tasks.UpdateAttributes() + update_attr.execute(lb_object, {'vip': {'fool1': 'bar1'}, + 'description': 'bar2'}) + + mock_vip.assert_called_once_with({'fool1': 'bar1'}) diff --git a/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py b/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py index 5e36bdd140..674e707891 100644 --- a/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py +++ b/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py @@ -23,6 +23,7 @@ from octavia.common import data_models as o_data_models from octavia.controller.worker.tasks import network_tasks from octavia.network import base as net_base from octavia.network import data_models +from octavia.tests.common import constants as t_constants import octavia.tests.unit.base as base @@ -32,14 +33,27 @@ PORT_ID = uuidutils.generate_uuid() SUBNET_ID = uuidutils.generate_uuid() NETWORK_ID = uuidutils.generate_uuid() IP_ADDRESS = "172.24.41.1" -VIP = o_data_models.Vip(port_id=PORT_ID, subnet_id=SUBNET_ID, - ip_address=IP_ADDRESS) +VIP = o_data_models.Vip(port_id=t_constants.MOCK_PORT_ID, + subnet_id=t_constants.MOCK_SUBNET_ID, + qos_policy_id=t_constants.MOCK_QOS_POLICY_ID1) +VIP2 = o_data_models.Vip(port_id=t_constants.MOCK_PORT_ID2, + subnet_id=t_constants.MOCK_SUBNET_ID2, + qos_policy_id=t_constants.MOCK_QOS_POLICY_ID2) LB = o_data_models.LoadBalancer(vip=VIP) +LB2 = o_data_models.LoadBalancer(vip=VIP2) FIRST_IP = {"ip_address": IP_ADDRESS, "subnet_id": SUBNET_ID} FIXED_IPS = [FIRST_IP] INTERFACE = data_models.Interface(id=uuidutils.generate_uuid(), compute_id=COMPUTE_ID, fixed_ips=FIXED_IPS, port_id=PORT_ID) +AMPS_DATA = [o_data_models.Amphora(id=t_constants.MOCK_AMP_ID1, + vrrp_port_id=t_constants.MOCK_VRRP_PORT_ID1, + vrrp_ip=t_constants.MOCK_VRRP_IP1), + o_data_models.Amphora(id=t_constants.MOCK_AMP_ID2, + vrrp_port_id=t_constants.MOCK_VRRP_PORT_ID2, + vrrp_ip=t_constants.MOCK_VRRP_IP2) + ] +UPDATE_DICT = {constants.LOADBALANCER_TOPOLOGY: None} class TestException(Exception): @@ -255,7 +269,7 @@ class TestNetworkTasks(base.TestCase): net_task = network_tasks.GetMemberPorts() net_task.execute(LB, self.amphora_mock) - mock_driver.get_port.assert_called_once_with(PORT_ID) + mock_driver.get_port.assert_called_once_with(t_constants.MOCK_PORT_ID) mock_driver.get_plugged_networks.assert_called_once_with(COMPUTE_ID) mock_driver.reset_mock() @@ -382,6 +396,119 @@ class TestNetworkTasks(base.TestCase): net.revert(["vip"], LB) mock_driver.unplug_vip.assert_called_once_with(LB, LB.vip) + @mock.patch('octavia.controller.worker.task_utils.TaskUtils.' + 'get_current_loadbalancer_from_db') + def test_apply_qos_on_creation(self, mock_get_lb_db, mock_get_net_driver): + mock_driver = mock.MagicMock() + mock_get_net_driver.return_value = mock_driver + net = network_tasks.ApplyQos() + mock_get_lb_db.return_value = LB + + # execute + UPDATE_DICT[ + constants.LOADBALANCER_TOPOLOGY] = constants.TOPOLOGY_SINGLE + update_dict = UPDATE_DICT + net.execute(LB, [AMPS_DATA[0]], update_dict) + mock_driver.apply_qos_on_port.assert_called_once_with( + VIP.qos_policy_id, AMPS_DATA[0].vrrp_port_id) + self.assertEqual(1, mock_driver.apply_qos_on_port.call_count) + standby_topology = constants.TOPOLOGY_ACTIVE_STANDBY + mock_driver.reset_mock() + update_dict[ + constants.LOADBALANCER_TOPOLOGY] = standby_topology + net.execute(LB, AMPS_DATA, update_dict) + mock_driver.apply_qos_on_port.assert_called_with( + t_constants.MOCK_QOS_POLICY_ID1, mock.ANY) + self.assertEqual(2, mock_driver.apply_qos_on_port.call_count) + + # revert + mock_driver.reset_mock() + update_dict = UPDATE_DICT + net.revert(None, LB, [AMPS_DATA[0]], update_dict) + self.assertEqual(0, mock_driver.apply_qos_on_port.call_count) + mock_driver.reset_mock() + update_dict[ + constants.LOADBALANCER_TOPOLOGY] = standby_topology + net.revert(None, LB, AMPS_DATA, update_dict) + self.assertEqual(0, mock_driver.apply_qos_on_port.call_count) + + @mock.patch('octavia.controller.worker.task_utils.TaskUtils.' + 'get_current_loadbalancer_from_db') + def test_apply_qos_on_update(self, mock_get_lb_db, mock_get_net_driver): + mock_driver = mock.MagicMock() + mock_get_net_driver.return_value = mock_driver + net = network_tasks.ApplyQos() + null_qos_vip = o_data_models.Vip(qos_policy_id=None) + null_qos_lb = o_data_models.LoadBalancer( + vip=null_qos_vip, topology=constants.TOPOLOGY_SINGLE, + amphorae=[AMPS_DATA[0]]) + + tmp_vip_object = o_data_models.Vip( + qos_policy_id=t_constants.MOCK_QOS_POLICY_ID1) + tmp_lb = o_data_models.LoadBalancer( + vip=tmp_vip_object, topology=constants.TOPOLOGY_SINGLE, + amphorae=[AMPS_DATA[0]]) + + # execute + update_dict = {'description': 'fool'} + net.execute(tmp_lb, update_dict=update_dict) + mock_driver.apply_qos_on_port.assert_called_once_with( + t_constants.MOCK_QOS_POLICY_ID1, AMPS_DATA[0].vrrp_port_id) + self.assertEqual(1, mock_driver.apply_qos_on_port.call_count) + + mock_driver.reset_mock() + update_dict = {'vip': {'qos_policy_id': None}} + net.execute(null_qos_lb, update_dict=update_dict) + mock_driver.apply_qos_on_port.assert_called_once_with( + None, AMPS_DATA[0].vrrp_port_id) + self.assertEqual(1, mock_driver.apply_qos_on_port.call_count) + + mock_driver.reset_mock() + update_dict = {'name': '123'} + net.execute(null_qos_lb, update_dict=update_dict) + self.assertEqual(0, mock_driver.apply_qos_on_port.call_count) + + mock_driver.reset_mock() + update_dict = {'description': 'fool'} + tmp_lb.amphorae = AMPS_DATA + tmp_lb.topology = constants.TOPOLOGY_ACTIVE_STANDBY + net.execute(tmp_lb, update_dict=update_dict) + mock_driver.apply_qos_on_port.assert_called_with( + t_constants.MOCK_QOS_POLICY_ID1, mock.ANY) + self.assertEqual(2, mock_driver.apply_qos_on_port.call_count) + + # revert + mock_driver.reset_mock() + tmp_lb.amphorae = [AMPS_DATA[0]] + tmp_lb.topology = constants.TOPOLOGY_SINGLE + update_dict = {'description': 'fool'} + mock_get_lb_db.return_value = tmp_lb + net.revert(None, tmp_lb, update_dict=update_dict) + self.assertEqual(0, mock_driver.apply_qos_on_port.call_count) + + mock_driver.reset_mock() + update_dict = {'vip': {'qos_policy_id': None}} + ori_lb_db = LB2 + ori_lb_db.amphorae = [AMPS_DATA[0]] + mock_get_lb_db.return_value = ori_lb_db + net.revert(None, null_qos_lb, update_dict=update_dict) + mock_driver.apply_qos_on_port.assert_called_once_with( + t_constants.MOCK_QOS_POLICY_ID2, AMPS_DATA[0].vrrp_port_id) + self.assertEqual(1, mock_driver.apply_qos_on_port.call_count) + + mock_driver.reset_mock() + update_dict = {'vip': { + 'qos_policy_id': t_constants.MOCK_QOS_POLICY_ID2}} + tmp_lb.amphorae = AMPS_DATA + tmp_lb.topology = constants.TOPOLOGY_ACTIVE_STANDBY + ori_lb_db = LB2 + ori_lb_db.amphorae = [AMPS_DATA[0]] + mock_get_lb_db.return_value = ori_lb_db + net.revert(None, tmp_lb, update_dict=update_dict) + mock_driver.apply_qos_on_port.assert_called_with( + t_constants.MOCK_QOS_POLICY_ID2, mock.ANY) + self.assertEqual(2, mock_driver.apply_qos_on_port.call_count) + def test_unplug_vip(self, mock_get_net_driver): mock_driver = mock.MagicMock() mock_get_net_driver.return_value = mock_driver diff --git a/octavia/tests/unit/controller/worker/test_task_utils.py b/octavia/tests/unit/controller/worker/test_task_utils.py index 447fb10067..9bf3bd8e94 100644 --- a/octavia/tests/unit/controller/worker/test_task_utils.py +++ b/octavia/tests/unit/controller/worker/test_task_utils.py @@ -262,3 +262,22 @@ class TestTaskUtils(base.TestCase): self.task_utils.mark_pool_prov_status_error(self.POOL_ID) self.assertFalse(mock_pool_repo_update.called) + + @mock.patch('octavia.db.api.get_session', return_value=TEST_SESSION) + @mock.patch('octavia.db.repositories.LoadBalancerRepository.get') + def test_get_current_loadbalancer_from_db(self, mock_lb_repo_get, + mock_get_session): + # Happy path + self.task_utils.get_current_loadbalancer_from_db(self.LOADBALANCER_ID) + + mock_lb_repo_get.assert_called_once_with( + TEST_SESSION, + id=self.LOADBALANCER_ID) + + # Exception path + mock_lb_repo_get.reset_mock() + mock_get_session.side_effect = Exception('fail') + + self.task_utils.get_current_loadbalancer_from_db(self.POOL_ID) + + self.assertFalse(mock_lb_repo_get.called) diff --git a/octavia/tests/unit/network/drivers/neutron/test_base.py b/octavia/tests/unit/network/drivers/neutron/test_base.py index f7d8de8a4e..606a8ebc02 100644 --- a/octavia/tests/unit/network/drivers/neutron/test_base.py +++ b/octavia/tests/unit/network/drivers/neutron/test_base.py @@ -410,3 +410,39 @@ class TestBaseNeutronNetworkDriver(base.TestCase): port1.fixed_ips[0].ip_address) self.assertEqual(t_constants.MOCK_IP_ADDRESS2, port2.fixed_ips[0].ip_address) + + def test_get_qos_policy(self): + get_qos = self.driver.neutron_client.show_qos_policy + get_qos.return_value = {'policy': { + 'id': t_constants.MOCK_NEUTRON_QOS_POLICY_ID}} + qos = self.driver.get_qos_policy( + t_constants.MOCK_NEUTRON_QOS_POLICY_ID) + self.assertIsInstance(qos, network_models.QosPolicy) + self.assertEqual(t_constants.MOCK_NEUTRON_QOS_POLICY_ID, + qos.id) + + get_qos.side_effect = neutron_client_exceptions.NotFound + self.assertRaises(network_base.QosPolicyNotFound, + self.driver.get_qos_policy, + t_constants.MOCK_NEUTRON_QOS_POLICY_ID) + + get_qos.side_effect = neutron_client_exceptions.ServiceUnavailable + self.assertRaises(network_base.NetworkException, + self.driver.get_qos_policy, + t_constants.MOCK_NEUTRON_QOS_POLICY_ID) + + def test_apply_or_undo_qos_on_port(self): + # The apply and undo qos function use the same "update_port" with + # neutron client. So testing them in one Uts. + update_port = self.driver.neutron_client.update_port + update_port.side_effect = neutron_client_exceptions.PortNotFoundClient + self.assertRaises(network_base.PortNotFound, + self.driver.apply_qos_on_port, + t_constants.MOCK_PORT_ID, + t_constants.MOCK_NEUTRON_QOS_POLICY_ID) + + update_port.side_effect = neutron_client_exceptions.ServiceUnavailable + self.assertRaises(network_base.NetworkException, + self.driver.apply_qos_on_port, + t_constants.MOCK_PORT_ID, + t_constants.MOCK_NEUTRON_QOS_POLICY_ID) diff --git a/octavia/tests/unit/network/drivers/test_network_noop_driver.py b/octavia/tests/unit/network/drivers/test_network_noop_driver.py index d1e2e7b166..546bf2cc52 100644 --- a/octavia/tests/unit/network/drivers/test_network_noop_driver.py +++ b/octavia/tests/unit/network/drivers/test_network_noop_driver.py @@ -25,6 +25,8 @@ class TestNoopNetworkDriver(base.TestCase): FAKE_UUID_2 = uuidutils.generate_uuid() FAKE_UUID_3 = uuidutils.generate_uuid() FAKE_UUID_4 = uuidutils.generate_uuid() + FAKE_UUID_5 = uuidutils.generate_uuid() + FAKE_UUID_6 = uuidutils.generate_uuid() def setUp(self): super(TestNoopNetworkDriver, self).setUp() @@ -46,6 +48,8 @@ class TestNoopNetworkDriver(base.TestCase): self.compute_id = self.FAKE_UUID_2 self.subnet_id = self.FAKE_UUID_3 self.subnet_name = 'subnet1' + self.qos_policy_id = self.FAKE_UUID_5 + self.vrrp_port_id = self.FAKE_UUID_6 def test_allocate_vip(self): self.driver.allocate_vip(self.load_balancer) @@ -170,3 +174,18 @@ class TestNoopNetworkDriver(base.TestCase): (self.load_balancer, 'get_network_configs'), self.driver.driver.networkconfigconfig[self.load_balancer.id] ) + + def test_get_qos_policy(self): + self.driver.get_qos_policy(self.qos_policy_id) + self.assertEqual( + (self.qos_policy_id, 'get_qos_policy'), + self.driver.driver.networkconfigconfig[self.qos_policy_id] + ) + + def test_apply_qos_on_port(self): + self.driver.apply_qos_on_port(self.qos_policy_id, self.vrrp_port_id) + self.assertEqual( + (self.qos_policy_id, self.vrrp_port_id, 'apply_qos_on_port'), + self.driver.driver.networkconfigconfig[self.qos_policy_id, + self.vrrp_port_id] + ) diff --git a/releasenotes/notes/extend_api_to_accept_qos_policy_id-128ab592a735f3b8.yaml b/releasenotes/notes/extend_api_to_accept_qos_policy_id-128ab592a735f3b8.yaml new file mode 100644 index 0000000000..3ce2a92bc2 --- /dev/null +++ b/releasenotes/notes/extend_api_to_accept_qos_policy_id-128ab592a735f3b8.yaml @@ -0,0 +1,5 @@ +--- +features: + - Now Octavia API can accept the QoS Policy id from neutron to support the + QoS requirements towards Load Balancer VIP port when create/update load + balancer.