Skip to content

Commit f722a4b

Browse files
committed
Move LVM helpers to dedicated module
1 parent f2c17c6 commit f722a4b

File tree

4 files changed

+144
-124
lines changed

4 files changed

+144
-124
lines changed

archinstall/lib/disk/device_handler.py

Lines changed: 0 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import json
21
import logging
32
import os
4-
import time
53
from collections.abc import Iterable
64
from pathlib import Path
7-
from typing import Literal, overload
85

96
from parted import Device, Disk, DiskException, FileSystem, Geometry, IOException, Partition, PartitionException, freshDisk, getAllDevices, getDevice, newDisk
107

@@ -19,17 +16,13 @@
1916
DiskEncryption,
2017
FilesystemType,
2118
LsblkInfo,
22-
LvmGroupInfo,
23-
LvmPVInfo,
2419
LvmVolume,
2520
LvmVolumeGroup,
26-
LvmVolumeInfo,
2721
ModificationStatus,
2822
PartitionFlag,
2923
PartitionGUID,
3024
PartitionModification,
3125
PartitionTable,
32-
SectorSize,
3326
Size,
3427
SubvolumeModification,
3528
Unit,
@@ -348,123 +341,12 @@ def format_encrypted(
348341
info(f'luks2 locking device: {dev_path}')
349342
luks_handler.lock()
350343

351-
def _lvm_info(
352-
self,
353-
cmd: str,
354-
info_type: Literal['lv', 'vg', 'pvseg'],
355-
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
356-
raw_info = SysCommand(cmd).decode().split('\n')
357-
358-
# for whatever reason the output sometimes contains
359-
# "File descriptor X leaked leaked on vgs invocation
360-
data = '\n'.join(raw for raw in raw_info if 'File descriptor' not in raw)
361-
362-
debug(f'LVM info: {data}')
363-
364-
reports = json.loads(data)
365-
366-
for report in reports['report']:
367-
if len(report[info_type]) != 1:
368-
raise ValueError('Report does not contain any entry')
369-
370-
entry = report[info_type][0]
371-
372-
match info_type:
373-
case 'pvseg':
374-
return LvmPVInfo(
375-
pv_name=Path(entry['pv_name']),
376-
lv_name=entry['lv_name'],
377-
vg_name=entry['vg_name'],
378-
)
379-
case 'lv':
380-
return LvmVolumeInfo(
381-
lv_name=entry['lv_name'],
382-
vg_name=entry['vg_name'],
383-
lv_size=Size(int(entry['lv_size'][:-1]), Unit.B, SectorSize.default()),
384-
)
385-
case 'vg':
386-
return LvmGroupInfo(
387-
vg_uuid=entry['vg_uuid'],
388-
vg_size=Size(int(entry['vg_size'][:-1]), Unit.B, SectorSize.default()),
389-
)
390-
391-
return None
392-
393-
@overload
394-
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['lv']) -> LvmVolumeInfo | None: ...
395-
396-
@overload
397-
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['vg']) -> LvmGroupInfo | None: ...
398-
399-
@overload
400-
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['pvseg']) -> LvmPVInfo | None: ...
401-
402-
def _lvm_info_with_retry(
403-
self,
404-
cmd: str,
405-
info_type: Literal['lv', 'vg', 'pvseg'],
406-
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
407-
# Retry for up to 5 mins
408-
max_retries = 100
409-
for attempt in range(max_retries):
410-
try:
411-
return self._lvm_info(cmd, info_type)
412-
except ValueError:
413-
if attempt < max_retries - 1:
414-
debug(f'LVM info query failed (attempt {attempt + 1}/{max_retries}), retrying in 3 seconds...')
415-
time.sleep(3)
416-
417-
debug(f'LVM info query failed after {max_retries} attempts')
418-
return None
419-
420-
def lvm_vol_info(self, lv_name: str) -> LvmVolumeInfo | None:
421-
cmd = f'lvs --reportformat json --unit B -S lv_name={lv_name}'
422-
423-
return self._lvm_info_with_retry(cmd, 'lv')
424-
425-
def lvm_group_info(self, vg_name: str) -> LvmGroupInfo | None:
426-
cmd = f'vgs --reportformat json --unit B -o vg_name,vg_uuid,vg_size -S vg_name={vg_name}'
427-
428-
return self._lvm_info_with_retry(cmd, 'vg')
429-
430-
def lvm_pvseg_info(self, vg_name: str, lv_name: str) -> LvmPVInfo | None:
431-
cmd = f'pvs --segments -o+lv_name,vg_name -S vg_name={vg_name},lv_name={lv_name} --reportformat json '
432-
433-
return self._lvm_info_with_retry(cmd, 'pvseg')
434-
435-
def lvm_vol_change(self, vol: LvmVolume, activate: bool) -> None:
436-
active_flag = 'y' if activate else 'n'
437-
cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}'
438-
439-
debug(f'lvchange volume: {cmd}')
440-
SysCommand(cmd)
441-
442344
def lvm_export_vg(self, vg: LvmVolumeGroup) -> None:
443345
cmd = f'vgexport {vg.name}'
444346

