def link_create(self, **kwarg): ''' Create a link. The method parameters will be passed to the `IPLinkRequest()` constructor as a dictionary. Examples:: ip.link_create(ifname='very_dummy', kind='dummy') ip.link_create(ifname='br0', kind='bridge') ip.link_create(ifname='v101', kind='vlan', vlan_id=101, link=1) ''' return self.link('add', **IPLinkRequest(kwarg))
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, rollback=False, newif=False): ''' Commit transaction. In the case of exception all changes applied during commit will be reverted. ''' def invalidate(): # 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' error = None added = None removed = None drop = True 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<---------------------------------------------------- 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: invalidate() self._exception = e self._tb = traceback.format_exc() # raise the exception if transaction.partial: transaction.errors.append(e) raise PartialCommitException() else: raise if newif: # Here we come only if the new interface is created # if not rollback and not self.wait_target('ipdb_scope'): invalidate() raise CreateException() 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() # 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) 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] 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 '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._interface_add(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(): self.ipdb._addr_add(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')) 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'): 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 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._interface_add(link, skip_slaves=True) for link in links: self.ipdb.update_slaves(link) links = self.nl.get_vlans() for link in links: self.ipdb._interface_add(link) for addr in self.nl.get_addr(): self.ipdb._addr_add(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.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, 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() 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 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: raise CreateException() # now we have our index and IP set and all other stuff snapshot = self.pick() try: removed = snapshot - transaction added = transaction - snapshot # 8<--------------------------------------------- # Interface slaves self['ports'].set_target(transaction['ports']) for i in removed['ports']: # detach the port port = self.ipdb.interfaces[i] port.set_target('master', None) port.mirror_target('master', 'link') self.nl.link('set', index=port['index'], master=0) for i in added['ports']: # enslave the port port = self.ipdb.interfaces[i] port.set_target('master', self['index']) port.mirror_target('master', 'link') self.nl.link('set', index=port['index'], master=self['index']) if 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') # RHEL 6.5 compat fix -- an explicit timeout # it gives a time for all the messages to pass if not self.ipdb.nl.capabilities['create_bridge']: time.sleep(1) # 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']: assert port.if_master == self['index'] else: assert port.if_master != self['index'] # 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']): self.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.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 self['ipaddr'].set_target(transaction['ipaddr']) for i in removed['ipaddr']: # 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( self.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 not x.args[0].startswith('illegal IP'): raise for i in added['ipaddr']: # 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 self.ipdb.update_addr( self.nl.addr('add', self['index'], i[0], i[1], **kwarg if kwarg else {}), 'add') # 8<-------------------------------------- # FIXME: kernel bug, sometimes `addr add` for # bond interfaces returns success, but does # really nothing if self['kind'] == 'bond': while True: try: # dirtiest hack, but we have to use it here time.sleep(0.1) self.nl.addr('add', self['index'], i[0], i[1]) # continue to try to add the address # until the kernel reports `file exists` # # a stupid solution, but must help except NetlinkError as e: if e.code == errno.EEXIST: break else: raise except Exception: raise # 8<-------------------------------------- if 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<--------------------------------------------- # reload interface to hit targets 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<--------------------------------------------- # Iterate callback chain for ch in self._commit_hooks: # An exception will rollback the transaction ch(self.dump(), snapshot.dump(), transaction.dump()) # 8<--------------------------------------------- except Exception as e: # something went wrong: roll the transaction back if not rollback: ret = self.commit(transaction=snapshot, rollback=True, newif=newif) # if some error was returned by the internal # closure, substitute the initial one if isinstance(ret, Exception): error = ret else: error = e error.traceback = traceback.format_exc() elif isinstance(e, NetlinkError) and \ getattr(e, 'code', 0) == errno.EPERM: # It is <Operation not permitted>, catched in # rollback. So return it -- see ~5 lines above e.traceback = traceback.format_exc() return e else: # somethig went wrong during automatic rollback. # that's the worst case, but it is still possible, # since we have no locks on OS level. self['ipaddr'].set_target(None) self['ports'].set_target(None) # 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 # # ACHTUNG: database reload is asynchronous, so after # getting RuntimeError() from commit(), take a seat # and rest for a while. It is an extremal case, it # should not became at all, and there is no sync. for link in self.nl.get_links(): self.ipdb.device_put(link) self.ipdb.update_addr(self.nl.get_addr()) x = RuntimeError() x.cause = e x.traceback = traceback.format_exc() raise x # 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) 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() 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 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: raise CreateException() # now we have our index and IP set and all other stuff snapshot = self.pick() try: removed = snapshot - transaction added = transaction - snapshot # 8<--------------------------------------------- # Interface slaves self['ports'].set_target(transaction['ports']) for i in removed['ports']: # detach the port port = self.ipdb.interfaces[i] port.set_target('master', None) port.mirror_target('master', 'link') self.nl.link('set', index=port['index'], master=0) for i in added['ports']: # enslave the port port = self.ipdb.interfaces[i] port.set_target('master', self['index']) port.mirror_target('master', 'link') self.nl.link('set', index=port['index'], master=self['index']) if 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') # RHEL 6.5 compat fix -- an explicit timeout # it gives a time for all the messages to pass if not self.ipdb.nl.capabilities['create_bridge']: time.sleep(1) # 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']): self.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.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 self['ipaddr'].set_target(transaction['ipaddr']) for i in removed['ipaddr']: # 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( self.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 not x.args[0].startswith('illegal IP'): raise for i in added['ipaddr']: # 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 self.ipdb.update_addr( self.nl.addr('add', self['index'], i[0], i[1], **kwarg if kwarg else {}), 'add') # 8<-------------------------------------- # FIXME: kernel bug, sometimes `addr add` for # bond interfaces returns success, but does # really nothing if self['kind'] == 'bond': while True: try: # dirtiest hack, but we have to use it here time.sleep(0.1) self.nl.addr('add', self['index'], i[0], i[1]) # continue to try to add the address # until the kernel reports `file exists` # # a stupid solution, but must help except NetlinkError as e: if e.code == errno.EEXIST: break else: raise except Exception: raise # 8<-------------------------------------- if 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<--------------------------------------------- # reload interface to hit targets 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<--------------------------------------------- # Iterate callback chain for ch in self._commit_hooks: # An exception will rollback the transaction ch(self.dump(), snapshot.dump(), transaction.dump()) # 8<--------------------------------------------- except Exception as e: # something went wrong: roll the transaction back if not rollback: ret = self.commit(transaction=snapshot, rollback=True, newif=newif) # if some error was returned by the internal # closure, substitute the initial one if isinstance(ret, Exception): error = ret else: error = e error.traceback = traceback.format_exc() elif isinstance(e, NetlinkError) and \ getattr(e, 'code', 0) == errno.EPERM: # It is <Operation not permitted>, catched in # rollback. So return it -- see ~5 lines above e.traceback = traceback.format_exc() return e else: # somethig went wrong during automatic rollback. # that's the worst case, but it is still possible, # since we have no locks on OS level. self['ipaddr'].clear_target() self['ports'].clear_target() # 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 # # ACHTUNG: database reload is asynchronous, so after # getting RuntimeError() from commit(), take a seat # and rest for a while. It is an extremal case, it # should not became at all, and there is no sync. for link in self.nl.get_links(): self.ipdb.device_put(link) self.ipdb.update_addr(self.nl.get_addr()) x = RuntimeError() x.cause = e x.traceback = traceback.format_exc() raise x # 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) 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, rollback=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() wd = None with self._write_lock: # if the interface does not exist, create it first ;) if not self._exists: request = IPLinkRequest(self.filter('common')) # create watchdog wd = self.ipdb.watchdog(ifname=self['ifname']) 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: # Operation not supported if x.code == 95 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.ipdb.detach(self['index']) self.ipdb.detach(self['ifname']) # 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 raise if wd is not None: wd.wait() # now we have our index and IP set and all other stuff snapshot = self.pick() try: removed = snapshot - transaction added = transaction - snapshot # 8<--------------------------------------------- # IP address changes self['ipaddr'].set_target(transaction['ipaddr']) for i in removed['ipaddr']: # 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.nl.addr('delete', self['index'], i[0], i[1]) except NetlinkError as x: # bypass only errno 99, 'Cannot assign address' if x.code != 99: raise except socket.error as x: # bypass illegal IP requests if not x.args[0].startswith('illegal IP'): raise for i in added['ipaddr']: # Ignore link-local IPv6 addresses if i[0][:4] == 'fe80' and i[1] == 64: continue self.nl.addr('add', self['index'], i[0], i[1]) # 8<-------------------------------------- # FIXME: kernel bug, sometimes `addr add` for # bond interfaces returns success, but does # really nothing if self['kind'] == 'bond': while True: try: # dirtiest hack, but we have to use it here time.sleep(0.1) self.nl.addr('add', self['index'], i[0], i[1]) # continue to try to add the address # until the kernel reports `file exists` # # a stupid solution, but must help except NetlinkError as e: if e.code == 17: break else: raise except Exception: raise # 8<-------------------------------------- if 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'): self.nl.get_addr() # 8<-------------------------------------- self['ipaddr'].target.wait(SYNC_TIMEOUT) if not self['ipaddr'].target.is_set(): raise CommitException('ipaddr target is not set') # 8<--------------------------------------------- # Interface slaves self['ports'].set_target(transaction['ports']) for i in removed['ports']: # detach the port port = self.ipdb.interfaces[i] port.set_target('master', None) port.mirror_target('master', 'link') self.nl.link('set', index=port['index'], master=0) for i in added['ports']: # enslave the port port = self.ipdb.interfaces[i] port.set_target('master', self['index']) port.mirror_target('master', 'link') self.nl.link('set', index=port['index'], master=self['index']) if removed['ports'] or added['ports']: self.nl.get_links(*(removed['ports'] | added['ports'])) self['ports'].target.wait(SYNC_TIMEOUT) if not self['ports'].target.is_set(): raise CommitException('ports target is not set') # RHEL 6.5 compat fix -- an explicit timeout # it gives a time for all the messages to pass compat.fix_timeout(1) # 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']: assert port.if_master == self['index'] else: assert port.if_master != self['index'] # 8<--------------------------------------------- # Interface changes request = IPLinkRequest() for key in added: if key in self._xfields['common']: 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']): self.nl.link('set', **request) # bridge changes if self['kind'] == 'bridge': brq = BridgeRequest() for key in added: if key in self._xfields['bridge']: brq[key] = added[key] brq['index'] = self['index'] brq['ifname'] = self['ifname'] self.nl.setbr(**brq) self.load_bridge() # bridge changes if self['kind'] == 'bond': boq = BondRequest() for key in added: if key in self._xfields['bond']: boq[key] = added[key] boq['index'] = self['index'] boq['ifname'] = self['ifname'] self.nl.setbo(**boq) self.load_bond() # reload interface to hit targets if transaction._targets: self.reload() # wait for targets for key, target in transaction._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) # 8<--------------------------------------------- # Interface removal if added.get('removal') or added.get('flicker'): wd = self.ipdb.watchdog(action='RTM_DELLINK', ifname=self['ifname']) if added.get('flicker'): self._flicker = True self.nl.link('delete', **self) wd.wait() if added.get('flicker'): self._exists = False if added.get('removal'): self._mode = 'invalid' if drop: self.drop(transaction) return self # 8<--------------------------------------------- # Iterate callback chain for ch in self._commit_hooks: # An exception will rollback the transaction ch(self.dump(), snapshot.dump(), transaction.dump()) # 8<--------------------------------------------- except Exception as e: # something went wrong: roll the transaction back if not rollback: ret = self.commit(transaction=snapshot, rollback=True) # if some error was returned by the internal # closure, substitute the initial one if isinstance(ret, Exception): error = ret else: error = e error.traceback = traceback.format_exc() elif isinstance(e, NetlinkError) and \ getattr(e, 'code', 0) == 1: # It is <Operation not permitted>, catched in # rollback. So return it -- see ~5 lines above e.traceback = traceback.format_exc() return e else: # somethig went wrong during automatic rollback. # that's the worst case, but it is still possible, # since we have no locks on OS level. self['ipaddr'].set_target(None) self['ports'].set_target(None) # 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 # # ACHTUNG: database reload is asynchronous, so after # getting RuntimeError() from commit(), take a seat # and rest for a while. It is an extremal case, it # should not became at all, and there is no sync. self.nl.get_links() self.nl.get_addr() x = RuntimeError() x.cause = e x.traceback = traceback.format_exc() raise x # 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(_BARRIER) return self