def test_parse(self): with self.assertRaisesRegex(ValueError, 'length'): DUID.parse(self.duid_bytes) length = len(self.duid_bytes) parsed_length, parsed_object = DUID.parse(self.duid_bytes, length=length) self.assertEqual(parsed_length, length) self.assertEqual(parsed_object, self.duid_object)
def test_parse_with_larger_buffer(self): offset = 50 buffer = bytes(50 * [0]) + self.duid_bytes + bytes(50 * [0]) length = len(self.duid_bytes) parsed_length, parsed_object = DUID.parse(buffer, offset=offset, length=length) self.assertEqual(parsed_length, length) self.assertEqual(parsed_object, self.duid_object)
def encode_duid(duid: DUID) -> str: """ Encode DUID as a string. :param duid: The DUID object :return: The string representing the DUID """ return codecs.encode(duid.save(), 'hex').decode('ascii')
def determine_server_duid(config: configparser.ConfigParser): """ Make sure we have a server DUID. :param config: The configuration """ # Try to get the server DUID from the configuration config_duid = config['server']['duid'] if config_duid.lower() not in ('', 'auto'): config_duid = config_duid.strip() try: duid = bytes.fromhex(config_duid.strip()) except ValueError: logger.critical("Configured hex DUID contains invalid characters") sys.exit(1) # Check if we can parse this DUID length, duid = DUID.parse(duid, length=len(duid)) if not isinstance(duid, DUID): logger.critical("Configured DUID is invalid") sys.exit(1) logger.debug("Using server DUID from configuration: {}".format(config_duid)) config['server']['duid'] = codecs.encode(duid.save(), 'hex').decode('ascii') return # Use the first interface's MAC address as default if config: interface_names = [section_name.split(' ')[1] for section_name in config.sections() if section_name.split(' ')[0] == 'interface'] interface_names.sort() for interface_name in interface_names: link_addresses = netifaces.ifaddresses(interface_name).get(netifaces.AF_LINK, []) link_addresses = [link_address['addr'] for link_address in link_addresses if link_address.get('addr')] link_addresses.sort() for link_address in link_addresses: # Try to decode try: ll_addr = bytes.fromhex(link_address.replace(':', '')) duid = LinkLayerDUID(hardware_type=1, link_layer_address=ll_addr).save() logger.debug("Using server DUID based on {} link address: " "{}".format(interface_name, codecs.encode(duid, 'hex').decode('ascii'))) config['server']['duid'] = codecs.encode(duid, 'hex').decode('ascii') return except ValueError: # Try the next one pass # We didn't find a useful server DUID logger.critical("Cannot find a usable DUID") sys.exit(1)
def decode_duid(duid_str: str) -> DUID: """ Decode DUID from a string. :param duid_str: The DUID string :return: The DUID object """ duid_bytes = bytes.fromhex(duid_str) duid_len, duid = DUID.parse(duid_bytes, length=len(duid_bytes)) return duid
def load_from(self, buffer: bytes, offset: int = 0, length: int = None) -> int: """ Load the internal state of this object from the given buffer. The buffer may contain more data after the structured element is parsed. This data is ignored. :param buffer: The buffer to read data from :param offset: The offset in the buffer where to start reading :param length: The amount of data we are allowed to read from the buffer :return: The number of bytes used from the buffer """ my_offset, option_len = self.parse_option_header(buffer, offset, length, min_length=2) duid_len, self.duid = DUID.parse(buffer, offset=offset + my_offset, length=option_len) my_offset += duid_len return my_offset
def load_from(self, buffer: bytes, offset: int = 0, length: int = None) -> int: """ Load the internal state of this object from the given buffer. The buffer may contain more data after the structured element is parsed. This data is ignored. :param buffer: The buffer to read data from :param offset: The offset in the buffer where to start reading :param length: The amount of data we are allowed to read from the buffer :return: The number of bytes used from the buffer """ my_offset = self.parse_remote_id_header(buffer, offset, length) # Then the interface number in its weird way slot_lower = (buffer[offset + my_offset] & 0b11110000) >> 4 module_lower = (buffer[offset + my_offset] & 0b00001000) >> 3 port_lower = buffer[offset + my_offset] & 0b00000111 my_offset += 1 slot_higher = buffer[offset + my_offset] & 0b11110000 module_higher = (buffer[offset + my_offset] & 0b00001000) >> 2 port_higher = (buffer[offset + my_offset] & 0b00000111) << 3 my_offset += 1 self.slot = slot_higher | slot_lower self.module = module_higher | module_lower self.port = port_higher | port_lower # Next is the VLAN self.vlan = unpack_from('!H', buffer, offset=offset + my_offset)[0] my_offset += 2 # Read the DUID length and check the data length duid_length = unpack_from('!H', buffer, offset=offset + my_offset)[0] my_offset += 2 if length != duid_length + my_offset: # Mismatch in length, invalid remote-id? raise ValueError("Cisco Remote-ID length incorrect") # Get the DUID read_length, self.duid = DUID.parse(buffer, offset=8, length=duid_length) my_offset += read_length if read_length != duid_length: # Mismatch in length, invalid DUID? raise ValueError("Cisco Remote-ID DUID length incorrect") return my_offset
def duid_ll(self): """ Get the link-layer address stored in the DUID, if any :return: Link-layer address or None """ # noinspection PyBroadException try: duid_str = self.duid if duid_str.startswith('0x'): duid_str = duid_str[2:] duid_bytes = bytes.fromhex(duid_str) length, duid = DUID.parse(duid_bytes, length=len(duid_bytes)) if getattr(duid, 'hardware_type') == 1 and hasattr(duid, 'link_layer_address'): return EUI(int.from_bytes(duid.link_layer_address, byteorder='big'), dialect=mac_unix_expanded) except: pass return None
def parse_csv_file(csv_filename: str) -> [(str, Assignment)]: """ Read the assignments from the file specified in the configuration :param csv_filename: The filename of the CSV file :return: An list of identifiers and their assignment """ logger.debug("Loading assignments from {}".format(csv_filename)) with open(csv_filename) as csv_file: # Auto-detect the CSV dialect sniffer = csv.Sniffer() sample = csv_file.read(10240) dialect = sniffer.sniff(sample) # If there is no header: assume that the columns are 'id', 'address' and 'prefix' in that order csv_has_header = sniffer.has_header(sample) fieldnames = ["id", "address", "prefix"] if not csv_has_header else None # Restart and parse csv_file.seek(0) reader = csv.DictReader(csv_file, dialect=dialect, fieldnames=fieldnames) # First line is column headings line = 1 for row in reader: line += 1 try: address_str = row["address"].strip().split(";") addresses = [IPv6Address(address) for address in address_str] logger.debug("Loaded addresses {!r}".format(addresses)) prefix_str = row["prefix"].strip() prefix = prefix_str and IPv6Network(prefix_str) or None # Validate and normalise id input row_id = row["id"] if row_id.startswith("duid:"): duid_hex = row_id.split(":", 1)[1] duid_bytes = codecs.decode(duid_hex, "hex") length, duid = DUID.parse(duid_bytes, length=len(duid_bytes)) duid_hex = codecs.encode(duid.save(), "hex").decode("ascii") row_id = "duid:{}".format(duid_hex) elif row_id.startswith("interface-id:"): interface_id_hex = row_id.split(":", 1)[1] interface_id = codecs.decode(interface_id_hex, "hex") interface_id_hex = codecs.encode(interface_id, "hex").decode("ascii") row_id = "interface_id:{}".format(interface_id_hex) elif row_id.startswith("interface-id-str:"): interface_id = row_id.split(":", 1)[1] interface_id_hex = codecs.encode(interface_id.encode("ascii"), "hex").decode("ascii") row_id = "interface_id:{}".format(interface_id_hex) elif row_id.startswith("remote-id:") or row_id.startswith("remote-id-str:"): remote_id_data = row_id.split(":", 1)[1] try: enterprise_id, remote_id = remote_id_data.split(":", 1) enterprise_id = int(enterprise_id) if row_id.startswith("remote-id:"): remote_id = codecs.decode(remote_id, "hex") else: remote_id = remote_id.encode("ascii") row_id = "remote-id:{}:{}".format( enterprise_id, codecs.encode(remote_id, "hex").decode("ascii") ) except ValueError: raise ValueError( "Remote-ID must be formatted as 'remote-id:<enterprise>:<remote-id-hex>', " "for example: 'remote-id:9:0123456789abcdef" ) else: raise ValueError( "The id must start with duid: or interface-id: followed by a hex-encoded " "value, interface-id-str: followed by an ascii string, remote-id: followed by " "an enterprise-id, a colon and a hex-encoded value or remote-id-str: followed" "by an enterprise-id, a colon and an ascii string" ) # Store the normalised id logger.debug("Loaded assignment for {}".format(row_id)) yield row_id, Assignment(address=addresses, prefix=prefix) except KeyError: raise configparser.Error("Assignment CSV must have columns 'id', 'address' and 'prefix'") except ValueError as e: logger.error("Ignoring line {} with invalid value: {}".format(line, e))
def parse_csv_file(csv_filename: str) -> List[Tuple[str, Assignment]]: """ Read the assignments from the file specified in the configuration :param csv_filename: The filename of the CSV file :return: An list of identifiers and their assignment """ logger.debug("Loading assignments from {}".format(csv_filename)) with open(csv_filename) as csv_file: # Auto-detect the CSV dialect sniffer = csv.Sniffer() sample = csv_file.read(10240) dialect = sniffer.sniff(sample) # Restart and parse csv_file.seek(0) reader = csv.DictReader(csv_file, dialect=dialect) # First line is column headings for row in reader: try: address_str = row['address'].strip() address = address_str and IPv6Address(address_str) or None prefix_str = row['prefix'].strip() prefix = prefix_str and IPv6Network(prefix_str) or None # Validate and normalise id input row_id = row['id'] if row_id.startswith('duid:'): duid_hex = row_id.split(':', 1)[1] duid_bytes = codecs.decode(duid_hex, 'hex') length, duid = DUID.parse(duid_bytes, length=len(duid_bytes)) duid_hex = codecs.encode(duid.save(), 'hex').decode('ascii') row_id = 'duid:{}'.format(duid_hex) elif row_id.startswith('interface-id:'): interface_id_hex = row_id.split(':', 1)[1] interface_id_hex = normalise_hex(interface_id_hex) interface_id = codecs.decode(interface_id_hex, 'hex') interface_id_hex = codecs.encode(interface_id, 'hex').decode('ascii') row_id = 'interface-id:{}'.format(interface_id_hex) elif row_id.startswith('interface-id-str:'): interface_id = row_id.split(':', 1)[1] interface_id_hex = codecs.encode(interface_id.encode('ascii'), 'hex').decode('ascii') row_id = 'interface-id:{}'.format(interface_id_hex) elif row_id.startswith('remote-id:') or row_id.startswith('remote-id-str:'): remote_id_data = row_id.split(':', 1)[1] try: enterprise_id, remote_id = remote_id_data.split(':', 1) enterprise_id = int(enterprise_id) if row_id.startswith('remote-id:'): remote_id = normalise_hex(remote_id) remote_id = codecs.decode(remote_id, 'hex') else: remote_id = remote_id.encode('ascii') row_id = 'remote-id:{}:{}'.format(enterprise_id, codecs.encode(remote_id, 'hex').decode('ascii')) except ValueError: raise ValueError("Remote-ID must be formatted as 'remote-id:<enterprise>:<remote-id-hex>', " "for example: 'remote-id:9:0123456789abcdef") elif row_id.startswith('subscriber-id:'): subscriber_id_hex = row_id.split(':', 1)[1] subscriber_id_hex = normalise_hex(subscriber_id_hex) subscriber_id = codecs.decode(subscriber_id_hex, 'hex') subscriber_id_hex = codecs.encode(subscriber_id, 'hex').decode('ascii') row_id = 'subscriber-id:{}'.format(subscriber_id_hex) elif row_id.startswith('subscriber-id-str:'): subscriber_id = row_id.split(':', 1)[1] subscriber_id_hex = codecs.encode(subscriber_id.encode('ascii'), 'hex').decode('ascii') row_id = 'subscriber-id:{}'.format(subscriber_id_hex) elif row_id.startswith('linklayer-id:') or row_id.startswith('linklayer-id-str:'): linklayer_id_data = row_id.split(':', 1)[1] try: linklayer_type, linklayer_id = linklayer_id_data.split(':', 1) linklayer_type = int(linklayer_type) if row_id.startswith('linklayer-id:'): linklayer_id = normalise_hex(linklayer_id) linklayer_id = codecs.decode(linklayer_id, 'hex') else: linklayer_id = linklayer_id.encode('ascii') row_id = 'linklayer-id:{}:{}'.format(linklayer_type, codecs.encode(linklayer_id, 'hex').decode('ascii')) except ValueError: raise ValueError("LinkLayer-ID must be formatted as 'linklayer-id:<type>:<address-hex>', " "for example: 'linklayer-id:1:002436ef1d89") else: raise ValueError("Unsupported ID type, supported types: duid, interface-id, interface-id-str," "remote-id, remote-id-str, subscriber-id, subscriber-id-str, linklayer-id and" "linklayer-id-str") # Store the normalised id logger.debug("Loaded assignment for {}".format(row_id)) yield row_id, Assignment(address=address, prefix=prefix) except KeyError: raise ValueError("Assignment CSV must have columns 'id', 'address' and 'prefix'") except ValueError as e: logger.error("Ignoring {} line {} with invalid value: {}".format(csv_file, reader.line_num, e))
def parse_csv_file(csv_filename: str) -> List[Tuple[str, Assignment]]: """ Read the assignments from the file specified in the configuration :param csv_filename: The filename of the CSV file :return: An list of identifiers and their assignment """ logger.debug("Loading assignments from {}".format(csv_filename)) with open(csv_filename) as csv_file: # Auto-detect the CSV dialect sniffer = csv.Sniffer() sample = csv_file.read(10240) dialect = sniffer.sniff(sample) # Restart and parse csv_file.seek(0) reader = csv.DictReader(csv_file, dialect=dialect) # First line is column headings for row in reader: try: address_str = row['address'].strip() address = address_str and IPv6Address(address_str) or None prefix_str = row['prefix'].strip() prefix = prefix_str and IPv6Network(prefix_str) or None # Validate and normalise id input row_id = row['id'] if row_id.startswith('duid:'): duid_hex = row_id.split(':', 1)[1] duid_bytes = codecs.decode(duid_hex, 'hex') length, duid = DUID.parse(duid_bytes, length=len(duid_bytes)) duid_hex = codecs.encode(duid.save(), 'hex').decode('ascii') row_id = 'duid:{}'.format(duid_hex) elif row_id.startswith('interface-id:'): interface_id_hex = row_id.split(':', 1)[1] interface_id_hex = normalise_hex(interface_id_hex) interface_id = codecs.decode(interface_id_hex, 'hex') interface_id_hex = codecs.encode(interface_id, 'hex').decode('ascii') row_id = 'interface_id:{}'.format(interface_id_hex) elif row_id.startswith('interface-id-str:'): interface_id = row_id.split(':', 1)[1] interface_id_hex = codecs.encode(interface_id.encode('ascii'), 'hex').decode('ascii') row_id = 'interface_id:{}'.format(interface_id_hex) elif row_id.startswith('remote-id:') or row_id.startswith('remote-id-str:'): remote_id_data = row_id.split(':', 1)[1] try: enterprise_id, remote_id = remote_id_data.split(':', 1) enterprise_id = int(enterprise_id) if row_id.startswith('remote-id:'): remote_id = normalise_hex(remote_id) remote_id = codecs.decode(remote_id, 'hex') else: remote_id = remote_id.encode('ascii') row_id = 'remote-id:{}:{}'.format(enterprise_id, codecs.encode(remote_id, 'hex').decode('ascii')) except ValueError: raise ValueError("Remote-ID must be formatted as 'remote-id:<enterprise>:<remote-id-hex>', " "for example: 'remote-id:9:0123456789abcdef") else: raise ValueError("The id must start with duid: or interface-id: followed by a hex-encoded " "value, interface-id-str: followed by an ascii string, remote-id: followed by " "an enterprise-id, a colon and a hex-encoded value or remote-id-str: followed" "by an enterprise-id, a colon and an ascii string") # Store the normalised id logger.debug("Loaded assignment for {}".format(row_id)) yield row_id, Assignment(address=address, prefix=prefix) except KeyError: raise ValueError("Assignment CSV must have columns 'id', 'address' and 'prefix'") except ValueError as e: logger.error("Ignoring {} line {} with invalid value: {}".format(csv_file, reader.line_num, e))