238 lines
8.9 KiB
Python
238 lines
8.9 KiB
Python
# 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
|