Add ZeroMQ RPC backend

This feature adds a new RPC backend for communication between
sysinv-api, sysinv-conductor and sysinv-agent processes.
This backend is implemented using a patched zerorpc library [1],
which is built on top of ZeroMQ and message-pack.
The motivation behind this change is to decouple sysinv from RabbitMQ,
and use a brokerless solution for RPC instead.

The key points are:
- All imports of rpcapi.py are replaced by rpcapiproxy.py, which
  decides the backend to use (rabbitmq or zeromq) according to
  configuration.
- During an upgrade process the rpc service listens to both rabbitmq
  and zeromq. For communication between hosts, the client backend api
  is chosen according to host software version.
- In future versions, the usage of RabbitMQ will no longer be
  necessary and its usage can be removed. I have marked these parts of
  code with "TODO(RPCHybridMode)" to easily track it.

[1] https://review.opendev.org/c/starlingx/integ/+/864310

TEST PLAN:
PASS: Bootstrap and host-unlock on AIO-SX, AIO-Duplex, Standard
PASS: Bootstrap and host-unlock on DC system-controller and subcloud
PASS: Verify sysinv.log and confirm no error occurs in RPC communication
PASS: Perform system cli commands that interacts with sysinv RPCs:
  - system host-cpu-max-frequency-modify
  - system license-install
  - system storage-backend-add ceph-external
  - system host-swact
PASS: Backup & Restore on AIO-SX
PASS: Bootstrap replay (updating mgmt and cluster subnet) on AIO-SX
PASS: Platform upgrade on AIO-DX (22.06 -> 22.12)
PASS: Platform upgrade on AIO-DX+ (22.06 -> 22.12)
PASS: Platform upgrade on AIO-SX (22.06 -> 22.12)

Depends-On: https://review.opendev.org/c/starlingx/tools/+/859576
Depends-On: https://review.opendev.org/c/starlingx/stx-puppet/+/859575
Depends-On: https://review.opendev.org/c/starlingx/ansible-playbooks/+/862609

Story: 2010087
Task: 46444

Change-Id: I5cd61b541a6d8c62628a0f99db0e35af1eae5961
Signed-off-by: Alyson Deives Pereira <alyson.deivespereira@windriver.com>
Signed-off-by: Eduardo Juliano Alberti <eduardo.alberti@windriver.com>
This commit is contained in:
Alyson Deives Pereira 2022-09-28 09:05:14 -03:00
parent aede2f1492
commit c6a41c20a9
44 changed files with 789 additions and 34 deletions

View File

@ -40,6 +40,7 @@ from tsconfig.tsconfig import CONTROLLER_UPGRADE_FLAG
from tsconfig.tsconfig import CONTROLLER_UPGRADE_COMPLETE_FLAG from tsconfig.tsconfig import CONTROLLER_UPGRADE_COMPLETE_FLAG
from tsconfig.tsconfig import CONTROLLER_UPGRADE_FAIL_FLAG from tsconfig.tsconfig import CONTROLLER_UPGRADE_FAIL_FLAG
from tsconfig.tsconfig import CONTROLLER_UPGRADE_STARTED_FLAG from tsconfig.tsconfig import CONTROLLER_UPGRADE_STARTED_FLAG
from tsconfig.tsconfig import SYSINV_HYBRID_RPC_FLAG
from controllerconfig.common import constants from controllerconfig.common import constants
from controllerconfig import utils as cutils from controllerconfig import utils as cutils
@ -893,6 +894,11 @@ def upgrade_controller(from_release, to_release):
LOG.error("Failed to stop %s service" % "sysinv-agent") LOG.error("Failed to stop %s service" % "sysinv-agent")
raise raise
# Creating Sysinv Hybrid Mode flag
# TODO(RPCHybridMode): This is only required for 21.12 -> 22.12 upgrades.
# Remove in future release.
open(SYSINV_HYBRID_RPC_FLAG, "w").close()
# Mount required filesystems from mate controller # Mount required filesystems from mate controller
LOG.info("Mounting filesystems") LOG.info("Mounting filesystems")
nfs_mount_filesystem(PLATFORM_PATH) nfs_mount_filesystem(PLATFORM_PATH)

View File

@ -0,0 +1,20 @@
#!/bin/bash
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
NAME=$(basename $0)
function log {
logger -p local1.info $1
}
log "$NAME: restarting sysinv services"
sm-restart service sysinv-conductor
sleep 2
pmon-restart sysinv-agent
exit 0

View File

@ -15,6 +15,7 @@ override_dh_install:
install -p -D -m 700 scripts/openstack_update_admin_password $(ROOT)/usr/bin/openstack_update_admin_password install -p -D -m 700 scripts/openstack_update_admin_password $(ROOT)/usr/bin/openstack_update_admin_password
install -p -D -m 700 scripts/upgrade_swact_migration.py $(ROOT)/usr/bin/upgrade_swact_migration.py install -p -D -m 700 scripts/upgrade_swact_migration.py $(ROOT)/usr/bin/upgrade_swact_migration.py
install -p -D -m 755 scripts/image-backup.sh $(ROOT)/usr/bin/image-backup.sh install -p -D -m 755 scripts/image-backup.sh $(ROOT)/usr/bin/image-backup.sh
install -p -D -m 755 scripts/sysinv-service-restart.sh $(ROOT)/usr/bin/sysinv-service-restart.sh
install -d -m 755 $(ROOT)/etc/goenabled.d/ install -d -m 755 $(ROOT)/etc/goenabled.d/
install -p -D -m 700 scripts/config_goenabled_check.sh $(ROOT)/etc/goenabled.d/config_goenabled_check.sh.controller install -p -D -m 700 scripts/config_goenabled_check.sh $(ROOT)/etc/goenabled.d/config_goenabled_check.sh.controller
install -d -m 755 $(ROOT)/etc/init.d install -d -m 755 $(ROOT)/etc/init.d

View File

@ -75,6 +75,7 @@ Build-Depends-Indep:
python3-fm-api, python3-fm-api,
python3-platform-util, python3-platform-util,
python3-cephclient, python3-cephclient,
zerorpc-python,
cgts-client, cgts-client,
controllerconfig controllerconfig
Standards-Version: 4.5.1 Standards-Version: 4.5.1
@ -141,6 +142,7 @@ Depends: ${python3:Depends}, ${misc:Depends},
python3-cgcs-patch, python3-cgcs-patch,
platform-util, platform-util,
python3-cephclient, python3-cephclient,
zerorpc-python,
cgts-client cgts-client
Description: Starlingx system inventory - daemon Description: Starlingx system inventory - daemon
Starlingx system inventory Starlingx system inventory

View File

