Files
distcloud/distributedcloud/dcmanager/api/controllers/v1/system_peers.py
Zhang Rong(Jon) e422731760 Fix unable to determine the SPG sync state if one site is down
If Site1 (the local site) is down while setting up the protection
group, the subcloud peer group sync state is unable to determine.
This commit will automatically create the non-primary association on
Site2 (the peer site) when creating a primary association, and
update the sync state to the non-primary association. Then the
operator can check the sync state on Site2 if Site1 is down.

Test Plan:
- PASS: Create a primary association and check the non-primary
        association on peer site. It was created, and sync_status
        will follow the primary association's sync_status.
- PASS: Delete the primary association and check the non-primary
        association on peer site. It was deleted.
- PASS: If you restart the "dcmanager-manager service" in the local
        site while the association sync_status is in "syncing", the
        sync_status will transition to "failed".
- PASS: Create a primary association and wait for the sync_status
        change to "in-sync". Delete the subcloud peer group on peer
        site, the deletion will fail because it is associating to
        the non-primary association.

Closes-Bug: 2046809

Change-Id: Ia917d0dc7c65fbea1e222fb52dbec79fdbe65b65
Signed-off-by: Zhang Rong(Jon) <rong.zhang@windriver.com>
2024-01-02 16:39:08 +08:00

499 lines
20 KiB
Python

# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import http.client as httpclient
import json
import uuid
import ipaddress
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_messaging import RemoteError
import pecan
from pecan import expose
from pecan import request
from dcmanager.api.controllers import restcomm
from dcmanager.api.policies import system_peers as system_peer_policy
from dcmanager.api import policy
from dcmanager.common.i18n import _
from dcmanager.common import utils
from dcmanager.db import api as db_api
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
# validation constants for System Peer
MAX_SYSTEM_PEER_NAME_LEN = 255
MAX_SYSTEM_PEER_MANAGER_ENDPOINT_LEN = 255
MAX_SYSTEM_PEER_MANAGER_USERNAME_LEN = 255
MAX_SYSTEM_PEER_MANAGER_PASSWORD_LEN = 255
MAX_SYSTEM_PEER_STRING_DEFAULT_LEN = 255
# validation constants for System Peer Administrative State
# Set to disabled this function will be disabled
#
# We will not support this function in the first release
SYSTEM_PEER_ADMINISTRATIVE_STATE_LIST = ["enabled", "disabled"]
MIN_SYSTEM_PEER_HEARTBEAT_INTERVAL = 10
MAX_SYSTEM_PEER_HEARTBEAT_INTERVAL = 600
MIN_SYSTEM_PEER_HEARTBEAT_FAILURE_THRESHOLD = 1
MAX_SYSTEM_PEER_HEARTBEAT_FAILURE_THRESHOLD = 30
# validation constants for System Peer Heartbeat Failure Policy
# Set to alarm this function will be triggered alarm when the
# heartbeat failure threshold is reached
# Set to rehome this function will be automatically rehome the
# subcloud when the heartbeat failure threshold is reached
# Set to delegate this function will be delegate the system when
# the heartbeat failure threshold is reached
#
# We will only support alarm in the first release
SYSTEM_PEER_HEARTBEAT_FAILURE_POLICY_LIST = \
["alarm", "rehome", "delegate"]
MIN_SYSTEM_PEER_HEARTBEAT_MAINTENACE_TIMEOUT = 300
MAX_SYSTEM_PEER_HEARTBEAT_MAINTENACE_TIMEOUT = 36000
class SystemPeersController(restcomm.GenericPathController):
def __init__(self):
super(SystemPeersController, self).__init__()
@expose(generic=True, template='json')
def index(self):
# Route the request to specific methods with parameters
pass
@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 _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):
peers = db_api.system_peer_get_all(context)
system_peer_list = list()
for peer in peers:
peer_dict = db_api.system_peer_db_model_to_dict(peer)
system_peer_list.append(peer_dict)
result = dict()
result['system_peers'] = system_peer_list
return result
@index.when(method='GET', template='json')
def get(self, peer_ref=None, subcloud_peer_groups=False):
"""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 subcloud_peer_groups: If this request should return subcloud
peer groups
"""
policy.authorize(system_peer_policy.POLICY_ROOT % "get", {},
restcomm.extract_credentials_for_policy())
context = restcomm.extract_context_from_environ()
if peer_ref is None:
# List of system peers requested
return self._get_system_peer_list(context)
peer = utils.system_peer_get_by_ref(context, peer_ref)
if peer is None:
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)
return system_peer_dict
def _validate_uuid(self, _uuid):
try:
uuid.UUID(str(_uuid))
return True
except ValueError:
LOG.exception("Invalid UUID: %s" % _uuid)
return False
def _validate_name(self, name):
if not name or name.isdigit() or len(name) >= MAX_SYSTEM_PEER_NAME_LEN:
LOG.debug("Invalid name: %s" % name)
return False
return True
def _validate_manager_endpoint(self, endpoint):
if not endpoint or len(endpoint) >= MAX_SYSTEM_PEER_MANAGER_ENDPOINT_LEN or \
not endpoint.startswith(("http", "https")):
LOG.debug("Invalid manager_endpoint: %s" % endpoint)
return False
return True
def _validate_manager_username(self, username):
if not username or len(username) >= MAX_SYSTEM_PEER_MANAGER_USERNAME_LEN:
LOG.debug("Invalid manager_username: %s" % username)
return False
return True
def _validate_manager_password(self, password):
if not password or len(password) >= MAX_SYSTEM_PEER_MANAGER_PASSWORD_LEN:
LOG.debug("Invalid manager_password: %s" % password)
return False
return True
def _validate_peer_controller_gateway_ip(self, ip):
if not ip or len(ip) >= MAX_SYSTEM_PEER_STRING_DEFAULT_LEN:
LOG.debug("Invalid peer_manager_gateway_address: %s" % ip)
return False
try:
ipaddress.ip_address(ip)
return True
except Exception:
LOG.warning("Invalid IP address: %s" % ip)
return False
def _validate_administrative_state(self, administrative_state):
if administrative_state not in SYSTEM_PEER_ADMINISTRATIVE_STATE_LIST:
LOG.debug("Invalid administrative_state: %s" % administrative_state)
return False
return True
def _validate_heartbeat_interval(self, heartbeat_interval):
try:
# Check the value is an integer
val = int(heartbeat_interval)
except ValueError:
LOG.warning("Invalid heartbeat_interval: %s" % heartbeat_interval)
return False
# We do not support less than min or greater than max
if val < MIN_SYSTEM_PEER_HEARTBEAT_INTERVAL or \
val > MAX_SYSTEM_PEER_HEARTBEAT_INTERVAL:
LOG.debug("Invalid heartbeat_interval: %s" % heartbeat_interval)
return False
return True
def _validate_heartbeat_failure_threshold(self,
heartbeat_failure_threshold):
try:
# Check the value is an integer
val = int(heartbeat_failure_threshold)
except ValueError:
LOG.warning("Invalid heartbeat_failure_threshold: %s" %
heartbeat_failure_threshold)
return False
# We do not support less than min or greater than max
if val < MIN_SYSTEM_PEER_HEARTBEAT_FAILURE_THRESHOLD or \
val > MAX_SYSTEM_PEER_HEARTBEAT_FAILURE_THRESHOLD:
LOG.debug("Invalid heartbeat_failure_threshold: %s" %
heartbeat_failure_threshold)
return False
return True
def _validate_heartbeat_failure_policy(self, heartbeat_failure_policy):
if heartbeat_failure_policy not in \
SYSTEM_PEER_HEARTBEAT_FAILURE_POLICY_LIST:
LOG.debug("Invalid heartbeat_failure_policy: %s" %
heartbeat_failure_policy)
return False
return True
def _validate_heartbeat_maintenance_timeout(self,
heartbeat_maintenance_timeout):
try:
# Check the value is an integer
val = int(heartbeat_maintenance_timeout)
except ValueError:
LOG.warning("Invalid heartbeat_maintenance_timeout: %s" %
heartbeat_maintenance_timeout)
return False
# We do not support less than min or greater than max
if val < MIN_SYSTEM_PEER_HEARTBEAT_MAINTENACE_TIMEOUT or \
val > MAX_SYSTEM_PEER_HEARTBEAT_MAINTENACE_TIMEOUT:
LOG.debug("Invalid heartbeat_maintenance_timeout: %s" %
heartbeat_maintenance_timeout)
return False
return True
@index.when(method='POST', template='json')
def post(self):
"""Create a new system peer."""
policy.authorize(system_peer_policy.POLICY_ROOT % "create", {},
restcomm.extract_credentials_for_policy())
context = restcomm.extract_context_from_environ()
LOG.info("Creating a new system peer: %s" % context)
payload = self._get_payload(request)
if not payload:
pecan.abort(httpclient.BAD_REQUEST, _('Body required'))
# Validate payload
peer_uuid = payload.get('peer_uuid')
if not self._validate_uuid(peer_uuid):
pecan.abort(httpclient.BAD_REQUEST, _('Invalid peer uuid'))
peer_name = payload.get('peer_name')
if not self._validate_name(peer_name):
pecan.abort(httpclient.BAD_REQUEST, _('Invalid peer name'))
endpoint = payload.get('manager_endpoint')
if not self._validate_manager_endpoint(endpoint):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer manager_endpoint'))
username = payload.get('manager_username')
if not self._validate_manager_username(username):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer manager_username'))
password = payload.get('manager_password')
if not self._validate_manager_password(password):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer manager_password'))
gateway_ip = payload.get('peer_controller_gateway_address')
if not self._validate_peer_controller_gateway_ip(gateway_ip):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer peer_controller_gateway_address'))
# Optional request parameters
kwargs = {}
administrative_state = payload.get('administrative_state')
if administrative_state:
if not self._validate_administrative_state(administrative_state):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer administrative_state'))
kwargs['administrative_state'] = administrative_state
heartbeat_interval = payload.get('heartbeat_interval')
if heartbeat_interval is not None:
if not self._validate_heartbeat_interval(heartbeat_interval):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_interval'))
kwargs['heartbeat_interval'] = heartbeat_interval
heartbeat_failure_threshold = \
payload.get('heartbeat_failure_threshold')
if heartbeat_failure_threshold is not None:
if not self._validate_heartbeat_failure_threshold(
heartbeat_failure_threshold):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_failure_threshold'))
kwargs['heartbeat_failure_threshold'] = heartbeat_failure_threshold
heartbeat_failure_policy = payload.get('heartbeat_failure_policy')
if heartbeat_failure_policy:
if not self._validate_heartbeat_failure_policy(
heartbeat_failure_policy):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_failure_policy'))
kwargs['heartbeat_failure_policy'] = heartbeat_failure_policy
heartbeat_maintenance_timeout = \
payload.get('heartbeat_maintenance_timeout')
if heartbeat_maintenance_timeout is not None:
if not self._validate_heartbeat_maintenance_timeout(
heartbeat_maintenance_timeout):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_maintenance_timeout'))
kwargs['heartbeat_maintenance_timeout'] = \
heartbeat_maintenance_timeout
try:
peer_ref = db_api.system_peer_create(context,
peer_uuid,
peer_name,
endpoint,
username,
password,
gateway_ip, **kwargs)
return db_api.system_peer_db_model_to_dict(peer_ref)
except db_exc.DBDuplicateEntry:
LOG.info("Peer create failed. Peer UUID %s already exists"
% peer_uuid)
pecan.abort(httpclient.CONFLICT,
_('A system peer with this UUID already exists'))
except RemoteError as e:
pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value)
except Exception as e:
LOG.exception(e)
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
_('Unable to create system peer'))
@index.when(method='PATCH', template='json')
def patch(self, peer_ref):
"""Update a system peer.
:param peer_ref: ID or UUID of system peer to update
"""
policy.authorize(system_peer_policy.POLICY_ROOT % "modify", {},
restcomm.extract_credentials_for_policy())
context = restcomm.extract_context_from_environ()
LOG.info("Updating system peer: %s" % context)
if peer_ref is None:
pecan.abort(httpclient.BAD_REQUEST,
_('System Peer UUID or ID required'))
payload = self._get_payload(request)
if not payload:
pecan.abort(httpclient.BAD_REQUEST, _('Body required'))
peer = utils.system_peer_get_by_ref(context, peer_ref)
if peer is None:
pecan.abort(httpclient.NOT_FOUND, _('System Peer not found'))
peer_uuid, peer_name, endpoint, username, password, gateway_ip, \
administrative_state, heartbeat_interval, \
heartbeat_failure_threshold, heartbeat_failure_policy, \
heartbeat_maintenance_timeout = (
payload.get('peer_uuid'),
payload.get('peer_name'),
payload.get('manager_endpoint'),
payload.get('manager_username'),
payload.get('manager_password'),
payload.get('peer_controller_gateway_address'),
payload.get('administrative_state'),
payload.get('heartbeat_interval'),
payload.get('heartbeat_failure_threshold'),
payload.get('heartbeat_failure_policy'),
payload.get('heartbeat_maintenance_timeout')
)
if not (peer_uuid or peer_name or endpoint or username or password
or administrative_state or heartbeat_interval
or heartbeat_failure_threshold or heartbeat_failure_policy
or heartbeat_maintenance_timeout or gateway_ip):
pecan.abort(httpclient.BAD_REQUEST, _('nothing to update'))
# Check value is not None or empty before calling validate
if peer_uuid:
if not self._validate_uuid(peer_uuid):
pecan.abort(httpclient.BAD_REQUEST, _('Invalid peer uuid'))
if peer_name:
if not self._validate_name(peer_name):
pecan.abort(httpclient.BAD_REQUEST, _('Invalid peer name'))
if endpoint:
if not self._validate_manager_endpoint(endpoint):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer manager_endpoint'))
if username:
if not self._validate_manager_username(username):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer manager_username'))
if password:
if not self._validate_manager_password(password):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer manager_password'))
if gateway_ip:
if not self._validate_peer_controller_gateway_ip(gateway_ip):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer peer_controller_gateway_address'))
if administrative_state:
if not self._validate_administrative_state(administrative_state):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer administrative_state'))
if heartbeat_interval:
if not self._validate_heartbeat_interval(heartbeat_interval):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_interval'))
if heartbeat_failure_threshold:
if not self._validate_heartbeat_failure_threshold(
heartbeat_failure_threshold):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_failure_threshold'))
if heartbeat_failure_policy:
if not self._validate_heartbeat_failure_policy(
heartbeat_failure_policy):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_failure_policy'))
if heartbeat_maintenance_timeout:
if not self._validate_heartbeat_maintenance_timeout(
heartbeat_maintenance_timeout):
pecan.abort(httpclient.BAD_REQUEST,
_('Invalid peer heartbeat_maintenance_timeout'))
try:
updated_peer = db_api.system_peer_update(
context,
peer.id,
peer_uuid, peer_name,
endpoint, username, password,
gateway_ip,
administrative_state,
heartbeat_interval,
heartbeat_failure_threshold,
heartbeat_failure_policy,
heartbeat_maintenance_timeout)
return db_api.system_peer_db_model_to_dict(updated_peer)
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 system peer'))
@index.when(method='delete', template='json')
def delete(self, peer_ref):
"""Delete the system peer."""
policy.authorize(system_peer_policy.POLICY_ROOT % "delete", {},
restcomm.extract_credentials_for_policy())
context = restcomm.extract_context_from_environ()
LOG.info("Deleting system peer: %s" % context)
if peer_ref is None:
pecan.abort(httpclient.BAD_REQUEST,
_('System Peer UUID or ID required'))
peer = utils.system_peer_get_by_ref(context, peer_ref)
if peer is None:
pecan.abort(httpclient.NOT_FOUND, _('System Peer not found'))
# A system peer cannot be deleted if it is used by any associations
association = db_api.\
peer_group_association_get_by_system_peer_id(context,
str(peer.id))
if len(association) > 0:
pecan.abort(httpclient.BAD_REQUEST,
_('Cannot delete a system peer which is '
'associated with peer group.'))
try:
db_api.system_peer_destroy(context, peer.id)
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 system peer'))