445347
debug(f'vgexport: {cmd}')
446348
SysCommand(cmd)
447349

448-
def lvm_import_vg(self, vg: LvmVolumeGroup) -> None:
449-
# Check if the VG is actually exported before trying to import it
450-
check_cmd = f'vgs --noheadings -o vg_exported {vg.name}'
451-
452-
try:
453-
result = SysCommand(check_cmd)
454-
is_exported = result.decode().strip() == 'exported'
455-
except SysCallError:
456-
# VG might not exist yet, skip import
457-
debug(f'Volume group {vg.name} not found, skipping import')
458-
return
459-
460-
if not is_exported:
461-
debug(f'Volume group {vg.name} is already active (not exported), skipping import')
462-
return
463-
464-
cmd = f'vgimport {vg.name}'
465-
debug(f'vgimport: {cmd}')
466-
SysCommand(cmd)
467-
468350
def lvm_vol_reduce(self, vol_path: Path, amount: Size) -> None:
469351
val = amount.format_size(Unit.B, include_unit=False)
470352
cmd = f'lvreduce -L -{val}B {vol_path}'

archinstall/lib/disk/filesystem.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from ..output import debug, info
2424
from .device_handler import device_handler
25+
from .lvm import lvm_group_info, lvm_vol_info
2526

2627

