Skip to content

Commit 0aa175b

Browse files
authored
Merge pull request #319 from xcp-ng/installer-features/prereqs
Installer features prerequisites
2 parents 45327bf + feebb8a commit 0aa175b

File tree

7 files changed

+85
-75
lines changed

7 files changed

+85
-75
lines changed

conftest.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,10 +432,9 @@ def vm_ref(request):
432432
if ref is None:
433433
# get default VM from test if there's one
434434
marker = request.node.get_closest_marker("default_vm")
435-
default_vm = marker.args[0] if marker is not None else None
436-
if default_vm is not None:
437-
logging.info(">> No VM specified on CLI. Using default: %s." % default_vm)
438-
ref = default_vm
435+
if marker is not None:
436+
ref = marker.args[0]
437+
logging.info(">> No VM specified on CLI. Using default: %s.", ref)
439438
else:
440439
# global default
441440
logging.info(">> No VM specified on CLI, and no default found in test definition. Using global default.")

data.py-dist

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Configuration file, to be adapted to one's needs
22

3-
from typing import Any, Dict, TYPE_CHECKING
3+
from __future__ import annotations
44

55
import legacycrypt as crypt # type: ignore
66
import os
7+
from typing import Any, TYPE_CHECKING
78

89
if TYPE_CHECKING:
910
from lib.typing import IsoImageDef
@@ -21,7 +22,7 @@ def hash_password(password):
2122

2223
HOST_DEFAULT_PASSWORD_HASH = hash_password(HOST_DEFAULT_PASSWORD)
2324

