示例#1
0
def _multi_send(method, context, topic, msg, timeout=None,
                envelope=False, _msg_id=None):
    """Wraps the sending of messages.

    Dispatches to the matchmaker and sends message to all relevant hosts.
    """
    conf = CONF
    LOG.debug("%(msg)s" % {'msg': ' '.join(map(pformat, (topic, msg)))})

    queues = _get_matchmaker().queues(topic)
    LOG.debug("Sending message(s) to: %s", queues)

    # Don't stack if we have no matchmaker results
    if not queues:
        LOG.warn(_("No matchmaker results. Not casting."))
        # While not strictly a timeout, callers know how to handle
        # this exception and a timeout isn't too big a lie.
        raise rpc_common.Timeout(_("No match from matchmaker."))

    # This supports brokerless fanout (addresses > 1)
    for queue in queues:
        (_topic, ip_addr) = queue
        _addr = "tcp://%s:%s" % (ip_addr, conf.rpc_zmq_port)

        if method.__name__ == '_cast':
            eventlet.spawn_n(method, _addr, context,
                             _topic, msg, timeout, envelope,
                             _msg_id)
            return
        return method(_addr, context, _topic, msg, timeout,
                      envelope)
示例#2
0
    def __call__(self, message_data):
        """Consumer callback to call a method on a proxy object.

        Parses the message for validity and fires off a thread to call the
        proxy object method.

        Message data should be a dictionary with two keys:
            method: string representing the method to call
            args: dictionary of arg: value

        Example: {'method': 'echo', 'args': {'value': 42}}

        """
        # It is important to clear the context here, because at this point
        # the previous context is stored in local.store.context
        if hasattr(local.store, 'context'):
            del local.store.context
        rpc_common._safe_log(LOG.debug, 'received %s', message_data)
        self.msg_id_cache.check_duplicate_message(message_data)
        ctxt = unpack_context(self.conf, message_data)
        method = message_data.get('method')
        args = message_data.get('args', {})
        version = message_data.get('version')
        namespace = message_data.get('namespace')
        if not method:
            LOG.warn(_('no method for message: %s') % message_data)
            ctxt.reply(_('No method for message: %s') % message_data,
                       connection_pool=self.connection_pool)
            return
        self.pool.spawn_n(self._process_data, ctxt, version, method, namespace,
                          args)
示例#3
0
 def _process_data(self, message_data):
     msg_id = message_data.pop('_msg_id', None)
     waiter = self._call_waiters.get(msg_id)
     if not waiter:
         LOG.warn(
             _('No calling threads waiting for msg_id : %(msg_id)s'
               ', message : %(data)s'), {
                   'msg_id': msg_id,
                   'data': message_data
               })
         LOG.warn(_('_call_waiters: %s') % str(self._call_waiters))
     else:
         waiter.put(message_data)
示例#4
0
    def __init__(self, info=None, topic=None, method=None):
        """Initiates Timeout object.

        :param info: Extra info to convey to the user
        :param topic: The topic that the rpc call was sent to
        :param rpc_method_name: The name of the rpc method being
                                called
        """
        self.info = info
        self.topic = topic
        self.method = method
        super(Timeout, self).__init__(None,
                                      info=info or _('<unknown>'),
                                      topic=topic or _('<unknown>'),
                                      method=method or _('<unknown>'))
示例#5
0
 def read(self, i=None):
     result = self.data.read(i)
     self.bytes_read += len(result)
     if self.bytes_read > self.limit:
         msg = _("Request is too large.")
         raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg)
     return result
示例#6
0
    def acquire(self):
        basedir = os.path.dirname(self.fname)

        if not os.path.exists(basedir):
            fileutils.ensure_tree(basedir)
            LOG.info(_LI('Created lock path: %s'), basedir)

        self.lockfile = open(self.fname, 'w')

        while True:
            try:
                # Using non-blocking locks since green threads are not
                # patched to deal with blocking locking calls.
                # Also upon reading the MSDN docs for locking(), it seems
                # to have a laughable 10 attempts "blocking" mechanism.
                self.trylock()
                LOG.debug('Got file lock "%s"', self.fname)
                return True
            except IOError as e:
                if e.errno in (errno.EACCES, errno.EAGAIN):
                    # external locks synchronise things like iptables
                    # updates - give it some time to prevent busy spinning
                    time.sleep(0.01)
                else:
                    raise threading.ThreadError(
                        _("Unable to acquire lock on"
                          " `%(filename)s` due to"
                          " %(exception)s") % {
                              'filename': self.fname,
                              'exception': e,
                          })