@ -52,4 +52,5 @@ python-barbicanclient
rfc3986 rfc3986
importlib-metadata>=3.3.0;python_version=="3.6" importlib-metadata>=3.3.0;python_version=="3.6"
importlib-resources==5.2.2;python_version=="3.6" importlib-resources==5.2.2;python_version=="3.6"
oslo.policy # Apache-2.0 oslo.policy # Apache-2.0
zerorpc @ git+https://github.com/0rpc/zerorpc-python.git@99ee6e47c8baf909b97eec94f184a19405f392a2 # MIT

View File

@ -35,7 +35,7 @@ from sysinv.common import service as sysinv_service
from sysinv.common import utils from sysinv.common import utils
from sysinv.common import disk_utils from sysinv.common import disk_utils
from sysinv.conductor import rpcapi as conductor_rpcapi from sysinv.conductor import rpcapiproxy as conductor_rpcapi
from sysinv.openstack.common import context from sysinv.openstack.common import context
from functools import cmp_to_key from functools import cmp_to_key

View File

@ -22,7 +22,7 @@ from oslo_log import log as logging
from sysinv.common import disk_utils from sysinv.common import disk_utils
from sysinv.common import constants from sysinv.common import constants
from sysinv.common import utils from sysinv.common import utils
from sysinv.conductor import rpcapi as conductor_rpcapi from sysinv.conductor import rpcapiproxy as conductor_rpcapi
from sysinv.openstack.common import context from sysinv.openstack.common import context
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -38,6 +38,7 @@ from eventlet.green import subprocess
import fileinput import fileinput
import os import os
import retrying import retrying
import six
import shutil import shutil
import sys import sys
import tempfile import tempfile
@ -66,12 +67,15 @@ from sysinv.common import service
from sysinv.common import utils from sysinv.common import utils
from sysinv.objects import base as objects_base from sysinv.objects import base as objects_base
from sysinv.puppet import common as puppet from sysinv.puppet import common as puppet
from sysinv.conductor import rpcapi as conductor_rpcapi from sysinv.conductor import rpcapiproxy as conductor_rpcapi
from sysinv.openstack.common import context as mycontext from sysinv.openstack.common import context as mycontext
from sysinv.openstack.common import periodic_task from sysinv.openstack.common import periodic_task
from sysinv.openstack.common.rpc.common import Timeout from sysinv.openstack.common.rpc.common import Timeout
from sysinv.openstack.common.rpc.common import serialize_remote_exception from sysinv.openstack.common.rpc.common import serialize_remote_exception
from sysinv.openstack.common.rpc import service as rpc_service
from sysinv.openstack.common.rpc.common import RemoteError from sysinv.openstack.common.rpc.common import RemoteError
from sysinv.zmq_rpc.zmq_rpc import ZmqRpcServer
from sysinv.zmq_rpc.zmq_rpc import is_rpc_hybrid_mode_active
import tsconfig.tsconfig as tsc import tsconfig.tsconfig as tsc
@ -157,8 +161,28 @@ class AgentManager(service.PeriodicService):
HOST_FILESYSTEMS} HOST_FILESYSTEMS}
def __init__(self, host, topic): def __init__(self, host, topic):
self.host = host
self.topic = topic
serializer = objects_base.SysinvObjectSerializer() serializer = objects_base.SysinvObjectSerializer()
super(AgentManager, self).__init__(host, topic, serializer=serializer) super(AgentManager, self).__init__()
self._rpc_service = None
self._zmq_rpc_service = None
# TODO(RPCHybridMode): Usage of RabbitMQ RPC is only required for
# 21.12 -> 22.12 upgrades.
# Remove this in new releases, when it's no longer necessary do the
# migration work through RabbitMQ and ZeroMQ
# NOTE: If more switches are necessary before RabbitMQ removal,
# refactor this into an RPC layer
if not CONF.rpc_backend_zeromq or is_rpc_hybrid_mode_active():
self._rpc_service = rpc_service.Service(self.host, self.topic,
manager=self,
serializer=serializer)
if CONF.rpc_backend_zeromq:
self._zmq_rpc_service = ZmqRpcServer(
self,
CONF.rpc_zeromq_bind_ip,
CONF.rpc_zeromq_agent_bind_port)
self._report_to_conductor_iplatform_avail_flag = False self._report_to_conductor_iplatform_avail_flag = False
self._report_to_conductor_fpga_info = True self._report_to_conductor_fpga_info = True
@ -191,7 +215,10 @@ class AgentManager(service.PeriodicService):
def start(self): def start(self):
super(AgentManager, self).start() super(AgentManager, self).start()
if self._rpc_service:
self._rpc_service.start()
if self._zmq_rpc_service:
self._zmq_rpc_service.run()
# Do not collect inventory and report to conductor at startup in # Do not collect inventory and report to conductor at startup in
# order to eliminate two inventory reports # order to eliminate two inventory reports
# (one from here and one from audit) being sent to the conductor # (one from here and one from audit) being sent to the conductor
@ -203,6 +230,13 @@ class AgentManager(service.PeriodicService):
if tsc.system_mode == constants.SYSTEM_MODE_SIMPLEX: if tsc.system_mode == constants.SYSTEM_MODE_SIMPLEX:
utils.touch(SYSINV_READY_FLAG) utils.touch(SYSINV_READY_FLAG)
def stop(self):
if self._rpc_service:
self._rpc_service.stop()
if self._zmq_rpc_service:
self._zmq_rpc_service.stop()
super(AgentManager, self).stop()
def _report_to_conductor(self): def _report_to_conductor(self):
""" Initial inventory report to conductor required """ Initial inventory report to conductor required
@ -1576,7 +1610,8 @@ class AgentManager(service.PeriodicService):
basename = os.path.basename(file_name) basename = os.path.basename(file_name)
fd, tmppath = tempfile.mkstemp(dir=dirname, prefix=basename) fd, tmppath = tempfile.mkstemp(dir=dirname, prefix=basename)
with os.fdopen(fd, 'wb') as f: with os.fdopen(fd, 'wb') as f:
f.write(f_content.encode()) f_content = six.ensure_binary(f_content)
f.write(f_content)
if os.path.islink(file_name): if os.path.islink(file_name):
os.unlink(file_name) os.unlink(file_name)
os.rename(tmppath, file_name) os.rename(tmppath, file_name)
@ -2129,3 +2164,23 @@ class AgentManager(service.PeriodicService):
except exception.SysinvException: except exception.SysinvException:
LOG.exception("Sysinv Agent exception updating ipv" LOG.exception("Sysinv Agent exception updating ipv"
"conductor.") "conductor.")
# TODO(RPCHybridMode): This is only useful for 21.12 -> 22.12 upgrades.
# Remove this method in new releases, when it's no longer necessary to
# perform upgrade through hybrid mode messaging system
def delete_sysinv_hybrid_state(self, context, host_uuid):
"""Delete the Sysinv flag of Hybrid Mode.
:param host_uuid: ihost uuid unique id
:return: None
"""
if self._ihost_uuid and self._ihost_uuid == host_uuid:
if os.path.exists(tsc.SYSINV_HYBRID_RPC_FLAG):
utils.delete_if_exists(tsc.SYSINV_HYBRID_RPC_FLAG)
LOG.info("Sysinv Hybrid Mode deleted.")
LOG.info("Sysinv services will be restarted")
# pylint: disable=not-callable
subprocess.call(['/usr/bin/sysinv-service-restart.sh'])
else:
LOG.info("Hybrid flag doesn't exist. Ignoring delete.")

