368 lines
15 KiB
Python
368 lines
15 KiB
Python
#
|
|
# Copyright (c) 2023-2024 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
import uuid
|
|
|
|
import mock
|
|
|
|
from six.moves import http_client
|
|
|
|
from dcmanager.db.sqlalchemy import api as db_api
|
|
from dcmanager.rpc import client as rpc_client
|
|
|
|
from dcmanager.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_AVAILABILITY_STATE_AVAILABLE = 'available'
|
|
|
|
# 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'
|
|
SAMPLE_ASSOCIATION_TYPE = 'primary'
|
|
|
|
|
|
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),
|
|
'migration_status': None
|
|
}
|
|
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),
|
|
'association_type': kw.get('association_type',
|
|
SAMPLE_ASSOCIATION_TYPE)
|
|
}
|
|
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,
|
|
availability_state=SAMPLE_AVAILABILITY_STATE_AVAILABLE)
|
|
|
|
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().sync_subcloud_peer_group_only.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)
|