示例#7
0
class Timeout(RPCException):
    """Signifies that a timeout has occurred.

    This exception is raised if the rpc_response_timeout is reached while
    waiting for a response from the remote side.
    """
    msg_fmt = _('Timeout while waiting on RPC response - '
                'topic: "%(topic)s", RPC method: "%(method)s" '
                'info: "%(info)s"')

    def __init__(self, info=None, topic=None, method=None):
        """Initiates Timeout object.

        :param info: Extra info to convey to the user
        :param topic: The topic that the rpc call was sent to
        :param rpc_method_name: The name of the rpc method being
                                called
        """
        self.info = info
        self.topic = topic
        self.method = method
        super(Timeout, self).__init__(None,
                                      info=info or _('<unknown>'),
                                      topic=topic or _('<unknown>'),
                                      method=method or _('<unknown>'))
示例#8
0
    def deprecated(self, msg, *args, **kwargs):
        """Call this method when a deprecated feature is used.

        If the system is configured for fatal deprecations then the message
        is logged at the 'critical' level and :class:`DeprecatedConfig` will
        be raised.

        Otherwise, the message will be logged (once) at the 'warn' level.

        :raises: :class:`DeprecatedConfig` if the system is configured for
                 fatal deprecations.

        """
        stdmsg = _("Deprecated: %s") % msg
        if CONF.fatal_deprecations:
            self.critical(stdmsg, *args, **kwargs)
            raise DeprecatedConfig(msg=stdmsg)

        # Using a list because a tuple with dict can't be stored in a set.
        sent_args = self._deprecated_messages_sent.setdefault(msg, list())

        if args in sent_args:
            # Already logged this message, so don't log it again.
            return

        sent_args.append(args)
        self.warn(stdmsg, *args, **kwargs)
示例#9
0
            def publisher(waiter):
                LOG.info(_LI("Creating proxy for topic: %s"), topic)

                try:
                    # The topic is received over the network,
                    # don't trust this input.
                    if self.badchars.search(topic) is not None:
                        emsg = _("Topic contained dangerous characters.")
                        LOG.warn(emsg)
                        raise RPCException(emsg)

                    out_sock = ZmqSocket("ipc://%s/zmq_topic_%s" %
                                         (ipc_dir, topic),
                                         sock_type, bind=True)
                except RPCException:
                    waiter.send_exception(*sys.exc_info())
                    return

                self.topic_proxy[topic] = eventlet.queue.LightQueue(
                    CONF.rpc_zmq_topic_backlog)
                self.sockets.append(out_sock)

                # It takes some time for a pub socket to open,
                # before we can have any faith in doing a send() to it.
                if sock_type == zmq.PUB:
                    eventlet.sleep(.5)

                waiter.send(True)

                while(True):
                    data = self.topic_proxy[topic].get()
                    out_sock.send(data, copy=False)
示例#10
0
    def __init__(self, addr, zmq_type, bind=True, subscribe=None):
        self.sock = _get_ctxt().socket(zmq_type)
        self.addr = addr
        self.type = zmq_type
        self.subscriptions = []

        # Support failures on sending/receiving on wrong socket type.
        self.can_recv = zmq_type in (zmq.PULL, zmq.SUB)
        self.can_send = zmq_type in (zmq.PUSH, zmq.PUB)
        self.can_sub = zmq_type in (zmq.SUB, )

        # Support list, str, & None for subscribe arg (cast to list)
        do_sub = {
            list: subscribe,
            str: [subscribe],
            type(None): []
        }[type(subscribe)]

        for f in do_sub:
            self.subscribe(f)

        str_data = {'addr': addr, 'type': self.socket_s(),
                    'subscribe': subscribe, 'bind': bind}

        LOG.debug("Connecting to %(addr)s with %(type)s", str_data)
        LOG.debug("-> Subscribed to %(subscribe)s", str_data)
        LOG.debug("-> bind: %(bind)s", str_data)

        try:
            if bind:
                self.sock.bind(addr)
            else:
                self.sock.connect(addr)
        except Exception:
            raise RPCException(_("Could not open socket."))
