Skip to content

Commit e036f78

Browse files
committed
image: T4516: add raid-1 install support
1 parent bd70176 commit e036f78

File tree

5 files changed

+426
-92
lines changed

5 files changed

+426
-92
lines changed

python/vyos/system/disk.py

+64-19
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,20 @@
1515

1616
from json import loads as json_loads
1717
from os import sync
18+
from dataclasses import dataclass
1819

1920
from psutil import disk_partitions
2021

2122
from vyos.utils.process import run, cmd
2223

2324

25+
@dataclass
26+
class DiskDetails:
27+
"""Disk details"""
28+
name: str
29+
partition: dict[str, str]
30+
31+
2432
def disk_cleanup(drive_path: str) -> None:
2533
"""Clean up disk partition table (MBR and GPT)
2634
Zeroize primary and secondary headers - first and last 17408 bytes
@@ -67,6 +75,62 @@ def parttable_create(drive_path: str, root_size: int) -> None:
6775
sync()
6876
run(f'partprobe {drive_path}')
6977

78+
partitions: list[str] = partition_list(drive_path)
79+
80+
disk: DiskDetails = DiskDetails(
81+
name = drive_path,
82+
partition = {
83+
'efi': next(x for x in partitions if x.endswith('2')),
84+
'root': next(x for x in partitions if x.endswith('3'))
85+
}
86+
)
87+
88+
return disk
89+
90+
91+
def partition_list(drive_path: str) -> list[str]:
92+
"""Get a list of partitions on a drive
93+
94+
Args:
95+
drive_path (str): path to a drive
96+
97+
Returns:
98+
list[str]: a list of partition paths
99+
"""
100+
lsblk: str = cmd(f'lsblk -Jp {drive_path}')
101+
drive_info: dict = json_loads(lsblk)
102+
device: list = drive_info.get('blockdevices')
103+
children: list[str] = device[0].get('children', []) if device else []
104+
partitions: list[str] = [child.get('name') for child in children]
105+
return partitions
106+
107+
108+
def partition_parent(partition_path: str) -> str:
109+
"""Get a parent device for a partition
110+
111+
Args:
112+
partition (str): path to a partition
113+
114+
Returns:
115+
str: path to a parent device
116+
"""
117+
parent: str = cmd(f'lsblk -ndpo pkname {partition_path}')
118+
return parent
119+
120+
121+
def from_partition(partition_path: str) -> DiskDetails:
122+
drive_path: str = partition_parent(partition_path)
123+
partitions: list[str] = partition_list(drive_path)
124+
125+
disk: DiskDetails = DiskDetails(
126+
name = drive_path,
127+
partition = {
128+
'efi': next(x for x in partitions if x.endswith('2')),
129+
'root': next(x for x in partitions if x.endswith('3'))
130+
}
131+
)
132+
133+
return disk
70134

71135
def filesystem_create(partition: str, fstype: str) -> None:
72136
"""Create a filesystem on a partition
@@ -138,25 +202,6 @@ def find_device(mountpoint: str) -> str:
138202
return ''
139203

140204