24-
# Public key for a private key available to the test runner
25+
# Public keys for a private keys available to the test runner
2526
TEST_SSH_PUBKEY = """
2627
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMnN/wVdQqHA8KsndfrLS7fktH/IEgxoa533efuXR6rw XCP-ng CI
2728
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKz9uQOoxq6Q0SQ0XTzQHhDolvuo/7EyrDZsYQbRELhcPJG8MT/o5u3HyJFhIP2+HqBSXXgmqRPJUkwz9wUwb2sUwf44qZm/pyPUWOoxyVtrDXzokU/uiaNKUMhbnfaXMz6Ogovtjua63qld2+ZRXnIgrVtYKtYBeu/qKGVSnf4FTOUKl1w3uKkr59IUwwAO8ay3wVnxXIHI/iJgq6JBgQNHbn3C/SpYU++nqL9G7dMyqGD36QPFuqH/cayL8TjNZ67TgAzsPX8OvmRSqjrv3KFbeSlpS/R4enHkSemhgfc8Z2f49tE7qxWZ6x4Uyp5E6ur37FsRf/tEtKIUJGMRXN XCP-ng CI
@@ -37,7 +38,7 @@ OBJECTS_NAME_PREFIX = None
3738
# skip_xo_config allows to not touch XO's configuration regarding the host
3839
# Else the default behaviour is to add the host to XO servers at the beginning
3940
# of the testing session and remove it at the end.
40-
HOSTS: Dict[str, Dict[str, Any]] = {
41+
HOSTS: dict[str, dict[str, Any]] = {
4142
# "10.0.0.1": {"user": "root", "password": ""},
4243
# "testhost1": {"user": "root", "password": "", 'skip_xo_config': True},
4344
}
@@ -107,7 +108,7 @@ OTHER_GUEST_TOOLS = {
107108
}
108109

109110
# Tools
110-
TOOLS: Dict[str, str] = {
111+
TOOLS: dict[str, str] = {
111112
# "iso-remaster": "/home/user/src/xcpng/xcp/scripts/iso-remaster/iso-remaster.sh",
112113
}
113114

@@ -127,7 +128,7 @@ ISO_IMAGES_CACHE = "/home/user/iso"
127128
# for local-only ISO with things like "locally-built/my.iso" or "xs/8.3.iso".
128129
# If 'net-only' is set to 'True' only source of type URL will be possible.
129130
# By default the parameter is set to False.
130-
ISO_IMAGES: Dict[str, "IsoImageDef"] = {
131+
ISO_IMAGES: dict[str, "IsoImageDef"] = {
131132
'83nightly': {'path': os.environ.get("XCPNG83_NIGHTLY",
132133
"http://unconfigured.iso"),
133134
'unsigned': True},
@@ -182,44 +183,44 @@ DEFAULT_SR = 'default'
182183
CACHE_IMPORTED_VM = False
183184

184185
# Default NFS device config:
185-
NFS_DEVICE_CONFIG: Dict[str, Dict[str, str]] = {
186+
NFS_DEVICE_CONFIG: dict[str, dict[str, str]] = {
186187
# 'server': '10.0.0.2', # URL/Hostname of NFS server
187188
# 'serverpath': '/path/to/shared/mount' # Path to shared mountpoint
188189
}
189190

190191
# Default NFS4+ only device config:
191-
NFS4_DEVICE_CONFIG: Dict[str, Dict[str, str]] = {
192+
NFS4_DEVICE_CONFIG: dict[str, dict[str, str]] = {
192193
# 'server': '10.0.0.2', # URL/Hostname of NFS server
193194
# 'serverpath': '/path_to_shared_mount' # Path to shared mountpoint
194195
# 'nfsversion': '4.1'
195196
}
196197

197198
# Default NFS ISO device config:
198-
NFS_ISO_DEVICE_CONFIG: Dict[str, Dict[str, str]] = {
199+
NFS_ISO_DEVICE_CONFIG: dict[str, dict[str, str]] = {
199200
# 'location': '10.0.0.2:/path/to/shared/mount' # URL/Hostname of NFS server and path to shared mountpoint
200201
}
201202

202203
# Default CIFS ISO device config:
203-
CIFS_ISO_DEVICE_CONFIG: Dict[str, Dict[str, str]] = {
204+
CIFS_ISO_DEVICE_CONFIG: dict[str, dict[str, str]] = {
204205
# 'location': r'\\10.0.0.2\<shared folder name>',
205206
# 'username': '<user>',
206207
# 'cifspassword': '<password>',
207208
# 'type': 'cifs',
208209
# 'vers': '<1.0> or <3.0>'
209210
}
210211

211-
CEPHFS_DEVICE_CONFIG: Dict[str, Dict[str, str]] = {
212+
CEPHFS_DEVICE_CONFIG: dict[str, dict[str, str]] = {
212213
# 'server': '10.0.0.2',
213214
# 'serverpath': '/vms'
214215
}
215216

216-
MOOSEFS_DEVICE_CONFIG: Dict[str, Dict[str, str]] = {
217+
MOOSEFS_DEVICE_CONFIG: dict[str, dict[str, str]] = {
217218
# 'masterhost': 'mfsmaster',
218219
# 'masterport': '9421',
219220
# 'rootpath': '/vms'
220221
}
221222

222-
LVMOISCSI_DEVICE_CONFIG: Dict[str, Dict[str, str]] = {
223+
LVMOISCSI_DEVICE_CONFIG: dict[str, dict[str, str]] = {
223224
# 'target': '192.168.1.1',
224225
# 'port': '3260',
225226
# 'targetIQN': 'target.example',
@@ -248,16 +249,7 @@ BASE_ANSWERFILES = dict(
248249
},
249250
)
250251

251-
IMAGE_EQUIVS: Dict[str, str] = {
252+
IMAGE_EQUIVS: dict[str, str] = {
252253
# 'install.test::Nested::install[bios-830-ext]-vm1-607cea0c825a4d578fa5fab56978627d8b2e28bb':
253254
# 'install.test::Nested::install[bios-830-ext]-vm1-addb4ead4da49856e1d2fb3ddf4e31027c6b693b',
254255
}
255-
256-
# compatibility settings for older tests
257-
DEFAULT_NFS_DEVICE_CONFIG = NFS_DEVICE_CONFIG
258-
DEFAULT_NFS4_DEVICE_CONFIG = NFS4_DEVICE_CONFIG
259-
DEFAULT_NFS_ISO_DEVICE_CONFIG = NFS_ISO_DEVICE_CONFIG
260-
DEFAULT_CIFS_ISO_DEVICE_CONFIG = CIFS_ISO_DEVICE_CONFIG
261-
DEFAULT_CEPHFS_DEVICE_CONFIG = CEPHFS_DEVICE_CONFIG
262-
DEFAULT_MOOSEFS_DEVICE_CONFIG = MOOSEFS_DEVICE_CONFIG
263-
DEFAULT_LVMOISCSI_DEVICE_CONFIG = LVMOISCSI_DEVICE_CONFIG

lib/installer.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def write_xml(self, filename):
1919

2020
def top_append(self, *defs):
2121
for defn in defs:
22+
if defn is None:
23+
continue
2224
self.defn['CONTENTS'].append(self._normalize_structure(defn))
2325
return self
2426

@@ -30,15 +32,30 @@ def top_setattr(self, attrs):
3032
# makes a mutable deep copy of all `contents`
3133
@staticmethod
3234
def _normalize_structure(defn):
33-
assert isinstance(defn, dict)
34-
assert 'TAG' in defn
35-
defn = dict(defn)
36-
if 'CONTENTS' not in defn:
37-
defn['CONTENTS'] = []
38-
if not isinstance(defn['CONTENTS'], str):
39-
defn['CONTENTS'] = [AnswerFile._normalize_structure(item)
40-
for item in defn['CONTENTS']]
41-
return defn
35+
assert isinstance(defn, dict), f"{defn!r} is not a dict"
36+
assert 'TAG' in defn, f"{defn} has no TAG"
37+
38+
# type mutation through nearly-shallow copy
39+
new_defn = {
40+
'TAG': defn['TAG'],
41+
'CONTENTS': [],
42+
}
43+
for key, value in defn.items():
44+
if key == 'CONTENTS':
45+
if isinstance(value, str):
46+
new_defn['CONTENTS'] = value
47+
else:
48+
new_defn['CONTENTS'] = [
49+
AnswerFile._normalize_structure(item)
50+
for item in value
51+
if item is not None
52+
]
53+
elif key == 'TAG':
54+
pass # already copied
55+
else:
56+
new_defn[key] = value
57+
58+
return new_defn
4259

4360
# convert to a ElementTree.Element tree suitable for further
4461
# modification before we serialize it to XML
@@ -50,14 +67,14 @@ def _defn_to_xml_et(defn, /, *, parent=None):
5067
assert isinstance(name, str)
5168
contents = defn.pop('CONTENTS', ())
5269
assert isinstance(contents, (str, list))
53-
element = ET.Element(name, **defn)
70+
element = ET.Element(name, {}, **defn)
5471
if parent is not None:
5572
parent.append(element)
5673
if isinstance(contents, str):
5774
element.text = contents
5875
else:
59-
for contents in contents:
60-
AnswerFile._defn_to_xml_et(contents, parent=element)
76+
for content in contents:
77+
AnswerFile._defn_to_xml_et(content, parent=element)
6178
return element
6279

6380
def poweroff(ip):

lib/pxe.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from __future__ import annotations
2+
13
from lib.commands import ssh, scp
24
from data import ARP_SERVER, PXE_CONFIG_SERVER
35

46
PXE_CONFIG_DIR = "/pxe/configs/custom"
57

6-
def generate_boot_conf(directory, installer, action):
8+
def generate_boot_conf(directory: str, installer: str, action: str) -> None:
79
# in case of restore, we disable the text ui from the installer completely,
810
# to workaround a bug that leaves us stuck on a confirmation dialog at the end of the operation.
911
rt = 'rt=1' if action == 'restore' else ''
@@ -15,25 +17,25 @@ def generate_boot_conf(directory, installer, action):
1517
{rt}
1618
""")
1719

