netwatch.py/netaddr-0.7.10/netaddr/eui/__init__.py

692 lines
23 KiB
Python

#-----------------------------------------------------------------------------
# Copyright (c) 2008-2012, David P. D. Moss. All rights reserved.
#
# Released under the BSD license. See the LICENSE file for details.
#-----------------------------------------------------------------------------
"""
Classes and functions for dealing with MAC addresses, EUI-48, EUI-64, OUI, IAB
identifiers.
"""
import sys as _sys
import os as _os
import os.path as _path
import re as _re
import csv as _csv
import pprint as _pprint
from netaddr.core import NotRegisteredError, AddrFormatError, \
AddrConversionError, Subscriber, Publisher, DictDotLookup
from netaddr.strategy import eui48 as _eui48, eui64 as _eui64
from netaddr.strategy.eui48 import mac_eui48
from netaddr.ip import IPAddress
from netaddr.compat import _is_int, _is_str
#-----------------------------------------------------------------------------
class BaseIdentifier(object):
"""Base class for all IEEE identifiers."""
__slots__ = ('_value',)
def __init__(self):
self._value = None
def __int__(self):
""":return: integer value of this identifier"""
return self._value
def __long__(self):
""":return: integer value of this identifier"""
return self._value
def __oct__(self):
""":return: octal string representation of this identifier."""
# Python 2.x only.
if self._value == 0:
return '0'
return '0%o' % self._value
def __hex__(self):
""":return: hexadecimal string representation of this identifier."""
# Python 2.x only.
return '0x%x' % self._value
def __index__(self):
"""
:return: return the integer value of this identifier when passed to
hex(), oct() or bin().
"""
# Python 3.x only.
return self._value
def __eq__(self, other):
"""
:return: ``True`` if this BaseIdentifier object is numerically the
same as other, ``False`` otherwise.
"""
try:
return (self.__class__, self._value) == (other.__class__, other._value)
except AttributeError:
return NotImplemented
#-----------------------------------------------------------------------------
class OUI(BaseIdentifier):
"""
An individual IEEE OUI (Organisationally Unique Identifier).
For online details see - http://standards.ieee.org/regauth/oui/
"""
__slots__ = ('records',)
def __init__(self, oui):
"""
Constructor
:param oui: an OUI string ``XX-XX-XX`` or an unsigned integer. \
Also accepts and parses full MAC/EUI-48 address strings (but not \
MAC/EUI-48 integers)!
"""
super(OUI, self).__init__()
# Lazy loading of IEEE data structures.
from netaddr.eui import ieee
self.records = []
if isinstance(oui, str):
#TODO: Improve string parsing here.
#TODO: Accept full MAC/EUI-48 addressses as well as XX-XX-XX
#TODO: and just take /16 (see IAB for details)
self._value = int(oui.replace('-', ''), 16)
elif _is_int(oui):
if 0 <= oui <= 0xffffff:
self._value = oui
else:
raise ValueError('OUI int outside expected range: %r' % oui)
else:
raise TypeError('unexpected OUI format: %r' % oui)
# Discover offsets.
if self._value in ieee.OUI_INDEX:
fh = open(ieee.OUI_REGISTRY)
for (offset, size) in ieee.OUI_INDEX[self._value]:
fh.seek(offset)
data = fh.read(size)
self._parse_data(data, offset, size)
fh.close()
else:
raise NotRegisteredError('OUI %r not registered!' % oui)
def __getstate__(self):
""":returns: Pickled state of an `OUI` object."""
return self._value, self.records
def __setstate__(self, state):
""":param state: data used to unpickle a pickled `OUI` object."""
self._value, self.records = state
def _parse_data(self, data, offset, size):
"""Returns a dict record from raw OUI record data"""
record = {
'idx': 0,
'oui': '',
'org': '',
'address' : [],
'offset': offset,
'size': size,
}
for line in data.split("\n"):
line = line.strip()
if not line:
continue
if '(hex)' in line:
record['idx'] = self._value
record['org'] = ' '.join(line.split()[2:])
record['oui'] = str(self)
elif '(base 16)' in line:
continue
else:
record['address'].append(line)
self.records.append(record)
@property
def reg_count(self):
"""Number of registered organisations with this OUI"""
return len(self.records)
def registration(self, index=0):
"""
The IEEE registration details for this OUI.
:param index: the index of record (may contain multiple registrations)
(Default: 0 - first registration)
:return: Objectified Python data structure containing registration
details.
"""
return DictDotLookup(self.records[index])
def __str__(self):
""":return: string representation of this OUI"""
int_val = self._value
words = []
for _ in range(3):
word = int_val & 0xff
words.append('%02x' % word)
int_val >>= 8
return '-'.join(reversed(words)).upper()
def __repr__(self):
""":return: executable Python string to recreate equivalent object."""
return "OUI('%s')" % self
#-----------------------------------------------------------------------------
class IAB(BaseIdentifier):
"""
An individual IEEE IAB (Individual Address Block) identifier.
For online details see - http://standards.ieee.org/regauth/oui/
"""
__slots__ = ('record',)
@staticmethod
def split_iab_mac(eui_int, strict=False):
"""
:param eui_int: a MAC IAB as an unsigned integer.
:param strict: If True, raises a ValueError if the last 12 bits of
IAB MAC/EUI-48 address are non-zero, ignores them otherwise.
(Default: False)
"""
if 0x50c2000 <= eui_int <= 0x50c2fff:
return eui_int, 0
user_mask = 2 ** 12 - 1
iab_mask = (2 ** 48 - 1) ^ user_mask
iab_bits = eui_int >> 12
user_bits = (eui_int | iab_mask) - iab_mask
if 0x50c2000 <= iab_bits <= 0x50c2fff:
if strict and user_bits != 0:
raise ValueError('%r is not a strict IAB!' % hex(user_bits))
else:
raise ValueError('%r is not an IAB address!' % hex(eui_int))
return iab_bits, user_bits
def __init__(self, iab, strict=False):
"""
Constructor
:param iab: an IAB string ``00-50-C2-XX-X0-00`` or an unsigned \
integer. This address looks like an EUI-48 but it should not \
have any non-zero bits in the last 3 bytes.
:param strict: If True, raises a ValueError if the last 12 bits \
of IAB MAC/EUI-48 address are non-zero, ignores them otherwise. \
(Default: False)
"""
super(IAB, self).__init__()
# Lazy loading of IEEE data structures.
from netaddr.eui import ieee
self.record = {
'idx': 0,
'iab': '',
'org': '',
'address' : [],
'offset': 0,
'size': 0,
}
if isinstance(iab, str):
#TODO: Improve string parsing here.
#TODO: '00-50-C2' is actually invalid.
#TODO: Should be '00-50-C2-00-00-00' (i.e. a full MAC/EUI-48)
int_val = int(iab.replace('-', ''), 16)
(iab_int, user_int) = IAB.split_iab_mac(int_val, strict)
self._value = iab_int
elif _is_int(iab):
(iab_int, user_int) = IAB.split_iab_mac(iab, strict)
self._value = iab_int
else:
raise TypeError('unexpected IAB format: %r!' % iab)
# Discover offsets.
if self._value in ieee.IAB_INDEX:
fh = open(ieee.IAB_REGISTRY)
(offset, size) = ieee.IAB_INDEX[self._value][0]
self.record['offset'] = offset
self.record['size'] = size
fh.seek(offset)
data = fh.read(size)
self._parse_data(data, offset, size)
fh.close()
else:
raise NotRegisteredError('IAB %r not unregistered!' % iab)
def __getstate__(self):
""":returns: Pickled state of an `IAB` object."""
return self._value, self.record
def __setstate__(self, state):
""":param state: data used to unpickle a pickled `IAB` object."""
self._value, self.record = state
def _parse_data(self, data, offset, size):
"""Returns a dict record from raw IAB record data"""
for line in data.split("\n"):
line = line.strip()
if not line:
continue
if '(hex)' in line:
self.record['idx'] = self._value
self.record['org'] = ' '.join(line.split()[2:])
self.record['iab'] = str(self)
elif '(base 16)' in line:
continue
else:
self.record['address'].append(line)
def registration(self):
""" The IEEE registration details for this IAB"""
return DictDotLookup(self.record)
def __str__(self):
""":return: string representation of this IAB"""
int_val = self._value << 12
words = []
for _ in range(6):
word = int_val & 0xff
words.append('%02x' % word)
int_val >>= 8
return '-'.join(reversed(words)).upper()
def __repr__(self):
""":return: executable Python string to recreate equivalent object."""
return "IAB('%s')" % self
#-----------------------------------------------------------------------------
class EUI(BaseIdentifier):
"""
An IEEE EUI (Extended Unique Identifier).
Both EUI-48 (used for layer 2 MAC addresses) and EUI-64 are supported.
Input parsing for EUI-48 addresses is flexible, supporting many MAC
variants.
"""
__slots__ = ('_module', '_dialect')
def __init__(self, addr, version=None, dialect=None):
"""
Constructor.
:param addr: an EUI-48 (MAC) or EUI-64 address in string format or \
an unsigned integer. May also be another EUI object (copy \
construction).
:param version: (optional) the explict EUI address version. Mainly \
used to distinguish between EUI-48 and EUI-64 identifiers \
specified as integers which may be numerically equivalent.
:param dialect: (optional) the mac_* dialect to be used to configure \
the formatting of EUI-48 (MAC) addresses.
"""
super(EUI, self).__init__()
self._module = None
if isinstance(addr, EUI):
# Copy constructor.
if version is not None and version != addr._module.version:
raise ValueError('cannot switch EUI versions using '
'copy constructor!')
self._module = addr._module
self._value = addr._value
self.dialect = addr.dialect
return
if version is not None:
if version == 48:
self._module = _eui48
elif version == 64:
self._module = _eui64
else:
raise ValueError('unsupported EUI version %r' % version)
else:
# Choose a default version when addr is an integer and version is
# not specified.
if _is_int(addr):
if 0 <= addr <= 0xffffffffffff:
self._module = _eui48
elif 0xffffffffffff < addr <= 0xffffffffffffffff:
self._module = _eui64
self.value = addr
# Choose a dialect for MAC formatting.
self.dialect = dialect
def __getstate__(self):
""":returns: Pickled state of an `EUI` object."""
return self._value, self._module.version, self.dialect
def __setstate__(self, state):
"""
:param state: data used to unpickle a pickled `EUI` object.
"""
value, version, dialect = state
self._value = value
if version == 48:
self._module = _eui48
elif version == 64:
self._module = _eui64
else:
raise ValueError('unpickling failed for object state: %s' \
% str(state))
self.dialect = dialect
def _get_value(self):
return self._value
def _set_value(self, value):
if self._module is None:
# EUI version is implicit, detect it from value.
for module in (_eui48, _eui64):
try:
self._value = module.str_to_int(value)
self._module = module
break
except AddrFormatError:
try:
if 0 <= int(value) <= module.max_int:
self._value = int(value)
self._module = module
break
except ValueError:
pass
if self._module is None:
raise AddrFormatError('failed to detect EUI version: %r'
% value)
else:
# EUI version is explicit.
if hasattr(value, 'upper'):
try:
self._value = self._module.str_to_int(value)
except AddrFormatError:
raise AddrFormatError('address %r is not an EUIv%d'
% (value, self._module.version))
else:
if 0 <= int(value) <= self._module.max_int:
self._value = int(value)
else:
raise AddrFormatError('bad address format: %r' % value)
value = property(_get_value, _set_value, None,
'a positive integer representing the value of this EUI indentifier.')
def _get_dialect(self):
return self._dialect
def _set_dialect(self, value):
if value is None:
self._dialect = mac_eui48
else:
if hasattr(value, 'word_size') and hasattr(value, 'word_fmt'):
self._dialect = value
else:
raise TypeError('custom dialects should subclass mac_eui48!')
dialect = property(_get_dialect, _set_dialect, None,
"a Python class providing support for the interpretation of "
"various MAC\n address formats.")
@property
def oui(self):
"""The OUI (Organisationally Unique Identifier) for this EUI."""
if self._module == _eui48:
return OUI(self.value >> 24)
elif self._module == _eui64:
return OUI(self.value >> 40)
@property
def ei(self):
"""The EI (Extension Identifier) for this EUI"""
if self._module == _eui48:
return '-'.join(["%02x" % i for i in self[3:6]]).upper()
elif self._module == _eui64:
return '-'.join(["%02x" % i for i in self[3:8]]).upper()
def is_iab(self):
""":return: True if this EUI is an IAB address, False otherwise"""
return 0x50c2000 <= (self._value >> 12) <= 0x50c2fff
@property
def iab(self):
"""
If is_iab() is True, the IAB (Individual Address Block) is returned,
``None`` otherwise.
"""
if self.is_iab():
return IAB(self._value >> 12)
@property
def version(self):
"""The EUI version represented by this EUI object."""
return self._module.version
def __getitem__(self, idx):
"""
:return: The integer value of the word referenced by index (both \
positive and negative). Raises ``IndexError`` if index is out \
of bounds. Also supports Python list slices for accessing \
word groups.
"""
if _is_int(idx):
# Indexing, including negative indexing goodness.
num_words = self._dialect.num_words
if not (-num_words) <= idx <= (num_words - 1):
raise IndexError('index out range for address type!')
return self._module.int_to_words(self._value, self._dialect)[idx]
elif isinstance(idx, slice):
words = self._module.int_to_words(self._value, self._dialect)
return [words[i] for i in range(*idx.indices(len(words)))]
else:
raise TypeError('unsupported type %r!' % idx)
def __setitem__(self, idx, value):
"""Sets the value of the word referenced by index in this address"""
if isinstance(idx, slice):
# TODO - settable slices.
raise NotImplementedError('settable slices are not supported!')
if not _is_int(idx):
raise TypeError('index not an integer!')
if not 0 <= idx <= (self._dialect.num_words - 1):
raise IndexError('index %d outside address type boundary!' % idx)
if not _is_int(value):
raise TypeError('value not an integer!')
if not 0 <= value <= self._dialect.max_word:
raise IndexError('value %d outside word size maximum of %d bits!'
% (value, self._dialect.word_size))
words = list(self._module.int_to_words(self._value, self._dialect))
words[idx] = value
self._value = self._module.words_to_int(words)
def __hash__(self):
""":return: hash of this EUI object suitable for dict keys, sets etc"""
return hash((self.version, self._value))
def __eq__(self, other):
"""
:return: ``True`` if this EUI object is numerically the same as other, \
``False`` otherwise.
"""
try:
return(self.version, self._value) == (other.version, other._value)
except AttributeError:
return NotImplemented
def __ne__(self, other):
"""
:return: ``False`` if this EUI object is numerically the same as the \
other, ``True`` otherwise.
"""
try:
return(self.version, self._value) != (other.version, other._value)
except AttributeError:
return NotImplemented
def __lt__(self, other):
"""
:return: ``True`` if this EUI object is numerically lower in value than \
other, ``False`` otherwise.
"""
try:
return (self.version, self._value) < (other.version, other._value)
except AttributeError:
return NotImplemented
def __le__(self, other):
"""
:return: ``True`` if this EUI object is numerically lower or equal in \
value to other, ``False`` otherwise.
"""
try:
return(self.version, self._value) <= (other.version, other._value)
except AttributeError:
return NotImplemented
def __gt__(self, other):
"""
:return: ``True`` if this EUI object is numerically greater in value \
than other, ``False`` otherwise.
"""
try:
return (self.version, self._value) > (other.version, other._value)
except AttributeError:
return NotImplemented
def __ge__(self, other):
"""
:return: ``True`` if this EUI object is numerically greater or equal \
in value to other, ``False`` otherwise.
"""
try:
return(self.version, self._value) >= (other.version, other._value)
except AttributeError:
return NotImplemented
def bits(self, word_sep=None):
"""
:param word_sep: (optional) the separator to insert between words. \
Default: None - use default separator for address type.
:return: human-readable binary digit string of this address.
"""
return self._module.int_to_bits(self._value, word_sep)
@property
def packed(self):
"""The value of this EUI address as a packed binary string."""
return self._module.int_to_packed(self._value)
@property
def words(self):
"""A list of unsigned integer octets found in this EUI address."""
return self._module.int_to_words(self._value)
@property
def bin(self):
"""
The value of this EUI adddress in standard Python binary
representational form (0bxxx). A back port of the format provided by
the builtin bin() function found in Python 2.6.x and higher.
"""
return self._module.int_to_bin(self._value)
def eui64(self):
"""
- If this object represents an EUI-48 it is converted to EUI-64 \
as per the standard.
- If this object is already and EUI-64, it just returns a new, \
numerically equivalent object is returned instead.
:return: The value of this EUI object as a new 64-bit EUI object.
"""
if self.version == 48:
eui64_words = ["%02x" % i for i in self[0:3]] + ['ff', 'fe'] + \
["%02x" % i for i in self[3:6]]
return self.__class__('-'.join(eui64_words))
else:
return EUI(str(self))
def ipv6_link_local(self):
"""
.. note:: This poses security risks in certain scenarios. \
Please read RFC 4941 for details. Reference: RFCs 4291 and 4941.
:return: new link local IPv6 `IPAddress` object based on this `EUI` \
using the technique described in RFC 4291.
"""
int_val = 0xfe800000000000000000000000000000
if self.version == 48:
eui64_tokens = ["%02x" % i for i in self[0:3]] + ['ff', 'fe'] + \
["%02x" % i for i in self[3:6]]
int_val += int(''.join(eui64_tokens), 16)
else:
int_val += self._value
# Modified EUI-64 format interface identifiers are formed by inverting
# the "u" bit (universal/local bit in IEEE EUI-64 terminology) when
# forming the interface identifier from IEEE EUI-64 identifiers. In
# the resulting Modified EUI-64 format, the "u" bit is set to one (1)
# to indicate universal scope, and it is set to zero (0) to indicate
# local scope.
int_val ^= 0x00000000000000000200000000000000
return IPAddress(int_val, 6)
@property
def info(self):
"""
A record dict containing IEEE registration details for this EUI
(MAC-48) if available, None otherwise.
"""
data = {'OUI': self.oui.registration()}
if self.is_iab():
data['IAB'] = self.iab.registration()
return DictDotLookup(data)
def __str__(self):
""":return: EUI in representational format"""
return self._module.int_to_str(self._value, self._dialect)
def __repr__(self):
""":return: executable Python string to recreate equivalent object."""
return "EUI('%s')" % self