def test_get_reader_install(self): # Given the right configuration options, the PXE configuration is # correctly rendered. method = PXEBootMethod() params = make_kernel_parameters(self, purpose="xinstall") fs_host = 'http://%s:5248/images' % (convert_host_to_uri_str( params.fs_host)) output = method.get_reader(backend=None, kernel_params=params) # The output is a BytesReader. self.assertThat(output, IsInstance(BytesReader)) output = output.read(10000).decode("utf-8") # The template has rendered without error. PXELINUX configurations # typically start with a DEFAULT line. self.assertThat(output, StartsWith("DEFAULT ")) # The PXE parameters are all set according to the options. image_dir = compose_image_path(osystem=params.osystem, arch=params.arch, subarch=params.subarch, release=params.release, label=params.label) self.assertThat( output, MatchesAll( MatchesRegex( r'.*^\s+KERNEL %s/%s/%s$' % (re.escape(fs_host), re.escape(image_dir), params.kernel), re.MULTILINE | re.DOTALL), MatchesRegex( r'.*^\s+INITRD %s/%s/%s$' % (re.escape(fs_host), re.escape(image_dir), params.initrd), re.MULTILINE | re.DOTALL), MatchesRegex(r'.*^\s+APPEND .+?$', re.MULTILINE | re.DOTALL)))
def test_link_bootloader_copies_previously_downloaded_files(self): method = PXEBootMethod() with tempdir() as tmp: new_dir = os.path.join(tmp, "new") current_dir = os.path.join(tmp, "current") os.makedirs(new_dir) os.makedirs(current_dir) factory.make_file(current_dir, method.bootloader_files[0]) for bootloader_file in method.bootloader_files[1:]: factory.make_file(current_dir, bootloader_file) real_syslinux_dir = os.path.join(tmp, "syslinux") os.makedirs(real_syslinux_dir) atomic_symlink( real_syslinux_dir, os.path.join(current_dir, "syslinux") ) method.link_bootloader(new_dir) for bootloader_file in method.bootloader_files: bootloader_file_path = os.path.join(new_dir, bootloader_file) self.assertTrue(os.path.isfile(bootloader_file_path)) syslinux_link = os.path.join(new_dir, "syslinux") self.assertTrue(os.path.islink(syslinux_link)) self.assertEqual( real_syslinux_dir, os.path.realpath(syslinux_link) )
def test_get_reader_scenarios(self): method = PXEBootMethod() get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name") get_ephemeral_name.return_value = factory.make_name("ephemeral") osystem = factory.make_name('osystem') arch = factory.make_name('arch') subarch = factory.make_name('subarch') options = { "backend": None, "kernel_params": make_kernel_parameters(testcase=self, osystem=osystem, subarch=subarch, arch=arch, purpose=self.purpose), } fs_host = 'http://%s:5248/images' % (convert_host_to_uri_str( options['kernel_params'].fs_host)) output = method.get_reader(**options).read(10000).decode("utf-8") config = parse_pxe_config(output) # The default section is defined. default_section_label = config.header["DEFAULT"] self.assertThat(config, Contains(default_section_label)) default_section = dict(config[default_section_label]) contains_arch_path = StartsWith("%s/%s/%s/%s" % (fs_host, osystem, arch, subarch)) self.assertThat(default_section["KERNEL"], contains_arch_path) self.assertThat(default_section["INITRD"], contains_arch_path) self.assertEqual("2", default_section["IPAPPEND"])
def test_link_bootloader_links_files_found_on_fs(self): method = PXEBootMethod() bootloader_dir = ('/var/lib/maas/boot-resources/snapshot-%s' % factory.make_name('snapshot')) def fake_exists(path): if '/usr/lib/syslinux/modules/bios' in path: return True else: return False self.patch(pxe_module.os.path, 'exists').side_effect = fake_exists mock_atomic_copy = self.patch(pxe_module, 'atomic_copy') mock_atomic_symlink = self.patch(pxe_module, 'atomic_symlink') mock_shutil_copy = self.patch(pxe_module.shutil, 'copy') method.link_bootloader(bootloader_dir) self.assertThat(mock_atomic_copy, MockNotCalled()) self.assertThat(mock_shutil_copy, MockNotCalled()) for bootloader_file in method.bootloader_files: bootloader_src = os.path.join('/usr/lib/syslinux/modules/bios', bootloader_file) bootloader_dst = os.path.join(bootloader_dir, bootloader_file) self.assertThat(mock_atomic_symlink, MockAnyCall(bootloader_src, bootloader_dst)) self.assertThat( mock_atomic_symlink, MockAnyCall('/usr/lib/syslinux/modules/bios', os.path.join(bootloader_dir, 'syslinux')))
def test_link_bootloader_links_files_found_on_fs(self): method = PXEBootMethod() bootloader_dir = ("/var/lib/maas/boot-resources/snapshot-%s" % factory.make_name("snapshot")) def fake_exists(path): if "/usr/lib/syslinux/modules/bios" in path: return True else: return False self.patch(pxe_module.os.path, "exists").side_effect = fake_exists mock_atomic_copy = self.patch(pxe_module, "atomic_copy") mock_atomic_symlink = self.patch(pxe_module, "atomic_symlink") mock_shutil_copy = self.patch(pxe_module.shutil, "copy") method.link_bootloader(bootloader_dir) self.assertThat(mock_atomic_copy, MockNotCalled()) self.assertThat(mock_shutil_copy, MockNotCalled()) for bootloader_file in method.bootloader_files: bootloader_src = os.path.join("/usr/lib/syslinux/modules/bios", bootloader_file) bootloader_dst = os.path.join(bootloader_dir, bootloader_file) self.assertThat( mock_atomic_symlink, MockAnyCall(bootloader_src, bootloader_dst), ) self.assertThat( mock_atomic_symlink, MockAnyCall( "/usr/lib/syslinux/modules/bios", os.path.join(bootloader_dir, "syslinux"), ), )
def test_link_bootloader_logs_missing_files(self): method = PXEBootMethod() mock_maaslog = self.patch(maaslog, 'error') # If we don't mock the return value and the test system has the # pxelinux and syslinux-common package installed the fallback kicks # which makes PXE work but this test fail. self.patch(os.path, 'exists').return_value = False method.link_bootloader('foo') self.assertThat(mock_maaslog, MockCalledOnce())
def test_get_reader_with_local_purpose(self): # If purpose is "local", the config.localboot.template should be # used. method = PXEBootMethod() options = { "backend": None, "kernel_params": make_kernel_parameters(purpose="local"), } output = method.get_reader(**options).read(10000) self.assertIn(b"LOCALBOOT 0", output)
def test_link_bootloader_logs_missing_files(self): method = PXEBootMethod() mock_maaslog = self.patch(maaslog, "error") # If we don't mock the return value and the test system has the # pxelinux and syslinux-common package installed the fallback kicks # which makes PXE work but this test fail. self.patch(os.path, "exists").return_value = False self.patch(pxe_module, "atomic_symlink") method.link_bootloader("foo") self.assertTrue(mock_maaslog.called)
def test_get_reader_with_local_purpose_amd64_arch(self): # Intel amd64 is a special case and needs to use the chain.c32 # loader as the LOCALBOOT PXE directive is unreliable. method = PXEBootMethod() options = { "backend": None, "kernel_params": make_kernel_parameters(arch="amd64", purpose="local"), } output = method.get_reader(**options).read(10000) self.assertIn(b"chain.c32", output) self.assertNotIn(b"LOCALBOOT", output)
def test_get_reader_ephemeral(self): # Given the right configuration options, the PXE configuration is # correctly rendered. method = PXEBootMethod() xtra = ( "custom_xtra_cfg=http://{{ kernel_params.fs_host }}/" "my_extra_config?mac={{ kernel_params.mac }}" ) params = make_kernel_parameters( self, arch="amd64", subarch="generic", purpose="ephemeral", extra_opts=xtra, ) output = method.get_reader(backend=None, kernel_params=params) # The output is a BytesReader. self.assertThat(output, IsInstance(BytesReader)) output = output.read(10000).decode("utf-8") # The template has rendered without error. PXELINUX configurations # typically start with a DEFAULT line. self.assertThat(output, StartsWith("DEFAULT ")) # The PXE parameters are all set according to the options. image_dir = compose_image_path( osystem=params.osystem, arch=params.arch, subarch=params.subarch, release=params.release, label=params.label, ) self.assertThat( output, MatchesAll( MatchesRegex( r".*^\s+KERNEL %s/%s$" % (re.escape(image_dir), params.kernel), re.MULTILINE | re.DOTALL, ), MatchesRegex( r".*^\s+APPEND initrd=%s/%s\s+" % (re.escape(image_dir), params.initrd), re.MULTILINE | re.DOTALL, ), MatchesRegex( r".*\s+custom_xtra_cfg=http://%s/my_extra_config.*?\s+" % (params.fs_host), re.MULTILINE | re.DOTALL, ), MatchesRegex(r".*\s+maas_url=.+?$", re.MULTILINE | re.DOTALL), ), )
def test_link_simplestream_bootloaders_creates_lpxelinux_and_links(self): method = PXEBootMethod() with tempdir() as tmp: stream_path = os.path.join(tmp, 'bootloader', method.bios_boot_method, method.bootloader_arches[0]) os.makedirs(stream_path) for bootloader_file in method.bootloader_files: factory.make_file(stream_path, bootloader_file) method.link_bootloader(tmp) self.assertTrue(os.path.exists(os.path.join(tmp, 'lpxelinux.0'))) self.assertTrue(os.path.islink(os.path.join(tmp, 'pxelinux.0')))
def test_get_reader_xinstall_mustang_dtb(self): # Architecture specific test. # Given the right configuration options, the PXE configuration is # correctly rendered for Mustang. method = PXEBootMethod() params = make_kernel_parameters( testcase=self, osystem="ubuntu", arch="arm64", subarch="xgene-uboot-mustang", purpose="xinstall", ) output = method.get_reader(backend=None, kernel_params=params) # The output is a BytesReader. self.assertThat(output, IsInstance(BytesReader)) output = output.read(10000).decode("utf-8") # The template has rendered without error. PXELINUX configurations # typically start with a DEFAULT line. self.assertThat(output, StartsWith("DEFAULT ")) # The PXE parameters are all set according to the options. image_dir = compose_image_path( osystem=params.osystem, arch=params.arch, subarch=params.subarch, release=params.release, label=params.label, ) self.assertThat( output, MatchesAll( MatchesRegex( r".*^\s+KERNEL %s/%s$" % (re.escape(image_dir), params.kernel), re.MULTILINE | re.DOTALL, ), MatchesRegex( r".*^\s+INITRD %s/%s$" % (re.escape(image_dir), params.initrd), re.MULTILINE | re.DOTALL, ), MatchesRegex( r".*^\s+FDT %s/%s$" % (re.escape(image_dir), params.boot_dtb), re.MULTILINE | re.DOTALL, ), MatchesRegex(r".*^\s+APPEND .+?$", re.MULTILINE | re.DOTALL), ), )
def test_get_reader_with_extra_arguments_does_not_affect_output(self): # get_reader() allows any keyword arguments as a safety valve. method = PXEBootMethod() options = { "backend": None, "kernel_params": make_kernel_parameters(self, purpose="install"), } # Capture the output before sprinking in some random options. output_before = method.get_reader(**options).read(10000) # Sprinkle some magic in. options.update((factory.make_name("name"), factory.make_name("value")) for _ in range(10)) # Capture the output after sprinking in some random options. output_after = method.get_reader(**options).read(10000) # The generated template is the same. self.assertEqual(output_before, output_after)
def test_get_reader_scenarios(self): # The commissioning config uses an extra PXELINUX module to auto # select between i386 and amd64. method = PXEBootMethod() get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name") get_ephemeral_name.return_value = factory.make_name("ephemeral") osystem = factory.make_name("osystem") options = { "backend": None, "kernel_params": make_kernel_parameters( testcase=self, osystem=osystem, subarch="generic", purpose="enlist", ), } fs_host = "http://%s:5248/images" % ( convert_host_to_uri_str(options["kernel_params"].fs_host) ) output = method.get_reader(**options).read(10000).decode("utf-8") config = parse_pxe_config(output) # The default section is defined. default_section_label = config.header["DEFAULT"] self.assertThat(config, Contains(default_section_label)) default_section = config[default_section_label] # The default section uses the ifcpu64 module, branching to the "i386" # or "amd64" labels accordingly. self.assertEqual("ifcpu64.c32", default_section["KERNEL"]) self.assertEqual( ["amd64", "--", "i386"], default_section["APPEND"].split() ) # Both "i386" and "amd64" sections exist. self.assertThat(config, ContainsAll(("i386", "amd64"))) # Each section defines KERNEL, INITRD, and APPEND settings. The # KERNEL and INITRD ones contain paths referring to their # architectures. for section_label in ("i386", "amd64"): section = config[section_label] self.assertThat( section, ContainsAll(("KERNEL", "INITRD", "APPEND")) ) contains_arch_path = StartsWith( "%s/%s/%s/" % (fs_host, osystem, section_label) ) self.assertThat(section["KERNEL"], contains_arch_path) self.assertThat(section["INITRD"], contains_arch_path) self.assertIn("APPEND", section)
def test_link_simplestream_bootloaders_creates_syslinux_link(self): method = PXEBootMethod() with tempdir() as tmp: stream_path = os.path.join(tmp, 'bootloader', method.bios_boot_method, method.bootloader_arches[0]) os.makedirs(stream_path) for bootloader_file in method.bootloader_files: factory.make_file(stream_path, bootloader_file) method.link_bootloader(tmp) for bootloader_file in method.bootloader_files: bootloader_file_path = os.path.join(tmp, bootloader_file) self.assertTrue(os.path.islink(bootloader_file_path)) syslinux_link = os.path.join(tmp, 'syslinux') self.assertTrue(os.path.islink(syslinux_link)) self.assertEquals(stream_path, os.path.realpath(syslinux_link))
def test_get_boot_method_reader_returns_rendered_params(self): # Fake configuration parameters, as discovered from the file path. fake_params = {"mac": factory.make_mac_address("-")} # Fake kernel configuration parameters, as returned from the RPC call. fake_kernel_params = make_kernel_parameters() fake_params = fake_kernel_params._asdict() # Stub the output of list_boot_images so the label is set in the # kernel parameters. boot_image = { "osystem": fake_params["osystem"], "release": fake_params["release"], "architecture": fake_params["arch"], "subarchitecture": fake_params["subarch"], "purpose": fake_params["purpose"], "supported_subarches": "", "label": fake_params["label"], } self.patch(tftp_module, "list_boot_images").return_value = [boot_image] del fake_params["label"] # Stub RPC call to return the fake configuration parameters. client = Mock() client.localIdent = factory.make_name("system_id") client.return_value = succeed(fake_params) client_service = Mock() client_service.getClientNow.return_value = succeed(client) # get_boot_method_reader() takes a dict() of parameters and returns an # `IReader` of a PXE configuration, rendered by # `PXEBootMethod.get_reader`. backend = TFTPBackend(self.make_dir(), client_service) # Stub get_reader to return the render parameters. method = PXEBootMethod() fake_render_result = factory.make_name("render").encode("utf-8") render_patch = self.patch(method, "get_reader") render_patch.return_value = BytesReader(fake_render_result) # Get the rendered configuration, which will actually be a JSON dump # of the render-time parameters. params_with_ip = dict(fake_params) params_with_ip['remote_ip'] = factory.make_ipv4_address() reader = yield backend.get_boot_method_reader(method, params_with_ip) self.addCleanup(reader.finish) self.assertIsInstance(reader, BytesReader) output = reader.read(10000) # The result has been rendered by `method.get_reader`. self.assertEqual(fake_render_result, output) self.assertThat( method.get_reader, MockCalledOnceWith(backend, kernel_params=fake_kernel_params, **params_with_ip))
def test_get_boot_method_reader_rendered_parms_for_depoyed_ephemeral(self): # Fake kernel configuration parameters, as returned from the RPC call. fake_kernel_params = make_kernel_parameters( purpose="local", label="local", osystem="caringo" ) fake_params = fake_kernel_params._asdict() # Stub the output of list_boot_images so the label is set in the # kernel parameters. boot_image = { "osystem": fake_params["osystem"], "release": fake_params["release"], "architecture": fake_params["arch"], "subarchitecture": fake_params["subarch"], "purpose": "ephemeral", "supported_subarches": "", "label": fake_params["label"], } self.patch(tftp_module, "list_boot_images").return_value = [boot_image] del fake_params["label"] # Stub RPC call to return the fake configuration parameters. client = Mock() client.localIdent = factory.make_name("system_id") client.return_value = succeed(fake_params) client_service = Mock() client_service.getClient.return_value = client # get_boot_method_reader() takes a dict() of parameters and returns an # `IReader` of a PXE configuration, rendered by # `PXEBootMethod.get_reader`. backend = TFTPBackend(self.make_dir(), client_service) # Stub get_reader to return the render parameters. method = PXEBootMethod() fake_render_result = factory.make_name("render").encode("utf-8") render_patch = self.patch(method, "get_reader") render_patch.return_value = BytesReader(fake_render_result) # Get the rendered configuration, which will actually be a JSON dump # of the render-time parameters. reader = yield backend.get_boot_method_reader(method, fake_params) self.addCleanup(reader.finish) self.assertIsInstance(reader, BytesReader) output = reader.read(10000) # The result has been rendered by `method.get_reader`. self.assertEqual(fake_render_result, output)
def test_get_boot_method_reader_returns_rendered_params_local_device(self): # Fake kernel configuration parameters, as returned from the RPC call. fake_kernel_params = make_kernel_parameters( purpose="local", label="local" ) fake_params = fake_kernel_params._asdict() del fake_params["label"] # Set purpose to `local-device` as this is what will be passed on. fake_params["purpose"] = "local-device" # Stub RPC call to return the fake configuration parameters. client = Mock() client.localIdent = factory.make_name("system_id") client.return_value = succeed(fake_params) client_service = Mock() client_service.getClientNow.return_value = succeed(client) # get_boot_method_reader() takes a dict() of parameters and returns an # `IReader` of a PXE configuration, rendered by # `PXEBootMethod.get_reader`. backend = TFTPBackend(self.make_dir(), client_service) # Stub get_reader to return the render parameters. method = PXEBootMethod() fake_render_result = factory.make_name("render").encode("utf-8") render_patch = self.patch(method, "get_reader") render_patch.return_value = BytesReader(fake_render_result) # Get the rendered configuration, which will actually be a JSON dump # of the render-time parameters. params_with_ip = dict(fake_params) params_with_ip["remote_ip"] = factory.make_ipv4_address() reader = yield backend.get_boot_method_reader(method, params_with_ip) self.addCleanup(reader.finish) self.assertIsInstance(reader, BytesReader) output = reader.read(10000) # The result has been rendered by `method.get_reader`. self.assertEqual(fake_render_result, output) self.assertThat( method.get_reader, MockCalledOnceWith( backend, kernel_params=fake_kernel_params, **params_with_ip ), )
def test_template_subdir(self): method = PXEBootMethod() self.assertEqual('pxe', method.template_subdir)
def test_get_boot_method_reader_grabs_new_client_on_lost_conn(self): # Fake kernel configuration parameters, as returned from the RPC call. fake_kernel_params = make_kernel_parameters() fake_params = fake_kernel_params._asdict() # Stub the output of list_boot_images so the label is set in the # kernel parameters. boot_image = { "osystem": fake_params["osystem"], "release": fake_params["release"], "architecture": fake_params["arch"], "subarchitecture": fake_params["subarch"], "purpose": fake_params["purpose"], "supported_subarches": "", "label": fake_params["label"], } self.patch(tftp_module, "list_boot_images").return_value = [boot_image] del fake_params["label"] # Stub RPC call to return the fake configuration parameters. clients = [] for _ in range(10): client = Mock() client.localIdent = factory.make_name("system_id") client.side_effect = lambda *args, **kwargs: ( succeed(dict(fake_params)) ) clients.append(client) client_service = Mock() client_service.getClientNow.side_effect = [ succeed(client) for client in clients ] client_service.getAllClients.side_effect = [clients[1:], clients[2:]] # get_boot_method_reader() takes a dict() of parameters and returns an # `IReader` of a PXE configuration, rendered by # `PXEBootMethod.get_reader`. backend = TFTPBackend(self.make_dir(), client_service) # Stub get_reader to return the render parameters. method = PXEBootMethod() fake_render_result = factory.make_name("render").encode("utf-8") render_patch = self.patch(method, "get_reader") render_patch.return_value = BytesReader(fake_render_result) # Get the reader once. remote_ip = factory.make_ipv4_address() params_with_ip = dict(fake_params) params_with_ip["remote_ip"] = remote_ip reader = yield backend.get_boot_method_reader(method, params_with_ip) self.addCleanup(reader.finish) # The first client is now saved. self.assertEqual(clients[0], backend.client_to_remote[remote_ip]) # Get the reader twice. params_with_ip = dict(fake_params) params_with_ip["remote_ip"] = remote_ip reader = yield backend.get_boot_method_reader(method, params_with_ip) self.addCleanup(reader.finish) # The second client is now saved. self.assertEqual(clients[1], backend.client_to_remote[remote_ip]) # Only the first and second client should have been called once, and # all the other clients should not have been called. self.assertEqual(1, clients[0].call_count) self.assertEqual(1, clients[1].call_count) for idx in range(2, 10): self.assertThat(clients[idx], MockNotCalled())
PowerNVBootMethod, ) from provisioningserver.boot.pxe import PXEBootMethod # noqa:E402 isort:skip from provisioningserver.boot.s390x import ( # noqa:E402 isort:skip S390XBootMethod, ) from provisioningserver.boot.uefi_amd64 import ( # noqa:E402 isort:skip UEFIAMD64BootMethod, UEFIAMD64HTTPBootMethod, ) from provisioningserver.boot.uefi_arm64 import ( # noqa:E402 isort:skip UEFIARM64BootMethod, ) from provisioningserver.boot.windows import ( # noqa:E402 isort:skip WindowsPXEBootMethod, ) builtin_boot_methods = [ IPXEBootMethod(), PXEBootMethod(), UEFIAMD64BootMethod(), UEFIAMD64HTTPBootMethod(), UEFIARM64BootMethod(), OpenFirmwarePPC64ELBootMethod(), PowerNVBootMethod(), WindowsPXEBootMethod(), S390XBootMethod(), ] for method in builtin_boot_methods: BootMethodRegistry.register_item(method.name, method)
def test_bootloader_path(self): method = PXEBootMethod() self.assertEqual('lpxelinux.0', method.bootloader_path)
def test_bootloader_path_does_not_include_tftp_root(self): tftproot = self.make_tftp_root() method = PXEBootMethod() self.assertThat(method.bootloader_path, Not(StartsWith(tftproot.path)))
def test_name(self): method = PXEBootMethod() self.assertEqual('pxe', method.name)
def test_arch_octet(self): method = PXEBootMethod() self.assertEqual('00:00', method.arch_octet)