Beispiel #1
0
def push_props_to_isl(tx, subject, *fields):
    if not fields:
        return

    copy_fields = {
        name: 'source.' + name for name in fields}
    q = textwrap.dedent("""
        MATCH (source:link_props) 
        WHERE source.src_switch = $src_switch
          AND source.src_port = $src_port
          AND source.dst_switch = $dst_switch
          AND source.dst_port = $dst_port
        MATCH
          (:switch {name: source.src_switch})
          -
          [target:isl {
            src_switch: source.src_switch,
            src_port: source.src_port,
            dst_switch: source.dst_switch,
            dst_port: source.dst_port
          }]
          ->
          (:switch {name: source.dst_switch})
        """) + db.format_set_fields(
            db.escape_fields(copy_fields, raw_values=True),
            field_prefix='target.')
    p = _make_match(subject)
    db.log_query('link props to ISL', q, p)
    tx.run(q, p)
Beispiel #2
0
def create_if_missing(tx, timestamp, *links):
    q = textwrap.dedent("""
        MATCH (src:switch {name: $src_switch})
        MATCH (dst:switch {name: $dst_switch})
        MERGE (src) - [target:isl {
          src_switch: $src_switch,
          src_port: $src_port,
          dst_switch: $dst_switch,
          dst_port: $dst_port
        }] -> (dst)
        ON CREATE SET 
          target.status=$status, target.actual=$status,
          target.latency=-1,
          target.time_create=$timestamp,
          target.time_modify=$timestamp
        ON MATCH SET target.time_modify=$timestamp""")

    for isl in sorted(links):
        for target in (isl, isl.reversed()):
            p = _make_match(target)
            p['status'] = 'inactive'
            p['timestamp'] = str(timestamp)

            logger.info('Ensure ISL %s exists', target)
            db.log_query('create ISL', q, p)
            tx.run(q, p)
Beispiel #3
0
def sync_with_link_props(tx, isl, *fields):
    copy_fields = {name: 'source.' + name for name in fields}
    q = textwrap.dedent("""
        MATCH (source:link_props) 
        WHERE source.src_switch = $src_switch
          AND source.src_port = $src_port
          AND source.dst_switch = $dst_switch
          AND source.dst_port = $dst_port
        MATCH
          (:switch {name: $src_switch})
          -
          [target:isl {
            src_switch: $src_switch,
            src_port: $src_port,
            dst_switch: $dst_switch,
            dst_port: $dst_port
          }]
          ->
          (:switch {name: $dst_switch})
        """) + db.format_set_fields(
        db.escape_fields(copy_fields, raw_values=True), field_prefix='target.')
    p = _make_match(isl)

    db.log_query('propagate link props to ISL', q, p)
    tx.run(q, p)
Beispiel #4
0
def get_old_flow(new_flow):
    query = (
        "MATCH (a:switch) - [r:flow {"
        " flowid: $flowid "
        "}] -> (b:switch) "
        "WHERE r.cookie <> $cookie "
        "RETURN r ")
    params = {
        'flowid': new_flow['flowid'],
        'cookie': int(new_flow['cookie'])
    }
    db.log_query('Get old flow', query, params)
    old_flows = graph.run(query, params).data()

    if not old_flows:
        message = 'Flow {} not found'.format(new_flow['flowid'])
        logger.error(message)
        # TODO (aovchinnikov): replace with specific exception.
        raise Exception(message)
    else:
        logger.info('Flows were found: %s', old_flows)

    for data in old_flows:
        old_flow = hydrate_flow(data['r'])
        logger.info('check cookies: %s ? %s',
                    new_flow['cookie'], old_flow['cookie'])
        if is_same_direction(new_flow['cookie'], old_flow['cookie']):
            logger.info('Flow was found: flow=%s', old_flow)
            return dict(old_flow)

    # FIXME(surabujin): use custom exception!!!
    raise Exception(
        'Requested flow {}(cookie={}) don\'t found corresponding flow (with '
        'matching direction in Neo4j)'.format(
            new_flow['flowid'], new_flow['cookie']))
