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
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)
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
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
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
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