Beispiel #1
0
    def commit(self,
               tid=None,
               transaction=None,
               commit_phase=1,
               commit_mask=0xff,
               newif=False):
        '''
        Commit transaction. In the case of exception all
        changes applied during commit will be reverted.
        '''

        if not commit_phase & commit_mask:
            return self

        def invalidate():
            # on failure, invalidate the interface and detach it
            # from the parent
            # 0. obtain lock on IPDB, to avoid deadlocks
            # ... all the DB updates will wait
            with self.ipdb.exclusive:
                # 1. drop the IPRoute() link
                self.nl = None
                # 2. clean up ipdb
                self.detach()
                # 3. invalidate the interface
                with self._direct_state:
                    for i in tuple(self.keys()):
                        del self[i]
                # 4. the rest
                self._mode = 'invalid'

        error = None
        added = None
        removed = None
        drop = True
        init = None

        if tid:
            transaction = self.global_tx[tid]
        else:
            if transaction:
                drop = False
            else:
                transaction = self.current_tx
        if transaction.partial:
            transaction.errors = []

        with self._write_lock:
            # if the interface does not exist, create it first ;)
            if self['ipdb_scope'] != 'system':

                newif = True
                self.set_target('ipdb_scope', 'system')
                try:
                    # 8<----------------------------------------------------
                    # ACHTUNG: hack for old platforms
                    if self['address'] == '00:00:00:00:00:00':
                        with self._direct_state:
                            self['address'] = None
                            self['broadcast'] = None
                    # 8<----------------------------------------------------
                    init = self.pick()
                    try:
                        self.nl.link('add', **self)
                    except NetlinkError as x:
                        # File exists
                        if x.code == errno.EEXIST:
                            # A bit special case, could be one of two cases:
                            #
                            # 1. A race condition between two different IPDB
                            #    processes
                            # 2. An attempt to create dummy0, gre0, bond0 when
                            #    the corrseponding module is not loaded. Being
                            #    loaded, the module creates a default interface
                            #    by itself, causing the request to fail
                            #
                            # The exception in that case can cause the DB
                            # inconsistence, since there can be queued not only
                            # the interface creation, but also IP address
                            # changes etc.
                            #
                            # So we ignore this particular exception and try to
                            # continue, as it is created by us.
                            pass

                        else:
                            raise
                except Exception as e:
                    if transaction.partial:
                        transaction.errors.append(e)
                        raise PartialCommitException()
                    else:
                        # If link('add', ...) raises an exception, no netlink
                        # broadcast will be sent, and the object is unmodified.
                        # After the exception forwarding, the object is ready
                        # to repeat the commit() call.
                        raise

        if transaction['ipdb_scope'] == 'create' and commit_phase > 1:
            if self['index']:
                wd = self.ipdb.watchdog(action='RTM_DELLINK',
                                        ifname=self['ifname'])
                with self._direct_state:
                    self['ipdb_scope'] = 'locked'
                self.nl.link('delete', index=self['index'])
                wd.wait()
            self.load_dict(transaction)
            return self

        elif newif:
            # Here we come only if a new interface is created
            #
            if commit_phase == 1 and not self.wait_target('ipdb_scope'):
                invalidate()
                raise CreateException()

            # Re-populate transaction.ipaddr to have a proper IP target
            #
            # The reason behind the code is that a new interface in the
            # "up" state will have automatic IPv6 addresses, that aren't
            # reflected in the transaction. This may cause a false IP
            # target mismatch and a commit failure.
            #
            # To avoid that, collect automatic addresses to the
            # transaction manually, since it is not yet properly linked.
            #
            for addr in self.ipdb.ipaddr[self['index']]:
                transaction['ipaddr'].add(addr)

        # now we have our index and IP set and all other stuff
        snapshot = self.pick()

        # resolve all delayed ports
        def resolve_ports(transaction, ports, callback, self, drop):
            def error(x):
                return KeyError('can not resolve port %s' % x)
            for port in tuple(ports):
                ifindex = self._resolve_port(port)
                if ifindex is None:
                    if transaction.partial:
                        transaction.errors.append(error(port))
                    else:
                        if drop:
                            self.drop(transaction.uid)
                        raise error(port)
                else:
                    ports.remove(port)
                    with transaction._direct_state:  # ????
                        callback(ifindex)
        resolve_ports(transaction,
                      transaction._delay_add_port,
                      transaction.add_port,
                      self, drop)
        resolve_ports(transaction,
                      transaction._delay_del_port,
                      transaction.del_port,
                      self, drop)

        try:
            removed, added = snapshot // transaction

            run = transaction._run
            nl = transaction.nl

            # 8<---------------------------------------------
            # Port vlans
            if removed['vlans'] or added['vlans']:

                if added['vlans']:
                    transaction['vlans'].add(1)
                self['vlans'].set_target(transaction['vlans'])

                for i in removed['vlans']:
                    if i != 1:
                        # remove vlan from the port
                        run(nl.vlan_filter, 'del',
                            index=self['index'],
                            vlan_info=self['vlans'][i])

                for i in added['vlans']:
                    if i != 1:
                        # add vlan to the port
                        run(nl.vlan_filter, 'add',
                            index=self['index'],
                            vlan_info=transaction['vlans'][i])

                self['vlans'].target.wait(SYNC_TIMEOUT)
                if not self['vlans'].target.is_set():
                    raise CommitException('vlans target is not set')

            # 8<---------------------------------------------
            # Ports
            if removed['ports'] or added['ports']:
                self['ports'].set_target(transaction['ports'])

                for i in removed['ports']:
                    # detach port
                    if i in self.ipdb.interfaces:
                        (self.ipdb.interfaces[i]
                         .set_target('master', None)
                         .mirror_target('master', 'link'))
                        run(nl.link, 'set', index=i, master=0)
                    else:
                        transaction.errors.append(KeyError(i))

                for i in added['ports']:
                    # attach port
                    if i in self.ipdb.interfaces:
                        (self.ipdb.interfaces[i]
                         .set_target('master', self['index'])
                         .mirror_target('master', 'link'))
                        run(nl.link, 'set', index=i, master=self['index'])
                    else:
                        transaction.errors.append(KeyError(i))

                self['ports'].target.wait(SYNC_TIMEOUT)
                if not self['ports'].target.is_set():
                    raise CommitException('ports target is not set')

                # wait for proper targets on ports
                for i in list(added['ports']) + list(removed['ports']):
                    port = self.ipdb.interfaces[i]
                    target = port._local_targets['master']
                    target.wait(SYNC_TIMEOUT)
                    with port._write_lock:
                        del port._local_targets['master']
                        del port._local_targets['link']
                    if not target.is_set():
                        raise CommitException('master target failed')
                    if i in added['ports']:
                        if port.if_master != self['index']:
                            raise CommitException('master set failed')
                    else:
                        if port.if_master == self['index']:
                            raise CommitException('master unset failed')

            # 8<---------------------------------------------
            # Interface changes
            request = IPLinkRequest()
            for key in added:
                if (key == 'net_ns_fd') or \
                        (key not in self._virtual_fields) and \
                        (key != 'kind'):
                    request[key] = added[key]

            # apply changes only if there is something to apply
            if any([request[item] is not None for item in request]):
                request['index'] = self['index']
                request['kind'] = self['kind']
                if request.get('address', None) == '00:00:00:00:00:00':
                    request.pop('address')
                    request.pop('broadcast', None)
                if tuple(filter(lambda x: x[:3] == 'br_', request)):
                    request['family'] = socket.AF_BRIDGE
                    run(nl.link,
                        (RTM_NEWLINK, NLM_F_REQUEST | NLM_F_ACK),
                        **request)
                else:
                    run(nl.link, 'set', **request)
                # hardcoded pause -- if the interface was moved
                # across network namespaces
                if 'net_ns_fd' in request:
                    while True:
                        # wait until the interface will disappear
                        # from the main network namespace
                        try:
                            for link in self.nl.get_links(self['index']):
                                self.ipdb.interfaces._new(link)
                        except NetlinkError as e:
                            if e.code == errno.ENODEV:
                                break
                            raise
                        except Exception:
                            raise
                    time.sleep(0.1)
                if not transaction.partial:
                    transaction.wait_all_targets()

            # 8<---------------------------------------------
            # IP address changes
            for _ in range(3):
                ip2add = transaction['ipaddr'] - self['ipaddr']
                ip2remove = self['ipaddr'] - transaction['ipaddr']

                if not ip2add and not ip2remove:
                    break

                self['ipaddr'].set_target(transaction['ipaddr'])
                ###
                # Remove
                #
                # The promote_secondaries sysctl causes the kernel
                # to add secondary addresses back after the primary
                # address is removed.
                #
                # The library can not tell this from the result of
                # an external program.
                #
                # One simple way to work that around is to remove
                # secondaries first.
                rip = sorted(ip2remove,
                             key=lambda x: self['ipaddr'][x]['flags'],
                             reverse=True)
                # 8<--------------------------------------
                for i in rip:
                    # Ignore link-local IPv6 addresses
                    if i[0][:4] == 'fe80' and i[1] == 64:
                        continue
                    # When you remove a primary IP addr, all the
                    # subnetwork can be removed. In this case you
                    # will fail, but it is OK, no need to roll back
                    try:
                        run(nl.addr, 'delete', self['index'], i[0], i[1])
                    except NetlinkError as x:
                        # bypass only errno 99,
                        # 'Cannot assign address'
                        if x.code != errno.EADDRNOTAVAIL:
                            raise
                    except socket.error as x:
                        # bypass illegal IP requests
                        if isinstance(x.args[0], basestring) and \
                                x.args[0].startswith('illegal IP'):
                            continue
                        raise
                ###
                # Add addresses
                # 8<--------------------------------------
                for i in ip2add:
                    # Ignore link-local IPv6 addresses
                    if i[0][:4] == 'fe80' and i[1] == 64:
                        continue
                    # Try to fetch additional address attributes
                    try:
                        kwarg = dict([k for k
                                      in transaction['ipaddr'][i].items()
                                      if k[0] in ('broadcast',
                                                  'anycast',
                                                  'scope')])
                    except KeyError:
                        kwarg = None
                    # feed the address to the OS
                    run(nl.addr, 'add', self['index'], i[0], i[1],
                        **kwarg if kwarg else {})

                # 8<--------------------------------------
                # bond and bridge interfaces do not send
                # IPv6 address updates, when are down
                #
                # beside of that, bridge interfaces are
                # down by default, so they never send
                # address updates from beginning
                #
                # so if we need, force address load
                #
                # FIXME: probably, we should handle other
                # types as well
                if self['kind'] in ('bond', 'bridge', 'veth'):
                    for addr in self.nl.get_addr(family=socket.AF_INET6):
                        self.ipdb.ipaddr._new(addr)

                # 8<--------------------------------------
                self['ipaddr'].target.wait(SYNC_TIMEOUT)
                if self['ipaddr'].target.is_set():
                    break
            else:
                raise CommitException('ipaddr target is not set')

            # 8<---------------------------------------------
            # Iterate callback chain
            for ch in self._commit_hooks:
                # An exception will rollback the transaction
                ch(self.dump(), snapshot.dump(), transaction.dump())

            # 8<---------------------------------------------
            # Interface removal
            if (added.get('ipdb_scope') in ('shadow', 'remove')):
                wd = self.ipdb.watchdog(action='RTM_DELLINK',
                                        ifname=self['ifname'])
                if added.get('ipdb_scope') in ('shadow', 'create'):
                    with self._direct_state:
                        self['ipdb_scope'] = 'locked'
                self.nl.link('delete', index=self['index'])
                wd.wait()
                if added.get('ipdb_scope') == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'shadow'
                if added['ipdb_scope'] == 'create':
                    self.load_dict(transaction)
                if drop:
                    self.drop(transaction.uid)
                return self
            # 8<---------------------------------------------

        except Exception as e:
            error = e
            # something went wrong: roll the transaction back
            if commit_phase == 1:
                if newif:
                    drop = False
                try:
                    self.commit(transaction=init if newif else snapshot,
                                commit_phase=2,
                                commit_mask=commit_mask,
                                newif=newif)
                except Exception as i_e:
                    error = RuntimeError()
                    error.cause = i_e
            else:
                # reload all the database -- it can take a long time,
                # but it is required since we have no idea, what is
                # the result of the failure
                links = self.nl.get_links()
                for link in links:
                    self.ipdb.interfaces._new(link)
                links = self.nl.get_vlans()
                for link in links:
                    self.ipdb.interfaces._new(link)
                for addr in self.nl.get_addr():
                    self.ipdb.ipaddr._new(addr)

            error.traceback = traceback.format_exc()
            for key in ('ipaddr', 'ports', 'vlans'):
                self[key].clear_target()

        # raise partial commit exceptions
        if transaction.partial and transaction.errors:
            error = PartialCommitException('partial commit error')

        # if it is not a rollback turn
        if drop and commit_phase == 1:
            # drop last transaction in any case
            self.drop(transaction.uid)

        # raise exception for failed transaction
        if error is not None:
            error.transaction = transaction
            raise error

        time.sleep(config.commit_barrier)

        # drop all collected errors, if any
        self.errors = []
        return self