Beispiel #5
0
def delete_flow_segments(flow, tx=None):
    """
    Whenever adjusting flow segments, always update available bandwidth. Even when creating a flow
    where we might remove anything old and then create the new .. it isn't guaranteed that the
    old segments are the same as the new segements.. so update bandwidth to be save.
    """
    flow_path = get_flow_path(flow)
    flowid = flow['flowid']
    parent_cookie = flow['cookie']
    logger.debug('DELETE Flow Segments : flowid: %s parent_cookie: 0x%x [path: %s]', flowid, parent_cookie, flow_path)
    delete_segment_query = (
        "MATCH (:switch) - [fs:flow_segment { "
        " flowid: $flowid, "
        " parent_cookie: $parent_cookie "
        "}] -> (:switch) "
        "DELETE fs")
    params = {
        'flowid': flowid,
        'parent_cookie': parent_cookie
    }
    db.log_query('Delete flow segments', delete_segment_query, params)
    if tx:
        tx.run(delete_segment_query, params)
    else:
        graph.run(delete_segment_query, params)
    update_flow_segment_available_bw(flow, tx)
Beispiel #6
0
def del_props(tx, isl, props):
    logger.info('ISL drop %s props request: %s', isl,
                ', '.join(repr(x) for x in props))

    remove = ['target.{}'.format(db.escape(x)) for x in props]
    if not remove:
        return

    remove.insert(0, '')
    p = _make_match(isl)
    q = textwrap.dedent("""
        MATCH
          (:switch {name: $src_switch})
          -
          [target:isl {
            src_switch: $src_switch,
            src_port: $src_port,
            dst_switch: $dst_switch,
            dst_port: $dst_port
          }]
          ->
          (:switch {name: $dst_switch})""") + '\nREMOVE '.join(remove)
    db.log_query('ISL drop props', q, p)
    stats = tx.run(q, p).stats()
    if 'max_bandwidth' in props:
        db_isl = fetch(tx, isl)
        set_props(tx, isl,
                  {'max_bandwidth': db_isl.get('default_max_bandwidth', 0)})
    return stats['contains_updates']
Beispiel #7
0
def drop_by_mask(tx, mask):
    logger.info('Delete link props by mask %s', mask)

    p = _make_match_by_mask(mask)
    if not p:
        raise exc.UnacceptableDataError(
            mask, 'reject to drop all link props in DB')

    where = ['target.{0}=${0}'.format(x) for x in p]
    q = 'MATCH (target:link_props)\n'
    if where:
        q += 'WHERE ' + '\n  AND '.join(where)
    q += '\nRETURN target, id(target) as ref'
    db.log_query('pre delete link props fetch', q, p)

    refs = []
    persistent = []
    for db_record in tx.run(q, p):
        persistent.append(model.LinkProps.new_from_db(db_record['target']))
        refs.append(db_record['ref'])

    q = 'MATCH (target:link_props)\n'
    q += 'WHERE id(target) in [{}]'.format(', '.join(str(x) for x in refs))
    q += '\nDELETE target'
    db.log_query('delete link props', q, {})
    tx.run(q)

    return persistent
Beispiel #8
0
def fetch(tx, isl):
    p = _make_match(isl)
    q = textwrap.dedent("""
        MATCH
          (:switch {name: $src_switch})
          -
          [target:isl {
            src_switch: $src_switch,
            src_port: $src_port,
            dst_switch: $dst_switch,
            dst_port: $dst_port
          }]
          ->
          (:switch {name: $dst_switch})
        RETURN target""")

    db.log_query('ISL fetch', q, p)
    cursor = tx.run(q, p)

    try:
        target = db.fetch_one(cursor)['target']
    except exc.DBEmptyResponse:
        raise exc.DBRecordNotFound(q, p)

    return target
Beispiel #9
0
def remove_flow(flow, parent_tx=None):
    """
    Deletes the flow and its flow segments. Start with flow segments (symmetrical mirror of store_flow).
    Leverage a parent transaction if it exists, otherwise create / close the transaction within this function.

    - flowid **AND** cookie are *the* primary keys for a flow:
        - both the forward and the reverse flow use the same flowid

    NB: store_flow is used for uni-direction .. whereas flow_id is used both directions .. need cookie to differentiate
    """

    logger.info('Remove flow: %s', flow['flowid'])
    tx = parent_tx if parent_tx else graph.begin()
    delete_flow_segments(flow, tx)
    query = (
        "MATCH (:switch) - [f:flow {"
        " flowid: $flowid, "
        " cookie: $cookie "
        "}] -> (:switch) "
        "DELETE f")
    params = {
        'flowid': flow['flowid'],
        'cookie': flow['cookie']
    }
    db.log_query('Remove flow', query, params)
    tx.run(query, params).data()
    if not parent_tx:
        tx.commit()
