156
172
class DataSourceSmartOS(sources.DataSource):
174
smartos_type = _unset
157
177
def __init__(self, sys_cfg, distro, paths):
158
178
sources.DataSource.__init__(self, sys_cfg, distro, paths)
159
self.is_smartdc = None
160
179
self.ds_cfg = util.mergemanydict([
162
181
util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),
163
182
BUILTIN_DS_CONFIG])
165
184
self.metadata = {}
185
self.network_data = None
186
self._network_config = None
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'
174
self.seed = self.ds_cfg.get("metadata_sockfile")
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
188
self.script_base_d = os.path.join(self.paths.get_cpath("scripts"))
186
192
def __str__(self):
187
193
root = sources.DataSource.__str__(self)
188
return "%s [seed=%s]" % (root, self.seed)
190
def _get_seed_file_object(self):
192
raise AttributeError("seed device is not set")
194
if self.smartos_type == 'lx-brand':
195
if not stat.S_ISSOCK(os.stat(self.seed).st_mode):
196
LOG.debug("Seed %s is not a socket", self.seed)
198
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
199
sock.connect(self.seed)
200
return sock.makefile('rwb')
202
if not stat.S_ISCHR(os.stat(self.seed).st_mode):
203
LOG.debug("Seed %s is not a character device")
205
ser = serial.Serial(self.seed, timeout=self.seed_timeout)
207
raise SystemError("Unable to open %s" % self.seed)
194
return "%s [client=%s]" % (root, self.md_client)
197
if self.smartos_type == self._unset:
198
self.smartos_type = get_smartos_environ()
199
if self.smartos_type is None:
200
self.md_client = None
202
if self.md_client == self._unset:
203
self.md_client = jmc_client_factory(
204
smartos_type=self.smartos_type,
205
metadata_sockfile=self.ds_cfg['metadata_sockfile'],
206
serial_device=self.ds_cfg['serial_device'],
207
serial_timeout=self.ds_cfg['serial_timeout'])
211
209
def _set_provisioned(self):
212
210
'''Mark the instance provisioning state as successful.
225
223
'/'.join([svc_path, 'provision_success']))
227
225
def get_data(self):
231
if not device_exists(self.seed):
232
LOG.debug("No metadata device '%s' found for SmartOS datasource",
236
uname_arch = os.uname()[4]
237
if uname_arch.startswith("arm") or uname_arch == "aarch64":
238
# Disabling because dmidcode in dmi_data() crashes kvm process
239
LOG.debug("Disabling SmartOS datasource on arm (LP: #1243287)")
242
# SDC KVM instances will provide dmi data, LX-brand does not
243
if self.smartos_type == 'kvm':
244
dmi_info = dmi_data()
246
LOG.debug("No dmidata utility found")
249
system_type = dmi_info
250
if 'smartdc' not in system_type.lower():
251
LOG.debug("Host is not on SmartOS. system_type=%s",
254
LOG.debug("Host is SmartOS, guest in KVM")
256
seed_obj = self._get_seed_file_object()
258
LOG.debug('Seed file object not found.')
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(',')]
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)
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)
231
if not self.smartos_type:
232
LOG.debug("Not running on smartos")
235
if not self.md_client.exists():
236
LOG.debug("No metadata device '%r' found for SmartOS datasource",
240
for ci_noun, attribute in SMARTOS_ATTRIB_MAP.items():
241
smartos_noun, strip = attribute
242
md[ci_noun] = self.md_client.get(smartos_noun, strip=strip)
244
for ci_noun, smartos_noun in SMARTOS_ATTRIB_JSON.items():
245
md[ci_noun] = self.md_client.get_json(smartos_noun)
273
247
# @datadictionary: This key may contain a program that is written
274
248
# to a file in the filesystem of the guest on each boot and then
326
301
return self.ds_cfg['disk_aliases'].get(name)
328
303
def get_config_obj(self):
304
if self.smartos_type == SMARTOS_ENV_KVM:
305
return BUILTIN_CLOUD_CONFIG
331
308
def get_instance_id(self):
332
309
return self.metadata['instance-id']
334
def query(self, noun, seed_file, strip=False, default=None, b64=None):
336
if noun in self.smartos_no_base64:
338
elif self.b64_all or noun in self.b64_keys:
341
return self._query_data(noun, seed_file, strip=strip,
342
default=default, b64=b64)
344
def _query_data(self, noun, seed_file, strip=False,
345
default=None, b64=None):
346
"""Makes a request via "GET <NOUN>"
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.
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.
361
response = JoyentMetadataClient(seed_file).get_metadata(noun)
367
b64 = self._query_data('b64-%s' % noun, seed_file, b64=False,
368
default=False, strip=True)
369
b64 = util.is_true(b64)
373
resp = "".join(response).rstrip()
375
resp = "".join(response)
379
return util.b64d(resp)
380
# Bogus input produces different errors in Python 2 and 3;
382
except (TypeError, binascii.Error):
383
LOG.warn("Failed base64 decoding key '%s'", noun)
389
def device_exists(device):
390
"""Symplistic method to determine if the device exists or not"""
391
return os.path.exists(device)
312
def network_config(self):
313
if self._network_config is None:
314
if self.network_data is not None:
315
self._network_config = (
316
convert_smartos_network_data(self.network_data))
317
return self._network_config
394
320
class JoyentMetadataFetchException(Exception):
436
365
LOG.debug('Value "%s" found.', value)
439
def get_metadata(self, metadata_key):
440
LOG.debug('Fetching metadata key "%s"...', metadata_key)
368
def request(self, rtype, param=None):
441
369
request_id = '{0:08x}'.format(random.randint(0, 0xffffffff))
442
message_body = '{0} GET {1}'.format(request_id,
443
util.b64e(metadata_key))
370
message_body = ' '.join((request_id, rtype,))
372
message_body += ' ' + base64.b64encode(param.encode()).decode()
444
373
msg = 'V2 {0} {1} {2}\n'.format(
445
374
len(message_body), self._checksum(message_body), message_body)
446
375
LOG.debug('Writing "%s" to metadata transport.', msg)
447
self.metasource.write(msg.encode('ascii'))
448
self.metasource.flush()
379
self.open_transport()
382
self.fp.write(msg.encode('ascii'))
450
385
response = bytearray()
451
response.extend(self.metasource.read(1))
386
response.extend(self.fp.read(1))
452
387
while response[-1:] != b'\n':
453
response.extend(self.metasource.read(1))
388
response.extend(self.fp.read(1))
391
self.close_transport()
454
393
response = response.rstrip().decode('ascii')
455
394
LOG.debug('Read "%s" from metadata transport.', response)
457
396
if 'SUCCESS' not in response:
460
return self._get_value_from_frame(request_id, response)
464
sys_type = util.read_dmi_data("system-product-name")
399
value = self._get_value_from_frame(request_id, response)
402
def get(self, key, default=None, strip=False):
403
result = self.request(rtype='GET', param=key)
407
result = result.strip()
410
def get_json(self, key, default=None):
411
result = self.get(key, default=default)
414
return json.loads(result)
417
result = self.request(rtype='KEYS')
419
result = result.split('\n')
422
def put(self, key, val):
423
param = b' '.join([base64.b64encode(i.encode())
424
for i in (key, val)]).decode()
425
return self.request(rtype='PUT', param=param)
427
def delete(self, key):
428
return self.request(rtype='DELETE', param=key)
430
def close_transport(self):
438
self.open_transport()
441
def __exit__(self, exc_type, exc_value, traceback):
442
self.close_transport()
445
def open_transport(self):
446
raise NotImplementedError
449
class JoyentMetadataSocketClient(JoyentMetadataClient):
450
def __init__(self, socketpath):
451
self.socketpath = socketpath
453
def open_transport(self):
454
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
455
sock.connect(self.socketpath)
456
self.fp = sock.makefile('rwb')
459
return os.path.exists(self.socketpath)
462
return "%s(socketpath=%s)" % (self.__class__.__name__, self.socketpath)
465
class JoyentMetadataSerialClient(JoyentMetadataClient):
466
def __init__(self, device, timeout=10, smartos_type=None):
467
super(JoyentMetadataSerialClient, self).__init__(smartos_type)
469
self.timeout = timeout
472
return os.path.exists(self.device)
474
def open_transport(self):
475
ser = serial.Serial(self.device, timeout=self.timeout)
477
raise SystemError("Unable to open %s" % self.device)
481
return "%s(device=%s, timeout=%s)" % (
482
self.__class__.__name__, self.device, self.timeout)
485
class JoyentMetadataLegacySerialClient(JoyentMetadataSerialClient):
486
"""V1 of the protocol was not safe for all values.
487
Thus, we allowed the user to pass values in as base64 encoded.
488
Users may still reasonably expect to be able to send base64 data
489
and have it transparently decoded. So even though the V2 format is
490
now used, and is safe (using base64 itself), we keep legacy support.
492
The way for a user to do this was:
493
a.) specify 'base64_keys' key whose value is a comma delimited
494
list of keys that were base64 encoded.
495
b.) base64_all: string interpreted as a boolean that indicates
496
if all keys are base64 encoded.
497
c.) set a key named b64-<keyname> with a boolean indicating that
498
<keyname> is base64 encoded."""
500
def __init__(self, device, timeout=10, smartos_type=None):
501
s = super(JoyentMetadataLegacySerialClient, self)
502
s.__init__(device, timeout, smartos_type)
503
self.base64_keys = None
504
self.base64_all = None
506
def _init_base64_keys(self, reset=False):
508
self.base64_keys = None
509
self.base64_all = None
512
if self.base64_all is None:
514
if 'base64_all' in keys:
515
self.base64_all = util.is_true(self._get("base64_all"))
517
self.base64_all = False
520
# short circuit if base64_all is true
523
if self.base64_keys is None:
527
if 'base64_keys' in keys:
528
b64_keys = set(self._get("base64_keys").split(","))
530
# now add any b64-<keyname> that has a true value
531
for key in [k[3:] for k in keys if k.startswith("b64-")]:
532
if util.is_true(self._get(key)):
538
self.base64_keys = b64_keys
540
def _get(self, key, default=None, strip=False):
541
return (super(JoyentMetadataLegacySerialClient, self).
542
get(key, default=default, strip=strip))
544
def is_b64_encoded(self, key, reset=False):
545
if key in NO_BASE64_DECODE:
548
self._init_base64_keys(reset=reset)
552
return key in self.base64_keys
554
def get(self, key, default=None, strip=False):
556
val = self._get(key, strip=False, default=mdefault)
560
if self.is_b64_encoded(key):
562
val = base64.b64decode(val.encode()).decode()
563
# Bogus input produces different errors in Python 2 and 3
564
except (TypeError, binascii.Error):
565
LOG.warn("Failed base64 decoding key '%s': %s", key, val)
573
def jmc_client_factory(
574
smartos_type=None, metadata_sockfile=METADATA_SOCKFILE,
575
serial_device=SERIAL_DEVICE, serial_timeout=SERIAL_TIMEOUT,
578
if smartos_type is None:
579
smartos_type = get_smartos_environ(uname_version)
581
if smartos_type is None:
583
elif smartos_type == SMARTOS_ENV_KVM:
584
return JoyentMetadataLegacySerialClient(
585
device=serial_device, timeout=serial_timeout,
586
smartos_type=smartos_type)
587
elif smartos_type == SMARTOS_ENV_LX_BRAND:
588
return JoyentMetadataSocketClient(socketpath=metadata_sockfile)
590
raise ValueError("Unknown value for smartos_type: %s" % smartos_type)
472
593
def write_boot_content(content, content_f, link=None, shebang=False,
522
643
util.ensure_dir(os.path.dirname(link))
523
644
os.symlink(content_f, link)
524
645
except IOError as e:
525
util.logexc(LOG, "failed establishing content link", e)
646
util.logexc(LOG, "failed establishing content link: %s", e)
649
def get_smartos_environ(uname_version=None, product_name=None,
652
if uname_arch is None:
653
uname_arch = uname[4]
655
if uname_arch.startswith("arm") or uname_arch == "aarch64":
658
# SDC LX-Brand Zones lack dmidecode (no /dev/mem) but
659
# report 'BrandZ virtual linux' as the kernel version
660
if uname_version is None:
661
uname_version = uname[3]
662
if uname_version.lower() == 'brandz virtual linux':
663
return SMARTOS_ENV_LX_BRAND
665
if product_name is None:
666
system_type = util.read_dmi_data("system-product-name")
668
system_type = product_name
670
if system_type and 'smartdc' in system_type.lower():
671
return SMARTOS_ENV_KVM
676
# Covert SMARTOS 'sdc:nics' data to network_config yaml
677
def convert_smartos_network_data(network_data=None):
678
"""Return a dictionary of network_config by parsing provided
679
SMARTOS sdc:nics configuration data
681
sdc:nics data is a dictionary of properties of a nic and the ip
682
configuration desired. Additional nic dictionaries are appended
685
Converting the format is straightforward though it does include
686
duplicate information as well as data which appears to be relevant
687
to the hostOS rather than the guest.
689
For each entry in the nics list returned from query sdc:nics, we
690
create a type: physical entry, and extract the interface properties:
691
'mac' -> 'mac_address', 'mtu', 'interface' -> 'name'. The remaining
692
keys are related to ip configuration. For each ip in the 'ips' list
693
we create a subnet entry under 'subnets' pairing the ip to a one in
722
for nic in network_data:
723
cfg = {k: v for k, v in nic.items()
724
if k in valid_keys['physical']}
727
'name': nic['interface']})
729
cfg.update({'mac_address': nic['mac']})
732
for ip, gw in zip(nic['ips'], nic['gateways']):
733
subnet = {k: v for k, v in nic.items()
734
if k in valid_keys['subnet']}
740
subnets.append(subnet)
741
cfg.update({'subnets': subnets})
744
return {'version': 1, 'config': config}
528
747
# Used to match classes to dependencies
530
(DataSourceSmartOS, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
749
(DataSourceSmartOS, (sources.DEP_FILESYSTEM, )),
534
753
# Return a list of data sources that match this set of dependencies
535
754
def get_datasource_list(depends):
536
755
return sources.list_from_depends(depends, datasources)
758
if __name__ == "__main__":
760
jmc = jmc_client_factory()
762
print("Do not appear to be on smartos.")
764
if len(sys.argv) == 1:
765
keys = (list(SMARTOS_ATTRIB_JSON.keys()) +
766
list(SMARTOS_ATTRIB_MAP.keys()))
772
if key in SMARTOS_ATTRIB_JSON:
773
keyname = SMARTOS_ATTRIB_JSON[key]
774
data[key] = jmc.get_json(keyname)
776
if key in SMARTOS_ATTRIB_MAP:
777
keyname, strip = SMARTOS_ATTRIB_MAP[key]
779
keyname, strip = (key, False)
780
val = jmc.get(keyname, strip=strip)
781
data[key] = jmc.get(keyname, strip=strip)
783
print(json.dumps(data, indent=1))