Add dcmanager peer group association management API support
Add dcmanager peer-group-association management API. This commit adds peer-group-association APIs of create/delete/update/show/list/sync, and adds system-peer API of list subcloud-peer-groups. Test Plan: 1. PASS - Verify that cloud manage peer-group-association through api successfully. 2. PASS - Verify that cloud get associated subcloud-peer-group list with system-peer api successfully. 3. PASS - Check create without providing the must required parameters. 4. PASS - Check create with wrong peer_group_priority. 5. PASS - Check delete with a not existing association id. 6. PASS - Check system_peer_manager associate peer group, sync peer group and delete association. 7. PASS - Create an association with 50 subclouds need to sync. Check the sync status on peer site after it is synced. Story: 2010852 Task: 48506 Change-Id: I41c16a8ab13e60f5b1de5b05fbbc51821f7f8d57 Signed-off-by: Zhang Rong(Jon) <rong.zhang@windriver.com>
This commit is contained in:
parent
bac49b1840
commit
477c9e6cb8
|
@ -2457,6 +2457,52 @@ Response Example
|
||||||
:language: json
|
:language: json
|
||||||
|
|
||||||
|
|
||||||
|
*****************************************************************
|
||||||
|
Shows subcloud peer groups that are associated with a system peer
|
||||||
|
*****************************************************************
|
||||||
|
|
||||||
|
.. rest_method:: GET /v1.0/system-peers/{system-peer}/subcloud-peer-groups
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
- system-peer: system_peer_uri
|
||||||
|
|
||||||
|
This operation does not accept a request body.
|
||||||
|
|
||||||
|
**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/system-peers/system-peers-get-peer-groups-response.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
|
||||||
*******************************
|
*******************************
|
||||||
Modifies a specific system peer
|
Modifies a specific system peer
|
||||||
*******************************
|
*******************************
|
||||||
|
@ -2895,4 +2941,263 @@ internalServerError (500), serviceUnavailable (503)
|
||||||
|
|
||||||
- subcloud-peer-group: subcloud_peer_group_uri
|
- subcloud-peer-group: subcloud_peer_group_uri
|
||||||
|
|
||||||
This operation does not accept a request body.
|
This operation does not accept a request body.
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
Peer Group Association
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Peer Group Associations are logical connections managed by a central System Controller.
|
||||||
|
It's a linking of the subcloud peer group and the system peer to
|
||||||
|
establish associations with local subcloud peer groups and peer sites.
|
||||||
|
|
||||||
|
*********************************
|
||||||
|
Lists all peer group associations
|
||||||
|
*********************************
|
||||||
|
|
||||||
|
.. rest_method:: GET /v1.0/peer-group-associations
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- peer-group-associations: peer_group_associations
|
||||||
|
- id: peer_group_association_id
|
||||||
|
- peer-group-id: association_peer_group_id
|
||||||
|
- system-peer-id: system_peer_id
|
||||||
|
- peer-group-priority: association_peer_group_priority
|
||||||
|
- sync-status: association_sync_status
|
||||||
|
- created-at: created_at
|
||||||
|
- updated-at: updated_at
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/peer-group-associations/associations-get-response.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
|
||||||
|
********************************
|
||||||
|
Creates a peer group association
|
||||||
|
********************************
|
||||||
|
|
||||||
|
.. rest_method:: POST /v1.0/peer-group-associations
|
||||||
|
|
||||||
|
**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_id: association_peer_group_id
|
||||||
|
- system_peer_id: system_peer_id
|
||||||
|
- peer_group_priority: association_peer_group_priority
|
||||||
|
|
||||||
|
Request Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/peer-group-associations/associations-post-request.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
**Response parameters**
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- id: peer_group_association_id
|
||||||
|
- peer-group-id: association_peer_group_id
|
||||||
|
- system-peer-id: system_peer_id
|
||||||
|
- peer-group-priority: association_peer_group_priority
|
||||||
|
- sync-status: association_sync_status
|
||||||
|
- sync-message: association_sync_message
|
||||||
|
- created-at: created_at
|
||||||
|
- updated-at: updated_at
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/peer-group-associations/associations-post-response.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
|
||||||
|
*********************************************************
|
||||||
|
Shows information about a specific peer group association
|
||||||
|
*********************************************************
|
||||||
|
|
||||||
|
.. rest_method:: GET /v1.0/peer-group-associations/{associate_id}
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
- associate_id: peer_group_association_uri
|
||||||
|
|
||||||
|
This operation does not accept a request body.
|
||||||
|
|
||||||
|
**Response parameters**
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- id: peer_group_association_id
|
||||||
|
- peer-group-id: association_peer_group_id
|
||||||
|
- system-peer-id: system_peer_id
|
||||||
|
- peer-group-priority: association_peer_group_priority
|
||||||
|
- sync-status: association_sync_status
|
||||||
|
- sync-message: association_sync_message
|
||||||
|
- created-at: created_at
|
||||||
|
- updated-at: updated_at
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/peer-group-associations/association-get-response.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
|
||||||
|
**********************************************
|
||||||
|
Synchronizes a specific peer group association
|
||||||
|
**********************************************
|
||||||
|
|
||||||
|
.. rest_method:: PATCH /v1.0/peer-group-associations/{associate_id}/sync
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
- associate_id: peer_group_association_uri
|
||||||
|
|
||||||
|
**Response parameters**
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- id: peer_group_association_id
|
||||||
|
- peer-group-id: association_peer_group_id
|
||||||
|
- system-peer-id: system_peer_id
|
||||||
|
- peer-group-priority: association_peer_group_priority
|
||||||
|
- sync-status: association_sync_status
|
||||||
|
- sync-message: association_sync_message
|
||||||
|
- created-at: created_at
|
||||||
|
- updated-at: updated_at
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/peer-group-associations/association-patch-response.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
|
||||||
|
******************************************
|
||||||
|
Modifies a specific peer group association
|
||||||
|
******************************************
|
||||||
|
|
||||||
|
.. rest_method:: PATCH /v1.0/peer-group-associations/{associate_id}
|
||||||
|
|
||||||
|
The attributes of a subcloud peer group which are modifiable:
|
||||||
|
|
||||||
|
- peer_group_priority
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
- associate_id: peer_group_association_uri
|
||||||
|
- peer_group_priority: association_peer_group_priority
|
||||||
|
|
||||||
|
Request Example
|
||||||
|
----------------
|
||||||
|
.. literalinclude:: samples/peer-group-associations/association-patch-request.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
**Response parameters**
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- id: peer_group_association_id
|
||||||
|
- peer-group-id: association_peer_group_id
|
||||||
|
- system-peer-id: system_peer_id
|
||||||
|
- peer-group-priority: association_peer_group_priority
|
||||||
|
- sync-status: association_sync_status
|
||||||
|
- sync-message: association_sync_message
|
||||||
|
- created-at: created_at
|
||||||
|
- updated-at: updated_at
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/peer-group-associations/association-patch-response.json
|
||||||
|
:language: json
|
||||||
|
|
||||||
|
|
||||||
|
*****************************************
|
||||||
|
Deletes a specific peer group association
|
||||||
|
*****************************************
|
||||||
|
|
||||||
|
.. rest_method:: DELETE /v1.0/peer-group-associations/{associate_id}
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
- associate_id: peer_group_association_uri
|
||||||
|
|
||||||
|
This operation does not accept a request body.
|
||||||
|
|
|
@ -6,6 +6,12 @@ backup_delete_release:
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
peer_group_association_uri:
|
||||||
|
description: |
|
||||||
|
The peer group association reference id.
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
release_uri:
|
release_uri:
|
||||||
description: |
|
description: |
|
||||||
The subcloud software version.
|
The subcloud software version.
|
||||||
|
@ -77,6 +83,31 @@ alarm_summary_uuid:
|
||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
association_peer_group_id:
|
||||||
|
description: |
|
||||||
|
The ID of the subcloud peer group as an integer.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
association_peer_group_priority:
|
||||||
|
description: |
|
||||||
|
The priority of the subcloud peer group in peer group association.
|
||||||
|
The lower the value, the higher the priority.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: integer
|
||||||
|
association_sync_message:
|
||||||
|
description: |
|
||||||
|
The sync message for association.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
association_sync_status:
|
||||||
|
description: |
|
||||||
|
The sync status for association.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
availability_status:
|
availability_status:
|
||||||
description: |
|
description: |
|
||||||
The availability status of the subcloud.
|
The availability status of the subcloud.
|
||||||
|
@ -397,6 +428,18 @@ peer_controller_gateway_address:
|
||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
peer_group_association_id:
|
||||||
|
description: |
|
||||||
|
The ID of a peer group association as an integer.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
peer_group_associations:
|
||||||
|
description: |
|
||||||
|
The list of ``peer-group-association`` objects.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: array
|
||||||
peer_name:
|
peer_name:
|
||||||
description: |
|
description: |
|
||||||
The name of a peer as a string.
|
The name of a peer as a string.
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"peer-group-id": 1,
|
||||||
|
"system-peer-id": 1,
|
||||||
|
"peer-group-priority": 1,
|
||||||
|
"sync-status": "synced",
|
||||||
|
"sync-message": null,
|
||||||
|
"created-at": "2023-08-21 09:24:07.394961",
|
||||||
|
"updated-at": null
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"peer_group_priority": 99
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"peer-group-id": 1,
|
||||||
|
"system-peer-id": 1,
|
||||||
|
"peer-group-priority": 99,
|
||||||
|
"sync-status": "synced",
|
||||||
|
"sync-message": null,
|
||||||
|
"created-at": "2023-08-21 09:24:07.394961",
|
||||||
|
"updated-at": "2023-08-21 10:24:07.394961"
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"peer_group_associations": [
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"peer-group-id": 1,
|
||||||
|
"system-peer-id": 1,
|
||||||
|
"peer-group-priority": 1,
|
||||||
|
"sync-status": "synced",
|
||||||
|
"created-at": "2023-08-21 09:24:07.394961",
|
||||||
|
"updated-at": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"peer_group_id": 1,
|
||||||
|
"system_peer_id": 1,
|
||||||
|
"peer_group_priority": 1
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"peer-group-id": 1,
|
||||||
|
"system-peer-id": 1,
|
||||||
|
"peer-group-priority": 1,
|
||||||
|
"sync-status": "syncing",
|
||||||
|
"sync-message": null,
|
||||||
|
"created-at": "2023-08-21 09:24:07.394961",
|
||||||
|
"updated-at": null
|
||||||
|
}
|
|
@ -3,8 +3,8 @@
|
||||||
"peer_name": "PeerDistributedCloud1",
|
"peer_name": "PeerDistributedCloud1",
|
||||||
"manager_endpoint": "http://128.128.128.1:5000/v3",
|
"manager_endpoint": "http://128.128.128.1:5000/v3",
|
||||||
"manager_username": "admin",
|
"manager_username": "admin",
|
||||||
"manager-password": "V2luZDEyMyQ=",
|
"manager_password": "V2luZDEyMyQ=",
|
||||||
"peer_controller_gateway-address": "192.168.204.1",
|
"peer_controller_gateway_address": "192.168.204.1",
|
||||||
"administrative_state": "enabled",
|
"administrative_state": "enabled",
|
||||||
"heartbeat_interval": 60,
|
"heartbeat_interval": 60,
|
||||||
"heartbeat_failure_threshold": 3,
|
"heartbeat_failure_threshold": 3,
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -3,8 +3,8 @@
|
||||||
"peer_name": "PeerDistributedCloud1",
|
"peer_name": "PeerDistributedCloud1",
|
||||||
"manager_endpoint": "http://128.128.128.1:5000/v3",
|
"manager_endpoint": "http://128.128.128.1:5000/v3",
|
||||||
"manager_username": "admin",
|
"manager_username": "admin",
|
||||||
"manager-password": "V2luZDEyMyQ=",
|
"manager_password": "V2luZDEyMyQ=",
|
||||||
"peer_controller_gateway-address": "192.168.204.1",
|
"peer_controller_gateway_address": "192.168.204.1",
|
||||||
"administrative_state": "enabled",
|
"administrative_state": "enabled",
|
||||||
"heartbeat_interval": 60,
|
"heartbeat_interval": 60,
|
||||||
"heartbeat_failure_threshold": 3,
|
"heartbeat_failure_threshold": 3,
|
||||||
|
|
|
@ -33,13 +33,15 @@ class DcmanagerClient(base.DriverBase):
|
||||||
self.token = session.get_token()
|
self.token = session.get_token()
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
def get_subcloud(self, subcloud_ref):
|
def get_subcloud(self, subcloud_ref, is_region_name=False):
|
||||||
"""Get subcloud."""
|
"""Get subcloud."""
|
||||||
if subcloud_ref is None:
|
if subcloud_ref is None:
|
||||||
raise ValueError("subcloud_ref is required.")
|
raise ValueError("subcloud_ref is required.")
|
||||||
url = f"{self.endpoint}/subclouds/{subcloud_ref}"
|
url = f"{self.endpoint}/subclouds/{subcloud_ref}"
|
||||||
|
|
||||||
headers = {"X-Auth-Token": self.token}
|
headers = {"X-Auth-Token": self.token}
|
||||||
|
if is_region_name:
|
||||||
|
headers["User-Agent"] = "dcmanager/1.0"
|
||||||
response = requests.get(url, headers=headers, timeout=self.timeout)
|
response = requests.get(url, headers=headers, timeout=self.timeout)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
|
@ -267,6 +269,11 @@ class DcmanagerClient(base.DriverBase):
|
||||||
'Subcloud Peer Group not found' in response.text:
|
'Subcloud Peer Group not found' in response.text:
|
||||||
raise exceptions.SubcloudPeerGroupNotFound(
|
raise exceptions.SubcloudPeerGroupNotFound(
|
||||||
peer_group_ref=peer_group_ref)
|
peer_group_ref=peer_group_ref)
|
||||||
|
elif response.status_code == 400 and \
|
||||||
|
'a peer group which is associated with a system peer' in \
|
||||||
|
response.text:
|
||||||
|
raise exceptions.SubcloudPeerGroupDeleteFailedAssociated(
|
||||||
|
peer_group_ref=peer_group_ref)
|
||||||
message = "Delete Subcloud Peer Group: peer_group_ref %s " \
|
message = "Delete Subcloud Peer Group: peer_group_ref %s " \
|
||||||
"failed with RC: %d" % (peer_group_ref, response.status_code)
|
"failed with RC: %d" % (peer_group_ref, response.status_code)
|
||||||
LOG.error(message)
|
LOG.error(message)
|
||||||
|
|
|
@ -135,3 +135,8 @@ class SubcloudNotFound(NotFound):
|
||||||
|
|
||||||
class SubcloudPeerGroupNotFound(NotFound):
|
class SubcloudPeerGroupNotFound(NotFound):
|
||||||
message = _("Subcloud Peer Group %(peer_group_ref)s not found")
|
message = _("Subcloud Peer Group %(peer_group_ref)s not found")
|
||||||
|
|
||||||
|
|
||||||
|
class SubcloudPeerGroupDeleteFailedAssociated(DCCommonException):
|
||||||
|
message = _("Subcloud Peer Group %(peer_group_ref)s delete failed "
|
||||||
|
"cause it is associated with a system peer.")
|
||||||
|
|
|
@ -0,0 +1,360 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import http.client as httpclient
|
||||||
|
import json
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_messaging import RemoteError
|
||||||
|
import pecan
|
||||||
|
from pecan import expose
|
||||||
|
from pecan import request
|
||||||
|
|
||||||
|
from dccommon import consts as dccommon_consts
|
||||||
|
from dccommon.drivers.openstack.sysinv_v1 import SysinvClient
|
||||||
|
from dcmanager.api.controllers import restcomm
|
||||||
|
from dcmanager.api.policies import peer_group_association as \
|
||||||
|
peer_group_association_policy
|
||||||
|
from dcmanager.api import policy
|
||||||
|
from dcmanager.common import consts
|
||||||
|
from dcmanager.common import exceptions as exception
|
||||||
|
from dcmanager.common.i18n import _
|
||||||
|
from dcmanager.common import phased_subcloud_deploy as psd_common
|
||||||
|
from dcmanager.db import api as db_api
|
||||||
|
from dcmanager.rpc import client as rpc_client
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MIN_PEER_GROUP_ASSOCIATION_PRIORITY = 1
|
||||||
|
MAX_PEER_GROUP_ASSOCIATION_PRIORITY = 65536
|
||||||
|
|
||||||
|
|
||||||
|
class PeerGroupAssociationsController(restcomm.GenericPathController):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(PeerGroupAssociationsController, 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_peer_group_association_list(self, context):
|
||||||
|
associations = db_api.peer_group_association_get_all(context)
|
||||||
|
association_list = []
|
||||||
|
|
||||||
|
for association in associations:
|
||||||
|
association_dict = db_api.peer_group_association_db_model_to_dict(
|
||||||
|
association)
|
||||||
|
# Remove the sync_message from the list response
|
||||||
|
association_dict.pop('sync-message', None)
|
||||||
|
association_list.append(association_dict)
|
||||||
|
|
||||||
|
result = {'peer_group_associations': association_list}
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_payload(request):
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body)
|
||||||
|
except Exception:
|
||||||
|
error_msg = 'Request body is malformed.'
|
||||||
|
LOG.exception(error_msg)
|
||||||
|
pecan.abort(400, _(error_msg))
|
||||||
|
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
pecan.abort(400, _('Invalid request body format'))
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _validate_peer_group_leader_id(self, system_leader_id):
|
||||||
|
ks_client = psd_common.get_ks_client()
|
||||||
|
sysinv_client = SysinvClient(
|
||||||
|
dccommon_consts.DEFAULT_REGION_NAME,
|
||||||
|
ks_client.session,
|
||||||
|
endpoint=ks_client.endpoint_cache.get_endpoint('sysinv'))
|
||||||
|
system = sysinv_client.get_system()
|
||||||
|
return True if system.uuid == system_leader_id else False
|
||||||
|
|
||||||
|
@index.when(method='GET', template='json')
|
||||||
|
def get(self, association_id=None):
|
||||||
|
"""Get details about peer group association.
|
||||||
|
|
||||||
|
:param association_id: ID of peer group association
|
||||||
|
"""
|
||||||
|
policy.authorize(peer_group_association_policy.POLICY_ROOT % "get", {},
|
||||||
|
restcomm.extract_credentials_for_policy())
|
||||||
|
context = restcomm.extract_context_from_environ()
|
||||||
|
|
||||||
|
if association_id is None:
|
||||||
|
# List of peer group association requested
|
||||||
|
return self._get_peer_group_association_list(context)
|
||||||
|
elif not association_id.isdigit():
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association ID must be an integer'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
association = db_api.peer_group_association_get(context,
|
||||||
|
association_id)
|
||||||
|
except exception.PeerGroupAssociationNotFound:
|
||||||
|
pecan.abort(httpclient.NOT_FOUND,
|
||||||
|
_('Peer Group Association not found'))
|
||||||
|
|
||||||
|
return db_api.peer_group_association_db_model_to_dict(association)
|
||||||
|
|
||||||
|
def _validate_peer_group_id(self, context, peer_group_id):
|
||||||
|
try:
|
||||||
|
db_api.subcloud_peer_group_get(context, peer_group_id)
|
||||||
|
except exception.SubcloudPeerGroupNotFound:
|
||||||
|
LOG.debug("Subcloud Peer Group Not Found, peer group id: %s"
|
||||||
|
% peer_group_id)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
LOG.warning("Get Subcloud Peer Group failed: %s; peer_group_id: %s"
|
||||||
|
% (e, peer_group_id))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_system_peer_id(self, context, system_peer_id):
|
||||||
|
try:
|
||||||
|
db_api.system_peer_get(context, system_peer_id)
|
||||||
|
except exception.SystemPeerNotFound:
|
||||||
|
LOG.debug("System Peer Not Found, system peer id: %s"
|
||||||
|
% system_peer_id)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
LOG.warning("Get System Peer failed: %s; system_peer_id: %s"
|
||||||
|
% (e, system_peer_id))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _validate_peer_group_priority(self, peer_group_priority):
|
||||||
|
try:
|
||||||
|
# Check the value is an integer
|
||||||
|
val = int(peer_group_priority)
|
||||||
|
except ValueError:
|
||||||
|
LOG.debug("Peer Group Priority is not Integer: %s"
|
||||||
|
% peer_group_priority)
|
||||||
|
return False
|
||||||
|
# Less than min or greater than max priority is not supported.
|
||||||
|
if val < MIN_PEER_GROUP_ASSOCIATION_PRIORITY or \
|
||||||
|
val > MAX_PEER_GROUP_ASSOCIATION_PRIORITY:
|
||||||
|
LOG.debug("Invalid Peer Group Priority out of support range: %s"
|
||||||
|
% peer_group_priority)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@index.when(method='POST', template='json')
|
||||||
|
def post(self):
|
||||||
|
"""Create a new peer group association."""
|
||||||
|
policy.authorize(peer_group_association_policy.POLICY_ROOT %
|
||||||
|
"create", {},
|
||||||
|
restcomm.extract_credentials_for_policy())
|
||||||
|
context = restcomm.extract_context_from_environ()
|
||||||
|
|
||||||
|
payload = self._get_payload(request)
|
||||||
|
if not payload:
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST, _('Body required'))
|
||||||
|
|
||||||
|
# Validate payload
|
||||||
|
peer_group_id = payload.get('peer_group_id')
|
||||||
|
if not self._validate_peer_group_id(context, peer_group_id):
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST, _('Invalid peer_group_id'))
|
||||||
|
|
||||||
|
system_peer_id = payload.get('system_peer_id')
|
||||||
|
if not self._validate_system_peer_id(context, system_peer_id):
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST, _('Invalid system_peer_id'))
|
||||||
|
|
||||||
|
peer_group_priority = payload.get('peer_group_priority')
|
||||||
|
peer_group = db_api.subcloud_peer_group_get(context, peer_group_id)
|
||||||
|
|
||||||
|
if (peer_group.group_priority == 0 and not peer_group_priority) or \
|
||||||
|
(peer_group.group_priority > 0 and peer_group_priority):
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association create with peer_group_'
|
||||||
|
'priority is required when the subcloud peer group '
|
||||||
|
'priority is 0, and it is not allowed when the '
|
||||||
|
'subcloud peer group priority is greater than 0.'))
|
||||||
|
|
||||||
|
if peer_group_priority and not self._validate_peer_group_priority(
|
||||||
|
peer_group_priority):
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Invalid peer_group_priority'))
|
||||||
|
|
||||||
|
sync_enabled = peer_group.group_priority == 0
|
||||||
|
|
||||||
|
# only one combination of peer_group_id + system_peer_id can exists
|
||||||
|
association = None
|
||||||
|
try:
|
||||||
|
association = db_api.\
|
||||||
|
peer_group_association_get_by_peer_group_and_system_peer_id(
|
||||||
|
context,
|
||||||
|
peer_group_id,
|
||||||
|
system_peer_id)
|
||||||
|
except exception.PeerGroupAssociationCombinationNotFound:
|
||||||
|
LOG.warning("Peer Group Association Combination Not Found, "
|
||||||
|
"peer_group_id: %s, system_peer_id: %s"
|
||||||
|
% (peer_group_id, system_peer_id))
|
||||||
|
except Exception as e:
|
||||||
|
LOG.warning("Peer Group Association get failed: %s;"
|
||||||
|
"peer_group_id: %s, system_peer_id: %s"
|
||||||
|
% (e, peer_group_id, system_peer_id))
|
||||||
|
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
|
||||||
|
_('peer_group_association_get_by_peer_group_and_'
|
||||||
|
'system_peer_id failed: %s' % e))
|
||||||
|
if association:
|
||||||
|
LOG.info("Failed to create Peer group association, association \
|
||||||
|
with peer_group_id:[%s],system_peer_id:[%s] \
|
||||||
|
already exists" % (peer_group_id, system_peer_id))
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('A Peer group association with same peer_group_id, '
|
||||||
|
'system_peer_id already exists'))
|
||||||
|
|
||||||
|
# Create the peer group association
|
||||||
|
try:
|
||||||
|
sync_status = consts.ASSOCIATION_SYNC_STATUS_SYNCING if \
|
||||||
|
sync_enabled else consts.ASSOCIATION_SYNC_STATUS_DISABLED
|
||||||
|
association = db_api.peer_group_association_create(
|
||||||
|
context, peer_group_id, system_peer_id, peer_group_priority,
|
||||||
|
sync_status)
|
||||||
|
|
||||||
|
if sync_enabled:
|
||||||
|
# Sync the subcloud peer group to peer site
|
||||||
|
self.rpc_client.sync_subcloud_peer_group(context, association.id)
|
||||||
|
return db_api.peer_group_association_db_model_to_dict(association)
|
||||||
|
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 peer group association'))
|
||||||
|
|
||||||
|
@index.when(method='PATCH', template='json')
|
||||||
|
def patch(self, association_id, sync=False):
|
||||||
|
"""Update a peer group association.
|
||||||
|
|
||||||
|
:param association_id: ID of peer group association to update
|
||||||
|
:param sync: sync action that sync the peer group
|
||||||
|
"""
|
||||||
|
|
||||||
|
policy.authorize(peer_group_association_policy.POLICY_ROOT % "modify",
|
||||||
|
{}, restcomm.extract_credentials_for_policy())
|
||||||
|
context = restcomm.extract_context_from_environ()
|
||||||
|
if association_id is None:
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association ID required'))
|
||||||
|
elif not association_id.isdigit():
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association ID must be an integer'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
association = db_api.peer_group_association_get(context,
|
||||||
|
association_id)
|
||||||
|
except exception.PeerGroupAssociationNotFound:
|
||||||
|
pecan.abort(httpclient.NOT_FOUND,
|
||||||
|
_('Peer Group Association not found'))
|
||||||
|
|
||||||
|
sync_disabled = association.sync_status == consts.\
|
||||||
|
ASSOCIATION_SYNC_STATUS_DISABLED
|
||||||
|
if sync_disabled:
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association sync or update is not allowed'
|
||||||
|
' when the sync_status is disabled.'))
|
||||||
|
|
||||||
|
if sync:
|
||||||
|
peer_group = db_api.subcloud_peer_group_get(
|
||||||
|
context, association.peer_group_id)
|
||||||
|
if not self._validate_peer_group_leader_id(peer_group.
|
||||||
|
system_leader_id):
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association sync is not allowed when '
|
||||||
|
'the subcloud peer group system_leader_id is not '
|
||||||
|
'the current system controller UUID.'))
|
||||||
|
try:
|
||||||
|
# Sync the subcloud peer group to peer site
|
||||||
|
self.rpc_client.sync_subcloud_peer_group(context,
|
||||||
|
association.id)
|
||||||
|
association = db_api.peer_group_association_update(
|
||||||
|
context, id=association_id,
|
||||||
|
sync_status=consts.ASSOCIATION_SYNC_STATUS_SYNCING,
|
||||||
|
sync_message='None')
|
||||||
|
return db_api.peer_group_association_db_model_to_dict(
|
||||||
|
association)
|
||||||
|
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 sync peer group association'))
|
||||||
|
|
||||||
|
payload = self._get_payload(request)
|
||||||
|
if not payload:
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST, _('Body required'))
|
||||||
|
|
||||||
|
peer_group_priority = payload.get('peer_group_priority')
|
||||||
|
# Check value is not None or empty before calling validate
|
||||||
|
if not peer_group_priority:
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST, _('nothing to update'))
|
||||||
|
if not self._validate_peer_group_priority(peer_group_priority):
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Invalid peer_group_priority'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ask dcmanager-manager to update the subcloud peer group priority
|
||||||
|
# to peer site. It will do the real work...
|
||||||
|
return self.rpc_client.update_subcloud_peer_group(
|
||||||
|
context, association.id, peer_group_priority)
|
||||||
|
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 peer group association'))
|
||||||
|
|
||||||
|
@index.when(method='delete', template='json')
|
||||||
|
def delete(self, association_id):
|
||||||
|
"""Delete the peer group association.
|
||||||
|
|
||||||
|
:param association_id: ID of peer group association to delete
|
||||||
|
"""
|
||||||
|
policy.authorize(peer_group_association_policy.POLICY_ROOT % "delete",
|
||||||
|
{}, restcomm.extract_credentials_for_policy())
|
||||||
|
context = restcomm.extract_context_from_environ()
|
||||||
|
|
||||||
|
if association_id is None:
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association ID required'))
|
||||||
|
# Validate the ID
|
||||||
|
if not association_id.isdigit():
|
||||||
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
|
_('Peer Group Association ID must be an integer'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
association = db_api.peer_group_association_get(context,
|
||||||
|
association_id)
|
||||||
|
sync_disabled = association.sync_status == consts.\
|
||||||
|
ASSOCIATION_SYNC_STATUS_DISABLED
|
||||||
|
if sync_disabled:
|
||||||
|
return db_api.peer_group_association_destroy(context,
|
||||||
|
association_id)
|
||||||
|
# Ask system-peer-manager to delete the association.
|
||||||
|
# It will do all the real work...
|
||||||
|
return self.rpc_client.delete_peer_group_association(
|
||||||
|
context, association_id)
|
||||||
|
except exception.PeerGroupAssociationNotFound:
|
||||||
|
pecan.abort(httpclient.NOT_FOUND,
|
||||||
|
_('Peer Group Association not found'))
|
||||||
|
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 peer group association'))
|
|
@ -18,6 +18,7 @@ import pecan
|
||||||
|
|
||||||
from dcmanager.api.controllers.v1 import alarm_manager
|
from dcmanager.api.controllers.v1 import alarm_manager
|
||||||
from dcmanager.api.controllers.v1 import notifications
|
from dcmanager.api.controllers.v1 import notifications
|
||||||
|
from dcmanager.api.controllers.v1 import peer_group_association
|
||||||
from dcmanager.api.controllers.v1 import phased_subcloud_deploy
|
from dcmanager.api.controllers.v1 import phased_subcloud_deploy
|
||||||
from dcmanager.api.controllers.v1 import subcloud_backup
|
from dcmanager.api.controllers.v1 import subcloud_backup
|
||||||
from dcmanager.api.controllers.v1 import subcloud_deploy
|
from dcmanager.api.controllers.v1 import subcloud_deploy
|
||||||
|
@ -58,6 +59,8 @@ class Controller(object):
|
||||||
PhasedSubcloudDeployController
|
PhasedSubcloudDeployController
|
||||||
sub_controllers["subcloud-peer-groups"] = \
|
sub_controllers["subcloud-peer-groups"] = \
|
||||||
subcloud_peer_group.SubcloudPeerGroupsController
|
subcloud_peer_group.SubcloudPeerGroupsController
|
||||||
|
sub_controllers["peer-group-associations"] = \
|
||||||
|
peer_group_association.PeerGroupAssociationsController
|
||||||
sub_controllers["system-peers"] = system_peers.\
|
sub_controllers["system-peers"] = system_peers.\
|
||||||
SystemPeersController
|
SystemPeersController
|
||||||
|
|
||||||
|
|
|
@ -485,16 +485,13 @@ class SubcloudPeerGroupsController(restcomm.GenericPathController):
|
||||||
pecan.abort(httpclient.NOT_FOUND, _('Subcloud Peer Group not found'))
|
pecan.abort(httpclient.NOT_FOUND, _('Subcloud Peer Group not found'))
|
||||||
|
|
||||||
LOG.info("Handling delete subcloud peer group request for: %s" % group)
|
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 cannot be deleted if it is used by any associations
|
||||||
'''
|
|
||||||
# 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,
|
association = db_api.peer_group_association_get_by_peer_group_id(context,
|
||||||
group.id)
|
group.id)
|
||||||
if len(association) > 0:
|
if len(association) > 0:
|
||||||
pecan.abort(httpclient.BAD_REQUEST,
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
_("Cannot delete a peer group "
|
_("Cannot delete a peer group "
|
||||||
"which is associated with a system peer."))
|
"which is associated with a system peer."))
|
||||||
'''
|
|
||||||
try:
|
try:
|
||||||
db_api.subcloud_peer_group_destroy(context, group.id)
|
db_api.subcloud_peer_group_destroy(context, group.id)
|
||||||
# Disassociate the subcloud.
|
# Disassociate the subcloud.
|
||||||
|
|
|
@ -514,7 +514,12 @@ class SubcloudsController(object):
|
||||||
if 'secondary' not in payload:
|
if 'secondary' not in payload:
|
||||||
psd_common.validate_sysadmin_password(payload)
|
psd_common.validate_sysadmin_password(payload)
|
||||||
|
|
||||||
psd_common.subcloud_region_create(payload, context)
|
# Use the region_name if it has been provided in the payload.
|
||||||
|
# The typical scenario is adding a secondary subcloud from
|
||||||
|
# peer site where subcloud region_name is known and can be
|
||||||
|
# put into the payload of the subcloud add request.
|
||||||
|
if 'region_name' not in payload:
|
||||||
|
psd_common.subcloud_region_create(payload, context)
|
||||||
|
|
||||||
psd_common.pre_deploy_create(payload, context, request)
|
psd_common.pre_deploy_create(payload, context, request)
|
||||||
|
|
||||||
|
@ -524,8 +529,14 @@ class SubcloudsController(object):
|
||||||
|
|
||||||
# Ask dcmanager-manager to add the subcloud.
|
# Ask dcmanager-manager to add the subcloud.
|
||||||
# It will do all the real work...
|
# It will do all the real work...
|
||||||
self.dcmanager_rpc_client.add_subcloud(
|
# If the subcloud is secondary, it will be synchronous operation.
|
||||||
context, subcloud.id, payload)
|
# A normal subcloud add will be a synchronous operation.
|
||||||
|
if 'secondary' in payload:
|
||||||
|
self.dcmanager_rpc_client.add_secondary_subcloud(
|
||||||
|
context, subcloud.id, payload)
|
||||||
|
else:
|
||||||
|
self.dcmanager_rpc_client.add_subcloud(
|
||||||
|
context, subcloud.id, payload)
|
||||||
|
|
||||||
return db_api.subcloud_db_model_to_dict(subcloud)
|
return db_api.subcloud_db_model_to_dict(subcloud)
|
||||||
except RemoteError as e:
|
except RemoteError as e:
|
||||||
|
|
|
@ -79,6 +79,10 @@ class SystemPeersController(restcomm.GenericPathController):
|
||||||
pecan.abort(400, _('Invalid request body format'))
|
pecan.abort(400, _('Invalid request body format'))
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
def _get_peer_group_list_for_system_peer(self, context, peer_id):
|
||||||
|
peer_groups = db_api.peer_group_get_for_system_peer(context, peer_id)
|
||||||
|
return utils.subcloud_peer_group_db_list_to_dict(peer_groups)
|
||||||
|
|
||||||
def _get_system_peer_list(self, context):
|
def _get_system_peer_list(self, context):
|
||||||
peers = db_api.system_peer_get_all(context)
|
peers = db_api.system_peer_get_all(context)
|
||||||
|
|
||||||
|
@ -92,10 +96,16 @@ class SystemPeersController(restcomm.GenericPathController):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@index.when(method='GET', template='json')
|
@index.when(method='GET', template='json')
|
||||||
def get(self, peer_ref=None):
|
def get(self, peer_ref=None, subcloud_peer_groups=False):
|
||||||
"""Get details about system peer.
|
"""Retrieve information about a system peer.
|
||||||
|
|
||||||
|
This function allows you to retrieve details about a specific
|
||||||
|
system peer or obtain a list of subcloud peer groups associated with
|
||||||
|
a specific system peer.
|
||||||
|
|
||||||
:param peer_ref: ID or UUID or Name of system peer
|
:param peer_ref: ID or UUID or Name of system peer
|
||||||
|
:param subcloud_peer_groups: If this request should return subcloud
|
||||||
|
peer groups
|
||||||
"""
|
"""
|
||||||
policy.authorize(system_peer_policy.POLICY_ROOT % "get", {},
|
policy.authorize(system_peer_policy.POLICY_ROOT % "get", {},
|
||||||
restcomm.extract_credentials_for_policy())
|
restcomm.extract_credentials_for_policy())
|
||||||
|
@ -108,6 +118,8 @@ class SystemPeersController(restcomm.GenericPathController):
|
||||||
peer = utils.system_peer_get_by_ref(context, peer_ref)
|
peer = utils.system_peer_get_by_ref(context, peer_ref)
|
||||||
if peer is None:
|
if peer is None:
|
||||||
pecan.abort(httpclient.NOT_FOUND, _('System Peer not found'))
|
pecan.abort(httpclient.NOT_FOUND, _('System Peer not found'))
|
||||||
|
if subcloud_peer_groups:
|
||||||
|
return self._get_peer_group_list_for_system_peer(context, peer.id)
|
||||||
system_peer_dict = db_api.system_peer_db_model_to_dict(peer)
|
system_peer_dict = db_api.system_peer_db_model_to_dict(peer)
|
||||||
return system_peer_dict
|
return system_peer_dict
|
||||||
|
|
||||||
|
@ -467,13 +479,14 @@ class SystemPeersController(restcomm.GenericPathController):
|
||||||
if peer is None:
|
if peer is None:
|
||||||
pecan.abort(httpclient.NOT_FOUND, _('System Peer not found'))
|
pecan.abort(httpclient.NOT_FOUND, _('System Peer not found'))
|
||||||
|
|
||||||
# TODO(jon): Add this back in when we have peer group associations
|
# A system peer cannot be deleted if it is used by any associations
|
||||||
# a system peer may not be deleted if it is use by any associations
|
association = db_api.\
|
||||||
# association = db_api.peer_group_association_get_by_system_peer_id(context,
|
peer_group_association_get_by_system_peer_id(context,
|
||||||
# str(peer.id))
|
str(peer.id))
|
||||||
# if len(association) > 0:
|
if len(association) > 0:
|
||||||
# pecan.abort(httpclient.BAD_REQUEST,
|
pecan.abort(httpclient.BAD_REQUEST,
|
||||||
# _('System peer associated with peer group'))
|
_('Cannot delete a system peer which is '
|
||||||
|
'associated with peer group.'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_api.system_peer_destroy(context, peer.id)
|
db_api.system_peer_destroy(context, peer.id)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import itertools
|
||||||
|
|
||||||
from dcmanager.api.policies import alarm_manager
|
from dcmanager.api.policies import alarm_manager
|
||||||
from dcmanager.api.policies import base
|
from dcmanager.api.policies import base
|
||||||
|
from dcmanager.api.policies import peer_group_association
|
||||||
from dcmanager.api.policies import phased_subcloud_deploy
|
from dcmanager.api.policies import phased_subcloud_deploy
|
||||||
from dcmanager.api.policies import subcloud_backup
|
from dcmanager.api.policies import subcloud_backup
|
||||||
from dcmanager.api.policies import subcloud_deploy
|
from dcmanager.api.policies import subcloud_deploy
|
||||||
|
@ -31,5 +32,6 @@ def list_rules():
|
||||||
subcloud_backup.list_rules(),
|
subcloud_backup.list_rules(),
|
||||||
phased_subcloud_deploy.list_rules(),
|
phased_subcloud_deploy.list_rules(),
|
||||||
subcloud_peer_group.list_rules(),
|
subcloud_peer_group.list_rules(),
|
||||||
|
peer_group_association.list_rules(),
|
||||||
system_peers.list_rules()
|
system_peers.list_rules()
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
#
|
||||||
|
# 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:peer_group_associations:%s'
|
||||||
|
|
||||||
|
|
||||||
|
peer_group_associations_rules = [
|
||||||
|
|
||||||
|
# CRUD of peer_group_associations entity
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'create',
|
||||||
|
check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS,
|
||||||
|
description="Create peer group association.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/v1.0/peer-group-associations'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'delete',
|
||||||
|
check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS,
|
||||||
|
description="Delete peer group association.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'DELETE',
|
||||||
|
'path': '/v1.0/peer-group-associations/{associate_id}'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'get',
|
||||||
|
check_str='rule:' + base.READER_IN_SYSTEM_PROJECTS,
|
||||||
|
description="Get peer group associations.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/v1.0/peer-group-associations'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/v1.0/peer-group-associations/{associate_id}'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=POLICY_ROOT % 'modify',
|
||||||
|
check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS,
|
||||||
|
description="Modify peer group association.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'PATCH',
|
||||||
|
'path': '/v1.0/peer-group-associations/{associate_id}'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'method': 'PATCH',
|
||||||
|
'path': '/v1.0/peer-group-associations/{associate_id}/sync'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return peer_group_associations_rules
|
|
@ -41,9 +41,15 @@ system_peers_rules = [
|
||||||
'method': 'GET',
|
'method': 'GET',
|
||||||
'path': '/v1.0/system-peers'
|
'path': '/v1.0/system-peers'
|
||||||
},
|
},
|
||||||
|
# Show details of a specified System Peer
|
||||||
{
|
{
|
||||||
'method': 'GET',
|
'method': 'GET',
|
||||||
'path': '/v1.0/system-peers/{system_peer}'
|
'path': '/v1.0/system-peers/{system_peer}'
|
||||||
|
},
|
||||||
|
# List Subcloud Peer Groups associated with the given System Peer
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/v1.0/system-peers/{system_peer}/subcloud-peer-groups'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
|
@ -445,3 +445,13 @@ STATES_FOR_SUBCLOUD_RENAME = [DEPLOY_STATE_DONE,
|
||||||
|
|
||||||
# batch rehome manage state wait timeout
|
# batch rehome manage state wait timeout
|
||||||
BATCH_REHOME_MGMT_STATES_TIMEOUT = 900
|
BATCH_REHOME_MGMT_STATES_TIMEOUT = 900
|
||||||
|
|
||||||
|
# System peer heartbeat status
|
||||||
|
SYSTEM_PEER_HEARTBEAT_STATUS_ALIVE = 'alive'
|
||||||
|
SYSTEM_PEER_HEARTBEAT_STATUS_FAILURE = 'failure'
|
||||||
|
|
||||||
|
# Peer group association sync status
|
||||||
|
ASSOCIATION_SYNC_STATUS_SYNCING = 'syncing'
|
||||||
|
ASSOCIATION_SYNC_STATUS_SYNCED = 'synced'
|
||||||
|
ASSOCIATION_SYNC_STATUS_FAILED = 'failed'
|
||||||
|
ASSOCIATION_SYNC_STATUS_DISABLED = 'disabled'
|
||||||
|
|
|
@ -177,6 +177,26 @@ class SubcloudPeerGroupNotFound(NotFound):
|
||||||
message = _("Subcloud Peer Group with id %(group_id)s doesn't exist.")
|
message = _("Subcloud Peer Group with id %(group_id)s doesn't exist.")
|
||||||
|
|
||||||
|
|
||||||
|
class PeerGroupAssociationCombinationNotFound(NotFound):
|
||||||
|
message = _("Peer Group Association between peer group: %(peer_group_id)s "
|
||||||
|
"and system peer: %(system_peer_id)s doesn't exist.")
|
||||||
|
|
||||||
|
|
||||||
|
class PeerGroupAssociationTargetNotMatch(NotFound):
|
||||||
|
message = _("Peer Group Association with peer site controller "
|
||||||
|
"UUID %(uuid)s doesn't match.")
|
||||||
|
|
||||||
|
|
||||||
|
class SubcloudPeerGroupHasWrongPriority(DCManagerException):
|
||||||
|
message = _("Subcloud Peer group of peer site has wrong "
|
||||||
|
"priority %(priority)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class PeerGroupAssociationNotFound(NotFound):
|
||||||
|
message = _("Peer Group Association with id %(association_id)s "
|
||||||
|
"doesn't exist.")
|
||||||
|
|
||||||
|
|
||||||
class SubcloudGroupNameViolation(DCManagerException):
|
class SubcloudGroupNameViolation(DCManagerException):
|
||||||
message = _("Default Subcloud Group name cannot be changed or reused.")
|
message = _("Default Subcloud Group name cannot be changed or reused.")
|
||||||
|
|
||||||
|
|
|
@ -381,6 +381,7 @@ def system_peer_db_model_to_dict(system_peer):
|
||||||
"heartbeat-failure-policy": system_peer.heartbeat_failure_policy,
|
"heartbeat-failure-policy": system_peer.heartbeat_failure_policy,
|
||||||
"heartbeat-maintenance-timeout": system_peer.
|
"heartbeat-maintenance-timeout": system_peer.
|
||||||
heartbeat_maintenance_timeout,
|
heartbeat_maintenance_timeout,
|
||||||
|
"heartbeat-status": system_peer.heartbeat_status,
|
||||||
"created-at": system_peer.created_at,
|
"created-at": system_peer.created_at,
|
||||||
"updated-at": system_peer.updated_at}
|
"updated-at": system_peer.updated_at}
|
||||||
return result
|
return result
|
||||||
|
@ -427,6 +428,11 @@ def system_peer_get_all(context):
|
||||||
return IMPL.system_peer_get_all(context)
|
return IMPL.system_peer_get_all(context)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_get_for_system_peer(context, peer_id):
|
||||||
|
"""Get subcloud peer groups associated with a system peer."""
|
||||||
|
return IMPL.peer_group_get_for_system_peer(context, peer_id)
|
||||||
|
|
||||||
|
|
||||||
def system_peer_update(context, peer_id,
|
def system_peer_update(context, peer_id,
|
||||||
peer_uuid, peer_name,
|
peer_uuid, peer_name,
|
||||||
endpoint, username, password,
|
endpoint, username, password,
|
||||||
|
@ -435,7 +441,8 @@ def system_peer_update(context, peer_id,
|
||||||
heartbeat_interval,
|
heartbeat_interval,
|
||||||
heartbeat_failure_threshold,
|
heartbeat_failure_threshold,
|
||||||
heartbeat_failure_policy,
|
heartbeat_failure_policy,
|
||||||
heartbeat_maintenance_timeout):
|
heartbeat_maintenance_timeout,
|
||||||
|
heartbeat_status=None):
|
||||||
"""Update the system peer or raise if it does not exist."""
|
"""Update the system peer or raise if it does not exist."""
|
||||||
return IMPL.system_peer_update(context, peer_id,
|
return IMPL.system_peer_update(context, peer_id,
|
||||||
peer_uuid, peer_name,
|
peer_uuid, peer_name,
|
||||||
|
@ -445,7 +452,8 @@ def system_peer_update(context, peer_id,
|
||||||
heartbeat_interval,
|
heartbeat_interval,
|
||||||
heartbeat_failure_threshold,
|
heartbeat_failure_threshold,
|
||||||
heartbeat_failure_policy,
|
heartbeat_failure_policy,
|
||||||
heartbeat_maintenance_timeout)
|
heartbeat_maintenance_timeout,
|
||||||
|
heartbeat_status)
|
||||||
|
|
||||||
|
|
||||||
def system_peer_destroy(context, peer_id):
|
def system_peer_destroy(context, peer_id):
|
||||||
|
@ -530,6 +538,76 @@ def subcloud_peer_group_update(context, group_id, peer_group_name, group_priorit
|
||||||
###################
|
###################
|
||||||
|
|
||||||
|
|
||||||
|
###################
|
||||||
|
# peer_group_association
|
||||||
|
def peer_group_association_db_model_to_dict(peer_group_association):
|
||||||
|
"""Convert peer_group_association db model to dictionary."""
|
||||||
|
result = {"id": peer_group_association.id,
|
||||||
|
"peer-group-id": peer_group_association.peer_group_id,
|
||||||
|
"system-peer-id": peer_group_association.system_peer_id,
|
||||||
|
"peer-group-priority": peer_group_association.peer_group_priority,
|
||||||
|
"sync-status": peer_group_association.sync_status,
|
||||||
|
"sync-message": peer_group_association.sync_message,
|
||||||
|
"created-at": peer_group_association.created_at,
|
||||||
|
"updated-at": peer_group_association.updated_at}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_create(context, peer_group_id, system_peer_id,
|
||||||
|
peer_group_priority, sync_status=None,
|
||||||
|
sync_message=None):
|
||||||
|
"""Create a peer_group_association."""
|
||||||
|
return IMPL.peer_group_association_create(context,
|
||||||
|
peer_group_id,
|
||||||
|
system_peer_id,
|
||||||
|
peer_group_priority,
|
||||||
|
sync_status,
|
||||||
|
sync_message)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_update(context, id, peer_group_priority=None,
|
||||||
|
sync_status=None, sync_message=None):
|
||||||
|
"""Update the system peer or raise if it does not exist."""
|
||||||
|
return IMPL.peer_group_association_update(context, id, peer_group_priority,
|
||||||
|
sync_status, sync_message)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_destroy(context, id):
|
||||||
|
"""Destroy the peer_group_association or raise if it does not exist."""
|
||||||
|
return IMPL.peer_group_association_destroy(context, id)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_get(context, id):
|
||||||
|
"""Retrieve a peer_group_association or raise if it does not exist."""
|
||||||
|
return IMPL.peer_group_association_get(context, id)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_get_all(context):
|
||||||
|
"""Retrieve all peer_group_associations."""
|
||||||
|
return IMPL.peer_group_association_get_all(context)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_get_by_peer_group_and_system_peer_id(context,
|
||||||
|
peer_group_id,
|
||||||
|
system_peer_id):
|
||||||
|
"""Get peer group associations by peer_group_id and system_peer_id."""
|
||||||
|
return IMPL.peer_group_association_get_by_peer_group_and_system_peer_id(
|
||||||
|
context, peer_group_id, system_peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_get_by_peer_group_id(context, peer_group_id):
|
||||||
|
"""Get the peer_group_association list by peer_group_id"""
|
||||||
|
return IMPL.peer_group_association_get_by_peer_group_id(context,
|
||||||
|
peer_group_id)
|
||||||
|
|
||||||
|
|
||||||
|
def peer_group_association_get_by_system_peer_id(context, system_peer_id):
|
||||||
|
"""Get the peer_group_association list by system_peer_id"""
|
||||||
|
return IMPL.peer_group_association_get_by_system_peer_id(context,
|
||||||
|
system_peer_id)
|
||||||
|
###################
|
||||||
|
|
||||||
|
|
||||||
def sw_update_strategy_db_model_to_dict(sw_update_strategy):
|
def sw_update_strategy_db_model_to_dict(sw_update_strategy):
|
||||||
"""Convert sw update db model to dictionary."""
|
"""Convert sw update db model to dictionary."""
|
||||||
result = {"id": sw_update_strategy.id,
|
result = {"id": sw_update_strategy.id,
|
||||||
|
|
|
@ -863,6 +863,18 @@ def system_peer_get_all(context):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# This method returns all subcloud peer groups for a particular system peer
|
||||||
|
@require_context
|
||||||
|
def peer_group_get_for_system_peer(context, peer_id):
|
||||||
|
return model_query(context, models.SubcloudPeerGroup). \
|
||||||
|
join(models.PeerGroupAssociation, models.SubcloudPeerGroup.id ==
|
||||||
|
models.PeerGroupAssociation.peer_group_id). \
|
||||||
|
filter(models.SubcloudPeerGroup.deleted == 0). \
|
||||||
|
filter(models.PeerGroupAssociation.system_peer_id == peer_id). \
|
||||||
|
order_by(models.SubcloudPeerGroup.id). \
|
||||||
|
all()
|
||||||
|
|
||||||
|
|
||||||
@require_admin_context
|
@require_admin_context
|
||||||
def system_peer_create(context,
|
def system_peer_create(context,
|
||||||
peer_uuid, peer_name,
|
peer_uuid, peer_name,
|
||||||
|
@ -872,7 +884,8 @@ def system_peer_create(context,
|
||||||
heartbeat_interval=60,
|
heartbeat_interval=60,
|
||||||
heartbeat_failure_threshold=3,
|
heartbeat_failure_threshold=3,
|
||||||
heartbeat_failure_policy="alarm",
|
heartbeat_failure_policy="alarm",
|
||||||
heartbeat_maintenance_timeout=600):
|
heartbeat_maintenance_timeout=600,
|
||||||
|
heartbeat_status="created"):
|
||||||
with write_session() as session:
|
with write_session() as session:
|
||||||
system_peer_ref = models.SystemPeer()
|
system_peer_ref = models.SystemPeer()
|
||||||
system_peer_ref.peer_uuid = peer_uuid
|
system_peer_ref.peer_uuid = peer_uuid
|
||||||
|
@ -888,6 +901,7 @@ def system_peer_create(context,
|
||||||
system_peer_ref.heartbeat_failure_policy = heartbeat_failure_policy
|
system_peer_ref.heartbeat_failure_policy = heartbeat_failure_policy
|
||||||
system_peer_ref.heartbeat_maintenance_timeout = \
|
system_peer_ref.heartbeat_maintenance_timeout = \
|
||||||
heartbeat_maintenance_timeout
|
heartbeat_maintenance_timeout
|
||||||
|
system_peer_ref.heartbeat_status = heartbeat_status
|
||||||
session.add(system_peer_ref)
|
session.add(system_peer_ref)
|
||||||
return system_peer_ref
|
return system_peer_ref
|
||||||
|
|
||||||
|
@ -901,7 +915,8 @@ def system_peer_update(context, peer_id,
|
||||||
heartbeat_interval=None,
|
heartbeat_interval=None,
|
||||||
heartbeat_failure_threshold=None,
|
heartbeat_failure_threshold=None,
|
||||||
heartbeat_failure_policy=None,
|
heartbeat_failure_policy=None,
|
||||||
heartbeat_maintenance_timeout=None):
|
heartbeat_maintenance_timeout=None,
|
||||||
|
heartbeat_status=None):
|
||||||
with write_session() as session:
|
with write_session() as session:
|
||||||
system_peer_ref = system_peer_get(context, peer_id)
|
system_peer_ref = system_peer_get(context, peer_id)
|
||||||
if peer_uuid is not None:
|
if peer_uuid is not None:
|
||||||
|
@ -928,6 +943,8 @@ def system_peer_update(context, peer_id,
|
||||||
if heartbeat_maintenance_timeout is not None:
|
if heartbeat_maintenance_timeout is not None:
|
||||||
system_peer_ref.heartbeat_maintenance_timeout = \
|
system_peer_ref.heartbeat_maintenance_timeout = \
|
||||||
heartbeat_maintenance_timeout
|
heartbeat_maintenance_timeout
|
||||||
|
if heartbeat_status is not None:
|
||||||
|
system_peer_ref.heartbeat_status = heartbeat_status
|
||||||
system_peer_ref.save(session)
|
system_peer_ref.save(session)
|
||||||
return system_peer_ref
|
return system_peer_ref
|
||||||
|
|
||||||
|
@ -1201,6 +1218,126 @@ def subcloud_peer_group_update(context,
|
||||||
##########################
|
##########################
|
||||||
|
|
||||||
|
|
||||||
|
##########################
|
||||||
|
# peer group association
|
||||||
|
##########################
|
||||||
|
@require_admin_context
|
||||||
|
def peer_group_association_create(context,
|
||||||
|
peer_group_id,
|
||||||
|
system_peer_id,
|
||||||
|
peer_group_priority,
|
||||||
|
sync_status,
|
||||||
|
sync_message):
|
||||||
|
with write_session() as session:
|
||||||
|
peer_group_association_ref = models.PeerGroupAssociation()
|
||||||
|
peer_group_association_ref.peer_group_id = peer_group_id
|
||||||
|
peer_group_association_ref.system_peer_id = system_peer_id
|
||||||
|
peer_group_association_ref.peer_group_priority = peer_group_priority
|
||||||
|
peer_group_association_ref.sync_status = sync_status
|
||||||
|
peer_group_association_ref.sync_message = sync_message
|
||||||
|
session.add(peer_group_association_ref)
|
||||||
|
return peer_group_association_ref
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def peer_group_association_update(context,
|
||||||
|
associate_id,
|
||||||
|
peer_group_priority=None,
|
||||||
|
sync_status=None,
|
||||||
|
sync_message=None):
|
||||||
|
with write_session() as session:
|
||||||
|
association_ref = peer_group_association_get(context, associate_id)
|
||||||
|
if peer_group_priority is not None:
|
||||||
|
association_ref.peer_group_priority = peer_group_priority
|
||||||
|
if sync_status is not None:
|
||||||
|
association_ref.sync_status = sync_status
|
||||||
|
if sync_message is not None:
|
||||||
|
association_ref.sync_message = sync_message
|
||||||
|
association_ref.save(session)
|
||||||
|
return association_ref
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def peer_group_association_destroy(context, association_id):
|
||||||
|
with write_session() as session:
|
||||||
|
association_ref = peer_group_association_get(context, association_id)
|
||||||
|
session.delete(association_ref)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def peer_group_association_get(context, association_id):
|
||||||
|
try:
|
||||||
|
result = model_query(context, models.PeerGroupAssociation). \
|
||||||
|
filter_by(deleted=0). \
|
||||||
|
filter_by(id=association_id). \
|
||||||
|
one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.PeerGroupAssociationNotFound(
|
||||||
|
association_id=association_id)
|
||||||
|
except MultipleResultsFound:
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
err="Multiple entries found for peer group association %s" %
|
||||||
|
association_id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def peer_group_association_get_all(context):
|
||||||
|
result = model_query(context, models.PeerGroupAssociation). \
|
||||||
|
filter_by(deleted=0). \
|
||||||
|
order_by(models.PeerGroupAssociation.id). \
|
||||||
|
all()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Each combination of 'peer_group_id' and 'system_peer_id' is unique
|
||||||
|
# and appears only once in the entries.
|
||||||
|
@require_context
|
||||||
|
def peer_group_association_get_by_peer_group_and_system_peer_id(context,
|
||||||
|
peer_group_id,
|
||||||
|
system_peer_id):
|
||||||
|
try:
|
||||||
|
result = model_query(context, models.PeerGroupAssociation). \
|
||||||
|
filter_by(deleted=0). \
|
||||||
|
filter_by(peer_group_id=peer_group_id). \
|
||||||
|
filter_by(system_peer_id=system_peer_id). \
|
||||||
|
one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise exception.PeerGroupAssociationCombinationNotFound(
|
||||||
|
peer_group_id=peer_group_id, system_peer_id=system_peer_id)
|
||||||
|
except MultipleResultsFound:
|
||||||
|
# This exception should never happen due to the UNIQUE setting for name
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
err="Multiple entries found for peer group association %s,%s" %
|
||||||
|
(peer_group_id, system_peer_id))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def peer_group_association_get_by_peer_group_id(context, peer_group_id):
|
||||||
|
result = model_query(context, models.PeerGroupAssociation). \
|
||||||
|
filter_by(deleted=0). \
|
||||||
|
filter_by(peer_group_id=peer_group_id). \
|
||||||
|
order_by(models.PeerGroupAssociation.id). \
|
||||||
|
all()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def peer_group_association_get_by_system_peer_id(context, system_peer_id):
|
||||||
|
result = model_query(context, models.PeerGroupAssociation). \
|
||||||
|
filter_by(deleted=0). \
|
||||||
|
filter_by(system_peer_id=system_peer_id). \
|
||||||
|
order_by(models.PeerGroupAssociation.id). \
|
||||||
|
all()
|
||||||
|
|
||||||
|
return result
|
||||||
|
##########################
|
||||||
|
|
||||||
|
|
||||||
@require_context
|
@require_context
|
||||||
def strategy_step_get(context, subcloud_id):
|
def strategy_step_get(context, subcloud_id):
|
||||||
result = model_query(context, models.StrategyStep). \
|
result = model_query(context, models.StrategyStep). \
|
||||||
|
|
|
@ -72,6 +72,33 @@ def upgrade(migrate_engine):
|
||||||
)
|
)
|
||||||
system_peer.create()
|
system_peer.create()
|
||||||
|
|
||||||
|
# Declare the new peer_group_association table
|
||||||
|
peer_group_association = sqlalchemy.Table(
|
||||||
|
'peer_group_association', meta,
|
||||||
|
sqlalchemy.Column('id', sqlalchemy.Integer,
|
||||||
|
primary_key=True,
|
||||||
|
autoincrement=True,
|
||||||
|
nullable=False),
|
||||||
|
sqlalchemy.Column('peer_group_id', sqlalchemy.Integer,
|
||||||
|
sqlalchemy.ForeignKey('subcloud_peer_group.id',
|
||||||
|
ondelete='CASCADE')),
|
||||||
|
sqlalchemy.Column('system_peer_id', sqlalchemy.Integer,
|
||||||
|
sqlalchemy.ForeignKey('system_peer.id',
|
||||||
|
ondelete='CASCADE')),
|
||||||
|
sqlalchemy.Column('peer_group_priority', sqlalchemy.Integer),
|
||||||
|
sqlalchemy.Column('sync_status', sqlalchemy.String(255)),
|
||||||
|
sqlalchemy.Column('sync_message', sqlalchemy.Text),
|
||||||
|
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
|
||||||
|
)
|
||||||
|
peer_group_association.create()
|
||||||
|
|
||||||
|
|
||||||
def downgrade(migrate_engine):
|
def downgrade(migrate_engine):
|
||||||
raise NotImplementedError('Database downgrade is unsupported.')
|
raise NotImplementedError('Database downgrade is unsupported.')
|
||||||
|
|
|
@ -117,6 +117,7 @@ class SystemPeer(BASE, DCManagerBase):
|
||||||
heartbeat_failure_threshold = Column(Integer)
|
heartbeat_failure_threshold = Column(Integer)
|
||||||
heartbeat_failure_policy = Column(String(255))
|
heartbeat_failure_policy = Column(String(255))
|
||||||
heartbeat_maintenance_timeout = Column(Integer)
|
heartbeat_maintenance_timeout = Column(Integer)
|
||||||
|
heartbeat_status = Column(String(255))
|
||||||
|
|
||||||
|
|
||||||
class SubcloudGroup(BASE, DCManagerBase):
|
class SubcloudGroup(BASE, DCManagerBase):
|
||||||
|
@ -145,6 +146,19 @@ class SubcloudPeerGroup(BASE, DCManagerBase):
|
||||||
system_leader_name = Column(String(255))
|
system_leader_name = Column(String(255))
|
||||||
|
|
||||||
|
|
||||||
|
class PeerGroupAssociation(BASE, DCManagerBase):
|
||||||
|
"""Represents a Peer Group Association"""
|
||||||
|
|
||||||
|
__tablename__ = 'peer_group_association'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
|
||||||
|
peer_group_id = Column(Integer)
|
||||||
|
system_peer_id = Column(Integer)
|
||||||
|
peer_group_priority = Column(Integer)
|
||||||
|
sync_status = Column(String(255))
|
||||||
|
sync_message = Column(Text())
|
||||||
|
|
||||||
|
|
||||||
class Subcloud(BASE, DCManagerBase):
|
class Subcloud(BASE, DCManagerBase):
|
||||||
"""Represents a subcloud"""
|
"""Represents a subcloud"""
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ from dcmanager.common.i18n import _
|
||||||
from dcmanager.common import messaging as rpc_messaging
|
from dcmanager.common import messaging as rpc_messaging
|
||||||
from dcmanager.common import utils
|
from dcmanager.common import utils
|
||||||
from dcmanager.manager.subcloud_manager import SubcloudManager
|
from dcmanager.manager.subcloud_manager import SubcloudManager
|
||||||
|
from dcmanager.manager.system_peer_manager import SystemPeerManager
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
@ -88,6 +89,7 @@ class DCManagerService(service.Service):
|
||||||
|
|
||||||
def init_managers(self):
|
def init_managers(self):
|
||||||
self.subcloud_manager = SubcloudManager()
|
self.subcloud_manager = SubcloudManager()
|
||||||
|
self.syspeer_manager = SystemPeerManager()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
utils.set_open_file_limit(cfg.CONF.worker_rlimit_nofile)
|
utils.set_open_file_limit(cfg.CONF.worker_rlimit_nofile)
|
||||||
|
@ -300,6 +302,21 @@ class DCManagerService(service.Service):
|
||||||
payload['peer_group'])
|
payload['peer_group'])
|
||||||
return self.subcloud_manager.batch_migrate_subcloud(context, payload)
|
return self.subcloud_manager.batch_migrate_subcloud(context, payload)
|
||||||
|
|
||||||
|
@request_context
|
||||||
|
def sync_subcloud_peer_group(self, context, association_id,
|
||||||
|
sync_subclouds=True, priority=None):
|
||||||
|
LOG.info("Handling sync_subcloud_peer_group request for: %s",
|
||||||
|
association_id)
|
||||||
|
return self.syspeer_manager.sync_subcloud_peer_group(
|
||||||
|
context, association_id, sync_subclouds, priority)
|
||||||
|
|
||||||
|
@request_context
|
||||||
|
def delete_peer_group_association(self, context, association_id):
|
||||||
|
LOG.info("Handling delete_peer_group_association request for: %s",
|
||||||
|
association_id)
|
||||||
|
return self.syspeer_manager.delete_peer_group_association(
|
||||||
|
context, association_id)
|
||||||
|
|
||||||
def _stop_rpc_server(self):
|
def _stop_rpc_server(self):
|
||||||
# Stop RPC connection to prevent new requests
|
# Stop RPC connection to prevent new requests
|
||||||
LOG.debug(_("Attempting to stop RPC service..."))
|
LOG.debug(_("Attempting to stop RPC service..."))
|
||||||
|
|
|
@ -0,0 +1,508 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from eventlet import greenpool
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from dccommon import consts as dccommon_consts
|
||||||
|
from dccommon.drivers.openstack.dcmanager_v1 import DcmanagerClient
|
||||||
|
from dccommon.drivers.openstack.peer_site import PeerSiteDriver
|
||||||
|
from dccommon.drivers.openstack.sysinv_v1 import SysinvClient
|
||||||
|
from dccommon import exceptions as dccommon_exceptions
|
||||||
|
from dcmanager.common import consts
|
||||||
|
from dcmanager.common import exceptions
|
||||||
|
from dcmanager.common.i18n import _
|
||||||
|
from dcmanager.common import manager
|
||||||
|
from dcmanager.db import api as db_api
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TEMP_BOOTSTRAP_PREFIX = 'peer_subcloud_bootstrap_yaml'
|
||||||
|
MAX_PARALLEL_SUBCLOUD_SYNC = 10
|
||||||
|
VERIFY_SUBCLOUD_SYNC_VALID = 'valid'
|
||||||
|
VERIFY_SUBCLOUD_SYNC_IGNORE = 'ignore'
|
||||||
|
|
||||||
|
|
||||||
|
class SystemPeerManager(manager.Manager):
|
||||||
|
"""Manages tasks related to system peers."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
LOG.debug(_('SystemPeerManager initialization...'))
|
||||||
|
|
||||||
|
super(SystemPeerManager, self).__init__(
|
||||||
|
service_name="system_peer_manager", *args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_peer_ks_client(peer):
|
||||||
|
"""This will get a new peer keystone client (and new token)"""
|
||||||
|
try:
|
||||||
|
os_client = PeerSiteDriver(
|
||||||
|
auth_url=peer.manager_endpoint,
|
||||||
|
username=peer.manager_username,
|
||||||
|
password=base64.b64decode(
|
||||||
|
peer.manager_password.encode("utf-8")).decode("utf-8"),
|
||||||
|
site_uuid=peer.peer_uuid)
|
||||||
|
return os_client.keystone_client
|
||||||
|
except Exception:
|
||||||
|
LOG.warn('Failure initializing KeystoneClient '
|
||||||
|
f'for system peer {peer.peer_name}')
|
||||||
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_peer_sysinv_client(peer):
|
||||||
|
p_ks_client = SystemPeerManager.get_peer_ks_client(peer)
|
||||||
|
sysinv_endpoint = p_ks_client.session.get_endpoint(
|
||||||
|
service_type='platform',
|
||||||
|
region_name=dccommon_consts.DEFAULT_REGION_NAME,
|
||||||
|
interface=dccommon_consts.KS_ENDPOINT_PUBLIC)
|
||||||
|
return SysinvClient(dccommon_consts.DEFAULT_REGION_NAME,
|
||||||
|
p_ks_client.session,
|
||||||
|
endpoint_type=dccommon_consts.
|
||||||
|
KS_ENDPOINT_PUBLIC,
|
||||||
|
endpoint=sysinv_endpoint)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_peer_dc_client(peer):
|
||||||
|
p_ks_client = SystemPeerManager.get_peer_ks_client(peer)
|
||||||
|
dc_endpoint = p_ks_client.session.get_endpoint(
|
||||||
|
service_type='dcmanager',
|
||||||
|
region_name=dccommon_consts.SYSTEM_CONTROLLER_NAME,
|
||||||
|
interface=dccommon_consts.KS_ENDPOINT_PUBLIC)
|
||||||
|
return DcmanagerClient(dccommon_consts.SYSTEM_CONTROLLER_NAME,
|
||||||
|
p_ks_client.session,
|
||||||
|
endpoint=dc_endpoint)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_peer_subcloud(dc_client, subcloud_name):
|
||||||
|
"""Get subcloud on peer site if exist.
|
||||||
|
|
||||||
|
:param dc_client: the dcmanager client object
|
||||||
|
:param subcloud_ref: subcloud name needs to check
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
peer_subcloud = dc_client.get_subcloud(subcloud_name)
|
||||||
|
return peer_subcloud
|
||||||
|
except dccommon_exceptions.SubcloudNotFound:
|
||||||
|
LOG.warn(f"Subcloud {subcloud_name} does not exist on peer site.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_subcloud_secondary(subcloud):
|
||||||
|
"""Check if subcloud on peer site is secondary.
|
||||||
|
|
||||||
|
:param subcloud: peer subcloud dictionary
|
||||||
|
"""
|
||||||
|
deploy_status = 'deploy-status' if 'deploy-status' in subcloud else \
|
||||||
|
'deploy_status'
|
||||||
|
if subcloud.get(deploy_status) not in (
|
||||||
|
consts.DEPLOY_STATE_SECONDARY_FAILED,
|
||||||
|
consts.DEPLOY_STATE_SECONDARY):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_peer_secondary_subcloud(dc_client, subcloud_ref):
|
||||||
|
"""Delete secondary subcloud on peer site.
|
||||||
|
|
||||||
|
:param dc_client: the dcmanager client object
|
||||||
|
:param subcloud_ref: subcloud name to delete
|
||||||
|
"""
|
||||||
|
peer_subcloud = SystemPeerManager.get_peer_subcloud(dc_client,
|
||||||
|
subcloud_ref)
|
||||||
|
if not peer_subcloud:
|
||||||
|
LOG.info(f"Skip delete Peer Site Subcloud {subcloud_ref} cause "
|
||||||
|
f"it doesn't exist.")
|
||||||
|
return
|
||||||
|
|
||||||
|
is_secondary = SystemPeerManager.is_subcloud_secondary(peer_subcloud)
|
||||||
|
if not is_secondary:
|
||||||
|
LOG.info(f"Ignoring delete Peer Site Subcloud {subcloud_ref} "
|
||||||
|
f"as is not in secondary state.")
|
||||||
|
return
|
||||||
|
|
||||||
|
dc_client.delete_subcloud(subcloud_ref)
|
||||||
|
LOG.info(f"Deleted Subcloud {subcloud_ref} on peer site.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _run_parallel_group_operation(op_type, op_function, thread_pool,
|
||||||
|
subclouds):
|
||||||
|
"""Run parallel group operation on subclouds."""
|
||||||
|
failed_subclouds = []
|
||||||
|
processed = 0
|
||||||
|
error_msg = {} # Dictinary to store error message for each subcloud
|
||||||
|
|
||||||
|
for subcloud, success in thread_pool.imap(op_function, subclouds):
|
||||||
|
processed += 1
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
failed_subclouds.append(subcloud)
|
||||||
|
if hasattr(subcloud, 'msg'):
|
||||||
|
error_msg[subcloud.name] = subcloud.msg
|
||||||
|
|
||||||
|
completion = float(processed) / float(len(subclouds)) * 100
|
||||||
|
remaining = len(subclouds) - processed
|
||||||
|
LOG.info("Processed subcloud %s for %s (operation %.0f%% "
|
||||||
|
"complete, %d subcloud(s) remaining)" %
|
||||||
|
(subcloud.name, op_type, completion, remaining))
|
||||||
|
|
||||||
|
return failed_subclouds, error_msg
|
||||||
|
|
||||||
|
def _add_or_update_subcloud(self, dc_client, peer_controller_gateway_ip,
|
||||||
|
dc_peer_pg_id, subcloud):
|
||||||
|
"""Add or update subcloud on peer site in parallel."""
|
||||||
|
with tempfile.NamedTemporaryFile(prefix=TEMP_BOOTSTRAP_PREFIX,
|
||||||
|
suffix=".yaml",
|
||||||
|
mode='w') as temp_file:
|
||||||
|
subcloud_name = subcloud.get('name')
|
||||||
|
region_name = subcloud.get('region_name')
|
||||||
|
rehome_data = json.loads(subcloud.rehome_data)
|
||||||
|
subcloud_payload = rehome_data['saved_payload']
|
||||||
|
|
||||||
|
subcloud_payload['systemcontroller_gateway_address'] = \
|
||||||
|
peer_controller_gateway_ip
|
||||||
|
|
||||||
|
yaml.dump(subcloud_payload, temp_file)
|
||||||
|
|
||||||
|
files = {"bootstrap_values": temp_file.name}
|
||||||
|
data = {
|
||||||
|
"bootstrap-address": subcloud_payload['bootstrap-address'],
|
||||||
|
"region_name": subcloud.region_name,
|
||||||
|
"location": subcloud.location,
|
||||||
|
"description": subcloud.description
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Sync subcloud information to peer site
|
||||||
|
peer_subcloud = self.get_peer_subcloud(dc_client, subcloud_name)
|
||||||
|
if peer_subcloud:
|
||||||
|
dc_peer_subcloud = dc_client.update_subcloud(subcloud_name,
|
||||||
|
files, data)
|
||||||
|
LOG.info(f"Updated Subcloud {dc_peer_subcloud.get('name')} "
|
||||||
|
"(region_name: "
|
||||||
|
f"{dc_peer_subcloud.get('region_name')}) on peer "
|
||||||
|
"site.")
|
||||||
|
else:
|
||||||
|
# Create subcloud on peer site if not exist
|
||||||
|
dc_peer_subcloud = dc_client. \
|
||||||
|
add_subcloud_with_secondary_status(files, data)
|
||||||
|
LOG.info(f"Created Subcloud {dc_peer_subcloud.get('name')} "
|
||||||
|
"(region_name: "
|
||||||
|
f"{dc_peer_subcloud.get('region_name')}) on peer "
|
||||||
|
"site.")
|
||||||
|
LOG.debug(f"Updating subcloud {subcloud_name} (region_name: "
|
||||||
|
f"{region_name}) with subcloud peer group id "
|
||||||
|
f"{dc_peer_pg_id} on peer site.")
|
||||||
|
# Update subcloud associated peer group on peer site
|
||||||
|
dc_client.update_subcloud(subcloud_name, files=None,
|
||||||
|
data={"peer_group": str(dc_peer_pg_id)})
|
||||||
|
return subcloud, True
|
||||||
|
except Exception as e:
|
||||||
|
subcloud.msg = str(e) # Store error message for subcloud
|
||||||
|
LOG.error(f"Failed to add/update Subcloud {subcloud_name} "
|
||||||
|
f"(region_name: {region_name}) "
|
||||||
|
f"on peer site: {str(e)}")
|
||||||
|
return subcloud, False
|
||||||
|
|
||||||
|
def _is_valid_for_subcloud_sync(self, subcloud):
|
||||||
|
"""Verify subcloud data for sync."""
|
||||||
|
subcloud_name = subcloud.get('name')
|
||||||
|
region_name = subcloud.get('region_name')
|
||||||
|
|
||||||
|
# Ignore the secondary subclouds to sync with peer site
|
||||||
|
if self.is_subcloud_secondary(subcloud):
|
||||||
|
LOG.info(f"Ignoring the Subcloud {subcloud_name} (region_name: "
|
||||||
|
f"{region_name}) in secondary status to sync with "
|
||||||
|
"peer site.")
|
||||||
|
return VERIFY_SUBCLOUD_SYNC_IGNORE
|
||||||
|
|
||||||
|
# Verify subcloud payload data
|
||||||
|
rehome_json = subcloud.rehome_data
|
||||||
|
if not rehome_json:
|
||||||
|
msg = f"Subcloud {subcloud_name} (region_name: " + \
|
||||||
|
f"{region_name}) does not have rehome_data."
|
||||||
|
return msg
|
||||||
|
|
||||||
|
rehome_data = json.loads(rehome_json)
|
||||||
|
if 'saved_payload' not in rehome_data:
|
||||||
|
msg = f"Subcloud {subcloud_name} (region_name: " + \
|
||||||
|
f"{region_name}) does not have saved_payload."
|
||||||
|
return msg
|
||||||
|
|
||||||
|
subcloud_payload = rehome_data['saved_payload']
|
||||||
|
if not subcloud_payload:
|
||||||
|
msg = f"Subcloud {subcloud_name} (region_name: " + \
|
||||||
|
f"{region_name}) saved_payload is empty."
|
||||||
|
return msg
|
||||||
|
|
||||||
|
if 'bootstrap-address' not in subcloud_payload:
|
||||||
|
msg = f"Subcloud {subcloud_name} (region_name: " + \
|
||||||
|
f"{region_name}) does not have bootstrap-address in " + \
|
||||||
|
"saved_payload."
|
||||||
|
return msg
|
||||||
|
|
||||||
|
if 'systemcontroller_gateway_address' not in subcloud_payload:
|
||||||
|
msg = f"Subcloud {subcloud_name} (region_name: " + \
|
||||||
|
f"{region_name}) does not have systemcontroller_" + \
|
||||||
|
"gateway_address in saved_payload."
|
||||||
|
return msg
|
||||||
|
|
||||||
|
return VERIFY_SUBCLOUD_SYNC_VALID
|
||||||
|
|
||||||
|
def _validate_subclouds_for_sync(self, subclouds, dc_client):
|
||||||
|
"""Validate subclouds for sync."""
|
||||||
|
valid_subclouds = []
|
||||||
|
error_msg = {} # Dictinary to store error message for each subcloud
|
||||||
|
|
||||||
|
for subcloud in subclouds:
|
||||||
|
subcloud_name = subcloud.get('name')
|
||||||
|
region_name = subcloud.get('region_name')
|
||||||
|
|
||||||
|
validation = self._is_valid_for_subcloud_sync(subcloud)
|
||||||
|
if validation != VERIFY_SUBCLOUD_SYNC_IGNORE and \
|
||||||
|
validation != VERIFY_SUBCLOUD_SYNC_VALID:
|
||||||
|
LOG.error(validation)
|
||||||
|
error_msg[subcloud_name] = validation
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
peer_subcloud = self.get_peer_subcloud(dc_client, subcloud_name)
|
||||||
|
if not peer_subcloud:
|
||||||
|
LOG.info(f"Subcloud {subcloud_name} (region_name: "
|
||||||
|
f"{region_name}) does not exist on peer site.")
|
||||||
|
valid_subclouds.append(subcloud)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self.is_subcloud_secondary(peer_subcloud):
|
||||||
|
msg = "Ignoring update Peer Site Subcloud " + \
|
||||||
|
f"{subcloud_name} (region_name: {region_name})" + \
|
||||||
|
" as is not in secondary state."
|
||||||
|
LOG.info(msg)
|
||||||
|
error_msg[subcloud_name] = msg
|
||||||
|
|
||||||
|
valid_subclouds.append(subcloud)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
subcloud.msg = str(e) # Store error message for subcloud
|
||||||
|
LOG.error(f"Failed to validate Subcloud {subcloud_name} "
|
||||||
|
f"(region_name: {region_name}): {str(e)}")
|
||||||
|
error_msg[subcloud_name] = str(e)
|
||||||
|
|
||||||
|
return valid_subclouds, error_msg
|
||||||
|
|
||||||
|
def _sync_subclouds(self, context, peer, dc_local_pg_id, dc_peer_pg_id):
|
||||||
|
"""Sync subclouds of local peer group to peer site.
|
||||||
|
|
||||||
|
:param context: request context object
|
||||||
|
:param peer: system peer object of the peer site
|
||||||
|
:param dc_local_pg_id: peer group id on local site for sync
|
||||||
|
:param dc_peer_pg_id: peer group id on peer site
|
||||||
|
"""
|
||||||
|
dc_client = self.get_peer_dc_client(peer)
|
||||||
|
subclouds = db_api.subcloud_get_for_peer_group(context, dc_local_pg_id)
|
||||||
|
|
||||||
|
subclouds_to_sync, error_msg = self._validate_subclouds_for_sync(
|
||||||
|
subclouds, dc_client)
|
||||||
|
|
||||||
|
# Use thread pool to limit number of operations in parallel
|
||||||
|
sync_pool = greenpool.GreenPool(size=MAX_PARALLEL_SUBCLOUD_SYNC)
|
||||||
|
|
||||||
|
# Spawn threads to sync each applicable subcloud
|
||||||
|
sync_function = functools.partial(self._add_or_update_subcloud,
|
||||||
|
dc_client,
|
||||||
|
peer.peer_controller_gateway_ip,
|
||||||
|
dc_peer_pg_id)
|
||||||
|
|
||||||
|
failed_subclouds, sync_error_msg = self._run_parallel_group_operation(
|
||||||
|
'peer sync', sync_function, sync_pool, subclouds_to_sync)
|
||||||
|
|
||||||
|
error_msg.update(sync_error_msg)
|
||||||
|
LOG.info("Subcloud peer sync operation finished")
|
||||||
|
|
||||||
|
dc_local_region_names = set()
|
||||||
|
for subcloud in subclouds:
|
||||||
|
# Ignore the secondary subclouds to sync with peer site
|
||||||
|
if not self.is_subcloud_secondary(subcloud):
|
||||||
|
# Count all subcloud need to be sync
|
||||||
|
dc_local_region_names.add(subcloud.get('name'))
|
||||||
|
|
||||||
|
dc_peer_subclouds = dc_client.get_subcloud_list_by_peer_group(
|
||||||
|
str(dc_peer_pg_id))
|
||||||
|
dc_peer_region_names = set(subcloud.get('name') for subcloud in
|
||||||
|
dc_peer_subclouds)
|
||||||
|
|
||||||
|
dc_peer_subcloud_diff_names = dc_peer_region_names - \
|
||||||
|
dc_local_region_names
|
||||||
|
for subcloud_to_delete in dc_peer_subcloud_diff_names:
|
||||||
|
try:
|
||||||
|
LOG.debug(f"Deleting Subcloud name {subcloud_to_delete} "
|
||||||
|
"on peer site.")
|
||||||
|
self.delete_peer_secondary_subcloud(dc_client,
|
||||||
|
subcloud_to_delete)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Subcloud delete failed: {str(e)}"
|
||||||
|
LOG.error(msg)
|
||||||
|
error_msg[subcloud_to_delete] = msg
|
||||||
|
|
||||||
|
return error_msg
|
||||||
|
|
||||||
|
def sync_subcloud_peer_group(self, context, association_id,
|
||||||
|
sync_subclouds=True, priority=None):
|
||||||
|
"""Sync subcloud peer group to peer site.
|
||||||
|
|
||||||
|
This function synchronizes subcloud peer groups from current site
|
||||||
|
to peer site, supporting two scenarios:
|
||||||
|
|
||||||
|
1. When creating an association between the system peer and a subcloud
|
||||||
|
peer group. This function creates the subcloud peer group on the
|
||||||
|
peer site and synchronizes the subclouds to it.
|
||||||
|
|
||||||
|
2. When synchronizing a subcloud peer group with the peer site. This
|
||||||
|
function syncs both the subcloud peer group and the subclouds
|
||||||
|
under it to the peer site.
|
||||||
|
|
||||||
|
:param context: request context object
|
||||||
|
:param association_id: id of association to sync
|
||||||
|
:param sync_subclouds: Enabled to sync subclouds to peer site
|
||||||
|
"""
|
||||||
|
LOG.info(f"Synchronize the association {association_id} of the "
|
||||||
|
"Subcloud Peer Group with the System Peer pointing to the "
|
||||||
|
"peer site.")
|
||||||
|
|
||||||
|
association = db_api.peer_group_association_get(context,
|
||||||
|
association_id)
|
||||||
|
peer = db_api.system_peer_get(context, association.system_peer_id)
|
||||||
|
dc_local_pg = db_api.subcloud_peer_group_get(context,
|
||||||
|
association.peer_group_id)
|
||||||
|
peer_group_name = dc_local_pg.peer_group_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
sysinv_client = self.get_peer_sysinv_client(peer)
|
||||||
|
|
||||||
|
# Check if the system_uuid of the peer site matches with the
|
||||||
|
# peer_uuid
|
||||||
|
system = sysinv_client.get_system()
|
||||||
|
if system.uuid != peer.peer_uuid:
|
||||||
|
LOG.error(f"Peer site system uuid {system.uuid} does not match "
|
||||||
|
f"with the peer_uuid {peer.peer_uuid}")
|
||||||
|
raise exceptions.PeerGroupAssociationTargetNotMatch(
|
||||||
|
uuid=system.uuid)
|
||||||
|
|
||||||
|
dc_client = self.get_peer_dc_client(peer)
|
||||||
|
|
||||||
|
peer_group_kwargs = {
|
||||||
|
'group-priority': association.peer_group_priority,
|
||||||
|
'group-state': dc_local_pg.group_state,
|
||||||
|
'system-leader-id': dc_local_pg.system_leader_id,
|
||||||
|
'system-leader-name': dc_local_pg.system_leader_name,
|
||||||
|
'max-subcloud-rehoming': dc_local_pg.max_subcloud_rehoming
|
||||||
|
}
|
||||||
|
if priority:
|
||||||
|
peer_group_kwargs['group-priority'] = priority
|
||||||
|
try:
|
||||||
|
dc_peer_pg = dc_client.get_subcloud_peer_group(
|
||||||
|
peer_group_name)
|
||||||
|
dc_peer_pg_id = dc_peer_pg.get('id')
|
||||||
|
dc_peer_pg_priority = dc_peer_pg.get('group_priority')
|
||||||
|
if dc_peer_pg_priority == 0:
|
||||||
|
LOG.error(f"Skip update. Peer Site {peer_group_name} "
|
||||||
|
f"has priority 0.")
|
||||||
|
raise exceptions.SubcloudPeerGroupHasWrongPriority(
|
||||||
|
priority=dc_peer_pg_priority)
|
||||||
|
|
||||||
|
dc_peer_pg = dc_client.update_subcloud_peer_group(
|
||||||
|
peer_group_name, **peer_group_kwargs)
|
||||||
|
LOG.info(f"Updated Subcloud Peer Group {peer_group_name} on "
|
||||||
|
f"peer site, ID is {dc_peer_pg.get('id')}.")
|
||||||
|
except dccommon_exceptions.SubcloudPeerGroupNotFound:
|
||||||
|
peer_group_kwargs['peer-group-name'] = peer_group_name
|
||||||
|
dc_peer_pg = dc_client.add_subcloud_peer_group(
|
||||||
|
**peer_group_kwargs)
|
||||||
|
dc_peer_pg_id = dc_peer_pg.get('id')
|
||||||
|
LOG.info(f"Created Subcloud Peer Group {peer_group_name} on "
|
||||||
|
f"peer site, ID is {dc_peer_pg_id}.")
|
||||||
|
|
||||||
|
association_update = {
|
||||||
|
'sync_status': consts.ASSOCIATION_SYNC_STATUS_SYNCED,
|
||||||
|
'sync_message': None
|
||||||
|
}
|
||||||
|
if sync_subclouds:
|
||||||
|
error_msg = self._sync_subclouds(context, peer, dc_local_pg.id,
|
||||||
|
dc_peer_pg_id)
|
||||||
|
if len(error_msg) > 0:
|
||||||
|
association_update['sync_status'] = \
|
||||||
|
consts.ASSOCIATION_SYNC_STATUS_FAILED
|
||||||
|
association_update['sync_message'] = json.dumps(error_msg)
|
||||||
|
if priority:
|
||||||
|
association_update['peer_group_priority'] = priority
|
||||||
|
association = db_api.peer_group_association_update(
|
||||||
|
context, association_id, **association_update)
|
||||||
|
|
||||||
|
return db_api.peer_group_association_db_model_to_dict(association)
|
||||||
|
|
||||||
|
except Exception as exception:
|
||||||
|
LOG.exception(f"Failed to sync peer group {peer_group_name} to "
|
||||||
|
f"peer site {peer.peer_name}")
|
||||||
|
db_api.peer_group_association_update(
|
||||||
|
context, association_id,
|
||||||
|
sync_status=consts.ASSOCIATION_SYNC_STATUS_FAILED,
|
||||||
|
sync_message=str(exception))
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
def delete_peer_group_association(self, context, association_id):
|
||||||
|
"""Delete association and remove related association from peer site.
|
||||||
|
|
||||||
|
:param context: request context object.
|
||||||
|
:param association_id: id of association to delete
|
||||||
|
"""
|
||||||
|
LOG.info(f"Deleting association peer group {association_id}.")
|
||||||
|
|
||||||
|
# Retrieve the peer group association details from the database
|
||||||
|
association = db_api.peer_group_association_get(context,
|
||||||
|
association_id)
|
||||||
|
peer = db_api.system_peer_get(context, association.system_peer_id)
|
||||||
|
peer_group = db_api.subcloud_peer_group_get(context,
|
||||||
|
association.peer_group_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dc_client = self.get_peer_dc_client(peer)
|
||||||
|
dc_peer_pg = dc_client.get_subcloud_peer_group(
|
||||||
|
peer_group.peer_group_name)
|
||||||
|
dc_peer_pg_priority = dc_peer_pg.get('group_priority')
|
||||||
|
if dc_peer_pg_priority == 0:
|
||||||
|
LOG.error(f"Failed to delete peer_group_association. "
|
||||||
|
f"Peer Group {peer_group.peer_group_name} "
|
||||||
|
f"has priority 0 on peer site.")
|
||||||
|
raise exceptions.SubcloudPeerGroupHasWrongPriority(
|
||||||
|
priority=dc_peer_pg_priority)
|
||||||
|
|
||||||
|
subclouds = db_api.subcloud_get_for_peer_group(context,
|
||||||
|
peer_group.id)
|
||||||
|
for subcloud in subclouds:
|
||||||
|
self.delete_peer_secondary_subcloud(dc_client,
|
||||||
|
subcloud.name)
|
||||||
|
try:
|
||||||
|
dc_client.delete_subcloud_peer_group(peer_group.peer_group_name)
|
||||||
|
LOG.info("Deleted Subcloud Peer Group "
|
||||||
|
f"{peer_group.peer_group_name} on peer site.")
|
||||||
|
except dccommon_exceptions.SubcloudPeerGroupNotFound:
|
||||||
|
LOG.debug(f"Subcloud Peer Group {peer_group.peer_group_name} "
|
||||||
|
"does not exist on peer site.")
|
||||||
|
except dccommon_exceptions.SubcloudPeerGroupDeleteFailedAssociated:
|
||||||
|
LOG.debug(f"Subcloud Peer Group {peer_group.peer_group_name} "
|
||||||
|
"delete failed as it is associated with system peer "
|
||||||
|
"on peer site.")
|
||||||
|
|
||||||
|
db_api.peer_group_association_destroy(context, association_id)
|
||||||
|
|
||||||
|
except Exception as exception:
|
||||||
|
LOG.exception("Failed to delete peer_group_association "
|
||||||
|
f"{association.id}")
|
||||||
|
raise exception
|
|
@ -135,6 +135,11 @@ class ManagerClient(RPCClient):
|
||||||
subcloud_id=subcloud_id,
|
subcloud_id=subcloud_id,
|
||||||
payload=payload))
|
payload=payload))
|
||||||
|
|
||||||
|
def add_secondary_subcloud(self, ctxt, subcloud_id, payload):
|
||||||
|
return self.call(ctxt, self.make_msg('add_subcloud',
|
||||||
|
subcloud_id=subcloud_id,
|
||||||
|
payload=payload))
|
||||||
|
|
||||||
def delete_subcloud(self, ctxt, subcloud_id):
|
def delete_subcloud(self, ctxt, subcloud_id):
|
||||||
return self.call(ctxt, self.make_msg('delete_subcloud',
|
return self.call(ctxt, self.make_msg('delete_subcloud',
|
||||||
subcloud_id=subcloud_id))
|
subcloud_id=subcloud_id))
|
||||||
|
@ -256,6 +261,19 @@ class ManagerClient(RPCClient):
|
||||||
return self.cast(ctxt, self.make_msg('batch_migrate_subcloud',
|
return self.cast(ctxt, self.make_msg('batch_migrate_subcloud',
|
||||||
payload=payload))
|
payload=payload))
|
||||||
|
|
||||||
|
def sync_subcloud_peer_group(self, ctxt, association_id):
|
||||||
|
return self.cast(ctxt, self.make_msg(
|
||||||
|
'sync_subcloud_peer_group', association_id=association_id))
|
||||||
|
|
||||||
|
def update_subcloud_peer_group(self, ctxt, association_id, priority):
|
||||||
|
return self.call(ctxt, self.make_msg(
|
||||||
|
'sync_subcloud_peer_group', association_id=association_id,
|
||||||
|
sync_subclouds=False, priority=priority))
|
||||||
|
|
||||||
|
def delete_peer_group_association(self, ctxt, association_id):
|
||||||
|
return self.call(ctxt, self.make_msg('delete_peer_group_association',
|
||||||
|
association_id=association_id))
|
||||||
|
|
||||||
|
|
||||||
class DCManagerNotifications(RPCClient):
|
class DCManagerNotifications(RPCClient):
|
||||||
"""DC Manager Notification interface to broadcast subcloud state changed
|
"""DC Manager Notification interface to broadcast subcloud state changed
|
||||||
|
|
|
@ -0,0 +1,358 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from six.moves import http_client
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from dcmanager.db.sqlalchemy import api as db_api
|
||||||
|
from dcmanager.rpc import client as rpc_client
|
||||||
|
|
||||||
|
from dcmanager.api.controllers.v1 import peer_group_association
|
||||||
|
from dcmanager.common import phased_subcloud_deploy as psd_common
|
||||||
|
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 GetMixin
|
||||||
|
from dcmanager.tests.unit.api.v1.controllers.mixins import UpdateMixin
|
||||||
|
from dcmanager.tests import utils
|
||||||
|
|
||||||
|
# SAMPLE SYSTEM PEER DATA
|
||||||
|
SAMPLE_SYSTEM_PEER_UUID = str(uuid.uuid4())
|
||||||
|
SAMPLE_SYSTEM_PEER_NAME = 'SystemPeer1'
|
||||||
|
SAMPLE_MANAGER_ENDPOINT = 'http://127.0.0.1:5000'
|
||||||
|
SAMPLE_MANAGER_USERNAME = 'admin'
|
||||||
|
SAMPLE_MANAGER_PASSWORD = 'password'
|
||||||
|
SAMPLE_PEER_CONTROLLER_GATEWAY_IP = '128.128.128.1'
|
||||||
|
SAMPLE_ADMINISTRATIVE_STATE = 'enabled'
|
||||||
|
SAMPLE_HEARTBEAT_INTERVAL = 10
|
||||||
|
SAMPLE_HEARTBEAT_FAILURE_THRESHOLD = 3
|
||||||
|
SAMPLE_HEARTBEAT_FAILURES_POLICY = 'alarm'
|
||||||
|
SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT = 600
|
||||||
|
SAMPLE_HEARTBEAT_STATUS_ALIVE = 'alive'
|
||||||
|
|
||||||
|
# SAMPLE SUBCLOUD PEER GROUP DATA
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_NAME = 'GroupX'
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_ID = str(uuid.uuid4())
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_NAME = 'dc-local'
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_MAX_SUBCLOUDS_REHOMING = 50
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_PRIORITY = 0
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_STATE = 'enabled'
|
||||||
|
|
||||||
|
# SAMPLE PEER GROUP ASSOCIATION DATA
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_ID = 1
|
||||||
|
SAMPLE_SYSTEM_PEER_ID = 1
|
||||||
|
SAMPLE_PEER_GROUP_PRIORITY = 1
|
||||||
|
SAMPLE_PEER_GROUP_PRIORITY_UPDATED = 99
|
||||||
|
SAMPLE_SYNC_STATUS = 'synced'
|
||||||
|
SAMPLE_SYNC_MESSAGE = 'None'
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSystem(object):
|
||||||
|
def __init__(self, uuid):
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
|
class FakeKeystoneClient(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.keystone_client = mock.MagicMock()
|
||||||
|
self.session = mock.MagicMock()
|
||||||
|
self.endpoint_cache = mock.MagicMock()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSysinvClient(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.system = FakeSystem(SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_ID)
|
||||||
|
|
||||||
|
def get_system(self):
|
||||||
|
return self.system
|
||||||
|
|
||||||
|
|
||||||
|
class PeerGroupAssociationAPIMixin(APIMixin):
|
||||||
|
|
||||||
|
API_PREFIX = '/v1.0/peer-group-associations'
|
||||||
|
RESULT_KEY = 'peer_group_associations'
|
||||||
|
EXPECTED_FIELDS = ['id',
|
||||||
|
'peer-group-id',
|
||||||
|
'system-peer-id',
|
||||||
|
'peer-group-priority',
|
||||||
|
'created-at',
|
||||||
|
'updated-at']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(PeerGroupAssociationAPIMixin, self).setUp()
|
||||||
|
self.fake_rpc_client.some_method = mock.MagicMock()
|
||||||
|
|
||||||
|
def _get_test_system_peer_dict(self, **kw):
|
||||||
|
# id should not be part of the structure
|
||||||
|
system_peer = {
|
||||||
|
'peer_uuid': kw.get('peer_uuid', SAMPLE_SYSTEM_PEER_UUID),
|
||||||
|
'peer_name': kw.get('peer_name', SAMPLE_SYSTEM_PEER_NAME),
|
||||||
|
'endpoint': kw.get('manager_endpoint', SAMPLE_MANAGER_ENDPOINT),
|
||||||
|
'username': kw.get('manager_username', SAMPLE_MANAGER_USERNAME),
|
||||||
|
'password': kw.get('manager_password', SAMPLE_MANAGER_PASSWORD),
|
||||||
|
'gateway_ip': kw.get(
|
||||||
|
'peer_controller_gateway_ip', SAMPLE_PEER_CONTROLLER_GATEWAY_IP),
|
||||||
|
'administrative_state': kw.get('administrative_state',
|
||||||
|
SAMPLE_ADMINISTRATIVE_STATE),
|
||||||
|
'heartbeat_interval': kw.get('heartbeat_interval',
|
||||||
|
SAMPLE_HEARTBEAT_INTERVAL),
|
||||||
|
'heartbeat_failure_threshold': kw.get(
|
||||||
|
'heartbeat_failure_threshold', SAMPLE_HEARTBEAT_FAILURE_THRESHOLD),
|
||||||
|
'heartbeat_failure_policy': kw.get(
|
||||||
|
'heartbeat_failure_policy', SAMPLE_HEARTBEAT_FAILURES_POLICY),
|
||||||
|
'heartbeat_maintenance_timeout': kw.get(
|
||||||
|
'heartbeat_maintenance_timeout',
|
||||||
|
SAMPLE_HEARTBEAT_MAINTENANCE_TIMEOUT)
|
||||||
|
}
|
||||||
|
return system_peer
|
||||||
|
|
||||||
|
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',
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_ID),
|
||||||
|
'system_leader_name': kw.get(
|
||||||
|
'system_leader_name',
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_SYSTEM_LEADER_NAME),
|
||||||
|
'group_priority': kw.get(
|
||||||
|
'group_priority',
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_PRIORITY),
|
||||||
|
'group_state': kw.get(
|
||||||
|
'group_state',
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_STATE),
|
||||||
|
'max_subcloud_rehoming': kw.get(
|
||||||
|
'max_subcloud_rehoming',
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_MAX_SUBCLOUDS_REHOMING)
|
||||||
|
}
|
||||||
|
return group
|
||||||
|
|
||||||
|
def _get_test_peer_group_association_dict(self, **kw):
|
||||||
|
# id should not be part of the structure
|
||||||
|
association = {
|
||||||
|
'peer_group_id': kw.get('peer_group_id',
|
||||||
|
SAMPLE_SUBCLOUD_PEER_GROUP_ID),
|
||||||
|
'system_peer_id': kw.get('system_peer_id', SAMPLE_SYSTEM_PEER_ID),
|
||||||
|
'peer_group_priority': kw.get('peer_group_priority',
|
||||||
|
SAMPLE_PEER_GROUP_PRIORITY),
|
||||||
|
'sync_status': kw.get('sync_status', SAMPLE_SYNC_STATUS),
|
||||||
|
'sync_message': kw.get('sync_message', SAMPLE_SYNC_MESSAGE)
|
||||||
|
}
|
||||||
|
return association
|
||||||
|
|
||||||
|
def _post_get_test_peer_group_association(self, **kw):
|
||||||
|
post_body = self._get_test_peer_group_association_dict(**kw)
|
||||||
|
return post_body
|
||||||
|
|
||||||
|
# The following methods are required for subclasses of APIMixin
|
||||||
|
def get_api_prefix(self):
|
||||||
|
return self.API_PREFIX
|
||||||
|
|
||||||
|
def get_result_key(self):
|
||||||
|
return self.RESULT_KEY
|
||||||
|
|
||||||
|
def get_expected_api_fields(self):
|
||||||
|
return self.EXPECTED_FIELDS
|
||||||
|
|
||||||
|
def get_omitted_api_fields(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _create_db_related_objects(self, context):
|
||||||
|
system_peer_fields = self._get_test_system_peer_dict()
|
||||||
|
peer = db_api.system_peer_create(context, **system_peer_fields)
|
||||||
|
|
||||||
|
peer_group_fields = self._get_test_subcloud_peer_group_dict()
|
||||||
|
peer_group = db_api.subcloud_peer_group_create(context,
|
||||||
|
**peer_group_fields)
|
||||||
|
|
||||||
|
return peer.id, peer_group.id
|
||||||
|
|
||||||
|
def _create_db_object(self, context, **kw):
|
||||||
|
|
||||||
|
peer_id, peer_group_id = self._create_db_related_objects(context)
|
||||||
|
|
||||||
|
kw['peer_group_id'] = peer_group_id if kw.get('peer_group_id') is None \
|
||||||
|
else kw.get('peer_group_id')
|
||||||
|
kw['system_peer_id'] = peer_id if kw.get('system_peer_id') is None \
|
||||||
|
else kw.get('system_peer_id')
|
||||||
|
creation_fields = self._get_test_peer_group_association_dict(**kw)
|
||||||
|
return db_api.peer_group_association_create(context, **creation_fields)
|
||||||
|
|
||||||
|
def get_post_object(self):
|
||||||
|
return self._post_get_test_peer_group_association()
|
||||||
|
|
||||||
|
def get_update_object(self):
|
||||||
|
update_object = {
|
||||||
|
'peer_group_priority': SAMPLE_PEER_GROUP_PRIORITY_UPDATED
|
||||||
|
}
|
||||||
|
return update_object
|
||||||
|
|
||||||
|
|
||||||
|
# Combine Peer Group Association API with mixins to test post, get, update and delete
|
||||||
|
class TestPeerGroupAssociationPost(testroot.DCManagerApiTest,
|
||||||
|
PeerGroupAssociationAPIMixin):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPeerGroupAssociationPost, self).setUp()
|
||||||
|
|
||||||
|
p = mock.patch.object(rpc_client, 'ManagerClient')
|
||||||
|
self.mock_rpc_client = p.start()
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
context = utils.dummy_context()
|
||||||
|
self.context = context
|
||||||
|
peer_id, _ = self._create_db_related_objects(context)
|
||||||
|
db_api.system_peer_update(context, peer_id=peer_id,
|
||||||
|
heartbeat_status=SAMPLE_HEARTBEAT_STATUS_ALIVE)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_create_success(self):
|
||||||
|
self.mock_rpc_client().sync_subcloud_peer_group.return_value = True
|
||||||
|
|
||||||
|
ndict = self.get_post_object()
|
||||||
|
response = self.app.post_json(self.get_api_prefix(),
|
||||||
|
ndict,
|
||||||
|
headers=self.get_api_headers())
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
|
||||||
|
def test_create_with_string_id_fails(self):
|
||||||
|
# A string system peer id is not permitted.
|
||||||
|
ndict = self.get_post_object()
|
||||||
|
ndict['system_peer_id'] = 'test-system-peer-id'
|
||||||
|
response = self.app.post_json(self.get_api_prefix(),
|
||||||
|
ndict,
|
||||||
|
headers=self.get_api_headers(),
|
||||||
|
expect_errors=True)
|
||||||
|
self.verify_post_failure(response)
|
||||||
|
|
||||||
|
def test_create_with_blank_id_fails(self):
|
||||||
|
# An empty system_peer_id is not permitted
|
||||||
|
ndict = self.get_post_object()
|
||||||
|
ndict['system_peer_id'] = ''
|
||||||
|
response = self.app.post_json(self.get_api_prefix(),
|
||||||
|
ndict,
|
||||||
|
headers=self.get_api_headers(),
|
||||||
|
expect_errors=True)
|
||||||
|
self.verify_post_failure(response)
|
||||||
|
|
||||||
|
def test_create_with_wrong_peer_group_priority_fails(self):
|
||||||
|
# A string peer group priority is not permitted.
|
||||||
|
ndict = self.get_post_object()
|
||||||
|
ndict['peer_group_id'] = 'peer-group-id'
|
||||||
|
response = self.app.post_json(self.get_api_prefix(),
|
||||||
|
ndict,
|
||||||
|
headers=self.get_api_headers(),
|
||||||
|
expect_errors=True)
|
||||||
|
self.verify_post_failure(response)
|
||||||
|
|
||||||
|
def test_create_with_bad_peer_group_priority(self):
|
||||||
|
# peer_group_priority must be an integer between 1 and 65536
|
||||||
|
ndict = self.get_post_object()
|
||||||
|
# All the entries in bad_values should be considered invalid
|
||||||
|
bad_values = [0, 65537, -2, 'abc']
|
||||||
|
for bad_value in bad_values:
|
||||||
|
ndict['peer_group_priority'] = bad_value
|
||||||
|
response = self.app.post_json(self.get_api_prefix(),
|
||||||
|
ndict,
|
||||||
|
headers=self.get_api_headers(),
|
||||||
|
expect_errors=True)
|
||||||
|
self.verify_post_failure(response)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPeerGroupAssociationGet(testroot.DCManagerApiTest,
|
||||||
|
PeerGroupAssociationAPIMixin,
|
||||||
|
GetMixin):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPeerGroupAssociationGet, self).setUp()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPeerGroupAssociationUpdate(testroot.DCManagerApiTest,
|
||||||
|
PeerGroupAssociationAPIMixin,
|
||||||
|
UpdateMixin):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPeerGroupAssociationUpdate, self).setUp()
|
||||||
|
|
||||||
|
def validate_updated_fields(self, sub_dict, full_obj):
|
||||||
|
for key, value in sub_dict.items():
|
||||||
|
key = key.replace('_', '-')
|
||||||
|
self.assertEqual(value, full_obj.get(key))
|
||||||
|
|
||||||
|
@mock.patch.object(rpc_client, 'ManagerClient')
|
||||||
|
def test_update_success(self, mock_client):
|
||||||
|
mock_client().update_subcloud_peer_group.return_value = {
|
||||||
|
'peer-group-priority': SAMPLE_PEER_GROUP_PRIORITY_UPDATED
|
||||||
|
}
|
||||||
|
context = utils.dummy_context()
|
||||||
|
single_obj = self._create_db_object(context)
|
||||||
|
update_data = self.get_update_object()
|
||||||
|
response = self.app.patch_json(self.get_single_url(single_obj.id),
|
||||||
|
headers=self.get_api_headers(),
|
||||||
|
params=update_data)
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
self.assertEqual(response.status_code, http_client.OK)
|
||||||
|
self.validate_updated_fields(update_data, response.json)
|
||||||
|
|
||||||
|
@mock.patch.object(psd_common, 'OpenStackDriver')
|
||||||
|
@mock.patch.object(peer_group_association, 'SysinvClient')
|
||||||
|
@mock.patch.object(rpc_client, 'ManagerClient')
|
||||||
|
def test_sync_association(self, mock_client, mock_sysinv_client, mock_keystone_client):
|
||||||
|
mock_client().sync_subcloud_peer_group.return_value = True
|
||||||
|
mock_keystone_client().keystone_client = FakeKeystoneClient()
|
||||||
|
mock_sysinv_client.return_value = FakeSysinvClient()
|
||||||
|
|
||||||
|
context = utils.dummy_context()
|
||||||
|
single_obj = self._create_db_object(context)
|
||||||
|
response = self.app.patch_json(
|
||||||
|
self.get_single_url(single_obj.id) + '/sync',
|
||||||
|
headers=self.get_api_headers())
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
self.assertEqual(response.status_code, http_client.OK)
|
||||||
|
mock_client().sync_subcloud_peer_group.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPeerGroupAssociationDelete(testroot.DCManagerApiTest,
|
||||||
|
PeerGroupAssociationAPIMixin):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPeerGroupAssociationDelete, self).setUp()
|
||||||
|
|
||||||
|
p = mock.patch.object(rpc_client, 'ManagerClient')
|
||||||
|
self.mock_rpc_client = p.start()
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
self.mock_rpc_client().delete_peer_group_association.return_value = True
|
||||||
|
|
||||||
|
def test_delete_success(self):
|
||||||
|
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())
|
||||||
|
self.mock_rpc_client().delete_peer_group_association. \
|
||||||
|
assert_called_once()
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
self.assertEqual(response.status_code, http_client.OK)
|
||||||
|
|
||||||
|
def test_double_delete(self):
|
||||||
|
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())
|
||||||
|
self.mock_rpc_client().delete_peer_group_association. \
|
||||||
|
assert_called_once()
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
self.assertEqual(response.status_code, http_client.OK)
|
||||||
|
|
||||||
|
db_api.peer_group_association_destroy(context, single_obj.id)
|
||||||
|
# delete the same object a second time. this should fail (NOT_FOUND)
|
||||||
|
response = self.app.delete(self.get_single_url(single_obj.id),
|
||||||
|
headers=self.get_api_headers(),
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(response.content_type, 'text/plain')
|
||||||
|
self.assertEqual(response.status_code, http_client.NOT_FOUND)
|
|
@ -0,0 +1,389 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
import json
|
||||||
|
import mock
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from dccommon import exceptions as dccommon_exceptions
|
||||||
|
from dcmanager.db.sqlalchemy import api as db_api
|
||||||
|
from dcmanager.manager import system_peer_manager
|
||||||
|
from dcmanager.tests import base
|
||||||
|
from dcmanager.tests.unit.common import fake_subcloud
|
||||||
|
|
||||||
|
# FAKE SYSINV DATA
|
||||||
|
FAKE_SITE0_SYSTEM_UUID = str(uuid.uuid4())
|
||||||
|
FAKE_SITE1_SYSTEM_UUID = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# FAKE SYSTEM PEER DATA
|
||||||
|
FAKE_SYSTEM_PEER_ID = 1
|
||||||
|
FAKE_SYSTEM_PEER_UUID = FAKE_SITE1_SYSTEM_UUID
|
||||||
|
FAKE_SYSTEM_PEER_NAME = 'PeerSite1'
|
||||||
|
FAKE_MANAGER_ENDPOINT = 'http://128.128.128.128:5000/v3'
|
||||||
|
FAKE_MANAGER_USERNAME = 'admin'
|
||||||
|
FAKE_MANAGER_PASSWORD = 'cGFzc3dvcmQ='
|
||||||
|
FAKE_PEER_CONTROLLER_GATEWAY_IP = '128.128.1.1'
|
||||||
|
|
||||||
|
# FAKE SUBCLOUD PEER GROUP DATA (SITE0)
|
||||||
|
FAKE_SITE0_PEER_GROUP_ID = 1
|
||||||
|
FAKE_SITE0_PEER_GROUP_NAME = 'PeerGroup1'
|
||||||
|
FAKE_SITE0_PEER_GROUP_SYSTEM_LEADER_ID = FAKE_SITE0_SYSTEM_UUID
|
||||||
|
FAKE_SITE0_PEER_GROUP_SYSTEM_LEADER_NAME = 'site0'
|
||||||
|
FAKE_SITE0_PEER_GROUP_MAX_SUBCLOUDS_REHOMING = 50
|
||||||
|
FAKE_SITE0_PEER_GROUP_PRIORITY = 0
|
||||||
|
FAKE_SITE0_PEER_GROUP_STATE = 'enabled'
|
||||||
|
|
||||||
|
# FAKE SUBCLOUD PEER GROUP DATA (SITE1)
|
||||||
|
FAKE_SITE1_PEER_GROUP_ID = 9
|
||||||
|
|
||||||
|
# FAKE SUBCLOUD DATA (SITE1)
|
||||||
|
FAKE_SITE1_SUBCLOUD1_ID = 11
|
||||||
|
FAKE_SITE1_SUBCLOUD1_DEPLOY_STATUS = 'secondary'
|
||||||
|
FAKE_SITE1_SUBCLOUD1_DATA = {"id": FAKE_SITE1_SUBCLOUD1_ID,
|
||||||
|
"name": "subcloud1",
|
||||||
|
"region-name": "subcloud1",
|
||||||
|
"deploy-status":
|
||||||
|
FAKE_SITE1_SUBCLOUD1_DEPLOY_STATUS}
|
||||||
|
FAKE_SITE1_SUBCLOUD2_ID = 12
|
||||||
|
FAKE_SITE1_SUBCLOUD2_DEPLOY_STATUS = 'secondary-failed'
|
||||||
|
FAKE_SITE1_SUBCLOUD2_DATA = {"id": FAKE_SITE1_SUBCLOUD2_ID,
|
||||||
|
"name": "subcloud2",
|
||||||
|
"region-name": "subcloud2",
|
||||||
|
"deploy-status":
|
||||||
|
FAKE_SITE1_SUBCLOUD2_DEPLOY_STATUS}
|
||||||
|
FAKE_SITE1_SUBCLOUD3_ID = 13
|
||||||
|
FAKE_SITE1_SUBCLOUD3_DEPLOY_STATUS = 'secondary'
|
||||||
|
FAKE_SITE1_SUBCLOUD3_DATA = {"id": FAKE_SITE1_SUBCLOUD3_ID,
|
||||||
|
"name": "subcloud3",
|
||||||
|
"region-name": "subcloud3",
|
||||||
|
"deploy-status":
|
||||||
|
FAKE_SITE1_SUBCLOUD3_DEPLOY_STATUS}
|
||||||
|
|
||||||
|
# FAKE PEER GROUP ASSOCIATION DATA
|
||||||
|
FAKE_ASSOCIATION_PEER_GROUP_ID = \
|
||||||
|
FAKE_SITE0_PEER_GROUP_ID
|
||||||
|
FAKE_ASSOCIATION_SYSTEM_PEER_ID = \
|
||||||
|
FAKE_SYSTEM_PEER_ID
|
||||||
|
FAKE_ASSOCIATION_PEER_GROUP_PRIORITY = 1
|
||||||
|
FAKE_ASSOCIATION_SYNC_STATUS = 'synced'
|
||||||
|
FAKE_ASSOCIATION_SYNC_MESSAGE = 'None'
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDCManagerAuditAPI(object):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSystem(object):
|
||||||
|
def __init__(self, uuid):
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
|
class FakePeerGroup(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.id = FAKE_SITE1_PEER_GROUP_ID
|
||||||
|
|
||||||
|
|
||||||
|
class FakeKeystoneClient(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.keystone_client = mock.MagicMock()
|
||||||
|
self.session = mock.MagicMock()
|
||||||
|
self.endpoint_cache = mock.MagicMock()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSysinvClient(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.system = FakeSystem(FAKE_SITE1_SYSTEM_UUID)
|
||||||
|
|
||||||
|
def get_system(self):
|
||||||
|
return self.system
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDcmanagerClient(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.peer_groups = [FakePeerGroup()]
|
||||||
|
|
||||||
|
def add_subcloud_peer_group(self, **kwargs):
|
||||||
|
return self.peer_groups
|
||||||
|
|
||||||
|
def get_subcloud_peer_group(self, peer_group_name):
|
||||||
|
return self.peer_groups
|
||||||
|
|
||||||
|
|
||||||
|
class FakeException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemPeerManager(base.DCManagerTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSystemPeerManager, self).setUp()
|
||||||
|
|
||||||
|
# Mock the DCManager Audit API
|
||||||
|
self.fake_dcmanager_audit_api = FakeDCManagerAuditAPI()
|
||||||
|
p = mock.patch('dcmanager.audit.rpcapi.ManagerAuditClient')
|
||||||
|
self.mock_dcmanager_audit_api = p.start()
|
||||||
|
self.mock_dcmanager_audit_api.return_value = \
|
||||||
|
self.fake_dcmanager_audit_api
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_subcloud_with_pg_static(ctxt, peer_group_id,
|
||||||
|
rehome_data=None, **kwargs):
|
||||||
|
subcloud = fake_subcloud.create_fake_subcloud(ctxt, **kwargs)
|
||||||
|
return db_api.subcloud_update(ctxt, subcloud.id,
|
||||||
|
peer_group_id=peer_group_id,
|
||||||
|
rehome_data=rehome_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_system_peer_static(ctxt, **kwargs):
|
||||||
|
values = {
|
||||||
|
'peer_uuid': FAKE_SYSTEM_PEER_UUID,
|
||||||
|
'peer_name': FAKE_SYSTEM_PEER_NAME,
|
||||||
|
'endpoint': FAKE_MANAGER_ENDPOINT,
|
||||||
|
'username': FAKE_MANAGER_USERNAME,
|
||||||
|
'password': FAKE_MANAGER_PASSWORD,
|
||||||
|
'gateway_ip': FAKE_PEER_CONTROLLER_GATEWAY_IP
|
||||||
|
}
|
||||||
|
values.update(kwargs)
|
||||||
|
return db_api.system_peer_create(ctxt, **values)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_subcloud_peer_group_static(ctxt, **kwargs):
|
||||||
|
values = {
|
||||||
|
"peer_group_name": FAKE_SITE0_PEER_GROUP_NAME,
|
||||||
|
"system_leader_id": FAKE_SITE0_PEER_GROUP_SYSTEM_LEADER_ID,
|
||||||
|
"system_leader_name": FAKE_SITE0_PEER_GROUP_SYSTEM_LEADER_NAME,
|
||||||
|
"group_priority": FAKE_SITE0_PEER_GROUP_PRIORITY,
|
||||||
|
"group_state": FAKE_SITE0_PEER_GROUP_STATE,
|
||||||
|
"max_subcloud_rehoming":
|
||||||
|
FAKE_SITE0_PEER_GROUP_MAX_SUBCLOUDS_REHOMING
|
||||||
|
}
|
||||||
|
values.update(kwargs)
|
||||||
|
return db_api.subcloud_peer_group_create(ctxt, **values)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_peer_group_association_static(ctxt, **kwargs):
|
||||||
|
values = {
|
||||||
|
"system_peer_id": FAKE_ASSOCIATION_SYSTEM_PEER_ID,
|
||||||
|
"peer_group_id": FAKE_ASSOCIATION_PEER_GROUP_ID,
|
||||||
|
"peer_group_priority": FAKE_ASSOCIATION_PEER_GROUP_PRIORITY,
|
||||||
|
"sync_status": FAKE_ASSOCIATION_SYNC_STATUS,
|
||||||
|
"sync_message": FAKE_ASSOCIATION_SYNC_MESSAGE
|
||||||
|
}
|
||||||
|
values.update(kwargs)
|
||||||
|
return db_api.peer_group_association_create(ctxt, **values)
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
spm = system_peer_manager.SystemPeerManager()
|
||||||
|
self.assertIsNotNone(spm)
|
||||||
|
self.assertEqual('system_peer_manager', spm.service_name)
|
||||||
|
self.assertEqual('localhost', spm.host)
|
||||||
|
|
||||||
|
@mock.patch.object(system_peer_manager, 'PeerSiteDriver')
|
||||||
|
@mock.patch.object(system_peer_manager, 'SysinvClient')
|
||||||
|
@mock.patch.object(system_peer_manager, 'DcmanagerClient')
|
||||||
|
def test_sync_subclouds(self, mock_dc_client,
|
||||||
|
mock_sysinv_client,
|
||||||
|
mock_keystone_client):
|
||||||
|
mock_keystone_client().keystone_client = FakeKeystoneClient()
|
||||||
|
mock_sysinv_client.return_value = FakeSysinvClient()
|
||||||
|
mock_dc_client.return_value = FakeDcmanagerClient()
|
||||||
|
mock_dc_client().add_subcloud_with_secondary_status = mock.MagicMock()
|
||||||
|
mock_dc_client().delete_subcloud = mock.MagicMock()
|
||||||
|
|
||||||
|
peer = self.create_system_peer_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_name='SystemPeer1')
|
||||||
|
peer_group = self.create_subcloud_peer_group_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_name='SubcloudPeerGroup1')
|
||||||
|
rehome_data = {
|
||||||
|
"saved_payload": {
|
||||||
|
"bootstrap-address": "192.168.10.10",
|
||||||
|
"systemcontroller_gateway_address": "192.168.204.101"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Create local dc subcloud1 mock data in database
|
||||||
|
self.create_subcloud_with_pg_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_id=peer_group.id,
|
||||||
|
rehome_data=json.dumps(rehome_data),
|
||||||
|
name='subcloud1',
|
||||||
|
region_name='subcloud1')
|
||||||
|
# Create local dc subcloud2 mock data in database
|
||||||
|
self.create_subcloud_with_pg_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_id=peer_group.id,
|
||||||
|
rehome_data=json.dumps(rehome_data),
|
||||||
|
name='subcloud2',
|
||||||
|
region_name='subcloud2')
|
||||||
|
peer_subcloud1 = FAKE_SITE1_SUBCLOUD1_DATA
|
||||||
|
peer_subcloud2 = FAKE_SITE1_SUBCLOUD2_DATA
|
||||||
|
peer_subcloud3 = FAKE_SITE1_SUBCLOUD3_DATA
|
||||||
|
mock_dc_client().get_subcloud = mock.MagicMock()
|
||||||
|
mock_dc_client().get_subcloud.side_effect = [
|
||||||
|
peer_subcloud1, dccommon_exceptions.SubcloudNotFound,
|
||||||
|
peer_subcloud1, dccommon_exceptions.SubcloudNotFound,
|
||||||
|
peer_subcloud3]
|
||||||
|
mock_dc_client().get_subcloud_list_by_peer_group = mock.MagicMock()
|
||||||
|
mock_dc_client().get_subcloud_list_by_peer_group.return_value = [
|
||||||
|
peer_subcloud1, peer_subcloud2, peer_subcloud3]
|
||||||
|
mock_dc_client().update_subcloud = mock.MagicMock()
|
||||||
|
mock_dc_client().update_subcloud.side_effect = [
|
||||||
|
peer_subcloud1, peer_subcloud1, peer_subcloud2]
|
||||||
|
|
||||||
|
spm = system_peer_manager.SystemPeerManager()
|
||||||
|
spm._sync_subclouds(self.ctx, peer, peer_group.id,
|
||||||
|
FAKE_SITE1_PEER_GROUP_ID)
|
||||||
|
|
||||||
|
mock_dc_client().get_subcloud.assert_has_calls([
|
||||||
|
mock.call(peer_subcloud1.get('name')),
|
||||||
|
mock.call(peer_subcloud2.get('name')),
|
||||||
|
mock.call(peer_subcloud3.get('name'))
|
||||||
|
])
|
||||||
|
mock_dc_client().update_subcloud.assert_has_calls([
|
||||||
|
mock.call('subcloud1', mock.ANY, mock.ANY),
|
||||||
|
mock.call('subcloud1', files=None,
|
||||||
|
data={'peer_group': str(FAKE_SITE1_PEER_GROUP_ID)})
|
||||||
|
])
|
||||||
|
mock_dc_client().add_subcloud_with_secondary_status. \
|
||||||
|
assert_called_once()
|
||||||
|
mock_dc_client().delete_subcloud.assert_called_once_with('subcloud3')
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
system_peer_manager.SystemPeerManager, '_sync_subclouds')
|
||||||
|
@mock.patch.object(system_peer_manager, 'PeerSiteDriver')
|
||||||
|
@mock.patch.object(system_peer_manager, 'SysinvClient')
|
||||||
|
@mock.patch.object(system_peer_manager, 'DcmanagerClient')
|
||||||
|
def test_sync_subcloud_peer_group(self,
|
||||||
|
mock_dc_client,
|
||||||
|
mock_sysinv_client,
|
||||||
|
mock_keystone_client,
|
||||||
|
mock_sync_subclouds):
|
||||||
|
mock_sync_subclouds.return_value = True
|
||||||
|
mock_keystone_client().keystone_client = FakeKeystoneClient()
|
||||||
|
mock_sysinv_client.return_value = FakeSysinvClient()
|
||||||
|
mock_dc_client.return_value = FakeDcmanagerClient()
|
||||||
|
mock_dc_client().get_subcloud_peer_group = mock.MagicMock()
|
||||||
|
mock_dc_client().update_subcloud_peer_group = mock.MagicMock()
|
||||||
|
|
||||||
|
peer = self.create_system_peer_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_name='SystemPeer1')
|
||||||
|
peer_group = self.create_subcloud_peer_group_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_name='SubcloudPeerGroup1')
|
||||||
|
association = self.create_peer_group_association_static(
|
||||||
|
self.ctx,
|
||||||
|
system_peer_id=peer.id,
|
||||||
|
peer_group_id=peer_group.id)
|
||||||
|
|
||||||
|
spm = system_peer_manager.SystemPeerManager()
|
||||||
|
spm.sync_subcloud_peer_group(self.ctx, association.id, False)
|
||||||
|
|
||||||
|
mock_dc_client().get_subcloud_peer_group.assert_called_once_with(
|
||||||
|
peer_group.peer_group_name)
|
||||||
|
mock_dc_client().update_subcloud_peer_group.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
system_peer_manager.SystemPeerManager, '_sync_subclouds')
|
||||||
|
@mock.patch.object(system_peer_manager, 'PeerSiteDriver')
|
||||||
|
@mock.patch.object(system_peer_manager, 'SysinvClient')
|
||||||
|
@mock.patch.object(system_peer_manager, 'DcmanagerClient')
|
||||||
|
def test_sync_subcloud_peer_group_not_exist(self, mock_dc_client,
|
||||||
|
mock_sysinv_client,
|
||||||
|
mock_keystone_client,
|
||||||
|
mock_sync_subclouds):
|
||||||
|
mock_sync_subclouds.return_value = True
|
||||||
|
mock_keystone_client().keystone_client = FakeKeystoneClient()
|
||||||
|
mock_sysinv_client.return_value = FakeSysinvClient()
|
||||||
|
mock_dc_client.return_value = FakeDcmanagerClient()
|
||||||
|
mock_dc_client().get_subcloud_peer_group = mock.MagicMock()
|
||||||
|
mock_dc_client().add_subcloud_peer_group = mock.MagicMock()
|
||||||
|
mock_dc_client().update_subcloud_peer_group = mock.MagicMock()
|
||||||
|
|
||||||
|
peer = self.create_system_peer_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_name='SystemPeer1')
|
||||||
|
peer_group = self.create_subcloud_peer_group_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_name='SubcloudPeerGroup1')
|
||||||
|
association = self.create_peer_group_association_static(
|
||||||
|
self.ctx,
|
||||||
|
system_peer_id=peer.id,
|
||||||
|
peer_group_id=peer_group.id)
|
||||||
|
|
||||||
|
mock_dc_client().get_subcloud_peer_group.side_effect = \
|
||||||
|
dccommon_exceptions.SubcloudPeerGroupNotFound
|
||||||
|
|
||||||
|
spm = system_peer_manager.SystemPeerManager()
|
||||||
|
spm.sync_subcloud_peer_group(self.ctx, association.id, False)
|
||||||
|
|
||||||
|
mock_dc_client().get_subcloud_peer_group.assert_called_once_with(
|
||||||
|
peer_group.peer_group_name)
|
||||||
|
mock_dc_client().add_subcloud_peer_group.assert_called_once_with(**{
|
||||||
|
'peer-group-name': peer_group.peer_group_name,
|
||||||
|
'group-priority': association.peer_group_priority,
|
||||||
|
'group-state': peer_group.group_state,
|
||||||
|
'system-leader-id': peer_group.system_leader_id,
|
||||||
|
'system-leader-name': peer_group.system_leader_name,
|
||||||
|
'max-subcloud-rehoming': peer_group.max_subcloud_rehoming
|
||||||
|
})
|
||||||
|
mock_dc_client().update_subcloud_peer_group.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch.object(system_peer_manager, 'PeerSiteDriver')
|
||||||
|
@mock.patch.object(system_peer_manager, 'DcmanagerClient')
|
||||||
|
def test_delete_peer_group_association(self,
|
||||||
|
mock_dc_client,
|
||||||
|
mock_keystone_client):
|
||||||
|
mock_keystone_client().keystone_client = FakeKeystoneClient()
|
||||||
|
mock_dc_client.return_value = FakeDcmanagerClient()
|
||||||
|
mock_dc_client().delete_subcloud_peer_group = mock.MagicMock()
|
||||||
|
mock_dc_client().delete_subcloud = mock.MagicMock()
|
||||||
|
|
||||||
|
peer = self.create_system_peer_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_name='SystemPeer1')
|
||||||
|
peer_group = self.create_subcloud_peer_group_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_name='SubcloudPeerGroup1')
|
||||||
|
# Create local dc subcloud1 mock data in database
|
||||||
|
subcloud1 = self.create_subcloud_with_pg_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_id=peer_group.id,
|
||||||
|
name='subcloud1')
|
||||||
|
# Create local dc subcloud2 mock data in database
|
||||||
|
subcloud2 = self.create_subcloud_with_pg_static(
|
||||||
|
self.ctx,
|
||||||
|
peer_group_id=peer_group.id,
|
||||||
|
name='subcloud2')
|
||||||
|
association = self.create_peer_group_association_static(
|
||||||
|
self.ctx,
|
||||||
|
system_peer_id=peer.id,
|
||||||
|
peer_group_id=peer_group.id)
|
||||||
|
peer_subcloud1 = FAKE_SITE1_SUBCLOUD1_DATA
|
||||||
|
peer_subcloud2 = FAKE_SITE1_SUBCLOUD2_DATA
|
||||||
|
mock_dc_client().get_subcloud = mock.MagicMock()
|
||||||
|
mock_dc_client().get_subcloud.side_effect = [
|
||||||
|
peer_subcloud1, peer_subcloud2]
|
||||||
|
|
||||||
|
mock_dc_client().get_subcloud_peer_group = mock.MagicMock()
|
||||||
|
mock_dc_client().get_subcloud_peer_group.return_value = {
|
||||||
|
'group_priority': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
spm = system_peer_manager.SystemPeerManager()
|
||||||
|
spm.delete_peer_group_association(self.ctx, association.id)
|
||||||
|
|
||||||
|
mock_dc_client().delete_subcloud.assert_has_calls([
|
||||||
|
mock.call(subcloud1.name),
|
||||||
|
mock.call(subcloud2.name)
|
||||||
|
])
|
||||||
|
mock_dc_client().delete_subcloud_peer_group.assert_called_once_with(
|
||||||
|
peer_group.peer_group_name)
|
||||||
|
|
||||||
|
associations = db_api.peer_group_association_get_all(self.ctx)
|
||||||
|
self.assertEqual(0, len(associations))
|
Loading…
Reference in New Issue