示例#11
0
def bool_from_string(subject, strict=False, default=False):
    """Interpret a string as a boolean.

    A case-insensitive match is performed such that strings matching 't',
    'true', 'on', 'y', 'yes', or '1' are considered True and, when
    `strict=False`, anything else returns the value specified by 'default'.

    Useful for JSON-decoded stuff and config file parsing.

    If `strict=True`, unrecognized values, including None, will raise a
    ValueError which is useful when parsing values passed in from an API call.
    Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
    """
    if not isinstance(subject, six.string_types):
        subject = str(subject)

    lowered = subject.strip().lower()

    if lowered in TRUE_STRINGS:
        return True
    elif lowered in FALSE_STRINGS:
        return False
    elif strict:
        acceptable = ', '.join(
            "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
        msg = _("Unrecognized value '%(val)s', acceptable values are:"
                " %(acceptable)s") % {'val': subject,
                                      'acceptable': acceptable}
        raise ValueError(msg)
    else:
        return default
示例#12
0
 def __iter__(self):
     for chunk in self.data:
         self.bytes_read += len(chunk)
         if self.bytes_read > self.limit:
             msg = _("Request is too large.")
             raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg)
         else:
             yield chunk
示例#13
0
 def __call__(self, req):
     if req.content_length > CONF.max_request_body_size:
         msg = _("Request is too large.")
         raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg)
     if req.content_length is None and req.is_body_readable:
         limiter = LimitingReader(req.body_file,
                                  CONF.max_request_body_size)
         req.body_file = limiter
     return self.application
示例#14
0
 def add_call_waiter(self, waiter, msg_id):
     self._num_call_waiters += 1
     if self._num_call_waiters > self._num_call_waiters_wrn_threshold:
         LOG.warn(
             _('Number of call waiters is greater than warning '
               'threshold: %d. There could be a MulticallProxyWaiter '
               'leak.') % self._num_call_waiters_wrn_threshold)
         self._num_call_waiters_wrn_threshold *= 2
     self._call_waiters[msg_id] = waiter
示例#15
0
def string_to_bytes(text, unit_system='IEC', return_int=False):
    """Converts a string into an float representation of bytes.

    The units supported for IEC ::

        Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it)
        KB, KiB, MB, MiB, GB, GiB, TB, TiB

    The units supported for SI ::

        kb(it), Mb(it), Gb(it), Tb(it)
        kB, MB, GB, TB

    Note that the SI unit system does not support capital letter 'K'

    :param text: String input for bytes size conversion.
    :param unit_system: Unit system for byte size conversion.
    :param return_int: If True, returns integer representation of text
                       in bytes. (default: decimal)
    :returns: Numerical representation of text in bytes.
    :raises ValueError: If text has an invalid value.

    """
    try:
        base, reg_ex = UNIT_SYSTEM_INFO[unit_system]
    except KeyError:
        msg = _('Invalid unit system: "%s"') % unit_system
        raise ValueError(msg)
    match = reg_ex.match(text)
    if match:
        magnitude = float(match.group(1))
        unit_prefix = match.group(2)
        if match.group(3) in ['b', 'bit']:
            magnitude /= 8
    else:
        msg = _('Invalid string format: %s') % text
        raise ValueError(msg)
    if not unit_prefix:
        res = magnitude
    else:
        res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix])
    if return_int:
        return int(math.ceil(res))
    return res
示例#16
0
def _get_matchmaker(*args, **kwargs):
    global matchmaker
    if not matchmaker:
        mm = CONF.rpc_zmq_matchmaker
        if mm.endswith('matchmaker.MatchMakerRing'):
            mm.replace('matchmaker', 'matchmaker_ring')
            LOG.warn(_('rpc_zmq_matchmaker = %(orig)s is deprecated; use'
                       ' %(new)s instead') % dict(
                     orig=CONF.rpc_zmq_matchmaker, new=mm))
        matchmaker = importutils.import_object(mm, *args, **kwargs)
    return matchmaker
示例#17
0
class LogConfigError(Exception):

    message = _('Error loading logging config %(log_config)s: %(err_msg)s')

    def __init__(self, log_config, err_msg):
        self.log_config = log_config
        self.err_msg = err_msg

    def __str__(self):
        return self.message % dict(log_config=self.log_config,
                                   err_msg=self.err_msg)