Beispiel #2
0
    def commit(self,
               tid=None,
               transaction=None,
               commit_phase=1,
               commit_mask=0xff):

        if not commit_phase & commit_mask:
            return self

        error = None
        drop = self.ipdb.txdrop
        devop = 'set'
        debug = {'traceback': None,
                 'next_stage': None}
        notx = True

        if tid or transaction:
            notx = False
        if tid:
            transaction = self.global_tx[tid]
        else:
            transaction = transaction or self.current_tx

        # create a new route
        if self['ipdb_scope'] != 'system':
            devop = 'add'

        # work on an existing route
        snapshot = self.pick()
        added, removed = transaction // snapshot
        added.pop('ipdb_scope', None)
        removed.pop('ipdb_scope', None)

        try:
            # rule add/set
            if any(added.values()) or devop == 'add':

                old_key = self.make_key(self)
                new_key = self.make_key(transaction)

                if new_key != old_key:
                    # check for the key conflict
                    if new_key in self.ipdb.rules:
                        raise CommitException('rule priority conflict')
                    else:
                        self.ipdb.rules[new_key] = self
                        self.nl.rule('del', **old_key._asdict())
                        self.nl.rule('add', **transaction)
                else:
                    if devop != 'add':
                        with self._direct_state:
                            self['ipdb_scope'] = 'locked'
                        wd = self.ipdb.watchdog('RTM_DELRULE',
                                                **old_key._asdict())
                        self.nl.rule('del', **old_key._asdict())
                        wd.wait()
                        with self._direct_state:
                            self['ipdb_scope'] = 'reload'
                    self.nl.rule('add', **transaction)
                transaction.wait_all_targets()
            # rule removal
            if (transaction['ipdb_scope'] in ('shadow', 'remove')) or\
                    ((transaction['ipdb_scope'] == 'create') and
                     commit_phase == 2):
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'locked'
                # create watchdog
                key = self.make_key(snapshot)
                wd = self.ipdb.watchdog('RTM_DELRULE', **key._asdict())
                self.nl.rule('del', **key._asdict())
                wd.wait()
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'shadow'
            # everything ok
            drop = True

        except Exception as e:

            error = e
            # prepare postmortem
            debug['traceback'] = traceback.format_exc()
            debug['error_stack'] = []
            debug['next_stage'] = None

            if commit_phase == 1:
                try:
                    self.commit(transaction=snapshot,
                                commit_phase=2,
                                commit_mask=commit_mask)
                except Exception as i_e:
                    debug['next_stage'] = i_e
                    error = RuntimeError()

        if drop and notx:
            self.drop(transaction.uid)

        if error is not None:
            error.debug = debug
            raise error

        return self
 def wait_all_targets(self):
     for key, target in self._targets.items():
         if key not in self._virtual_fields:
             target.wait(SYNC_TIMEOUT)
             if not target.is_set():
                 raise CommitException('target %s is not set' % key)