Beispiel #10
0
def update_config():
    q = 'MERGE (target:config {name: "config"})\n'
    q += db.format_set_fields(db.escape_fields(
            {x: '$' + x for x in features_status_app_to_transport_map.values()},
            raw_values=True), field_prefix='target.')
    p = {
        y: features_status[x]
        for x, y in features_status_app_to_transport_map.items()}

    with graph.begin() as tx:
        db.log_query('CONFIG update', q, p)
        tx.run(q, p)
Beispiel #11
0
def read_config():
    q = 'MATCH (target:config {name: "config"}) RETURN target LIMIT 2'
    db.log_query('CONFIG read', q, None)
    with graph.begin() as tx:
        cursor = tx.run(q)
        try:
            config_node = db.fetch_one(cursor)['target']
            for feature, name in features_status_app_to_transport_map.items():
                features_status[feature] = config_node[name]
        except exc.DBEmptyResponse:
            logger.info(
                    'There is no persistent config in DB, fallback to'
                    ' builtin defaults')
Beispiel #12
0
    def activate_switch(self):
        switch_id = self.payload['switch_id']

        logger.info('Switch %s activation request: timestamp=%s',
                    switch_id, self.timestamp)

        with graph.begin() as tx:
            flow_utils.precreate_switches(tx, switch_id)

            q = 'MATCH (target:switch {name: $dpid}) SET target.state="active"'
            p = {'dpid': switch_id}
            db.log_query('SWITCH activate', q, p)
            tx.run(q, p)
Beispiel #13
0
def get_flow_by_id_and_cookie(flow_id, cookie):
    query = ("MATCH ()-[r:flow]->() WHERE r.flowid=$flow_id "
             "and r.cookie=$cookie RETURN r")
    params = {'flow_id': flow_id, 'cookie': cookie}
    db.log_query('Get flow by id and cookie', query, params)
    result = graph.run(query, params).data()
    if not result:
        return

    flow = hydrate_flow(result[0]['r'])
    logger.debug('Found flow for id %s and cookie %s: %s', flow_id, cookie,
                 flow)
    return flow
Beispiel #14
0
def precreate_switches(tx, *nodes):
    switches = [x.lower() for x in nodes]
    switches.sort()

    for dpid in switches:
        query = (
            "MERGE (sw:switch {name: $dpid}) "
            "ON CREATE SET sw.state='inactive' "
            "ON MATCH SET sw.tx_override_workaround='dummy'")
        params = {
            'dpid': dpid
        }
        db.log_query('Precreate switches', query, params)
        tx.run(query, params)
Beispiel #15
0
def get_flows_by_src_switch(switch_id):
    query = ("MATCH (sw:switch)-[r:flow]->(:switch) "
             "WHERE sw.name=$switch_id RETURN r")
    params = {'switch_id': switch_id}
    db.log_query('Get flows by src switch', query, params)
    result = graph.run(query, params).data()

    flows = []
    for item in result:
        flows.append(hydrate_flow(item['r']))

    logger.debug('Found flows for switch %s: %s', switch_id, flows)

    return flows
Beispiel #16
0
def get_flow_segment_by_src_switch_and_cookie(switch_id, cookie):
    query = ("MATCH p = (sw:switch)-[fs:flow_segment]->(:switch) "
             "WHERE sw.name=$switch_id AND fs.cookie=$cookie "
             "RETURN fs")
    params = {'switch_id': switch_id, 'cookie': cookie}
    db.log_query('Get flow segment by src switch and cookie', query, params)
    result = graph.run(query, params).data()
    if not result:
        return

    segment = result[0]['fs']
    logger.debug('Found segment for switch %s and cookie %s: %s', switch_id,
                 cookie, segment)
    return segment
Beispiel #17
0
def set_props(tx, subject):
    db_subject = fetch(tx, subject)
    origin, update = db.locate_changes(db_subject, subject.props_db_view())
    if update:
        q = textwrap.dedent("""
        MATCH (target:link_props) 
        WHERE id(target)=$target_id
        """) + db.format_set_fields(
                db.escape_fields(update), field_prefix='target.')
        p = {'target_id': db.neo_id(db_subject)}

        db.log_query('propagate link props to ISL', q, p)
        tx.run(q, p)

    return origin, update.keys()