141-
def raid_create(raid_name: str,
142-
raid_members: list[str],
143-
raid_level: str = 'raid1') -> None:
144-
"""Create a RAID array
145-
146-
Args:
147-
raid_name (str): a name of array (data, backup, test, etc.)
148-
raid_members (list[str]): a list of array members
149-
raid_level (str, optional): an array level. Defaults to 'raid1'.
150-
"""
151-
raid_devices_num: int = len(raid_members)
152-
raid_members_str: str = ' '.join(raid_members)
153-
command: str = f'mdadm --create /dev/md/{raid_name} --metadata=1.2 \
154-
--raid-devices={raid_devices_num} --level={raid_level} \
155-
{raid_members_str}'
156-
157-
run(command)
158-
159-
160205
def disks_size() -> dict[str, int]:
161206
"""Get a dictionary with physical disks and their sizes
162207

python/vyos/system/grub.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from uuid import uuid5, NAMESPACE_URL, UUID
2020

2121
from vyos.template import render
22-
from vyos.utils.process import run, cmd
22+
from vyos.utils.process import cmd
2323
from vyos.system import disk
2424

2525
# Define variables
@@ -49,7 +49,7 @@
4949
REGEX_KERNEL_CMDLINE: str = r'^BOOT_IMAGE=/(?P<boot_type>boot|live)/((?P<image_version>.+)/)?vmlinuz.*$'
5050

5151

52-
def install(drive_path: str, boot_dir: str, efi_dir: str) -> None:
52+
def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS') -> None:
5353
"""Install GRUB for both BIOS and EFI modes (hybrid boot)
5454
5555
Args:
@@ -62,11 +62,11 @@ def install(drive_path: str, boot_dir: str, efi_dir: str) -> None:
6262
{drive_path} --force',
6363
f'grub-install --no-floppy --recheck --target=x86_64-efi \
6464
--force-extra-removable --boot-directory={boot_dir} \
65-
--efi-directory={efi_dir} --bootloader-id="VyOS" \
65+
--efi-directory={efi_dir} --bootloader-id="{id}" \
6666
--no-uefi-secure-boot'
6767
]
6868
for command in commands:
69-
run(command)
69+
cmd(command)
7070

7171

7272
def gen_version_uuid(version_name: str) -> str:
@@ -294,7 +294,7 @@ def set_default(version_name: str, root_dir: str = '') -> None:
294294
vars_write(vars_file, vars_current)
295295

296296