View File

@ -294,3 +294,19 @@ class AgentAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
transaction_id=transaction_id, transaction_id=transaction_id,
retimer_included=retimer_included), retimer_included=retimer_included),
topic=topic) topic=topic)
# TODO(RPCHybridMode): This is only useful for 21.12 -> 22.12 upgrades.
# Remove this method in new releases, when it's no longer necessary to
# perform upgrade through hybrid mode messaging system
def delete_sysinv_hybrid_state(self, context, host_uuid):
"""Asynchronously, have the agent to delete sysinv hybrid
mode flag
:param context: request context.
:param host_uuid: ihost uuid unique id
:returns: pass or fail
"""
return self.cast(context,
self.make_msg('delete_sysinv_hybrid_state',
host_uuid=host_uuid))

View File

@ -0,0 +1,26 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
from oslo_config import cfg
from oslo_log import log
import sysinv.agent.rpcapi as rpcapi
from sysinv.agent.rpcapizmq import AgentAPI as ZMQAgentAPI
from sysinv.agent.rpcapi import AgentAPI as AMQPAgentAPI
from sysinv.zmq_rpc.zmq_rpc import is_rpc_hybrid_mode_active
LOG = log.getLogger(__name__)
MANAGER_TOPIC = rpcapi.MANAGER_TOPIC
def AgentAPI(topic=None):
rpc_backend = cfg.CONF.rpc_backend
rpc_backend_zeromq = cfg.CONF.rpc_backend_zeromq
rpc_backend_hybrid_mode = is_rpc_hybrid_mode_active()
LOG.debug("Current agent rpc_backend: {} "
"use_zeromq: {} hybrid_mode: {}".format(rpc_backend,
rpc_backend_zeromq,
rpc_backend_hybrid_mode))
if rpc_backend_zeromq:
return ZMQAgentAPI(topic)
return AMQPAgentAPI(topic)

View File

@ -0,0 +1,62 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
"""
Client side of the agent RPC API using ZeroMQ backend.
"""
from oslo_config import cfg
from oslo_log import log
from sysinv.agent.rpcapi import AgentAPI as BaseAgentAPI
from sysinv.agent.rpcapi import MANAGER_TOPIC
from sysinv.zmq_rpc.zmq_rpc import ZmqRpcClient
from sysinv.zmq_rpc.zmq_rpc import is_rpc_hybrid_mode_active
from sysinv.zmq_rpc.zmq_rpc import is_zmq_backend_available
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class AgentAPI(ZmqRpcClient, BaseAgentAPI):
def __init__(self, topic=None):
if topic is None:
topic = MANAGER_TOPIC
host = None
port = CONF.rpc_zeromq_agent_bind_port
super(AgentAPI, self).__init__(host, port, topic)
def call(self, context, msg, topic=None, version=None, timeout=None):
if is_rpc_hybrid_mode_active():
host_uuid = msg['args']['host_uuid']
if not is_zmq_backend_available(host_uuid):
LOG.debug("RPC hybrid mode is active and agent zmq backend is "
"not yet available in host {}. Calling RPC call "
"method {} through rabbitmq".format(host_uuid,
msg['method']))
rpcapi = BaseAgentAPI()
return rpcapi.call(context, msg, topic, version, timeout)
return super(AgentAPI, self).call(context, msg, timeout)
def cast(self, context, msg, topic=None, version=None):
if is_rpc_hybrid_mode_active():
host_uuid = msg['args']['host_uuid']
if not is_zmq_backend_available(host_uuid):
LOG.debug("RPC hybrid mode is active and agent zmq backend is "
"not yet available in host {}. Calling RPC cast "
"method {} through rabbitmq".format(host_uuid,
msg['method']))
rpcapi = BaseAgentAPI()
return rpcapi.cast(context, msg, topic, version)
return super(AgentAPI, self).cast(context, msg)
def fanout_cast(self, context, msg, topic=None, version=None):
if is_rpc_hybrid_mode_active():
method = msg['method']
LOG.debug("RPC hybrid mode is active. Calling RPC fanout_cast "
"method {} through rabbitmq and zmq".format(method))
rpcapi = BaseAgentAPI()
rpcapi.fanout_cast(context, msg, topic, version)
return super(AgentAPI, self).fanout_cast(context, msg)

View File

@ -33,7 +33,7 @@ from sysinv.api.controllers.v1 import link
from sysinv.api.controllers.v1 import partition from sysinv.api.controllers.v1 import partition
from sysinv.api.controllers.v1 import types from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils from sysinv.api.controllers.v1 import utils
from sysinv.agent import rpcapi as agent_rpcapi from sysinv.agent import rpcapiproxy as agent_rpcapi
from sysinv.common import exception from sysinv.common import exception
from sysinv.common import constants from sysinv.common import constants
from sysinv.common import utils as cutils from sysinv.common import utils as cutils

View File

@ -34,7 +34,7 @@ from sysinv._i18n import _
from sysinv.api.policies import base as base_policy from sysinv.api.policies import base as base_policy
from sysinv.common import context from sysinv.common import context
from sysinv.common import utils from sysinv.common import utils
from sysinv.conductor import rpcapi from sysinv.conductor import rpcapiproxy as rpcapi
from sysinv.db import api as dbapi from sysinv.db import api as dbapi
from sysinv.common import policy from sysinv.common import policy
from webob import exc from webob import exc

View File

@ -37,7 +37,7 @@ from oslo_config import cfg
from oslo_log import log from oslo_log import log
from sysinv._i18n import _ from sysinv._i18n import _
from sysinv.common import service as sysinv_service from sysinv.common import service as sysinv_service
from sysinv.conductor import rpcapi as conductor_rpcapi from sysinv.conductor import rpcapiproxy as conductor_rpcapi
from sysinv.openstack.common import context from sysinv.openstack.common import context
CONF = cfg.CONF CONF = cfg.CONF

View File

