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