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