18-
def server_push_config(mac_address, tmp_local_path):
20+
def server_push_config(mac_address: str, tmp_local_path: str) -> None:
1921
assert mac_address
2022
remote_dir = f'{PXE_CONFIG_DIR}/{mac_address}/'
2123
server_remove_config(mac_address)
2224
ssh(PXE_CONFIG_SERVER, ['mkdir', '-p', remote_dir])
2325
scp(PXE_CONFIG_SERVER, f'{tmp_local_path}/boot.conf', remote_dir)
2426
scp(PXE_CONFIG_SERVER, f'{tmp_local_path}/answerfile.xml', remote_dir)
2527

26-
def server_remove_config(mac_address):
28+
def server_remove_config(mac_address: str) -> None:
2729
assert mac_address # protection against deleting the whole parent dir!
2830
remote_dir = f'{PXE_CONFIG_DIR}/{mac_address}/'
2931
ssh(PXE_CONFIG_SERVER, ['rm', '-rf', remote_dir])
3032

31-
def server_remove_bootconf(mac_address):
33+
def server_remove_bootconf(mac_address: str) -> None:
3234
assert mac_address
3335
distant_file = f'{PXE_CONFIG_DIR}/{mac_address}/boot.conf'
3436
ssh(PXE_CONFIG_SERVER, ['rm', '-rf', distant_file])
3537

