14
14
__metaclass__ = type
17
from datetime import datetime
21
from subprocess import (
19
from subprocess import CalledProcessError
26
from apiclient.creds import convert_tuple_to_string
27
from apiclient.maas_client import MAASClient
28
from apiclient.testing.credentials import make_api_credentials
30
22
from celery import states
31
from celery.app import app_or_default
32
from celery.task import Task
33
23
from maastesting.celery import CeleryFixture
34
24
from maastesting.factory import factory
35
25
from maastesting.fakemethod import (
39
from maastesting.matchers import (
44
29
from netaddr import IPNetwork
45
from provisioningserver import (
52
from provisioningserver.boot import tftppath
53
from provisioningserver.dhcp import (
30
from provisioningserver import tasks
57
31
from provisioningserver.dns.config import (
59
32
MAAS_NAMED_CONF_NAME,
60
33
MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME,
61
34
MAAS_NAMED_RNDC_CONF_NAME,
62
35
MAAS_RNDC_CONF_NAME,
37
from provisioningserver.dns.testing import patch_dns_config_path
64
38
from provisioningserver.dns.zoneconfig import (
65
39
DNSForwardZoneConfig,
66
40
DNSReverseZoneConfig,
68
from provisioningserver.import_images import boot_resources
69
from provisioningserver.power.poweraction import PowerActionFail
70
from provisioningserver.tags import MissingCredentials
71
42
from provisioningserver.tasks import (
72
add_new_dhcp_host_map,
73
ALREADY_STOPPED_MESSAGE,
74
ALREADY_STOPPED_RETURNCODE,
75
enlist_nodes_from_mscm,
76
enlist_nodes_from_ucsm,
86
44
RNDC_COMMAND_MAX_RETRY,
87
45
setup_rndc_configuration,
90
UPDATE_NODE_TAGS_MAX_RETRY,
93
47
write_dns_zone_config,
94
48
write_full_dns_config,
96
from provisioningserver.testing.boot_images import make_boot_image_params
97
from provisioningserver.testing.config import (
101
50
from provisioningserver.testing.testcase import PservTestCase
102
from provisioningserver.utils import filter_dict
103
import provisioningserver.utils.fs as fs_module
104
51
from provisioningserver.utils.shell import ExternalProcessError
105
from testresources import FixtureResource
106
52
from testtools.matchers import (
113
# An arbitrary MAC address. Not using a properly random one here since
114
# we might accidentally affect real machines on the network.
115
arbitrary_mac = "AA:BB:CC:DD:EE:FF"
118
celery_config = app_or_default().conf
121
class TestRefreshSecrets(PservTestCase):
122
"""Tests for the `refresh_secrets` task."""
125
("celery", FixtureResource(CeleryFixture())),
128
def test_does_not_require_arguments(self):
130
# Nothing is refreshed, but there is no error either.
133
def test_breaks_on_unknown_item(self):
134
self.assertRaises(AssertionError, refresh_secrets, not_an_item=None)
136
def test_works_as_a_task(self):
137
self.assertTrue(refresh_secrets.delay().successful())
139
def test_updates_api_credentials(self):
140
credentials = make_api_credentials()
142
api_credentials=convert_tuple_to_string(credentials))
143
self.assertEqual(credentials, auth.get_recorded_api_credentials())
145
def test_updates_nodegroup_uuid(self):
146
nodegroup_uuid = factory.make_name('nodegroupuuid')
147
refresh_secrets(nodegroup_uuid=nodegroup_uuid)
148
self.assertEqual(nodegroup_uuid, cache.cache.get('nodegroup_uuid'))
151
class TestPowerTasks(PservTestCase):
154
("celery", FixtureResource(CeleryFixture())),
157
def test_ether_wake_power_on_with_not_enough_template_args(self):
158
# In eager test mode the assertion is raised immediately rather
159
# than being stored in the AsyncResult, so we need to test for
160
# that instead of using result.get().
162
PowerActionFail, power_on.delay, "ether_wake")
164
def test_ether_wake_power_on(self):
165
result = power_on.delay(
166
"ether_wake", mac_address=arbitrary_mac)
167
self.assertTrue(result.successful())
169
def test_ether_wake_does_not_support_power_off(self):
171
PowerActionFail, power_off.delay,
172
"ether_wake", mac=arbitrary_mac)
175
class TestDHCPTasks(PservTestCase):
178
("celery", FixtureResource(CeleryFixture())),
181
def assertRecordedStdin(self, recorder, *args):
182
# Helper to check that the function recorder "recorder" has all
183
# of the items mentioned in "args" which are extracted from
184
# stdin. We can just check that all the parameters that were
185
# passed are being used.
187
recorder.extract_args()[0][0],
190
def make_dhcp_config_params(self):
191
"""Fake up a dict of dhcp configuration parameters."""
205
{param: factory.make_string() for param in param_names}
207
'omapi_key': factory.make_string(),
210
def test_upload_dhcp_leases(self):
212
leases, 'parse_leases_file',
213
Mock(return_value=(datetime.utcnow(), {})))
214
self.patch(leases, 'process_leases', Mock())
215
tasks.upload_dhcp_leases.delay()
216
self.assertEqual(1, leases.process_leases.call_count)
218
def test_add_new_dhcp_host_map(self):
219
# We don't want to actually run omshell in the task, so we stub
220
# out the wrapper class's _run method and record what it would
222
mac = factory.getRandomMACAddress()
223
ip = factory.getRandomIPAddress()
224
server_address = factory.make_string()
225
key = factory.make_string()
226
recorder = FakeMethod(result=(0, "hardware-type"))
227
self.patch(Omshell, '_run', recorder)
228
add_new_dhcp_host_map.delay({ip: mac}, server_address, key)
230
self.assertRecordedStdin(recorder, ip, mac, server_address, key)
232
def test_add_new_dhcp_host_map_failure(self):
233
# Check that task failures are caught. Nothing much happens in
234
# the Task code right now though.
235
mac = factory.getRandomMACAddress()
236
ip = factory.getRandomIPAddress()
237
server_address = factory.make_string()
238
key = factory.make_string()
239
self.patch(Omshell, '_run', FakeMethod(result=(0, "this_will_fail")))
241
CalledProcessError, add_new_dhcp_host_map.delay,
242
{mac: ip}, server_address, key)
244
def test_remove_dhcp_host_map(self):
245
# We don't want to actually run omshell in the task, so we stub
246
# out the wrapper class's _run method and record what it would
248
ip = factory.getRandomIPAddress()
249
server_address = factory.make_string()
250
key = factory.make_string()
251
recorder = FakeMethod(result=(0, "obj: <null>"))
252
self.patch(Omshell, '_run', recorder)
253
remove_dhcp_host_map.delay(ip, server_address, key)
255
self.assertRecordedStdin(recorder, ip, server_address, key)
257
def test_remove_dhcp_host_map_failure(self):
258
# Check that task failures are caught. Nothing much happens in
259
# the Task code right now though.
260
ip = factory.getRandomIPAddress()
261
server_address = factory.make_string()
262
key = factory.make_string()
263
self.patch(Omshell, '_run', FakeMethod(result=(0, "this_will_fail")))
265
CalledProcessError, remove_dhcp_host_map.delay,
266
ip, server_address, key)
268
def test_write_dhcp_config_invokes_script_correctly(self):
270
mocked_proc.returncode = 0
271
mocked_proc.communicate = Mock(return_value=('output', 'error output'))
272
mocked_popen = self.patch(
273
fs_module, "Popen", Mock(return_value=mocked_proc))
275
config_params = self.make_dhcp_config_params()
276
write_dhcp_config(**config_params)
278
# It should construct Popen with the right parameters.
279
self.assertThat(mocked_popen, MockAnyCall(
280
["sudo", "-n", "maas-provision", "atomic-write", "--filename",
281
celery_config.DHCP_CONFIG_FILE, "--mode", "0644"], stdin=PIPE))
283
# It should then pass the content to communicate().
284
content = config.get_config(**config_params).encode("ascii")
285
self.assertThat(mocked_proc.communicate, MockAnyCall(content))
287
# Similarly, it also writes the DHCPD interfaces to
288
# /var/lib/maas/dhcpd-interfaces.
289
self.assertThat(mocked_popen, MockAnyCall(
291
"sudo", "-n", "maas-provision", "atomic-write", "--filename",
292
celery_config.DHCP_INTERFACES_FILE, "--mode", "0644",
296
def test_restart_dhcp_server_sends_command(self):
297
self.patch(tasks, 'call_and_check')
298
restart_dhcp_server()
299
self.assertThat(tasks.call_and_check, MockCalledOnceWith(
300
['sudo', '-n', 'service', 'maas-dhcp-server', 'restart']))
302
def test_stop_dhcp_server_sends_command_and_writes_empty_config(self):
303
self.patch(tasks, 'call_and_check')
304
self.patch(tasks, 'sudo_write_file')
306
self.assertThat(tasks.call_and_check, MockCalledOnceWith(
307
['sudo', '-n', 'service', 'maas-dhcp-server', 'stop'],
308
env={'LC_ALL': 'C'}))
309
self.assertThat(tasks.sudo_write_file, MockCalledOnceWith(
310
celery_config.DHCP_CONFIG_FILE, tasks.DISABLED_DHCP_SERVER))
312
def test_stop_dhcp_server_ignores_already_stopped_error(self):
313
# Add whitespaces around the error message to make sure they
315
output = ' ' + ALREADY_STOPPED_MESSAGE + '\n'
316
exception = ExternalProcessError(
317
ALREADY_STOPPED_RETURNCODE, [], output=output)
318
self.patch(tasks, 'call_and_check', Mock(side_effect=exception))
319
self.patch(tasks, 'sudo_write_file')
320
self.assertIsNone(stop_dhcp_server())
322
def test_stop_dhcp_server_raises_other_returncodes(self):
323
# Use a returncode that is *not* ALREADY_STOPPED_RETURNCODE.
324
returncode = ALREADY_STOPPED_RETURNCODE + 1
325
exception = ExternalProcessError(
326
returncode, [], output=ALREADY_STOPPED_MESSAGE)
327
self.patch(tasks, 'call_and_check', Mock(side_effect=exception))
328
self.patch(tasks, 'sudo_write_file')
329
self.assertRaises(ExternalProcessError, stop_dhcp_server)
331
def test_stop_dhcp_server_raises_other_error_outputs(self):
332
# Use an error output that is *not* ALREADY_STOPPED_MESSAGE.
333
output = factory.make_string()
334
exception = ExternalProcessError(
335
ALREADY_STOPPED_RETURNCODE, [], output=output)
336
self.patch(tasks, 'call_and_check', Mock(side_effect=exception))
337
self.patch(tasks, 'sudo_write_file')
338
self.assertRaises(ExternalProcessError, stop_dhcp_server)
341
59
def assertTaskRetried(runner, result, nb_retries, task_name):
342
60
# In celery version 2.5 (in Saucy) a retried tasks that eventually
554
def test_write_full_dns_attached_to_dns_worker_queue(self):
556
write_full_dns_config.queue,
557
celery_config.WORKER_QUEUE_DNS)
560
class TestBootImagesTasks(PservTestCase):
563
("celery", FixtureResource(CeleryFixture())),
566
def test_sends_boot_images_to_server(self):
567
self.useFixture(set_tftp_root(self.make_dir()))
569
auth.record_api_credentials(':'.join(make_api_credentials()))
570
image = make_boot_image_params()
571
self.patch(tftppath, 'list_boot_images', Mock(return_value=[image]))
572
self.patch(boot_images, "get_cluster_uuid")
573
self.patch(MAASClient, 'post')
575
report_boot_images.delay()
577
args, kwargs = MAASClient.post.call_args
578
self.assertItemsEqual([image], json.loads(kwargs['images']))
581
class TestTagTasks(PservTestCase):
584
super(TestTagTasks, self).setUp()
585
self.celery = self.useFixture(CeleryFixture())
587
def test_update_node_tags_can_be_retried(self):
589
# The update_node_tags task can be retried.
590
# Simulate a temporary failure.
591
number_of_failures = UPDATE_NODE_TAGS_MAX_RETRY
592
raised_exception = MissingCredentials(
593
factory.make_name('exception'), random.randint(100, 200))
594
simulate_failures = MultiFakeMethod(
595
[FakeMethod(failure=raised_exception)] * number_of_failures +
597
self.patch(tags, 'process_node_tags', simulate_failures)
598
tag = factory.make_string()
599
result = update_node_tags.delay(
600
tag, '//node', tag_nsmap=None, retry=True)
602
self, result, UPDATE_NODE_TAGS_MAX_RETRY + 1,
603
'provisioningserver.tasks.update_node_tags')
605
def test_update_node_tags_is_retried_a_limited_number_of_times(self):
607
# If we simulate UPDATE_NODE_TAGS_MAX_RETRY + 1 failures, the
609
number_of_failures = UPDATE_NODE_TAGS_MAX_RETRY + 1
610
raised_exception = MissingCredentials(
611
factory.make_name('exception'), random.randint(100, 200))
612
simulate_failures = MultiFakeMethod(
613
[FakeMethod(failure=raised_exception)] * number_of_failures +
615
self.patch(tags, 'process_node_tags', simulate_failures)
616
tag = factory.make_string()
618
MissingCredentials, update_node_tags.delay, tag,
619
'//node', tag_nsmap=None, retry=True)
622
class TestImportBootImages(PservTestCase):
624
def make_archive_url(self, name=None):
626
name = factory.make_name('archive')
627
return 'http://%s.example.com/%s' % (name, factory.make_name('path'))
629
def patch_boot_resources_function(self):
630
"""Patch out `boot_resources.import_images`.
632
Returns the installed fake. After the fake has been called, but not
633
before, its `env` attribute will have a copy of the environment dict.
637
"""Fake function; records a copy of the environment."""
639
def __call__(self, *args, **kwargs):
641
self.env = os.environ.copy()
643
return self.patch(boot_resources, 'import_images', CaptureEnv())
645
def test_import_boot_images_integrates_with_boot_resources_function(self):
646
# If the config specifies no sources, nothing will be imported. But
647
# the task succeeds without errors.
648
fixture = self.useFixture(BootSourcesFixture([]))
649
self.patch(boot_resources, 'logger')
650
self.patch(boot_resources, 'locate_config').return_value = (
652
import_boot_images(sources=[])
653
self.assertIsInstance(import_boot_images, Task)
655
def test_import_boot_images_sets_GPGHOME(self):
656
home = factory.make_name('home')
657
self.patch(tasks, 'MAAS_USER_GPGHOME', home)
658
fake = self.patch_boot_resources_function()
659
import_boot_images(sources=[])
660
self.assertEqual(home, fake.env['GNUPGHOME'])
662
def test_import_boot_images_sets_proxy_if_given(self):
663
proxy = 'http://%s.example.com' % factory.make_name('proxy')
664
proxy_vars = ['http_proxy', 'https_proxy']
665
fake = self.patch_boot_resources_function()
666
import_boot_images(sources=[], http_proxy=proxy)
670
for var in proxy_vars
672
filter_dict(fake.env, proxy_vars))
674
def test_import_boot_images_leaves_proxy_unchanged_if_not_given(self):
675
proxy_vars = ['http_proxy', 'https_proxy']
676
fake = self.patch_boot_resources_function()
677
import_boot_images(sources=[])
678
self.assertEqual({}, filter_dict(fake.env, proxy_vars))
680
def test_import_boot_images_calls_callback(self):
681
self.patch_boot_resources_function()
682
mock_callback = Mock()
683
import_boot_images(sources=[], callback=mock_callback)
684
self.assertThat(mock_callback.delay, MockCalledOnceWith())
686
def test_import_boot_images_accepts_sources_parameter(self):
687
fake = self.patch(boot_resources, 'import_images')
690
'path': "http://example.com",
695
'subarches': ["generic"],
696
'labels': ["release"]
701
import_boot_images(sources=sources)
702
self.assertThat(fake, MockCalledOnceWith(sources))
705
class TestAddUCSM(PservTestCase):
707
def test_enlist_nodes_from_ucsm(self):
709
username = 'username'
710
password = 'password'
711
mock = self.patch(tasks, 'probe_and_enlist_ucsm')
712
enlist_nodes_from_ucsm(url, username, password)
713
self.assertThat(mock, MockCalledOnceWith(url, username, password))
716
class TestAddMSCM(PservTestCase):
718
def test_enlist_nodes_from_mscm(self):
720
username = 'username'
721
password = 'password'
722
mock = self.patch(tasks, 'probe_and_enlist_mscm')
723
enlist_nodes_from_mscm(host, username, password)
724
self.assertThat(mock, MockCalledOnceWith(host, username, password))