Beispiel #4
0
    def commit(self,
               tid=None,
               transaction=None,
               commit_phase=1,
               commit_mask=0xff):

        if not commit_phase & commit_mask:
            return self

        error = None
        drop = self.ipdb.txdrop
        devop = 'set'
        cleanup = []
        # FIXME -- make a debug object
        debug = {'traceback': None, 'next_stage': None}
        notx = True

        if tid or transaction:
            notx = False

        if tid:
            transaction = self.global_tx[tid]
        else:
            transaction = transaction or self.current_tx

        # ignore global rollbacks on invalid routes
        if self['ipdb_scope'] == 'create' and commit_phase > 1:
            return

        # create a new route
        if self['ipdb_scope'] != 'system':
            devop = 'add'

        # work on an existing route
        snapshot = self.pick()
        added, removed = transaction // snapshot
        added.pop('ipdb_scope', None)
        removed.pop('ipdb_scope', None)

        try:
            # route set
            if self['family'] != AF_MPLS:
                cleanup = [
                    any(snapshot['metrics'].values())
                    and not any(added.get('metrics', {}).values()),
                    any(snapshot['encap'].values())
                    and not any(added.get('encap', {}).values())
                ]
            if any(added.values()) or \
                    any(cleanup) or \
                    removed.get('multipath', None) or \
                    devop == 'add':
                # prepare multipath target sync
                wlist = []
                if transaction['multipath']:
                    mplen = len(transaction['multipath'])
                    if mplen == 1:
                        # set up local targets
                        for nh in transaction['multipath']:
                            for key in ('oif', 'gateway', 'newdst'):
                                if nh.get(key, None):
                                    self.set_target(key, nh[key])
                                    wlist.append(key)
                        mpt = None
                    else:

                        def mpcheck(mpset):
                            return len(mpset) == mplen

                        mpt = self['multipath'].set_target(mpcheck, True)
                else:
                    mpt = None

                # prepare the anchor key to catch *possible* route update
                old_key = self.make_key(self)
                new_key = self.make_key(transaction)
                if old_key != new_key:
                    # assume we can not move routes between tables (yet ;)
                    if self['family'] == AF_MPLS:
                        route_index = self.ipdb.routes.tables['mpls'].idx
                    else:
                        route_index = (self.ipdb.routes.tables[self['table']
                                                               or 254].idx)
                    # re-link the route record
                    if new_key in route_index:
                        raise CommitException('route idx conflict')
                    else:
                        route_index[new_key] = {'key': new_key, 'route': self}
                    # wipe the old key, if needed
                    if old_key in route_index:
                        del route_index[old_key]
                self.nl.route(devop, **transaction)
                # delete old record, if required
                if (old_key != new_key) and (devop == 'set'):
                    self.nl.route('del', **dict(old_key._asdict()))
                transaction.wait_all_targets()
                for key in ('metrics', 'via'):
                    if transaction[key] and transaction[key]._targets:
                        transaction[key].wait_all_targets()
                if mpt is not None:
                    mpt.wait(SYNC_TIMEOUT)
                    if not mpt.is_set():
                        raise CommitException('multipath target is not set')
                    self['multipath'].clear_target(mpt)
                for key in wlist:
                    self.wait_target(key)
            # route removal
            if (transaction['ipdb_scope'] in ('shadow', 'remove')) or\
                    ((transaction['ipdb_scope'] == 'create') and
                     commit_phase == 2):
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'locked'
                # create watchdog
                wd = self.ipdb.watchdog('RTM_DELROUTE',
                                        **self.wd_key(snapshot))
                for route in self.nl.route('delete', **snapshot):
                    self.ipdb.routes.load_netlink(route)
                wd.wait()
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'shadow'

            # success, so it's safe to drop the transaction
            drop = True

        except Exception as e:

            error = e
            # prepare postmortem
            debug['traceback'] = traceback.format_exc()
            debug['error_stack'] = []
            debug['next_stage'] = None

            if commit_phase == 1:
                try:
                    self.commit(transaction=snapshot,
                                commit_phase=2,
                                commit_mask=commit_mask)
                except Exception as i_e:
                    debug['next_stage'] = i_e
                    error = RuntimeError()

        if drop and notx:
            self.drop(transaction.uid)

        if error is not None:
            error.debug = debug
            raise error

        self.ipdb.routes.gc()
        return self