Beispiel #18
0
def get_flow_segments_by_dst_switch(switch_id):
    query = ("MATCH p = (:switch)-[fs:flow_segment]->(sw:switch) "
             "WHERE sw.name=$switch_id "
             "RETURN fs")
    params = {'switch_id': switch_id}
    db.log_query('Get flow segments by dst switch', query, params)
    result = graph.run(query, params).data()

    # group flow_segments by parent cookie, it is helpful for building
    # transit switch rules
    segments = []
    for relationship in result:
        segments.append(relationship['fs'])

    logger.debug('Found segments for switch %s: %s', switch_id, segments)

    return segments
Beispiel #19
0
def set_link_props(tx, isl, props):
    target = fetch_link_props(tx, isl)
    origin, update = _locate_changes(target, props)
    if update:
        q = textwrap.dedent("""
        MATCH (target:link_props) 
        WHERE id(target)=$target_id
        """) + db.format_set_fields(db.escape_fields(update),
                                    field_prefix='target.')

        p = {'target_id': db.neo_id(target)}
        db.log_query('link_props set props', q, p)
        tx.run(q, p)

        sync_with_link_props(tx, isl, *update.keys())

    return origin
Beispiel #20
0
def fetch(tx, subject):
    p = _make_match(subject)
    q = textwrap.dedent("""
        MATCH (target:link_props {
            src_switch: $src_switch,
            src_port: $src_port,
            dst_switch: $dst_switch,
            dst_port: $dst_port})
        RETURN target""")

    db.log_query('link props update', q, p)
    cursor = tx.run(q, p)

    try:
        db_object = db.fetch_one(cursor)['target']
    except exc.DBEmptyResponse:
        raise exc.DBRecordNotFound(q, p)

    return db_object
Beispiel #21
0
def drop(tx, subject):
    logger.info("Delete %s request", subject)
    q = textwrap.dedent("""
        MATCH (target:link_props)
        WHERE target.src_switch=$src_switch,
          AND target.src_port=$src_port,
          AND target.dst_port=$dst_port,
          AND target.dst_switch=$dst_switch
        DELETE target
        RETURN target""")
    p = _make_match(subject)

    db.log_query('delete link props', q, p)
    cursor = tx.run(q, p)
    try:
        db_subject = db.fetch_one(cursor)['target']
    except exc.DBEmptyResponse:
        raise exc.DBRecordNotFound(q, p)

    return model.LinkProps.new_from_db(db_subject)
Beispiel #22
0
def fetch_flow_segments(flowid, parent_cookie):
    """
    :param flowid: the ID for the entire flow, typically consistent across updates, whereas the cookie may change
    :param parent_cookie: the cookie for the flow as a whole; individual segments may vary
    :return: array of segments
    """
    fetch_query = (
        "MATCH (:switch) - [fs:flow_segment { "
        " flowid: $flowid,"
        " parent_cookie: $parent_cookie "
        "}] -> (:switch) "
        "RETURN fs "
        "ORDER BY fs.seq_id")
    params = {
        'flowid': flowid,
        'parent_cookie': parent_cookie
    }
    db.log_query('Fetch flow segments', fetch_query, params)
    # This query returns type py2neo.types.Relationship .. it has a dict method to return the properties
    result = graph.run(fetch_query, params).data()
    return [dict(x['fs']) for x in result]