36-
def arp_addresses_for(mac_address):
38+
def arp_addresses_for(mac_address: str) -> list[str]:
3739
output = ssh(
3840
ARP_SERVER,
3941
['ip', 'neigh', 'show', 'nud', 'reachable',

lib/typing.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import sys
12
from typing import TypedDict
2-
from typing_extensions import NotRequired
3+
4+
if sys.version_info >= (3, 11):
5+
from typing import NotRequired
6+
else:
7+
from typing_extensions import NotRequired
38

49
IsoImageDef = TypedDict('IsoImageDef',
510
{'path': str,

tests/install/conftest.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,6 @@ def answerfile(request):
6868
answerfile_def = callable_marker(marker.args[0], request)
6969
assert isinstance(answerfile_def, AnswerFile)
7070

71-
answerfile_def.top_append(
72-
dict(TAG="admin-interface",
73-
name="eth0",
74-
proto="dhcp",
75-
),
76-
)
77-
7871
yield answerfile_def
7972

8073

@@ -103,7 +96,7 @@ def installer_iso(request):
10396
)
10497

10598
@pytest.fixture(scope='function')
106-
def install_disk(request):
99+
def system_disks_names(request):
107100
firmware = request.getfixturevalue("firmware")
108101
yield {"uefi": "nvme0n1", "bios": "sda"}[firmware]
109102

@@ -179,7 +172,7 @@ def remastered_iso(installer_iso, answerfile):
179172
test "$eth_mac" = "$br_mac"
180173
fi
181174
182-
if [ $(readlink "/bin/ping") = busybox ]; then
175+
if [ "$(readlink /bin/ping)" = busybox ]; then
183176
# XS before 7.0
184177
PINGARGS=""
185178
else

tests/install/test.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,18 @@ def helper_vm_with_plugged_disk(running_vm, create_vms):
2626

2727
all_vdis = [VDI(uuid, host=host_vm.host) for uuid in host_vm.vdi_uuids()]
2828
disk_vdis = [vdi for vdi in all_vdis if not vdi.readonly()]
29-
vdi, = disk_vdis
3029

31-
vbd = helper_vm.create_vbd("1", vdi.uuid)
30+
vbds = [helper_vm.create_vbd(str(n + 1), vdi.uuid) for n, vdi in enumerate(disk_vdis)]
3231
try:
33-
vbd.plug()
32+
for vbd in vbds:
33+
vbd.plug()
3434

3535
yield helper_vm
3636

3737
finally:
38-
vbd.unplug()
39-
vbd.destroy()
38+
for vbd in reversed(vbds):
39+
vbd.unplug()
40+
vbd.destroy()
4041

4142
@pytest.mark.dependency()
4243
class TestNested:
@@ -78,18 +79,19 @@ class TestNested:
7879
vifs=[dict(index=0, network_name=NETWORKS["MGMT"])],
7980
))
8081
@pytest.mark.answerfile(
81-
lambda install_disk, local_sr, package_source, iso_version: AnswerFile("INSTALL")
82+
lambda system_disks_names, local_sr, package_source, iso_version: AnswerFile("INSTALL")
8283
.top_setattr({} if local_sr == "nosr" else {"sr-type": local_sr})
8384
.top_append(
84-
{"TAG": "source", "type": "local"} if package_source == "iso"
85-
else {"TAG": "source", "type": "url",
86-
"CONTENTS": ISO_IMAGES[iso_version]['net-url']} if package_source == "net"
87-
else {},
85+
{"iso": {"TAG": "source", "type": "local"},
86+
"net": {"TAG": "source", "type": "url",
87+
"CONTENTS": ISO_IMAGES[iso_version]['net-url']},
88+
}[package_source],
89+
{"TAG": "admin-interface", "name": "eth0", "proto": "dhcp"},
8890
{"TAG": "primary-disk",
8991
"guest-storage": "no" if local_sr == "nosr" else "yes",
90-
"CONTENTS": install_disk},
92+
"CONTENTS": system_disks_names[0]},
9193
))
92-
def test_install(self, vm_booted_with_installer, install_disk,
94+
def test_install(self, vm_booted_with_installer, system_disks_names,
9395
firmware, iso_version, package_source, local_sr):
9496
host_vm = vm_booted_with_installer
9597
installer.monitor_install(ip=host_vm.ip)
@@ -332,15 +334,15 @@ def test_boot_inst(self, create_vms,
332334
vm="vm1",
333335
image_test=f"TestNested::test_boot_inst[{firmware}-{orig_version}-{machine}-{package_source}-{local_sr}]")])
334336
@pytest.mark.answerfile(
335-
lambda install_disk, package_source, iso_version: AnswerFile("UPGRADE").top_append(
336-
{"TAG": "source", "type": "local"} if package_source == "iso"
337-
else {"TAG": "source", "type": "url",
338-
"CONTENTS": ISO_IMAGES[iso_version]['net-url']} if package_source == "net"
339-
else {},
337+
lambda system_disks_names, package_source, iso_version: AnswerFile("UPGRADE").top_append(
338+
{"iso": {"TAG": "source", "type": "local"},
339+
"net": {"TAG": "source", "type": "url",
340+
"CONTENTS": ISO_IMAGES[iso_version]['net-url']},
341+
}[package_source],
340342
{"TAG": "existing-installation",
341-
"CONTENTS": install_disk},
343+
"CONTENTS": system_disks_names[0]},
342344
))
343-
def test_upgrade(self, vm_booted_with_installer, install_disk,
345+
def test_upgrade(self, vm_booted_with_installer, system_disks_names,
344346
firmware, orig_version, iso_version, machine, package_source, local_sr):
345347
host_vm = vm_booted_with_installer
346348
installer.monitor_upgrade(ip=host_vm.ip)
@@ -393,11 +395,11 @@ def test_boot_upg(self, create_vms,
393395
vm="vm1",
394396
image_test=f"TestNested::test_boot_upg[{firmware}-{orig_version}-host1-{package_source}-{local_sr}]")])
395397
@pytest.mark.answerfile(
396-
lambda install_disk: AnswerFile("RESTORE").top_append(
398+
lambda system_disks_names: AnswerFile("RESTORE").top_append(
397399
{"TAG": "backup-disk",
398-
"CONTENTS": install_disk},
400+
"CONTENTS": system_disks_names[0]},
399401
))
400-
def test_restore(self, vm_booted_with_installer, install_disk,
402+
def test_restore(self, vm_booted_with_installer, system_disks_names,
401403
firmware, orig_version, iso_version, package_source, local_sr):
402404
host_vm = vm_booted_with_installer
403405
installer.monitor_restore(ip=host_vm.ip)

0 commit comments

Comments
 (0)