Beispiel #5
0
    def commit(self,
               tid=None,
               transaction=None,
               commit_phase=1,
               commit_mask=0xff):

        if not commit_phase & commit_mask:
            return self

        error = None
        drop = True
        devop = 'set'

        if tid:
            transaction = self.global_tx[tid]
        else:
            if transaction:
                drop = False
            else:
                transaction = self.current_tx

        # create a new route
        if self['ipdb_scope'] != 'system':
            devop = 'add'

        # work on an existing route
        snapshot = self.pick()
        added, removed = transaction // snapshot
        added.pop('ipdb_scope', None)
        removed.pop('ipdb_scope', None)

        try:
            # rule add/set
            if any(added.values()) or devop == 'add':

                old_key = self.make_key(self)
                new_key = self.make_key(transaction)

                if new_key != old_key:
                    # check for the key conflict
                    if new_key in self.ipdb.rules:
                        raise CommitException('rule priority conflict')
                    else:
                        self.ipdb.rules[new_key] = self
                        self.nl.rule('del', priority=old_key)
                        self.nl.rule('add', **transaction)
                else:
                    if devop != 'add':
                        with self._direct_state:
                            self['ipdb_scope'] = 'shadow'
                        self.nl.rule('del', priority=old_key)
                        with self._direct_state:
                            self['ipdb_scope'] = 'reload'
                    self.nl.rule('add', **transaction)
                transaction.wait_all_targets()
            # rule removal
            if (transaction['ipdb_scope'] in ('shadow', 'remove')) or\
                    ((transaction['ipdb_scope'] == 'create') and
                     commit_phase == 2):
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'locked'
                # create watchdog
                wd = self.ipdb.watchdog('RTM_DELRULE',
                                        priority=self['priority'])
                for rule in self.nl.rule('delete', **snapshot):
                    self.ipdb.rules.load_netlink(rule)
                wd.wait()
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'shadow'

        except Exception as e:
            if devop == 'add':
                error = e
                self.nl = None
                self['ipdb_scope'] = 'invalid'
                del self.ipdb.rules[self.make_key(self)]
            elif commit_phase == 1:
                ret = self.commit(transaction=snapshot,
                                  commit_phase=2,
                                  commit_mask=commit_mask)
                if isinstance(ret, Exception):
                    error = ret
                else:
                    error = e
            else:
                if drop:
                    self.drop(transaction.uid)
                x = RuntimeError()
                x.cause = e
                raise x

        if drop and commit_phase == 1:
            self.drop(transaction.uid)

        if error is not None:
            error.transaction = transaction
            raise error

        return self
