pvl/hosts/host.py
changeset 739 5149c39f3dfc
parent 738 3104fdf7ea26
child 740 74352351d6f5
equal deleted inserted replaced
738:3104fdf7ea26 739:5149c39f3dfc
    67         Normalize ethernet str.
    67         Normalize ethernet str.
    68     """
    68     """
    69 
    69 
    70     return ':'.join('%02x' % int(x, 16) for x in value.split(':'))
    70     return ':'.join('%02x' % int(x, 16) for x in value.split(':'))
    71 
    71 
    72 def parse_dhcp_boot(boot):
       
    73     """
       
    74         Parse the dhcp boot=... option
       
    75 
       
    76         >>> print parse_dhcp_boot(None)
       
    77         {}
       
    78         >>> print parse_dhcp_boot({'filename': '/foo'})
       
    79         {'filename': '/foo'}
       
    80         >>> print parse_dhcp_boot({'filename': '/foo', 'next-server': 'bar'})
       
    81         {'next-server': 'bar', 'filename': '/foo'}
       
    82         >>> print parse_dhcp_boot('/foo')
       
    83         {'filename': '/foo'}
       
    84         >>> print parse_dhcp_boot('bar:/foo')
       
    85         {'next-server': 'bar', 'filename': '/foo'}
       
    86         >>> print parse_dhcp_boot('bar:')
       
    87         {'next-server': 'bar'}
       
    88         >>> print parse_dhcp_boot('foo')
       
    89         Traceback (most recent call last):
       
    90             ...
       
    91         ValueError: invalid boot=foo
       
    92     """
       
    93     
       
    94     # normalize to dict
       
    95     if not boot:
       
    96         boot = { }
       
    97     elif not isinstance(boot, dict):
       
    98         boot = { None: boot }
       
    99     else:
       
   100         boot = dict(boot)
       
   101     
       
   102     # support either an instanced dict or a plain str or a mixed instanced-with-plain-str
       
   103     boot_str = boot.pop(None, None)
       
   104 
       
   105     if not (set(boot) <= set(('filename', 'next-server', None))):
       
   106         raise ValueError("Invalid boot.*: {instances}".format(instances=' '.join(boot)))
       
   107 
       
   108     # any boot= given overrides boot.* fields
       
   109     if not boot_str:
       
   110         pass
       
   111     elif boot_str.startswith('/'):
       
   112         boot['filename'] = boot_str
       
   113 
       
   114     elif boot_str.endswith(':'):
       
   115         boot['next-server'] = boot_str[:-1]
       
   116 
       
   117     elif ':' in boot_str:
       
   118         boot['next-server'], boot['filename'] = boot_str.split(':', 1)
       
   119 
       
   120     else :
       
   121         raise ValueError("invalid boot={boot}".format(boot=boot_str))
       
   122     
       
   123     return boot
       
   124 
       
   125 def parse_str(value):
    72 def parse_str(value):
   126     """
    73     """
   127         Normalize optional string value.
    74         Normalize optional string value.
   128     """
    75     """
   129 
    76 
   153     """
   100     """
   154         A host is a network node that can have multiple ethernet interfaces, and multiple IP addresses in different domains.
   101         A host is a network node that can have multiple ethernet interfaces, and multiple IP addresses in different domains.
   155     """
   102     """
   156 
   103 
   157     EXTENSIONS = { }
   104     EXTENSIONS = { }
       
   105     EXTENSION_FIELDS = { }
   158 
   106 
   159     @classmethod
   107     @classmethod
   160     def build_extensions(cls, extensions):
   108     def parse_extensions(cls, extensions, extra):
   161         for extension, value in extensions.iteritems():
   109         """
       
   110             Parse extensions and extension fields to yield
       
   111                 (HostExtension, field, value)
       
   112         """
       
   113 
       
   114         for extension, values in extensions.iteritems():
   162             extension_cls = cls.EXTENSIONS.get(extension)
   115             extension_cls = cls.EXTENSIONS.get(extension)
   163 
   116 
   164             if extension_cls:
   117             if not extension_cls:
   165                 yield extension, extension_cls.build(**value)
       
   166             else:
       
   167                 log.warning("skip unknown extension: %s", extension)
   118                 log.warning("skip unknown extension: %s", extension)
       
   119                 continue
       
   120             
       
   121             for field, value in values.iteritems():
       
   122                 yield extension_cls, field, value
       
   123 
       
   124         for field, value in extra.iteritems():
       
   125             extension_cls = cls.EXTENSION_FIELDS.get(field)
       
   126 
       
   127             if not extension_cls:
       
   128                 raise ValueError("unknown field: {field}".format(field=field))
       
   129             
       
   130             yield extension_cls, field, value
   168 
   131 
   169     @classmethod
   132     @classmethod
   170     def build (cls, name, domain,
   133     def build (cls, name, domain,
   171             ip=None, ip6=None,
   134             ip=None, ip6=None,
   172             ethernet=None,
   135             ethernet=None,
   173             owner=None,
   136             owner=None,
   174             location=None,
   137             location=None,
   175             alias=None, alias4=None, alias6=None,
   138             alias=None, alias4=None, alias6=None,
   176             forward=None, reverse=None,
   139             forward=None, reverse=None,
   177             down=None,
   140             down=None,
   178             boot=None,
       
   179             extensions={ },
   141             extensions={ },
       
   142             **extra
   180     ) :
   143     ) :
   181         """
   144         """
   182             Return a Host initialized from data attributes.
   145             Return a Host initialized from data attributes.
   183 
   146 
   184             This handles all string parsing to our data types.
   147             This handles all string parsing to our data types.
   185         """
   148         """
       
   149 
       
   150         extension_classes = { }
       
   151 
       
   152         for extension_cls, field, value in cls.parse_extensions(extensions, extra):
       
   153             extension_classes.setdefault(extension_cls, dict())[field] = value
       
   154 
       
   155         extensions = {extension_cls.EXTENSION: extension_cls.build(**params) for extension_cls, params in extension_classes.iteritems()}
   186 
   156 
   187         return cls(name,
   157         return cls(name,
   188                 domain      = domain,
   158                 domain      = domain,
   189                 ip4         = parse_ip(ip, ipaddr.IPv4Address),
   159                 ip4         = parse_ip(ip, ipaddr.IPv4Address),
   190                 ip6         = parse_ip(ip6, ipaddr.IPv6Address),
   160                 ip6         = parse_ip(ip6, ipaddr.IPv6Address),
   195                 alias4      = parse_list(alias4),
   165                 alias4      = parse_list(alias4),
   196                 alias6      = parse_list(alias6),
   166                 alias6      = parse_list(alias6),
   197                 forward     = parse_str(forward),
   167                 forward     = parse_str(forward),
   198                 reverse     = parse_str(reverse),
   168                 reverse     = parse_str(reverse),
   199                 down        = parse_bool(down),
   169                 down        = parse_bool(down),
   200                 boot        = parse_dhcp_boot(boot),
   170                 extensions  = extensions,
   201                 extensions  = dict(cls.build_extensions(extensions)),
       
   202         )
   171         )
   203 
   172 
   204     def __init__ (self, name, domain,
   173     def __init__ (self, name, domain,
   205             ip4=None, ip6=None,
   174             ip4=None, ip6=None,
   206             ethernet={ },
   175             ethernet={ },
   207             owner=None,
   176             owner=None,
   208             location=None,
   177             location=None,
   209             alias=(), alias4=(), alias6=(),
   178             alias=(), alias4=(), alias6=(),
   210             forward=None, reverse=None,
   179             forward=None, reverse=None,
   211             down=None,
   180             down=None,
   212             boot=None,
       
   213             extensions={},
   181             extensions={},
   214     ):
   182     ):
   215         """
   183         """
   216             name        - str
   184             name        - str
   217             domain      - str
   185             domain      - str
   240         self.alias = alias
   208         self.alias = alias
   241         self.alias4 = alias4
   209         self.alias4 = alias4
   242         self.alias6 = alias6
   210         self.alias6 = alias6
   243         self.owner = owner
   211         self.owner = owner
   244         self.location = location
   212         self.location = location
   245         self.boot = boot
       
   246         self.forward = forward
   213         self.forward = forward
   247         self.reverse = reverse
   214         self.reverse = reverse
   248         self.down = down
   215         self.down = down
   249         self.extensions = extensions
   216         self.extensions = extensions
   250 
   217 
   294         Base class for Host.EXTENSIONS
   261         Base class for Host.EXTENSIONS
   295 
   262 
   296         Provides default no-op behaviours for extension hooks.
   263         Provides default no-op behaviours for extension hooks.
   297     """
   264     """
   298 
   265 
       
   266     EXTENSION = None
       
   267     EXTENSION_FIELDS = ()
       
   268 
   299     def addresses (self):
   269     def addresses (self):
   300         """
   270         """
   301             Yield additional (sublabel, ipaddr) records.
   271             Yield additional (sublabel, ipaddr) records.
   302         """
   272         """
   303 
   273 
   304         return ()
   274         return ()
   305 
   275 
   306 def extension (cls):
   276 def register_extension (cls):
   307     """
   277     """
   308         Register an extension class
   278         Register an extension class
   309     """
   279     """
   310 
   280 
   311     Host.EXTENSIONS[cls.EXTENSION] = cls
   281     Host.EXTENSIONS[cls.EXTENSION] = cls
   312 
   282 
       
   283     for field in cls.EXTENSION_FIELDS:
       
   284         Host.EXTENSION_FIELDS[field] = cls