5502: Node Manager attaches a local SSD to GCE compute nodes.
[arvados.git] / services / nodemanager / tests / test_computenode_driver_gce.py
1 #!/usr/bin/env python
2
3 from __future__ import absolute_import, print_function
4
5 import json
6 import time
7 import unittest
8
9 import mock
10
11 import arvnodeman.computenode.driver.gce as gce
12 from . import testutil
13
14 class GCEComputeNodeDriverTestCase(testutil.DriverTestMixin, unittest.TestCase):
15     TEST_CLASS = gce.ComputeNodeDriver
16
17     def setUp(self):
18         super(GCEComputeNodeDriverTestCase, self).setUp()
19         self.driver_mock().list_images.return_value = [
20             testutil.cloud_object_mock('testimage', selfLink='image-link')]
21         self.driver_mock().ex_list_disktypes.return_value = [
22             testutil.cloud_object_mock(name, selfLink=name + '-link')
23             for name in ['pd-standard', 'pd-ssd', 'local-ssd']]
24         self.driver_mock.reset_mock()
25
26     def new_driver(self, auth_kwargs={}, list_kwargs={}, create_kwargs={}):
27         create_kwargs.setdefault('image', 'testimage')
28         return super(GCEComputeNodeDriverTestCase, self).new_driver(
29             auth_kwargs, list_kwargs, create_kwargs)
30
31     def test_driver_instantiation(self):
32         kwargs = {'user_id': 'foo'}
33         driver = self.new_driver(auth_kwargs=kwargs)
34         self.assertTrue(self.driver_mock.called)
35         self.assertEqual(kwargs, self.driver_mock.call_args[1])
36
37     def test_create_image_loaded_at_initialization_by_name(self):
38         image_mocks = [testutil.cloud_object_mock(c) for c in 'abc']
39         list_method = self.driver_mock().list_images
40         list_method.return_value = image_mocks
41         driver = self.new_driver(create_kwargs={'image': 'b'})
42         self.assertEqual(1, list_method.call_count)
43
44     def test_create_includes_ping_secret(self):
45         arv_node = testutil.arvados_node_mock(info={'ping_secret': 'ssshh'})
46         driver = self.new_driver()
47         driver.create_node(testutil.MockSize(1), arv_node)
48         metadata = self.driver_mock().create_node.call_args[1]['ex_metadata']
49         self.assertIn('ping_secret=ssshh', metadata.get('arv-ping-url'))
50
51     def test_create_sets_default_hostname(self):
52         driver = self.new_driver()
53         driver.create_node(testutil.MockSize(1),
54                            testutil.arvados_node_mock(254, hostname=None))
55         create_kwargs = self.driver_mock().create_node.call_args[1]
56         self.assertEqual('compute-0000000000000fe-zzzzz',
57                          create_kwargs.get('name'))
58         self.assertEqual('dynamic.compute.zzzzz.arvadosapi.com',
59                          create_kwargs.get('ex_metadata', {}).get('hostname'))
60
61     def test_create_tags_from_list_tags(self):
62         driver = self.new_driver(list_kwargs={'tags': 'testA, testB'})
63         driver.create_node(testutil.MockSize(1), testutil.arvados_node_mock())
64         self.assertEqual(['testA', 'testB'],
65                          self.driver_mock().create_node.call_args[1]['ex_tags'])
66
67     def test_create_with_two_disks_attached(self):
68         driver = self.new_driver(create_kwargs={'image': 'testimage'})
69         driver.create_node(testutil.MockSize(1), testutil.arvados_node_mock())
70         create_disks = self.driver_mock().create_node.call_args[1].get(
71             'ex_disks_gce_struct', [])
72         self.assertEqual(2, len(create_disks))
73         self.assertTrue(create_disks[0].get('autoDelete'))
74         self.assertTrue(create_disks[0].get('boot'))
75         self.assertEqual('PERSISTENT', create_disks[0].get('type'))
76         init_params = create_disks[0].get('initializeParams', {})
77         self.assertEqual('pd-standard-link', init_params.get('diskType'))
78         self.assertEqual('image-link', init_params.get('sourceImage'))
79         # Our node images expect the SSD to be named `tmp` to find and mount it.
80         self.assertEqual('tmp', create_disks[1].get('deviceName'))
81         self.assertTrue(create_disks[1].get('autoDelete'))
82         self.assertFalse(create_disks[1].get('boot', 'unset'))
83         self.assertEqual('SCRATCH', create_disks[1].get('type'))
84         init_params = create_disks[1].get('initializeParams', {})
85         self.assertEqual('local-ssd-link', init_params.get('diskType'))
86
87     def test_list_nodes_requires_tags_match(self):
88         # A node matches if our list tags are a subset of the node's tags.
89         # Test behavior with no tags, no match, partial matches, different
90         # order, and strict supersets.
91         cloud_mocks = [
92             testutil.cloud_node_mock(node_num, tags=tag_set)
93             for node_num, tag_set in enumerate(
94                 [[], ['bad'], ['good'], ['great'], ['great', 'ok'],
95                  ['great', 'good'], ['good', 'fantastic', 'great']])]
96         cloud_mocks.append(testutil.cloud_node_mock())
97         self.driver_mock().list_nodes.return_value = cloud_mocks
98         driver = self.new_driver(list_kwargs={'tags': 'good, great'})
99         self.assertItemsEqual(['5', '6'], [n.id for n in driver.list_nodes()])
100
101     def build_gce_metadata(self, metadata_dict):
102         # Convert a plain metadata dictionary to the GCE data structure.
103         return {
104             'kind': 'compute#metadata',
105             'fingerprint': 'testprint',
106             'items': [{'key': key, 'value': metadata_dict[key]}
107                       for key in metadata_dict],
108             }
109
110     def check_sync_node_updates_hostname_tag(self, plain_metadata):
111         start_metadata = self.build_gce_metadata(plain_metadata)
112         arv_node = testutil.arvados_node_mock(1)
113         cloud_node = testutil.cloud_node_mock(
114             2, metadata=start_metadata.copy(),
115             zone=testutil.cloud_object_mock('testzone'))
116         driver = self.new_driver()
117         driver.sync_node(cloud_node, arv_node)
118         args, kwargs = self.driver_mock().connection.async_request.call_args
119         self.assertEqual('/zones/testzone/instances/2/setMetadata', args[0])
120         for key in ['kind', 'fingerprint']:
121             self.assertEqual(start_metadata[key], kwargs['data'][key])
122         plain_metadata['hostname'] = 'compute1.zzzzz.arvadosapi.com'
123         self.assertEqual(
124             plain_metadata,
125             {item['key']: item['value'] for item in kwargs['data']['items']})
126
127     def test_sync_node_updates_hostname_tag(self):
128         self.check_sync_node_updates_hostname_tag(
129             {'testkey': 'testvalue', 'hostname': 'startvalue'})
130
131     def test_sync_node_adds_hostname_tag(self):
132         self.check_sync_node_updates_hostname_tag({'testkey': 'testval'})
133
134     def test_sync_node_raises_exception_on_failure(self):
135         arv_node = testutil.arvados_node_mock(8)
136         cloud_node = testutil.cloud_node_mock(
137             9, metadata={}, zone=testutil.cloud_object_mock('failzone'))
138         mock_response = self.driver_mock().connection.async_request()
139         mock_response.success.return_value = False
140         mock_response.error = 'sync error test'
141         driver = self.new_driver()
142         with self.assertRaises(Exception) as err_check:
143             driver.sync_node(cloud_node, arv_node)
144         self.assertIs(err_check.exception.__class__, Exception)
145         self.assertIn('sync error test', str(err_check.exception))
146
147     def test_node_create_time_zero_for_unknown_nodes(self):
148         node = testutil.cloud_node_mock()
149         self.assertEqual(0, gce.ComputeNodeDriver.node_start_time(node))
150
151     def test_node_create_time_for_known_node(self):
152         node = testutil.cloud_node_mock(metadata=self.build_gce_metadata(
153                 {'booted_at': '1970-01-01T00:01:05Z'}))
154         self.assertEqual(65, gce.ComputeNodeDriver.node_start_time(node))
155
156     def test_node_create_time_recorded_when_node_boots(self):
157         start_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
158         arv_node = testutil.arvados_node_mock()
159         driver = self.new_driver()
160         driver.create_node(testutil.MockSize(1), arv_node)
161         metadata = self.driver_mock().create_node.call_args[1]['ex_metadata']
162         self.assertLessEqual(start_time, metadata.get('booted_at'))
163
164     def test_known_node_fqdn(self):
165         name = 'fqdntest.zzzzz.arvadosapi.com'
166         node = testutil.cloud_node_mock(metadata=self.build_gce_metadata(
167                 {'hostname': name}))
168         self.assertEqual(name, gce.ComputeNodeDriver.node_fqdn(node))
169
170     def test_unknown_node_fqdn(self):
171         # Return an empty string.  This lets fqdn be safely compared
172         # against an expected value, and ComputeNodeMonitorActor
173         # should try to update it.
174         node = testutil.cloud_node_mock(metadata=self.build_gce_metadata({}))
175         self.assertEqual('', gce.ComputeNodeDriver.node_fqdn(node))
176
177     def test_deliver_ssh_key_in_metadata(self):
178         test_ssh_key = 'ssh-rsa-foo'
179         arv_node = testutil.arvados_node_mock(1)
180         with mock.patch('__builtin__.open',
181                         mock.mock_open(read_data=test_ssh_key)) as mock_file:
182             driver = self.new_driver(create_kwargs={'ssh_key': 'ssh-key-file'})
183         mock_file.assert_called_once_with('ssh-key-file')
184         driver.create_node(testutil.MockSize(1), arv_node)
185         metadata = self.driver_mock().create_node.call_args[1]['ex_metadata']
186         self.assertEqual('root:ssh-rsa-foo', metadata.get('sshKeys'))
187
188     def test_create_driver_with_service_accounts(self):
189         service_accounts = {'email': 'foo@bar', 'scopes': ['storage-full']}
190         srv_acct_config = {'service_accounts': json.dumps(service_accounts)}
191         arv_node = testutil.arvados_node_mock(1)
192         driver = self.new_driver(create_kwargs=srv_acct_config)
193         driver.create_node(testutil.MockSize(1), arv_node)
194         self.assertEqual(
195             service_accounts,
196             self.driver_mock().create_node.call_args[1]['ex_service_accounts'])