Beispiel #6
0
    def commit(self, tid=None, transaction=None, rollback=False, newif=False):
        '''
        Commit transaction. In the case of exception all
        changes applied during commit will be reverted.
        '''
        error = None
        added = None
        removed = None
        drop = True
        if tid:
            transaction = self._transactions[tid]
        else:
            if transaction:
                drop = False
            else:
                transaction = self.last()
        if transaction.partial:
            transaction.errors = []

        wd = None
        with self._write_lock:
            # if the interface does not exist, create it first ;)
            if self['ipdb_scope'] != 'system':
                request = IPLinkRequest(self.filter('common'))

                # create watchdog
                wd = self.ipdb.watchdog(ifname=self['ifname'])

                newif = True
                try:
                    # 8<----------------------------------------------------
                    # ACHTUNG: hack for old platforms
                    if request.get('address', None) == '00:00:00:00:00:00':
                        del request['address']
                        del request['broadcast']
                    # 8<----------------------------------------------------
                    try:
                        self.nl.link('add', **request)
                    except NetlinkError as x:
                        # File exists
                        if x.code == errno.EEXIST:
                            # A bit special case, could be one of two cases:
                            #
                            # 1. A race condition between two different IPDB
                            #    processes
                            # 2. An attempt to create dummy0, gre0, bond0 when
                            #    the corrseponding module is not loaded. Being
                            #    loaded, the module creates a default interface
                            #    by itself, causing the request to fail
                            #
                            # The exception in that case can cause the DB
                            # inconsistence, since there can be queued not only
                            # the interface creation, but also IP address
                            # changes etc.
                            #
                            # So we ignore this particular exception and try to
                            # continue, as it is created by us.
                            pass

                        # Operation not supported
                        elif x.code == errno.EOPNOTSUPP and \
                                request.get('index', 0) != 0:
                            # ACHTUNG: hack for old platforms
                            request = IPLinkRequest({
                                'ifname': self['ifname'],
                                'kind': self['kind'],
                                'index': 0
                            })
                            self.nl.link('add', **request)
                        else:
                            raise
                except Exception as e:
                    # on failure, invalidate the interface and detach it
                    # from the parent
                    # 1. drop the IPRoute() link
                    self.nl = None
                    # 2. clean up ipdb
                    self.detach()
                    # 3. invalidate the interface
                    with self._direct_state:
                        for i in tuple(self.keys()):
                            del self[i]
                    # 4. the rest
                    self._mode = 'invalid'
                    self._exception = e
                    self._tb = traceback.format_exc()
                    # raise the exception
                    if transaction.partial:
                        transaction.errors.append(e)
                        raise PartialCommitException()
                    else:
                        raise

        if wd is not None:
            wd.wait()
            if self['index'] == 0:
                # Only the interface creation time issue on
                # old or compat platforms. The interface index
                # may be not known yet, but we can not continue
                # without it. It will be updated anyway, but
                # it is better to force the lookup.
                ix = self.nl.link_lookup(ifname=self['ifname'])
                if ix:
                    self['index'] = ix[0]
                else:
                    if transaction.partial:
                        transaction.errors.append(CreateException())
                        raise PartialCommitException()
                    else:
                        raise CreateException()

        # now we have our index and IP set and all other stuff
        snapshot = self.pick()

        # resolve all delayed ports
        def resolve_ports(transaction, ports, callback, self, drop):
            def error(x):
                return KeyError('can not resolve port %s' % x)

            for port in tuple(ports):
                ifindex = self._resolve_port(port)
                if ifindex is None:
                    if transaction.partial:
                        transaction.errors.append(error(port))
                    else:
                        if drop:
                            self.drop(transaction)
                        raise error(port)
                else:
                    ports.remove(port)
                    callback(ifindex, direct=True)

        resolve_ports(transaction, transaction._delay_add_port,
                      transaction.add_port, self, drop)
        resolve_ports(transaction, transaction._delay_del_port,
                      transaction.del_port, self, drop)

        try:
            removed = snapshot - transaction
            added = transaction - snapshot

            run = transaction._run
            nl = transaction.nl

            # 8<---------------------------------------------
            # Port vlans
            self['vlans'].set_target(transaction['vlans'])
            for i in removed['vlans']:
                # remove vlan from the port
                run(nl.link,
                    'vlan-del',
                    index=self['index'],
                    vlan_info=self['vlans'][i])

            for i in added['vlans']:
                # add vlan to the port
                run(nl.link,
                    'vlan-add',
                    index=self['index'],
                    vlan_info=transaction['vlans'][i])

            if (not transaction.partial) and \
                    (removed['vlans'] or added['vlans']):
                self['vlans'].target.wait(SYNC_TIMEOUT)
                if not self['vlans'].target.is_set():
                    raise CommitException('vlans target is not set')

            # 8<---------------------------------------------
            # Ports
            self['ports'].set_target(transaction['ports'])
            for i in removed['ports']:
                # detach port
                if i in self.ipdb.interfaces:
                    port = self.ipdb.interfaces[i]
                    port.set_target('master', None)
                    port.mirror_target('master', 'link')
                    run(nl.link, 'set', index=port['index'], master=0)
                else:
                    transaction.errors.append(KeyError(i))

            for i in added['ports']:
                # attach port
                if i in self.ipdb.interfaces:
                    port = self.ipdb.interfaces[i]
                    port.set_target('master', self['index'])
                    port.mirror_target('master', 'link')
                    run(nl.link,
                        'set',
                        index=port['index'],
                        master=self['index'])
                else:
                    transaction.errors.append(KeyError(i))

            if (not transaction.partial) and \
                    (removed['ports'] or added['ports']):
                for link in self.nl.get_links(*(removed['ports']
                                                | added['ports'])):
                    self.ipdb.device_put(link)
                self['ports'].target.wait(SYNC_TIMEOUT)
                if not self['ports'].target.is_set():
                    raise CommitException('ports target is not set')

                # wait for proper targets on ports
                for i in list(added['ports']) + list(removed['ports']):
                    port = self.ipdb.interfaces[i]
                    target = port._local_targets['master']
                    target.wait(SYNC_TIMEOUT)
                    del port._local_targets['master']
                    del port._local_targets['link']
                    if not target.is_set():
                        raise CommitException('master target failed')
                    if i in added['ports']:
                        if port.if_master != self['index']:
                            raise CommitException('master set failed')
                    else:
                        if port.if_master == self['index']:
                            raise CommitException('master unset failed')

            # 8<---------------------------------------------
            # Interface changes
            request = IPLinkRequest()
            for key in added:
                if (key in self._xfields['common']) and \
                        (key != 'kind'):
                    request[key] = added[key]
            request['index'] = self['index']

            # apply changes only if there is something to apply
            if any([
                    request[item] is not None for item in request
                    if item != 'index'
            ]):
                if request.get('address', None) == '00:00:00:00:00:00':
                    request.pop('address')
                    request.pop('broadcast', None)
                run(nl.link, 'set', **request)
                # hardcoded pause -- if the interface was moved
                # across network namespaces
                if not transaction.partial and 'net_ns_fd' in request:
                    while True:
                        # wait until the interface will disappear
                        # from the main network namespace
                        try:
                            for link in self.nl.get_links(self['index']):
                                self.ipdb.device_put(link)
                        except NetlinkError as e:
                            if e.code == errno.ENODEV:
                                break
                            raise
                        except Exception:
                            raise
                    time.sleep(0.1)

            # 8<---------------------------------------------
            # IP address changes
            #
            # There is one corner case: if the interface didn't
            # exist before commit(), the transaction may not
            # contain automatic IPv6 addresses.
            #
            # So fetch here possible addresses and use it to
            # extend the transaction
            target = self._commit_real_ip().union(set(transaction['ipaddr']))
            self['ipaddr'].set_target(target)

            # The promote_secondaries sysctl causes the kernel
            # to add secondary addresses back after the primary
            # address is removed.
            #
            # The library can not tell this from the result of
            # an external program.
            #
            # One simple way to work that around is to remove
            # secondaries first.
            rip = sorted(removed['ipaddr'],
                         key=lambda x: self['ipaddr'][x]['flags'],
                         reverse=True)
            # 8<--------------------------------------
            for i in rip:
                # Ignore link-local IPv6 addresses
                if i[0][:4] == 'fe80' and i[1] == 64:
                    continue
                # When you remove a primary IP addr, all subnetwork
                # can be removed. In this case you will fail, but
                # it is OK, no need to roll back
                try:
                    self.ipdb.update_addr(
                        run(nl.addr, 'delete', self['index'], i[0], i[1]),
                        'remove')
                except NetlinkError as x:
                    # bypass only errno 99, 'Cannot assign address'
                    if x.code != errno.EADDRNOTAVAIL:
                        raise
                except socket.error as x:
                    # bypass illegal IP requests
                    if isinstance(x.args[0], basestring) and \
                            x.args[0].startswith('illegal IP'):
                        continue
                    raise

            # 8<--------------------------------------
            target = added['ipaddr']
            for i in range(3):  # just to be sure
                self._commit_add_ip(target, transaction)
                if transaction.partial:
                    break
                real = self._commit_real_ip()
                if real >= set(transaction['ipaddr']):
                    break
                else:
                    target = set(transaction['ipaddr']) - real
            else:
                raise CommitException('ipaddr setup error', i)

            # 8<--------------------------------------
            if (not transaction.partial) and \
                    (removed['ipaddr'] or added['ipaddr']):
                # 8<--------------------------------------
                # bond and bridge interfaces do not send
                # IPv6 address updates, when are down
                #
                # beside of that, bridge interfaces are
                # down by default, so they never send
                # address updates from beginning
                #
                # so if we need, force address load
                #
                # FIXME: probably, we should handle other
                # types as well
                if self['kind'] in ('bond', 'bridge', 'veth'):
                    self.ipdb.update_addr(self.nl.get_addr(), 'add')
                # 8<--------------------------------------
                self['ipaddr'].target.wait(SYNC_TIMEOUT)
                if not self['ipaddr'].target.is_set():
                    raise CommitException('ipaddr target is not set')

            # 8<---------------------------------------------
            # Iterate callback chain
            for ch in self._commit_hooks:
                # An exception will rollback the transaction
                ch(self.dump(), snapshot.dump(), transaction.dump())

            # 8<---------------------------------------------
            # reload interface to hit targets
            if not transaction.partial:
                if transaction._targets:
                    try:
                        self.reload()
                    except NetlinkError as e:
                        if e.code == errno.ENODEV:  # No such device
                            if ('net_ns_fd' in added) or \
                                    ('net_ns_pid' in added):
                                # it means, that the device was moved
                                # to another netns; just give up
                                if drop:
                                    self.drop(transaction)
                                return self
                # wait for targets
                transaction._wait_all_targets()

            # 8<---------------------------------------------
            # Interface removal
            if (added.get('ipdb_scope') in ('shadow', 'remove')) or\
                    ((added.get('ipdb_scope') == 'create') and rollback):
                wd = self.ipdb.watchdog(action='RTM_DELLINK',
                                        ifname=self['ifname'])
                if added.get('ipdb_scope') in ('shadow', 'create'):
                    self.set_item('ipdb_scope', 'locked')
                self.nl.link('delete', **self)
                wd.wait()
                if added.get('ipdb_scope') == 'shadow':
                    self.set_item('ipdb_scope', 'shadow')
                if added['ipdb_scope'] == 'create':
                    self.load_dict(transaction)
                if drop:
                    self.drop(transaction)
                return self
            # 8<---------------------------------------------

        except Exception as e:
            error = e
            # something went wrong: roll the transaction back
            if not rollback:
                try:
                    self.commit(transaction=snapshot,
                                rollback=True,
                                newif=newif)
                except Exception as i_e:
                    error = RuntimeError()
                    error.cause = i_e
            else:
                # reload all the database -- it can take a long time,
                # but it is required since we have no idea, what is
                # the result of the failure
                links = self.nl.get_links()
                for link in links:
                    self.ipdb.device_put(link, skip_slaves=True)
                for link in links:
                    self.ipdb.update_slaves(link)
                links = self.nl.get_vlans()
                for link in links:
                    self.ipdb.update_dev(link)
                self.ipdb.update_addr(self.nl.get_addr())

            error.traceback = traceback.format_exc()
            for key in ('ipaddr', 'ports', 'vlans'):
                self[key].clear_target()

        # raise partial commit exceptions
        if transaction.partial and transaction.errors:
            error = PartialCommitException('partial commit error')

        # if it is not a rollback turn
        if drop and not rollback:
            # drop last transaction in any case
            self.drop(transaction)

        # raise exception for failed transaction
        if error is not None:
            error.transaction = transaction
            raise error

        time.sleep(config.commit_barrier)

        # drop all collected errors, if any
        self.errors = []
        return self