@ -10,7 +10,7 @@ import yaml
from sysinv.common import constants from sysinv.common import constants
from sysinv.common import service from sysinv.common import service
from sysinv.conductor import rpcapi as conductor_rpcapi from sysinv.conductor import rpcapiproxy as conductor_rpcapi
from sysinv.db import api from sysinv.db import api
from sysinv.openstack.common import context from sysinv.openstack.common import context

View File

@ -75,3 +75,7 @@ class RequestContext(context.RequestContext):
result.update(super(RequestContext, self).to_dict()) result.update(super(RequestContext, self).to_dict())
return result return result
@classmethod
def from_dict(cls, values):
return cls(**values)

View File

@ -25,7 +25,7 @@ from oslo_service import service
from sysinv.openstack.common import context from sysinv.openstack.common import context
from sysinv.openstack.common import periodic_task from sysinv.openstack.common import periodic_task
from sysinv.openstack.common import rpc from sysinv.openstack.common import rpc
from sysinv.openstack.common.rpc import service as rpc_service from sysinv.openstack.common import service as base_service
from sysinv import version from sysinv import version
@ -47,7 +47,14 @@ cfg.CONF.register_opts([
CONF = cfg.CONF CONF = cfg.CONF
class PeriodicService(rpc_service.Service, periodic_task.PeriodicTasks): class PeriodicService(base_service.Service, periodic_task.PeriodicTasks):
def __init__(self, manager=None):
super(PeriodicService, self).__init__()
if manager is None:
self.manager = self
else:
self.manager = manager
def start(self): def start(self):
super(PeriodicService, self).start() super(PeriodicService, self).start()

View File

@ -39,6 +39,7 @@ import re
import requests import requests
import ruamel.yaml as yaml import ruamel.yaml as yaml
import shutil import shutil
import six
import socket import socket
import tempfile import tempfile
import time import time
@ -77,7 +78,7 @@ from platform_util.license import license
from sqlalchemy.orm import exc from sqlalchemy.orm import exc
from six.moves import http_client as httplib from six.moves import http_client as httplib
from sysinv._i18n import _ from sysinv._i18n import _
from sysinv.agent import rpcapi as agent_rpcapi from sysinv.agent import rpcapiproxy as agent_rpcapi
from sysinv.api.controllers.v1 import address_pool from sysinv.api.controllers.v1 import address_pool
from sysinv.api.controllers.v1 import cpu_utils from sysinv.api.controllers.v1 import cpu_utils
from sysinv.api.controllers.v1 import kube_app as kube_api from sysinv.api.controllers.v1 import kube_app as kube_api
@ -112,11 +113,14 @@ from sysinv.objects import base as objects_base
from sysinv.objects import kube_app as kubeapp_obj from sysinv.objects import kube_app as kubeapp_obj
from sysinv.openstack.common import context as ctx from sysinv.openstack.common import context as ctx
from sysinv.openstack.common import periodic_task from sysinv.openstack.common import periodic_task
from sysinv.openstack.common.rpc import service as rpc_service
from sysinv.puppet import common as puppet_common from sysinv.puppet import common as puppet_common
from sysinv.puppet import puppet from sysinv.puppet import puppet
from sysinv.helm import helm from sysinv.helm import helm
from sysinv.helm.lifecycle_constants import LifecycleConstants from sysinv.helm.lifecycle_constants import LifecycleConstants
from sysinv.helm.lifecycle_hook import LifecycleHookInfo from sysinv.helm.lifecycle_hook import LifecycleHookInfo
from sysinv.zmq_rpc.zmq_rpc import ZmqRpcServer
from sysinv.zmq_rpc.zmq_rpc import is_rpc_hybrid_mode_active
MANAGER_TOPIC = 'sysinv.conductor_manager' MANAGER_TOPIC = 'sysinv.conductor_manager'
@ -222,9 +226,29 @@ class ConductorManager(service.PeriodicService):
my_host_id = None my_host_id = None
def __init__(self, host, topic): def __init__(self, host, topic):
self.host = host
self.topic = topic
serializer = objects_base.SysinvObjectSerializer() serializer = objects_base.SysinvObjectSerializer()
super(ConductorManager, self).__init__(host, topic, super(ConductorManager, self).__init__()
serializer=serializer) self._rpc_service = None
self._zmq_rpc_service = None
# TODO(RPCHybridMode): Usage of RabbitMQ RPC is only required for
# 21.12 -> 22.12 upgrades.
# Remove this in new releases, when it's no longer necessary do the
# migration work through RabbitMQ and ZeroMQ
# NOTE: If more switches are necessary before RabbitMQ removal,
# refactor this into an RPC layer
if not CONF.rpc_backend_zeromq or is_rpc_hybrid_mode_active():
self._rpc_service = rpc_service.Service(self.host, self.topic,
manager=self,
serializer=serializer)
if CONF.rpc_backend_zeromq:
self._zmq_rpc_service = ZmqRpcServer(
self,
CONF.rpc_zeromq_conductor_bind_ip,
CONF.rpc_zeromq_conductor_bind_port)
self.dbapi = None self.dbapi = None
self.fm_api = None self.fm_api = None
self.fm_log = None self.fm_log = None
@ -282,6 +306,12 @@ class ConductorManager(service.PeriodicService):
self._start() self._start()
# accept API calls and run periodic tasks after # accept API calls and run periodic tasks after
# initializing conductor manager service # initializing conductor manager service
if self._rpc_service:
self._rpc_service.start()
if self._zmq_rpc_service:
self._zmq_rpc_service.run()
super(ConductorManager, self).start() super(ConductorManager, self).start()
# greenthreads must be called after super.start for it to work properly # greenthreads must be called after super.start for it to work properly
@ -393,6 +423,13 @@ class ConductorManager(service.PeriodicService):
""" Periodic tasks are run at pre-specified intervals. """ """ Periodic tasks are run at pre-specified intervals. """
return self.run_periodic_tasks(context, raise_on_error=raise_on_error) return self.run_periodic_tasks(context, raise_on_error=raise_on_error)
def stop(self):
if self._rpc_service:
self._rpc_service.stop()
if self._zmq_rpc_service:
self._zmq_rpc_service.stop()
super(ConductorManager, self).stop()
@contextmanager @contextmanager
def session(self): def session(self):
session = dbapi.get_instance().get_session(autocommit=True) session = dbapi.get_instance().get_session(autocommit=True)
@ -12187,6 +12224,16 @@ class ConductorManager(service.PeriodicService):
# Delete upgrade record # Delete upgrade record
self.dbapi.software_upgrade_destroy(upgrade.uuid) self.dbapi.software_upgrade_destroy(upgrade.uuid)
# TODO(RPCHybridMode): This is only useful for 21.12 -> 22.12 upgrades.
# Remove this in new releases, when it's no longer necessary
# do the migration work through RabbitMQ and ZeroMQ
if (tsc.system_mode is not constants.SYSTEM_MODE_SIMPLEX):
rpcapi = agent_rpcapi.AgentAPI()
controller_1 = self.dbapi.ihost_get_by_hostname(
constants.CONTROLLER_1_HOSTNAME)
LOG.info("Deleting Sysinv Hybrid state")
rpcapi.delete_sysinv_hybrid_state(context, controller_1['uuid'])
# Clear upgrades alarm # Clear upgrades alarm
entity_instance_id = "%s=%s" % (fm_constants.FM_ENTITY_TYPE_HOST, entity_instance_id = "%s=%s" % (fm_constants.FM_ENTITY_TYPE_HOST,
constants.CONTROLLER_HOSTNAME) constants.CONTROLLER_HOSTNAME)
@ -12778,6 +12825,9 @@ class ConductorManager(service.PeriodicService):
LOG.info("Overwriting file %s in %s " % LOG.info("Overwriting file %s in %s " %
(ceph_conf_filename, tsc.PLATFORM_CEPH_CONF_PATH)) (ceph_conf_filename, tsc.PLATFORM_CEPH_CONF_PATH))
# contents might be bytes, make sure it is str
contents = six.ensure_str(contents)
try: try:
with open(opt_ceph_conf_file, 'w+') as f: with open(opt_ceph_conf_file, 'w+') as f:
f.write(contents) f.write(contents)
@ -12794,6 +12844,10 @@ class ConductorManager(service.PeriodicService):
""" """
LOG.info("Install license file.") LOG.info("Install license file.")
# contents might be bytes, make sure it is str
contents = six.ensure_str(contents)
license_file = os.path.join(tsc.PLATFORM_CONF_PATH, license_file = os.path.join(tsc.PLATFORM_CONF_PATH,
constants.LICENSE_FILE) constants.LICENSE_FILE)
temp_license_file = license_file + '.temp' temp_license_file = license_file + '.temp'
@ -13018,6 +13072,9 @@ class ConductorManager(service.PeriodicService):
LOG.info("config_certificate mode=%s" % mode) LOG.info("config_certificate mode=%s" % mode)
# pem_contents might be bytes, make sure it is str
pem_contents = six.ensure_str(pem_contents)
cert_list, private_key = \ cert_list, private_key = \
self._extract_keys_from_pem(mode, pem_contents, self._extract_keys_from_pem(mode, pem_contents,
serialization.PrivateFormat.PKCS8, serialization.PrivateFormat.PKCS8,

View File

@ -0,0 +1,47 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
import os
from oslo_config import cfg
from oslo_log import log
import sysinv.conductor.rpcapi as rpcapi
from sysinv.conductor.rpcapi import ConductorAPI as AMQPConductorAPI
from sysinv.conductor.rpcapizmq import ConductorAPI as ZMQConductorAPI
from sysinv.zmq_rpc.zmq_rpc import is_rpc_hybrid_mode_active
from sysinv.zmq_rpc.zmq_rpc import check_connection
LOG = log.getLogger(__name__)
MANAGER_TOPIC = rpcapi.MANAGER_TOPIC
def ConductorAPI(topic=None):
rpc_backend_zeromq = cfg.CONF.rpc_backend_zeromq
rpc_backend_hybrid_mode = is_rpc_hybrid_mode_active()
rpc_backend = cfg.CONF.rpc_backend
LOG.debug("Current conductor rpc_backend: {} "
"use_zeromq: {} hybrid_mode: {}".format(rpc_backend,
rpc_backend_zeromq,
rpc_backend_hybrid_mode))
# Hybrid mode is expected to be defined for controller-1 only during upgrade
# all other nodes should be running ZeroMQ exclusively
if rpc_backend_hybrid_mode:
# in controller-1 agent, we need to know if conductor
# is able to listen to ZeroRPC.
# If conductor is running on same host, we know it is running in
# hybrid mode, and we assume ZeroMQ is preferred.
# Otherwise, it can be conductor running on controller-0 before
# migrate to ZeroMQ, so we verify before send the RPC call
# if ZeroMQ is running and if yes, use it, otherwise use RabbitMQ
if os.path.isfile("/var/run/sysinv-conductor.pid"):
return ZMQConductorAPI(topic)
else:
if check_connection(cfg.CONF.rpc_zeromq_conductor_bind_ip,
cfg.CONF.rpc_zeromq_conductor_bind_port):
return ZMQConductorAPI(topic)
else:
return AMQPConductorAPI(topic)
if rpc_backend_zeromq:
return ZMQConductorAPI(topic)
return AMQPConductorAPI(topic)

View File

@ -0,0 +1,45 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
"""
Client side of the conductor RPC API using ZeroMQ backend.
"""
import os
from oslo_config import cfg
from oslo_log import log
from sysinv.common import constants
from sysinv.conductor.rpcapi import ConductorAPI as BaseConductorAPI
from sysinv.conductor.rpcapi import MANAGER_TOPIC
from sysinv.zmq_rpc.zmq_rpc import ZmqRpcClient
CONF = cfg.CONF
LOG = log.getLogger(__name__)
class ConductorAPI(ZmqRpcClient, BaseConductorAPI):
def __init__(self, topic=None):
if topic is None:
topic = MANAGER_TOPIC
host = CONF.rpc_zeromq_conductor_bind_ip
# It is expected to have a value assigned
# if we are using default value, puppet was not executed or
# there was an issue.
# We can still use it in case conductor is running locally
# otherwise we try to communicate using controller hostname
if host == "::" and not os.path.isfile("/var/run/sysinv-conductor.pid"):
host = constants.CONTROLLER_HOSTNAME
port = CONF.rpc_zeromq_conductor_bind_port
super(ConductorAPI, self).__init__(host, port, topic)
def call(self, context, msg, topic=None, version=None, timeout=None):
return super(ConductorAPI, self).call(context, msg, timeout)
def cast(self, context, msg, topic=None, version=None):
return super(ConductorAPI, self).cast(context, msg)
def fanout_cast(self, context, msg, topic=None, version=None):
return super(ConductorAPI, self).fanout_cast(context, msg)

View File

@ -20,7 +20,7 @@ import zlib
from eventlet.green import subprocess from eventlet.green import subprocess
from oslo_log import log as logging from oslo_log import log as logging
from sysinv.agent import rpcapi as agent_rpcapi from sysinv.agent import rpcapiproxy as agent_rpcapi
from sysinv.common import exception from sysinv.common import exception
from sysinv.common import kubernetes from sysinv.common import kubernetes
from sysinv.openstack.common import context from sysinv.openstack.common import context

View File

@ -59,8 +59,12 @@ class RequestContext(object):
'auth_token': self.auth_token, 'auth_token': self.auth_token,
'request_id': self.request_id} 'request_id': self.request_id}
@classmethod
def from_dict(cls, values):
return cls(**values)
def get_admin_context(show_deleted="no"):
def get_admin_context(show_deleted=False):
context = RequestContext(None, context = RequestContext(None,
tenant=None, tenant=None,
is_admin=True, is_admin=True,

View File

@ -40,6 +40,21 @@ rpc_opts = [
cfg.StrOpt('rpc_backend', cfg.StrOpt('rpc_backend',
default='%s.impl_kombu' % __package__, default='%s.impl_kombu' % __package__,
help="The messaging module to use, defaults to kombu."), help="The messaging module to use, defaults to kombu."),
cfg.BoolOpt('rpc_backend_zeromq',
default=True,
help='Use ZeroMQ for RPC communication'),
cfg.StrOpt('rpc_zeromq_bind_ip',
default='::',
help='Bind IP address for ZeroMQ RPC backend'),
cfg.StrOpt('rpc_zeromq_conductor_bind_ip',
default='::',
help='Bind IP address for sysinv-conductor ZeroMQ RPC backend'),
cfg.StrOpt('rpc_zeromq_agent_bind_port',
default=9502,
help='Bind port for sysinv-agent ZeroMQ RPC server'),
cfg.StrOpt('rpc_zeromq_conductor_bind_port',
default=9501,
help='Bind port for sysinv-conductor ZeroMQ RPC server'),
cfg.IntOpt('rpc_thread_pool_size', cfg.IntOpt('rpc_thread_pool_size',
default=64, default=64,
help='Size of RPC thread pool'), help='Size of RPC thread pool'),

View File

@ -138,6 +138,28 @@ class Timeout(RPCException):
method=method or _('<unknown>')) method=method or _('<unknown>'))
class LostRemote(RPCException):
"""Signifies that a heartbeat has failed for zerorpc rpc backend.
This exception is raised if a heartbeat is not received while
waiting for a response from the remote side.
"""
message = _('%(lost_remote_msg)s - '
'topic: "%(topic)s", RPC method: "%(method)s" ')
def __init__(self, lost_remote_msg=None, topic=None, method=None):
self.lost_remote_msg = _('Lost remote after waiting for heartbeat')
self.topic = topic
self.method = method
if lost_remote_msg:
self.lost_remote_msg = lost_remote_msg
super(LostRemote, self).__init__(
None,
lost_remote_msg=self.lost_remote_msg,
topic=topic or _('<unknown>'),
method=method or _('<unknown>'))
class DuplicateMessageError(RPCException): class DuplicateMessageError(RPCException):
message = _("Found duplicate message(%(msg_id)s). Skipping it.") message = _("Found duplicate message(%(msg_id)s). Skipping it.")

View File

@ -93,7 +93,16 @@ class SystemInventoryPuppet(openstack.OpenstackBasePuppet):
'sysinv::api::openstack_keystone_tenant': 'sysinv::api::openstack_keystone_tenant':
self._operator.keystone.get_admin_project_name(), self._operator.keystone.get_admin_project_name(),
'sysinv::api::openstack_keyring_service': 'sysinv::api::openstack_keyring_service':
self.OPENSTACK_KEYRING_SERVICE self.OPENSTACK_KEYRING_SERVICE,
'sysinv::rpc_zeromq_conductor_bind_ip': self._get_management_address()
}
def get_host_config(self, host):
node_ip = self._get_address_by_name(
host.hostname, constants.NETWORK_TYPE_MGMT).address
return {
'sysinv::rpc_zeromq_bind_ip': node_ip
} }
def get_secure_system_config(self): def get_secure_system_config(self):

View File

@ -206,7 +206,7 @@ class ApiCertificateTestCaseMixin(object):
super(ApiCertificateTestCaseMixin, self).setUp() super(ApiCertificateTestCaseMixin, self).setUp()
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -68,7 +68,7 @@ class ApiControllerFSTestCaseMixin(base.FunctionalTest,
1, 1,
'extension-lv') 'extension-lv')
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -62,7 +62,7 @@ class TestDeviceImage(base.FunctionalTest, dbbase.BaseHostTestCase):
# Mock the Conductor API # Mock the Conductor API
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -52,7 +52,7 @@ class ApiDNSTestCaseMixin(object):
def setUp(self): def setUp(self):
super(ApiDNSTestCaseMixin, self).setUp() super(ApiDNSTestCaseMixin, self).setUp()
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -56,7 +56,7 @@ class ApiHelmChartTestCaseMixin(base.FunctionalTest,
def setUp(self): def setUp(self):
super(ApiHelmChartTestCaseMixin, self).setUp() super(ApiHelmChartTestCaseMixin, self).setUp()
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -61,7 +61,7 @@ class TestHost(base.FunctionalTest, dbbase.BaseHostTestCase):
# Mock the conductor API # Mock the conductor API
self.fake_conductor_api = FakeConductorAPI(self.dbapi) self.fake_conductor_api = FakeConductorAPI(self.dbapi)
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -52,7 +52,7 @@ class ApiHostFSTestCaseMixin(base.FunctionalTest,
30, 30,
'docker-lv') 'docker-lv')
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -2249,7 +2249,7 @@ class TestAIOUnlockedPost(InterfaceTestCase):
# Mock the conductor API # Mock the conductor API
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)
@ -2361,7 +2361,7 @@ class TestAIOUnlockedPatch(InterfaceTestCase):
# Mock the conductor API # Mock the conductor API
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -94,7 +94,7 @@ class TestKubeRootCAUpdate(base.FunctionalTest):
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
# rather than start the fake_conductor_api.service, we stage its dbapi # rather than start the fake_conductor_api.service, we stage its dbapi
self.fake_conductor_api.service.dbapi = self.dbapi self.fake_conductor_api.service.dbapi = self.dbapi
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.headers = API_HEADERS self.headers = API_HEADERS
@ -720,7 +720,7 @@ class TestKubeRootCAHostUpdate(base.FunctionalTest):
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
# rather than start the fake_conductor_api.service, we stage its dbapi # rather than start the fake_conductor_api.service, we stage its dbapi
self.fake_conductor_api.service.dbapi = self.dbapi self.fake_conductor_api.service.dbapi = self.dbapi
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -95,7 +95,7 @@ class TestKubeUpgrade(base.FunctionalTest):
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
# rather than start the fake_conductor_api.service, we stage its dbapi # rather than start the fake_conductor_api.service, we stage its dbapi
self.fake_conductor_api.service.dbapi = self.dbapi self.fake_conductor_api.service.dbapi = self.dbapi
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -52,7 +52,7 @@ class ApiNTPTestCaseMixin(object):
def setUp(self): def setUp(self):
super(ApiNTPTestCaseMixin, self).setUp() super(ApiNTPTestCaseMixin, self).setUp()
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -57,7 +57,7 @@ class TestPartition(base.FunctionalTest):
# Mock the conductor API # Mock the conductor API
self.fake_conductor_api = FakeConductorAPI(self.dbapi) self.fake_conductor_api = FakeConductorAPI(self.dbapi)
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -40,7 +40,7 @@ class TestUpgrade(base.FunctionalTest, dbbase.BaseSystemTestCase):
# Mock the Conductor API # Mock the Conductor API
self.fake_conductor_api = FakeConductorAPI() self.fake_conductor_api = FakeConductorAPI()
p = mock.patch('sysinv.conductor.rpcapi.ConductorAPI') p = mock.patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = p.start() self.mock_conductor_api = p.start()
self.mock_conductor_api.return_value = self.fake_conductor_api self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(p.stop) self.addCleanup(p.stop)

