Merge lp:~rcj/cloud-init/lp1540965 into lp:~cloud-init-dev/cloud-init/trunk
- lp1540965
- Merge into trunk
Proposed by
Robert C Jennings
on 2016-02-04
| 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 | 2016-02-04 | Pending | |
|
Review via email:
|
|||
Commit Message
Description of the Change
To post a comment you must log in.
| Scott Moser (smoser) wrote : | # |
| Robert C Jennings (rcj) wrote : | # |
I removed the test-requiremen
lp:~rcj/cloud-init/lp1540965
updated
on 2016-02-04
- 1158. By Robert C Jennings on 2016-02-04
-
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
| 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