示例#18
0
    def reconnect(self):
        """Handles reconnecting and re-establishing queues.
        Will retry up to self.max_retries number of times.
        self.max_retries = 0 means to retry forever.
        Sleep between tries, starting at self.interval_start
        seconds, backing off self.interval_stepping number of seconds
        each attempt.
        """

        attempt = 0
        while True:
            params = self.params_list[next(self.next_broker_indices)]
            attempt += 1
            try:
                self._connect(params)
                return
            except (IOError, self.connection_errors) as e:
                pass
            except Exception as e:
                # NOTE(comstud): Unfortunately it's possible for amqplib
                # to return an error not covered by its transport
                # connection_errors in the case of a timeout waiting for
                # a protocol response.  (See paste link in LP888621)
                # So, we check all exceptions for 'timeout' in them
                # and try to reconnect in this case.
                if 'timeout' not in str(e):
                    raise

            log_info = {}
            log_info['err_str'] = str(e)
            log_info['max_retries'] = self.max_retries
            log_info.update(params)

            if self.max_retries and attempt == self.max_retries:
                msg = _('Unable to connect to AMQP server on '
                        '%(hostname)s:%(port)d after %(max_retries)d '
                        'tries: %(err_str)s') % log_info
                LOG.error(msg)
                raise rpc_common.RPCException(msg)

            if attempt == 1:
                sleep_time = self.interval_start or 1
            elif attempt > 1:
                sleep_time += self.interval_stepping
            if self.interval_max:
                sleep_time = min(sleep_time, self.interval_max)

            log_info['sleep_time'] = sleep_time
            LOG.error(
                _LE('AMQP server on %(hostname)s:%(port)d is '
                    'unreachable: %(err_str)s. Trying again in '
                    '%(sleep_time)d seconds.') % log_info)
            time.sleep(sleep_time)
示例#19
0
def notify(context, publisher_id, event_type, priority, payload):
    """Sends a notification using the specified driver

    :param publisher_id: the source worker_type.host of the message
    :param event_type:   the literal type of event (ex. Instance Creation)
    :param priority:     patterned after the enumeration of Python logging
                         levels in the set (DEBUG, WARN, INFO, ERROR, CRITICAL)
    :param payload:       A python dictionary of attributes

    Outgoing message format includes the above parameters, and appends the
    following:

    message_id
      a UUID representing the id for this notification

    timestamp
      the GMT timestamp the notification was sent at

    The composite message will be constructed as a dictionary of the above
    attributes, which will then be sent via the transport mechanism defined
    by the driver.

    Message example::

        {'message_id': str(uuid.uuid4()),
         'publisher_id': 'compute.host1',
         'timestamp': timeutils.utcnow(),
         'priority': 'WARN',
         'event_type': 'compute.create_instance',
         'payload': {'instance_id': 12, ... }}

    """
    if priority not in log_levels:
        raise BadPriorityException(_('%s not in valid priorities') % priority)

    # Ensure everything is JSON serializable.
    payload = jsonutils.to_primitive(payload, convert_instances=True)

    msg = dict(message_id=str(uuid.uuid4()),
               publisher_id=publisher_id,
               event_type=event_type,
               priority=priority,
               payload=payload,
               timestamp=str(timeutils.utcnow()))

    for driver in _get_drivers():
        try:
            driver.notify(context, msg)
        except Exception as e:
            LOG.exception(
                _LE("Problem '%(e)s' attempting to "
                    "send to notification system. "
                    "Payload=%(payload)s") % dict(e=e, payload=payload))
示例#20
0
 def _extract_bytes(self, details):
     # Replace it with the byte amount
     real_size = self.SIZE_RE.search(details)
     if not real_size:
         raise ValueError(_('Invalid input value "%s".') % details)
     magnitude = real_size.group(1)
     unit_of_measure = real_size.group(2)
     bytes_info = real_size.group(3)
     if bytes_info:
         return int(real_size.group(4))
     elif not unit_of_measure:
         return int(magnitude)
     return strutils.string_to_bytes('%s%sB' % (magnitude, unit_of_measure),
                                     return_int=True)