View File

@ -38,6 +38,7 @@ class ConfFixture(config_fixture.Config):
self.conf.set_default('host', 'fake-mini') self.conf.set_default('host', 'fake-mini')
self.conf.set_default('rpc_backend', self.conf.set_default('rpc_backend',
'sysinv.openstack.common.rpc.impl_fake') 'sysinv.openstack.common.rpc.impl_fake')
self.conf.set_default('rpc_backend_zeromq', False)
self.conf.set_default('rpc_cast_timeout', 5) self.conf.set_default('rpc_cast_timeout', 5)
self.conf.set_default('rpc_response_timeout', 5) self.conf.set_default('rpc_response_timeout', 5)
self.conf.set_default('connection', "sqlite://", group='database') self.conf.set_default('connection', "sqlite://", group='database')

View File

@ -0,0 +1,3 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0

View File

@ -0,0 +1,46 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
import zerorpc
from oslo_config import cfg
from sysinv.zmq_rpc.serializer import decode
from sysinv.zmq_rpc.serializer import encode
CONF = cfg.CONF
class ClientProvider(object):
def __init__(self):
self.clients = {}
def _create_client(self, endpoint):
# pylint: disable=unexpected-keyword-arg
return zerorpc.Client(
connect_to=endpoint,
encoder=encode,
decoder=decode,
# TODO: with the default of 5s we get heartbeat timeouts when
# executing some RPCs that take longer than that to finish.
# We need to understand why this is happening because this scenario
# should be supported by zerorpc
heartbeat=None,
# TODO: we need to determine the correct timeout value here based on
# the max time an RPC can take to execute
timeout=CONF.rpc_response_timeout)
def get_client_for_endpoint(self, endpoint):
client = self.clients.get(endpoint, None)
if client is None:
client = self._create_client(endpoint)
self.clients[endpoint] = client
return client
def cleanup(self):
for endpoint, client in self.clients.items():
try:
client.close()
except Exception:
pass
self.clients.clear()

