diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index 2ff5afd1d..b951cda7a 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -229,6 +229,7 @@ This operation does not accept a request body. - management-end-ip: management_end_ip - management-subnet: management_subnet - management-gateway-ip: management_gateway_ip + - peer_group_id: subcloud_peer_group_id - rehome_data: rehome_data - created-at: created_at - updated-at: updated_at @@ -294,6 +295,7 @@ This operation does not accept a request body. - management-subnet: management_subnet - management-gateway-ip: management_gateway_ip - oam_floating_ip: oam_floating_ip + - peer_group_id: subcloud_peer_group_id - rehome_data: rehome_data - created-at: created_at - updated-at: updated_at @@ -335,6 +337,8 @@ The attributes of a subcloud which are modifiable: - management-end-ip +- peer_group_id + - bootstrap_values - bootstrap_address @@ -363,6 +367,7 @@ serviceUnavailable (503) - management-gateway-ip: subcloud_management_gateway_ip - management-start-ip: subcloud_management_start_ip - management-end-ip: subcloud_management_end_ip + - peer_group_id: subcloud_peer_group_id - bootstrap-address: bootstrap_address - sysadmin-password: sysadmin_password - bootstrap-values: bootstrap_values_for_rehome @@ -379,6 +384,7 @@ Request Example - id: subcloud_id - group_id: group_id + - peer_group_id: subcloud_peer_group_id - name: subcloud_name - description: subcloud_description - location: subcloud_location @@ -2560,4 +2566,333 @@ internalServerError (500), serviceUnavailable (503) - system-peer: system_peer_uri +This operation does not accept a request body. + +-------------------- +Subcloud Peer Groups +-------------------- + +Subcloud Peer Groups are logical groupings managed by a central System Controller. +It's a group of the current managed subclouds which are supposed to be duplicated +in a peer site as secondary subclouds + +****************************** +Lists all subcloud peer groups +****************************** + +.. rest_method:: GET /v1.0/subcloud-peer-groups + +This operation does not accept a request body. + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), +badMethod (405), HTTPUnprocessableEntity (422), +internalServerError (500), serviceUnavailable (503) + + +**Response parameters** + +.. rest_parameters:: parameters.yaml + + - subcloud_peer_groups: subcloud_peer_groups + - id: subcloud_peer_group_id + - peer_group_name: subcloud_peer_group_name + - group_priority: subcloud_peer_group_priority + - group_state: subcloud_peer_group_administrative_state + - max_subcloud_rehoming: subcloud_peer_group_max_subcloud_rehoming + - system_leader_id: subcloud_peer_group_system_leader_id + - system_leader_name: subcloud_peer_group_system_leader_name + - created_at: created_at + - updated_at: updated_at + +Response Example +---------------- + +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-groups-get-response.json + :language: json + + +***************************** +Creates a subcloud peer group +***************************** + +.. rest_method:: POST /v1.0/subcloud-peer-groups + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), badMethod (405), +HTTPUnprocessableEntity (422), internalServerError (500), +serviceUnavailable (503) + +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - peer_group_name: subcloud_peer_group_name + - group_priority: subcloud_peer_group_priority + - group_state: subcloud_peer_group_administrative_state + - max_subcloud_rehoming: subcloud_peer_group_max_subcloud_rehoming + - system_leader_id: subcloud_peer_group_system_leader_id + - system_leader_name: subcloud_peer_group_system_leader_name + +Request Example +---------------- + +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-groups-post-request.json + :language: json + +**Response parameters** + +.. rest_parameters:: parameters.yaml + + - id: subcloud_peer_group_id + - peer_group_name: subcloud_peer_group_name + - group_priority: subcloud_peer_group_priority + - group_state: subcloud_peer_group_administrative_state + - max_subcloud_rehoming: subcloud_peer_group_max_subcloud_rehoming + - system_leader_id: subcloud_peer_group_system_leader_id + - system_leader_name: subcloud_peer_group_system_leader_name + - created_at: created_at + - updated_at: updated_at + +Response Example +---------------- + +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-groups-post-response.json + :language: json + + +*************************************************** +Shows information about a specific subcloud group +*************************************************** + +.. rest_method:: GET /v1.0/subcloud-peer-groups/​{subcloud-peer-group}​ + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), +itemNotFound (404), badMethod (405), HTTPUnprocessableEntity (422), +internalServerError (500), serviceUnavailable (503) + +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - subcloud-peer-group: subcloud_peer_group_uri + +This operation does not accept a request body. + +**Response parameters** + +.. rest_parameters:: parameters.yaml + + - id: subcloud_peer_group_id + - peer_group_name: subcloud_peer_group_name + - group_priority: subcloud_peer_group_priority + - group_state: subcloud_peer_group_administrative_state + - max_subcloud_rehoming: subcloud_peer_group_max_subcloud_rehoming + - system_leader_id: subcloud_peer_group_system_leader_id + - system_leader_name: subcloud_peer_group_system_leader_name + - created_at: created_at + - updated_at: updated_at + +Response Example +---------------- + +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-groups-post-response.json + :language: json + + +****************************************************** +Shows subclouds that are part of a subcloud peer group +****************************************************** + +.. rest_method:: GET /v1.0/subcloud-peer-groups/​{subcloud-peer-group}​/subclouds + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), +itemNotFound (404), badMethod (405), HTTPUnprocessableEntity (422), +internalServerError (500), serviceUnavailable (503) + +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - subcloud-peer-group: subcloud_peer_group_uri + +This operation does not accept a request body. + +**Response parameters** + +.. rest_parameters:: parameters.yaml + + - subclouds: subclouds + - id: subcloud_id + - group_id: group_id + - name: subcloud_name + - description: subcloud_description + - location: subcloud_location + - software-version: software_version + - availability-status: availability_status + - error-description: error_description + - deploy-status: deploy_status + - backup-status: backup_status + - backup-datetime: backup_datetime + - openstack-installed: openstack_installed + - management-state: management_state + - systemcontroller-gateway-ip: systemcontroller_gateway_ip + - management-start-ip: management_start_ip + - management-end-ip: management_end_ip + - management-subnet: management_subnet + - management-gateway-ip: management_gateway_ip + - created-at: created_at + - updated-at: updated_at + - data_install: data_install + - data_upgrade: data_upgrade + +Response Example +---------------- + +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-groups-get-subclouds-response.json + :language: json + + +*************************************** +Modifies a specific subcloud peer group +*************************************** + +.. rest_method:: PATCH /v1.0/subcloud-peer-groups/​{subcloud-peer-group}​ + +The attributes of a subcloud peer group which are modifiable: + +- peer_group_name + +- group_priority + +- group_state + +- max_subcloud_rehoming + +- system_leader_id + +- system_leader_name + + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), badMethod (405), +HTTPUnprocessableEntity (422), internalServerError (500), +serviceUnavailable (503) + +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - subcloud-peer-group: subcloud_peer_group_uri + - peer_group_name: subcloud_peer_group_name + - group_priority: subcloud_peer_group_priority + - group_state: subcloud_peer_group_administrative_state + - max_subcloud_rehoming: subcloud_peer_group_max_subcloud_rehoming + - system_leader_id: subcloud_peer_group_system_leader_id + - system_leader_name: subcloud_peer_group_system_leader_name + +Request Example +---------------- +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-group-patch-request.json + :language: json + +**Response parameters** + +.. rest_parameters:: parameters.yaml + + - id: subcloud_peer_group_id + - peer_group_name: subcloud_peer_group_name + - group_priority: subcloud_peer_group_priority + - group_state: subcloud_peer_group_administrative_state + - max_subcloud_rehoming: subcloud_peer_group_max_subcloud_rehoming + - system_leader_id: subcloud_peer_group_system_leader_id + - system_leader_name: subcloud_peer_group_system_leader_name + - created_at: created_at + - updated_at: updated_at + +Response Example +---------------- + +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-group-patch-response.json + :language: json + + +************************************** +Migrate a specific subcloud peer group +************************************** + +.. rest_method:: PATCH /v1.0/subcloud-peer-groups/​{subcloud-peer-group}​/migrate + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), +itemNotFound (404), badMethod (405), HTTPUnprocessableEntity (422), +internalServerError (500), serviceUnavailable (503) + +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - subcloud-peer-group: subcloud_peer_group_uri + - sysadmin-password: sysadmin_password + +Request Example +---------------- +.. literalinclude:: samples/subcloud-peer-groups/subcloud-peer-groups-patch-migrate-request.json + :language: json + + +************************************** +Deletes a specific subcloud peer group +************************************** + +.. rest_method:: DELETE /v1.0/subcloud-peer-groups/​{subcloud-peer-group}​ + +**Normal response codes** + +200 + +**Error response codes** + +badRequest (400), unauthorized (401), forbidden (403), +itemNotFound (404), badMethod (405), HTTPUnprocessableEntity (422), +internalServerError (500), serviceUnavailable (503) + +**Request parameters** + +.. rest_parameters:: parameters.yaml + + - subcloud-peer-group: subcloud_peer_group_uri + This operation does not accept a request body. \ No newline at end of file diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index b92416c90..f612a1116 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -25,6 +25,12 @@ subcloud_options_uri: in: path required: true type: string +subcloud_peer_group_uri: + description: | + The subcloud peer group reference, name or id. + in: path + required: true + type: string subcloud_uri: description: | The subcloud reference, name or id. @@ -671,6 +677,56 @@ subcloud_name: in: body required: true type: string +subcloud_peer_group_administrative_state: + description: | + The administrative state of the subcloud peer group. + Valid value is enabled/disabled. + in: body + required: false + type: string +subcloud_peer_group_id: + description: | + The ID of the subcloud peer group associated with this object. + in: body + required: false + type: string +subcloud_peer_group_max_subcloud_rehoming: + description: | + The maximum number of subclouds to rehome in parallel. + in: body + required: false + type: integer +subcloud_peer_group_name: + description: | + The NAME of the subcloud peer group. + in: body + required: true + type: string +subcloud_peer_group_priority: + description: | + The priority of the subcloud peer group. + Number lower priority is higher. + in: body + required: false + type: integer +subcloud_peer_group_system_leader_id: + description: | + UUID of the peer system. + in: body + required: false + type: string +subcloud_peer_group_system_leader_name: + description: | + NAME of the peer system. + in: body + required: false + type: string +subcloud_peer_groups: + description: | + The list of ``subcloud-peer-group`` objects. + in: body + required: true + type: array subcloud_uuid: description: | The ID of a subcloud as a uuid. diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-get-response.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-get-response.json new file mode 100644 index 000000000..dc9dd01a8 --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-get-response.json @@ -0,0 +1,11 @@ +{ + "id": 1, + "peer_group_name": "dc1-pg", + "group_priority": 0, + "group_state": "enabled", + "max_subcloud_rehoming": 10, + "system_leader_id": "ac62f555-9386-42f1-b3a1-51ecb709409d", + "system_leader_name": "dc1-name", + "created-at": "2023-07-26 00:51:01.396694", + "updated-at": "2023-07-26 00:57:35.941816" +} \ No newline at end of file diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-patch-request.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-patch-request.json new file mode 100644 index 000000000..2783263bb --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-patch-request.json @@ -0,0 +1,11 @@ +{ + "id": 1, + "peer_group_name": "dc1-pg", + "group_priority": 0, + "group_state": "enabled", + "max_subcloud_rehoming": 10, + "system_leader_id": "ac62f555-9386-42f1-b3a1-51ecb709409d", + "system_leader_name": "dc1-name", + "created-at": "2023-07-26 00:51:01.396694", + "updated-at": "2023-08-07 06:09:04.086417" +} \ No newline at end of file diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-patch-response.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-patch-response.json new file mode 100644 index 000000000..2783263bb --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-group-patch-response.json @@ -0,0 +1,11 @@ +{ + "id": 1, + "peer_group_name": "dc1-pg", + "group_priority": 0, + "group_state": "enabled", + "max_subcloud_rehoming": 10, + "system_leader_id": "ac62f555-9386-42f1-b3a1-51ecb709409d", + "system_leader_name": "dc1-name", + "created-at": "2023-07-26 00:51:01.396694", + "updated-at": "2023-08-07 06:09:04.086417" +} \ No newline at end of file diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-get-response.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-get-response.json new file mode 100644 index 000000000..ac59c05c1 --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-get-response.json @@ -0,0 +1,13 @@ +{ + "subcloud_peer_groups": [{ + "id": 1, + "peer_group_name": "dc1-pg", + "group_priority": 0, + "group_state": "enabled", + "max_subcloud_rehoming": 10, + "system_leader_id": "ac62f555-9386-42f1-b3a1-51ecb709409d", + "system_leader_name": "dc1-name", + "created-at": "2023-07-26 00:51:01.396694", + "updated-at": "2023-08-07 06:09:04.086417" + }] +} \ No newline at end of file diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-get-subclouds-response.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-get-subclouds-response.json new file mode 100644 index 000000000..fac63015d --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-get-subclouds-response.json @@ -0,0 +1,28 @@ +{ + "subclouds": [{ + "id": 23, + "name": "fakesub1", + "description": "desc", + "location": "PEK SE Lab", + "software-version": "23.09", + "management-state": "unmanaged", + "availability-status": "offline", + "deploy-status": "secondary", + "backup-status": null, + "backup-datetime": null, + "error-description": "No errors present", + "management-subnet": "192.168.38.0/24", + "management-start-ip": "192.168.38.2", + "management-end-ip": "192.168.38.200", + "management-gateway-ip": "192.168.38.1", + "openstack-installed": false, + "systemcontroller-gateway-ip": "192.168.10.1", + "data_install": null, + "data_upgrade": null, + "created-at": "2023-08-04 05:45:04.416188", + "updated-at": "2023-08-04 08:55:13.034874", + "group_id": 1, + "peer_group_id": "6", + "rehome_data": "{\"saved_payload\": {\"system_mode\": \"simplex\", \"name\": \"fakesub2\", \"description\": \"bbb\", \"location\": \"PEK SE Lab\", \"external_oam_subnet\": \"128.224.115.0/24\", \"external_oam_gateway_address\": \"128.224.115.1\", \"external_oam_floating_address\": \"128.224.115.15\", \"management_subnet\": \"192.168.38.0/24\", \"management_start_address\": \"192.168.38.2\", \"management_end_address\": \"192.168.38.200\", \"management_gateway_address\": \"192.168.38.1\", \"systemcontroller_gateway_address\": \"192.168.10.1\", \"docker_http_proxy\": \"http://147.11.252.42:9090\", \"docker_https_proxy\": \"http://147.11.252.42:9090\", \"docker_no_proxy\": [], \"sysadmin_password\": \"Wind123$\", \"bootstrap-address\": \"192.168.58.2\"}}" + }] +} \ No newline at end of file diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-patch-migrate-request.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-patch-migrate-request.json new file mode 100644 index 000000000..084a4736e --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-patch-migrate-request.json @@ -0,0 +1,3 @@ +{ + "sysadmin_password": "XXXXXXX" +} \ No newline at end of file diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-post-request.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-post-request.json new file mode 100644 index 000000000..f38e4f297 --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-post-request.json @@ -0,0 +1,8 @@ +{ + "peer_group_name": "pg-name", + "group_priority": 0, + "group_state": "enabled", + "max_subcloud_rehoming": 10, + "system_leader_id": "ac62f555-9386-42f1-b3a1-51ecb709409d", + "system_leader_name": "dc1-name" +} \ No newline at end of file diff --git a/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-post-response.json b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-post-response.json new file mode 100644 index 000000000..38c67a477 --- /dev/null +++ b/api-ref/source/samples/subcloud-peer-groups/subcloud-peer-groups-post-response.json @@ -0,0 +1,11 @@ +{ + "id": 9, + "peer_group_name": "pg-name", + "group_priority": 0, + "group_state": "enabled", + "max_subcloud_rehoming": 10, + "system_leader_id": "ac62f555-9386-42f1-b3a1-51ecb709409d", + "system_leader_name": "dc1-name", + "created-at": "2023-08-07 06:13:52.664047", + "updated-at": null +} \ No newline at end of file diff --git a/distributedcloud/dcmanager/api/controllers/v1/root.py b/distributedcloud/dcmanager/api/controllers/v1/root.py index d7deec5c4..d0df7b33e 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/root.py +++ b/distributedcloud/dcmanager/api/controllers/v1/root.py @@ -22,6 +22,7 @@ from dcmanager.api.controllers.v1 import phased_subcloud_deploy from dcmanager.api.controllers.v1 import subcloud_backup from dcmanager.api.controllers.v1 import subcloud_deploy from dcmanager.api.controllers.v1 import subcloud_group +from dcmanager.api.controllers.v1 import subcloud_peer_group from dcmanager.api.controllers.v1 import subclouds from dcmanager.api.controllers.v1 import sw_update_options from dcmanager.api.controllers.v1 import sw_update_strategy @@ -55,6 +56,8 @@ class Controller(object): SubcloudBackupController sub_controllers["phased-subcloud-deploy"] = phased_subcloud_deploy.\ PhasedSubcloudDeployController + sub_controllers["subcloud-peer-groups"] = \ + subcloud_peer_group.SubcloudPeerGroupsController sub_controllers["system-peers"] = system_peers.\ SystemPeersController diff --git a/distributedcloud/dcmanager/api/controllers/v1/subcloud_peer_group.py b/distributedcloud/dcmanager/api/controllers/v1/subcloud_peer_group.py new file mode 100644 index 000000000..004faf24c --- /dev/null +++ b/distributedcloud/dcmanager/api/controllers/v1/subcloud_peer_group.py @@ -0,0 +1,424 @@ +# Copyright (c) 2023 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from oslo_config import cfg +from oslo_db import exception as db_exc +from oslo_log import log as logging +from oslo_messaging import RemoteError + +import http.client as httpclient +import json +import pecan +from pecan import expose +from pecan import request +import uuid + +from dccommon import consts as dccommon_consts +from dccommon.drivers.openstack.sdk_platform import OpenStackDriver +from dccommon.drivers.openstack.sysinv_v1 import SysinvClient +from dcmanager.api.controllers import restcomm +from dcmanager.api.policies import subcloud_peer_group as subcloud_peer_group_policy +from dcmanager.api import policy +from dcmanager.common import consts +from dcmanager.common.i18n import _ +from dcmanager.common import utils +from dcmanager.db import api as db_api +from dcmanager.rpc import client as rpc_client + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +# validation constants for Subcloud Peer Group +MAX_SUBCLOUD_PEER_GROUP_NAME_LEN = 255 +MIN_SUBCLOUD_PEER_GROUP_SUBCLOUD_REHOMING = 1 +MAX_SUBCLOUD_PEER_GROUP_SUBCLOUD_REHOMING = 250 +MAX_SYSTEM_LEADER_NAME_LEN = 255 +MAX_SUBCLOUD_PEER_GROUP_PRIORITY = 65536 +MIN_SUBCLOUD_PEER_GROUP_PRIORITY = 0 +DEFAULT_SUBCLOUD_PEER_GROUP_PRIORITY = 0 +DEFAULT_SUBCLOUD_PEER_GROUP_MAX_REHOMING = 10 +SUPPORTED_GROUP_STATES = [ + consts.OPERATIONAL_ENABLED, + consts.OPERATIONAL_DISABLED +] + + +class SubcloudPeerGroupsController(restcomm.GenericPathController): + + def __init__(self): + super(SubcloudPeerGroupsController, self).__init__() + self.rpc_client = rpc_client.ManagerClient() + + @expose(generic=True, template='json') + def index(self): + # Route the request to specific methods with parameters + pass + + def _get_subcloud_list_for_peer_group(self, context, group_id): + subclouds = db_api.subcloud_get_for_peer_group(context, group_id) + return utils.subcloud_db_list_to_dict(subclouds) + + def _get_subcloud_peer_group_list(self, context): + groups = db_api.subcloud_peer_group_get_all(context) + subcloud_peer_group_list = [] + + for group in groups: + group_dict = db_api.subcloud_peer_group_db_model_to_dict(group) + subcloud_peer_group_list.append(group_dict) + + result = {'subcloud_peer_groups': subcloud_peer_group_list} + return result + + def _get_local_system(self): + try: + ks_client = OpenStackDriver( + region_name=dccommon_consts.DEFAULT_REGION_NAME, + region_clients=None + ) + sysinv_client = SysinvClient( + dccommon_consts.DEFAULT_REGION_NAME, + ks_client.keystone_client.session, + endpoint=ks_client.keystone_client.endpoint_cache.get_endpoint + ("sysinv"), + ) + system = sysinv_client.get_system() + return system + except Exception: + pecan.abort(httpclient.BAD_REQUEST, + _("Failed to get local system info")) + + def _get_subcloud_status_for_peer_group(self, context, group): + subclouds = db_api.subcloud_get_for_peer_group(context, group.id) + pg_status = dict() + pg_status['peer_group_id'] = group.id + pg_status['peer_group_name'] = group.peer_group_name + pg_status['total_subclouds'] = len(subclouds) + pg_status['complete'] = 0 + pg_status['waiting_for_migrate'] = 0 + pg_status['rehoming'] = 0 + pg_status['rehome_failed'] = 0 + pg_status['managed'] = 0 + pg_status['unmanaged'] = 0 + for subcloud in subclouds: + if subcloud.management_state == 'managed': + pg_status['managed'] += 1 + else: + pg_status['unmanaged'] += 1 + + if subcloud.deploy_status == 'secondary': + pg_status['waiting_for_migrate'] += 1 + elif subcloud.deploy_status == 'rehome-failed': + pg_status['rehome_failed'] += 1 + elif subcloud.deploy_status == 'rehome-prep-failed': + pg_status['rehome_failed'] += 1 + elif subcloud.deploy_status == 'complete': + pg_status['complete'] += 1 + elif subcloud.deploy_status == 'rehoming': + pg_status['rehoming'] += 1 + return pg_status + + @index.when(method='GET', template='json') + def get(self, group_ref=None, verb=None): + """Get details about subcloud peer group. + + :param verb: Specifies the get action to be taken + to the subcloud-peer-group get operation + :param group_ref: ID or name of subcloud peer group + """ + policy.authorize(subcloud_peer_group_policy.POLICY_ROOT % "get", {}, + restcomm.extract_credentials_for_policy()) + context = restcomm.extract_context_from_environ() + + if group_ref is None: + # List of subcloud peer groups requested + return self._get_subcloud_peer_group_list(context) + + group = utils.subcloud_peer_group_get_by_ref(context, group_ref) + if group is None: + pecan.abort(httpclient.NOT_FOUND, _("Subcloud Peer Group not found")) + if verb is None: + subcloud_peer_group_dict = db_api.subcloud_peer_group_db_model_to_dict(group) + return subcloud_peer_group_dict + elif verb == 'subclouds': + # Return only the subclouds for this subcloud peer group + return self._get_subcloud_list_for_peer_group(context, group.id) + elif verb == 'status': + return self._get_subcloud_status_for_peer_group(context, group) + else: + pecan.abort(400, _('Invalid request')) + + @index.when(method='POST', template='json') + def post(self): + """Create a new subcloud peer group.""" + policy.authorize(subcloud_peer_group_policy.POLICY_ROOT % "create", {}, + restcomm.extract_credentials_for_policy()) + context = restcomm.extract_context_from_environ() + + payload = json.loads(request.body) + if not payload: + pecan.abort(httpclient.BAD_REQUEST, _('Body required')) + + LOG.info("Handling create subcloud peer group request for: %s" % payload) + peer_group_name = payload.get('peer-group-name') + group_priority = payload.get('group-priority') + group_state = payload.get('group-state') + system_leader_id = payload.get('system-leader-id') + system_leader_name = payload.get('system-leader-name') + max_subcloud_rehoming = payload.get('max-subcloud-rehoming') + + local_system = None + # Validate payload + # peer_group_name is mandatory + if not self._validate_name(peer_group_name): + pecan.abort(httpclient.BAD_REQUEST, _('Invalid peer-group-name')) + if not system_leader_id: + # 1.Operator does not need to (and should not) specify + # system_leader_id for a local subcloud peer group which + # is supposed to group local subclouds being managed by + # local system, since the leader should be the local system + # 2.system_leader_id should be specified via API when the + # subcloud peer group is duplicated into peer system which + # is not the leader of this subcloud peer group + if not local_system: + local_system = self._get_local_system() + system_leader_id = local_system.uuid + elif not self._validate_system_leader_id(system_leader_id): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid system-leader-id [%s]' % (system_leader_id))) + if not system_leader_name: + # Get system_leader_name from local DC + # if no system_leader_name provided + if not local_system: + local_system = self._get_local_system() + system_leader_name = local_system.name + elif not self._validate_system_leader_name(system_leader_name): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid system-leader-name')) + if not group_priority: + group_priority = DEFAULT_SUBCLOUD_PEER_GROUP_PRIORITY + elif not self._validate_group_priority(group_priority): + pecan.abort(httpclient.BAD_REQUEST, _('Invalid group-priority')) + if not group_state: + group_state = consts.OPERATIONAL_ENABLED + elif not self._validate_group_state(group_state): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid group-state')) + if not max_subcloud_rehoming: + max_subcloud_rehoming = DEFAULT_SUBCLOUD_PEER_GROUP_MAX_REHOMING + elif not self._validate_max_subcloud_rehoming(max_subcloud_rehoming): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid max-subcloud-rehoming')) + + try: + group_ref = db_api.subcloud_peer_group_create(context, + peer_group_name, + group_priority, + group_state, + max_subcloud_rehoming, + system_leader_id, + system_leader_name) + return db_api.subcloud_peer_group_db_model_to_dict(group_ref) + except db_exc.DBDuplicateEntry: + pecan.abort(httpclient.CONFLICT, + _('A subcloud peer group with this name already exists')) + except RemoteError as e: + pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value) + except Exception as e: + LOG.exception(e) + pecan.abort(httpclient.INTERNAL_SERVER_ERROR, + _('Unable to create subcloud peer group')) + + @index.when(method='PATCH', template='json') + def patch(self, group_ref, verb=None): + """Update a subcloud peer group. + + :param verb: Specifies the get action to be taken + to the subcloud-peer-group patch operation + :param group_ref: ID or name of subcloud group to update + """ + + policy.authorize(subcloud_peer_group_policy.POLICY_ROOT % "modify", {}, + restcomm.extract_credentials_for_policy()) + context = restcomm.extract_context_from_environ() + if group_ref is None: + pecan.abort(httpclient.BAD_REQUEST, + _('Subcloud Peer Group Name or ID required')) + + group = utils.subcloud_peer_group_get_by_ref(context, group_ref) + if group is None: + pecan.abort(httpclient.NOT_FOUND, _('Subcloud Peer Group not found')) + if verb is None: + payload = json.loads(request.body) + if not payload: + pecan.abort(httpclient.BAD_REQUEST, _('Body required')) + + LOG.info("Handling update subcloud peer group request for: %s" % payload) + peer_group_name = payload.get('peer-group-name') + group_priority = payload.get('group-priority') + group_state = payload.get('group-state') + system_leader_id = payload.get('system-leader-id') + system_leader_name = payload.get('system-leader-name') + max_subcloud_rehoming = payload.get('max-subcloud-rehoming') + + if not ( + peer_group_name + or group_priority + or group_state + or system_leader_id + or system_leader_name + or max_subcloud_rehoming + ): + pecan.abort(httpclient.BAD_REQUEST, _('nothing to update')) + + # Check value is not None or empty before calling validation function + if peer_group_name and not self._validate_name(peer_group_name): + pecan.abort(httpclient.BAD_REQUEST, _('Invalid peer-group-name')) + if group_priority and not self._validate_group_priority(group_priority): + pecan.abort(httpclient.BAD_REQUEST, _('Invalid group-priority')) + if group_state and not self._validate_group_state(group_state): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid group-state')) + if (max_subcloud_rehoming and + not self._validate_max_subcloud_rehoming(max_subcloud_rehoming)): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid max-subcloud-rehoming')) + if (system_leader_id and + not self._validate_system_leader_id(system_leader_id)): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid system-leader-id')) + if (system_leader_name and + not self._validate_system_leader_name(system_leader_name)): + pecan.abort(httpclient.BAD_REQUEST, + _('Invalid system-leader-name')) + + try: + updated_peer_group = db_api.subcloud_peer_group_update( + context, + group.id, + peer_group_name=peer_group_name, + group_priority=group_priority, + group_state=group_state, + max_subcloud_rehoming=max_subcloud_rehoming, + system_leader_id=system_leader_id, + system_leader_name=system_leader_name) + return db_api.subcloud_peer_group_db_model_to_dict(updated_peer_group) + except RemoteError as e: + pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value) + except Exception as e: + # additional exceptions. + LOG.exception(e) + pecan.abort(httpclient.INTERNAL_SERVER_ERROR, + _('Unable to update subcloud peer group')) + elif verb == 'migrate': + # TODO(tao): Subcloud Peer Group migrate implementation will + # be submitted in the follow-up review. + pass + else: + pecan.abort(400, _('Invalid request')) + + def _validate_name(self, name): + # Reject post and update operations for name that: + # - attempt to set to None + # - attempt to set to a number + # - exceed the max length + if not name: + return False + if name.isdigit(): + LOG.warning("Invalid name [%s], can not be digit" % name) + return False + if len(name) > MAX_SUBCLOUD_PEER_GROUP_NAME_LEN: + LOG.warning("Invalid name length") + return False + # none is not a valid name + if name.lower() == 'none': + LOG.warning("Invalid name, cannot use 'none' as name") + return False + return True + + def _validate_group_priority(self, priority): + try: + # Check the value is an integer + val = int(priority) + except ValueError: + return False + # We do not support less than min or greater than max + if val < MIN_SUBCLOUD_PEER_GROUP_PRIORITY: + return False + if val > MAX_SUBCLOUD_PEER_GROUP_PRIORITY: + return False + return True + + def _validate_group_state(self, state): + if state not in SUPPORTED_GROUP_STATES: + return False + return True + + def _validate_max_subcloud_rehoming(self, max_parallel_str): + try: + # Check the value is an integer + val = int(max_parallel_str) + except ValueError: + return False + + # We do not support less than min or greater than max + if val < MIN_SUBCLOUD_PEER_GROUP_SUBCLOUD_REHOMING: + return False + if val > MAX_SUBCLOUD_PEER_GROUP_SUBCLOUD_REHOMING: + return False + return True + + def _validate_system_leader_name(self, name): + if len(name) > MAX_SYSTEM_LEADER_NAME_LEN: + return False + return True + + def _validate_system_leader_id(self, uuid_str): + try: + uuid.UUID(str(uuid_str)) + return True + except Exception: + return False + + @index.when(method='delete', template='json') + def delete(self, group_ref): + """Delete the subcloud peer group.""" + policy.authorize(subcloud_peer_group_policy.POLICY_ROOT % "delete", {}, + restcomm.extract_credentials_for_policy()) + context = restcomm.extract_context_from_environ() + + if group_ref is None: + pecan.abort(httpclient.BAD_REQUEST, + _('Subcloud Peer Group Name or ID required')) + group = utils.subcloud_peer_group_get_by_ref(context, group_ref) + if group is None: + LOG.info("Subcloud Peer Group [%s] not found" % group_ref) + pecan.abort(httpclient.NOT_FOUND, _('Subcloud Peer Group not found')) + + LOG.info("Handling delete subcloud peer group request for: %s" % group) + # TODO(Jon): uncomment in Association of System and Peer Group management commit + ''' + # a peer group may not be deleted if it is used by any associations + association = db_api.peer_group_association_get_by_peer_group_id(context, + group.id) + if len(association) > 0: + pecan.abort(httpclient.BAD_REQUEST, + _("Cannot delete a peer group " + "which is associated with a system peer.")) + ''' + try: + db_api.subcloud_peer_group_destroy(context, group.id) + # Disassociate the subcloud. + subclouds = db_api.subcloud_get_for_peer_group(context, group.id) + for subcloud in subclouds: + db_api.subcloud_update(context, subcloud.id, + peer_group_id='none') + except RemoteError as e: + pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value) + except Exception as e: + LOG.exception(e) + pecan.abort(httpclient.INTERNAL_SERVER_ERROR, + _('Unable to delete subcloud peer group')) diff --git a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py index ca04a8e1a..9c06022bd 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py @@ -652,6 +652,7 @@ class SubcloudsController(object): description = payload.get('description') location = payload.get('location') bootstrap_values = payload.get('bootstrap_values') + peer_group = payload.get('peer_group') bootstrap_address = payload.get('bootstrap_address') # Syntax checking @@ -683,6 +684,26 @@ class SubcloudsController(object): exceptions.SubcloudGroupNotFound): pecan.abort(400, _('Invalid group')) + # Verify the peer_group is valid + peer_group_id = None + if peer_group is not None: + # peer_group may be passed in the payload as an int or str + peer_group = str(peer_group) + # Check if user wants to remove a subcloud + # from a subcloud-peer-group by + # setting peer_group_id as 'none', + # then we will pass 'none' string as + # the peer_group_id, + # update_subcloud() will handle it and + # Set the peer_group_id DB into None. + if peer_group.lower() == 'none': + peer_group_id = 'none' + else: + pgrp = utils.subcloud_peer_group_get_by_ref(context, peer_group) + if not pgrp: + pecan.abort(400, _('Invalid peer group')) + peer_group_id = pgrp.id + if consts.INSTALL_VALUES in payload: psd_common.validate_install_values(payload, subcloud) payload['data_install'] = json.dumps(payload[consts.INSTALL_VALUES]) @@ -697,6 +718,7 @@ class SubcloudsController(object): description=description, location=location, group_id=group_id, data_install=payload.get('data_install'), force=force_flag, + peer_group_id=peer_group_id, bootstrap_values=bootstrap_values, bootstrap_address=bootstrap_address) return subcloud diff --git a/distributedcloud/dcmanager/api/policies/__init__.py b/distributedcloud/dcmanager/api/policies/__init__.py index 817cde5b8..ea8db37cf 100644 --- a/distributedcloud/dcmanager/api/policies/__init__.py +++ b/distributedcloud/dcmanager/api/policies/__init__.py @@ -12,6 +12,7 @@ from dcmanager.api.policies import phased_subcloud_deploy from dcmanager.api.policies import subcloud_backup from dcmanager.api.policies import subcloud_deploy from dcmanager.api.policies import subcloud_group +from dcmanager.api.policies import subcloud_peer_group from dcmanager.api.policies import subclouds from dcmanager.api.policies import sw_update_options from dcmanager.api.policies import sw_update_strategy @@ -29,5 +30,6 @@ def list_rules(): subcloud_group.list_rules(), subcloud_backup.list_rules(), phased_subcloud_deploy.list_rules(), + subcloud_peer_group.list_rules(), system_peers.list_rules() ) diff --git a/distributedcloud/dcmanager/api/policies/subcloud_peer_group.py b/distributedcloud/dcmanager/api/policies/subcloud_peer_group.py new file mode 100644 index 000000000..83bbdf4cc --- /dev/null +++ b/distributedcloud/dcmanager/api/policies/subcloud_peer_group.py @@ -0,0 +1,85 @@ +# +# Copyright (c) 2023 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from dcmanager.api.policies import base +from oslo_policy import policy + +POLICY_ROOT = 'dc_api:subcloud_peer_groups:%s' + + +_subcloud_peer_groups_rules = [ + + # CRUD of subcloud-peer-groups entity + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'create', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Create subcloud peer group.", + operations=[ + { + 'method': 'POST', + 'path': '/v1.0/subcloud-peer-groups' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'delete', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Delete subcloud peer group.", + operations=[ + { + 'method': 'DELETE', + 'path': '/v1.0/subcloud-peer-groups/{subcloud_peer_group}' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'get', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Get Subcloud Peer Group data", + operations=[ + { + 'method': 'GET', + 'path': '/v1.0/subcloud-peer-groups/' + }, + # Show details of a specified Subcloud Peer Group + { + 'method': 'GET', + 'path': '/v1.0/subcloud-peer-groups/{subcloud_peer_group}' + }, + # Show subclouds status of the subcloud-peer-group + { + 'method': 'GET', + 'path': '/v1.0/subcloud-peer-groups/{subcloud_peer_group}/status' + }, + # List Subclouds assigned to the given Subcloud Peer Group + { + 'method': 'GET', + 'path': '/v1.0/subcloud-peer-groups/{subcloud_peer_group}/subclouds' + } + ] + ), + # Update a Subcloud Peer Group with specified configuration + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'modify', + check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + description="Update a Subcloud Peer Group with specified configuration", + operations=[ + { + 'method': 'PATCH', + 'path': '/v1.0/subcloud-peer-groups/{subcloud_peer_group}' + }, + # Migrate subclouds entity of the subcloud-peer-group + { + 'method': 'PATCH', + 'path': '/v1.0/subcloud-peer-groups/{subcloud_peer_group}/migrate' + } + ] + ) +] + + +def list_rules(): + return _subcloud_peer_groups_rules diff --git a/distributedcloud/dcmanager/common/exceptions.py b/distributedcloud/dcmanager/common/exceptions.py index 650a96733..c7d8fa0de 100644 --- a/distributedcloud/dcmanager/common/exceptions.py +++ b/distributedcloud/dcmanager/common/exceptions.py @@ -169,6 +169,14 @@ class SubcloudGroupNameNotFound(NotFound): message = _("Subcloud Group with name %(name)s doesn't exist.") +class SubcloudPeerGroupNameNotFound(NotFound): + message = _("Subcloud Peer Group with name %(name)s doesn't exist.") + + +class SubcloudPeerGroupNotFound(NotFound): + message = _("Subcloud Peer Group with id %(group_id)s doesn't exist.") + + class SubcloudGroupNameViolation(DCManagerException): message = _("Default Subcloud Group name cannot be changed or reused.") diff --git a/distributedcloud/dcmanager/common/utils.py b/distributedcloud/dcmanager/common/utils.py index d7d75587e..0ed75f687 100644 --- a/distributedcloud/dcmanager/common/utils.py +++ b/distributedcloud/dcmanager/common/utils.py @@ -517,6 +517,11 @@ def system_peer_get_by_ref(context, peer_ref): return None +def subcloud_peer_group_db_list_to_dict(peer_groups): + return {'subcloud_peer_groups': [db_api.subcloud_peer_group_db_model_to_dict( + peer_group) for peer_group in peer_groups]} + + def subcloud_get_by_ref(context, subcloud_ref): """Handle getting a subcloud by either name, or ID @@ -548,6 +553,21 @@ def subcloud_group_get_by_ref(context, group_ref): return group +def subcloud_peer_group_get_by_ref(context, group_ref): + """Handle getting a peer group by either name, or ID""" + try: + if group_ref.isdigit(): + # Lookup subcloud group as an ID + group = db_api.subcloud_peer_group_get(context, group_ref) + else: + # Lookup subcloud group as a name + group = db_api.subcloud_peer_group_get_by_name(context, group_ref) + except (exceptions.SubcloudPeerGroupNotFound, + exceptions.SubcloudPeerGroupNameNotFound): + return None + return group + + def subcloud_db_list_to_dict(subclouds): return {'subclouds': [db_api.subcloud_db_model_to_dict(subcloud) for subcloud in subclouds]} diff --git a/distributedcloud/dcmanager/db/api.py b/distributedcloud/dcmanager/db/api.py index 5cf18b4cc..1ca1182d7 100644 --- a/distributedcloud/dcmanager/db/api.py +++ b/distributedcloud/dcmanager/db/api.py @@ -125,6 +125,7 @@ def subcloud_db_model_to_dict(subcloud): "created-at": subcloud.created_at, "updated-at": subcloud.updated_at, "group_id": subcloud.group_id, + "peer_group_id": subcloud.peer_group_id, "rehome_data": subcloud.rehome_data} return result @@ -195,7 +196,7 @@ def subcloud_update(context, subcloud_id, management_state=None, data_install=None, data_upgrade=None, first_identity_sync_complete=None, systemcontroller_gateway_ip=None, - rehome_data=None): + peer_group_id=None, rehome_data=None): """Update a subcloud or raise if it does not exist.""" return IMPL.subcloud_update(context, subcloud_id, management_state, availability_status, software_version, name, @@ -205,7 +206,7 @@ def subcloud_update(context, subcloud_id, management_state=None, backup_datetime, error_description, openstack_installed, group_id, data_install, data_upgrade, first_identity_sync_complete, - systemcontroller_gateway_ip, rehome_data) + systemcontroller_gateway_ip, peer_group_id, rehome_data) def subcloud_bulk_update_by_ids(context, subcloud_ids, update_form): @@ -450,9 +451,84 @@ def system_peer_update(context, peer_id, def system_peer_destroy(context, peer_id): """Destroy the system peer or raise if it does not exist.""" return IMPL.system_peer_destroy(context, peer_id) +################### ################### +# subcloud_peer_group +def subcloud_peer_group_db_model_to_dict(subcloud_peer_group): + """Convert subcloud_peer_group db model to dictionary.""" + result = {"id": subcloud_peer_group.id, + "peer_group_name": subcloud_peer_group.peer_group_name, + "group_priority": subcloud_peer_group.group_priority, + "group_state": subcloud_peer_group.group_state, + "max_subcloud_rehoming": subcloud_peer_group.max_subcloud_rehoming, + "system_leader_id": subcloud_peer_group.system_leader_id, + "system_leader_name": subcloud_peer_group.system_leader_name, + "created-at": subcloud_peer_group.created_at, + "updated-at": subcloud_peer_group.updated_at} + return result + + +def subcloud_peer_group_create(context, peer_group_name, group_priority, group_state, + max_subcloud_rehoming, system_leader_id, system_leader_name): + """Create a subcloud_peer_group.""" + return IMPL.subcloud_peer_group_create(context, + peer_group_name, + group_priority, + group_state, + max_subcloud_rehoming, + system_leader_id, + system_leader_name) + + +def subcloud_peer_group_destroy(context, group_id): + """Destroy the subcloud peer group or raise if it does not exist.""" + return IMPL.subcloud_peer_group_destroy(context, group_id) + + +def subcloud_peer_group_get(context, group_id): + """Retrieve a subcloud_peer_group or raise if it does not exist.""" + return IMPL.subcloud_peer_group_get(context, group_id) + + +def subcloud_peer_group_get_by_name(context, name): + """Retrieve a subcloud_peer_group by name or raise if it does not exist.""" + return IMPL.subcloud_peer_group_get_by_name(context, name) + + +def subcloud_peer_group_get_by_leader_id(context, system_leader_id): + """Retrieve subcloud peer groups by system_leader_id.""" + return IMPL.subcloud_peer_group_get_by_leader_id(context, system_leader_id) + + +def subcloud_get_for_peer_group(context, group_id): + """Retrieve all subclouds belonging to a subcloud_peer_group + + or raise if it does not exist. + """ + return IMPL.subcloud_get_for_peer_group(context, group_id) + + +def subcloud_peer_group_get_all(context): + """Retrieve all subcloud peer groups.""" + return IMPL.subcloud_peer_group_get_all(context) + + +def subcloud_peer_group_update(context, group_id, peer_group_name, group_priority, + group_state, max_subcloud_rehoming, system_leader_id, + system_leader_name): + """Update the subcloud peer group or raise if it does not exist.""" + return IMPL.subcloud_peer_group_update(context, + group_id, + peer_group_name, + group_priority, + group_state, + max_subcloud_rehoming, + system_leader_id, + system_leader_name) +################### + def sw_update_strategy_db_model_to_dict(sw_update_strategy): """Convert sw update db model to dictionary.""" diff --git a/distributedcloud/dcmanager/db/sqlalchemy/api.py b/distributedcloud/dcmanager/db/sqlalchemy/api.py index f57505f34..6d08d5c47 100644 --- a/distributedcloud/dcmanager/db/sqlalchemy/api.py +++ b/distributedcloud/dcmanager/db/sqlalchemy/api.py @@ -420,6 +420,7 @@ def subcloud_update(context, subcloud_id, management_state=None, data_upgrade=None, first_identity_sync_complete=None, systemcontroller_gateway_ip=None, + peer_group_id=None, rehome_data=None): with write_session() as session: subcloud_ref = subcloud_get(context, subcloud_id) @@ -466,6 +467,11 @@ def subcloud_update(context, subcloud_id, management_state=None, if systemcontroller_gateway_ip is not None: subcloud_ref.systemcontroller_gateway_ip = \ systemcontroller_gateway_ip + if peer_group_id is not None: + if str(peer_group_id).lower() == 'none': + subcloud_ref.peer_group_id = None + else: + subcloud_ref.peer_group_id = peer_group_id if rehome_data is not None: subcloud_ref.rehome_data = rehome_data subcloud_ref.save(session) @@ -1069,6 +1075,132 @@ def initialize_subcloud_group_default(engine): ########################## +########################## +# subcloud peer group +########################## +@require_context +def subcloud_peer_group_get(context, group_id): + try: + result = model_query(context, models.SubcloudPeerGroup). \ + filter_by(deleted=0). \ + filter_by(id=group_id). \ + one() + except NoResultFound: + raise exception.SubcloudPeerGroupNotFound(group_id=group_id) + except MultipleResultsFound: + raise exception.InvalidParameterValue( + err="Multiple entries found for subcloud peer group %s" % group_id) + + return result + + +@require_context +def subcloud_get_for_peer_group(context, peer_group_id): + """Get all subclouds for a subcloud peer group. + + :param context: request context object + :param peer_group_id: ID of the subcloud peer group + """ + return model_query(context, models.Subcloud). \ + filter_by(deleted=0). \ + filter_by(peer_group_id=peer_group_id). \ + order_by(models.Subcloud.id). \ + all() + + +@require_context +def subcloud_peer_group_get_all(context): + result = model_query(context, models.SubcloudPeerGroup). \ + filter_by(deleted=0). \ + order_by(models.SubcloudPeerGroup.id). \ + all() + + return result + + +@require_context +def subcloud_peer_group_get_by_name(context, name): + try: + result = model_query(context, models.SubcloudPeerGroup). \ + filter_by(deleted=0). \ + filter_by(peer_group_name=name). \ + one() + except NoResultFound: + raise exception.SubcloudPeerGroupNameNotFound(name=name) + except MultipleResultsFound: + # This exception should never happen due to the UNIQUE setting for name + raise exception.InvalidParameterValue( + err="Multiple entries found for subcloud peer group %s" % name) + + return result + + +@require_context +def subcloud_peer_group_get_by_leader_id(context, system_leader_id): + result = model_query(context, models.SubcloudPeerGroup). \ + filter_by(deleted=0). \ + filter_by(system_leader_id=system_leader_id). \ + order_by(models.SubcloudPeerGroup.id). \ + all() + + return result + + +@require_admin_context +def subcloud_peer_group_create(context, + peer_group_name, + group_priority, + group_state, + max_subcloud_rehoming, + system_leader_id, + system_leader_name): + with write_session() as session: + subcloud_peer_group_ref = models.SubcloudPeerGroup() + subcloud_peer_group_ref.peer_group_name = peer_group_name + subcloud_peer_group_ref.group_priority = group_priority + subcloud_peer_group_ref.group_state = group_state + subcloud_peer_group_ref.max_subcloud_rehoming = max_subcloud_rehoming + subcloud_peer_group_ref.system_leader_id = system_leader_id + subcloud_peer_group_ref.system_leader_name = system_leader_name + session.add(subcloud_peer_group_ref) + return subcloud_peer_group_ref + + +@require_admin_context +def subcloud_peer_group_destroy(context, group_id): + with write_session() as session: + subcloud_peer_group_ref = subcloud_peer_group_get(context, group_id) + session.delete(subcloud_peer_group_ref) + + +@require_admin_context +def subcloud_peer_group_update(context, + group_id, + peer_group_name=None, + group_priority=None, + group_state=None, + max_subcloud_rehoming=None, + system_leader_id=None, + system_leader_name=None): + with write_session() as session: + subcloud_peer_group_ref = subcloud_peer_group_get(context, group_id) + if peer_group_name is not None: + subcloud_peer_group_ref.peer_group_name = peer_group_name + if group_priority is not None: + subcloud_peer_group_ref.group_priority = group_priority + if group_state is not None: + subcloud_peer_group_ref.group_state = group_state + if max_subcloud_rehoming is not None: + subcloud_peer_group_ref.max_subcloud_rehoming = max_subcloud_rehoming + if system_leader_id is not None: + subcloud_peer_group_ref.system_leader_id = system_leader_id + if system_leader_name is not None: + subcloud_peer_group_ref.system_leader_name = system_leader_name + subcloud_peer_group_ref.save(session) + return subcloud_peer_group_ref +########################## + + @require_context def strategy_step_get(context, subcloud_id): result = model_query(context, models.StrategyStep). \ diff --git a/distributedcloud/dcmanager/db/sqlalchemy/migrate_repo/versions/014_add_subcloud_peer_group_and_association.py b/distributedcloud/dcmanager/db/sqlalchemy/migrate_repo/versions/014_add_subcloud_peer_group_and_association.py index e71a0389b..af04d98f2 100644 --- a/distributedcloud/dcmanager/db/sqlalchemy/migrate_repo/versions/014_add_subcloud_peer_group_and_association.py +++ b/distributedcloud/dcmanager/db/sqlalchemy/migrate_repo/versions/014_add_subcloud_peer_group_and_association.py @@ -16,6 +16,32 @@ def upgrade(migrate_engine): # Add the 'rehome_data' column to the subclouds table. subclouds.create_column(sqlalchemy.Column('rehome_data', sqlalchemy.Text)) + # Declare the new subcloud_peer_group table + subcloud_peer_group = sqlalchemy.Table( + 'subcloud_peer_group', meta, + sqlalchemy.Column('id', sqlalchemy.Integer, + primary_key=True, + autoincrement=True, + nullable=False), + sqlalchemy.Column('peer_group_name', sqlalchemy.String(255), unique=True), + sqlalchemy.Column('group_priority', sqlalchemy.Integer), + sqlalchemy.Column('group_state', sqlalchemy.String(255)), + sqlalchemy.Column('system_leader_id', sqlalchemy.String(255)), + sqlalchemy.Column('system_leader_name', sqlalchemy.String(255)), + sqlalchemy.Column('max_subcloud_rehoming', sqlalchemy.Integer), + sqlalchemy.Column('reserved_1', sqlalchemy.Text), + sqlalchemy.Column('reserved_2', sqlalchemy.Text), + sqlalchemy.Column('created_at', sqlalchemy.DateTime), + sqlalchemy.Column('updated_at', sqlalchemy.DateTime), + sqlalchemy.Column('deleted_at', sqlalchemy.DateTime), + sqlalchemy.Column('deleted', sqlalchemy.Integer, default=0), + mysql_engine=ENGINE, + mysql_charset=CHARSET + ) + subcloud_peer_group.create() + # Add the 'peer_greoup_id' column to the subclouds table. + subclouds.create_column(sqlalchemy.Column('peer_group_id', sqlalchemy.Integer)) + # Declare the new system_peer table system_peer = sqlalchemy.Table( 'system_peer', meta, diff --git a/distributedcloud/dcmanager/db/sqlalchemy/models.py b/distributedcloud/dcmanager/db/sqlalchemy/models.py index e7a12c45a..2ec76318a 100644 --- a/distributedcloud/dcmanager/db/sqlalchemy/models.py +++ b/distributedcloud/dcmanager/db/sqlalchemy/models.py @@ -131,6 +131,20 @@ class SubcloudGroup(BASE, DCManagerBase): max_parallel_subclouds = Column(Integer) +class SubcloudPeerGroup(BASE, DCManagerBase): + """Represents a subcloud group""" + + __tablename__ = 'subcloud_peer_group' + + id = Column(Integer, primary_key=True, autoincrement=True, nullable=False) + peer_group_name = Column(String(255), unique=True) + group_priority = Column(Integer) + group_state = Column(String(255)) + max_subcloud_rehoming = Column(Integer) + system_leader_id = Column(String(255)) + system_leader_name = Column(String(255)) + + class Subcloud(BASE, DCManagerBase): """Represents a subcloud""" @@ -158,6 +172,8 @@ class Subcloud(BASE, DCManagerBase): systemcontroller_gateway_ip = Column(String(255)) audit_fail_count = Column(Integer) first_identity_sync_complete = Column(Boolean, default=False) + peer_group_id = Column(Integer, + ForeignKey('subcloud_peer_group.id')) rehome_data = Column(Text()) # multiple subclouds can be in a particular group diff --git a/distributedcloud/dcmanager/manager/service.py b/distributedcloud/dcmanager/manager/service.py index e85caeffe..da2ae3317 100644 --- a/distributedcloud/dcmanager/manager/service.py +++ b/distributedcloud/dcmanager/manager/service.py @@ -136,7 +136,7 @@ class DCManagerService(service.Service): description=None, location=None, group_id=None, data_install=None, force=None, deploy_status=None, - bootstrap_values=None, bootstrap_address=None): + peer_group_id=None, bootstrap_values=None, bootstrap_address=None): # Updates a subcloud LOG.info("Handling update_subcloud request for: %s" % subcloud_id) subcloud = self.subcloud_manager.update_subcloud(context, subcloud_id, @@ -147,6 +147,7 @@ class DCManagerService(service.Service): data_install, force, deploy_status, + peer_group_id, bootstrap_values, bootstrap_address) return subcloud diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index 2a1a982c7..70683aecf 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -2313,6 +2313,7 @@ class SubcloudManager(manager.Manager): data_install=None, force=None, deploy_status=None, + peer_group_id=None, bootstrap_values=None, bootstrap_address=None): """Update subcloud and notify orchestrators. @@ -2326,6 +2327,7 @@ class SubcloudManager(manager.Manager): :param data_install: subcloud install values :param force: force flag :param deploy_status: update to expected deploy status + :param peer_group_id: id of peer group :param bootstrap_values: bootstrap_values yaml content :param bootstrap_address: oam IP for rehome """ @@ -2449,6 +2451,7 @@ class SubcloudManager(manager.Manager): group_id=group_id, data_install=data_install, deploy_status=new_deploy_status, + peer_group_id=peer_group_id, rehome_data=rehome_data ) diff --git a/distributedcloud/dcmanager/rpc/client.py b/distributedcloud/dcmanager/rpc/client.py index 9cafd765b..d98239bbf 100644 --- a/distributedcloud/dcmanager/rpc/client.py +++ b/distributedcloud/dcmanager/rpc/client.py @@ -148,7 +148,7 @@ class ManagerClient(RPCClient): def update_subcloud(self, ctxt, subcloud_id, management_state=None, description=None, location=None, group_id=None, data_install=None, force=None, - deploy_status=None, bootstrap_values=None, bootstrap_address=None): + deploy_status=None, peer_group_id=None, bootstrap_values=None, bootstrap_address=None): return self.call(ctxt, self.make_msg('update_subcloud', subcloud_id=subcloud_id, management_state=management_state, @@ -158,6 +158,7 @@ class ManagerClient(RPCClient): data_install=data_install, force=force, deploy_status=deploy_status, + peer_group_id=peer_group_id, bootstrap_values=bootstrap_values, bootstrap_address=bootstrap_address)) diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_peer_group.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_peer_group.py new file mode 100644 index 000000000..536462719 --- /dev/null +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subcloud_peer_group.py @@ -0,0 +1,332 @@ +# Copyright (c) 2023 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import mock +from six.moves import http_client + +from dcmanager.db.sqlalchemy import api as db_api +from dcmanager.rpc import client as rpc_client + +from dcmanager.tests.unit.api import test_root_controller as testroot +from dcmanager.tests.unit.api.v1.controllers.mixins import APIMixin +from dcmanager.tests.unit.api.v1.controllers.mixins import PostJSONMixin +from dcmanager.tests.unit.api.v1.controllers.test_subclouds \ + import FAKE_SUBCLOUD_DATA +from dcmanager.tests import utils + +SAMPLE_SUBCLOUD_PEER_GROUP_NAME = 'GroupX' +SAMPLE_SUBCLOUD_PEER_GROUP_MAX_SUBCLOUDS_REHOMING = 50 +SAMPLE_SUBCLOUD_PEER_GROUP_PIRORITY = 0 + +API_PREFIX = '/v1.0/subcloud-peer-groups' +RESULT_KEY = 'subcloud_peer_groups' +EXPECTED_FIELDS = ["id", + "peer_group_name", + "group_priority", + "group_state", + "max_subcloud_rehoming", + "system_leader_id", + "system_leader_name", + "created-at", + "updated-at"] + + +class SubcloudPeerGroupAPIMixin(APIMixin): + + def validate_entry(self, result_item): + self.assert_fields(result_item) + + def setUp(self): + super(SubcloudPeerGroupAPIMixin, self).setUp() + self.fake_rpc_client.some_method = mock.MagicMock() + + def _get_test_subcloud_peer_group_request(self, **kw): + # id should not be part of the structure + group = { + 'peer-group-name': kw.get('peer_group_name', SAMPLE_SUBCLOUD_PEER_GROUP_NAME), + 'system-leader-id': kw.get( + 'system_leader_id', + '62c9592d-f799-4db9-8d40-6786a74d6021'), + 'system-leader-name': kw.get( + 'system_leader_name', + 'dc-test'), + 'group-priority': kw.get( + 'group_priority', + '0'), + 'group-state': kw.get( + 'group_state', + 'enabled'), + 'max-subcloud-rehoming': kw.get( + 'max_subcloud_rehoming', + SAMPLE_SUBCLOUD_PEER_GROUP_MAX_SUBCLOUDS_REHOMING) + } + return group + + def _get_test_subcloud_peer_group_dict(self, **kw): + # id should not be part of the structure + group = { + 'peer_group_name': kw.get('peer_group_name', SAMPLE_SUBCLOUD_PEER_GROUP_NAME), + 'system_leader_id': kw.get( + 'system_leader_id', + '62c9592d-f799-4db9-8d40-6786a74d6021'), + 'system_leader_name': kw.get( + 'system_leader_name', + 'dc-test'), + 'group_priority': kw.get( + 'group_priority', + '0'), + 'group_state': kw.get( + 'group_state', + 'enabled'), + 'max_subcloud_rehoming': kw.get( + 'max_subcloud_rehoming', + SAMPLE_SUBCLOUD_PEER_GROUP_MAX_SUBCLOUDS_REHOMING) + } + return group + + def _post_get_test_subcloud_peer_group(self, **kw): + post_body = self._get_test_subcloud_peer_group_request(**kw) + return post_body + + # The following methods are required for subclasses of APIMixin + def get_api_prefix(self): + return API_PREFIX + + def get_result_key(self): + return RESULT_KEY + + def get_expected_api_fields(self): + return EXPECTED_FIELDS + + def get_omitted_api_fields(self): + return [] + + def _create_db_object(self, context, **kw): + creation_fields = self._get_test_subcloud_peer_group_dict(**kw) + return db_api.subcloud_peer_group_create(context, **creation_fields) + + def get_post_object(self): + return self._post_get_test_subcloud_peer_group() + + def get_update_object(self): + update_object = { + 'system_leader_name': 'Updated system_leader_name' + } + return update_object + + +# Combine Subcloud Group API with mixins to test post, get, update and delete +class TestSubcloudPeerGroupPost(testroot.DCManagerApiTest, + SubcloudPeerGroupAPIMixin, + PostJSONMixin): + def setUp(self): + super(TestSubcloudPeerGroupPost, self).setUp() + + def verify_post_failure(self, response): + # Failures will return text rather than json + self.assertEqual(response.content_type, 'text/plain') + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_create_with_numerical_name_fails(self, mock_client): + # A numerical name is not permitted. otherwise the 'get' operations + # which support getting by either name or ID could become confused + # if a name for one group was the same as an ID for another. + ndict = self.get_post_object() + ndict['peer-group-name'] = '123' + response = self.app.post_json(API_PREFIX, + ndict, + headers=self.get_api_headers(), + expect_errors=True) + self.verify_post_failure(response) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_create_with_none_string_name_fails(self, mock_client): + # A name as 'none' not permitted. + # None is a special word for clean a peer-group-id from subcloud. + ndict = self.get_post_object() + ndict['peer-group-name'] = 'none' + response = self.app.post_json(API_PREFIX, + ndict, + headers=self.get_api_headers(), + expect_errors=True) + self.verify_post_failure(response) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_create_with_blank_name_fails(self, mock_client): + # An empty name is not permitted + ndict = self.get_post_object() + ndict['peer-group-name'] = '' + response = self.app.post_json(API_PREFIX, + ndict, + headers=self.get_api_headers(), + expect_errors=True) + self.verify_post_failure(response) + + +class TestSubcloudPeerGroupGet(testroot.DCManagerApiTest, + SubcloudPeerGroupAPIMixin): + def setUp(self): + super(TestSubcloudPeerGroupGet, self).setUp() + # Override initial_list_size. Default group is setup during db sync + self.initial_list_size = 1 + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_get_single_by_name(self, mock_client): + # create a group + context = utils.dummy_context() + group_name = 'TestGroup' + system_id = '0907033e-b7ec-4832-92ad-4b0913580b3b' + self._create_db_object( + context, peer_group_name=group_name, system_leader_id=system_id) + + # Test that a GET operation for a valid ID works + response = self.app.get(self.get_single_url(group_name), + headers=self.get_api_headers()) + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) + self.validate_entry(response.json) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_list_subclouds_empty(self, mock_client): + # API GET on: subcloud-peer-groups//subclouds + # create a subcloud peer group + context = utils.dummy_context() + group_name = 'TestGroup' + system_id = '0907033e-b7ec-4832-92ad-4b0913580b3b' + self._create_db_object( + context, peer_group_name=group_name, system_leader_id=system_id) + url = '%s/%s/subclouds' % (API_PREFIX, group_name) + response = self.app.get(url, + headers=self.get_api_headers()) + # This API returns 'subclouds' rather than 'subcloud-peer-groups' + self.assertIn('subclouds', response.json) + # no subclouds exist yet, so this length should be zero + result_list = response.json.get('subclouds') + self.assertEqual(0, len(result_list)) + + def _create_subcloud_db_object(self, context): + creation_fields = { + 'name': FAKE_SUBCLOUD_DATA.get('name'), + 'description': FAKE_SUBCLOUD_DATA.get('description'), + 'location': FAKE_SUBCLOUD_DATA.get('location'), + 'software_version': FAKE_SUBCLOUD_DATA.get('software_version'), + 'management_subnet': FAKE_SUBCLOUD_DATA.get('management_subnet'), + 'management_gateway_ip': + FAKE_SUBCLOUD_DATA.get('management_gateway_ip'), + 'management_start_ip': + FAKE_SUBCLOUD_DATA.get('management_start_ip'), + 'management_end_ip': FAKE_SUBCLOUD_DATA.get('management_end_ip'), + 'systemcontroller_gateway_ip': + FAKE_SUBCLOUD_DATA.get('systemcontroller_gateway_ip'), + 'deploy_status': FAKE_SUBCLOUD_DATA.get('deploy_status'), + 'error_description': FAKE_SUBCLOUD_DATA.get('error_description'), + 'openstack_installed': + FAKE_SUBCLOUD_DATA.get('openstack_installed'), + 'group_id': FAKE_SUBCLOUD_DATA.get('group_id', 1), + 'region_name': FAKE_SUBCLOUD_DATA.get('region_name', "RegionOne") + } + return db_api.subcloud_create(context, **creation_fields) + + def _update_subcloud_peer_group_id(self, ctx, subcloud, pg_id): + return db_api.subcloud_update(ctx, subcloud.id, peer_group_id=pg_id) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_list_subclouds_populated(self, mock_client): + context = utils.dummy_context() + + # Create subcloud peer group + group_name = 'TestGroup' + system_id = '0907033e-b7ec-4832-92ad-4b0913580b3b' + pg = self._create_db_object( + context, peer_group_name=group_name, system_leader_id=system_id) + + # Create subcloud set peer-group-id as above subcloud-peer-group + subcloud = self._create_subcloud_db_object(context) + self._update_subcloud_peer_group_id(context, subcloud, pg.id) + + # API GET on: subcloud-peer-groups//subclouds + url = '%s/%s/subclouds' % (API_PREFIX, pg.id) + response = self.app.get(url, + headers=self.get_api_headers()) + # This API returns 'subclouds' rather than 'subcloud-groups' + self.assertIn('subclouds', response.json) + # the subcloud created earlier will have been queried + result_list = response.json.get('subclouds') + self.assertEqual(1, len(result_list)) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_get_status(self, mock_client): + context = utils.dummy_context() + + # Create subcloud peer group + group_name = 'TestGroup' + system_id = '0907033e-b7ec-4832-92ad-4b0913580b3b' + pg = self._create_db_object( + context, peer_group_name=group_name, system_leader_id=system_id) + + # Create subcloud set peer-group-id as above subcloud-peer-group + subcloud = self._create_subcloud_db_object(context) + self._update_subcloud_peer_group_id(context, subcloud, pg.id) + + # API GET on: subcloud-peer-groups//status + url = '%s/%s/status' % (API_PREFIX, pg.id) + response = self.app.get(url, + headers=self.get_api_headers()) + + self.assertIn('total_subclouds', response.json) + self.assertIn('peer_group_id', response.json) + + +class TestSubcloudPeerGroupUpdate(testroot.DCManagerApiTest, + SubcloudPeerGroupAPIMixin): + def setUp(self): + super(TestSubcloudPeerGroupUpdate, self).setUp() + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_update_invalid_system_leader_id(self, mock_client): + context = utils.dummy_context() + single_obj = self._create_db_object(context) + update_data = { + 'system_leader_id': 'not-valid-uuid' + } + response = self.app.patch_json(self.get_single_url(single_obj.id), + headers=self.get_api_headers(), + params=update_data, + expect_errors=True) + # Failures will return text rather than json + self.assertEqual(response.content_type, 'text/plain') + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_update_invalid_max_subcloud_rehoming(self, mock_client): + context = utils.dummy_context() + single_obj = self._create_db_object(context) + update_data = { + 'max_subcloud_rehoming': -1 + } + response = self.app.patch_json(self.get_single_url(single_obj.id), + headers=self.get_api_headers(), + params=update_data, + expect_errors=True) + # Failures will return text rather than json + self.assertEqual(response.content_type, 'text/plain') + self.assertEqual(response.status_code, http_client.BAD_REQUEST) + + +class TestSubcloudPeerGroupDelete(testroot.DCManagerApiTest, + SubcloudPeerGroupAPIMixin): + def setUp(self): + super(TestSubcloudPeerGroupDelete, self).setUp() + + @mock.patch.object(rpc_client, 'ManagerClient') + def test_delete_success(self, mock_client): + context = utils.dummy_context() + single_obj = self._create_db_object(context) + response = self.app.delete(self.get_single_url(single_obj.id), + headers=self.get_api_headers()) + # Failures will return text rather than json + self.assertEqual(response.content_type, 'application/json') + self.assertEqual(response.status_code, http_client.OK) diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py index 4095d360d..4bac9c6d7 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py @@ -1236,6 +1236,7 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): group_id=None, data_install=json.dumps(install_data), force=None, + peer_group_id=None, bootstrap_values=None, bootstrap_address=None) self.assertEqual(response.status_int, 200) @@ -1309,6 +1310,7 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): group_id=None, data_install=json.dumps(install_data), force=None, + peer_group_id=None, bootstrap_values=None, bootstrap_address=None) self.assertEqual(response.status_int, 200) @@ -1348,6 +1350,7 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): group_id=None, data_install=json.dumps(install_data), force=None, + peer_group_id=None, bootstrap_values=None, bootstrap_address=None) self.assertEqual(response.status_int, 200) @@ -1413,6 +1416,7 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest): group_id=None, data_install=None, force=True, + peer_group_id=None, bootstrap_values=None, bootstrap_address=None) self.assertEqual(response.status_int, 200) diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_service.py b/distributedcloud/dcmanager/tests/unit/manager/test_service.py index e122e8a62..7ec7ef570 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_service.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_service.py @@ -100,7 +100,7 @@ class TestDCManagerService(base.DCManagerTestCase): self.context, subcloud_id=1, management_state='testmgmtstatus') mock_subcloud_manager().update_subcloud.assert_called_once_with( - self.context, 1, 'testmgmtstatus', None, None, None, None, None, None, None, None) + self.context, 1, 'testmgmtstatus', None, None, None, None, None, None, None, None, None) @mock.patch.object(service, 'SubcloudManager') @mock.patch.object(service, 'rpc_messaging') diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index e0aedc52a..f20e0bc6a 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -431,6 +431,19 @@ class TestSubcloudManager(base.DCManagerTestCase): values.update(kwargs) return db_api.subcloud_create(ctxt, **values) + @staticmethod + def create_subcloud_peer_group_static(ctxt, **kwargs): + values = { + "peer_group_name": "pgname", + "system_leader_id": "12e0cb13-2c5c-480e-b0ea-9161fc03f3ef", + "system_leader_name": "DC0", + "group_priority": 0, + "group_state": "enabled", + "max_subcloud_rehoming": 50 + } + values.update(kwargs) + return db_api.subcloud_peer_group_create(ctxt, **values) + def test_init(self): sm = subcloud_manager.SubcloudManager() self.assertIsNotNone(sm) @@ -2935,3 +2948,57 @@ class TestSubcloudManager(base.DCManagerTestCase): mock_remove.assert_has_calls(calls, any_order=True) mock_rmtree.assert_called_with(install_dir) + + def test_update_subcloud_peer_group_id(self): + + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_DONE) + fake_peer_group_id = 123 + + fake_dcmanager_cermon_api = FakeDCManagerNotifications() + + p = mock.patch('dcmanager.rpc.client.DCManagerNotifications') + mock_dcmanager_api = p.start() + mock_dcmanager_api.return_value = fake_dcmanager_cermon_api + + sm = subcloud_manager.SubcloudManager() + sm.update_subcloud(self.ctx, + subcloud.id, + peer_group_id=fake_peer_group_id) + + # Verify subcloud was updated with correct values + updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) + self.assertEqual(fake_peer_group_id, + updated_subcloud.peer_group_id) + + def test_update_subcloud_peer_group_id_to_none(self): + + subcloud = self.create_subcloud_static( + self.ctx, + name='subcloud1', + deploy_status=consts.DEPLOY_STATE_DONE) + fake_peer_group_id = 123 + + fake_dcmanager_cermon_api = FakeDCManagerNotifications() + + p = mock.patch('dcmanager.rpc.client.DCManagerNotifications') + mock_dcmanager_api = p.start() + mock_dcmanager_api.return_value = fake_dcmanager_cermon_api + + sm = subcloud_manager.SubcloudManager() + sm.update_subcloud(self.ctx, + subcloud.id, + peer_group_id=fake_peer_group_id) + # Verify subcloud was updated with correct values + updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) + self.assertEqual(fake_peer_group_id, + updated_subcloud.peer_group_id) + sm.update_subcloud(self.ctx, + subcloud.id, + peer_group_id='NoNe') + # Verify subcloud was updated to None + updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name) + self.assertEqual(None, + updated_subcloud.peer_group_id)