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