View File

@ -0,0 +1,65 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
import datetime
import ipaddress
import netaddr
import uuid
from oslo_utils import timeutils
from sysinv.objects.base import SysinvObject
from sysinv.common.context import RequestContext
from sysinv.openstack.common.context import RequestContext as BaseRequestContext
from sysinv.openstack.common.rpc.amqp import RpcContext
from sysinv.openstack.common.rpc.common import CommonRpcContext
def encode(obj, chain=None):
if isinstance(obj, (RequestContext, BaseRequestContext,
RpcContext, CommonRpcContext)):
if isinstance(obj, RequestContext):
context_type = b'request'
elif isinstance(obj, BaseRequestContext):
context_type = b'base_request'
elif isinstance(obj, RpcContext):
context_type = b'rpc'
else:
context_type = b'common_rpc'
return {b'context': True,
b'context_type': context_type,
b'data': obj.to_dict()}
if hasattr(obj, 'obj_to_primitive') and callable(obj.obj_to_primitive):
return obj.obj_to_primitive()
if isinstance(obj, datetime.datetime):
return obj.strftime(timeutils.PERFECT_TIME_FORMAT)
if isinstance(obj, uuid.UUID):
return str(obj)
if netaddr and isinstance(obj, (netaddr.IPAddress, netaddr.IPNetwork)):
return str(obj)
if ipaddress and isinstance(obj,
(ipaddress.IPv4Address,
ipaddress.IPv6Address)):
return str(obj)
if isinstance(obj, Exception):
return repr(obj)
return obj if chain is None else chain(obj)
def decode(obj, chain=None):
try:
if b'context' in obj:
context_dict = obj[b'data']
context_type = obj[b'context_type']
if context_type == b'request':
return RequestContext.from_dict(context_dict)
if context_type == b'base_request':
return BaseRequestContext.from_dict(context_dict)
if context_type == b'rpc':
return RpcContext.from_dict(context_dict)
return CommonRpcContext.from_dict(context_dict)
if isinstance(obj, dict) and 'sysinv_object.name' in obj:
return SysinvObject.obj_from_primitive(obj)
return obj if chain is None else chain(obj)
except KeyError:
return obj if chain is None else chain(obj)

