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_root_keys(self):
 418         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 419         ret = dsrc.get_data()
 420         self.assertTrue(ret)
 421         self.assertEqual(MOCK_RETURNS['root_authorized_keys'],
 422                          dsrc.metadata['public-keys'])
 423 
 424     def test_hostname_b64(self):
 425         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 426         ret = dsrc.get_data()
 427         self.assertTrue(ret)
 428         self.assertEqual(MOCK_RETURNS['hostname'],
 429                          dsrc.metadata['local-hostname'])
 430 
 431     def test_hostname(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_userdata(self):
 439         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 440         ret = dsrc.get_data()
 441         self.assertTrue(ret)
 442         self.assertEqual(MOCK_RETURNS['user-data'],
 443                          dsrc.metadata['legacy-user-data'])
 444         self.assertEqual(MOCK_RETURNS['cloud-init:user-data'],
 445                          dsrc.userdata_raw)
 446 
 447     def test_sdc_nics(self):
 448         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 449         ret = dsrc.get_data()
 450         self.assertTrue(ret)
 451         self.assertEqual(json.loads(MOCK_RETURNS['sdc:nics']),
 452                          dsrc.metadata['network-data'])
 453 
 454     def test_sdc_scripts(self):
 455         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 456         ret = dsrc.get_data()
 457         self.assertTrue(ret)
 458         self.assertEqual(MOCK_RETURNS['user-script'],
 459                          dsrc.metadata['user-script'])
 460 
 461         legacy_script_f = "%s/user-script" % self.legacy_user_d
 462         self.assertTrue(os.path.exists(legacy_script_f))
 463         self.assertTrue(os.path.islink(legacy_script_f))
 464         user_script_perm = oct(os.stat(legacy_script_f)[stat.ST_MODE])[-3:]
 465         self.assertEqual(user_script_perm, '700')
 466 
 467     def test_scripts_shebanged(self):
 468         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 469         ret = dsrc.get_data()
 470         self.assertTrue(ret)
 471         self.assertEqual(MOCK_RETURNS['user-script'],
 472                          dsrc.metadata['user-script'])
 473 
 474         legacy_script_f = "%s/user-script" % self.legacy_user_d
 475         self.assertTrue(os.path.exists(legacy_script_f))
 476         self.assertTrue(os.path.islink(legacy_script_f))
 477         shebang = None
 478         with open(legacy_script_f, 'r') as f:
 479             shebang = f.readlines()[0].strip()
 480         self.assertEqual(shebang, "#!/bin/bash")
 481         user_script_perm = oct(os.stat(legacy_script_f)[stat.ST_MODE])[-3:]
 482         self.assertEqual(user_script_perm, '700')
 483 
 484     def test_scripts_shebang_not_added(self):
 485         """
 486             Test that the SmartOS requirement that plain text scripts
 487             are executable. This test makes sure that plain texts scripts
 488             with out file magic have it added appropriately by cloud-init.
 489         """
 490 
 491         my_returns = MOCK_RETURNS.copy()
 492         my_returns['user-script'] = '\n'.join(['#!/usr/bin/perl',
 493                                                'print("hi")', ''])
 494 
 495         dsrc = self._get_ds(mockdata=my_returns)
 496         ret = dsrc.get_data()
 497         self.assertTrue(ret)
 498         self.assertEqual(my_returns['user-script'],
 499                          dsrc.metadata['user-script'])
 500 
 501         legacy_script_f = "%s/user-script" % self.legacy_user_d
 502         self.assertTrue(os.path.exists(legacy_script_f))
 503         self.assertTrue(os.path.islink(legacy_script_f))
 504         shebang = None
 505         with open(legacy_script_f, 'r') as f:
 506             shebang = f.readlines()[0].strip()
 507         self.assertEqual(shebang, "#!/usr/bin/perl")
 508 
 509     def test_userdata_removed(self):
 510         """
 511             User-data in the SmartOS world is supposed to be written to a file
 512             each and every boot. This tests to make sure that in the event the
 513             legacy user-data is removed, the existing user-data is backed-up
 514             and there is no /var/db/user-data left.
 515         """
 516 
 517         user_data_f = "%s/mdata-user-data" % self.legacy_user_d
 518         with open(user_data_f, 'w') as f:
 519             f.write("PREVIOUS")
 520 
 521         my_returns = MOCK_RETURNS.copy()
 522         del my_returns['user-data']
 523 
 524         dsrc = self._get_ds(mockdata=my_returns)
 525         ret = dsrc.get_data()
 526         self.assertTrue(ret)
 527         self.assertFalse(dsrc.metadata.get('legacy-user-data'))
 528 
 529         found_new = False
 530         for root, _dirs, files in os.walk(self.legacy_user_d):
 531             for name in files:
 532                 name_f = os.path.join(root, name)
 533                 permissions = oct(os.stat(name_f)[stat.ST_MODE])[-3:]
 534                 if re.match(r'.*\/mdata-user-data$', name_f):
 535                     found_new = True
 536                     print(name_f)
 537                     self.assertEqual(permissions, '400')
 538 
 539         self.assertFalse(found_new)
 540 
 541     def test_vendor_data_not_default(self):
 542         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 543         ret = dsrc.get_data()
 544         self.assertTrue(ret)
 545         self.assertEqual(MOCK_RETURNS['sdc:vendor-data'],
 546                          dsrc.metadata['vendor-data'])
 547 
 548     def test_default_vendor_data(self):
 549         my_returns = MOCK_RETURNS.copy()
 550         def_op_script = my_returns['sdc:vendor-data']
 551         del my_returns['sdc:vendor-data']
 552         dsrc = self._get_ds(mockdata=my_returns)
 553         ret = dsrc.get_data()
 554         self.assertTrue(ret)
 555         self.assertNotEqual(def_op_script, dsrc.metadata['vendor-data'])
 556 
 557         # we expect default vendor-data is a boothook
 558         self.assertTrue(dsrc.vendordata_raw.startswith("#cloud-boothook"))
 559 
 560     def test_disable_iptables_flag(self):
 561         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 562         ret = dsrc.get_data()
 563         self.assertTrue(ret)
 564         self.assertEqual(MOCK_RETURNS['disable_iptables_flag'],
 565                          dsrc.metadata['iptables_disable'])
 566 
 567     def test_motd_sys_info(self):
 568         dsrc = self._get_ds(mockdata=MOCK_RETURNS)
 569         ret = dsrc.get_data()
 570         self.assertTrue(ret)
 571         self.assertEqual(MOCK_RETURNS['enable_motd_sys_info'],
 572                          dsrc.metadata['motd_sys_info'])
 573 
 574     def test_default_ephemeral(self):
 575         # Test to make sure that the builtin config has the ephemeral
 576         # configuration.
 577         dsrc = self._get_ds()
 578         cfg = dsrc.get_config_obj()
 579 
 580         ret = dsrc.get_data()
 581         self.assertTrue(ret)
 582 
 583         assert 'disk_setup' in cfg
 584         assert 'fs_setup' in cfg
 585         self.assertIsInstance(cfg['disk_setup'], dict)
 586         self.assertIsInstance(cfg['fs_setup'], list)
 587 
 588     def test_override_disk_aliases(self):
 589         # Test to make sure that the built-in DS is overriden
 590         builtin = DataSourceSmartOS.BUILTIN_DS_CONFIG
 591 
 592         mydscfg = {'disk_aliases': {'FOO': '/dev/bar'}}
 593 
 594         # expect that these values are in builtin, or this is pointless
 595         for k in mydscfg:
 596             self.assertIn(k, builtin)
 597 
 598         dsrc = self._get_ds(ds_cfg=mydscfg)
 599         ret = dsrc.get_data()
 600         self.assertTrue(ret)
 601 
 602         self.assertEqual(mydscfg['disk_aliases']['FOO'],
 603                          dsrc.ds_cfg['disk_aliases']['FOO'])
 604 
 605         self.assertEqual(dsrc.device_name_to_device('FOO'),
 606                          mydscfg['disk_aliases']['FOO'])
 607 
 608 
 609 class TestJoyentMetadataClient(FilesystemMockingTestCase):
 610 
 611     def setUp(self):
 612         super(TestJoyentMetadataClient, self).setUp()
 613 
 614         self.serial = mock.MagicMock(spec=serial.Serial)
 615         self.request_id = 0xabcdef12
 616         self.metadata_value = 'value'
 617         self.response_parts = {
 618             'command': 'SUCCESS',
 619             'crc': 'b5a9ff00',
 620             'length': 17 + len(b64e(self.metadata_value)),
 621             'payload': b64e(self.metadata_value),
 622             'request_id': '{0:08x}'.format(self.request_id),
 623         }
 624 
 625         def make_response():
 626             payloadstr = ''
 627             if 'payload' in self.response_parts:
 628                 payloadstr = ' {0}'.format(self.response_parts['payload'])
 629             return ('V2 {length} {crc} {request_id} '
 630                     '{command}{payloadstr}\n'.format(
 631                         payloadstr=payloadstr,
 632                         **self.response_parts).encode('ascii'))
 633 
 634         self.metasource_data = None
 635 
 636         def read_response(length):
 637             if not self.metasource_data:
 638                 self.metasource_data = make_response()
 639                 self.metasource_data_len = len(self.metasource_data)
 640             resp = self.metasource_data[:length]
 641             self.metasource_data = self.metasource_data[length:]
 642             return resp
 643 
 644         self.serial.read.side_effect = read_response
 645         self.patched_funcs.enter_context(
 646             mock.patch('cloudinit.sources.DataSourceSmartOS.random.randint',
 647                        mock.Mock(return_value=self.request_id)))
 648 
 649     def _get_client(self):
 650         return DataSourceSmartOS.JoyentMetadataClient(
 651             fp=self.serial, smartos_type=DataSourceSmartOS.SMARTOS_ENV_KVM)
 652 
 653     def assertEndsWith(self, haystack, prefix):
 654         self.assertTrue(haystack.endswith(prefix),
 655                         "{0} does not end with '{1}'".format(
 656                             repr(haystack), prefix))
 657 
 658     def assertStartsWith(self, haystack, prefix):
 659         self.assertTrue(haystack.startswith(prefix),
 660                         "{0} does not start with '{1}'".format(
 661                             repr(haystack), prefix))
 662 
 663     def test_get_metadata_writes_a_single_line(self):
 664         client = self._get_client()
 665         client.get('some_key')
 666         self.assertEqual(1, self.serial.write.call_count)
 667         written_line = self.serial.write.call_args[0][0]
 668         print(type(written_line))
 669         self.assertEndsWith(written_line.decode('ascii'),
 670                             b'\n'.decode('ascii'))
 671         self.assertEqual(1, written_line.count(b'\n'))
 672 
 673     def _get_written_line(self, key='some_key'):
 674         client = self._get_client()
 675         client.get(key)
 676         return self.serial.write.call_args[0][0]
 677 
 678     def test_get_metadata_writes_bytes(self):
 679         self.assertIsInstance(self._get_written_line(), six.binary_type)
 680 
 681     def test_get_metadata_line_starts_with_v2(self):
 682         foo = self._get_written_line()
 683         self.assertStartsWith(foo.decode('ascii'), b'V2'.decode('ascii'))
 684 
 685     def test_get_metadata_uses_get_command(self):
 686         parts = self._get_written_line().decode('ascii').strip().split(' ')
 687         self.assertEqual('GET', parts[4])
 688 
 689     def test_get_metadata_base64_encodes_argument(self):
 690         key = 'my_key'
 691         parts = self._get_written_line(key).decode('ascii').strip().split(' ')
 692         self.assertEqual(b64e(key), parts[5])
 693 
 694     def test_get_metadata_calculates_length_correctly(self):
 695         parts = self._get_written_line().decode('ascii').strip().split(' ')
 696         expected_length = len(' '.join(parts[3:]))
 697         self.assertEqual(expected_length, int(parts[1]))
 698 
 699     def test_get_metadata_uses_appropriate_request_id(self):
 700         parts = self._get_written_line().decode('ascii').strip().split(' ')
 701         request_id = parts[3]
 702         self.assertEqual(8, len(request_id))
 703         self.assertEqual(request_id, request_id.lower())
 704 
 705     def test_get_metadata_uses_random_number_for_request_id(self):
 706         line = self._get_written_line()
 707         request_id = line.decode('ascii').strip().split(' ')[3]
 708         self.assertEqual('{0:08x}'.format(self.request_id), request_id)
 709 
 710     def test_get_metadata_checksums_correctly(self):
 711         parts = self._get_written_line().decode('ascii').strip().split(' ')
 712         expected_checksum = '{0:08x}'.format(
 713             crc32(' '.join(parts[3:]).encode('utf-8')) & 0xffffffff)
 714         checksum = parts[2]
 715         self.assertEqual(expected_checksum, checksum)
 716 
 717     def test_get_metadata_reads_a_line(self):
 718         client = self._get_client()
 719         client.get('some_key')
 720         self.assertEqual(self.metasource_data_len, self.serial.read.call_count)
 721 
 722     def test_get_metadata_returns_valid_value(self):
 723         client = self._get_client()
 724         value = client.get('some_key')
 725         self.assertEqual(self.metadata_value, value)
 726 
 727     def test_get_metadata_throws_exception_for_incorrect_length(self):
 728         self.response_parts['length'] = 0
 729         client = self._get_client()
 730         self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
 731                           client.get, 'some_key')
 732 
 733     def test_get_metadata_throws_exception_for_incorrect_crc(self):
 734         self.response_parts['crc'] = 'deadbeef'
 735         client = self._get_client()
 736         self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
 737                           client.get, 'some_key')
 738 
 739     def test_get_metadata_throws_exception_for_request_id_mismatch(self):
 740         self.response_parts['request_id'] = 'deadbeef'
 741         client = self._get_client()
 742         client._checksum = lambda _: self.response_parts['crc']
 743         self.assertRaises(DataSourceSmartOS.JoyentMetadataFetchException,
 744                           client.get, 'some_key')
 745 
 746     def test_get_metadata_returns_None_if_value_not_found(self):
 747         self.response_parts['payload'] = ''
 748         self.response_parts['command'] = 'NOTFOUND'
 749         self.response_parts['length'] = 17
 750         client = self._get_client()
 751         client._checksum = lambda _: self.response_parts['crc']
 752         self.assertIsNone(client.get('some_key'))
 753 
 754 
 755 class TestNetworkConversion(TestCase):
 756     def test_convert_simple(self):
 757         expected = {
 758             'version': 1,
 759             'config': [
 760                 {'name': 'net0', 'type': 'physical',
 761                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 762                               'address': '8.12.42.102/24'}],
 763                  'mtu': 1500, 'mac_address': '90:b8:d0:f5:e4:f5'},
 764                 {'name': 'net1', 'type': 'physical',
 765                  'subnets': [{'type': 'static',
 766                               'address': '192.168.128.93/22'}],
 767                  'mtu': 8500, 'mac_address': '90:b8:d0:a5:ff:cd'}]}
 768         found = convert_net(SDC_NICS)
 769         self.assertEqual(expected, found)
 770 
 771     def test_convert_simple_alt(self):
 772         expected = {
 773             'version': 1,
 774             'config': [
 775                 {'name': 'net0', 'type': 'physical',
 776                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 777                               'address': '8.12.42.51/24'}],
 778                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 779                 {'name': 'net1', 'type': 'physical',
 780                  'subnets': [{'type': 'static',
 781                               'address': '10.210.1.217/24'}],
 782                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 783         found = convert_net(SDC_NICS_ALT)
 784         self.assertEqual(expected, found)
 785 
 786     def test_convert_simple_dhcp(self):
 787         expected = {
 788             'version': 1,
 789             'config': [
 790                 {'name': 'net0', 'type': 'physical',
 791                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 792                               'address': '8.12.42.51/24'}],
 793                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 794                 {'name': 'net1', 'type': 'physical',
 795                  'subnets': [{'type': 'dhcp4'}],
 796                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 797         found = convert_net(SDC_NICS_DHCP)
 798         self.assertEqual(expected, found)
 799 
 800     def test_convert_simple_multi_ip(self):
 801         expected = {
 802             'version': 1,
 803             'config': [
 804                 {'name': 'net0', 'type': 'physical',
 805                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 806                               'address': '8.12.42.51/24'},
 807                              {'type': 'static',
 808                               'address': '8.12.42.52/24'}],
 809                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 810                 {'name': 'net1', 'type': 'physical',
 811                  'subnets': [{'type': 'static',
 812                               'address': '10.210.1.217/24'},
 813                              {'type': 'static',
 814                               'address': '10.210.1.151/24'}],
 815                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 816         found = convert_net(SDC_NICS_MIP)
 817         self.assertEqual(expected, found)
 818 
 819     def test_convert_with_dns(self):
 820         expected = {
 821             'version': 1,
 822             'config': [
 823                 {'name': 'net0', 'type': 'physical',
 824                  'subnets': [{'type': 'static', 'gateway': '8.12.42.1',
 825                               'address': '8.12.42.51/24'}],
 826                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 827                 {'name': 'net1', 'type': 'physical',
 828                  'subnets': [{'type': 'dhcp4'}],
 829                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'},
 830                 {'type': 'nameserver',
 831                  'address': ['8.8.8.8', '8.8.8.1'], 'search': ["local"]}]}
 832         found = convert_net(
 833             network_data=SDC_NICS_DHCP, dns_servers=['8.8.8.8', '8.8.8.1'],
 834             dns_domain="local")
 835         self.assertEqual(expected, found)
 836 
 837     def test_convert_simple_multi_ipv6(self):
 838         expected = {
 839             'version': 1,
 840             'config': [
 841                 {'name': 'net0', 'type': 'physical',
 842                  'subnets': [{'type': 'static', 'address':
 843                               '2001:4800:78ff:1b:be76:4eff:fe06:96b3/64'},
 844                              {'type': 'static', 'gateway': '8.12.42.1',
 845                               'address': '8.12.42.51/24'}],
 846                  'mtu': 1500, 'mac_address': '90:b8:d0:ae:64:51'},
 847                 {'name': 'net1', 'type': 'physical',
 848                  'subnets': [{'type': 'static',
 849                               'address': '10.210.1.217/24'}],
 850                  'mtu': 1500, 'mac_address': '90:b8:d0:bd:4f:9c'}]}
 851         found = convert_net(SDC_NICS_MIP_IPV6)
 852         self.assertEqual(expected, found)
 853 
 854     def test_convert_simple_both_ipv4_ipv6(self):
 855         expected = {
 856             'version': 1,
 857             'config': [
 858                 {'mac_address': '90:b8:d0:ae:64:51', 'mtu': 1500,
 859                  'name': 'net0', 'type': 'physical',
 860                  'subnets': [{'address': '2001::10/64', 'gateway': '2001::1',
 861                               'type': 'static'},
 862                              {'address': '8.12.42.51/24',
 863                               'gateway': '8.12.42.1',
 864                               'type': 'static'},
 865                              {'address': '2001::11/64', 'type': 'static'},
 866                              {'address': '8.12.42.52/32', 'type': 'static'}]},
 867                 {'mac_address': '90:b8:d0:bd:4f:9c', 'mtu': 1500,
 868                  'name': 'net1', 'type': 'physical',
 869                  'subnets': [{'address': '10.210.1.217/24',
 870                               'type': 'static'}]}]}
 871         found = convert_net(SDC_NICS_IPV4_IPV6)
 872         self.assertEqual(expected, found)
 873 
 874     def test_gateways_not_on_all_nics(self):
 875         expected = {
 876             'version': 1,
 877             'config': [
 878                 {'mac_address': '90:b8:d0:d8:82:b4', 'mtu': 1500,
 879                  'name': 'net0', 'type': 'physical',
 880                  'subnets': [{'address': '8.12.42.26/24',
 881                               'gateway': '8.12.42.1', 'type': 'static'}]},
 882                 {'mac_address': '90:b8:d0:0a:51:31', 'mtu': 1500,
 883                  'name': 'net1', 'type': 'physical',
 884                  'subnets': [{'address': '10.210.1.27/24',
 885                               'type': 'static'}]}]}
 886         found = convert_net(SDC_NICS_SINGLE_GATEWAY)
 887         self.assertEqual(expected, found)
 888 
 889 
 890 @unittest.skipUnless(DataSourceSmartOS.get_smartos_environ() ==
 891                      DataSourceSmartOS.SMARTOS_ENV_KVM,
 892                      "Only supported on KVM and bhyve guests under SmartOS")
 893 @unittest.skipUnless(os.access(DataSourceSmartOS.SERIAL_DEVICE, os.W_OK),
 894                      "Requires write access to " +
 895                      DataSourceSmartOS.SERIAL_DEVICE)
 896 class TestSerialConcurrency(TestCase):
 897     """
 898        This class tests locking on an actual serial port, and as such can only
 899        be run in a kvm or bhyve guest running on a SmartOS host.  A test run on
 900        a metadata socket will not be valid because a metadata socket ensures
 901        there is only one session over a connection.  In contrast, in the
 902        absence of proper locking multiple processes opening the same serial
 903        port can corrupt each others' exchanges with the metadata server.
 904     """
 905     def setUp(self):
 906         self.mdata_proc = multiprocessing.Process(target=self.start_mdata_loop)
 907         self.mdata_proc.start()
 908         super(TestSerialConcurrency, self).setUp()
 909 
 910     def tearDown(self):
 911         # os.kill() rather than mdata_proc.terminate() to avoid console spam.
 912         os.kill(self.mdata_proc.pid, signal.SIGKILL)
 913         self.mdata_proc.join()
 914         super(TestSerialConcurrency, self).tearDown()
 915 
 916     def start_mdata_loop(self):
 917         """
 918            The mdata-get command is repeatedly run in a separate process so
 919            that it may try to race with metadata operations performed in the
 920            main test process.  Use of mdata-get is better than two processes
 921            using the protocol implementation in DataSourceSmartOS because we
 922            are testing to be sure that cloud-init and mdata-get respect each
 923            others locks.
 924         """
 925         while True:
 926             try:
 927                 subprocess.check_output(['/usr/sbin/mdata-get', 'sdc:routes'])
 928             except subprocess.CalledProcessError:
 929                 pass
 930 
 931     def test_all_keys(self):
 932         self.assertIsNotNone(self.mdata_proc.pid)
 933         ds = DataSourceSmartOS
 934         keys = [tup[0] for tup in ds.SMARTOS_ATTRIB_MAP.values()]
 935         keys.extend(ds.SMARTOS_ATTRIB_JSON.values())
 936 
 937         client = ds.jmc_client_factory()
 938         self.assertIsNotNone(client)
 939 
 940         # The behavior that we are testing for was observed mdata-get running
 941         # 10 times at roughly the same time as cloud-init fetched each key
 942         # once.  cloud-init would regularly see failures before making it
 943         # through all keys once.
 944         for it in range(0, 3):
 945             for key in keys:
 946                 # We don't care about the return value, just that it doesn't
 947                 # thrown any exceptions.
 948                 client.get(key)
 949 
 950         self.assertIsNone(self.mdata_proc.exitcode)
 951 
 952 # vi: ts=4 expandtab