Beispiel #23
0
def update_isl_bandwidth(src_switch, src_port, dst_switch, dst_port, tx=None):
    """
    This will update the available_bandwidth for the isl that matches the src/dst information.
    It does this by looking for all flow segments over the ISL, where ignore_bandwidth = false.
    Because there may not be any segments, have to use "OPTIONAL MATCH"
    """
    # print('Update ISL Bandwidth from %s:%d --> %s:%d' % (src_switch, src_port, dst_switch, dst_port))

    available_bw_query = (
        "MATCH "
        " (src:switch {name: $src_switch}), "
        " (dst:switch {name: $dst_switch}) "
        "WITH src,dst "
        "MATCH (src) - [i:isl { "
        " src_port: $src_port, "
        " dst_port: $dst_port "
        "}] -> (dst) "
        "WITH src,dst,i "
        "OPTIONAL MATCH (src) - [fs:flow_segment { "
        " src_port: $src_port, "
        " dst_port: $dst_port, "
        " ignore_bandwidth: false "
        "}] -> (dst) "
        "WITH sum(fs.bandwidth) AS used_bandwidth, i as i "
        "SET i.available_bandwidth = i.max_bandwidth - used_bandwidth ")

    logger.debug('Update ISL Bandwidth from %s:%d --> %s:%d' % (src_switch, src_port, dst_switch, dst_port))
    params = {
        'src_switch': src_switch,
        'src_port': src_port,
        'dst_switch': dst_switch,
        'dst_port': dst_port,
    }
    db.log_query('Update ISL bandwidth', available_bw_query, params)
    if tx:
        tx.run(available_bw_query, params)
    else:
        graph.run(available_bw_query, params)
Beispiel #24
0
    def create_switch(self):
        switch_id = self.payload['switch_id']

        logger.info('Switch %s creation request: timestamp=%s',
                    switch_id, self.timestamp)

        with graph.begin() as tx:
            flow_utils.precreate_switches(tx, switch_id)

            p = {
                'address': self.payload['address'],
                'hostname': self.payload['hostname'],
                'description': self.payload['description'],
                'controller': self.payload['controller'],
                'state': 'active'}
            q = 'MATCH (target:switch {name: $dpid})\n' + db.format_set_fields(
                    db.escape_fields(
                            {x: '$' + x for x in p}, raw_values=True),
                    field_prefix='target.')
            p['dpid'] = switch_id

            db.log_query('SWITCH create', q, p)
            tx.run(q, p)
Beispiel #25
0
def create_if_missing(tx, *batch):
    q = textwrap.dedent("""
        MERGE (target:link_props {
            src_switch: $src_switch,
            src_port: $src_port,
            dst_port: $dst_port,
            dst_switch: $dst_switch})
        ON CREATE SET 
          target.time_create=$time_create,
          target.time_modify=$time_modify
        ON MATCH SET target.time_modify=$time_modify""")

    for link in sorted(batch):
        p = _make_match(link)
        time_fields = [link.time_create, link.time_modify]
        time_fields = [
            str(x) if isinstance(x, model.TimeProperty) else x
            for x in time_fields]
        p['time_create'], p['time_modify'] = time_fields

        logger.info('Ensure link property %s exists', link)
        db.log_query('create link props', q, p)
        tx.run(q, p)
Beispiel #26
0
def merge_flow_relationship(flow, tx):
    q = (
        "MERGE (src:switch {name: $src_switch}) "
        " ON CREATE SET src.state = 'inactive' "
        "MERGE (dst:switch {name: $dst_switch}) "
        " ON CREATE SET dst.state = 'inactive' "
        "MERGE (src)-[f:flow {"
        " flowid: $flowid, "
        " cookie: $cookie } ]->(dst)"
        "SET f.src_switch = src.name, "
        " f.src_port = $src_port, "
        " f.src_vlan = $src_vlan, "
        " f.dst_switch = dst.name, "
        " f.dst_port = $dst_port, "
        " f.dst_vlan = $dst_vlan, "
        " f.meter_id = $meter_id, "
        " f.bandwidth = $bandwidth, "
        " f.ignore_bandwidth = $ignore_bandwidth, "
        " f.periodic_pings = $periodic_pings, "
        " f.transit_vlan = $transit_vlan, "
        " f.description = $description, "
        " f.last_updated = $last_updated, "
        " f.flowpath = $flowpath"
    )

    p = model.dash_to_underscore(flow)
    # FIXME(surabujin): do we really want to keep this time representation?
    # FIXME(surabujin): format datetime as '1532609693'(don 't match with
    #                   format used in PCE/resource cache)
    p['last_updated'] = str(int(time.time()))

    path = p['flowpath'].copy()
    path.pop('clazz', None)
    p['flowpath'] = json.dumps(path)

    db.log_query('Save(update) flow', q, p)
    tx.run(q, p)