示例#21
0
    def _run(self, name, method_type, args, kwargs, func):
        if method_type not in ('pre', 'post'):
            msg = _("Wrong type of hook method. "
                    "Only 'pre' and 'post' type allowed")
            raise ValueError(msg)

        for extension in self.api.extensions:
            obj = extension.obj
            hook_method = getattr(obj, method_type, None)
            if hook_method:
                msg = (_("Running %(name)s %(type)s-hook: %(obj)s") % {
                    'name': name,
                    'type': method_type,
                    'obj': obj
                })
                LOG.debug(msg)
                try:
                    if func:
                        hook_method(func, *args, **kwargs)
                    else:
                        hook_method(*args, **kwargs)
                except Exception:
                    LOG.exception(_LE('Error during %s-hook') % method_type)
示例#22
0
 def _extract_details(self, root_cmd, root_details, lines_after):
     real_details = root_details
     if root_cmd == 'backing_file':
         # Replace it with the real backing file
         backing_match = self.BACKING_FILE_RE.match(root_details)
         if backing_match:
             real_details = backing_match.group(2).strip()
     elif root_cmd in ['virtual_size', 'cluster_size', 'disk_size']:
         # Replace it with the byte amount (if we can convert it)
         if root_details == 'None':
             real_details = 0
         else:
             real_details = self._extract_bytes(root_details)
     elif root_cmd == 'file_format':
         real_details = real_details.strip().lower()
     elif root_cmd == 'snapshot_list':
         # Next line should be a header, starting with 'ID'
         if not lines_after or not lines_after[0].startswith("ID"):
             msg = _("Snapshot list encountered but no header found")
             raise ValueError(msg)
         del lines_after[0]
         real_details = []
         # This is the sprintf pattern we will try to match
         # "%-10s%-20s%7s%20s%15s"
         # ID TAG VM SIZE DATE VM CLOCK (current header)
         while lines_after:
             line = lines_after[0]
             line_pieces = line.split()
             if len(line_pieces) != 6:
                 break
             # Check against this pattern in the final position
             # "%02d:%02d:%02d.%03d"
             date_pieces = line_pieces[5].split(":")
             if len(date_pieces) != 3:
                 break
             real_details.append({
                 'id':
                 line_pieces[0],
                 'tag':
                 line_pieces[1],
                 'vm_size':
                 line_pieces[2],
                 'date':
                 line_pieces[3],
                 'vm_clock':
                 line_pieces[4] + " " + line_pieces[5],
             })
             del lines_after[0]
     return real_details
示例#23
0
    def start_heartbeat(self):
        """Implementation of MatchMakerBase.start_heartbeat.

        Launches greenthread looping send_heartbeats(),
        yielding for CONF.matchmaker_heartbeat_freq seconds
        between iterations.
        """
        if not self.hosts:
            raise MatchMakerException(_("Register before starting heartbeat."))

        def do_heartbeat():
            while True:
                self.send_heartbeats()
                eventlet.sleep(CONF.matchmaker_heartbeat_freq)

        self._heart = eventlet.spawn(do_heartbeat)
示例#24
0
    def set_rules(self, rules, overwrite=True, use_conf=False):
        """Create a new Rules object based on the provided dict of rules.

        :param rules: New rules to use. It should be an instance of dict.
        :param overwrite: Whether to overwrite current rules or update them
                          with the new rules.
        :param use_conf: Whether to reload rules from cache or config file.
        """

        if not isinstance(rules, dict):
            raise TypeError(
                _("Rules must be an instance of dict or Rules, "
                  "got %s instead") % type(rules))
        self.use_conf = use_conf
        if overwrite:
            self.rules = Rules(rules, self.default_rule)
        else:
            self.rules.update(rules)
示例#25
0
class RemoteError(RPCException):
    """Signifies that a remote class has raised an exception.

    Contains a string representation of the type of the original exception,
    the value of the original exception, and the traceback.  These are
    sent to the parent as a joined string so printing the exception
    contains all of the relevant info.

    """
    msg_fmt = _("Remote error: %(exc_type)s %(value)s\n%(traceback)s.")

    def __init__(self, exc_type=None, value=None, traceback=None):
        self.exc_type = exc_type
        self.value = value
        self.traceback = traceback
        super(RemoteError, self).__init__(exc_type=exc_type,
                                          value=value,
                                          traceback=traceback)
