Merge lp:~smoser/cloud-init/trunk.joyent-cleanup into lp:~cloud-init-dev/cloud-init/trunk
- trunk.joyent-cleanup
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 1225 |
Proposed branch: | lp:~smoser/cloud-init/trunk.joyent-cleanup |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
1140 lines (+544/-349) 2 files modified
cloudinit/sources/DataSourceSmartOS.py (+416/-175) tests/unittests/test_datasource/test_smartos.py (+128/-174) |
To merge this branch: | bzr merge lp:~smoser/cloud-init/trunk.joyent-cleanup |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
cloud-init Commiters | Pending | ||
Review via email: mp+295981@code.launchpad.net |
Commit message
smartos: separate client out of datasource and support network config
This does:
* support reading 'sdc:nics' and converting it to internal
'network-config' format.
* change the datasource to be a local datasource (which is required
to apply networking configuration).
This change requires some rework of init to allow a datasource
to be found locally but not resolve all userdata/metadata until
networking is up. (that is coming)
* support 'list', 'put', and 'delete' in the metadata client.
* add 'get_json' to get known values that have json content.
* add a simple main to datasource demonstrating how to read.
* separate out the "legacy" behavior of having optionally
base64 encoded data into JoyentMetadataL
Note, tests have been dropped that expected datasource.md[key] to be
base64 encoded. Now, the client takes care of decoding that. There
is not any real reason to store the base64 values in the datasource.
Description of the change
- 1230. By Scott Moser
-
assertEquals
- 1231. By Scott Moser
-
return dict not None on get_config_obj
Scott Moser (smoser) wrote : | # |
- 1232. By Scott Moser
-
assertEqual
- 1233. By Scott Moser
-
use constants for kvm and lx-brand
Preview Diff
1 | === modified file 'cloudinit/sources/DataSourceSmartOS.py' |
2 | --- cloudinit/sources/DataSourceSmartOS.py 2016-04-12 16:57:50 +0000 |
3 | +++ cloudinit/sources/DataSourceSmartOS.py 2016-05-31 17:14:35 +0000 |
4 | @@ -32,13 +32,13 @@ |
5 | # http://us-east.manta.joyent.com/jmc/public/mdata/datadict.html |
6 | # Comments with "@datadictionary" are snippets of the definition |
7 | |
8 | +import base64 |
9 | import binascii |
10 | -import contextlib |
11 | +import json |
12 | import os |
13 | import random |
14 | import re |
15 | import socket |
16 | -import stat |
17 | |
18 | import serial |
19 | |
20 | @@ -64,14 +64,36 @@ |
21 | 'operator-script': ('sdc:operator-script', False), |
22 | } |
23 | |
24 | +SMARTOS_ATTRIB_JSON = { |
25 | + # Cloud-init Key : (SmartOS Key known JSON) |
26 | + 'network-data': 'sdc:nics', |
27 | +} |
28 | + |
29 | +SMARTOS_ENV_LX_BRAND = "lx-brand" |
30 | +SMARTOS_ENV_KVM = "kvm" |
31 | + |
32 | DS_NAME = 'SmartOS' |
33 | DS_CFG_PATH = ['datasource', DS_NAME] |
34 | +NO_BASE64_DECODE = [ |
35 | + 'iptables_disable', |
36 | + 'motd_sys_info', |
37 | + 'root_authorized_keys', |
38 | + 'sdc:datacenter_name', |
39 | + 'sdc:uuid' |
40 | + 'user-data', |
41 | + 'user-script', |
42 | +] |
43 | + |
44 | +METADATA_SOCKFILE = '/native/.zonecontrol/metadata.sock' |
45 | +SERIAL_DEVICE = '/dev/ttyS1' |
46 | +SERIAL_TIMEOUT = 60 |
47 | + |
48 | # BUILT-IN DATASOURCE CONFIGURATION |
49 | # The following is the built-in configuration. If the values |
50 | # are not set via the system configuration, then these default |
51 | # will be used: |
52 | # serial_device: which serial device to use for the meta-data |
53 | -# seed_timeout: how long to wait on the device |
54 | +# serial_timeout: how long to wait on the device |
55 | # no_base64_decode: values which are not base64 encoded and |
56 | # are fetched directly from SmartOS, not meta-data values |
57 | # base64_keys: meta-data keys that are delivered in base64 |
58 | @@ -81,16 +103,10 @@ |
59 | # fs_setup: describes how to format the ephemeral drive |
60 | # |
61 | BUILTIN_DS_CONFIG = { |
62 | - 'serial_device': '/dev/ttyS1', |
63 | - 'metadata_sockfile': '/native/.zonecontrol/metadata.sock', |
64 | - 'seed_timeout': 60, |
65 | - 'no_base64_decode': ['root_authorized_keys', |
66 | - 'motd_sys_info', |
67 | - 'iptables_disable', |
68 | - 'user-data', |
69 | - 'user-script', |
70 | - 'sdc:datacenter_name', |
71 | - 'sdc:uuid'], |
72 | + 'serial_device': SERIAL_DEVICE, |
73 | + 'serial_timeout': SERIAL_TIMEOUT, |
74 | + 'metadata_sockfile': METADATA_SOCKFILE, |
75 | + 'no_base64_decode': NO_BASE64_DECODE, |
76 | 'base64_keys': [], |
77 | 'base64_all': False, |
78 | 'disk_aliases': {'ephemeral0': '/dev/vdb'}, |
79 | @@ -154,59 +170,40 @@ |
80 | |
81 | |
82 | class DataSourceSmartOS(sources.DataSource): |
83 | + _unset = "_unset" |
84 | + smartos_environ = _unset |
85 | + md_client = _unset |
86 | + |
87 | def __init__(self, sys_cfg, distro, paths): |
88 | sources.DataSource.__init__(self, sys_cfg, distro, paths) |
89 | - self.is_smartdc = None |
90 | self.ds_cfg = util.mergemanydict([ |
91 | self.ds_cfg, |
92 | util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}), |
93 | BUILTIN_DS_CONFIG]) |
94 | |
95 | self.metadata = {} |
96 | + self.network_data = None |
97 | + self._network_config = None |
98 | |
99 | - # SDC LX-Brand Zones lack dmidecode (no /dev/mem) but |
100 | - # report 'BrandZ virtual linux' as the kernel version |
101 | - if os.uname()[3].lower() == 'brandz virtual linux': |
102 | - LOG.debug("Host is SmartOS, guest in Zone") |
103 | - self.is_smartdc = True |
104 | - self.smartos_type = 'lx-brand' |
105 | - self.cfg = {} |
106 | - self.seed = self.ds_cfg.get("metadata_sockfile") |
107 | - else: |
108 | - self.is_smartdc = True |
109 | - self.smartos_type = 'kvm' |
110 | - self.seed = self.ds_cfg.get("serial_device") |
111 | - self.cfg = BUILTIN_CLOUD_CONFIG |
112 | - self.seed_timeout = self.ds_cfg.get("serial_timeout") |
113 | - self.smartos_no_base64 = self.ds_cfg.get('no_base64_decode') |
114 | - self.b64_keys = self.ds_cfg.get('base64_keys') |
115 | - self.b64_all = self.ds_cfg.get('base64_all') |
116 | self.script_base_d = os.path.join(self.paths.get_cpath("scripts")) |
117 | + self.smartos_type = None |
118 | + |
119 | + self._init() |
120 | |
121 | def __str__(self): |
122 | root = sources.DataSource.__str__(self) |
123 | - return "%s [seed=%s]" % (root, self.seed) |
124 | - |
125 | - def _get_seed_file_object(self): |
126 | - if not self.seed: |
127 | - raise AttributeError("seed device is not set") |
128 | - |
129 | - if self.smartos_type == 'lx-brand': |
130 | - if not stat.S_ISSOCK(os.stat(self.seed).st_mode): |
131 | - LOG.debug("Seed %s is not a socket", self.seed) |
132 | - return None |
133 | - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
134 | - sock.connect(self.seed) |
135 | - return sock.makefile('rwb') |
136 | - else: |
137 | - if not stat.S_ISCHR(os.stat(self.seed).st_mode): |
138 | - LOG.debug("Seed %s is not a character device") |
139 | - return None |
140 | - ser = serial.Serial(self.seed, timeout=self.seed_timeout) |
141 | - if not ser.isOpen(): |
142 | - raise SystemError("Unable to open %s" % self.seed) |
143 | - return ser |
144 | - return None |
145 | + return "%s [client=%s]" % (root, self.md_client) |
146 | + |
147 | + def _init(self): |
148 | + if self.smartos_environ == self._unset: |
149 | + self.smartos_type = get_smartos_environ() |
150 | + |
151 | + if self.md_client == self._unset: |
152 | + self.md_client = jmc_client_factory( |
153 | + smartos_type=self.smartos_type, |
154 | + metadata_sockfile=self.ds_cfg['metadata_sockfile'], |
155 | + serial_device=self.ds_cfg['serial_device'], |
156 | + serial_timeout=self.ds_cfg['serial_timeout']) |
157 | |
158 | def _set_provisioned(self): |
159 | '''Mark the instance provisioning state as successful. |
160 | @@ -225,50 +222,26 @@ |
161 | '/'.join([svc_path, 'provision_success'])) |
162 | |
163 | def get_data(self): |
164 | + self._init() |
165 | + |
166 | md = {} |
167 | ud = "" |
168 | |
169 | - if not device_exists(self.seed): |
170 | - LOG.debug("No metadata device '%s' found for SmartOS datasource", |
171 | - self.seed) |
172 | - return False |
173 | - |
174 | - uname_arch = os.uname()[4] |
175 | - if uname_arch.startswith("arm") or uname_arch == "aarch64": |
176 | - # Disabling because dmidcode in dmi_data() crashes kvm process |
177 | - LOG.debug("Disabling SmartOS datasource on arm (LP: #1243287)") |
178 | - return False |
179 | - |
180 | - # SDC KVM instances will provide dmi data, LX-brand does not |
181 | - if self.smartos_type == 'kvm': |
182 | - dmi_info = dmi_data() |
183 | - if dmi_info is None: |
184 | - LOG.debug("No dmidata utility found") |
185 | - return False |
186 | - |
187 | - system_type = dmi_info |
188 | - if 'smartdc' not in system_type.lower(): |
189 | - LOG.debug("Host is not on SmartOS. system_type=%s", |
190 | - system_type) |
191 | - return False |
192 | - LOG.debug("Host is SmartOS, guest in KVM") |
193 | - |
194 | - seed_obj = self._get_seed_file_object() |
195 | - if seed_obj is None: |
196 | - LOG.debug('Seed file object not found.') |
197 | - return False |
198 | - with contextlib.closing(seed_obj) as seed: |
199 | - b64_keys = self.query('base64_keys', seed, strip=True, b64=False) |
200 | - if b64_keys is not None: |
201 | - self.b64_keys = [k.strip() for k in str(b64_keys).split(',')] |
202 | - |
203 | - b64_all = self.query('base64_all', seed, strip=True, b64=False) |
204 | - if b64_all is not None: |
205 | - self.b64_all = util.is_true(b64_all) |
206 | - |
207 | - for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items(): |
208 | - smartos_noun, strip = attribute |
209 | - md[ci_noun] = self.query(smartos_noun, seed, strip=strip) |
210 | + if not self.smartos_type: |
211 | + LOG.debug("Not running on smartos") |
212 | + return False |
213 | + |
214 | + if not self.md_client.exists(): |
215 | + LOG.debug("No metadata device '%r' found for SmartOS datasource", |
216 | + self.md_client) |
217 | + return False |
218 | + |
219 | + for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items(): |
220 | + smartos_noun, strip = attribute |
221 | + md[ci_noun] = self.md_client.get(smartos_noun, strip=strip) |
222 | + |
223 | + for ci_noun, smartos_noun in SMARTOS_ATTRIB_JSON.items(): |
224 | + md[ci_noun] = self.md_client.get_json(smartos_noun) |
225 | |
226 | # @datadictionary: This key may contain a program that is written |
227 | # to a file in the filesystem of the guest on each boot and then |
228 | @@ -318,6 +291,7 @@ |
229 | self.metadata = util.mergemanydict([md, self.metadata]) |
230 | self.userdata_raw = ud |
231 | self.vendordata_raw = md['vendor-data'] |
232 | + self.network_data = md['network-data'] |
233 | |
234 | self._set_provisioned() |
235 | return True |
236 | @@ -326,69 +300,20 @@ |
237 | return self.ds_cfg['disk_aliases'].get(name) |
238 | |
239 | def get_config_obj(self): |
240 | - return self.cfg |
241 | + if self.smartos_type == SMARTOS_ENV_KVM: |
242 | + return BUILTIN_CLOUD_CONFIG |
243 | + return {} |
244 | |
245 | def get_instance_id(self): |
246 | return self.metadata['instance-id'] |
247 | |
248 | - def query(self, noun, seed_file, strip=False, default=None, b64=None): |
249 | - if b64 is None: |
250 | - if noun in self.smartos_no_base64: |
251 | - b64 = False |
252 | - elif self.b64_all or noun in self.b64_keys: |
253 | - b64 = True |
254 | - |
255 | - return self._query_data(noun, seed_file, strip=strip, |
256 | - default=default, b64=b64) |
257 | - |
258 | - def _query_data(self, noun, seed_file, strip=False, |
259 | - default=None, b64=None): |
260 | - """Makes a request via "GET <NOUN>" |
261 | - |
262 | - In the response, the first line is the status, while subsequent |
263 | - lines are is the value. A blank line with a "." is used to |
264 | - indicate end of response. |
265 | - |
266 | - If the response is expected to be base64 encoded, then set |
267 | - b64encoded to true. Unfortantely, there is no way to know if |
268 | - something is 100% encoded, so this method relies on being told |
269 | - if the data is base64 or not. |
270 | - """ |
271 | - |
272 | - if not noun: |
273 | - return False |
274 | - |
275 | - response = JoyentMetadataClient(seed_file).get_metadata(noun) |
276 | - |
277 | - if response is None: |
278 | - return default |
279 | - |
280 | - if b64 is None: |
281 | - b64 = self._query_data('b64-%s' % noun, seed_file, b64=False, |
282 | - default=False, strip=True) |
283 | - b64 = util.is_true(b64) |
284 | - |
285 | - resp = None |
286 | - if b64 or strip: |
287 | - resp = "".join(response).rstrip() |
288 | - else: |
289 | - resp = "".join(response) |
290 | - |
291 | - if b64: |
292 | - try: |
293 | - return util.b64d(resp) |
294 | - # Bogus input produces different errors in Python 2 and 3; |
295 | - # catch both. |
296 | - except (TypeError, binascii.Error): |
297 | - LOG.warn("Failed base64 decoding key '%s'", noun) |
298 | - return resp |
299 | - |
300 | - return resp |
301 | - |
302 | - |
303 | -def device_exists(device): |
304 | - """Symplistic method to determine if the device exists or not""" |
305 | - return os.path.exists(device) |
306 | + @property |
307 | + def network_config(self): |
308 | + if self._network_config is None: |
309 | + if self.network_data is not None: |
310 | + self._network_config = ( |
311 | + convert_smartos_network_data(self.network_data)) |
312 | + return self._network_config |
313 | |
314 | |
315 | class JoyentMetadataFetchException(Exception): |
316 | @@ -407,8 +332,11 @@ |
317 | r' (?P<body>(?P<request_id>[0-9a-f]+) (?P<status>SUCCESS|NOTFOUND)' |
318 | r'( (?P<payload>.+))?)') |
319 | |
320 | - def __init__(self, metasource): |
321 | - self.metasource = metasource |
322 | + def __init__(self, smartos_type=None, fp=None): |
323 | + if smartos_type is None: |
324 | + smartos_type = get_smartos_environ() |
325 | + self.smartos_type = smartos_type |
326 | + self.fp = fp |
327 | |
328 | def _checksum(self, body): |
329 | return '{0:08x}'.format( |
330 | @@ -436,37 +364,227 @@ |
331 | LOG.debug('Value "%s" found.', value) |
332 | return value |
333 | |
334 | - def get_metadata(self, metadata_key): |
335 | - LOG.debug('Fetching metadata key "%s"...', metadata_key) |
336 | + def request(self, rtype, param=None): |
337 | request_id = '{0:08x}'.format(random.randint(0, 0xffffffff)) |
338 | - message_body = '{0} GET {1}'.format(request_id, |
339 | - util.b64e(metadata_key)) |
340 | + message_body = ' '.join((request_id, rtype,)) |
341 | + if param: |
342 | + message_body += ' ' + base64.b64encode(param.encode()).decode() |
343 | msg = 'V2 {0} {1} {2}\n'.format( |
344 | len(message_body), self._checksum(message_body), message_body) |
345 | LOG.debug('Writing "%s" to metadata transport.', msg) |
346 | - self.metasource.write(msg.encode('ascii')) |
347 | - self.metasource.flush() |
348 | + |
349 | + need_close = False |
350 | + if not self.fp: |
351 | + self.open_transport() |
352 | + need_close = True |
353 | + |
354 | + self.fp.write(msg.encode('ascii')) |
355 | + self.fp.flush() |
356 | |
357 | response = bytearray() |
358 | - response.extend(self.metasource.read(1)) |
359 | + response.extend(self.fp.read(1)) |
360 | while response[-1:] != b'\n': |
361 | - response.extend(self.metasource.read(1)) |
362 | + response.extend(self.fp.read(1)) |
363 | + |
364 | + if need_close: |
365 | + self.close_transport() |
366 | + |
367 | response = response.rstrip().decode('ascii') |
368 | LOG.debug('Read "%s" from metadata transport.', response) |
369 | |
370 | if 'SUCCESS' not in response: |
371 | return None |
372 | |
373 | - return self._get_value_from_frame(request_id, response) |
374 | - |
375 | - |
376 | -def dmi_data(): |
377 | - sys_type = util.read_dmi_data("system-product-name") |
378 | - |
379 | - if not sys_type: |
380 | - return None |
381 | - |
382 | - return sys_type |
383 | + value = self._get_value_from_frame(request_id, response) |
384 | + return value |
385 | + |
386 | + def get(self, key, default=None, strip=False): |
387 | + result = self.request(rtype='GET', param=key) |
388 | + if result is None: |
389 | + return default |
390 | + if result and strip: |
391 | + result = result.strip() |
392 | + return result |
393 | + |
394 | + def get_json(self, key, default=None): |
395 | + result = self.get(key, default=default) |
396 | + if result is None: |
397 | + return default |
398 | + return json.loads(result) |
399 | + |
400 | + def list(self): |
401 | + result = self.request(rtype='KEYS') |
402 | + if result: |
403 | + result = result.split('\n') |
404 | + return result |
405 | + |
406 | + def put(self, key, val): |
407 | + param = b' '.join([base64.b64encode(i.encode()) |
408 | + for i in (key, val)]).decode() |
409 | + return self.request(rtype='PUT', param=param) |
410 | + |
411 | + def delete(self, key): |
412 | + return self.request(rtype='DELETE', param=key) |
413 | + |
414 | + def close_transport(self): |
415 | + if self.fp: |
416 | + self.fp.close() |
417 | + self.fp = None |
418 | + |
419 | + def __enter__(self): |
420 | + if self.fp: |
421 | + return self |
422 | + self.open_transport() |
423 | + return self |
424 | + |
425 | + def __exit__(self, exc_type, exc_value, traceback): |
426 | + self.close_transport() |
427 | + return |
428 | + |
429 | + def open_transport(self): |
430 | + raise NotImplementedError |
431 | + |
432 | + |
433 | +class JoyentMetadataSocketClient(JoyentMetadataClient): |
434 | + def __init__(self, socketpath): |
435 | + self.socketpath = socketpath |
436 | + |
437 | + def open_transport(self): |
438 | + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
439 | + sock.connect(self.socketpath) |
440 | + self.fp = sock.makefile('rwb') |
441 | + |
442 | + def exists(self): |
443 | + return os.path.exists(self.socketpath) |
444 | + |
445 | + def __repr__(self): |
446 | + return "%s(socketpath=%s)" % (self.__class__.__name__, self.socketpath) |
447 | + |
448 | + |
449 | +class JoyentMetadataSerialClient(JoyentMetadataClient): |
450 | + def __init__(self, device, timeout=10, smartos_type=None): |
451 | + super(JoyentMetadataSerialClient, self).__init__(smartos_type) |
452 | + self.device = device |
453 | + self.timeout = timeout |
454 | + |
455 | + def exists(self): |
456 | + return os.path.exists(self.device) |
457 | + |
458 | + def open_transport(self): |
459 | + ser = serial.Serial(self.device, timeout=self.timeout) |
460 | + if not ser.isOpen(): |
461 | + raise SystemError("Unable to open %s" % self.device) |
462 | + self.fp = ser |
463 | + |
464 | + def __repr__(self): |
465 | + return "%s(device=%s, timeout=%s)" % ( |
466 | + self.__class__.__name__, self.device, self.timeout) |
467 | + |
468 | + |
469 | +class JoyentMetadataLegacySerialClient(JoyentMetadataSerialClient): |
470 | + """V1 of the protocol was not safe for all values. |
471 | + Thus, we allowed the user to pass values in as base64 encoded. |
472 | + Users may still reasonably expect to be able to send base64 data |
473 | + and have it transparently decoded. So even though the V2 format is |
474 | + now used, and is safe (using base64 itself), we keep legacy support. |
475 | + |
476 | + The way for a user to do this was: |
477 | + a.) specify 'base64_keys' key whose value is a comma delimited |
478 | + list of keys that were base64 encoded. |
479 | + b.) base64_all: string interpreted as a boolean that indicates |
480 | + if all keys are base64 encoded. |
481 | + c.) set a key named b64-<keyname> with a boolean indicating that |
482 | + <keyname> is base64 encoded.""" |
483 | + |
484 | + def __init__(self, device, timeout=10, smartos_type=None): |
485 | + s = super(JoyentMetadataLegacySerialClient, self) |
486 | + s.__init__(device, timeout, smartos_type) |
487 | + self.base64_keys = None |
488 | + self.base64_all = None |
489 | + |
490 | + def _init_base64_keys(self, reset=False): |
491 | + if reset: |
492 | + self.base64_keys = None |
493 | + self.base64_all = None |
494 | + |
495 | + keys = None |
496 | + if self.base64_all is None: |
497 | + keys = self.list() |
498 | + if 'base64_all' in keys: |
499 | + self.base64_all = util.is_true(self._get("base64_all")) |
500 | + else: |
501 | + self.base64_all = False |
502 | + |
503 | + if self.base64_all: |
504 | + # short circuit if base64_all is true |
505 | + return |
506 | + |
507 | + if self.base64_keys is None: |
508 | + if keys is None: |
509 | + keys = self.list() |
510 | + b64_keys = set() |
511 | + if 'base64_keys' in keys: |
512 | + b64_keys = set(self._get("base64_keys").split(",")) |
513 | + |
514 | + # now add any b64-<keyname> that has a true value |
515 | + for key in [k[3:] for k in keys if k.startswith("b64-")]: |
516 | + if util.is_true(self._get(key)): |
517 | + b64_keys.add(key) |
518 | + else: |
519 | + if key in b64_keys: |
520 | + b64_keys.remove(key) |
521 | + |
522 | + self.base64_keys = b64_keys |
523 | + |
524 | + def _get(self, key, default=None, strip=False): |
525 | + return (super(JoyentMetadataLegacySerialClient, self). |
526 | + get(key, default=default, strip=strip)) |
527 | + |
528 | + def is_b64_encoded(self, key, reset=False): |
529 | + if key in NO_BASE64_DECODE: |
530 | + return False |
531 | + |
532 | + self._init_base64_keys(reset=reset) |
533 | + if self.base64_all: |
534 | + return True |
535 | + |
536 | + return key in self.base64_keys |
537 | + |
538 | + def get(self, key, default=None, strip=False): |
539 | + mdefault = object() |
540 | + val = self._get(key, strip=False, default=mdefault) |
541 | + if val is mdefault: |
542 | + return default |
543 | + |
544 | + if self.is_b64_encoded(key): |
545 | + try: |
546 | + val = base64.b64decode(val.encode()).decode() |
547 | + # Bogus input produces different errors in Python 2 and 3 |
548 | + except (TypeError, binascii.Error): |
549 | + LOG.warn("Failed base64 decoding key '%s': %s", key, val) |
550 | + |
551 | + if strip: |
552 | + val = val.strip() |
553 | + |
554 | + return val |
555 | + |
556 | + |
557 | +def jmc_client_factory( |
558 | + smartos_type=None, metadata_sockfile=METADATA_SOCKFILE, |
559 | + serial_device=SERIAL_DEVICE, serial_timeout=SERIAL_TIMEOUT, |
560 | + uname_version=None): |
561 | + |
562 | + if smartos_type is None: |
563 | + smartos_type = get_smartos_environ(uname_version) |
564 | + |
565 | + if smartos_type == SMARTOS_ENV_KVM: |
566 | + return JoyentMetadataLegacySerialClient( |
567 | + device=serial_device, timeout=serial_timeout, |
568 | + smartos_type=smartos_type) |
569 | + elif smartos_type == SMARTOS_ENV_LX_BRAND: |
570 | + return JoyentMetadataSocketClient(socketpath=metadata_sockfile) |
571 | + |
572 | + raise ValueError("Unknown value for smartos_type: %s" % smartos_type) |
573 | |
574 | |
575 | def write_boot_content(content, content_f, link=None, shebang=False, |
576 | @@ -522,15 +640,138 @@ |
577 | util.ensure_dir(os.path.dirname(link)) |
578 | os.symlink(content_f, link) |
579 | except IOError as e: |
580 | - util.logexc(LOG, "failed establishing content link", e) |
581 | + util.logexc(LOG, "failed establishing content link: %s", e) |
582 | + |
583 | + |
584 | +def get_smartos_environ(uname_version=None, product_name=None, |
585 | + uname_arch=None): |
586 | + uname = os.uname() |
587 | + if uname_arch is None: |
588 | + uname_arch = uname[4] |
589 | + |
590 | + if uname_arch.startswith("arm") or uname_arch == "aarch64": |
591 | + return None |
592 | + |
593 | + # SDC LX-Brand Zones lack dmidecode (no /dev/mem) but |
594 | + # report 'BrandZ virtual linux' as the kernel version |
595 | + if uname_version is None: |
596 | + uname_version = uname[3] |
597 | + if uname_version.lower() == 'brandz virtual linux': |
598 | + return SMARTOS_ENV_LX_BRAND |
599 | + |
600 | + if product_name is None: |
601 | + system_type = util.read_dmi_data("system-product-name") |
602 | + else: |
603 | + system_type = product_name |
604 | + |
605 | + if system_type and 'smartdc' in system_type.lower(): |
606 | + return SMARTOS_ENV_KVM |
607 | + |
608 | + return None |
609 | + |
610 | + |
611 | +# Covert SMARTOS 'sdc:nics' data to network_config yaml |
612 | +def convert_smartos_network_data(network_data=None): |
613 | + """Return a dictionary of network_config by parsing provided |
614 | + SMARTOS sdc:nics configuration data |
615 | + |
616 | + sdc:nics data is a dictionary of properties of a nic and the ip |
617 | + configuration desired. Additional nic dictionaries are appended |
618 | + to the list. |
619 | + |
620 | + Converting the format is straightforward though it does include |
621 | + duplicate information as well as data which appears to be relevant |
622 | + to the hostOS rather than the guest. |
623 | + |
624 | + For each entry in the nics list returned from query sdc:nics, we |
625 | + create a type: physical entry, and extract the interface properties: |
626 | + 'mac' -> 'mac_address', 'mtu', 'interface' -> 'name'. The remaining |
627 | + keys are related to ip configuration. For each ip in the 'ips' list |
628 | + we create a subnet entry under 'subnets' pairing the ip to a one in |
629 | + the 'gateways' list. |
630 | + """ |
631 | + |
632 | + valid_keys = { |
633 | + 'physical': [ |
634 | + 'mac_address', |
635 | + 'mtu', |
636 | + 'name', |
637 | + 'params', |
638 | + 'subnets', |
639 | + 'type', |
640 | + ], |
641 | + 'subnet': [ |
642 | + 'address', |
643 | + 'broadcast', |
644 | + 'dns_nameservers', |
645 | + 'dns_search', |
646 | + 'gateway', |
647 | + 'metric', |
648 | + 'netmask', |
649 | + 'pointopoint', |
650 | + 'routes', |
651 | + 'scope', |
652 | + 'type', |
653 | + ], |
654 | + } |
655 | + |
656 | + config = [] |
657 | + for nic in network_data: |
658 | + cfg = {k: v for k, v in nic.items() |
659 | + if k in valid_keys['physical']} |
660 | + cfg.update({ |
661 | + 'type': 'physical', |
662 | + 'name': nic['interface']}) |
663 | + if 'mac' in nic: |
664 | + cfg.update({'mac_address': nic['mac']}) |
665 | + |
666 | + subnets = [] |
667 | + for ip, gw in zip(nic['ips'], nic['gateways']): |
668 | + subnet = {k: v for k, v in nic.items() |
669 | + if k in valid_keys['subnet']} |
670 | + subnet.update({ |
671 | + 'type': 'static', |
672 | + 'address': ip, |
673 | + 'gateway': gw, |
674 | + }) |
675 | + subnets.append(subnet) |
676 | + cfg.update({'subnets': subnets}) |
677 | + config.append(cfg) |
678 | + |
679 | + return {'version': 1, 'config': config} |
680 | |
681 | |
682 | # Used to match classes to dependencies |
683 | datasources = [ |
684 | - (DataSourceSmartOS, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), |
685 | + (DataSourceSmartOS, (sources.DEP_FILESYSTEM, )), |
686 | ] |
687 | |
688 | |
689 | # Return a list of data sources that match this set of dependencies |
690 | def get_datasource_list(depends): |
691 | return sources.list_from_depends(depends, datasources) |
692 | + |
693 | + |
694 | +if __name__ == "__main__": |
695 | + import sys |
696 | + jmc = jmc_client_factory() |
697 | + if len(sys.argv) == 1: |
698 | + keys = (list(SMARTOS_ATTRIB_JSON.keys()) + |
699 | + list(SMARTOS_ATTRIB_MAP.keys())) |
700 | + else: |
701 | + keys = sys.argv[1:] |
702 | + |
703 | + data = {} |
704 | + for key in keys: |
705 | + if key in SMARTOS_ATTRIB_JSON: |
706 | + keyname = SMARTOS_ATTRIB_JSON[key] |
707 | + data[key] = jmc.get_json(keyname) |
708 | + else: |
709 | + if key in SMARTOS_ATTRIB_MAP: |
710 | + keyname, strip = SMARTOS_ATTRIB_MAP[key] |
711 | + else: |
712 | + keyname, strip = (key, False) |
713 | + val = jmc.get(keyname, strip=strip) |
714 | + data[key] = jmc.get(keyname, strip=strip) |
715 | + |
716 | + print(json.dumps(data, indent=1)) |
717 | |
718 | === modified file 'tests/unittests/test_datasource/test_smartos.py' |
719 | --- tests/unittests/test_datasource/test_smartos.py 2016-05-12 20:43:11 +0000 |
720 | +++ tests/unittests/test_datasource/test_smartos.py 2016-05-31 17:14:35 +0000 |
721 | @@ -25,6 +25,7 @@ |
722 | from __future__ import print_function |
723 | |
724 | from binascii import crc32 |
725 | +import json |
726 | import os |
727 | import os.path |
728 | import re |
729 | @@ -40,12 +41,49 @@ |
730 | from cloudinit.sources import DataSourceSmartOS |
731 | from cloudinit.util import b64e |
732 | |
733 | -from .. import helpers |
734 | +from ..helpers import mock, FilesystemMockingTestCase, TestCase |
735 | |
736 | -try: |
737 | - from unittest import mock |
738 | -except ImportError: |
739 | - import mock |
740 | +SDC_NICS = json.loads(""" |
741 | +[ |
742 | + { |
743 | + "nic_tag": "external", |
744 | + "primary": true, |
745 | + "mtu": 1500, |
746 | + "model": "virtio", |
747 | + "gateway": "8.12.42.1", |
748 | + "netmask": "255.255.255.0", |
749 | + "ip": "8.12.42.102", |
750 | + "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe", |
751 | + "gateways": [ |
752 | + "8.12.42.1" |
753 | + ], |
754 | + "vlan_id": 324, |
755 | + "mac": "90:b8:d0:f5:e4:f5", |
756 | + "interface": "net0", |
757 | + "ips": [ |
758 | + "8.12.42.102/24" |
759 | + ] |
760 | + }, |
761 | + { |
762 | + "nic_tag": "sdc_overlay/16187209", |
763 | + "gateway": "192.168.128.1", |
764 | + "model": "virtio", |
765 | + "mac": "90:b8:d0:a5:ff:cd", |
766 | + "netmask": "255.255.252.0", |
767 | + "ip": "192.168.128.93", |
768 | + "network_uuid": "4cad71da-09bc-452b-986d-03562a03a0a9", |
769 | + "gateways": [ |
770 | + "192.168.128.1" |
771 | + ], |
772 | + "vlan_id": 2, |
773 | + "mtu": 8500, |
774 | + "interface": "net1", |
775 | + "ips": [ |
776 | + "192.168.128.93/22" |
777 | + ] |
778 | + } |
779 | +] |
780 | +""") |
781 | |
782 | MOCK_RETURNS = { |
783 | 'hostname': 'test-host', |
784 | @@ -60,79 +98,66 @@ |
785 | 'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']), |
786 | 'user-data': '\n'.join(['something', '']), |
787 | 'user-script': '\n'.join(['/bin/true', '']), |
788 | + 'sdc:nics': json.dumps(SDC_NICS), |
789 | } |
790 | |
791 | DMI_DATA_RETURN = 'smartdc' |
792 | |
793 | |
794 | -def get_mock_client(mockdata): |
795 | - class MockMetadataClient(object): |
796 | - |
797 | - def __init__(self, serial): |
798 | - pass |
799 | - |
800 | - def get_metadata(self, metadata_key): |
801 | - return mockdata.get(metadata_key) |
802 | - return MockMetadataClient |
803 | - |
804 | - |
805 | -class TestSmartOSDataSource(helpers.FilesystemMockingTestCase): |
806 | +class PsuedoJoyentClient(object): |
807 | + def __init__(self, data=None): |
808 | + if data is None: |
809 | + data = MOCK_RETURNS.copy() |
810 | + self.data = data |
811 | + return |
812 | + |
813 | + def get(self, key, default=None, strip=False): |
814 | + if key in self.data: |
815 | + r = self.data[key] |
816 | + if strip: |
817 | + r = r.strip() |
818 | + else: |
819 | + r = default |
820 | + return r |
821 | + |
822 | + def get_json(self, key, default=None): |
823 | + result = self.get(key, default=default) |
824 | + if result is None: |
825 | + return default |
826 | + return json.loads(result) |
827 | + |
828 | + def exists(self): |
829 | + return True |
830 | + |
831 | + |
832 | +class TestSmartOSDataSource(FilesystemMockingTestCase): |
833 | def setUp(self): |
834 | super(TestSmartOSDataSource, self).setUp() |
835 | |
836 | + dsmos = 'cloudinit.sources.DataSourceSmartOS' |
837 | + patcher = mock.patch(dsmos + ".jmc_client_factory") |
838 | + self.jmc_cfact = patcher.start() |
839 | + self.addCleanup(patcher.stop) |
840 | + patcher = mock.patch(dsmos + ".get_smartos_environ") |
841 | + self.get_smartos_environ = patcher.start() |
842 | + self.addCleanup(patcher.stop) |
843 | + |
844 | self.tmp = tempfile.mkdtemp() |
845 | self.addCleanup(shutil.rmtree, self.tmp) |
846 | + self.paths = c_helpers.Paths({'cloud_dir': self.tmp}) |
847 | + |
848 | self.legacy_user_d = tempfile.mkdtemp() |
849 | - self.addCleanup(shutil.rmtree, self.legacy_user_d) |
850 | - |
851 | - # If you should want to watch the logs... |
852 | - self._log = None |
853 | - self._log_file = None |
854 | - self._log_handler = None |
855 | - |
856 | - # patch cloud_dir, so our 'seed_dir' is guaranteed empty |
857 | - self.paths = c_helpers.Paths({'cloud_dir': self.tmp}) |
858 | - |
859 | - self.unapply = [] |
860 | - super(TestSmartOSDataSource, self).setUp() |
861 | + self.orig_lud = DataSourceSmartOS.LEGACY_USER_D |
862 | + DataSourceSmartOS.LEGACY_USER_D = self.legacy_user_d |
863 | |
864 | def tearDown(self): |
865 | - helpers.FilesystemMockingTestCase.tearDown(self) |
866 | - if self._log_handler and self._log: |
867 | - self._log.removeHandler(self._log_handler) |
868 | - apply_patches([i for i in reversed(self.unapply)]) |
869 | + DataSourceSmartOS.LEGACY_USER_D = self.orig_lud |
870 | super(TestSmartOSDataSource, self).tearDown() |
871 | |
872 | - def _patchIn(self, root): |
873 | - self.restore() |
874 | - self.patchOS(root) |
875 | - self.patchUtils(root) |
876 | - |
877 | - def apply_patches(self, patches): |
878 | - ret = apply_patches(patches) |
879 | - self.unapply += ret |
880 | - |
881 | - def _get_ds(self, sys_cfg=None, ds_cfg=None, mockdata=None, dmi_data=None, |
882 | - is_lxbrand=False): |
883 | - mod = DataSourceSmartOS |
884 | - |
885 | - if mockdata is None: |
886 | - mockdata = MOCK_RETURNS |
887 | - |
888 | - if dmi_data is None: |
889 | - dmi_data = DMI_DATA_RETURN |
890 | - |
891 | - def _dmi_data(): |
892 | - return dmi_data |
893 | - |
894 | - def _os_uname(): |
895 | - if not is_lxbrand: |
896 | - # LP: #1243287. tests assume this runs, but running test on |
897 | - # arm would cause them all to fail. |
898 | - return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', 'x86_64') |
899 | - else: |
900 | - return ('LINUX', 'NODENAME', 'RELEASE', 'BRANDZ VIRTUAL LINUX', |
901 | - 'X86_64') |
902 | + def _get_ds(self, mockdata=None, mode=DataSourceSmartOS.SMARTOS_ENV_KVM, |
903 | + sys_cfg=None, ds_cfg=None): |
904 | + self.jmc_cfact.return_value = PsuedoJoyentClient(mockdata) |
905 | + self.get_smartos_environ.return_value = mode |
906 | |
907 | if sys_cfg is None: |
908 | sys_cfg = {} |
909 | @@ -141,44 +166,8 @@ |
910 | sys_cfg['datasource'] = sys_cfg.get('datasource', {}) |
911 | sys_cfg['datasource']['SmartOS'] = ds_cfg |
912 | |
913 | - self.apply_patches([(mod, 'LEGACY_USER_D', self.legacy_user_d)]) |
914 | - self.apply_patches([ |
915 | - (mod, 'JoyentMetadataClient', get_mock_client(mockdata))]) |
916 | - self.apply_patches([(mod, 'dmi_data', _dmi_data)]) |
917 | - self.apply_patches([(os, 'uname', _os_uname)]) |
918 | - self.apply_patches([(mod, 'device_exists', lambda d: True)]) |
919 | - dsrc = mod.DataSourceSmartOS(sys_cfg, distro=None, |
920 | - paths=self.paths) |
921 | - self.apply_patches([(dsrc, '_get_seed_file_object', mock.MagicMock())]) |
922 | - return dsrc |
923 | - |
924 | - def test_seed(self): |
925 | - # default seed should be /dev/ttyS1 |
926 | - dsrc = self._get_ds() |
927 | - ret = dsrc.get_data() |
928 | - self.assertTrue(ret) |
929 | - self.assertEqual('kvm', dsrc.smartos_type) |
930 | - self.assertEqual('/dev/ttyS1', dsrc.seed) |
931 | - |
932 | - def test_seed_lxbrand(self): |
933 | - # default seed should be /dev/ttyS1 |
934 | - dsrc = self._get_ds(is_lxbrand=True) |
935 | - ret = dsrc.get_data() |
936 | - self.assertTrue(ret) |
937 | - self.assertEqual('lx-brand', dsrc.smartos_type) |
938 | - self.assertEqual('/native/.zonecontrol/metadata.sock', dsrc.seed) |
939 | - |
940 | - def test_issmartdc(self): |
941 | - dsrc = self._get_ds() |
942 | - ret = dsrc.get_data() |
943 | - self.assertTrue(ret) |
944 | - self.assertTrue(dsrc.is_smartdc) |
945 | - |
946 | - def test_issmartdc_lxbrand(self): |
947 | - dsrc = self._get_ds(is_lxbrand=True) |
948 | - ret = dsrc.get_data() |
949 | - self.assertTrue(ret) |
950 | - self.assertTrue(dsrc.is_smartdc) |
951 | + return DataSourceSmartOS.DataSourceSmartOS( |
952 | + sys_cfg, distro=None, paths=self.paths) |
953 | |
954 | def test_no_base64(self): |
955 | ds_cfg = {'no_base64_decode': ['test_var1'], 'all_base': True} |
956 | @@ -214,58 +203,6 @@ |
957 | self.assertEqual(MOCK_RETURNS['hostname'], |
958 | dsrc.metadata['local-hostname']) |
959 | |
960 | - def test_base64_all(self): |
961 | - # metadata provided base64_all of true |
962 | - my_returns = MOCK_RETURNS.copy() |
963 | - my_returns['base64_all'] = "true" |
964 | - for k in ('hostname', 'cloud-init:user-data'): |
965 | - my_returns[k] = b64e(my_returns[k]) |
966 | - |
967 | - dsrc = self._get_ds(mockdata=my_returns) |
968 | - ret = dsrc.get_data() |
969 | - self.assertTrue(ret) |
970 | - self.assertEqual(MOCK_RETURNS['hostname'], |
971 | - dsrc.metadata['local-hostname']) |
972 | - self.assertEqual(MOCK_RETURNS['cloud-init:user-data'], |
973 | - dsrc.userdata_raw) |
974 | - self.assertEqual(MOCK_RETURNS['root_authorized_keys'], |
975 | - dsrc.metadata['public-keys']) |
976 | - self.assertEqual(MOCK_RETURNS['disable_iptables_flag'], |
977 | - dsrc.metadata['iptables_disable']) |
978 | - self.assertEqual(MOCK_RETURNS['enable_motd_sys_info'], |
979 | - dsrc.metadata['motd_sys_info']) |
980 | - |
981 | - def test_b64_userdata(self): |
982 | - my_returns = MOCK_RETURNS.copy() |
983 | - my_returns['b64-cloud-init:user-data'] = "true" |
984 | - my_returns['b64-hostname'] = "true" |
985 | - for k in ('hostname', 'cloud-init:user-data'): |
986 | - my_returns[k] = b64e(my_returns[k]) |
987 | - |
988 | - dsrc = self._get_ds(mockdata=my_returns) |
989 | - ret = dsrc.get_data() |
990 | - self.assertTrue(ret) |
991 | - self.assertEqual(MOCK_RETURNS['hostname'], |
992 | - dsrc.metadata['local-hostname']) |
993 | - self.assertEqual(MOCK_RETURNS['cloud-init:user-data'], |
994 | - dsrc.userdata_raw) |
995 | - self.assertEqual(MOCK_RETURNS['root_authorized_keys'], |
996 | - dsrc.metadata['public-keys']) |
997 | - |
998 | - def test_b64_keys(self): |
999 | - my_returns = MOCK_RETURNS.copy() |
1000 | - my_returns['base64_keys'] = 'hostname,ignored' |
1001 | - for k in ('hostname',): |
1002 | - my_returns[k] = b64e(my_returns[k]) |
1003 | - |
1004 | - dsrc = self._get_ds(mockdata=my_returns) |
1005 | - ret = dsrc.get_data() |
1006 | - self.assertTrue(ret) |
1007 | - self.assertEqual(MOCK_RETURNS['hostname'], |
1008 | - dsrc.metadata['local-hostname']) |
1009 | - self.assertEqual(MOCK_RETURNS['cloud-init:user-data'], |
1010 | - dsrc.userdata_raw) |
1011 | - |
1012 | def test_userdata(self): |
1013 | dsrc = self._get_ds(mockdata=MOCK_RETURNS) |
1014 | ret = dsrc.get_data() |
1015 | @@ -275,6 +212,13 @@ |
1016 | self.assertEqual(MOCK_RETURNS['cloud-init:user-data'], |
1017 | dsrc.userdata_raw) |
1018 | |
1019 | + def test_sdc_nics(self): |
1020 | + dsrc = self._get_ds(mockdata=MOCK_RETURNS) |
1021 | + ret = dsrc.get_data() |
1022 | + self.assertTrue(ret) |
1023 | + self.assertEqual(json.loads(MOCK_RETURNS['sdc:nics']), |
1024 | + dsrc.metadata['network-data']) |
1025 | + |
1026 | def test_sdc_scripts(self): |
1027 | dsrc = self._get_ds(mockdata=MOCK_RETURNS) |
1028 | ret = dsrc.get_data() |
1029 | @@ -430,18 +374,7 @@ |
1030 | mydscfg['disk_aliases']['FOO']) |
1031 | |
1032 | |
1033 | -def apply_patches(patches): |
1034 | - ret = [] |
1035 | - for (ref, name, replace) in patches: |
1036 | - if replace is None: |
1037 | - continue |
1038 | - orig = getattr(ref, name) |
1039 | - setattr(ref, name, replace) |
1040 | - ret.append((ref, name, orig)) |
1041 | - return ret |
1042 | - |
1043 | - |
1044 | -class TestJoyentMetadataClient(helpers.FilesystemMockingTestCase): |
1045 | +class TestJoyentMetadataClient(FilesystemMockingTestCase): |
1046 | |
1047 | def setUp(self): |
1048 | super(TestJoyentMetadataClient, self).setUp() |
1049 | @@ -481,7 +414,8 @@ |
1050 | mock.Mock(return_value=self.request_id))) |
1051 | |
1052 | def _get_client(self): |
1053 | - return DataSourceSmartOS.JoyentMetadataClient(self.serial) |
1054 | + return DataSourceSmartOS.JoyentMetadataClient( |
1055 | + fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM) |
1056 | |
1057 | def assertEndsWith(self, haystack, prefix): |
1058 | self.assertTrue(haystack.endswith(prefix), |
1059 | @@ -495,7 +429,7 @@ |
1060 | |
1061 | def test_get_metadata_writes_a_single_line(self): |
1062 | client = self._get_client() |
1063 | - client.get_metadata('some_key') |
1064 | + client.get('some_key') |
1065 | self.assertEqual(1, self.serial.write.call_count) |
1066 | written_line = self.serial.write.call_args[0][0] |
1067 | print(type(written_line)) |
1068 | @@ -505,7 +439,7 @@ |
1069 | |
1070 | def _get_written_line(self, key='some_key'): |
1071 | client = self._get_client() |
1072 | - client.get_metadata(key) |
1073 | + client.get(key) |
1074 | return self.serial.write.call_args[0][0] |
1075 | |
1076 | def test_get_metadata_writes_bytes(self): |
1077 | @@ -549,32 +483,32 @@ |
1078 | |
1079 | def test_get_metadata_reads_a_line(self): |
1080 | client = self._get_client() |
1081 | - client.get_metadata('some_key') |
1082 | + client.get('some_key') |
1083 | self.assertEqual(self.metasource_data_len, self.serial.read.call_count) |
1084 | |
1085 | def test_get_metadata_returns_valid_value(self): |
1086 | client = self._get_client() |
1087 | - value = client.get_metadata('some_key') |
1088 | + value = client.get('some_key') |
1089 | self.assertEqual(self.metadata_value, value) |
1090 | |
1091 | def test_get_metadata_throws_exception_for_incorrect_length(self): |
1092 | self.response_parts['length'] = 0 |
1093 | client = self._get_client() |
1094 | self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException, |
1095 | - client.get_metadata, 'some_key') |
1096 | + client.get, 'some_key') |
1097 | |
1098 | def test_get_metadata_throws_exception_for_incorrect_crc(self): |
1099 | self.response_parts['crc'] = 'deadbeef' |
1100 | client = self._get_client() |
1101 | self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException, |
1102 | - client.get_metadata, 'some_key') |
1103 | + client.get, 'some_key') |
1104 | |
1105 | def test_get_metadata_throws_exception_for_request_id_mismatch(self): |
1106 | self.response_parts['request_id'] = 'deadbeef' |
1107 | client = self._get_client() |
1108 | client._checksum = lambda _: self.response_parts['crc'] |
1109 | self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException, |
1110 | - client.get_metadata, 'some_key') |
1111 | + client.get, 'some_key') |
1112 | |
1113 | def test_get_metadata_returns_None_if_value_not_found(self): |
1114 | self.response_parts['payload'] = '' |
1115 | @@ -582,4 +516,24 @@ |
1116 | self.response_parts['length'] = 17 |
1117 | client = self._get_client() |
1118 | client._checksum = lambda _: self.response_parts['crc'] |
1119 | - self.assertIsNone(client.get_metadata('some_key')) |
1120 | + self.assertIsNone(client.get('some_key')) |
1121 | + |
1122 | + |
1123 | +class TestNetworkConversion(TestCase): |
1124 | + |
1125 | + def test_convert_simple(self): |
1126 | + expected = { |
1127 | + 'version': 1, |
1128 | + 'config': [ |
1129 | + {'name': 'net0', 'type': 'physical', |
1130 | + 'subnets': [{'type': 'static', 'gateway': '8.12.42.1', |
1131 | + 'netmask': '255.255.255.0', |
1132 | + 'address': '8.12.42.102/24'}], |
1133 | + 'mtu': 1500, 'mac_address': '90:b8:d0:f5:e4:f5'}, |
1134 | + {'name': 'net1', 'type': 'physical', |
1135 | + 'subnets': [{'type': 'static', 'gateway': '192.168.128.1', |
1136 | + 'netmask': '255.255.252.0', |
1137 | + 'address': '192.168.128.93/22'}], |
1138 | + 'mtu': 8500, 'mac_address': '90:b8:d0:a5:ff:cd'}]} |
1139 | + found = DataSourceSmartOS.convert_smartos_network_data(SDC_NICS) |
1140 | + self.assertEqual(expected, found) |
note, this does depend on https:/ /code.launchpad .net/~smoser/ cloud-init/ trunk.fix- networking/ in order to have the datasource sanely declare that it is 'local'. Otherwise user-data would be pulled and such at local point (without networking).