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