Beispiel #27
0
def merge_flow_segments(_flow, tx=None):
    """
    This function creates each segment relationship in a flow, and then it calls the function to
    update bandwidth. This should always be down when creating/merging flow segments.

    To create segments, we leverages the flow path .. and the flow path is a series of nodes, where
    each 2 nodes are the endpoints of an ISL.
    """
    flow = copy.deepcopy(_flow)
    create_segment_query = (
        "MERGE "
        " (src:switch {name: $src_switch}) "
        "ON CREATE SET src.state='inactive' "
        "MERGE "
        " (dst:switch {name: $dst_switch}) "
        "ON CREATE SET dst.state='inactive' "
        "MERGE (src) - [fs:flow_segment {"
        " flowid: $flowid, "
        " parent_cookie: $parent_cookie "
        "}] -> (dst) "
        "SET "
        " fs.cookie=$cookie, "
        " fs.src_switch=$src_switch, "
        " fs.src_port=$src_port, "
        " fs.dst_switch=$dst_switch, "
        " fs.dst_port=$dst_port, "
        " fs.seq_id=$seq_id, "
        " fs.segment_latency=$segment_latency, "
        " fs.bandwidth=$bandwidth, "
        " fs.ignore_bandwidth=$ignore_bandwidth ")

    flow_path = get_flow_path(flow)
    flow_cookie = flow['cookie']
    flow['parent_cookie'] = flow_cookie  # primary key of parent is flowid & cookie
    logger.debug('MERGE Flow Segments : %s [path: %s]', flow['flowid'], flow_path)

    for i in range(0, len(flow_path), 2):
        src = flow_path[i]
        dst = flow_path[i+1]
        # <== SRC
        flow['src_switch'] = src['switch_id']
        flow['src_port'] = src['port_no']
        flow['seq_id'] = src['seq_id']
        # Ignore latency if not provided
        flow['segment_latency'] = src.get('segment_latency', 'NULL')
        # ==> DEST
        flow['dst_switch'] = dst['switch_id']
        flow['dst_port'] = dst['port_no']
        # Allow for per segment cookies .. see if it has one set .. otherwise use the cookie of the flow
        # NB: use the "dst cookie" .. since for flow segments, the delete rule will use the dst switch
        flow['cookie'] = dst.get('cookie', flow_cookie)

        db.log_query('Merge flow segment', create_segment_query, flow)
        # TODO: Preference for transaction around the entire delete
        # TODO: Preference for batch command
        if tx:
            tx.run(create_segment_query, flow)
        else:
            graph.run(create_segment_query, flow)

    update_flow_segment_available_bw(flow, tx)
Beispiel #28
0
def update_status(tx, isl, mtime=True):
    logging.info("Sync status both sides of ISL %s to each other", isl)

    q = textwrap.dedent("""
        MATCH
          (:switch {name: $src_switch})
          -
          [self:isl {
            src_switch: $src_switch,
            src_port: $src_port,
            dst_switch: $dst_switch,
            dst_port: $dst_port
          }]
          ->
          (:switch {name: $dst_switch})
        MATCH
          (:switch {name: $peer_src_switch})
          -
          [peer:isl {
            src_switch: $peer_src_switch,
            src_port: $peer_src_port,
            dst_switch: $peer_dst_switch,
            dst_port: $peer_dst_port
          }]
          ->
          (:switch {name: $peer_dst_switch})

        WITH self, peer, CASE 
          WHEN self.actual = $status_up AND peer.actual = $status_up
            THEN $status_up 
          WHEN self.actual = $status_moved OR peer.actual = $status_moved
            THEN $status_moved
          ELSE $status_down 
        END AS isl_status  

        SET self.status=isl_status
        SET peer.status=isl_status""")
    p = {
        'status_up': 'active',
        'status_moved': 'moved',
        'status_down': 'inactive'
    }
    p.update(_make_match(isl))
    p.update({'peer_' + k: v for k, v in _make_match(isl.reversed()).items()})

    expected_update_properties_count = 2
    if mtime:
        if not isinstance(mtime, model.TimeProperty):
            mtime = model.TimeProperty.now()

        q += '\nSET self.time_modify=$mtime, peer.time_modify=$mtime'
        p['mtime'] = str(mtime)
        expected_update_properties_count = 4

    db.log_query('ISL update status', q, p)
    cursor = tx.run(q, p)
    cursor.evaluate()  # to fetch first record

    stats = cursor.stats()
    if stats['properties_set'] != expected_update_properties_count:
        logger.error(
            'Failed to sync ISL\'s %s records statuses. Looks like it is '
            'unidirectional.', isl)