Merge lp:~rcj/cloud-init/lp1540965 into lp:~cloud-init-dev/cloud-init/trunk
- lp1540965
- Merge into trunk
Proposed by
Robert C Jennings
Status: | Merged |
---|---|
Merged at revision: | 1159 |
Proposed branch: | lp:~rcj/cloud-init/lp1540965 |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
614 lines (+221/-138) 3 files modified
cloudinit/sources/DataSourceSmartOS.py (+159/-108) doc/examples/cloud-config-datasources.txt (+7/-0) tests/unittests/test_datasource/test_smartos.py (+55/-30) |
To merge this branch: | bzr merge lp:~rcj/cloud-init/lp1540965 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser | Pending | ||
Review via email: mp+285097@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote : | # |
Revision history for this message
Robert C Jennings (rcj) wrote : | # |
I removed the test-requiremen
lp:~rcj/cloud-init/lp1540965
updated
- 1158. By Robert C Jennings
-
SmartOS: Add support for Joyent LX-Brand Zones (LP: #1540965)
LX-brand zones on Joyent's SmartOS use a different metadata source
(socket file) than the KVM-based SmartOS virtualization (serial port).
This patch adds support for recognizing the different flavors of
virtualization on SmartOS and setting up a metadata source file object.
After the file object is created, the rest of the code for the datasource
Revision history for this message
Robert C Jennings (rcj) wrote : | # |
I overwrote my prior commit but I have added comments below for the changes from the last commit.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cloudinit/sources/DataSourceSmartOS.py' |
2 | --- cloudinit/sources/DataSourceSmartOS.py 2015-03-25 17:59:42 +0000 |
3 | +++ cloudinit/sources/DataSourceSmartOS.py 2016-02-04 21:52:54 +0000 |
4 | @@ -20,10 +20,13 @@ |
5 | # Datasource for provisioning on SmartOS. This works on Joyent |
6 | # and public/private Clouds using SmartOS. |
7 | # |
8 | -# SmartOS hosts use a serial console (/dev/ttyS1) on Linux Guests. |
9 | +# SmartOS hosts use a serial console (/dev/ttyS1) on KVM Linux Guests |
10 | # The meta-data is transmitted via key/value pairs made by |
11 | # requests on the console. For example, to get the hostname, you |
12 | # would send "GET hostname" on /dev/ttyS1. |
13 | +# For Linux Guests running in LX-Brand Zones on SmartOS hosts |
14 | +# a socket (/native/.zonecontrol/metadata.sock) is used instead |
15 | +# of a serial console. |
16 | # |
17 | # Certain behavior is defined by the DataDictionary |
18 | # http://us-east.manta.joyent.com/jmc/public/mdata/datadict.html |
19 | @@ -34,6 +37,8 @@ |
20 | import os |
21 | import random |
22 | import re |
23 | +import socket |
24 | +import stat |
25 | |
26 | import serial |
27 | |
28 | @@ -46,6 +51,7 @@ |
29 | |
30 | SMARTOS_ATTRIB_MAP = { |
31 | # Cloud-init Key : (SmartOS Key, Strip line endings) |
32 | + 'instance-id': ('sdc:uuid', True), |
33 | 'local-hostname': ('hostname', True), |
34 | 'public-keys': ('root_authorized_keys', True), |
35 | 'user-script': ('user-script', False), |
36 | @@ -76,6 +82,7 @@ |
37 | # |
38 | BUILTIN_DS_CONFIG = { |
39 | 'serial_device': '/dev/ttyS1', |
40 | + 'metadata_sockfile': '/native/.zonecontrol/metadata.sock', |
41 | 'seed_timeout': 60, |
42 | 'no_base64_decode': ['root_authorized_keys', |
43 | 'motd_sys_info', |
44 | @@ -83,6 +90,7 @@ |
45 | 'user-data', |
46 | 'user-script', |
47 | 'sdc:datacenter_name', |
48 | + 'sdc:uuid', |
49 | ], |
50 | 'base64_keys': [], |
51 | 'base64_all': False, |
52 | @@ -150,17 +158,27 @@ |
53 | def __init__(self, sys_cfg, distro, paths): |
54 | sources.DataSource.__init__(self, sys_cfg, distro, paths) |
55 | self.is_smartdc = None |
56 | - |
57 | self.ds_cfg = util.mergemanydict([ |
58 | self.ds_cfg, |
59 | util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}), |
60 | BUILTIN_DS_CONFIG]) |
61 | |
62 | self.metadata = {} |
63 | - self.cfg = BUILTIN_CLOUD_CONFIG |
64 | |
65 | - self.seed = self.ds_cfg.get("serial_device") |
66 | - self.seed_timeout = self.ds_cfg.get("serial_timeout") |
67 | + # SDC LX-Brand Zones lack dmidecode (no /dev/mem) but |
68 | + # report 'BrandZ virtual linux' as the kernel version |
69 | + if os.uname()[3].lower() == 'brandz virtual linux': |
70 | + LOG.debug("Host is SmartOS, guest in Zone") |
71 | + self.is_smartdc = True |
72 | + self.smartos_type = 'lx-brand' |
73 | + self.cfg = {} |
74 | + self.seed = self.ds_cfg.get("metadata_sockfile") |
75 | + else: |
76 | + self.is_smartdc = True |
77 | + self.smartos_type = 'kvm' |
78 | + self.seed = self.ds_cfg.get("serial_device") |
79 | + self.cfg = BUILTIN_CLOUD_CONFIG |
80 | + self.seed_timeout = self.ds_cfg.get("serial_timeout") |
81 | self.smartos_no_base64 = self.ds_cfg.get('no_base64_decode') |
82 | self.b64_keys = self.ds_cfg.get('base64_keys') |
83 | self.b64_all = self.ds_cfg.get('base64_all') |
84 | @@ -170,12 +188,49 @@ |
85 | root = sources.DataSource.__str__(self) |
86 | return "%s [seed=%s]" % (root, self.seed) |
87 | |
88 | + def _get_seed_file_object(self): |
89 | + if not self.seed: |
90 | + raise AttributeError("seed device is not set") |
91 | + |
92 | + if self.smartos_type == 'lx-brand': |
93 | + if not stat.S_ISSOCK(os.stat(self.seed).st_mode): |
94 | + LOG.debug("Seed %s is not a socket", self.seed) |
95 | + return None |
96 | + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
97 | + sock.connect(self.seed) |
98 | + return sock.makefile('rwb') |
99 | + else: |
100 | + if not stat.S_ISCHR(os.stat(self.seed).st_mode): |
101 | + LOG.debug("Seed %s is not a character device") |
102 | + return None |
103 | + ser = serial.Serial(self.seed, timeout=self.seed_timeout) |
104 | + if not ser.isOpen(): |
105 | + raise SystemError("Unable to open %s" % self.seed) |
106 | + return ser |
107 | + return None |
108 | + |
109 | + def _set_provisioned(self): |
110 | + '''Mark the instance provisioning state as successful. |
111 | + |
112 | + When run in a zone, the host OS will look for /var/svc/provisioning |
113 | + to be renamed as /var/svc/provision_success. This should be done |
114 | + after meta-data is successfully retrieved and from this point |
115 | + the host considers the provision of the zone to be a success and |
116 | + keeps the zone running. |
117 | + ''' |
118 | + |
119 | + LOG.debug('Instance provisioning state set as successful') |
120 | + svc_path = '/var/svc' |
121 | + if os.path.exists('/'.join([svc_path, 'provisioning'])): |
122 | + os.rename('/'.join([svc_path, 'provisioning']), |
123 | + '/'.join([svc_path, 'provision_success'])) |
124 | + |
125 | def get_data(self): |
126 | md = {} |
127 | ud = "" |
128 | |
129 | if not device_exists(self.seed): |
130 | - LOG.debug("No serial device '%s' found for SmartOS datasource", |
131 | + LOG.debug("No metadata device '%s' found for SmartOS datasource", |
132 | self.seed) |
133 | return False |
134 | |
135 | @@ -185,29 +240,36 @@ |
136 | LOG.debug("Disabling SmartOS datasource on arm (LP: #1243287)") |
137 | return False |
138 | |
139 | - dmi_info = dmi_data() |
140 | - if dmi_info is False: |
141 | - LOG.debug("No dmidata utility found") |
142 | - return False |
143 | - |
144 | - system_uuid, system_type = tuple(dmi_info) |
145 | - if 'smartdc' not in system_type.lower(): |
146 | - LOG.debug("Host is not on SmartOS. system_type=%s", system_type) |
147 | - return False |
148 | - self.is_smartdc = True |
149 | - md['instance-id'] = system_uuid |
150 | - |
151 | - b64_keys = self.query('base64_keys', strip=True, b64=False) |
152 | - if b64_keys is not None: |
153 | - self.b64_keys = [k.strip() for k in str(b64_keys).split(',')] |
154 | - |
155 | - b64_all = self.query('base64_all', strip=True, b64=False) |
156 | - if b64_all is not None: |
157 | - self.b64_all = util.is_true(b64_all) |
158 | - |
159 | - for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items(): |
160 | - smartos_noun, strip = attribute |
161 | - md[ci_noun] = self.query(smartos_noun, strip=strip) |
162 | + # SDC KVM instances will provide dmi data, LX-brand does not |
163 | + if self.smartos_type == 'kvm': |
164 | + dmi_info = dmi_data() |
165 | + if dmi_info is False: |
166 | + LOG.debug("No dmidata utility found") |
167 | + return False |
168 | + |
169 | + system_type = dmi_info |
170 | + if 'smartdc' not in system_type.lower(): |
171 | + LOG.debug("Host is not on SmartOS. system_type=%s", |
172 | + system_type) |
173 | + return False |
174 | + LOG.debug("Host is SmartOS, guest in KVM") |
175 | + |
176 | + seed_obj = self._get_seed_file_object() |
177 | + if seed_obj is None: |
178 | + LOG.debug('Seed file object not found.') |
179 | + return False |
180 | + with contextlib.closing(seed_obj) as seed: |
181 | + b64_keys = self.query('base64_keys', seed, strip=True, b64=False) |
182 | + if b64_keys is not None: |
183 | + self.b64_keys = [k.strip() for k in str(b64_keys).split(',')] |
184 | + |
185 | + b64_all = self.query('base64_all', seed, strip=True, b64=False) |
186 | + if b64_all is not None: |
187 | + self.b64_all = util.is_true(b64_all) |
188 | + |
189 | + for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items(): |
190 | + smartos_noun, strip = attribute |
191 | + md[ci_noun] = self.query(smartos_noun, seed, strip=strip) |
192 | |
193 | # @datadictionary: This key may contain a program that is written |
194 | # to a file in the filesystem of the guest on each boot and then |
195 | @@ -240,7 +302,7 @@ |
196 | |
197 | # Handle the cloud-init regular meta |
198 | if not md['local-hostname']: |
199 | - md['local-hostname'] = system_uuid |
200 | + md['local-hostname'] = md['instance-id'] |
201 | |
202 | ud = None |
203 | if md['user-data']: |
204 | @@ -257,6 +319,8 @@ |
205 | self.metadata = util.mergemanydict([md, self.metadata]) |
206 | self.userdata_raw = ud |
207 | self.vendordata_raw = md['vendor-data'] |
208 | + |
209 | + self._set_provisioned() |
210 | return True |
211 | |
212 | def device_name_to_device(self, name): |
213 | @@ -268,16 +332,59 @@ |
214 | def get_instance_id(self): |
215 | return self.metadata['instance-id'] |
216 | |
217 | - def query(self, noun, strip=False, default=None, b64=None): |
218 | + def query(self, noun, seed_file, strip=False, default=None, b64=None): |
219 | if b64 is None: |
220 | if noun in self.smartos_no_base64: |
221 | b64 = False |
222 | elif self.b64_all or noun in self.b64_keys: |
223 | b64 = True |
224 | |
225 | - return query_data(noun=noun, strip=strip, seed_device=self.seed, |
226 | - seed_timeout=self.seed_timeout, default=default, |
227 | - b64=b64) |
228 | + return self._query_data(noun, seed_file, strip=strip, |
229 | + default=default, b64=b64) |
230 | + |
231 | + def _query_data(self, noun, seed_file, strip=False, |
232 | + default=None, b64=None): |
233 | + """Makes a request via "GET <NOUN>" |
234 | + |
235 | + In the response, the first line is the status, while subsequent |
236 | + lines are is the value. A blank line with a "." is used to |
237 | + indicate end of response. |
238 | + |
239 | + If the response is expected to be base64 encoded, then set |
240 | + b64encoded to true. Unfortantely, there is no way to know if |
241 | + something is 100% encoded, so this method relies on being told |
242 | + if the data is base64 or not. |
243 | + """ |
244 | + |
245 | + if not noun: |
246 | + return False |
247 | + |
248 | + response = JoyentMetadataClient(seed_file).get_metadata(noun) |
249 | + |
250 | + if response is None: |
251 | + return default |
252 | + |
253 | + if b64 is None: |
254 | + b64 = self._query_data('b64-%s' % noun, seed_file, b64=False, |
255 | + default=False, strip=True) |
256 | + b64 = util.is_true(b64) |
257 | + |
258 | + resp = None |
259 | + if b64 or strip: |
260 | + resp = "".join(response).rstrip() |
261 | + else: |
262 | + resp = "".join(response) |
263 | + |
264 | + if b64: |
265 | + try: |
266 | + return util.b64d(resp) |
267 | + # Bogus input produces different errors in Python 2 and 3; |
268 | + # catch both. |
269 | + except (TypeError, binascii.Error): |
270 | + LOG.warn("Failed base64 decoding key '%s'", noun) |
271 | + return resp |
272 | + |
273 | + return resp |
274 | |
275 | |
276 | def device_exists(device): |
277 | @@ -285,25 +392,6 @@ |
278 | return os.path.exists(device) |
279 | |
280 | |
281 | -def get_serial(seed_device, seed_timeout): |
282 | - """This is replaced in unit testing, allowing us to replace |
283 | - serial.Serial with a mocked class. |
284 | - |
285 | - The timeout value of 60 seconds should never be hit. The value |
286 | - is taken from SmartOS own provisioning tools. Since we are reading |
287 | - each line individually up until the single ".", the transfer is |
288 | - usually very fast (i.e. microseconds) to get the response. |
289 | - """ |
290 | - if not seed_device: |
291 | - raise AttributeError("seed_device value is not set") |
292 | - |
293 | - ser = serial.Serial(seed_device, timeout=seed_timeout) |
294 | - if not ser.isOpen(): |
295 | - raise SystemError("Unable to open %s" % seed_device) |
296 | - |
297 | - return ser |
298 | - |
299 | - |
300 | class JoyentMetadataFetchException(Exception): |
301 | pass |
302 | |
303 | @@ -320,8 +408,8 @@ |
304 | r' (?P<body>(?P<request_id>[0-9a-f]+) (?P<status>SUCCESS|NOTFOUND)' |
305 | r'( (?P<payload>.+))?)') |
306 | |
307 | - def __init__(self, serial): |
308 | - self.serial = serial |
309 | + def __init__(self, metasource): |
310 | + self.metasource = metasource |
311 | |
312 | def _checksum(self, body): |
313 | return '{0:08x}'.format( |
314 | @@ -356,67 +444,30 @@ |
315 | util.b64e(metadata_key)) |
316 | msg = 'V2 {0} {1} {2}\n'.format( |
317 | len(message_body), self._checksum(message_body), message_body) |
318 | - LOG.debug('Writing "%s" to serial port.', msg) |
319 | - self.serial.write(msg.encode('ascii')) |
320 | - response = self.serial.readline().decode('ascii') |
321 | - LOG.debug('Read "%s" from serial port.', response) |
322 | + LOG.debug('Writing "%s" to metadata transport.', msg) |
323 | + self.metasource.write(msg.encode('ascii')) |
324 | + self.metasource.flush() |
325 | + |
326 | + response = bytearray() |
327 | + response.extend(self.metasource.read(1)) |
328 | + while response[-1:] != b'\n': |
329 | + response.extend(self.metasource.read(1)) |
330 | + response = response.rstrip().decode('ascii') |
331 | + LOG.debug('Read "%s" from metadata transport.', response) |
332 | + |
333 | + if 'SUCCESS' not in response: |
334 | + return None |
335 | + |
336 | return self._get_value_from_frame(request_id, response) |
337 | |
338 | |
339 | -def query_data(noun, seed_device, seed_timeout, strip=False, default=None, |
340 | - b64=None): |
341 | - """Makes a request to via the serial console via "GET <NOUN>" |
342 | - |
343 | - In the response, the first line is the status, while subsequent lines |
344 | - are is the value. A blank line with a "." is used to indicate end of |
345 | - response. |
346 | - |
347 | - If the response is expected to be base64 encoded, then set b64encoded |
348 | - to true. Unfortantely, there is no way to know if something is 100% |
349 | - encoded, so this method relies on being told if the data is base64 or |
350 | - not. |
351 | - """ |
352 | - if not noun: |
353 | - return False |
354 | - |
355 | - with contextlib.closing(get_serial(seed_device, seed_timeout)) as ser: |
356 | - client = JoyentMetadataClient(ser) |
357 | - response = client.get_metadata(noun) |
358 | - |
359 | - if response is None: |
360 | - return default |
361 | - |
362 | - if b64 is None: |
363 | - b64 = query_data('b64-%s' % noun, seed_device=seed_device, |
364 | - seed_timeout=seed_timeout, b64=False, |
365 | - default=False, strip=True) |
366 | - b64 = util.is_true(b64) |
367 | - |
368 | - resp = None |
369 | - if b64 or strip: |
370 | - resp = "".join(response).rstrip() |
371 | - else: |
372 | - resp = "".join(response) |
373 | - |
374 | - if b64: |
375 | - try: |
376 | - return util.b64d(resp) |
377 | - # Bogus input produces different errors in Python 2 and 3; catch both. |
378 | - except (TypeError, binascii.Error): |
379 | - LOG.warn("Failed base64 decoding key '%s'", noun) |
380 | - return resp |
381 | - |
382 | - return resp |
383 | - |
384 | - |
385 | def dmi_data(): |
386 | - sys_uuid = util.read_dmi_data("system-uuid") |
387 | sys_type = util.read_dmi_data("system-product-name") |
388 | |
389 | - if not sys_uuid or not sys_type: |
390 | + if not sys_type: |
391 | return None |
392 | |
393 | - return (sys_uuid.lower(), sys_type) |
394 | + return sys_type |
395 | |
396 | |
397 | def write_boot_content(content, content_f, link=None, shebang=False, |
398 | |
399 | === modified file 'doc/examples/cloud-config-datasources.txt' |
400 | --- doc/examples/cloud-config-datasources.txt 2013-12-12 19:22:14 +0000 |
401 | +++ doc/examples/cloud-config-datasources.txt 2016-02-04 21:52:54 +0000 |
402 | @@ -51,12 +51,19 @@ |
403 | policy: on # [can be 'on', 'off' or 'force'] |
404 | |
405 | SmartOS: |
406 | + # For KVM guests: |
407 | # Smart OS datasource works over a serial console interacting with |
408 | # a server on the other end. By default, the second serial console is the |
409 | # device. SmartOS also uses a serial timeout of 60 seconds. |
410 | serial_device: /dev/ttyS1 |
411 | serial_timeout: 60 |
412 | |
413 | + # For LX-Brand Zones guests: |
414 | + # Smart OS datasource works over a socket interacting with |
415 | + # the host on the other end. By default, the socket file is in |
416 | + # the native .zoncontrol directory. |
417 | + metadata_sockfile: /native/.zonecontrol/metadata.sock |
418 | + |
419 | # a list of keys that will not be base64 decoded even if base64_all |
420 | no_base64_decode: ['root_authorized_keys', 'motd_sys_info', |
421 | 'iptables_disable'] |
422 | |
423 | === modified file 'tests/unittests/test_datasource/test_smartos.py' |
424 | --- tests/unittests/test_datasource/test_smartos.py 2015-05-01 09:38:56 +0000 |
425 | +++ tests/unittests/test_datasource/test_smartos.py 2016-02-04 21:52:54 +0000 |
426 | @@ -31,6 +31,7 @@ |
427 | import stat |
428 | import tempfile |
429 | import uuid |
430 | +import unittest |
431 | from binascii import crc32 |
432 | |
433 | import serial |
434 | @@ -56,12 +57,13 @@ |
435 | 'cloud-init:user-data': '\n'.join(['#!/bin/sh', '/bin/true', '']), |
436 | 'sdc:datacenter_name': 'somewhere2', |
437 | 'sdc:operator-script': '\n'.join(['bin/true', '']), |
438 | + 'sdc:uuid': str(uuid.uuid4()), |
439 | 'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']), |
440 | 'user-data': '\n'.join(['something', '']), |
441 | 'user-script': '\n'.join(['/bin/true', '']), |
442 | } |
443 | |
444 | -DMI_DATA_RETURN = (str(uuid.uuid4()), 'smartdc') |
445 | +DMI_DATA_RETURN = 'smartdc' |
446 | |
447 | |
448 | def get_mock_client(mockdata): |
449 | @@ -111,7 +113,8 @@ |
450 | ret = apply_patches(patches) |
451 | self.unapply += ret |
452 | |
453 | - def _get_ds(self, sys_cfg=None, ds_cfg=None, mockdata=None, dmi_data=None): |
454 | + def _get_ds(self, sys_cfg=None, ds_cfg=None, mockdata=None, dmi_data=None, |
455 | + is_lxbrand=False): |
456 | mod = DataSourceSmartOS |
457 | |
458 | if mockdata is None: |
459 | @@ -124,9 +127,13 @@ |
460 | return dmi_data |
461 | |
462 | def _os_uname(): |
463 | - # LP: #1243287. tests assume this runs, but running test on |
464 | - # arm would cause them all to fail. |
465 | - return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', 'x86_64') |
466 | + if not is_lxbrand: |
467 | + # LP: #1243287. tests assume this runs, but running test on |
468 | + # arm would cause them all to fail. |
469 | + return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', 'x86_64') |
470 | + else: |
471 | + return ('LINUX', 'NODENAME', 'RELEASE', 'BRANDZ VIRTUAL LINUX', |
472 | + 'X86_64') |
473 | |
474 | if sys_cfg is None: |
475 | sys_cfg = {} |
476 | @@ -136,7 +143,6 @@ |
477 | sys_cfg['datasource']['SmartOS'] = ds_cfg |
478 | |
479 | self.apply_patches([(mod, 'LEGACY_USER_D', self.legacy_user_d)]) |
480 | - self.apply_patches([(mod, 'get_serial', mock.MagicMock())]) |
481 | self.apply_patches([ |
482 | (mod, 'JoyentMetadataClient', get_mock_client(mockdata))]) |
483 | self.apply_patches([(mod, 'dmi_data', _dmi_data)]) |
484 | @@ -144,6 +150,7 @@ |
485 | self.apply_patches([(mod, 'device_exists', lambda d: True)]) |
486 | dsrc = mod.DataSourceSmartOS(sys_cfg, distro=None, |
487 | paths=self.paths) |
488 | + self.apply_patches([(dsrc, '_get_seed_file_object', mock.MagicMock())]) |
489 | return dsrc |
490 | |
491 | def test_seed(self): |
492 | @@ -151,14 +158,29 @@ |
493 | dsrc = self._get_ds() |
494 | ret = dsrc.get_data() |
495 | self.assertTrue(ret) |
496 | + self.assertEquals('kvm', dsrc.smartos_type) |
497 | self.assertEquals('/dev/ttyS1', dsrc.seed) |
498 | |
499 | + def test_seed_lxbrand(self): |
500 | + # default seed should be /dev/ttyS1 |
501 | + dsrc = self._get_ds(is_lxbrand=True) |
502 | + ret = dsrc.get_data() |
503 | + self.assertTrue(ret) |
504 | + self.assertEquals('lx-brand', dsrc.smartos_type) |
505 | + self.assertEquals('/native/.zonecontrol/metadata.sock', dsrc.seed) |
506 | + |
507 | def test_issmartdc(self): |
508 | dsrc = self._get_ds() |
509 | ret = dsrc.get_data() |
510 | self.assertTrue(ret) |
511 | self.assertTrue(dsrc.is_smartdc) |
512 | |
513 | + def test_issmartdc_lxbrand(self): |
514 | + dsrc = self._get_ds(is_lxbrand=True) |
515 | + ret = dsrc.get_data() |
516 | + self.assertTrue(ret) |
517 | + self.assertTrue(dsrc.is_smartdc) |
518 | + |
519 | def test_no_base64(self): |
520 | ds_cfg = {'no_base64_decode': ['test_var1'], 'all_base': True} |
521 | dsrc = self._get_ds(ds_cfg=ds_cfg) |
522 | @@ -169,7 +191,8 @@ |
523 | dsrc = self._get_ds(mockdata=MOCK_RETURNS) |
524 | ret = dsrc.get_data() |
525 | self.assertTrue(ret) |
526 | - self.assertEquals(DMI_DATA_RETURN[0], dsrc.metadata['instance-id']) |
527 | + self.assertEquals(MOCK_RETURNS['sdc:uuid'], |
528 | + dsrc.metadata['instance-id']) |
529 | |
530 | def test_root_keys(self): |
531 | dsrc = self._get_ds(mockdata=MOCK_RETURNS) |
532 | @@ -407,18 +430,6 @@ |
533 | self.assertEqual(dsrc.device_name_to_device('FOO'), |
534 | mydscfg['disk_aliases']['FOO']) |
535 | |
536 | - @mock.patch('cloudinit.sources.DataSourceSmartOS.JoyentMetadataClient') |
537 | - @mock.patch('cloudinit.sources.DataSourceSmartOS.get_serial') |
538 | - def test_serial_console_closed_on_error(self, get_serial, metadata_client): |
539 | - class OurException(Exception): |
540 | - pass |
541 | - metadata_client.side_effect = OurException |
542 | - try: |
543 | - DataSourceSmartOS.query_data('noun', 'device', 0) |
544 | - except OurException: |
545 | - pass |
546 | - self.assertEqual(1, get_serial.return_value.close.call_count) |
547 | - |
548 | |
549 | def apply_patches(patches): |
550 | ret = [] |
551 | @@ -447,14 +458,25 @@ |
552 | } |
553 | |
554 | def make_response(): |
555 | - payload = '' |
556 | - if self.response_parts['payload']: |
557 | - payload = ' {0}'.format(self.response_parts['payload']) |
558 | - del self.response_parts['payload'] |
559 | - return ( |
560 | - 'V2 {length} {crc} {request_id} {command}{payload}\n'.format( |
561 | - payload=payload, **self.response_parts).encode('ascii')) |
562 | - self.serial.readline.side_effect = make_response |
563 | + payloadstr = '' |
564 | + if 'payload' in self.response_parts: |
565 | + payloadstr = ' {0}'.format(self.response_parts['payload']) |
566 | + return ('V2 {length} {crc} {request_id} ' |
567 | + '{command}{payloadstr}\n'.format( |
568 | + payloadstr=payloadstr, |
569 | + **self.response_parts).encode('ascii')) |
570 | + |
571 | + self.metasource_data = None |
572 | + |
573 | + def read_response(length): |
574 | + if not self.metasource_data: |
575 | + self.metasource_data = make_response() |
576 | + self.metasource_data_len = len(self.metasource_data) |
577 | + resp = self.metasource_data[:length] |
578 | + self.metasource_data = self.metasource_data[length:] |
579 | + return resp |
580 | + |
581 | + self.serial.read.side_effect = read_response |
582 | self.patched_funcs.enter_context( |
583 | mock.patch('cloudinit.sources.DataSourceSmartOS.random.randint', |
584 | mock.Mock(return_value=self.request_id))) |
585 | @@ -477,7 +499,9 @@ |
586 | client.get_metadata('some_key') |
587 | self.assertEqual(1, self.serial.write.call_count) |
588 | written_line = self.serial.write.call_args[0][0] |
589 | - self.assertEndsWith(written_line, b'\n') |
590 | + print(type(written_line)) |
591 | + self.assertEndsWith(written_line.decode('ascii'), |
592 | + b'\n'.decode('ascii')) |
593 | self.assertEqual(1, written_line.count(b'\n')) |
594 | |
595 | def _get_written_line(self, key='some_key'): |
596 | @@ -489,7 +513,8 @@ |
597 | self.assertIsInstance(self._get_written_line(), six.binary_type) |
598 | |
599 | def test_get_metadata_line_starts_with_v2(self): |
600 | - self.assertStartsWith(self._get_written_line(), b'V2') |
601 | + foo = self._get_written_line() |
602 | + self.assertStartsWith(foo.decode('ascii'), b'V2'.decode('ascii')) |
603 | |
604 | def test_get_metadata_uses_get_command(self): |
605 | parts = self._get_written_line().decode('ascii').strip().split(' ') |
606 | @@ -526,7 +551,7 @@ |
607 | def test_get_metadata_reads_a_line(self): |
608 | client = self._get_client() |
609 | client.get_metadata('some_key') |
610 | - self.assertEqual(1, self.serial.readline.call_count) |
611 | + self.assertEqual(self.metasource_data_len, self.serial.read.call_count) |
612 | |
613 | def test_get_metadata_returns_valid_value(self): |
614 | client = self._get_client() |
please see comments in line. ts.txt change unless you have justification for it that i did not understand.
looks fine, but i dont want the test-requiremen