1 # Copyright (C) 2013 Canonical Ltd.
   2 # Copyright (c) 2018, Joyent, Inc.
   3 #
   4 # Author: Ben Howard <ben.howard@canonical.com>
   5 #
   6 # This file is part of cloud-init. See LICENSE file for license information.
   7 
   8 '''This is a testcase for the SmartOS datasource.
   9 
  10 It replicates a serial console and acts like the SmartOS console does in
  11 order to validate return responses.
  12 
  13 '''
  14 
  15 from __future__ import print_function
  16 
  17 from binascii import crc32
  18 import json
  19 import multiprocessing
  20 import os
  21 import os.path
  22 import re
  23 import shutil
  24 import signal
  25 import stat
  26 import subprocess
  27 import tempfile
  28 import unittest
  29 import uuid
  30 
  31 from cloudinit import serial
  32 from cloudinit.sources import DataSourceSmartOS
  33 from cloudinit.sources.DataSourceSmartOS import (
  34     convert_smartos_network_data as convert_net)
  35 
  36 import six
  37 
  38 from cloudinit import helpers as c_helpers
  39 from cloudinit.util import b64e
  40 
  41 from cloudinit.tests.helpers import mock, FilesystemMockingTestCase, TestCase
  42 
  43 SDC_NICS = json.loads("""
  44 [
  45     {
  46         "nic_tag": "external",
  47         "primary": true,
  48         "mtu": 1500,
  49         "model": "virtio",
  50         "gateway": "8.12.42.1",
  51         "netmask": "255.255.255.0",
  52         "ip": "8.12.42.102",
  53         "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
  54         "gateways": [
  55             "8.12.42.1"
  56         ],
  57         "vlan_id": 324,
  58         "mac": "90:b8:d0:f5:e4:f5",
  59         "interface": "net0",
  60         "ips": [
  61             "8.12.42.102/24"
  62         ]
  63     },
  64     {
  65         "nic_tag": "sdc_overlay/16187209",
  66         "gateway": "192.168.128.1",
  67         "model": "virtio",
  68         "mac": "90:b8:d0:a5:ff:cd",
  69         "netmask": "255.255.252.0",
  70         "ip": "192.168.128.93",
  71         "network_uuid": "4cad71da-09bc-452b-986d-03562a03a0a9",
  72         "gateways": [
  73             "192.168.128.1"
  74         ],
  75         "vlan_id": 2,
  76         "mtu": 8500,
  77         "interface": "net1",
  78         "ips": [
  79             "192.168.128.93/22"
  80         ]
  81     }
  82 ]
  83 """)
  84 
  85 
  86 SDC_NICS_ALT = json.loads("""
  87 [
  88     {
  89         "interface": "net0",
  90         "mac": "90:b8:d0:ae:64:51",
  91         "vlan_id": 324,
  92         "nic_tag": "external",
  93         "gateway": "8.12.42.1",
  94         "gateways": [
  95           "8.12.42.1"
  96         ],
  97         "netmask": "255.255.255.0",
  98         "ip": "8.12.42.51",
  99         "ips": [
 100           "8.12.42.51/24"
 101         ],
 102         "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
 103         "model": "virtio",
 104         "mtu": 1500,
 105         "primary": true
 106     },
 107     {
 108         "interface": "net1",
 109         "mac": "90:b8:d0:bd:4f:9c",
 110         "vlan_id": 600,
 111         "nic_tag": "internal",
 112         "netmask": "255.255.255.0",
 113         "ip": "10.210.1.217",
 114         "ips": [
 115           "10.210.1.217/24"
 116         ],
 117         "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
 118         "model": "virtio",
 119         "mtu": 1500
 120     }
 121 ]
 122 """)
 123 
 124 SDC_NICS_DHCP = json.loads("""
 125 [
 126     {
 127         "interface": "net0",
 128         "mac": "90:b8:d0:ae:64:51",
 129         "vlan_id": 324,
 130         "nic_tag": "external",
 131         "gateway": "8.12.42.1",
 132         "gateways": [
 133           "8.12.42.1"
 134         ],
 135         "netmask": "255.255.255.0",
 136         "ip": "8.12.42.51",
 137         "ips": [
 138           "8.12.42.51/24"
 139         ],
 140         "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
 141         "model": "virtio",
 142         "mtu": 1500,
 143         "primary": true
 144     },
 145     {
 146         "interface": "net1",
 147         "mac": "90:b8:d0:bd:4f:9c",
 148         "vlan_id": 600,
 149         "nic_tag": "internal",
 150         "netmask": "255.255.255.0",
 151         "ip": "10.210.1.217",
 152         "ips": [
 153           "dhcp"
 154         ],
 155         "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
 156         "model": "virtio",
 157         "mtu": 1500
 158     }
 159 ]
 160 """)
 161 
 162 SDC_NICS_MIP = json.loads("""
 163 [
 164     {
 165         "interface": "net0",
 166         "mac": "90:b8:d0:ae:64:51",
 167         "vlan_id": 324,
 168         "nic_tag": "external",
 169         "gateway": "8.12.42.1",
 170         "gateways": [
 171           "8.12.42.1"
 172         ],
 173         "netmask": "255.255.255.0",
 174         "ip": "8.12.42.51",
 175         "ips": [
 176           "8.12.42.51/24",
 177           "8.12.42.52/24"
 178         ],
 179         "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
 180         "model": "virtio",
 181         "mtu": 1500,
 182         "primary": true
 183     },
 184     {
 185         "interface": "net1",
 186         "mac": "90:b8:d0:bd:4f:9c",
 187         "vlan_id": 600,
 188         "nic_tag": "internal",
 189         "netmask": "255.255.255.0",
 190         "ip": "10.210.1.217",
 191         "ips": [
 192           "10.210.1.217/24",
 193           "10.210.1.151/24"
 194         ],
 195         "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
 196         "model": "virtio",
 197         "mtu": 1500
 198     }
 199 ]
 200 """)
 201 
 202 SDC_NICS_MIP_IPV6 = json.loads("""
 203 [
 204     {
 205         "interface": "net0",
 206         "mac": "90:b8:d0:ae:64:51",
 207         "vlan_id": 324,
 208         "nic_tag": "external",
 209         "gateway": "8.12.42.1",
 210         "gateways": [
 211           "8.12.42.1"
 212         ],
 213         "netmask": "255.255.255.0",
 214         "ip": "8.12.42.51",
 215         "ips": [
 216           "2001:4800:78ff:1b:be76:4eff:fe06:96b3/64",
 217           "8.12.42.51/24"
 218         ],
 219         "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
 220         "model": "virtio",
 221         "mtu": 1500,
 222         "primary": true
 223     },
 224     {
 225         "interface": "net1",
 226         "mac": "90:b8:d0:bd:4f:9c",
 227         "vlan_id": 600,
 228         "nic_tag": "internal",
 229         "netmask": "255.255.255.0",
 230         "ip": "10.210.1.217",
 231         "ips": [
 232           "10.210.1.217/24"
 233         ],
 234         "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
 235         "model": "virtio",
 236         "mtu": 1500
 237     }
 238 ]
 239 """)
 240 
 241 SDC_NICS_IPV4_IPV6 = json.loads("""
 242 [
 243     {
 244         "interface": "net0",
 245         "mac": "90:b8:d0:ae:64:51",
 246         "vlan_id": 324,
 247         "nic_tag": "external",
 248         "gateway": "8.12.42.1",
 249         "gateways": ["8.12.42.1", "2001::1", "2001::2"],
 250         "netmask": "255.255.255.0",
 251         "ip": "8.12.42.51",
 252         "ips": ["2001::10/64", "8.12.42.51/24", "2001::11/64",
 253                 "8.12.42.52/32"],
 254         "network_uuid": "992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
 255         "model": "virtio",
 256         "mtu": 1500,
 257         "primary": true
 258     },
 259     {
 260         "interface": "net1",
 261         "mac": "90:b8:d0:bd:4f:9c",
 262         "vlan_id": 600,
 263         "nic_tag": "internal",
 264         "netmask": "255.255.255.0",
 265         "ip": "10.210.1.217",
 266         "ips": ["10.210.1.217/24"],
 267         "gateways": ["10.210.1.210"],
 268         "network_uuid": "98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
 269         "model": "virtio",
 270         "mtu": 1500
 271     }
 272 ]
 273 """)
 274 
 275 SDC_NICS_SINGLE_GATEWAY = json.loads("""
 276 [
 277   {
 278     "interface":"net0",
 279     "mac":"90:b8:d0:d8:82:b4",
 280     "vlan_id":324,
 281     "nic_tag":"external",
 282     "gateway":"8.12.42.1",
 283     "gateways":["8.12.42.1"],
 284     "netmask":"255.255.255.0",
 285     "ip":"8.12.42.26",
 286     "ips":["8.12.42.26/24"],
 287     "network_uuid":"992fc7ce-6aac-4b74-aed6-7b9d2c6c0bfe",
 288     "model":"virtio",
 289     "mtu":1500,
 290     "primary":true
 291   },
 292   {
 293     "interface":"net1",
 294     "mac":"90:b8:d0:0a:51:31",
 295     "vlan_id":600,
 296     "nic_tag":"internal",
 297     "netmask":"255.255.255.0",
 298     "ip":"10.210.1.27",
 299     "ips":["10.210.1.27/24"],
 300     "network_uuid":"98657fdf-11f4-4ee2-88a4-ce7fe73e33a6",
 301     "model":"virtio",
 302     "mtu":1500
 303   }
 304 ]
 305 """)
 306 
 307 
 308 MOCK_RETURNS = {
 309     'hostname': 'test-host',
 310     'root_authorized_keys': 'ssh-rsa AAAAB3Nz...aC1yc2E= keyname',
 311     'disable_iptables_flag': None,
 312     'enable_motd_sys_info': None,
 313     'test-var1': 'some data',
 314     'cloud-init:user-data': '\n'.join(['#!/bin/sh', '/bin/true', '']),
 315     'sdc:datacenter_name': 'somewhere2',
 316     'sdc:operator-script': '\n'.join(['bin/true', '']),
 317     'sdc:uuid': str(uuid.uuid4()),
 318     'sdc:vendor-data': '\n'.join(['VENDOR_DATA', '']),
 319     'user-data': '\n'.join(['something', '']),
 320     'user-script': '\n'.join(['/bin/true', '']),
 321     'sdc:nics': json.dumps(SDC_NICS),
 322 }
 323 
 324 DMI_DATA_RETURN = 'smartdc'
 325 
 326 
 327 class PsuedoJoyentClient(object):
 328     def __init__(self, data=None):
 329         if data is None:
 330             data = MOCK_RETURNS.copy()
 331         self.data = data
 332         self._is_open = False
 333         return
 334 
 335     def get(self, key, default=None, strip=False):
 336         if key in self.data:
 337             r = self.data[key]
 338             if strip:
 339                 r = r.strip()
 340         else:
 341             r = default
 342         return r
 343 
 344     def get_json(self, key, default=None):
 345         result = self.get(key, default=default)
 346         if result is None:
 347             return default
 348         return json.loads(result)
 349 
 350     def exists(self):
 351         return True
 352 
 353     def open_transport(self):
 354         assert(not self._is_open)
 355         self._is_open = True
 356 
 357     def close_transport(self):
 358         assert(self._is_open)
 359         self._is_open = False
 360 
 361 
 362 class TestSmartOSDataSource(FilesystemMockingTestCase):
 363     def setUp(self):
 364         super(TestSmartOSDataSource, self).setUp()
 365 
 366         dsmos = 'cloudinit.sources.DataSourceSmartOS'
 367         patcher = mock.patch(dsmos + ".jmc_client_factory")
 368         self.jmc_cfact = patcher.start()
 369         self.addCleanup(patcher.stop)
 370         patcher = mock.patch(dsmos + ".get_smartos_environ")
 371         self.get_smartos_environ = patcher.start()
 372         self.addCleanup(patcher.stop)
 373 
 374         self.tmp = tempfile.mkdtemp()
 375         self.addCleanup(shutil.rmtree, self.tmp)
 376         self.paths = c_helpers.Paths(
 377             {'cloud_dir': self.tmp, 'run_dir': self.tmp})
 378 
 379         self.legacy_user_d = os.path.join(self.tmp, 'legacy_user_tmp')
 380         os.mkdir(self.legacy_user_d)
 381 
 382         self.orig_lud = DataSourceSmartOS.LEGACY_USER_D
 383         DataSourceSmartOS.LEGACY_USER_D = self.legacy_user_d
 384 
 385     def tearDown(self):
 386         DataSourceSmartOS.LEGACY_USER_D = self.orig_lud
 387         super(TestSmartOSDataSource, self).tearDown()
 388 
 389     def _get_ds(self, mockdata=None, mode=DataSourceSmartOS.SMARTOS_ENV_KVM,
 390                 sys_cfg=None, ds_cfg=None):
 391         self.jmc_cfact.return_value = PsuedoJoyentClient(mockdata)
 392         self.get_smartos_environ.return_value = mode
 393 
 394         if sys_cfg is None:
 395             sys_cfg = {}
 396 
 397         if ds_cfg is not None:
 398             sys_cfg['datasource'] = sys_cfg.get('datasource', {})
 399             sys_cfg['datasource']['SmartOS'] = ds_cfg
 400 
 401         return DataSourceSmartOS.DataSourceSmartOS(
 402             sys_cfg, distro=None, paths=self.paths)
 403 
 404     def test_no_base64(self):
 405         ds_cfg = {'no_base64_decode': ['test_var1'], 'all_base': True}
 406         dsrc = self._get_ds(ds_cfg=ds_cfg)
 407         ret = dsrc.get_data()
 408         self.assertTrue(ret)
 409 
 410     def test_uuid(self):
 411         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 412         ret = dsrc.get_data()
 413         self.assertTrue(ret)
 414         self.assertEqual(MOCK_RETURNS['sdc:uuid'],
 415                          dsrc.metadata['instance-id'])
 416 
 417     def test_routes(self):
 418         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 419         ret = dsrc.get_data()
 420         self.assertTrue(ret)
 421         self.assertEqual(MOCK_RETURNS['sdc:routes'],
 422                          dsrc.metadata['routes'])
 423 
 424     def test_root_keys(self):
 425         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 426         ret = dsrc.get_data()
 427         self.assertTrue(ret)
 428         self.assertEqual(MOCK_RETURNS['root_authorized_keys'],
 429                          dsrc.metadata['public-keys'])
 430 
 431     def test_hostname_b64(self):
 432         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 433         ret = dsrc.get_data()
 434         self.assertTrue(ret)
 435         self.assertEqual(MOCK_RETURNS['hostname'],
 436                          dsrc.metadata['local-hostname'])
 437 
 438     def test_hostname(self):
 439         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 440         ret = dsrc.get_data()
 441         self.assertTrue(ret)
 442         self.assertEqual(MOCK_RETURNS['hostname'],
 443                          dsrc.metadata['local-hostname'])
 444 
 445     def test_userdata(self):
 446         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 447         ret = dsrc.get_data()
 448         self.assertTrue(ret)
 449         self.assertEqual(MOCK_RETURNS['user-data'],
 450                          dsrc.metadata['legacy-user-data'])
 451         self.assertEqual(MOCK_RETURNS['cloud-init:user-data'],
 452                          dsrc.userdata_raw)
 453 
 454     def test_sdc_nics(self):
 455         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 456         ret = dsrc.get_data()
 457         self.assertTrue(ret)
 458         self.assertEqual(json.loads(MOCK_RETURNS['sdc:nics']),
 459                          dsrc.metadata['network-data'])
 460 
 461     def test_sdc_scripts(self):
 462         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 463         ret = dsrc.get_data()
 464         self.assertTrue(ret)
 465         self.assertEqual(MOCK_RETURNS['user-script'],
 466                          dsrc.metadata['user-script'])
 467 
 468         legacy_script_f = "%s/user-script" % self.legacy_user_d
 469         self.assertTrue(os.path.exists(legacy_script_f))
 470         self.assertTrue(os.path.islink(legacy_script_f))
 471         user_script_perm = oct(os.stat(legacy_script_f)[stat.ST_MODE])[-3:]
 472         self.assertEqual(user_script_perm, '700')
 473 
 474     def test_scripts_shebanged(self):
 475         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 476         ret = dsrc.get_data()
 477         self.assertTrue(ret)
 478         self.assertEqual(MOCK_RETURNS['user-script'],
 479                          dsrc.metadata['user-script'])
 480 
 481         legacy_script_f = "%s/user-script" % self.legacy_user_d
 482         self.assertTrue(os.path.exists(legacy_script_f))
 483         self.assertTrue(os.path.islink(legacy_script_f))
 484         shebang = None
 485         with open(legacy_script_f, 'r') as f:
 486             shebang = f.readlines()[0].strip()
 487         self.assertEqual(shebang, "#!/bin/bash")
 488         user_script_perm = oct(os.stat(legacy_script_f)[stat.ST_MODE])[-3:]
 489         self.assertEqual(user_script_perm, '700')
 490 
 491     def test_scripts_shebang_not_added(self):
 492         """
 493             Test that the SmartOS requirement that plain text scripts
 494             are executable. This test makes sure that plain texts scripts
 495             with out file magic have it added appropriately by cloud-init.
 496         """
 497 
 498         my_returns = MOCK_RETURNS.copy()
 499         my_returns['user-script'] = '\n'.join(['#!/usr/bin/perl',
 500                                                'print("hi")', ''])
 501 
 502         dsrc = self._get_ds(mockdata=my_returns)
 503         ret = dsrc.get_data()
 504         self.assertTrue(ret)
 505         self.assertEqual(my_returns['user-script'],
 506                          dsrc.metadata['user-script'])
 507 
 508         legacy_script_f = "%s/user-script" % self.legacy_user_d
 509         self.assertTrue(os.path.exists(legacy_script_f))
 510         self.assertTrue(os.path.islink(legacy_script_f))
 511         shebang = None
 512         with open(legacy_script_f, 'r') as f:
 513             shebang = f.readlines()[0].strip()
 514         self.assertEqual(shebang, "#!/usr/bin/perl")
 515 
 516     def test_userdata_removed(self):
 517         """
 518             User-data in the SmartOS world is supposed to be written to a file
 519             each and every boot. This tests to make sure that in the event the
 520             legacy user-data is removed, the existing user-data is backed-up
 521             and there is no /var/db/user-data left.
 522         """
 523 
 524         user_data_f = "%s/mdata-user-data" % self.legacy_user_d
 525         with open(user_data_f, 'w') as f:
 526             f.write("PREVIOUS")
 527 
 528         my_returns = MOCK_RETURNS.copy()
 529         del my_returns['user-data']
 530 
 531         dsrc = self._get_ds(mockdata=my_returns)
 532         ret = dsrc.get_data()
 533         self.assertTrue(ret)
 534         self.assertFalse(dsrc.metadata.get('legacy-user-data'))
 535 
 536         found_new = False
 537         for root, _dirs, files in os.walk(self.legacy_user_d):
 538             for name in files:
 539                 name_f = os.path.join(root, name)
 540                 permissions = oct(os.stat(name_f)[stat.ST_MODE])[-3:]
 541                 if re.match(r'.*\/mdata-user-data$', name_f):
 542                     found_new = True
 543                     print(name_f)
 544                     self.assertEqual(permissions, '400')
 545 
 546         self.assertFalse(found_new)
 547 
 548     def test_vendor_data_not_default(self):
 549         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 550         ret = dsrc.get_data()
 551         self.assertTrue(ret)
 552         self.assertEqual(MOCK_RETURNS['sdc:vendor-data'],
 553                          dsrc.metadata['vendor-data'])
 554 
 555     def test_default_vendor_data(self):
 556         my_returns = MOCK_RETURNS.copy()
 557         def_op_script = my_returns['sdc:vendor-data']
 558         del my_returns['sdc:vendor-data']
 559         dsrc = self._get_ds(mockdata=my_returns)
 560         ret = dsrc.get_data()
 561         self.assertTrue(ret)
 562         self.assertNotEqual(def_op_script, dsrc.metadata['vendor-data'])
 563 
 564         # we expect default vendor-data is a boothook
 565         self.assertTrue(dsrc.vendordata_raw.startswith("#cloud-boothook"))
 566 
 567     def test_disable_iptables_flag(self):
 568         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 569         ret = dsrc.get_data()
 570         self.assertTrue(ret)
 571         self.assertEqual(MOCK_RETURNS['disable_iptables_flag'],
 572                          dsrc.metadata['iptables_disable'])
 573 
 574     def test_motd_sys_info(self):
 575         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 576         ret = dsrc.get_data()
 577         self.assertTrue(ret)
 578         self.assertEqual(MOCK_RETURNS['enable_motd_sys_info'],
 579                          dsrc.metadata['motd_sys_info'])
 580 
 581     def test_default_ephemeral(self):
 582         # Test to make sure that the builtin config has the ephemeral
 583         # configuration.
 584         dsrc = self._get_ds()
 585         cfg = dsrc.get_config_obj()
 586 
 587         ret = dsrc.get_data()
 588         self.assertTrue(ret)
 589 
 590         assert 'disk_setup' in cfg
 591         assert 'fs_setup' in cfg
 592         self.assertIsInstance(cfg['disk_setup'], dict)
 593         self.assertIsInstance(cfg['fs_setup'], list)
 594 
 595     def test_override_disk_aliases(self):
 596         # Test to make sure that the built-in DS is overriden
 597         builtin = DataSourceSmartOS.BUILTIN_DS_CONFIG
 598 
 599         mydscfg = {'disk_aliases': {'FOO': '/dev/bar'}}
 600 
 601         # expect that these values are in builtin, or this is pointless
 602         for k in mydscfg:
 603             self.assertIn(k, builtin)
 604 
 605         dsrc = self._get_ds(ds_cfg=mydscfg)
 606         ret = dsrc.get_data()
 607         self.assertTrue(ret)
 608 
 609         self.assertEqual(mydscfg['disk_aliases']['FOO'],
 610                          dsrc.ds_cfg['disk_aliases']['FOO'])
 611 
 612         self.assertEqual(dsrc.device_name_to_device('FOO'),
 613                          mydscfg['disk_aliases']['FOO'])
 614 
 615 
 616 class TestJoyentMetadataClient(FilesystemMockingTestCase):
 617 
 618     def setUp(self):
 619         super(TestJoyentMetadataClient, self).setUp()
 620 
 621         self.serial = mock.MagicMock(spec=serial.Serial)
 622         self.request_id = 0xabcdef12
 623         self.metadata_value = 'value'
 624         self.response_parts = {
 625             'command': 'SUCCESS',
 626             'crc': 'b5a9ff00',
 627             'length': 17 + len(b64e(self.metadata_value)),
 628             'payload': b64e(self.metadata_value),
 629             'request_id': '{0:08x}'.format(self.request_id),
 630         }
 631 
 632         def make_response():
 633             payloadstr = ''
 634             if 'payload' in self.response_parts:
 635                 payloadstr = ' {0}'.format(self.response_parts['payload'])
 636             return ('V2 {length} {crc} {request_id} '
 637                     '{command}{payloadstr}\n'.format(
 638                         payloadstr=payloadstr,
 639                         **self.response_parts).encode('ascii'))
 640 
 641         self.metasource_data = None
 642 
 643         def read_response(length):
 644             if not self.metasource_data:
 645                 self.metasource_data = make_response()
 646                 self.metasource_data_len = len(self.metasource_data)
 647             resp = self.metasource_data[:length]
 648             self.metasource_data = self.metasource_data[length:]
 649             return resp
 650 
 651         self.serial.read.side_effect = read_response
 652         self.patched_funcs.enter_context(
 653             mock.patch('cloudinit.sources.DataSourceSmartOS.random.randint',
 654                        mock.Mock(return_value=self.request_id)))
 655 
 656     def _get_client(self):
 657         return DataSourceSmartOS.JoyentMetadataClient(
 658             fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM)
 659 
 660     def assertEndsWith(self, haystack, prefix):
 661         self.assertTrue(haystack.endswith(prefix),
 662                         "{0} does not end with '{1}'".format(
 663                             repr(haystack), prefix))
 664 
 665     def assertStartsWith(self, haystack, prefix):
 666         self.assertTrue(haystack.startswith(prefix),
 667                         "{0} does not start with '{1}'".format(
 668                             repr(haystack), prefix))
 669 
 670     def test_get_metadata_writes_a_single_line(self):
 671         client = self._get_client()
 672         client.get('some_key')
 673         self.assertEqual(1, self.serial.write.call_count)
 674         written_line = self.serial.write.call_args[0][0]
 675         print(type(written_line))
 676         self.assertEndsWith(written_line.decode('ascii'),
 677                             b'\n'.decode('ascii'))
 678         self.assertEqual(1, written_line.count(b'\n'))
 679 
 680     def _get_written_line(self, key='some_key'):
 681         client = self._get_client()
 682         client.get(key)
 683         return self.serial.write.call_args[0][0]
 684 
 685     def test_get_metadata_writes_bytes(self):
 686         self.assertIsInstance(self._get_written_line(), six.binary_type)
 687 
 688     def test_get_metadata_line_starts_with_v2(self):
 689         foo = self._get_written_line()
 690         self.assertStartsWith(foo.decode('ascii'), b'V2'.decode('ascii'))
 691 
 692     def test_get_metadata_uses_get_command(self):
 693         parts = self._get_written_line().decode('ascii').strip().split(' ')
 694         self.assertEqual('GET', parts[4])
 695 
 696     def test_get_metadata_base64_encodes_argument(self):
 697         key = 'my_key'
 698         parts = self._get_written_line(key).decode('ascii').strip().split(' ')
 699         self.assertEqual(b64e(key), parts[5])
 700 
 701     def test_get_metadata_calculates_length_correctly(self):
 702         parts = self._get_written_line().decode('ascii').strip().split(' ')
 703         expected_length = len(' '.join(parts[3:]))
 704         self.assertEqual(expected_length, int(parts[1]))
 705 
 706     def test_get_metadata_uses_appropriate_request_id(self):
 707         parts = self._get_written_line().decode('ascii').strip().split(' ')
 708         request_id = parts[3]
 709         self.assertEqual(8, len(request_id))
 710         self.assertEqual(request_id, request_id.lower())
 711 
 712     def test_get_metadata_uses_random_number_for_request_id(self):
 713         line = self._get_written_line()
 714         request_id = line.decode('ascii').strip().split(' ')[3]
 715         self.assertEqual('{0:08x}'.format(self.request_id), request_id)
 716 
 717     def test_get_metadata_checksums_correctly(self):
 718         parts = self._get_written_line().decode('ascii').strip().split(' ')
 719         expected_checksum = '{0:08x}'.format(
 720             crc32(' '.join(parts[3:]).encode('utf-8')) & 0xffffffff)
 721         checksum = parts[2]
 722         self.assertEqual(expected_checksum, checksum)
 723 
 724     def test_get_metadata_reads_a_line(self):
 725         client = self._get_client()
 726         client.get('some_key')
 727         self.assertEqual(self.metasource_data_len, self.serial.read.call_count)
 728 
 729     def test_get_metadata_returns_valid_value(self):
 730         client = self._get_client()
 731         value = client.get('some_key')
 732         self.assertEqual(self.metadata_value, value)
 733 
 734     def test_get_metadata_throws_exception_for_incorrect_length(self):
 735         self.response_parts['length'] = 0
 736         client = self._get_client()
 737         self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
 738                           client.get, 'some_key')
 739 
 740     def test_get_metadata_throws_exception_for_incorrect_crc(self):
 741         self.response_parts['crc'] = 'deadbeef'
 742         client = self._get_client()
 743         self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
 744                           client.get, 'some_key')
 745 
 746     def test_get_metadata_throws_exception_for_request_id_mismatch(self):
 747         self.response_parts['request_id'] = 'deadbeef'
 748         client = self._get_client()
 749         client._checksum = lambda _: self.response_parts['crc']
 750         self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
 751                           client.get, 'some_key')
 752 
 753     def test_get_metadata_returns_None_if_value_not_found(self):
 754         self.response_parts['payload'] = ''
 755         self.response_parts['command'] = 'NOTFOUND'
 756         self.response_parts['length'] = 17
 757         client = self._get_client()
 758         client._checksum = lambda _: self.response_parts['crc']
 759         self.assertIsNone(client.get('some_key'))
 760 
 761 
 762 class TestNetworkConversion(TestCase):
 763     def test_convert_simple(self):
 764         expected = {
 765             'version': 1,
 766             'config': [
 767                 {'name': 'net0', 'type': 'physical',
 768                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 769                               'address': '8.12.42.102/24'}],
 770                  'mtu': 1500, 'mac_address': '90:b8:d0:f5:e4:f5'},
 771                 {'name': 'net1', 'type': 'physical',
 772                  'subnets': [{'type': 'static',
 773                               'address': '192.168.128.93/22'}],
 774                  'mtu': 8500, 'mac_address': '90:b8:d0:a5:ff:cd'}]}
 775         found = convert_net(SDC_NICS)
 776         self.assertEqual(expected, found)
 777 
 778     def test_convert_simple_alt(self):
 779         expected = {
 780             'version': 1,
 781             'config': [
 782                 {'name': 'net0', 'type': 'physical',
 783                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 784                               'address': '8.12.42.51/24'}],
 785                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 786                 {'name': 'net1', 'type': 'physical',
 787                  'subnets': [{'type': 'static',
 788                               'address': '10.210.1.217/24'}],
 789                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 790         found = convert_net(SDC_NICS_ALT)
 791         self.assertEqual(expected, found)
 792 
 793     def test_convert_simple_dhcp(self):
 794         expected = {
 795             'version': 1,
 796             'config': [
 797                 {'name': 'net0', 'type': 'physical',
 798                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 799                               'address': '8.12.42.51/24'}],
 800                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 801                 {'name': 'net1', 'type': 'physical',
 802                  'subnets': [{'type': 'dhcp4'}],
 803                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 804         found = convert_net(SDC_NICS_DHCP)
 805         self.assertEqual(expected, found)
 806 
 807     def test_convert_simple_multi_ip(self):
 808         expected = {
 809             'version': 1,
 810             'config': [
 811                 {'name': 'net0', 'type': 'physical',
 812                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 813                               'address': '8.12.42.51/24'},
 814                              {'type': 'static',
 815                               'address': '8.12.42.52/24'}],
 816                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 817                 {'name': 'net1', 'type': 'physical',
 818                  'subnets': [{'type': 'static',
 819                               'address': '10.210.1.217/24'},
 820                              {'type': 'static',
 821                               'address': '10.210.1.151/24'}],
 822                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 823         found = convert_net(SDC_NICS_MIP)
 824         self.assertEqual(expected, found)
 825 
 826     def test_convert_with_dns(self):
 827         expected = {
 828             'version': 1,
 829             'config': [
 830                 {'name': 'net0', 'type': 'physical',
 831                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 832                               'address': '8.12.42.51/24'}],
 833                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 834                 {'name': 'net1', 'type': 'physical',
 835                  'subnets': [{'type': 'dhcp4'}],
 836                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'},
 837                 {'type': 'nameserver',
 838                  'address': ['8.8.8.8', '8.8.8.1'], 'search': ["local"]}]}
 839         found = convert_net(
 840             network_data=SDC_NICS_DHCP, dns_servers=['8.8.8.8', '8.8.8.1'],
 841             dns_domain="local")
 842         self.assertEqual(expected, found)
 843 
 844     def test_convert_simple_multi_ipv6(self):
 845         expected = {
 846             'version': 1,
 847             'config': [
 848                 {'name': 'net0', 'type': 'physical',
 849                  'subnets': [{'type': 'static', 'address':
 850                               '2001:4800:78ff:1b:be76:4eff:fe06:96b3/64'},
 851                              {'type': 'static', 'gateway': '8.12.42.1',
 852                               'address': '8.12.42.51/24'}],
 853                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 854                 {'name': 'net1', 'type': 'physical',
 855                  'subnets': [{'type': 'static',
 856                               'address': '10.210.1.217/24'}],
 857                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 858         found = convert_net(SDC_NICS_MIP_IPV6)
 859         self.assertEqual(expected, found)
 860 
 861     def test_convert_simple_both_ipv4_ipv6(self):
 862         expected = {
 863             'version': 1,
 864             'config': [
 865                 {'mac_address': '90:b8:d0:ae:64:51', 'mtu': 1500,
 866                  'name': 'net0', 'type': 'physical',
 867                  'subnets': [{'address': '2001::10/64', 'gateway': '2001::1',
 868                               'type': 'static'},
 869                              {'address': '8.12.42.51/24',
 870                               'gateway': '8.12.42.1',
 871                               'type': 'static'},
 872                              {'address': '2001::11/64', 'type': 'static'},
 873                              {'address': '8.12.42.52/32', 'type': 'static'}]},
 874                 {'mac_address': '90:b8:d0:bd:4f:9c', 'mtu': 1500,
 875                  'name': 'net1', 'type': 'physical',
 876                  'subnets': [{'address': '10.210.1.217/24',
 877                               'type': 'static'}]}]}
 878         found = convert_net(SDC_NICS_IPV4_IPV6)
 879         self.assertEqual(expected, found)
 880 
 881     def test_gateways_not_on_all_nics(self):
 882         expected = {
 883             'version': 1,
 884             'config': [
 885                 {'mac_address': '90:b8:d0:d8:82:b4', 'mtu': 1500,
 886                  'name': 'net0', 'type': 'physical',
 887                  'subnets': [{'address': '8.12.42.26/24',
 888                               'gateway': '8.12.42.1', 'type': 'static'}]},
 889                 {'mac_address': '90:b8:d0:0a:51:31', 'mtu': 1500,
 890                  'name': 'net1', 'type': 'physical',
 891                  'subnets': [{'address': '10.210.1.27/24',
 892                               'type': 'static'}]}]}
 893         found = convert_net(SDC_NICS_SINGLE_GATEWAY)
 894         self.assertEqual(expected, found)
 895 
 896 
 897 @unittest.skipUnless(DataSourceSmartOS.get_smartos_environ() ==
 898                      DataSourceSmartOS.SMARTOS_ENV_KVM,
 899                      "Only supported on KVM and bhyve guests under SmartOS")
 900 @unittest.skipUnless(os.access(DataSourceSmartOS.SERIAL_DEVICE, os.W_OK),
 901                      "Requires write access to " +
 902                      DataSourceSmartOS.SERIAL_DEVICE)
 903 class TestSerialConcurrency(TestCase):
 904     """
 905        This class tests locking on an actual serial port, and as such can only
 906        be run in a kvm or bhyve guest running on a SmartOS host.  A test run on
 907        a metadata socket will not be valid because a metadata socket ensures
 908        there is only one session over a connection.  In contrast, in the
 909        absence of proper locking multiple processes opening the same serial
 910        port can corrupt each others' exchanges with the metadata server.
 911     """
 912     def setUp(self):
 913         self.mdata_proc = multiprocessing.Process(target=self.start_mdata_loop)
 914         self.mdata_proc.start()
 915         super(TestSerialConcurrency, self).setUp()
 916 
 917     def tearDown(self):
 918         # os.kill() rather than mdata_proc.terminate() to avoid console spam.
 919         os.kill(self.mdata_proc.pid, signal.SIGKILL)
 920         self.mdata_proc.join()
 921         super(TestSerialConcurrency, self).tearDown()
 922 
 923     def start_mdata_loop(self):
 924         """
 925            The mdata-get command is repeatedly run in a separate process so
 926            that it may try to race with metadata operations performed in the
 927            main test process.  Use of mdata-get is better than two processes
 928            using the protocol implementation in DataSourceSmartOS because we
 929            are testing to be sure that cloud-init and mdata-get respect each
 930            others locks.
 931         """
 932         while True:
 933             try:
 934                 subprocess.check_output(['/usr/sbin/mdata-get', 'sdc:routes'])
 935             except subprocess.CalledProcessError:
 936                 pass
 937 
 938     def test_all_keys(self):
 939         self.assertIsNotNone(self.mdata_proc.pid)
 940         ds = DataSourceSmartOS
 941         keys = [tup[0] for tup in ds.SMARTOS_ATTRIB_MAP.values()]
 942         keys.extend(ds.SMARTOS_ATTRIB_JSON.values())
 943 
 944         client = ds.jmc_client_factory()
 945         self.assertIsNotNone(client)
 946 
 947         # The behavior that we are testing for was observed mdata-get running
 948         # 10 times at roughly the same time as cloud-init fetched each key
 949         # once.  cloud-init would regularly see failures before making it
 950         # through all keys once.
 951         for it in range(0, 3):
 952             for key in keys:
 953                 # We don't care about the return value, just that it doesn't
 954                 # thrown any exceptions.
 955                 client.get(key)
 956 
 957         self.assertIsNone(self.mdata_proc.exitcode)
 958 
 959 # vi: ts=4 expandtab