示例#26
0
class RPCException(Exception):
    msg_fmt = _("An unknown RPC related exception occurred.")

    def __init__(self, message=None, **kwargs):
        self.kwargs = kwargs

        if not message:
            try:
                message = self.msg_fmt % kwargs

            except Exception:
                # kwargs doesn't match a variable in the message
                # log the issue and the kwargs
                LOG.exception(_LE('Exception in string format operation'))
                for name, value in six.iteritems(kwargs):
                    LOG.error("%s: %s" % (name, value))
                # at least get the core message out if something happened
                message = self.msg_fmt

        super(RPCException, self).__init__(message)
示例#27
0
def _find_facility_from_conf():
    facility_names = logging.handlers.SysLogHandler.facility_names
    facility = getattr(logging.handlers.SysLogHandler,
                       CONF.syslog_log_facility,
                       None)

    if facility is None and CONF.syslog_log_facility in facility_names:
        facility = facility_names.get(CONF.syslog_log_facility)

    if facility is None:
        valid_facilities = facility_names.keys()
        consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON',
                  'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS',
                  'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP',
                  'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3',
                  'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7']
        valid_facilities.extend(consts)
        raise TypeError(_('syslog facility must be one of: %s') %
                        ', '.join("'%s'" % fac
                                  for fac in valid_facilities))

    return facility
示例#28
0
 def __init__(self, rule):
     msg = _("Policy doesn't allow %s to be performed.") % rule
     super(PolicyNotAuthorized, self).__init__(msg)
示例#29
0
import re

from oslo_config import cfg
import six
import six.moves.urllib.parse as urlparse
import six.moves.urllib.request as urlrequest

from eon.openstack.common import fileutils
from eon.openstack.common.gettextutils import _, _LE
from eon.openstack.common import jsonutils
from eon.openstack.common import log as logging

policy_opts = [
    cfg.StrOpt('policy_file',
               default='policy.json',
               help=_('JSON file containing policy')),
    cfg.StrOpt('policy_default_rule',
               default='default',
               help=_('Rule enforced when requested rule is not found')),
]

CONF = cfg.CONF
CONF.register_opts(policy_opts)

LOG = logging.getLogger(__name__)

_checks = {}


class PolicyNotAuthorized(Exception):
    def __init__(self, rule):
示例#30
0
def link_request_ids(context,
                     source_id,
                     target_id=None,
                     stage=None,
                     target_name=None,
                     notifier=None):
    """Links the Request ID from the Source service to
       the Request ID returned from the Target service.

       Linkages are logged and emitted as INFO notifications.

       :params context: context object
       :params source_id: the Request ID of the source
       :params target_id: the Request ID of the target
       :params stage: optional event name extension to
                      indicate which part of the linkage
                      this is.
       :params target_name: human readable name of the
                            target system you are talking to.
       :params notifier: notifier object

       A typical use case is: System A asking System B
       to perform some action. The linkages might look
       like this:

       link_request_ids(sys_A.request_ID, stage="start")
       # send request to System B and get request ID
       link_request_ids(sys_A.request_ID, target_id=sys_B.request.ID)
       # optionally wait for System B to complete
       link_request_ids(sys_A.request_ID, target_id=sys_B.request.ID,
                        stage="end")

       But, it could be as simple as:
       link_request_ids(sys_A.request_ID, target_id=sys_B.request.ID)
       """

    event_name = "request.link"
    if stage:
        event_name += ".%s" % stage

    rtarget_id = ""
    if target_id:
        rtarget_id = _("TargetId=%(id)s ") % {'id': target_id}

    rtarget_name = ""
    if target_name:
        rtarget_name = _("Target='%(name)s' ") % {'name': target_name}

    arrow = ""
    if target_name or target_id:
        arrow = " -> "

    LOG.info(
        _LI("Request ID Link: %(event_name)s "
            "'%(source_id)s'%(arrow)s"
            "%(target_name)s%(target_id)s") % {
                "event_name": event_name,
                "source_id": source_id,
                "target_name": rtarget_name,
                "arrow": arrow,
                "target_id": rtarget_id
            })

    if notifier:
        payload = {
            "source_request_id": source_id,
            "target_request_id": target_id,
            "target_name": target_name,
            "stage": stage
        }
        notifier.info(context, event_name, payload)