1
from __future__ import (
17
from contextlib import closing
18
from distutils.version import StrictVersion
19
from itertools import count
20
from textwrap import dedent
21
from time import sleep
22
from unittest import (
34
from urllib.request import urlopen
35
from urllib.error import HTTPError
38
from urllib.parse import urlparse
40
from urlparse import urlparse
46
from simplejson import loads
47
from testtools import TestCase
48
from testtools.content import (
53
from testtools.matchers import (
63
from timeout import timeout
64
# Import `pause_until_released` if you need to block execution at a given state
65
# so you can examine the machines in the testing pool.
67
assertCommandReturnCode,
68
assertStartedUpstartService,
69
change_logs_permissions,
76
pause_until_released # If this is not used below, linters will complain.
82
# Needed by the imports below - linters will complain.
83
sys.path.insert(0, "/usr/share/maas")
84
os.environ['DJANGO_SETTINGS_MODULE'] = 'maas.settings'
89
from django.core.exceptions import AppRegistryNotReady
90
from django.core.management import call_command
91
from django.contrib.auth.models import User
93
from maasserver.enum import NODE_STATUS
94
from maasserver.models.user import get_creds_tuple
95
from maasserver.utils import get_local_cluster_UUID
96
from apiclient.creds import convert_tuple_to_string
103
CLUSTER_CONTROLLER_IP,
104
DO_NOT_USE_ARM_NODES,
129
def setup_juju_cloud(server, cloud_yaml):
135
endpoint: {server}""".format(server=server).encode('utf8'))
136
cloud_yaml.file.flush()
139
def setup_juju_authentication(credentials_yaml, oauth):
140
credentials_yaml.write("""
145
maas-oauth: {oauth}""".format(oauth=oauth))
146
credentials_yaml.flush()
149
admin = User.objects.get(username=ADMIN_USER)
150
token = admin.tokens.all()[0]
151
return convert_tuple_to_string(get_creds_tuple(token))
155
home_dir = os.path.expanduser('~/')
156
ssh_dir = os.path.join(home_dir, '.ssh')
158
ssh_key = os.path.join(ssh_dir, 'id_rsa')
159
if not os.path.exists(ssh_key):
164
run_command(['ssh-keygen', '-t', 'rsa', '-N', '', '-f', ssh_key])
166
ssh_config = os.path.join(ssh_dir, 'config')
167
with open(ssh_config, 'w') as f:
168
f.write('StrictHostKeyChecking no')
171
def setup_local_dns():
172
with open('/etc/resolv.conf', encoding='ascii') as fd:
174
content = 'nameserver 127.0.0.1\n' + content
175
with open('/etc/resolv.conf', mode='w', encoding='ascii') as f:
179
DEB_PROXY_CONFIG = """
180
cache_peer %s parent %s 0 no-query no-digest
181
never_direct allow all
182
""" % (urlparse(SQUID_DEB_PROXY_URL).hostname,
183
urlparse(SQUID_DEB_PROXY_URL).port)
188
"test_restart_dbus_avahi",
189
"test_update_maas_url",
190
"test_update_maas_url_rack",
191
"test_check_initial_services_systemctl",
192
"test_check_rpc_info",
193
"test_update_preseed_arm",
195
"test_maas_logged_in",
196
"test_set_main_archive",
197
"test_main_archive_in_enlist_userdata_package_mirrors_config",
198
"test_main_archive_in_enlist_userdata_apt_config",
199
"test_set_http_proxy",
200
"test_add_ssh_key", # This is needed to deploy nodes.
201
"test_region_rack_connected",
202
"test_set_custom_boot_source",
203
"test_stop_image_import", # Stop before updating boot sources
204
"test_add_boot_source_selection_ppc64el",
205
# From this point on, we can connect to the external world.
206
"test_start_image_import", # Start importing now, check later.
207
"test_create_dynamic_range",
208
"test_reserve_bmc_range",
209
"test_create_slave_device_and_link_subnet",
210
"test_slave_device_interface_linked",
211
"test_set_up_dhcp_vlan",
212
"test_check_dhcp_service_systemctl",
213
"test_update_dns_config_systemctl",
214
"test_add_new_zones",
217
"test_add_new_spaces",
218
"test_add_subnet_to_space",
221
"test_delete_subnet",
223
"test_add_new_fabrics",
224
"test_add_vlan_to_fabric",
228
"test_delete_fabric",
229
"test_region_imported_images", # Check the images imported before.
230
"test_rack_imported_images", # Check rack imported images
231
# From this point on, we can enlist and deploy nodes.
232
"test_poweron_nodes_to_enlist",
233
"test_check_machines_new",
234
"test_assign_machines_to_test_zone",
235
"test_set_machines_ipmi_config",
236
"test_start_commissioning_machines",
237
"test_check_nodes_ready",
238
# At this point, we can start working with Juju...
239
"test_configure_juju",
240
"test_juju_bootstrap",
241
"test_apply_tag_to_all_machines",
242
"test_check_tag_applied_to_all_machines",
243
"test_juju_deploy_postgresql",
244
# Or, alternatively deploy manually
245
"test_allocate_machines",
246
"test_deploy_machines",
247
"test_machines_deployed",
248
"test_ip_addresses_not_in_dynamic_range",
249
"test_ip_addresses_not_in_bmc_range",
1
from unittest import TestLoader
3
from common import TestMAASIntegration
6
suite = open('integration-suite')
7
tests = [line.strip() for line in suite.readlines()]
9
tests_order = get_tests_order()
252
12
def sorting_method(ignored, first_test, second_test):
253
13
return tests_order.index(first_test) - tests_order.index(second_test)
256
TestLoader.sortTestMethodsUsing = sorting_method
258
# The 'maas-cli' has been renamed to 'maas' in version 1931.
259
maascli = 'maas' if get_maas_revision() >= 1931 else 'maas-cli'
261
# The 'maas-region-admin' has been renamed to 'maas-region' in version .
262
maas_region_admin = (
263
'maas-region' if get_maas_revision() >= 4738 else 'maas-region-admin')
265
# Uses the pre-1.6 API, where a cluster interface had no "name" field.
266
# MAAS will take the name from the "interface" field, as long as it's
267
# a unique name within the cluster. Newer clients would pass a name.
268
REGION_DHCP_CONFIG = {
269
"ip": "10.245.136.6",
271
"subnet_mask": "255.255.248.0",
272
"broadcast_ip": "10.245.143.255",
273
"router_ip": "10.245.136.1",
275
"ip_range_low": "10.245.136.10",
276
"ip_range_high": "10.245.136.200",
277
# add cidr as it is now default in new networking configs.
278
"cidr": "10.245.136.0/21"
282
class TestMAASIntegration(TestCase):
284
def get_node_count(self):
285
"""The number of available nodes."""
286
count = len(LENOVO_LAB)
288
count += len(ARM_LAB)
290
count += len(PPC_SYSTEMS)
293
def _wait_maas_running(self):
294
for tries in range(10):
296
urlopen(MAAS_URL).code
297
except HTTPError as e:
303
def _run_maas_cli(self, args):
304
retcode, output, err = run_command([maascli, "maas"] + args)
307
'retcode for %s maas %s' % (maascli, args),
308
text_content('%d' % retcode))
310
'stdout for %s maas %s' % (maascli, args),
311
text_content(output))
313
'stderr for %s maas %s' % (maascli, args),
317
def _update_dhcpd_apparmor_profile(self):
318
"""Workaround for raring due to bug 1107686."""
319
with open("/etc/apparmor.d/usr.sbin.dhcpd", "r+") as dhcpd_fd:
320
dhcpd_file = dhcpd_fd.read().decode('ascii')
321
dhcpd_file = dhcpd_file.replace(
322
'network packet packet,',
323
'network packet packet,\n network packet raw,')
325
dhcpd_fd.write(dhcpd_file.encode('utf-8'))
326
cmd = ["service", "apparmor", "reload"]
327
expected_output = '* Reloading AppArmor profiles'
328
assertCommandReturnCode(self, cmd, expected_output)
330
def _get_rack_interface_link(self, primary_rack_system_id, ip):
331
rack, err = self._run_maas_cli(
332
['rack-controller', 'read', primary_rack_system_id])
333
rack_controller = loads(rack)
335
for interface in rack_controller['interface_set']:
336
for link in interface['links']:
337
if ip == link['ip_address']:
341
def _set_up_dhcp(self, primary_rack_system_id, ip):
342
# Support the two following cases:
343
# 1. eth1 is auto-detected
344
# 2. eth1 not auto-dectected. in 2.0 this would be a bug.
345
rack_interface_link = self._get_rack_interface_link(
346
primary_rack_system_id, ip
348
rack_fabric = rack_interface_link['subnet']['vlan']['fabric']
349
rack_vlan = rack_interface_link['subnet']['vlan']['name']
352
'vlan', 'update', rack_fabric, rack_vlan, 'dhcp_on=True',
353
'primary_rack=%s' % primary_rack_system_id]
355
output, err = self._run_maas_cli(maas_dhcp_cmd)
358
vlan_result = loads(output)
361
#from maas.provisioningserver.service_monitor import get_service_state
362
pause_until_released(ng_list.__repr__())
363
# The JSON object returned by MAAS doesn't include the router_ip
364
# address, so let's remove it from the dhcp_config before comparing.
365
self.assertEqual(True, vlan_result['dhcp_on'])
367
def _boot_nodes(self):
368
# Run ipmipower to boot up nodes.
369
for ipmi_address in LENOVO_LAB.values():
370
self.power_off(ipmi_address, user=POWER_USER, password=POWER_PASS)
371
self.power_on(ipmi_address, user=POWER_USER, password=POWER_PASS)
373
for ipmi_address in PPC_SYSTEMS.values():
374
self.power_off(ipmi_address, password='admin', driver='LAN_2_0')
375
self.power_on(ipmi_address, password='admin', driver='LAN_2_0')
377
for ipmi_address in ARM_LAB.values():
378
self.power_off(ipmi_address, user='admin', password='admin')
379
self.power_on(ipmi_address, user='admin', password='admin')
381
def power_on(self, ip, user=None, password=None, driver=None):
382
cmd = ["ipmipower", "-h", ip]
383
if driver is not None:
384
cmd += ["-D", driver]
387
if password is not None:
388
cmd += ["-p", password]
390
expected_output = "%s: ok" % ip
391
assertCommandReturnCode(self, cmd, expected_output)
393
def power_off(self, ip, user=None, password=None, driver=None):
394
cmd = ["ipmipower", "-h", ip]
397
if driver is not None:
398
cmd += ["-D", driver]
399
if password is not None:
400
cmd += ["-p", password]
402
expected_output = "%s: ok" % ip
403
assertCommandReturnCode(self, cmd, expected_output)
405
def _get_machines_with_status(self, status):
406
"""Retrieve from the API all nodes with `status` (or substatus)."""
407
# Start by listing all nodes. The output will be shown on failure, so
408
# even if we can filter by status, doing it client-side may provide
409
# more helpful debug output.
410
output, err = self._run_maas_cli(["machines", "read"])
411
machine_list = loads(output)
412
# In 1.9 we used to look at the substatus for new status's. However 2.0+
413
# we simply look at the status
415
machine for machine in machine_list
416
if status == machine['status']
420
def _wait_machines(self, status=None, num_expected=None):
421
"""Wait for `num_expected` nodes with status `status`."""
422
if num_expected is None:
423
num_expected = self.get_node_count()
425
'wait_%d_%d' % (num_expected, status),
427
"Waiting for %d node(s) with status %d."
428
% (num_expected, status)))
430
filtered_list = self._get_machines_with_status(status)
431
while len(filtered_list) != num_expected:
433
filtered_list = self._get_machines_with_status(status)
436
def _run_juju_command(self, args, env=None):
437
command = ["juju", "--debug"]
439
retcode, output, err = run_command(command, env=env)
440
command_name = " ".join(map(pipes.quote, command))
441
self.addDetail(command_name, text_content(output))
442
self.addDetail(command_name, text_content(err))
443
return retcode, output, err
445
def get_juju_status(self):
446
# Juju2 status defaults to tabular so we need to force YAML
447
_, status_output, _ = self._run_juju_command(["status", "--format=yaml"])
448
status = yaml.safe_load(status_output)
451
def _wait_machines_running(self, nb_machines):
452
"""Wait until at least `nb_machines` have their agent running."""
454
status = self.get_juju_status()
455
if status is not None:
456
machines = status['machines'].values()
458
machine for machine in machines
459
if machine.get('machine-status', {}).get('current', '') in [
460
'running', 'started']]
461
if len(running_machines) >= nb_machines:
465
def _wait_units_started(self, application, nb_units):
466
"""Wait until an application has at least `nb_units` units."""
468
status = self.get_juju_status()
470
units = status['applications'][application]['units'].values()
474
unit for unit in units
475
if unit.get('workload-status', {}).get('current', '') == 'active']
476
if len(started_units) >= nb_units:
480
def test_create_admin(self):
481
"""Run maas createsuperuser."""
482
cmd_output = call_command(
483
"createadmin", username=ADMIN_USER, password=PASSWORD,
484
email="example@canonical.com", noinput=True)
485
self.assertEqual(cmd_output, None)
487
# Since revision 1828, MAAS doesn't use avahi/dbus anymore so
488
# we do not need to restart these daemons.
490
get_maas_revision() >= 1828, "Avahi/DBUS are not used anymore")
491
def test_restart_dbus_avahi(self):
492
cmd = ["service", "dbus", "restart"]
493
expected_output = 'dbus start/running'
494
assertCommandReturnCode(self, cmd, expected_output)
495
cmd = ["service", "avahi-daemon", "restart"]
496
expected_output = 'avahi-daemon start/running'
497
assertCommandReturnCode(self, cmd, expected_output)
500
os.path.exists("/etc/maas/maas_local_settings.py"),
501
"local_config_set is not available")
502
def test_update_maas_url(self):
503
# XXX: allenap: Using a debconf file to set the initial value of the
504
# MAAS URL should not work; see test_update_maas_url_old(), but I'm
505
# not sure if that's true.
507
"maas-region", "local_config_set", "--maas-url",
508
"http://10.245.136.6/MAAS",
510
assertCommandReturnCode(self, cmd, "")
512
cmd = ["systemctl", "restart", "maas-regiond"]
513
for retry in retries(delay=10, timeout=60):
514
retcode, output, err = run_command(cmd)
518
self.fail("maas-regiond cannot be restarted")
520
def test_update_maas_url_rack(self):
522
"maas-rack", "config", "--region-url",
523
"http://10.245.136.6:5240/MAAS",
525
assertCommandReturnCode(self, cmd, "")
527
cmd = ["systemctl", "restart", "maas-rackd"]
528
for retry in retries(delay=10, timeout=60):
529
retcode, output, err = run_command(cmd)
533
self.fail("maas-rackd could not be restarted")
536
StrictVersion(get_ubuntu_version()) < StrictVersion('15.10'),
537
'From 15.10 onwards, we use systemd')
538
def test_check_initial_services_systemctl(self):
539
cmd = ["systemctl", "status", "maas-rackd"]
540
retcode, output, err = run_command(cmd)
541
self.assertEqual(0, retcode)
542
self.assertIn('Active: active (running)', output)
543
self.assertEqual('', err)
545
def test_check_rpc_info(self):
546
# Ensure that the region is publishing RPC info to the clusters. This
547
# is a reasonable indication that the region is running correctly.
548
url = "%s/rpc/" % MAAS_URL.rstrip("/")
549
self.addDetail('url', text_content(url))
552
for elapsed, remaining in retries(timeout=300, delay=10):
553
attempt = next(attempts)
555
with closing(urlopen(url)) as response:
556
details = content_from_stream(response, buffer_now=True)
557
except HTTPError as error:
560
self.addDetail('info', details)
563
self.addDetail('errors', text_content(
564
"\n--\n".join([error for error in errors])))
566
"RPC info not published after %d attempts over %d seconds."
567
% (attempt, elapsed))
569
@skipIf(DO_NOT_USE_ARM_NODES, "Don't test ARM nodes")
570
def test_update_preseed_arm(self):
571
# XXX: matsubara add workaround to boot arm nodes.
573
# Try the old location first, the one used before the templates
574
# were moved to /etc/maas/.
576
"/usr/share/maas/preseeds/enlist_userdata", "rb+")
578
userdata_fd = open("/etc/maas/preseeds/enlist_userdata", "rb+")
579
userdata = userdata_fd.read().decode('utf-8')
580
url = 'http://ports.ubuntu.com/ubuntu-ports'
581
userdata += '\n' + dedent("""\
583
- source: "deb %s precise-proposed main"
586
userdata_fd.write(userdata.encode('utf-8'))
589
def test_login_api(self):
590
assert(self._wait_maas_running())
591
token_str = get_token_str()
592
api_url = MAAS_URL + "/api/2.0/"
593
cmd = [maascli, "login", "maas", api_url, token_str]
594
expected_output = "\nYou are now logged in to the MAAS server"
596
retcode, output, err = run_command(cmd, env=os.environ.copy())
599
elif retcode == 1 and 'Expected application/json' in err:
600
# The cli may error with 'Expected application/json, got:
601
# text/html; charset=utf-8\n' with retcode 1 before all pieces
602
# are up and running.
603
sleep(10) # We need to give MAAS some time.
604
self.assertIn(expected_output, output)
606
def test_maas_logged_in(self):
607
"""Make sure MAAS is successfully logged in. Also, it signals a handy
608
place to put a pause."""
609
# pause_until_released('test_maas_logged_in')
610
cmd = ["maas", "list"]
611
retcode, output, err = run_command(cmd)
612
self.assertEqual(0, retcode)
613
self.assertEqual('', err)
615
def test_set_main_archive(self):
616
output, err = self._run_maas_cli([
617
"maas", "set-config", "name=main_archive",
618
"value=%s" % MAIN_ARCHIVE])
619
self.assertThat(output, Contains("OK"))
620
output, err = self._run_maas_cli([
621
"maas", "get-config", "name=main_archive"])
622
self.assertThat(output, Contains(MAIN_ARCHIVE))
625
StrictVersion(get_maas_version()[:3]) > StrictVersion('2.0'),
626
'Cloud-init "package_mirror" config is used in MAAS 2.0 and earlier')
627
def test_main_archive_in_enlist_userdata_package_mirrors_config(self):
628
# Check that the user-data passed on to nodes have
629
# the 'primary' mirror address.
630
output = urlopen(USER_DATA_URL).read().decode('utf-8')
631
self.assertThat(output, Contains(
632
'primary: ["%s"]' % MAIN_ARCHIVE))
635
StrictVersion(get_maas_version()[:3]) < StrictVersion('2.1'),
636
'Cloud-init "apt" config is used solely in MAAS 2.1+')
637
def test_main_archive_in_enlist_userdata_apt_config(self):
638
output = urlopen(USER_DATA_URL).read().decode('utf-8')
639
# Old APT configuration.
640
self.assertThat(yaml.safe_load(output), ContainsDict({
641
'system_info': ContainsDict({
642
'package_mirrors': MatchesListwise([
644
"arches": Equals(["i386", "amd64"]),
645
"search": MatchesDict({
646
"primary": Equals([MAIN_ARCHIVE]),
647
"security": Equals([MAIN_ARCHIVE]),
649
"failsafe": MatchesDict({
651
"http://archive.ubuntu.com/ubuntu"),
653
"http://security.ubuntu.com/ubuntu"),
657
"arches": Equals(["default"]),
658
"search": MatchesDict({
659
"primary": Equals([PORTS_ARCHIVE]),
660
"security": Equals([PORTS_ARCHIVE]),
662
"failsafe": MatchesDict({
664
"http://ports.ubuntu.com/ubuntu-ports"),
666
"http://ports.ubuntu.com/ubuntu-ports"),
672
# New APT configuration.
673
self.assertThat(yaml.safe_load(output), ContainsDict({
674
'apt': ContainsDict({
675
'preserve_sources_list': Equals(False),
676
'primary': MatchesListwise([
678
"arches": Equals(["amd64", "i386"]),
679
"uri": Equals(MAIN_ARCHIVE),
682
"arches": Equals(["default"]),
683
"uri": Equals(PORTS_ARCHIVE),
689
def test_set_http_proxy(self):
690
output, err = self._run_maas_cli([
691
"maas", "set-config", "name=http_proxy",
692
"value=%s" % HTTP_PROXY])
693
self.assertThat(output, Contains("OK"))
694
output, err = self._run_maas_cli([
695
"maas", "get-config", "name=http_proxy"])
696
self.assertThat(output, Contains(HTTP_PROXY))
698
# Check that the user-data passed on to nodes have the proxy address.
699
output = urlopen(USER_DATA_URL).read().decode('utf-8')
700
self.assertThat(output, Contains(
701
"apt_proxy: %s" % HTTP_PROXY))
703
# XXX: Brendan Donegan - 13-07-2016:
704
# https://bugs.launchpad.net/maas/+bug/1603156
705
# Restart regiond as otherwise the import of boot resources
706
# does not start succesfully in the CI
707
cmd = ["systemctl", "restart", "maas-regiond"]
708
retcode, output, err = run_command(cmd)
709
self.assertEqual(0, retcode)
710
self.assertEqual('', output) # No news is good news
711
self.assertEqual('', err)
713
assert(self._wait_maas_running())
715
def get_master_rack(self):
716
output, err = self._run_maas_cli(["rack-controllers", "read"])
717
rack_controllers = loads(output)
718
if len(rack_controllers) > 0:
719
return rack_controllers[0]
722
def get_rack_service_status(self, rack_id, service_name):
723
output, err = self._run_maas_cli(["rack-controller", "read", rack_id])
724
rack_info = loads(output)
725
for service in rack_info['service_set']:
726
if service['name'] == service_name:
727
return service['status']
731
def test_region_rack_connected(self):
732
# The master cluster is connected and changed the uuid field of the
733
# nodegroup object from 'master' to its UUID.
734
while self.get_master_rack() is None:
736
rack_id = self.get_master_rack()['system_id']
737
while self.get_rack_service_status(rack_id, 'rackd') != 'running':
740
def test_create_dynamic_range(self):
741
output, err = self._run_maas_cli(
742
['ipranges', 'create', 'type=dynamic',
743
'start_ip=%s' % REGION_DHCP_CONFIG['ip_range_low'],
744
'end_ip=%s' % REGION_DHCP_CONFIG['ip_range_high']])
745
# If the range was configured correct, it should return the subnet where
746
# the range belogs to. As such, we check the output to ensure it is there.
747
self.assertThat(output, Contains('"cidr": "%s"' % REGION_DHCP_CONFIG['cidr']))
748
# TODO: We need to read all IP ranges for the subnet and check that the range
749
# has actually been created regardless whether we checked the output above.
750
output, err = self._run_maas_cli(
751
['ipranges', 'read'])
753
def _get_rack_systemid_on_region(self):
754
output, err = self._run_maas_cli(["rack-controllers", "read"])
755
rack_controllers = loads(output)
757
for rack in rack_controllers:
758
if rack['node_type'] == region_rack:
759
return rack['system_id']
761
def test_create_slave_device_and_link_subnet(self):
762
# Create a device with the same IP as the MAAS CI slave so
763
# that the IP address cannot be 'stolen' by another machine
764
output, _ = self._run_maas_cli([
765
"devices", "create", "hostname={}".format(SLAVE_HOSTNAME),
766
"mac_addresses={}".format(SLAVE_MAC)
768
device = loads(output)
769
device_id = device["system_id"]
770
interface_id = "{}".format(device["interface_set"][0]["id"])
772
# Update the vlan on the interface so we can link the subnet
773
rack_system_id = self._get_rack_systemid_on_region()
774
rack_interface_link = self._get_rack_interface_link(
775
rack_system_id, REGION_DHCP_CONFIG['ip']
777
subnet = rack_interface_link['subnet']['name']
778
vlan_id = rack_interface_link['subnet']['vlan']['id']
779
output, _ = self._run_maas_cli([
780
"interface", "update", device_id,
781
interface_id, "vlan={}".format(vlan_id),
783
updated_interface = loads(output)
784
self.assertThat(updated_interface['vlan']['id'], Equals(vlan_id))
785
output, _ = self._run_maas_cli([
786
"interface", "link-subnet", device_id, interface_id,
787
"mode=STATIC", "subnet={}".format(subnet),
788
"ip_address={}".format(SLAVE_IP),
790
linked_interface = loads(output)
792
linked_interface['links'][0]['ip_address'],
796
def test_slave_device_interface_linked(self):
798
Check that after we created the slave device, that the
799
representation in MAAS looks sensible.
802
output, _ = self._run_maas_cli(["devices", "read"])
803
devices = loads(output)
804
for device in devices:
805
if device['hostname'] == SLAVE_HOSTNAME:
806
slave_device = device
807
self.assertIsNotNone(
808
slave_device, "Could not find {} in devices".format(SLAVE_HOSTNAME)
810
self.assertThat(slave_device['ip_addresses'], Contains(SLAVE_IP))
812
iface['mac_address'] for iface in slave_device['interface_set']
814
self.assertThat(mac_addresses, Contains(SLAVE_MAC.lower()))
817
def test_set_up_dhcp_vlan(self):
818
rack_system_id = self._get_rack_systemid_on_region()
819
self._set_up_dhcp(rack_system_id, REGION_DHCP_CONFIG['ip'])
821
# Wait for the task to complete and create the dhcpd.conf file.
822
while os.path.exists("/var/lib/maas/dhcpd.conf") is False:
824
"Waiting task create dhcpd.conf file.",
825
content_from_file("/var/log/maas/maas.log"))
829
StrictVersion(get_ubuntu_version()) < StrictVersion('15.10'),
830
'From 15.10 onwards, we use systemd')
831
# Timeout after 2 minutes, provided that it may take a while for the
832
# rack controller to write dhcp config and start maas-dhcpd
834
def test_check_dhcp_service_systemctl(self):
835
cmd = ["systemctl", "status", "maas-dhcpd"]
836
# systemd will return 3 if a 'condition failed. This typically means that
837
# /var/lib/maas/dhcpd.conf is not there yet, and we should wait for a bit
838
# to see if the config is written and maas-dhcp is brought up by the rack.
839
retcode, output, err = run_command(cmd)
841
# query systemd every 3 seconds to see if maas-dhcpd us running
843
retcode, output, err = run_command(cmd)
844
#pause_until_released((retcode, output, err).__repr__())
845
self.assertEqual(0, retcode)
846
self.assertIn('Active: active (running)', output)
847
self.assertEqual('', err)
850
StrictVersion(get_ubuntu_version()) < StrictVersion('15.10'),
851
'From 15.10 onwards, we use systemd')
852
def test_update_dns_config_systemctl(self):
853
dns_config = open("/etc/bind/named.conf.options", 'w')
854
dns_config.write(LAB_DNS_CONFIG)
856
cmd = ["systemctl", "restart", "bind9"]
857
retcode, output, err = run_command(cmd)
858
self.assertEqual(0, retcode)
859
self.assertEqual('', output) # No news is good news
860
self.assertEqual('', err)
864
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.5'),
865
"Zone feature only available after 1.5")
866
def test_add_new_zones(self):
867
# Create 2 new zones.
868
output, err = self._run_maas_cli(
869
["zones", "create", "name=test-zone", "description='A test zone'"])
871
self.assertEqual('test-zone', zone['name'])
872
output, err = self._run_maas_cli(
873
["zones", "create", "name=delete-zone",
874
"description='A test zone to be deleted'"])
876
self.assertEqual('delete-zone', zone['name'])
879
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.5'),
880
"Zone feature only available after 1.5")
881
def test_list_zones(self):
882
output, err = self._run_maas_cli(["zones", "read"])
883
zones = loads(output)
884
expected = [u'default', u'delete-zone', u'test-zone']
885
self.assertEqual(expected, sorted([zone['name'] for zone in zones]))
888
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.5'),
889
"Zone feature only available after 1.5")
890
def test_delete_zone(self):
891
_, err = self._run_maas_cli(["zone", "delete", "delete-zone"])
892
# List the remaining zones after the delete command.
893
output, err = self._run_maas_cli(["zones", "read"])
894
zones = loads(output)
896
'delete-zone', sorted([zone['name'] for zone in zones]))
900
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
901
'Space feature only available after 1.9')
902
def test_add_new_spaces(self):
903
# Create 2 new spaces.
904
output, err = self._run_maas_cli(
905
['spaces', 'create', 'name=test-space'])
906
out_dict = loads(output)
907
self.assertEqual('test-space', out_dict['name'])
908
output, err = self._run_maas_cli(
909
['spaces', 'create', 'name=delete-space'])
910
out_dict = loads(output)
911
self.assertEqual('delete-space', out_dict['name'])
914
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
915
'Subnet feature only available after 1.9')
916
def test_add_subnet_to_space(self):
917
output, err = self._run_maas_cli(
918
['subnets', 'create', 'space=0', 'name=test-subnet',
919
'cidr=192.168.200.0/24'])
920
out_dict = loads(output)
921
self.assertEqual('test-subnet', out_dict['name'])
922
self.assertEqual('192.168.200.0/24', out_dict['cidr'])
923
self.assertEqual('space-0', out_dict['space'])
926
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
927
'Space feature only available after 1.9')
928
def test_list_spaces(self):
929
output, err = self._run_maas_cli(['spaces', 'read'])
930
out_dict = loads(output)
931
expected = ['delete-space', 'space-0', 'test-space']
932
self.assertItemsEqual(
933
expected, [item['name'] for item in out_dict])
936
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
937
'Subnet feature only available after 1.9')
938
def test_list_subnets(self):
939
output, err = self._run_maas_cli(['subnets', 'read'])
940
out_dict = loads(output)
941
self.assertEqual(2, len(out_dict), 'space-0 should now have 2 subnets')
944
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
945
'Subnet feature only available after 1.9')
946
def test_delete_subnet(self):
947
_, err = self._run_maas_cli(['subnet', 'delete', 'test-subnet'])
948
output, err = self._run_maas_cli(['subnets', 'read'])
950
self.assertEqual(1, len(out), 'space-0 should now have 1 subnet')
951
self.assertNotIn('test-subnet', [n['name'] for n in out])
953
out[0]['name'], out[0]['cidr'],
954
'Name and CIDR should be equal for the default subnet.')
957
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
958
'Space feature only available after 1.9')
959
def test_delete_space(self):
960
_, err = self._run_maas_cli(['space', 'delete', 'delete-space'])
961
# List the remaining zones after the delete command.
962
output, err = self._run_maas_cli(['spaces', 'read'])
963
out_dict = loads(output)
965
'delete-space', [item['name'] for item in out_dict])
969
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
970
'Fabric feature only available after 1.9')
971
def test_add_new_fabrics(self):
972
# Create 2 new fabrics.
973
output, err = self._run_maas_cli(
974
['fabrics', 'create', 'name=test-fabric'])
975
out_dict = loads(output)
976
self.assertEqual('test-fabric', out_dict['name'])
977
output, err = self._run_maas_cli(
978
['fabrics', 'create', 'name=delete-fabric'])
979
out_dict = loads(output)
980
self.assertEqual('delete-fabric', out_dict['name'])
983
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
984
'VLAN feature only available after 1.9')
985
def test_add_vlan_to_fabric(self):
986
output, err = self._run_maas_cli(
987
['vlans', 'create', 'test-fabric', 'name=test-vlan', 'vid=2'])
988
out_dict = loads(output)
989
self.assertEqual('test-vlan', out_dict['name'])
990
self.assertEqual('test-fabric', out_dict['fabric'])
991
self.assertEqual(2, out_dict['vid'])
994
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
995
'Fabric feature only available after 1.9')
996
def test_list_fabrics(self):
997
output, err = self._run_maas_cli(['fabrics', 'read'])
998
out_dict = loads(output)
999
fabric_names = [item['name'] for item in out_dict]
1000
self.assertThat(fabric_names, Contains('test-fabric'))
1001
fabric_names.remove('test-fabric')
1002
self.assertThat(fabric_names, Contains('delete-fabric'))
1003
fabric_names.remove('delete-fabric')
1004
for fabric_name in fabric_names:
1005
self.assertThat(fabric_name, MatchesRegex('fabric-\d$'))
1008
[[v['name'] for v in f['vlans']]
1009
for f in out_dict if f['name'] == 'test-fabric'][0])
1012
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
1013
'VLAN feature only available after 1.9')
1014
def test_list_vlans(self):
1015
output, err = self._run_maas_cli(['vlans', 'read', 'test-fabric'])
1016
out_dict = loads(output)
1017
expected = ['untagged', 'test-vlan']
1018
self.assertItemsEqual(expected, [v['name'] for v in out_dict])
1021
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
1022
'VLAN feature only available after 1.9')
1023
def test_delete_vlan(self):
1024
_, err = self._run_maas_cli(
1025
['vlan', 'delete', 'test-fabric', 'test-vlan'])
1026
output, err = self._run_maas_cli(['vlans', 'read', 'test-fabric'])
1028
self.assertNotIn('test-vlan', [v['name'] for v in out])
1029
self.assertEqual(1, len(out), 'Fabric should have only one VLAN now.')
1031
def test_reserve_bmc_range(self):
1032
_, err = self._run_maas_cli(
1033
['ipranges', 'create', 'type=reserved',
1034
'start_ip=' + BMC_START_IP, 'end_ip=' + BMC_END_IP,
1037
output, err = self._run_maas_cli(
1038
['ipranges', 'read']
1041
# TODO: assert that newly created range is there
1044
if range['comment'] == 'BMCs':
1046
self.assertIsNotNone(bmc_range, 'BMC range not found')
1047
self.assertThat(bmc_range['start_ip'], Equals(BMC_START_IP))
1048
self.assertThat(bmc_range['end_ip'], Equals(BMC_END_IP))
1049
self.assertThat(bmc_range['type'], Equals('reserved'))
1052
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.9'),
1053
"Fabric feature only available after 1.9")
1054
def test_delete_fabric(self):
1055
_, err = self._run_maas_cli(["fabric", "delete", "delete-fabric"])
1056
# List the remaining zones after the delete command.
1057
output, err = self._run_maas_cli(["fabrics", "read"])
1058
out_dict = loads(output)
1060
'delete-fabric', sorted([item['name'] for item in out_dict]))
1062
@skipUnless(TEST_CUSTOM_IMAGES, "Not testing custom images")
1063
def test_set_custom_boot_source(self):
1064
boot_source_url = "http://10.55.32.203/proposed/streams/v1/index.json"
1065
output, _ = self._run_maas_cli([
1066
"boot-source", "update", "1",
1067
"url=" + boot_source_url,
1069
boot_source = loads(output)
1070
self.assertThat(boot_source['url'], Equals(boot_source_url))
1072
def test_stop_image_import(self):
1073
output, err = self._run_maas_cli(
1074
["boot-resources", "stop-import"])
1076
output, Contains("Import of boot resources is being stopped")
1079
@skipUnless(USE_PPC_NODES, "Not testing PPC systems")
1080
def test_add_boot_source_selection_ppc64el(self):
1081
# Add the ppc64el boot source selection to all boot sources
1082
output, err = self._run_maas_cli(["boot-sources", "read"])
1083
boot_sources = loads(output)
1084
for source in boot_sources:
1085
output, err = self._run_maas_cli(
1086
["boot-source-selections", "read", str(source["id"])]
1088
selections = loads(output)
1089
# To add a new arch we need to specify the arches= parameter
1090
# multiple times, e.g. arches=amd64 arches=ppc64el
1091
for selection in selections:
1092
new_arches = selection['arches'] + ['ppc64el']
1093
selection_update_params = [
1094
'arches={arch}'.format(arch=arch) for arch in new_arches
1096
output, err = self._run_maas_cli([
1097
"boot-source-selection",
1100
str(selection["id"]),
1101
] + selection_update_params)
1102
updated_selection = loads(output)
1103
self.assertThat(updated_selection["arches"], Equals(new_arches))
1105
def test_start_image_import(self):
1106
output, err = self._run_maas_cli(
1107
["boot-resources", "import"])
1109
output, Contains("Import of boot resources started")
1112
@timeout(60 * 60) # Allow for up to one hour
1113
def test_region_imported_images(self):
1114
# MAAS 2.1 and up uses new kernel naming scheme
1115
if StrictVersion(get_maas_version()[:3]) > StrictVersion('2.0'):
1116
expected_resources = set([
1117
(u'ubuntu/xenial', 'amd64/hwe-16.04'),
1118
(u'ubuntu/xenial', 'amd64/hwe-16.04-lowlatency'),
1119
(u'ubuntu/xenial', 'amd64/ga-16.04'),
1120
(u'ubuntu/xenial', 'amd64/ga-16.04-lowlatency'),
1123
expected_resources.update([
1124
(u'ubuntu/xenial', 'ppc64el/hwe-16.04'),
1125
(u'ubuntu/xenial', 'ppc64el/ga-16.04'),
1128
expected_resources = set([
1129
(u'ubuntu/xenial', 'amd64/hwe-x'),
1132
expected_resources.update([
1133
(u'ubuntu/xenial', 'ppc64el/hwe-x'),
1136
complete_resources = set()
1137
while not expected_resources.issubset(complete_resources):
1138
resources_output, err = self._run_maas_cli(
1139
["boot-resources", "read"])
1140
resources = loads(resources_output)
1141
for resource in resources:
1142
resource_id = resource['id']
1143
resource_name = resource['name']
1144
resource_arch = resource['architecture']
1145
output, err = self._run_maas_cli(
1146
['boot-resource', 'read', '%s' % resource_id])
1147
resource_data = loads(output)
1148
for _, resource_set in resource_data['sets'].items():
1149
if resource_set['complete']:
1150
complete_resources.add((resource_name, resource_arch))
1152
"Waiting for region to import boot resources.",
1153
text_content(resources_output))
1156
@timeout(60 * 30) # Allow for up to 30 mins
1157
def test_rack_imported_images(self):
1158
rack_system_id = self._get_rack_systemid_on_region()
1160
output, err = self._run_maas_cli(
1161
["rack-controller", "list-boot-images", rack_system_id])
1162
rack_images = loads(output)
1163
if rack_images['status'] == 'synced':
1166
"Waiting for rack to import boot resources.",
1167
text_content(output))
1170
def test_add_ssh_key(self):
1171
"""Add our ssh key to the admin account."""
1173
home_dir = os.path.expanduser('~/')
1174
ssh_dir = os.path.join(home_dir, '.ssh')
1176
os.path.join(ssh_dir, 'id_rsa.pub')).read()
1177
out, err = self._run_maas_cli(["sshkeys", "create", "key=%s" % ssh_key])
1178
out, err = self._run_maas_cli(["sshkeys", "read"])
1180
self.assertEqual(ssh_key, keys[0]['key'])
1182
def test_poweron_nodes_to_enlist(self):
1186
def test_check_machines_new(self):
1187
self._wait_machines(NODE_STATUS.NEW)
1190
StrictVersion(get_maas_version()[:3]) < StrictVersion('1.5'),
1191
"Zone feature only available after 1.5")
1192
def test_assign_machines_to_test_zone(self):
1193
# Set two of the declared nodes to the test-zone created earlier.
1194
output, err = self._run_maas_cli(["machines", "read"])
1195
machines = loads(output)
1196
for machine in machines[:2]:
1198
["machine", "update", machine['system_id'], "zone=test-zone"])
1199
# Check nodes are in the test-zone
1200
output, err = self._run_maas_cli(["machines", "read", "zone=test-zone"])
1201
machines = loads(output)
1202
self.assertEqual(2, len(machines))
1204
def test_set_machines_ipmi_config(self):
1205
"""Set IPMI configuration for each node."""
1207
all_machines.update(LENOVO_LAB)
1208
all_machines.update(ARM_LAB)
1209
all_machines.update(PPC_SYSTEMS)
1210
for mac in all_machines.keys():
1211
# run maas-cli command to search node by mac and return system_id
1212
out, err = self._run_maas_cli(
1213
["machines", "read", "mac_address=%s" % mac])
1214
machine_list = loads(out)
1215
for machine in machine_list:
1218
power_user = power_pass = 'admin'
1219
elif mac in PPC_SYSTEMS:
1221
power_pass = 'admin'
1222
power_driver = 'LAN_2_0'
1224
power_user = POWER_USER
1225
power_pass = POWER_PASS
1226
self._run_maas_cli([
1227
"machine", "update", machine['system_id'], "power_type=ipmi",
1228
"power_parameters_power_address=" + all_machines[mac],
1229
"power_parameters_power_user=" + power_user,
1230
"power_parameters_power_pass=" + power_pass,
1231
"power_parameters_power_driver=" + power_driver,
1234
def test_start_commissioning_machines(self):
1235
# Use maas-cli to accept all nodes.
1236
output, err = self._run_maas_cli(["machines", "accept-all"])
1237
for node in loads(output):
1238
self.assertEqual(node['status'], 1)
1241
def test_check_nodes_ready(self):
1242
self._wait_machines(NODE_STATUS.READY)
1244
def test_apply_tag_to_all_machines(self):
1245
# Use maas-cli to set a tag on all nodes.
1246
output, err = self._run_maas_cli(
1247
["tags", "create", "name=all", "definition=true()",
1248
"comment=A tag present on all nodes"])
1250
self.assertEqual(tag['name'], "all")
1253
def test_check_tag_applied_to_all_machines(self):
1254
# Wait for all nodes to have new tag applied.
1255
expected_node_count = self.get_node_count()
1257
output, err = self._run_maas_cli(["tag", "machines", "all"])
1258
nodes = loads(output)
1259
if len(nodes) == expected_node_count:
1263
@skipUnless(TEST_JUJU, "Not testing juju")
1264
def test_configure_juju(self):
1265
# Proxy is currently broken: disable using the lab's proxy
1266
# as parent for now.
1267
token_str = get_token_str()
1268
# Workaround bug 972829 (in juju precise).
1269
server_url = MAAS_URL.replace('/MAAS', ':80/MAAS')
1270
with tempfile.NamedTemporaryFile() as cloud_yaml:
1271
# Create cloud configuration
1273
server=server_url, cloud_yaml=cloud_yaml
1276
retcode, _, _ = self._run_juju_command(
1277
['add-cloud', 'testmaas', cloud_yaml.name]
1279
self.assertThat(retcode, Equals(0))
1280
retcode, list_clouds_output, _ = self._run_juju_command(
1283
self.assertThat(retcode, Equals(0))
1284
self.assertThat(list_clouds_output, Contains("testmaas"))
1286
with open(os.path.expanduser(
1287
'~/.local/share/juju/credentials.yaml'), mode='w', encoding='utf8') as credentials:
1288
setup_juju_authentication(oauth=token_str, credentials_yaml=credentials)
1289
retcode, list_creds_output, _ = self._run_juju_command(
1290
['list-credentials']
1292
self.assertThat(retcode, Equals(0))
1293
self.assertThat(list_creds_output, Contains("testmaas"))
1297
@skipUnless(TEST_JUJU, "Not testing juju")
1298
def test_juju_bootstrap(self):
1299
# Wait a bit to let all the nodes go down.
1300
# XXX: rvb 2013-04-23 bug=1171418
1302
# Proxy has to be set while bootstrapping
1303
with tempfile.NamedTemporaryFile() as bootstrap_config:
1304
bootstrap_config.write("""
1306
https-proxy: {proxy}""".format(proxy=HTTP_PROXY).encode('utf8'))
1307
bootstrap_config.file.flush()
1308
env = os.environ.copy()
1309
env['http_proxy'] = HTTP_PROXY
1310
env['https_proxy'] = HTTP_PROXY
1311
retcode, _, _ = self._run_juju_command([
1312
'bootstrap', 'autopkgtest', 'testmaas',
1313
'--config={config}'.format(config=bootstrap_config.name)
1315
self.assertThat(retcode, Equals(0))
1317
@skipUnless(TEST_JUJU, "Not testing juju")
1318
def test_juju_deploy_postgresql(self):
1319
# Deploy postgresql.
1320
env = os.environ.copy()
1321
env['http_proxy'] = HTTP_PROXY
1322
env['https_proxy'] = HTTP_PROXY
1323
retcode, _, _ = self._run_juju_command(["deploy", "postgresql"], env=env)
1324
self.assertThat(retcode, Equals(0))
1325
self._wait_machines_running(1)
1326
self._wait_units_started('postgresql', 1)
1328
nb_deployed_machines = 2
1331
@skipIf(TEST_JUJU, "Tested allocation/deployment with Juju.")
1332
def test_allocate_machines(self):
1333
# Get all nodes that are Ready
1334
ready_machines = self._wait_machines(NODE_STATUS.READY)
1335
for machine in ready_machines:
1336
system_id = machine['system_id']
1338
['machines', 'allocate', 'system_id=' + system_id]
1342
@skipIf(TEST_JUJU, "Tested allocation/deployment with Juju.")
1343
def test_deploy_machines(self):
1344
# Get all nodes that are Allocated
1345
allocated_machines = self._wait_machines(
1346
NODE_STATUS.ALLOCATED,
1348
for machine in allocated_machines:
1349
system_id = machine['system_id']
1350
self._run_maas_cli(['machine', 'deploy', system_id])
1352
def test_machines_deployed(self):
1354
deployed_machines = self._wait_machines(
1355
NODE_STATUS.DEPLOYED,
1356
num_expected=self.nb_deployed_machines,
1359
deployed_machines = self._wait_machines(NODE_STATUS.DEPLOYED)
1360
for machine in deployed_machines:
1361
self.assertThat(machine.get('status_name', ''), Equals('Deployed'))
1364
def test_ip_addresses_not_in_dynamic_range(self):
1365
# Make sure the deployed machines have addresses in the dynamic range.
1367
deployed_machines = self._wait_machines(
1368
NODE_STATUS.DEPLOYED, num_expected=self.nb_of_deployed_machines
1371
deployed_machines = self._wait_machines(NODE_STATUS.DEPLOYED)
1373
for deployed_machine in deployed_machines:
1374
ips = [IPAddress(ip) for ip in deployed_machine['ip_addresses']]
1376
REGION_DHCP_CONFIG['ip_range_low'],
1377
REGION_DHCP_CONFIG['ip_range_high'])
1379
self.assertThat(ip_range, Not(Contains(ip)))
1382
def test_ip_addresses_not_in_bmc_range(self):
1383
# Make sure that the deployed machines do not have addresses
1384
# in the range reserved for the BMCs
1386
deployed_machines = self._wait_machines(
1387
NODE_STATUS.DEPLOYED, num_expected=self.nb_of_deployed_machines
1390
deployed_machines = self._wait_machines(NODE_STATUS.DEPLOYED)
1391
for deployed_machine in deployed_machines:
1392
ips = [IPAddress(ip) for ip in deployed_machine['ip_addresses']]
1393
ip_range = IPRange(BMC_START_IP, BMC_END_IP)
1395
self.assertThat(ip_range, Not(Contains(ip)))
1399
def dump_database(cls):
1400
"""Dump the Django DB to /var/log/maas."""
1401
ret, stdout, stderr = run_command([
1402
"sudo", maas_region_admin, "dumpdata",
1404
"--exclude", "maasserver.candidatename",
1406
log_dir = "/var/log/maas"
1407
stdout_path = os.path.join(log_dir, 'dumpdata.stdout')
1408
with open(stdout_path, 'wb') as w_file:
1409
w_file.write(stdout.encode('utf-8'))
1410
stderr_path = os.path.join(log_dir, 'dumpdata.stderr')
1411
with open(stderr_path, 'wb') as w_file:
1412
w_file.write(stderr.encode('utf-8'))
1416
# Signal to the cluster that the region controller tests finished.
1417
if not USE_CC_NODES:
1418
raise SkipTest("Not testing Cluster controller")
1419
context = zmq.Context()
1420
socket = context.socket(zmq.REQ)
1421
socket.connect('tcp://%s:5555' % CLUSTER_CONTROLLER_IP)
1422
socket.send("Region controller tests finished.")
1425
def copy_juju_log(cls):
1426
# Use timeout because juju debug-log never returns (known juju bug).
1427
ret, stdout, stderr = run_command(
1428
["timeout", "10", "juju", "debug-log", "--replay", "-l", "TRACE"])
1429
log_dir = "/var/log/maas"
1430
stdout_path = os.path.join(log_dir, 'juju-log-replay.stdout')
1431
with open(stdout_path, 'wb') as w_file:
1432
w_file.write(stdout.encode('utf-8'))
1433
ret, stdout, stderr = run_command([
1434
"juju", "scp", "0:/var/log/juju/*", log_dir])
1437
def tearDownClass(cls):