297-
def common_write(root_dir: str = '') -> None:
297+
def common_write(root_dir: str = '', grub_common: dict[str, str] = {}) -> None:
298298
"""Write common GRUB configuration file (overwrite everything)
299299
300300
Args:
@@ -304,7 +304,7 @@ def common_write(root_dir: str = '') -> None:
304304
if not root_dir:
305305
root_dir = disk.find_persistence()
306306
common_config = f'{root_dir}/{CFG_VYOS_COMMON}'
307-
render(common_config, TMPL_GRUB_COMMON, {})
307+
render(common_config, TMPL_GRUB_COMMON, grub_common)
308308

309309

310310
def create_structure(root_dir: str = '') -> None:
@@ -335,3 +335,6 @@ def set_console_type(console_type: str, root_dir: str = '') -> None:
335335
vars_current: dict[str, str] = vars_read(vars_file)
336336
vars_current['console_type'] = str(console_type)
337337
vars_write(vars_file, vars_current)
338+
339+
def set_raid(root_dir: str = '') -> None:
340+
pass

python/vyos/system/raid.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
2+
#
3+
# This library is free software; you can redistribute it and/or
4+
# modify it under the terms of the GNU Lesser General Public
5+
# License as published by the Free Software Foundation; either
6+
# version 2.1 of the License, or (at your option) any later version.
7+
#
8+
# This library is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+
# Lesser General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU Lesser General Public License
14+
# along with this library. If not, see <http://www.gnu.org/licenses/>.
15+
16+
"""RAID related functions"""
17+
18+
from pathlib import Path
19+
from shutil import copy
20+
from dataclasses import dataclass
21+
22+
from vyos.utils.process import cmd
23+
from vyos.system import disk
24+
25+
26+
@dataclass
27+
class RaidDetails:
28+
"""RAID type"""
29+
name: str
30+
level: str
31+
members: list[str]
32+
disks: list[disk.DiskDetails]
33+
34+
35+
def raid_create(raid_members: list[str],
36+
raid_name: str = 'md0',
37+
raid_level: str = 'raid1') -> None:
38+
"""Create a RAID array
39+
40+
Args:
41+
raid_name (str): a name of array (data, backup, test, etc.)
42+
raid_members (list[str]): a list of array members
43+
raid_level (str, optional): an array level. Defaults to 'raid1'.
44+
"""
45+
raid_devices_num: int = len(raid_members)
46+
raid_members_str: str = ' '.join(raid_members)
47+
if Path('/sys/firmware/efi').exists():
48+
for part in raid_members:
49+
drive: str = disk.partition_parent(part)
50+
command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}'
51+
cmd(command)
52+
else:
53+
for part in raid_members:
54+
drive: str = disk.partition_parent(part)
55+
command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}'
56+
cmd(command)
57+
for part in raid_members:
58+
command: str = f'mdadm --zero-superblock {part}'
59+
cmd(command)
60+
command: str = f'mdadm --create /dev/{raid_name} -R --metadata=1.0 \
61+
--raid-devices={raid_devices_num} --level={raid_level} \
62+
{raid_members_str}'
63+
64+
cmd(command)
65+
66+
raid = RaidDetails(
67+
name = f'/dev/{raid_name}',
68+
level = raid_level,
69+
members = raid_members,
70+
disks = [disk.from_partition(m) for m in raid_members]
71+
)
72+
73+
return raid
74+
75+
def update_initramfs() -> None:
76+
"""Update initramfs"""
77+
mdadm_script = '/etc/initramfs-tools/scripts/local-top/mdadm'
78+
copy('/usr/share/initramfs-tools/scripts/local-block/mdadm', mdadm_script)
79+
p = Path(mdadm_script)
80+
p.write_text(p.read_text().replace('$((COUNT + 1))', '20'))
81+
command: str = 'update-initramfs -u'
82+
cmd(command)
83+
84+
def update_default(target_dir: str) -> None:
85+
"""Update /etc/default/mdadm to start MD monitoring daemon at boot
86+
"""
87+
source_mdadm_config = '/etc/default/mdadm'
88+
target_mdadm_config = Path(target_dir).joinpath('/etc/default/mdadm')
89+
target_mdadm_config_dir = Path(target_mdadm_config).parent
90+
Path.mkdir(target_mdadm_config_dir, parents=True, exist_ok=True)
91+
s = Path(source_mdadm_config).read_text().replace('START_DAEMON=false',
92+
'START_DAEMON=true')
93+
Path(target_mdadm_config).write_text(s)
94+
95+
def get_uuid(device: str) -> str:
96+
"""Get UUID of a device"""
97+
command: str = f'tune2fs -l {device}'
98+
l = cmd(command).splitlines()
99+
uuid = next((x for x in l if x.startswith('Filesystem UUID')), '')
100+
return uuid.split(':')[1].strip() if uuid else ''
101+
102+
def get_uuids(raid_details: RaidDetails) -> tuple[str]:
103+
"""Get UUIDs of RAID members
104+
105+
Args:
106+
raid_name (str): a name of array (data, backup, test, etc.)
107+
108+
Returns:
109+
tuple[str]: root_disk uuid, root_md uuid
110+
"""
111+
raid_name: str = raid_details.name
112+
root_partition: str = raid_details.members[0]
113+
uuid_root_disk: str = get_uuid(root_partition)
114+
uuid_root_md: str = get_uuid(raid_name)
115+
return uuid_root_disk, uuid_root_md

python/vyos/utils/io.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# You should have received a copy of the GNU Lesser General Public
1414
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
1515

16+
from typing import Callable
17+
1618
def print_error(str='', end='\n'):
1719
"""
1820
Print `str` to stderr, terminated with `end`.
@@ -73,7 +75,8 @@ def is_dumb_terminal():
7375
import os
7476
return os.getenv('TERM') in ['vt100', 'dumb']
7577

76-
def select_entry(l: list, list_msg: str = '', prompt_msg: str = '') -> str:
78+
def select_entry(l: list, list_msg: str = '', prompt_msg: str = '',
79+
list_format: Callable = None,) -> str:
7780
"""Select an entry from a list
7881
7982
Args:
@@ -87,7 +90,10 @@ def select_entry(l: list, list_msg: str = '', prompt_msg: str = '') -> str:
8790
en = list(enumerate(l, 1))
8891
print(list_msg)
8992
for i, e in en:
90-
print(f'\t{i}: {e}')
93+
if list_format:
94+
print(f'\t{i}: {list_format(e)}')
95+
else:
96+
print(f'\t{i}: {e}')
9197
select = ask_input(prompt_msg, numeric_only=True,
9298
valid_responses=range(1, len(l)+1))
9399
return next(filter(lambda x: x[0] == select, en))[1]

0 commit comments

Comments
 (0)