15107: Add LoginCluster test.
[arvados.git] / services / nodemanager / tests / test_computenode_driver_gce.py
1 #!/usr/bin/env python
2 # Copyright (C) The Arvados Authors. All rights reserved.
3 #
4 # SPDX-License-Identifier: AGPL-3.0
5
6 from __future__ import absolute_import, print_function
7
8 import json
9 import time
10 import unittest
11
12 import mock
13
14 import arvnodeman.computenode.driver.gce as gce
15 from . import testutil
16
17 class GCEComputeNodeDriverTestCase(testutil.DriverTestMixin, unittest.TestCase):
18     TEST_CLASS = gce.ComputeNodeDriver
19
20     def setUp(self):
21         super(GCEComputeNodeDriverTestCase, self).setUp()
22         self.driver_mock().list_images.return_value = [
23             testutil.cloud_object_mock('testimage', selfLink='image-link')]
24         self.driver_mock().ex_list_disktypes.return_value = [
25             testutil.cloud_object_mock(name, selfLink=name + '-link')
26             for name in ['pd-standard', 'pd-ssd', 'local-ssd']]
27         self.driver_mock.reset_mock()
28
29     def new_driver(self, auth_kwargs={}, list_kwargs={}, create_kwargs={}):
30         create_kwargs.setdefault('image', 'testimage')
31         return super(GCEComputeNodeDriverTestCase, self).new_driver(
32             auth_kwargs, list_kwargs, create_kwargs)
33
34     def test_driver_instantiation(self):
35         kwargs = {'user_id': 'foo'}
36         driver = self.new_driver(auth_kwargs=kwargs)
37         self.assertTrue(self.driver_mock.called)
38         self.assertEqual(kwargs, self.driver_mock.call_args[1])
39
40     def test_create_image_loaded_at_initialization_by_name(self):
41         image_mocks = [testutil.cloud_object_mock(c) for c in 'abc']
42         list_method = self.driver_mock().list_images
43         list_method.return_value = image_mocks
44         driver = self.new_driver(create_kwargs={'image': 'b'})
45         self.assertEqual(1, list_method.call_count)
46
47     def test_create_includes_ping_secret(self):
48         arv_node = testutil.arvados_node_mock(info={'ping_secret': 'ssshh'})
49         driver = self.new_driver()
50         driver.create_node(testutil.MockSize(1), arv_node)
51         metadata = self.driver_mock().create_node.call_args[1]['ex_metadata']
52         self.assertIn('ping_secret=ssshh', metadata.get('arv-ping-url'))
53
54     def test_create_includes_arvados_node_size(self):
55         arv_node = testutil.arvados_node_mock()
56         size = testutil.MockSize(1)
57         driver = self.new_driver()
58         driver.create_node(size, arv_node)
59         create_method = self.driver_mock().create_node
60         self.assertIn(
61             ('arvados_node_size', size.id),
62             create_method.call_args[1].get('ex_metadata', {'metadata':'missing'}).items()
63         )
64
65     def test_create_raises_but_actually_succeeded(self):
66         arv_node = testutil.arvados_node_mock(1, hostname=None)
67         driver = self.new_driver()
68         nodelist = [testutil.cloud_node_mock(1)]
69         nodelist[0].name = 'compute-000000000000001-zzzzz'
70         self.driver_mock().list_nodes.return_value = nodelist
71         self.driver_mock().create_node.side_effect = IOError
72         n = driver.create_node(testutil.MockSize(1), arv_node)
73         self.assertEqual('compute-000000000000001-zzzzz', n.name)
74
75     def test_create_sets_default_hostname(self):
76         driver = self.new_driver()
77         driver.create_node(testutil.MockSize(1),
78                            testutil.arvados_node_mock(254, hostname=None))
79         create_kwargs = self.driver_mock().create_node.call_args[1]
80         self.assertEqual('compute-0000000000000fe-zzzzz',
81                          create_kwargs.get('name'))
82         self.assertEqual('dynamic.compute.zzzzz.arvadosapi.com',
83                          create_kwargs.get('ex_metadata', {}).get('hostname'))
84
85     def test_create_tags_from_list_tags(self):
86         driver = self.new_driver(list_kwargs={'tags': 'testA, testB'})
87         driver.create_node(testutil.MockSize(1), testutil.arvados_node_mock())
88         self.assertEqual(['testA', 'testB'],
89                          self.driver_mock().create_node.call_args[1]['ex_tags'])
90
91     def test_create_with_two_disks_attached(self):
92         driver = self.new_driver(create_kwargs={'image': 'testimage'})
93         driver.create_node(testutil.MockSize(1), testutil.arvados_node_mock())
94         create_disks = self.driver_mock().create_node.call_args[1].get(
95             'ex_disks_gce_struct', [])
96         self.assertEqual(2, len(create_disks))
97         self.assertTrue(create_disks[0].get('autoDelete'))
98         self.assertTrue(create_disks[0].get('boot'))
99         self.assertEqual('PERSISTENT', create_disks[0].get('type'))
100         init_params = create_disks[0].get('initializeParams', {})
101         self.assertEqual('pd-standard-link', init_params.get('diskType'))
102         self.assertEqual('image-link', init_params.get('sourceImage'))
103         # Our node images expect the SSD to be named `tmp` to find and mount it.
104         self.assertEqual('tmp', create_disks[1].get('deviceName'))
105         self.assertTrue(create_disks[1].get('autoDelete'))
106         self.assertFalse(create_disks[1].get('boot', 'unset'))
107         self.assertEqual('SCRATCH', create_disks[1].get('type'))
108         init_params = create_disks[1].get('initializeParams', {})
109         self.assertEqual('local-ssd-link', init_params.get('diskType'))
110
111     def test_list_nodes_requires_tags_match(self):
112         # A node matches if our list tags are a subset of the node's tags.
113         # Test behavior with no tags, no match, partial matches, different
114         # order, and strict supersets.
115         cloud_mocks = [
116             testutil.cloud_node_mock(node_num, tags=tag_set)
117             for node_num, tag_set in enumerate(
118                 [[], ['bad'], ['good'], ['great'], ['great', 'ok'],
119                  ['great', 'good'], ['good', 'fantastic', 'great']])]
120         cloud_mocks.append(testutil.cloud_node_mock())
121         self.driver_mock().list_nodes.return_value = cloud_mocks
122         driver = self.new_driver(list_kwargs={'tags': 'good, great'})
123         self.assertItemsEqual(['5', '6'], [n.id for n in driver.list_nodes()])
124
125     def build_gce_metadata(self, metadata_dict):
126         # Convert a plain metadata dictionary to the GCE data structure.
127         return {
128             'kind': 'compute#metadata',
129             'fingerprint': 'testprint',
130             'items': [{'key': key, 'value': metadata_dict[key]}
131                       for key in metadata_dict],
132             }
133
134     def check_sync_node_updates_hostname_tag(self, plain_metadata):
135         start_metadata = self.build_gce_metadata(plain_metadata)
136         arv_node = testutil.arvados_node_mock(1)
137         cloud_node = testutil.cloud_node_mock(
138             2, metadata=start_metadata.copy(),
139             zone=testutil.cloud_object_mock('testzone'))
140         self.driver_mock().ex_get_node.return_value = cloud_node
141         driver = self.new_driver()
142         driver.sync_node(cloud_node, arv_node)
143         args, kwargs = self.driver_mock().ex_set_node_metadata.call_args
144         self.assertEqual(cloud_node, args[0])
145         plain_metadata['hostname'] = 'compute1.zzzzz.arvadosapi.com'
146         self.assertEqual(
147             plain_metadata,
148             {item['key']: item['value'] for item in args[1]})
149
150     def test_sync_node_updates_hostname_tag(self):
151         self.check_sync_node_updates_hostname_tag(
152             {'testkey': 'testvalue', 'hostname': 'startvalue'})
153
154     def test_sync_node_adds_hostname_tag(self):
155         self.check_sync_node_updates_hostname_tag({'testkey': 'testval'})
156
157     def test_sync_node_raises_exception_on_failure(self):
158         arv_node = testutil.arvados_node_mock(8)
159         cloud_node = testutil.cloud_node_mock(
160             9, metadata={}, zone=testutil.cloud_object_mock('failzone'))
161         mock_response = self.driver_mock().ex_set_node_metadata.side_effect = (Exception('sync error test'),)
162         driver = self.new_driver()
163         with self.assertRaises(Exception) as err_check:
164             driver.sync_node(cloud_node, arv_node)
165         self.assertIs(err_check.exception.__class__, Exception)
166         self.assertIn('sync error test', str(err_check.exception))
167
168     def test_node_create_time_zero_for_unknown_nodes(self):
169         node = testutil.cloud_node_mock()
170         self.assertEqual(0, gce.ComputeNodeDriver.node_start_time(node))
171
172     def test_node_create_time_for_known_node(self):
173         node = testutil.cloud_node_mock(metadata=self.build_gce_metadata(
174                 {'booted_at': '1970-01-01T00:01:05Z'}))
175         self.assertEqual(65, gce.ComputeNodeDriver.node_start_time(node))
176
177     def test_node_create_time_recorded_when_node_boots(self):
178         start_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
179         arv_node = testutil.arvados_node_mock()
180         driver = self.new_driver()
181         driver.create_node(testutil.MockSize(1), arv_node)
182         metadata = self.driver_mock().create_node.call_args[1]['ex_metadata']
183         self.assertLessEqual(start_time, metadata.get('booted_at'))
184
185     def test_known_node_fqdn(self):
186         name = 'fqdntest.zzzzz.arvadosapi.com'
187         node = testutil.cloud_node_mock(metadata=self.build_gce_metadata(
188                 {'hostname': name}))
189         self.assertEqual(name, gce.ComputeNodeDriver.node_fqdn(node))
190
191     def test_unknown_node_fqdn(self):
192         # Return an empty string.  This lets fqdn be safely compared
193         # against an expected value, and ComputeNodeMonitorActor
194         # should try to update it.
195         node = testutil.cloud_node_mock(metadata=self.build_gce_metadata({}))
196         self.assertEqual('', gce.ComputeNodeDriver.node_fqdn(node))
197
198     def test_deliver_ssh_key_in_metadata(self):
199         test_ssh_key = 'ssh-rsa-foo'
200         arv_node = testutil.arvados_node_mock(1)
201         with mock.patch('__builtin__.open',
202                         mock.mock_open(read_data=test_ssh_key)) as mock_file:
203             driver = self.new_driver(create_kwargs={'ssh_key': 'ssh-key-file'})
204         mock_file.assert_called_once_with('ssh-key-file')
205         driver.create_node(testutil.MockSize(1), arv_node)
206         metadata = self.driver_mock().create_node.call_args[1]['ex_metadata']
207         self.assertEqual('root:ssh-rsa-foo', metadata.get('sshKeys'))
208
209     def test_create_driver_with_service_accounts(self):
210         service_accounts = {'email': 'foo@bar', 'scopes': ['storage-full']}
211         srv_acct_config = {'service_accounts': json.dumps(service_accounts)}
212         arv_node = testutil.arvados_node_mock(1)
213         driver = self.new_driver(create_kwargs=srv_acct_config)
214         driver.create_node(testutil.MockSize(1), arv_node)
215         self.assertEqual(
216             service_accounts,
217             self.driver_mock().create_node.call_args[1]['ex_service_accounts'])
218
219     def test_fix_string_size(self):
220         # As of 0.18, the libcloud GCE driver sets node.size to the size's name.
221         # It's supposed to be the actual size object.  Make sure our driver
222         # patches that up in listings.
223         size = testutil.MockSize(2)
224         node = testutil.cloud_node_mock(size=size)
225         node.size = size.id
226         self.driver_mock().list_sizes.return_value = [size]
227         self.driver_mock().list_nodes.return_value = [node]
228         driver = self.new_driver()
229         nodelist = driver.list_nodes()
230         self.assertEqual(1, len(nodelist))
231         self.assertIs(node, nodelist[0])
232         self.assertIs(size, nodelist[0].size)
233
234     def test_skip_fix_when_size_not_string(self):
235         # Ensure we don't monkeypatch node sizes unless we need to.
236         size = testutil.MockSize(3)
237         node = testutil.cloud_node_mock(size=size)
238         self.driver_mock().list_nodes.return_value = [node]
239         driver = self.new_driver()
240         nodelist = driver.list_nodes()
241         self.assertEqual(1, len(nodelist))
242         self.assertIs(node, nodelist[0])
243         self.assertIs(size, nodelist[0].size)
244
245     def test_node_found_after_timeout_has_fixed_size(self):
246         size = testutil.MockSize(4)
247         cloud_node = testutil.cloud_node_mock(size=size.id)
248         self.check_node_found_after_timeout_has_fixed_size(size, cloud_node)
249
250     def test_list_empty_nodes(self):
251         self.driver_mock().list_nodes.return_value = []
252         self.assertEqual([], self.new_driver().list_nodes())