2728
class FilesystemHandler:
@@ -168,7 +169,7 @@ def _setup_lvm(
168169
device_handler.lvm_vg_create(pv_dev_paths, vg.name)
169170

170171
# figure out what the actual available size in the group is
171-
vg_info = device_handler.lvm_group_info(vg.name)
172+
vg_info = lvm_group_info(vg.name)
172173

173174
if not vg_info:
174175
raise ValueError('Unable to fetch VG info')
@@ -202,7 +203,7 @@ def _setup_lvm(
202203

203204
while True:
204205
debug('Fetching LVM volume info')
205-
lv_info = device_handler.lvm_vol_info(lv.name)
206+
lv_info = lvm_vol_info(lv.name)
206207
if lv_info is not None:
207208
break
208209

archinstall/lib/disk/lvm.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import json
2+
import time
3+
from pathlib import Path
4+
from typing import Literal, overload
5+
6+
from archinstall.lib.command import SysCommand
7+
from archinstall.lib.exceptions import SysCallError
8+
from archinstall.lib.models.device import (
9+
LvmGroupInfo,
10+
LvmPVInfo,
11+
LvmVolume,
12+
LvmVolumeGroup,
13+
LvmVolumeInfo,
14+
SectorSize,
15+
Size,
16+
Unit,
17+
)
18+
from archinstall.lib.output import debug
19+
20+
21+
def _lvm_info(
22+
cmd: str,
23+
info_type: Literal['lv', 'vg', 'pvseg'],
24+
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
25+
raw_info = SysCommand(cmd).decode().split('\n')
26+
27+
# for whatever reason the output sometimes contains
28+
# "File descriptor X leaked leaked on vgs invocation
29+
data = '\n'.join(raw for raw in raw_info if 'File descriptor' not in raw)
30+
31+
debug(f'LVM info: {data}')
32+
33+
reports = json.loads(data)
34+
35+
for report in reports['report']:
36+
if len(report[info_type]) != 1:
37+
raise ValueError('Report does not contain any entry')
38+
39+
entry = report[info_type][0]
40+
41+
match info_type:
42+
case 'pvseg':
43+
return LvmPVInfo(
44+
pv_name=Path(entry['pv_name']),
45+
lv_name=entry['lv_name'],
46+
vg_name=entry['vg_name'],
47+
)
48+
case 'lv':
49+
return LvmVolumeInfo(
50+
lv_name=entry['lv_name'],
51+
vg_name=entry['vg_name'],
52+
lv_size=Size(int(entry['lv_size'][:-1]), Unit.B, SectorSize.default()),
53+
)
54+
case 'vg':
55+
return LvmGroupInfo(
56+
vg_uuid=entry['vg_uuid'],
57+
vg_size=Size(int(entry['vg_size'][:-1]), Unit.B, SectorSize.default()),
58+
)
59+
60+
return None
61+
62+
63+
@overload
64+
def _lvm_info_with_retry(cmd: str, info_type: Literal['lv']) -> LvmVolumeInfo | None: ...
65+
66+
67+
@overload
68+
def _lvm_info_with_retry(cmd: str, info_type: Literal['vg']) -> LvmGroupInfo | None: ...
69+
70+
71+
@overload
72+
def _lvm_info_with_retry(cmd: str, info_type: Literal['pvseg']) -> LvmPVInfo | None: ...
73+
74+
75+
def _lvm_info_with_retry(
76+
cmd: str,
77+
info_type: Literal['lv', 'vg', 'pvseg'],
78+
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
79+
# Retry for up to 5 mins
80+
max_retries = 100
81+
for attempt in range(max_retries):
82+
try:
83+
return _lvm_info(cmd, info_type)
84+
except ValueError:
85+
if attempt < max_retries - 1:
86+
debug(f'LVM info query failed (attempt {attempt + 1}/{max_retries}), retrying in 3 seconds...')
87+
time.sleep(3)
88+
89+
debug(f'LVM info query failed after {max_retries} attempts')
90+
return None
91+
92+
93+
def lvm_vol_info(lv_name: str) -> LvmVolumeInfo | None:
94+
cmd = f'lvs --reportformat json --unit B -S lv_name={lv_name}'
95+
96+
return _lvm_info_with_retry(cmd, 'lv')
97+
98+
99+
def lvm_group_info(vg_name: str) -> LvmGroupInfo | None:
100+
cmd = f'vgs --reportformat json --unit B -o vg_name,vg_uuid,vg_size -S vg_name={vg_name}'
101+
102+
return _lvm_info_with_retry(cmd, 'vg')
103+
104+
105+
def lvm_pvseg_info(vg_name: str, lv_name: str) -> LvmPVInfo | None:
106+
cmd = f'pvs --segments -o+lv_name,vg_name -S vg_name={vg_name},lv_name={lv_name} --reportformat json '
107+
108+
return _lvm_info_with_retry(cmd, 'pvseg')
109+
110+
111+
def lvm_vol_change(vol: LvmVolume, activate: bool) -> None:
112+
active_flag = 'y' if activate else 'n'
113+
cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}'
114+
115+
debug(f'lvchange volume: {cmd}')
116+
SysCommand(cmd)
117+
118+
119+
def lvm_import_vg(vg: LvmVolumeGroup) -> None:
120+
# Check if the VG is actually exported before trying to import it
121+
check_cmd = f'vgs --noheadings -o vg_exported {vg.name}'
122+
123+
try:
124+
result = SysCommand(check_cmd)
125+
is_exported = result.decode().strip() == 'exported'
126+
except SysCallError:
127+
# VG might not exist yet, skip import
128+
debug(f'Volume group {vg.name} not found, skipping import')
129+
return
130+
131+
if not is_exported:
132+
debug(f'Volume group {vg.name} is already active (not exported), skipping import')
133+
return
134+
135+
cmd = f'vgimport {vg.name}'
136+
debug(f'vgimport: {cmd}')
137+
SysCommand(cmd)

archinstall/lib/installer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from types import TracebackType
1515
from typing import Any, Self
1616

17-
from archinstall.lib.disk.device_handler import device_handler
1817
from archinstall.lib.disk.fido import Fido2
18+
from archinstall.lib.disk.lvm import lvm_import_vg, lvm_pvseg_info, lvm_vol_change
1919
from archinstall.lib.disk.utils import (
2020
get_lsblk_by_mountpoint,
2121
get_lsblk_info,
@@ -341,10 +341,10 @@ def _import_lvm(self) -> None:
341341
return
342342

343343
for vg in lvm_config.vol_groups:
344-
device_handler.lvm_import_vg(vg)
344+
lvm_import_vg(vg)
345345

346346
for vol in vg.volumes:
347-
device_handler.lvm_vol_change(vol, True)
347+
lvm_vol_change(vol, True)
348348

349349
def _prepare_luks_lvm(
350350
self,
@@ -1147,7 +1147,7 @@ def _get_kernel_params_lvm(
11471147
if not lvm.vg_name:
11481148
raise ValueError(f'Unable to determine VG name for {lvm.name}')
11491149

1150-
pv_seg_info = device_handler.lvm_pvseg_info(lvm.vg_name, lvm.name)
1150+
pv_seg_info = lvm_pvseg_info(lvm.vg_name, lvm.name)
11511151

11521152
if not pv_seg_info:
11531153
raise ValueError(f'Unable to determine PV segment info for {lvm.vg_name}/{lvm.name}')

0 commit comments

Comments
 (0)