Merge lp:~smoser/cloud-init/trunk.joyent-cleanup into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Scott Moser
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
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 JoyentMetadataLegacySerialClient.

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.

To post a comment you must log in.
1230. By Scott Moser

assertEquals

1231. By Scott Moser

return dict not None on get_config_obj

Revision history for this message
Scott Moser (smoser) wrote :

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

1232. By Scott Moser

assertEqual

1233. By Scott Moser

use constants for kvm and lx-brand

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cloudinit/sources/DataSourceSmartOS.py'
--- cloudinit/sources/DataSourceSmartOS.py 2016-04-12 16:57:50 +0000
+++ cloudinit/sources/DataSourceSmartOS.py 2016-05-31 17:14:35 +0000
@@ -32,13 +32,13 @@
32# http://us-east.manta.joyent.com/jmc/public/mdata/datadict.html32# http://us-east.manta.joyent.com/jmc/public/mdata/datadict.html
33# Comments with "@datadictionary" are snippets of the definition33# Comments with "@datadictionary" are snippets of the definition
3434
35import base64
35import binascii36import binascii
36import contextlib37import json
37import os38import os
38import random39import random
39import re40import re
40import socket41import socket
41import stat
4242
43import serial43import serial
4444
@@ -64,14 +64,36 @@
64 'operator-script': ('sdc:operator-script', False),64 'operator-script': ('sdc:operator-script', False),
65}65}
6666
67SMARTOS_ATTRIB_JSON = {
68 # Cloud-init Key : (SmartOS Key known JSON)
69 'network-data': 'sdc:nics',
70}
71
72SMARTOS_ENV_LX_BRAND = "lx-brand"
73SMARTOS_ENV_KVM = "kvm"
74
67DS_NAME = 'SmartOS'75DS_NAME = 'SmartOS'
68DS_CFG_PATH = ['datasource', DS_NAME]76DS_CFG_PATH = ['datasource', DS_NAME]
77NO_BASE64_DECODE = [
78 'iptables_disable',
79 'motd_sys_info',
80 'root_authorized_keys',
81 'sdc:datacenter_name',
82 'sdc:uuid'
83 'user-data',
84 'user-script',
85]
86
87METADATA_SOCKFILE = '/native/.zonecontrol/metadata.sock'
88SERIAL_DEVICE = '/dev/ttyS1'
89SERIAL_TIMEOUT = 60
90
69# BUILT-IN DATASOURCE CONFIGURATION91# BUILT-IN DATASOURCE CONFIGURATION
70# The following is the built-in configuration. If the values92# The following is the built-in configuration. If the values
71# are not set via the system configuration, then these default93# are not set via the system configuration, then these default
72# will be used:94# will be used:
73# serial_device: which serial device to use for the meta-data95# serial_device: which serial device to use for the meta-data
74# seed_timeout: how long to wait on the device96# serial_timeout: how long to wait on the device
75# no_base64_decode: values which are not base64 encoded and97# no_base64_decode: values which are not base64 encoded and
76# are fetched directly from SmartOS, not meta-data values98# are fetched directly from SmartOS, not meta-data values
77# base64_keys: meta-data keys that are delivered in base6499# base64_keys: meta-data keys that are delivered in base64
@@ -81,16 +103,10 @@
81# fs_setup: describes how to format the ephemeral drive103# fs_setup: describes how to format the ephemeral drive
82#104#
83BUILTIN_DS_CONFIG = {105BUILTIN_DS_CONFIG = {
84 'serial_device': '/dev/ttyS1',106 'serial_device': SERIAL_DEVICE,
85 'metadata_sockfile': '/native/.zonecontrol/metadata.sock',107 'serial_timeout': SERIAL_TIMEOUT,
86 'seed_timeout': 60,108 'metadata_sockfile': METADATA_SOCKFILE,
87 'no_base64_decode': ['root_authorized_keys',109 'no_base64_decode': NO_BASE64_DECODE,
88 'motd_sys_info',
89 'iptables_disable',
90 'user-data',
91 'user-script',
92 'sdc:datacenter_name',
93 'sdc:uuid'],
94 'base64_keys': [],110 'base64_keys': [],
95 'base64_all': False,111 'base64_all': False,
96 'disk_aliases': {'ephemeral0': '/dev/vdb'},112 'disk_aliases': {'ephemeral0': '/dev/vdb'},
@@ -154,59 +170,40 @@
154170
155171
156class DataSourceSmartOS(sources.DataSource):172class DataSourceSmartOS(sources.DataSource):
173 _unset = "_unset"
174 smartos_environ = _unset
175 md_client = _unset
176
157 def __init__(self, sys_cfg, distro, paths):177 def __init__(self, sys_cfg, distro, paths):
158 sources.DataSource.__init__(self, sys_cfg, distro, paths)178 sources.DataSource.__init__(self, sys_cfg, distro, paths)
159 self.is_smartdc = None
160 self.ds_cfg = util.mergemanydict([179 self.ds_cfg = util.mergemanydict([
161 self.ds_cfg,180 self.ds_cfg,
162 util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),181 util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),
163 BUILTIN_DS_CONFIG])182 BUILTIN_DS_CONFIG])
164183
165 self.metadata = {}184 self.metadata = {}
185 self.network_data = None
186 self._network_config = None
166187
167 # SDC LX-Brand Zones lack dmidecode (no /dev/mem) but
168 # report 'BrandZ virtual linux' as the kernel version
169 if os.uname()[3].lower() == 'brandz virtual linux':
170 LOG.debug("Host is SmartOS, guest in Zone")
171 self.is_smartdc = True
172 self.smartos_type = 'lx-brand'
173 self.cfg = {}
174 self.seed = self.ds_cfg.get("metadata_sockfile")
175 else:
176 self.is_smartdc = True
177 self.smartos_type = 'kvm'
178 self.seed = self.ds_cfg.get("serial_device")
179 self.cfg = BUILTIN_CLOUD_CONFIG
180 self.seed_timeout = self.ds_cfg.get("serial_timeout")
181 self.smartos_no_base64 = self.ds_cfg.get('no_base64_decode')
182 self.b64_keys = self.ds_cfg.get('base64_keys')
183 self.b64_all = self.ds_cfg.get('base64_all')
184 self.script_base_d = os.path.join(self.paths.get_cpath("scripts"))188 self.script_base_d = os.path.join(self.paths.get_cpath("scripts"))
189 self.smartos_type = None
190
191 self._init()
185192
186 def __str__(self):193 def __str__(self):
187 root = sources.DataSource.__str__(self)194 root = sources.DataSource.__str__(self)
188 return "%s [seed=%s]" % (root, self.seed)195 return "%s [client=%s]" % (root, self.md_client)
189196
190 def _get_seed_file_object(self):197 def _init(self):
191 if not self.seed:198 if self.smartos_environ == self._unset:
192 raise AttributeError("seed device is not set")199 self.smartos_type = get_smartos_environ()
193200
194 if self.smartos_type == 'lx-brand':201 if self.md_client == self._unset:
195 if not stat.S_ISSOCK(os.stat(self.seed).st_mode):202 self.md_client = jmc_client_factory(
196 LOG.debug("Seed %s is not a socket", self.seed)203 smartos_type=self.smartos_type,
197 return None204 metadata_sockfile=self.ds_cfg['metadata_sockfile'],
198 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)205 serial_device=self.ds_cfg['serial_device'],
199 sock.connect(self.seed)206 serial_timeout=self.ds_cfg['serial_timeout'])
200 return sock.makefile('rwb')
201 else:
202 if not stat.S_ISCHR(os.stat(self.seed).st_mode):
203 LOG.debug("Seed %s is not a character device")
204 return None
205 ser = serial.Serial(self.seed, timeout=self.seed_timeout)
206 if not ser.isOpen():
207 raise SystemError("Unable to open %s" % self.seed)
208 return ser
209 return None
210207
211 def _set_provisioned(self):208 def _set_provisioned(self):
212 '''Mark the instance provisioning state as successful.209 '''Mark the instance provisioning state as successful.
@@ -225,50 +222,26 @@
225 '/'.join([svc_path, 'provision_success']))222 '/'.join([svc_path, 'provision_success']))
226223
227 def get_data(self):224 def get_data(self):
225 self._init()
226
228 md = {}227 md = {}
229 ud = ""228 ud = ""
230229
231 if not device_exists(self.seed):230 if not self.smartos_type:
232 LOG.debug("No metadata device '%s' found for SmartOS datasource",231 LOG.debug("Not running on smartos")
233 self.seed)232 return False
234 return False233
235234 if not self.md_client.exists():
236 uname_arch = os.uname()[4]235 LOG.debug("No metadata device '%r' found for SmartOS datasource",
237 if uname_arch.startswith("arm") or uname_arch == "aarch64":236 self.md_client)
238 # Disabling because dmidcode in dmi_data() crashes kvm process237 return False
239 LOG.debug("Disabling SmartOS datasource on arm (LP: #1243287)")238
240 return False239 for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items():
241240 smartos_noun, strip = attribute
242 # SDC KVM instances will provide dmi data, LX-brand does not241 md[ci_noun] = self.md_client.get(smartos_noun, strip=strip)
243 if self.smartos_type == 'kvm':242
244 dmi_info = dmi_data()243 for ci_noun, smartos_noun in SMARTOS_ATTRIB_JSON.items():
245 if dmi_info is None:244 md[ci_noun] = self.md_client.get_json(smartos_noun)
246 LOG.debug("No dmidata utility found")
247 return False
248
249 system_type = dmi_info
250 if 'smartdc' not in system_type.lower():
251 LOG.debug("Host is not on SmartOS. system_type=%s",
252 system_type)
253 return False
254 LOG.debug("Host is SmartOS, guest in KVM")
255
256 seed_obj = self._get_seed_file_object()
257 if seed_obj is None:
258 LOG.debug('Seed file object not found.')
259 return False
260 with contextlib.closing(seed_obj) as seed:
261 b64_keys = self.query('base64_keys', seed, strip=True, b64=False)
262 if b64_keys is not None:
263 self.b64_keys = [k.strip() for k in str(b64_keys).split(',')]
264
265 b64_all = self.query('base64_all', seed, strip=True, b64=False)
266 if b64_all is not None:
267 self.b64_all = util.is_true(b64_all)
268
269 for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items():
270 smartos_noun, strip = attribute
271 md[ci_noun] = self.query(smartos_noun, seed, strip=strip)
272245
273 # @datadictionary: This key may contain a program that is written246 # @datadictionary: This key may contain a program that is written
274 # to a file in the filesystem of the guest on each boot and then247 # to a file in the filesystem of the guest on each boot and then
@@ -318,6 +291,7 @@
318 self.metadata = util.mergemanydict([md, self.metadata])291 self.metadata = util.mergemanydict([md, self.metadata])
319 self.userdata_raw = ud292 self.userdata_raw = ud
320 self.vendordata_raw = md['vendor-data']293 self.vendordata_raw = md['vendor-data']
294 self.network_data = md['network-data']
321295
322 self._set_provisioned()296 self._set_provisioned()
323 return True297 return True
@@ -326,69 +300,20 @@
326 return self.ds_cfg['disk_aliases'].get(name)300 return self.ds_cfg['disk_aliases'].get(name)
327301
328 def get_config_obj(self):302 def get_config_obj(self):
329 return self.cfg303 if self.smartos_type == SMARTOS_ENV_KVM:
304 return BUILTIN_CLOUD_CONFIG
305 return {}
330306
331 def get_instance_id(self):307 def get_instance_id(self):
332 return self.metadata['instance-id']308 return self.metadata['instance-id']
333309
334 def query(self, noun, seed_file, strip=False, default=None, b64=None):310 @property
335 if b64 is None:311 def network_config(self):
336 if noun in self.smartos_no_base64:312 if self._network_config is None:
337 b64 = False313 if self.network_data is not None:
338 elif self.b64_all or noun in self.b64_keys:314 self._network_config = (
339 b64 = True315 convert_smartos_network_data(self.network_data))
340316 return self._network_config
341 return self._query_data(noun, seed_file, strip=strip,
342 default=default, b64=b64)
343
344 def _query_data(self, noun, seed_file, strip=False,
345 default=None, b64=None):
346 """Makes a request via "GET <NOUN>"
347
348 In the response, the first line is the status, while subsequent
349 lines are is the value. A blank line with a "." is used to
350 indicate end of response.
351
352 If the response is expected to be base64 encoded, then set
353 b64encoded to true. Unfortantely, there is no way to know if
354 something is 100% encoded, so this method relies on being told
355 if the data is base64 or not.
356 """
357
358 if not noun:
359 return False
360
361 response = JoyentMetadataClient(seed_file).get_metadata(noun)
362
363 if response is None:
364 return default
365
366 if b64 is None:
367 b64 = self._query_data('b64-%s' % noun, seed_file, b64=False,
368 default=False, strip=True)
369 b64 = util.is_true(b64)
370
371 resp = None
372 if b64 or strip:
373 resp = "".join(response).rstrip()
374 else:
375 resp = "".join(response)
376
377 if b64:
378 try:
379 return util.b64d(resp)
380 # Bogus input produces different errors in Python 2 and 3;
381 # catch both.
382 except (TypeError, binascii.Error):
383 LOG.warn("Failed base64 decoding key '%s'", noun)
384 return resp
385
386 return resp
387
388
389def device_exists(device):
390 """Symplistic method to determine if the device exists or not"""
391 return os.path.exists(device)
392317
393318
394class JoyentMetadataFetchException(Exception):319class JoyentMetadataFetchException(Exception):
@@ -407,8 +332,11 @@
407 r' (?P<body>(?P<request_id>[0-9a-f]+) (?P<status>SUCCESS|NOTFOUND)'332 r' (?P<body>(?P<request_id>[0-9a-f]+) (?P<status>SUCCESS|NOTFOUND)'
408 r'( (?P<payload>.+))?)')333 r'( (?P<payload>.+))?)')
409334
410 def __init__(self, metasource):335 def __init__(self, smartos_type=None, fp=None):
411 self.metasource = metasource336 if smartos_type is None:
337 smartos_type = get_smartos_environ()
338 self.smartos_type = smartos_type
339 self.fp = fp
412340
413 def _checksum(self, body):341 def _checksum(self, body):
414 return '{0:08x}'.format(342 return '{0:08x}'.format(
@@ -436,37 +364,227 @@
436 LOG.debug('Value "%s" found.', value)364 LOG.debug('Value "%s" found.', value)
437 return value365 return value
438366
439 def get_metadata(self, metadata_key):367 def request(self, rtype, param=None):
440 LOG.debug('Fetching metadata key "%s"...', metadata_key)
441 request_id = '{0:08x}'.format(random.randint(0, 0xffffffff))368 request_id = '{0:08x}'.format(random.randint(0, 0xffffffff))
442 message_body = '{0} GET {1}'.format(request_id,369 message_body = ' '.join((request_id, rtype,))
443 util.b64e(metadata_key))370 if param:
371 message_body += ' ' + base64.b64encode(param.encode()).decode()
444 msg = 'V2 {0} {1} {2}\n'.format(372 msg = 'V2 {0} {1} {2}\n'.format(
445 len(message_body), self._checksum(message_body), message_body)373 len(message_body), self._checksum(message_body), message_body)
446 LOG.debug('Writing "%s" to metadata transport.', msg)374 LOG.debug('Writing "%s" to metadata transport.', msg)
447 self.metasource.write(msg.encode('ascii'))375
448 self.metasource.flush()376 need_close = False
377 if not self.fp:
378 self.open_transport()
379 need_close = True
380
381 self.fp.write(msg.encode('ascii'))
382 self.fp.flush()
449383
450 response = bytearray()384 response = bytearray()
451 response.extend(self.metasource.read(1))385 response.extend(self.fp.read(1))
452 while response[-1:] != b'\n':386 while response[-1:] != b'\n':
453 response.extend(self.metasource.read(1))387 response.extend(self.fp.read(1))
388
389 if need_close:
390 self.close_transport()
391
454 response = response.rstrip().decode('ascii')392 response = response.rstrip().decode('ascii')
455 LOG.debug('Read "%s" from metadata transport.', response)393 LOG.debug('Read "%s" from metadata transport.', response)
456394
457 if 'SUCCESS' not in response:395 if 'SUCCESS' not in response:
458 return None396 return None
459397
460 return self._get_value_from_frame(request_id, response)398 value = self._get_value_from_frame(request_id, response)
461399 return value
462400
463def dmi_data():401 def get(self, key, default=None, strip=False):
464 sys_type = util.read_dmi_data("system-product-name")402 result = self.request(rtype='GET', param=key)
465403 if result is None:
466 if not sys_type:404 return default
467 return None405 if result and strip:
468406 result = result.strip()
469 return sys_type407 return result
408
409 def get_json(self, key, default=None):
410 result = self.get(key, default=default)
411 if result is None:
412 return default
413 return json.loads(result)
414
415 def list(self):
416 result = self.request(rtype='KEYS')
417 if result:
418 result = result.split('\n')
419 return result
420
421 def put(self, key, val):
422 param = b' '.join([base64.b64encode(i.encode())
423 for i in (key, val)]).decode()
424 return self.request(rtype='PUT', param=param)
425
426 def delete(self, key):
427 return self.request(rtype='DELETE', param=key)
428
429 def close_transport(self):
430 if self.fp:
431 self.fp.close()
432 self.fp = None
433
434 def __enter__(self):
435 if self.fp:
436 return self
437 self.open_transport()
438 return self
439
440 def __exit__(self, exc_type, exc_value, traceback):
441 self.close_transport()
442 return
443
444 def open_transport(self):
445 raise NotImplementedError
446
447
448class JoyentMetadataSocketClient(JoyentMetadataClient):
449 def __init__(self, socketpath):
450 self.socketpath = socketpath
451
452 def open_transport(self):
453 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
454 sock.connect(self.socketpath)
455 self.fp = sock.makefile('rwb')
456
457 def exists(self):
458 return os.path.exists(self.socketpath)
459
460 def __repr__(self):
461 return "%s(socketpath=%s)" % (self.__class__.__name__, self.socketpath)
462
463
464class JoyentMetadataSerialClient(JoyentMetadataClient):
465 def __init__(self, device, timeout=10, smartos_type=None):
466 super(JoyentMetadataSerialClient, self).__init__(smartos_type)
467 self.device = device
468 self.timeout = timeout
469
470 def exists(self):
471 return os.path.exists(self.device)
472
473 def open_transport(self):
474 ser = serial.Serial(self.device, timeout=self.timeout)
475 if not ser.isOpen():
476 raise SystemError("Unable to open %s" % self.device)
477 self.fp = ser
478
479 def __repr__(self):
480 return "%s(device=%s, timeout=%s)" % (
481 self.__class__.__name__, self.device, self.timeout)
482
483
484class JoyentMetadataLegacySerialClient(JoyentMetadataSerialClient):
485 """V1 of the protocol was not safe for all values.
486 Thus, we allowed the user to pass values in as base64 encoded.
487 Users may still reasonably expect to be able to send base64 data
488 and have it transparently decoded. So even though the V2 format is
489 now used, and is safe (using base64 itself), we keep legacy support.
490
491 The way for a user to do this was:
492 a.) specify 'base64_keys' key whose value is a comma delimited
493 list of keys that were base64 encoded.
494 b.) base64_all: string interpreted as a boolean that indicates
495 if all keys are base64 encoded.
496 c.) set a key named b64-<keyname> with a boolean indicating that
497 <keyname> is base64 encoded."""
498
499 def __init__(self, device, timeout=10, smartos_type=None):
500 s = super(JoyentMetadataLegacySerialClient, self)
501 s.__init__(device, timeout, smartos_type)
502 self.base64_keys = None
503 self.base64_all = None
504
505 def _init_base64_keys(self, reset=False):
506 if reset:
507 self.base64_keys = None
508 self.base64_all = None
509
510 keys = None
511 if self.base64_all is None:
512 keys = self.list()
513 if 'base64_all' in keys:
514 self.base64_all = util.is_true(self._get("base64_all"))
515 else:
516 self.base64_all = False
517
518 if self.base64_all:
519 # short circuit if base64_all is true
520 return
521
522 if self.base64_keys is None:
523 if keys is None:
524 keys = self.list()
525 b64_keys = set()
526 if 'base64_keys' in keys:
527 b64_keys = set(self._get("base64_keys").split(","))
528
529 # now add any b64-<keyname> that has a true value
530 for key in [k[3:] for k in keys if k.startswith("b64-")]:
531 if util.is_true(self._get(key)):
532 b64_keys.add(key)
533 else:
534 if key in b64_keys:
535 b64_keys.remove(key)
536
537 self.base64_keys = b64_keys
538
539 def _get(self, key, default=None, strip=False):
540 return (super(JoyentMetadataLegacySerialClient, self).
541 get(key, default=default, strip=strip))
542
543 def is_b64_encoded(self, key, reset=False):
544 if key in NO_BASE64_DECODE:
545 return False
546
547 self._init_base64_keys(reset=reset)
548 if self.base64_all:
549 return True
550
551 return key in self.base64_keys
552
553 def get(self, key, default=None, strip=False):
554 mdefault = object()
555 val = self._get(key, strip=False, default=mdefault)
556 if val is mdefault:
557 return default
558
559 if self.is_b64_encoded(key):
560 try:
561 val = base64.b64decode(val.encode()).decode()
562 # Bogus input produces different errors in Python 2 and 3
563 except (TypeError, binascii.Error):
564 LOG.warn("Failed base64 decoding key '%s': %s", key, val)
565
566 if strip:
567 val = val.strip()
568
569 return val
570
571
572def jmc_client_factory(
573 smartos_type=None, metadata_sockfile=METADATA_SOCKFILE,
574 serial_device=SERIAL_DEVICE, serial_timeout=SERIAL_TIMEOUT,
575 uname_version=None):
576
577 if smartos_type is None:
578 smartos_type = get_smartos_environ(uname_version)
579
580 if smartos_type == SMARTOS_ENV_KVM:
581 return JoyentMetadataLegacySerialClient(
582 device=serial_device, timeout=serial_timeout,
583 smartos_type=smartos_type)
584 elif smartos_type == SMARTOS_ENV_LX_BRAND:
585 return JoyentMetadataSocketClient(socketpath=metadata_sockfile)
586
587 raise ValueError("Unknown value for smartos_type: %s" % smartos_type)
470588
471589
472def write_boot_content(content, content_f, link=None, shebang=False,590def write_boot_content(content, content_f, link=None, shebang=False,
@@ -522,15 +640,138 @@
522 util.ensure_dir(os.path.dirname(link))640 util.ensure_dir(os.path.dirname(link))
523 os.symlink(content_f, link)641 os.symlink(content_f, link)
524 except IOError as e:642 except IOError as e:
525 util.logexc(LOG, "failed establishing content link", e)643 util.logexc(LOG, "failed establishing content link: %s", e)
644
645
646def get_smartos_environ(uname_version=None, product_name=None,
647 uname_arch=None):
648 uname = os.uname()
649 if uname_arch is None:
650 uname_arch = uname[4]
651
652 if uname_arch.startswith("arm") or uname_arch == "aarch64":
653 return None
654
655 # SDC LX-Brand Zones lack dmidecode (no /dev/mem) but
656 # report 'BrandZ virtual linux' as the kernel version
657 if uname_version is None:
658 uname_version = uname[3]
659 if uname_version.lower() == 'brandz virtual linux':
660 return SMARTOS_ENV_LX_BRAND
661
662 if product_name is None:
663 system_type = util.read_dmi_data("system-product-name")
664 else:
665 system_type = product_name
666
667 if system_type and 'smartdc' in system_type.lower():
668 return SMARTOS_ENV_KVM
669
670 return None
671
672
673# Covert SMARTOS 'sdc:nics' data to network_config yaml
674def convert_smartos_network_data(network_data=None):
675 """Return a dictionary of network_config by parsing provided
676 SMARTOS sdc:nics configuration data
677
678 sdc:nics data is a dictionary of properties of a nic and the ip
679 configuration desired. Additional nic dictionaries are appended
680 to the list.
681
682 Converting the format is straightforward though it does include
683 duplicate information as well as data which appears to be relevant
684 to the hostOS rather than the guest.
685
686 For each entry in the nics list returned from query sdc:nics, we
687 create a type: physical entry, and extract the interface properties:
688 'mac' -> 'mac_address', 'mtu', 'interface' -> 'name'. The remaining
689 keys are related to ip configuration. For each ip in the 'ips' list
690 we create a subnet entry under 'subnets' pairing the ip to a one in
691 the 'gateways' list.
692 """
693
694 valid_keys = {
695 'physical': [
696 'mac_address',
697 'mtu',
698 'name',
699 'params',
700 'subnets',
701 'type',
702 ],
703 'subnet': [
704 'address',
705 'broadcast',
706 'dns_nameservers',
707 'dns_search',
708 'gateway',
709 'metric',
710 'netmask',
711 'pointopoint',
712 'routes',
713 'scope',
714 'type',
715 ],
716 }
717
718 config = []
719 for nic in network_data:
720 cfg = {k: v for k, v in nic.items()
721 if k in valid_keys['physical']}
722 cfg.update({
723 'type': 'physical',
724 'name': nic['interface']})
725 if 'mac' in nic:
726 cfg.update({'mac_address': nic['mac']})
727
728 subnets = []
729 for ip, gw in zip(nic['ips'], nic['gateways']):
730 subnet = {k: v for k, v in nic.items()
731 if k in valid_keys['subnet']}
732 subnet.update({
733 'type': 'static',
734 'address': ip,
735 'gateway': gw,
736 })
737 subnets.append(subnet)
738 cfg.update({'subnets': subnets})
739 config.append(cfg)
740
741 return {'version': 1, 'config': config}
526742
527743
528# Used to match classes to dependencies744# Used to match classes to dependencies
529datasources = [745datasources = [
530 (DataSourceSmartOS, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),746 (DataSourceSmartOS, (sources.DEP_FILESYSTEM, )),
531]747]
532748
533749
534# Return a list of data sources that match this set of dependencies750# Return a list of data sources that match this set of dependencies
535def get_datasource_list(depends):751def get_datasource_list(depends):
536 return sources.list_from_depends(depends, datasources)752 return sources.list_from_depends(depends, datasources)
753
754
755if __name__ == "__main__":
756 import sys
757 jmc = jmc_client_factory()
758 if len(sys.argv) == 1:
759 keys = (list(SMARTOS_ATTRIB_JSON.keys()) +
760 list(SMARTOS_ATTRIB_MAP.keys()))
761 else:
762 keys = sys.argv[1:]
763
764 data = {}
765 for key in keys:
766 if key in SMARTOS_ATTRIB_JSON:
767 keyname = SMARTOS_ATTRIB_JSON[key]
768 data[key] = jmc.get_json(keyname)
769 else:
770 if key in SMARTOS_ATTRIB_MAP:
771 keyname, strip = SMARTOS_ATTRIB_MAP[key]
772 else:
773 keyname, strip = (key, False)
774 val = jmc.get(keyname, strip=strip)
775 data[key] = jmc.get(keyname, strip=strip)
776
777 print(json.dumps(data, indent=1))
537778
=== modified file 'tests/unittests/test_datasource/test_smartos.py'
--- tests/unittests/test_datasource/test_smartos.py 2016-05-12 20:43:11 +0000
+++ tests/unittests/test_datasource/test_smartos.py 2016-05-31 17:14:35 +0000
@@ -25,6 +25,7 @@
25from __future__ import print_function25from __future__ import print_function
2626
27from binascii import crc3227from binascii import crc32
28import json
28import os29import os
29import os.path30import os.path
30import re31import re
@@ -40,12 +41,49 @@
40from cloudinit.sources import DataSourceSmartOS41from cloudinit.sources import DataSourceSmartOS
41from cloudinit.util import b64e42from cloudinit.util import b64e
4243
43from .. import helpers44from ..helpers import mock, FilesystemMockingTestCase, TestCase
4445
45try:46SDC_NICS = json.loads("""
46 from unittest import mock47[
47except ImportError:48 {
48 import mock49 "nic_tag": "external",
50 "primary": true,
51 "mtu": 1500,
52 "model": "virtio",
53 "gateway": "8.12.42.1",
54 "netmask": "255.255.255.0",
55 "ip": "8.12.42.102",
56 "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
57 "gateways": [
58 "8.12.42.1"
59 ],
60 "vlan_id": 324,
61 "mac": "90:b8:d0:f5:e4:f5",
62 "interface": "net0",
63 "ips": [
64 "8.12.42.102/24"
65 ]
66 },
67 {
68 "nic_tag": "sdc_overlay/16187209",
69 "gateway": "192.168.128.1",
70 "model": "virtio",
71 "mac": "90:b8:d0:a5:ff:cd",
72 "netmask": "255.255.252.0",
73 "ip": "192.168.128.93",
74 "network_uuid": "4cad71da-09bc-452b-986d-03562a03a0a9",
75 "gateways": [
76 "192.168.128.1"
77 ],
78 "vlan_id": 2,
79 "mtu": 8500,
80 "interface": "net1",
81 "ips": [
82 "192.168.128.93/22"
83 ]
84 }
85]
86""")
4987
50MOCK_RETURNS = {88MOCK_RETURNS = {
51 'hostname': 'test-host',89 'hostname': 'test-host',
@@ -60,79 +98,66 @@
60 'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']),98 'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']),
61 'user-data': '\n'.join(['something', '']),99 'user-data': '\n'.join(['something', '']),
62 'user-script': '\n'.join(['/bin/true', '']),100 'user-script': '\n'.join(['/bin/true', '']),
101 'sdc:nics': json.dumps(SDC_NICS),
63}102}
64103
65DMI_DATA_RETURN = 'smartdc'104DMI_DATA_RETURN = 'smartdc'
66105
67106
68def get_mock_client(mockdata):107class PsuedoJoyentClient(object):
69 class MockMetadataClient(object):108 def __init__(self, data=None):
70109 if data is None:
71 def __init__(self, serial):110 data = MOCK_RETURNS.copy()
72 pass111 self.data = data
73112 return
74 def get_metadata(self, metadata_key):113
75 return mockdata.get(metadata_key)114 def get(self, key, default=None, strip=False):
76 return MockMetadataClient115 if key in self.data:
77116 r = self.data[key]
78117 if strip:
79class TestSmartOSDataSource(helpers.FilesystemMockingTestCase):118 r = r.strip()
119 else:
120 r = default
121 return r
122
123 def get_json(self, key, default=None):
124 result = self.get(key, default=default)
125 if result is None:
126 return default
127 return json.loads(result)
128
129 def exists(self):
130 return True
131
132
133class TestSmartOSDataSource(FilesystemMockingTestCase):
80 def setUp(self):134 def setUp(self):
81 super(TestSmartOSDataSource, self).setUp()135 super(TestSmartOSDataSource, self).setUp()
82136
137 dsmos = 'cloudinit.sources.DataSourceSmartOS'
138 patcher = mock.patch(dsmos + ".jmc_client_factory")
139 self.jmc_cfact = patcher.start()
140 self.addCleanup(patcher.stop)
141 patcher = mock.patch(dsmos + ".get_smartos_environ")
142 self.get_smartos_environ = patcher.start()
143 self.addCleanup(patcher.stop)
144
83 self.tmp = tempfile.mkdtemp()145 self.tmp = tempfile.mkdtemp()
84 self.addCleanup(shutil.rmtree, self.tmp)146 self.addCleanup(shutil.rmtree, self.tmp)
147 self.paths = c_helpers.Paths({'cloud_dir': self.tmp})
148
85 self.legacy_user_d = tempfile.mkdtemp()149 self.legacy_user_d = tempfile.mkdtemp()
86 self.addCleanup(shutil.rmtree, self.legacy_user_d)150 self.orig_lud = DataSourceSmartOS.LEGACY_USER_D
87151 DataSourceSmartOS.LEGACY_USER_D = self.legacy_user_d
88 # If you should want to watch the logs...
89 self._log = None
90 self._log_file = None
91 self._log_handler = None
92
93 # patch cloud_dir, so our 'seed_dir' is guaranteed empty
94 self.paths = c_helpers.Paths({'cloud_dir': self.tmp})
95
96 self.unapply = []
97 super(TestSmartOSDataSource, self).setUp()
98152
99 def tearDown(self):153 def tearDown(self):
100 helpers.FilesystemMockingTestCase.tearDown(self)154 DataSourceSmartOS.LEGACY_USER_D = self.orig_lud
101 if self._log_handler and self._log:
102 self._log.removeHandler(self._log_handler)
103 apply_patches([i for i in reversed(self.unapply)])
104 super(TestSmartOSDataSource, self).tearDown()155 super(TestSmartOSDataSource, self).tearDown()
105156
106 def _patchIn(self, root):157 def _get_ds(self, mockdata=None, mode=DataSourceSmartOS.SMARTOS_ENV_KVM,
107 self.restore()158 sys_cfg=None, ds_cfg=None):
108 self.patchOS(root)159 self.jmc_cfact.return_value = PsuedoJoyentClient(mockdata)
109 self.patchUtils(root)160 self.get_smartos_environ.return_value = mode
110
111 def apply_patches(self, patches):
112 ret = apply_patches(patches)
113 self.unapply += ret
114
115 def _get_ds(self, sys_cfg=None, ds_cfg=None, mockdata=None, dmi_data=None,
116 is_lxbrand=False):
117 mod = DataSourceSmartOS
118
119 if mockdata is None:
120 mockdata = MOCK_RETURNS
121
122 if dmi_data is None:
123 dmi_data = DMI_DATA_RETURN
124
125 def _dmi_data():
126 return dmi_data
127
128 def _os_uname():
129 if not is_lxbrand:
130 # LP: #1243287. tests assume this runs, but running test on
131 # arm would cause them all to fail.
132 return ('LINUX', 'NODENAME', 'RELEASE', 'VERSION', 'x86_64')
133 else:
134 return ('LINUX', 'NODENAME', 'RELEASE', 'BRANDZ VIRTUAL LINUX',
135 'X86_64')
136161
137 if sys_cfg is None:162 if sys_cfg is None:
138 sys_cfg = {}163 sys_cfg = {}
@@ -141,44 +166,8 @@
141 sys_cfg['datasource'] = sys_cfg.get('datasource', {})166 sys_cfg['datasource'] = sys_cfg.get('datasource', {})
142 sys_cfg['datasource']['SmartOS'] = ds_cfg167 sys_cfg['datasource']['SmartOS'] = ds_cfg
143168
144 self.apply_patches([(mod, 'LEGACY_USER_D', self.legacy_user_d)])169 return DataSourceSmartOS.DataSourceSmartOS(
145 self.apply_patches([170 sys_cfg, distro=None, paths=self.paths)
146 (mod, 'JoyentMetadataClient', get_mock_client(mockdata))])
147 self.apply_patches([(mod, 'dmi_data', _dmi_data)])
148 self.apply_patches([(os, 'uname', _os_uname)])
149 self.apply_patches([(mod, 'device_exists', lambda d: True)])
150 dsrc = mod.DataSourceSmartOS(sys_cfg, distro=None,
151 paths=self.paths)
152 self.apply_patches([(dsrc, '_get_seed_file_object', mock.MagicMock())])
153 return dsrc
154
155 def test_seed(self):
156 # default seed should be /dev/ttyS1
157 dsrc = self._get_ds()
158 ret = dsrc.get_data()
159 self.assertTrue(ret)
160 self.assertEqual('kvm', dsrc.smartos_type)
161 self.assertEqual('/dev/ttyS1', dsrc.seed)
162
163 def test_seed_lxbrand(self):
164 # default seed should be /dev/ttyS1
165 dsrc = self._get_ds(is_lxbrand=True)
166 ret = dsrc.get_data()
167 self.assertTrue(ret)
168 self.assertEqual('lx-brand', dsrc.smartos_type)
169 self.assertEqual('/native/.zonecontrol/metadata.sock', dsrc.seed)
170
171 def test_issmartdc(self):
172 dsrc = self._get_ds()
173 ret = dsrc.get_data()
174 self.assertTrue(ret)
175 self.assertTrue(dsrc.is_smartdc)
176
177 def test_issmartdc_lxbrand(self):
178 dsrc = self._get_ds(is_lxbrand=True)
179 ret = dsrc.get_data()
180 self.assertTrue(ret)
181 self.assertTrue(dsrc.is_smartdc)
182171
183 def test_no_base64(self):172 def test_no_base64(self):
184 ds_cfg = {'no_base64_decode': ['test_var1'], 'all_base': True}173 ds_cfg = {'no_base64_decode': ['test_var1'], 'all_base': True}
@@ -214,58 +203,6 @@
214 self.assertEqual(MOCK_RETURNS['hostname'],203 self.assertEqual(MOCK_RETURNS['hostname'],
215 dsrc.metadata['local-hostname'])204 dsrc.metadata['local-hostname'])
216205
217 def test_base64_all(self):
218 # metadata provided base64_all of true
219 my_returns = MOCK_RETURNS.copy()
220 my_returns['base64_all'] = "true"
221 for k in ('hostname', 'cloud-init:user-data'):
222 my_returns[k] = b64e(my_returns[k])
223
224 dsrc = self._get_ds(mockdata=my_returns)
225 ret = dsrc.get_data()
226 self.assertTrue(ret)
227 self.assertEqual(MOCK_RETURNS['hostname'],
228 dsrc.metadata['local-hostname'])
229 self.assertEqual(MOCK_RETURNS['cloud-init:user-data'],
230 dsrc.userdata_raw)
231 self.assertEqual(MOCK_RETURNS['root_authorized_keys'],
232 dsrc.metadata['public-keys'])
233 self.assertEqual(MOCK_RETURNS['disable_iptables_flag'],
234 dsrc.metadata['iptables_disable'])
235 self.assertEqual(MOCK_RETURNS['enable_motd_sys_info'],
236 dsrc.metadata['motd_sys_info'])
237
238 def test_b64_userdata(self):
239 my_returns = MOCK_RETURNS.copy()
240 my_returns['b64-cloud-init:user-data'] = "true"
241 my_returns['b64-hostname'] = "true"
242 for k in ('hostname', 'cloud-init:user-data'):
243 my_returns[k] = b64e(my_returns[k])
244
245 dsrc = self._get_ds(mockdata=my_returns)
246 ret = dsrc.get_data()
247 self.assertTrue(ret)
248 self.assertEqual(MOCK_RETURNS['hostname'],
249 dsrc.metadata['local-hostname'])
250 self.assertEqual(MOCK_RETURNS['cloud-init:user-data'],
251 dsrc.userdata_raw)
252 self.assertEqual(MOCK_RETURNS['root_authorized_keys'],
253 dsrc.metadata['public-keys'])
254
255 def test_b64_keys(self):
256 my_returns = MOCK_RETURNS.copy()
257 my_returns['base64_keys'] = 'hostname,ignored'
258 for k in ('hostname',):
259 my_returns[k] = b64e(my_returns[k])
260
261 dsrc = self._get_ds(mockdata=my_returns)
262 ret = dsrc.get_data()
263 self.assertTrue(ret)
264 self.assertEqual(MOCK_RETURNS['hostname'],
265 dsrc.metadata['local-hostname'])
266 self.assertEqual(MOCK_RETURNS['cloud-init:user-data'],
267 dsrc.userdata_raw)
268
269 def test_userdata(self):206 def test_userdata(self):
270 dsrc = self._get_ds(mockdata=MOCK_RETURNS)207 dsrc = self._get_ds(mockdata=MOCK_RETURNS)
271 ret = dsrc.get_data()208 ret = dsrc.get_data()
@@ -275,6 +212,13 @@
275 self.assertEqual(MOCK_RETURNS['cloud-init:user-data'],212 self.assertEqual(MOCK_RETURNS['cloud-init:user-data'],
276 dsrc.userdata_raw)213 dsrc.userdata_raw)
277214
215 def test_sdc_nics(self):
216 dsrc = self._get_ds(mockdata=MOCK_RETURNS)
217 ret = dsrc.get_data()
218 self.assertTrue(ret)
219 self.assertEqual(json.loads(MOCK_RETURNS['sdc:nics']),
220 dsrc.metadata['network-data'])
221
278 def test_sdc_scripts(self):222 def test_sdc_scripts(self):
279 dsrc = self._get_ds(mockdata=MOCK_RETURNS)223 dsrc = self._get_ds(mockdata=MOCK_RETURNS)
280 ret = dsrc.get_data()224 ret = dsrc.get_data()
@@ -430,18 +374,7 @@
430 mydscfg['disk_aliases']['FOO'])374 mydscfg['disk_aliases']['FOO'])
431375
432376
433def apply_patches(patches):377class TestJoyentMetadataClient(FilesystemMockingTestCase):
434 ret = []
435 for (ref, name, replace) in patches:
436 if replace is None:
437 continue
438 orig = getattr(ref, name)
439 setattr(ref, name, replace)
440 ret.append((ref, name, orig))
441 return ret
442
443
444class TestJoyentMetadataClient(helpers.FilesystemMockingTestCase):
445378
446 def setUp(self):379 def setUp(self):
447 super(TestJoyentMetadataClient, self).setUp()380 super(TestJoyentMetadataClient, self).setUp()
@@ -481,7 +414,8 @@
481 mock.Mock(return_value=self.request_id)))414 mock.Mock(return_value=self.request_id)))
482415
483 def _get_client(self):416 def _get_client(self):
484 return DataSourceSmartOS.JoyentMetadataClient(self.serial)417 return DataSourceSmartOS.JoyentMetadataClient(
418 fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM)
485419
486 def assertEndsWith(self, haystack, prefix):420 def assertEndsWith(self, haystack, prefix):
487 self.assertTrue(haystack.endswith(prefix),421 self.assertTrue(haystack.endswith(prefix),
@@ -495,7 +429,7 @@
495429
496 def test_get_metadata_writes_a_single_line(self):430 def test_get_metadata_writes_a_single_line(self):
497 client = self._get_client()431 client = self._get_client()
498 client.get_metadata('some_key')432 client.get('some_key')
499 self.assertEqual(1, self.serial.write.call_count)433 self.assertEqual(1, self.serial.write.call_count)
500 written_line = self.serial.write.call_args[0][0]434 written_line = self.serial.write.call_args[0][0]
501 print(type(written_line))435 print(type(written_line))
@@ -505,7 +439,7 @@
505439
506 def _get_written_line(self, key='some_key'):440 def _get_written_line(self, key='some_key'):
507 client = self._get_client()441 client = self._get_client()
508 client.get_metadata(key)442 client.get(key)
509 return self.serial.write.call_args[0][0]443 return self.serial.write.call_args[0][0]
510444
511 def test_get_metadata_writes_bytes(self):445 def test_get_metadata_writes_bytes(self):
@@ -549,32 +483,32 @@
549483
550 def test_get_metadata_reads_a_line(self):484 def test_get_metadata_reads_a_line(self):
551 client = self._get_client()485 client = self._get_client()
552 client.get_metadata('some_key')486 client.get('some_key')
553 self.assertEqual(self.metasource_data_len, self.serial.read.call_count)487 self.assertEqual(self.metasource_data_len, self.serial.read.call_count)
554488
555 def test_get_metadata_returns_valid_value(self):489 def test_get_metadata_returns_valid_value(self):
556 client = self._get_client()490 client = self._get_client()
557 value = client.get_metadata('some_key')491 value = client.get('some_key')
558 self.assertEqual(self.metadata_value, value)492 self.assertEqual(self.metadata_value, value)
559493
560 def test_get_metadata_throws_exception_for_incorrect_length(self):494 def test_get_metadata_throws_exception_for_incorrect_length(self):
561 self.response_parts['length'] = 0495 self.response_parts['length'] = 0
562 client = self._get_client()496 client = self._get_client()
563 self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,497 self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
564 client.get_metadata, 'some_key')498 client.get, 'some_key')
565499
566 def test_get_metadata_throws_exception_for_incorrect_crc(self):500 def test_get_metadata_throws_exception_for_incorrect_crc(self):
567 self.response_parts['crc'] = 'deadbeef'501 self.response_parts['crc'] = 'deadbeef'
568 client = self._get_client()502 client = self._get_client()
569 self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,503 self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
570 client.get_metadata, 'some_key')504 client.get, 'some_key')
571505
572 def test_get_metadata_throws_exception_for_request_id_mismatch(self):506 def test_get_metadata_throws_exception_for_request_id_mismatch(self):
573 self.response_parts['request_id'] = 'deadbeef'507 self.response_parts['request_id'] = 'deadbeef'
574 client = self._get_client()508 client = self._get_client()
575 client._checksum = lambda _: self.response_parts['crc']509 client._checksum = lambda _: self.response_parts['crc']
576 self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,510 self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
577 client.get_metadata, 'some_key')511 client.get, 'some_key')
578512
579 def test_get_metadata_returns_None_if_value_not_found(self):513 def test_get_metadata_returns_None_if_value_not_found(self):
580 self.response_parts['payload'] = ''514 self.response_parts['payload'] = ''
@@ -582,4 +516,24 @@
582 self.response_parts['length'] = 17516 self.response_parts['length'] = 17
583 client = self._get_client()517 client = self._get_client()
584 client._checksum = lambda _: self.response_parts['crc']518 client._checksum = lambda _: self.response_parts['crc']
585 self.assertIsNone(client.get_metadata('some_key'))519 self.assertIsNone(client.get('some_key'))
520
521
522class TestNetworkConversion(TestCase):
523
524 def test_convert_simple(self):
525 expected = {
526 'version': 1,
527 'config': [
528 {'name': 'net0', 'type': 'physical',
529 'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
530 'netmask': '255.255.255.0',
531 'address': '8.12.42.102/24'}],
532 'mtu': 1500, 'mac_address': '90:b8:d0:f5:e4:f5'},
533 {'name': 'net1', 'type': 'physical',
534 'subnets': [{'type': 'static', 'gateway': '192.168.128.1',
535 'netmask': '255.255.252.0',
536 'address': '192.168.128.93/22'}],
537 'mtu': 8500, 'mac_address': '90:b8:d0:a5:ff:cd'}]}
538 found = DataSourceSmartOS.convert_smartos_network_data(SDC_NICS)
539 self.assertEqual(expected, found)