Add Subcloud Peer group management

Group of the current managed subclouds which are supposed
to be duplicated in a peer site as secondary subclouds.

This commit add subcloud-peer-group APIs of
create/delete/update/show/list/
list-subclouds of a subcloud-peer-group

Update setting peer-group for subcloud, Using DB of subclouds'
'peer_group_id' Column.

Update subcloud update API, add peer_group parameter
Usage:
Add a subcloud to peer-group:
dcmanager subcloud update SUBCLOUD --peer-group PEER_GROUP
Remove a subcloud from peer-group:
dcmanager subcloud update SUBCLOUD --peer-group none

Test Plan:
1. PASS - Create a subcloud-peer-group
2. PASS - Update an existing subcloud's peer-group to a existing
              subcloud-peer-group successfully;
3. PASS - Verify subcloud-peer-group list-subclouds can get the expected
              Subcloud above successfully;
4. PASS - Update group_priority/group_state/max_subcloud_rehoming/
               system_leader_id/system_leader_name
              of a subcloud-peer-group successfully;
5. PASS - Check can get subcloud status of a subcloud-peer-group
               successfully;
6. PASS - Delete a subcloud-peer-group completes successfully.
7. PASS - Delete a subcloud-peer-group while it still has subclouds
               associated to it. the subclouds' peer-group-id is auto
               set to None successfully;
8. PASS - Add a subcloud, update the peer-group-id as a non-existing
              subcloud-peer-group, get error message successfully;
9. PASS - Update subcloud peer group with invalid
               group_priority/group_state/max_subcloud_rehoming/
               system_leader_id/system_leader_name

Story: 2010852
Task: 48485
Change-Id: I93d0808b8cf02eba0e6f687007df42e2d2ea1848
Signed-off-by: Wang Tao <tao.wang@windriver.com>
This commit is contained in:
twang4 2023-08-28 11:54:23 +08:00
parent a0dfc1adc3
commit 8196e7f946
28 changed files with 1714 additions and 5 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}]
}

View File

@ -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\"}}"
}]
}

View File

@ -0,0 +1,3 @@
{
"sysadmin_password": "XXXXXXX"
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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

View File

@ -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'))

View File

@ -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

View File

@ -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()
)

View File

@ -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

View File

@ -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.")

View File

@ -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]}

View File

@ -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."""

View File

@ -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). \

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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))

View File

@ -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/<uuid>/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/<uuid>/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/<uuid>/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)

View File

@ -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)

View File

@ -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')

View File

@ -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)