Beispiel #7
0
    def commit(self,
               tid=None,
               transaction=None,
               commit_phase=1,
               commit_mask=0xff):

        if not commit_phase & commit_mask:
            return self

        error = None
        drop = True
        devop = 'set'
        cleanup = []

        if tid:
            transaction = self.global_tx[tid]
        else:
            if transaction:
                drop = False
            else:
                transaction = self.current_tx

        # create a new route
        if self['ipdb_scope'] != 'system':
            devop = 'add'

        # work on an existing route
        snapshot = self.pick()
        added, removed = transaction // snapshot
        added.pop('ipdb_scope', None)
        removed.pop('ipdb_scope', None)

        try:
            # route set
            if self['family'] != AF_MPLS:
                cleanup = [
                    any(snapshot['metrics'].values())
                    and not any(added.get('metrics', {}).values()),
                    any(snapshot['encap'].values())
                    and not any(added.get('encap', {}).values())
                ]
            if any(added.values()) or \
                    any(cleanup) or \
                    removed.get('multipath', None) or \
                    devop == 'add':
                # prepare multipath target sync
                wlist = []
                if transaction['multipath']:
                    mplen = len(transaction['multipath'])
                    if mplen == 1:
                        # set up local targets
                        for nh in transaction['multipath']:
                            for key in ('gateway', 'oif', 'newdst'):
                                if nh.get(key, None):
                                    self.set_target(key, nh[key])
                                    wlist.append(key)
                        mpt = None
                    else:

                        def mpcheck(mpset):
                            return len(mpset) == mplen

                        mpt = self['multipath'].set_target(mpcheck, True)
                else:
                    mpt = None

                # prepare the anchor key to catch *possible* route update
                old_key = self.make_key(self)
                new_key = self.make_key(transaction)
                if old_key != new_key:
                    # assume we can not move routes between tables (yet ;)
                    if self['family'] == AF_MPLS:
                        route_index = self.ipdb.routes.tables['mpls'].idx
                    else:
                        route_index = (self.ipdb.routes.tables[self['table']
                                                               or 254].idx)
                    if new_key not in route_index:
                        route_index[new_key] = {'key': new_key, 'route': self}
                    else:
                        raise CommitException('route idx conflict')
                    self.nl.route(devop, **transaction)
                    del route_index[old_key]
                else:
                    self.nl.route(devop, **transaction)
                transaction.wait_all_targets()
                for key in ('metrics', 'via'):
                    if transaction[key] and transaction[key]._targets:
                        transaction[key].wait_all_targets()
                if mpt is not None:
                    mpt.wait(SYNC_TIMEOUT)
                    if not mpt.is_set():
                        raise CommitException('multipath target is not set')
                    self['multipath'].clear_target(mpt)
                for key in wlist:
                    self.wait_target(key)
            # route removal
            if (transaction['ipdb_scope'] in ('shadow', 'remove')) or\
                    ((transaction['ipdb_scope'] == 'create') and
                     commit_phase == 2):
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'locked'
                # create watchdog
                wd = self.ipdb.watchdog('RTM_DELROUTE',
                                        **self.wd_key(snapshot))
                for route in self.nl.route('delete', **snapshot):
                    self.ipdb.routes.load_netlink(route)
                wd.wait()
                if transaction['ipdb_scope'] == 'shadow':
                    with self._direct_state:
                        self['ipdb_scope'] = 'shadow'

        except Exception as e:
            if devop == 'add':
                error = e
                self.nl = None
                with self._direct_state:
                    self['ipdb_scope'] = 'invalid'
                if self['family'] == AF_MPLS:
                    route_index = self.ipdb.routes.tables['mpls'].idx
                else:
                    route_index = (self.ipdb.routes.tables[self['table']
                                                           or 254].idx)
                route_key = self.make_key(self)
                del route_index[route_key]
            elif commit_phase == 1:
                ret = self.commit(transaction=snapshot,
                                  commit_phase=2,
                                  commit_mask=commit_mask)
                if isinstance(ret, Exception):
                    error = ret
                else:
                    error = e
            else:
                if drop:
                    self.drop(transaction.uid)
                x = RuntimeError()
                x.cause = e
                raise x

        if drop and commit_phase == 1:
            self.drop(transaction.uid)

        if error is not None:
            error.transaction = transaction
            raise error

        return self