View File

@ -0,0 +1,237 @@
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
import zerorpc
import eventlet
import os
from eventlet import greenthread
from oslo_log import log
from zerorpc import exceptions
from sysinv.db import api
from sysinv.objects.base import SysinvObject
from sysinv.zmq_rpc.client_provider import ClientProvider
from sysinv.zmq_rpc.serializer import decode
from sysinv.zmq_rpc.serializer import encode
import sysinv.openstack.common.rpc.common as rpc_common
import tsconfig.tsconfig as tsc
LOG = log.getLogger(__name__)
client_provider = ClientProvider()
class RpcWrapper(object):
def __init__(self, target):
self.target = target
self.target_methods = [f for f in dir(self.target) if
not f.startswith('_')]
def __getattr__(self, func):
def method(context, **kwargs):
if func in self.target_methods:
# hydrate any sysinv object passed as argument with the context
kwargs = self._inject_context(context, kwargs)
LOG.debug("Calling RPC server method {} with context {} args {}"
.format(func, context, kwargs))
retval = getattr(self.target, func)(context, **kwargs)
LOG.debug("Finished RPC server method {} with context {} args {}"
.format(func, context, kwargs))
return retval
else:
raise AttributeError
return method
def __dir__(self):
return dir(self.target)
def _process_iterable(self, context, action_fn, values):
"""Process an iterable, taking an action on each value.
:param:context: Request context
:param:action_fn: Action to take on each item in values
:param:values: Iterable container of things to take action on
:returns: A new container of the same type (except set) with
items from values having had action applied.
"""
iterable = values.__class__
if iterable == set:
# NOTE(danms): A set can't have an unhashable value inside, such as
# a dict. Convert sets to tuples, which is fine, since we can't
# send them over RPC anyway.
iterable = tuple
return iterable([action_fn(context, value) for value in values])
def _inject_context_to_arg(self, ctx, arg):
if isinstance(arg, SysinvObject):
arg._context = ctx
elif isinstance(arg, (tuple, list, set)):
arg = self._process_iterable(ctx, self._inject_context_to_arg, arg)
return arg
def _inject_context(self, context, kwargs):
new_kwargs = dict()
for argname, arg in kwargs.items():
new_kwargs[argname] = self._inject_context_to_arg(context, arg)
return new_kwargs
class ZmqRpcServer(object):
def __init__(self, target, host, port):
self.target = target
self.endpoint = get_tcp_endpoint(host, port)
self.server = None
def run(self):
def _run_in_thread():
try:
LOG.info("Starting zmq server at {}".format(self.endpoint))
# pylint: disable=unexpected-keyword-arg
# TODO with the default of 5s hearbeat we get LostRemote
# exceptions when executing some RPCs that take longer than
# that to finish. We need to understand why this happens
# because this scenario should be supported by zerorpc
self.server = zerorpc.Server(RpcWrapper(self.target),
heartbeat=None,
encoder=encode,
decoder=decode)
self.server.bind(self.endpoint)
self.server.run()
except eventlet.greenlet.GreenletExit:
return
except Exception as e:
LOG.error("Error while running zmq rpc server at {}: "
"{}".format(self.endpoint, str(e)))
return
return greenthread.spawn(_run_in_thread)
def stop(self):
if self.server:
self.server.close()
client_provider.cleanup()
class ZmqRpcClient(object):
def __init__(self, host, port, topic):
try:
self.host = host
self.port = port
self.topic = topic
self.client = None
if host is not None:
endpoint = get_tcp_endpoint(host, port)
self.client = client_provider.get_client_for_endpoint(endpoint)
LOG.debug("Started zmq rpc client to [{}]:{}".format(
self.host, self.port))
except Exception as e:
LOG.error("Error while running zmq client to {}:{}: {}".format(
self.host, self.port, str(e)))
def _exec(self, client, context, method, **kwargs):
if not client:
host_uuid = kwargs.get('host_uuid', None)
if host_uuid is None:
raise Exception("Missing host_uuid parameter for rpc endpoint")
dbapi = api.get_instance()
host = dbapi.ihost_get(host_uuid)
endpoint = get_tcp_endpoint(host.mgmt_ip, self.port)
client = client_provider.get_client_for_endpoint(endpoint)
try:
LOG.debug(
"Calling RPC client method {} with context {} args {}".format(
method, context, kwargs))
return getattr(client, method)(context, **kwargs)
except exceptions.TimeoutExpired:
raise rpc_common.Timeout(topic=self.topic,
method=method)
except exceptions.RemoteError as e:
raise rpc_common.RemoteError(exc_type=e.name,
value=e.msg,
traceback=e.traceback)
except exceptions.LostRemote as e:
raise rpc_common.LostRemote(lost_remote_msg=str(e),
topic=self.topic,
method=method)
def call(self, context, msg, timeout=None):
method = msg['method']
args = msg['args']
if timeout is not None:
args['timeout_'] = timeout
return self._exec(self.client, context, method, **args)
def cast(self, context, msg):
method = msg['method']
args = msg['args']
args['async_'] = True
return self._exec(self.client, context, method, **args)
def fanout_cast(self, context, msg):
method = msg['method']
args = msg['args']
args['async_'] = True
endpoints = self.get_fanout_endpoints()
for endpoint in endpoints:
client = client_provider.get_client_for_endpoint(endpoint)
LOG.debug("Calling fanout method {} to endpoint {}".format(
method, endpoint))
self._exec(client, context, method, **args)
def get_fanout_endpoints(self):
endpoints = []
dbapi = api.get_instance()
hosts = dbapi.ihost_get_list()
for host in hosts:
LOG.debug(
"Evaluating host {} to add as endpoint ("
"availability={}, operational={}, "
"personality={}, subfunctions={})".format(
host.hostname, host.availability, host.operational,
host.personality, host.subfunctions))
endpoint = get_tcp_endpoint(host.mgmt_ip, self.port)
endpoints.append(endpoint)
LOG.debug("Add host {} with endpoint {} to fanout request".format(
host.hostname, endpoint))
if not endpoints:
endpoint = get_tcp_endpoint("::", self.port)
LOG.warning("No host available. Add localhost with endpoint {} "
"to fanout request.".format(endpoint))
endpoints.append(endpoint)
return endpoints
# TODO(RPCHybridMode): This function is only useful for 21.12 -> 22.12 upgrades.
# Remove in future release.
def is_rpc_hybrid_mode_active():
return os.path.isfile(tsc.SYSINV_HYBRID_RPC_FLAG)
# TODO(RPCHybridMode): This function is only useful for 21.12 -> 22.12 upgrades.
# Remove in future release.
def is_zmq_backend_available(host_uuid):
dbapi = api.get_instance()
host = dbapi.ihost_get(host_uuid)
host_upgrade = dbapi.host_upgrade_get_by_host(host.id)
target_load = dbapi.load_get(host_upgrade.target_load)
return target_load.software_version >= tsc.SW_VERSION_22_12
def get_tcp_endpoint(host, port):
return "tcp://[{}]:{}".format(host, port)
def check_connection(host, port):
ret = True
client = zerorpc.Client(heartbeat=None)
try:
client.connect(get_tcp_endpoint(host, port))
client._zerorpc_list()
except (zerorpc.TimeoutExpired, zerorpc.RemoteError):
ret = False
client.close()
return ret

View File

@ -249,6 +249,10 @@ UPGRADE_ABORT_FLAG = os.path.join(
PTP_UPDATE_PARAMETERS_DONE = '.update_ptp_parameters_done' PTP_UPDATE_PARAMETERS_DONE = '.update_ptp_parameters_done'
PTP_UPDATE_PARAMETERS_FLAG = os.path.join(CONFIG_PATH, PTP_UPDATE_PARAMETERS_FLAG = os.path.join(CONFIG_PATH,
PTP_UPDATE_PARAMETERS_DONE) PTP_UPDATE_PARAMETERS_DONE)
# TODO(RPCHybridMode): This is required only for 21.12 -> 22.12 upgrades.
# Remove in future release.
SYSINV_HYBRID_RPC_FLAG = os.path.join(
PLATFORM_CONF_PATH, '.sysinv_hybrid_rpc')
# Set on controller-0 (by controller-1) to indicate that data migration has # Set on controller-0 (by controller-1) to indicate that data migration has
# started # started