summaryrefslogtreecommitdiffstats
path: root/src/utils/disk.py
blob: c424c6116bf07dd9f91e255851035cdc84529ed5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

import os
import logging
import shlex
import subprocess
import json
from src.log import OgError

import fdisk


def get_disks():
    """
    Walks /sys/block/ and returns files starting with 'sd',
    'nvme' or 'vd'
    """
    return sorted([ dev for dev in os.listdir('/sys/block/')
                    if dev.startswith('sd')
                    or dev.startswith('nvme')
                    or dev.startswith('vd')])


def get_partition_device(disknum, partnum):
    """
    Returns the device path, given a disk and partition number
    """
    disk_index = disknum - 1
    if disk_index < 0 or disk_index >= len(get_disks()):
        raise OgError(f'Invalid disk number {disknum}, {len(get_disks())} disks available.')

    disk = get_disks()[disk_index]
    cxt = fdisk.Context(f'/dev/{disk}')

    for pa in cxt.partitions:
        if pa.partno == partnum - 1:
            return cxt.partition_to_string(pa, fdisk.FDISK_FIELD_DEVICE)

    raise OgError(f'No such partition with disk index {disknum} and partition index {partnum}')


def get_efi_partition(disknum, enforce_gpt):
    """
    Look for an EFI System Partition at the n-th disk. If disknum is invalid an
    exception is thrown.
    If enforce_gpt is set to True the ESP will be ignored in a MBR partition
    scheme.

    Returns tuple with:
        - Device name containing the ESP
        - /dev/{device} string
        - Partition number (starting at 1)
    """
    disk_index = disknum - 1
    if disk_index < 0 or disk_index >= len(get_disks()):
        raise OgError(f'Invalid disk number {disknum} when trying to find EFI partition, {len(get_disks())} disks available.')

    disk = get_disks()[disk_index]
    cxt = fdisk.Context(f'/dev/{disk}')

    if enforce_gpt and cxt.label == fdisk.FDISK_DISKLABEL_DOS:
        raise OgError(f'Windows EFI partition requires GPT partition scheme, but /dev/{disk} has DOS partition scheme')

    logging.info('Searching EFI partition...')
    for pa in cxt.partitions:
        if pa.type.name == 'EFI System':
            logging.info(f'EFI partition found at /dev/{disk}')
            return cxt.partition_to_string(pa, fdisk.FDISK_FIELD_DEVICE), f'/dev/{disk}', pa.partno + 1
    raise OgError(f'Cannot find EFI partition at /dev/{disk}')


def get_partition_id(disk_index, part_index):
    device = get_partition_device(disk_index, part_index)
    cmd = f'blkid -s PARTUUID -o value {device}'
    proc = subprocess.run(shlex.split(cmd),
                          stdout=subprocess.PIPE,
                          encoding='utf-8')
    if proc.returncode != 0:
            raise OgError(f'failed to query partition UUID for {device}')
    return proc.stdout.strip()


def get_disk_id(disk_index):
    disk = get_disks()[disk_index - 1]
    disk_path = f'/dev/{disk}'
    cmd = f'blkid -s PTUUID -o value {disk_path}'
    proc = subprocess.run(shlex.split(cmd),
                          stdout=subprocess.PIPE,
                          encoding='utf-8')
    if proc.returncode != 0:
            raise OgError(f'failed to query disk UUID for {disk_path}')
    return proc.stdout.strip()


def get_filesystem_id(disk_index, part_index):
    device = get_partition_device(disk_index, part_index)
    cmd = f'blkid -s UUID -o value {device}'
    proc = subprocess.run(shlex.split(cmd),
                          stdout=subprocess.PIPE,
                          encoding='utf-8')
    if proc.returncode != 0:
            raise OgError(f'failed to query filesystem UUID for {device}')
    return proc.stdout.strip()


def get_sector_size(disk):
    disk_index = disk - 1

    if disk_index < 0 or disk_index >= len(get_disks()):
        raise OgError(f'Invalid disk number {disk} when trying to find ESP, {len(get_disks())} disks available.')

    device_name = get_disks()[disk_index]
    file_path = f'/sys/class/block/{device_name}/queue/hw_sector_size'

    try:
        with open(file_path, 'r') as f:
            data = f.read().strip()
    except OSError as e:
        raise OgError(f'Error while trying to read {file_path}: {e}') from e
    return int(data)


def get_partition_start_offset(disk, partition):
    disk_name = get_disks()[disk - 1]
    disk_path = f'/dev/{disk_name}'
    part_number = partition - 1

    cmd = f'sfdisk -J {disk_path}'
    proc = subprocess.run(shlex.split(cmd), capture_output=True, text=True)

    if proc.returncode != 0:
        raise OgError(f'Failed to query sfdisk')

    try:
        part_data_json = json.loads(proc.stdout)
    except json.JSONDecodeError as e:
        raise OgError(f'Invalid sfdisk output: {e}') from e

    try:
        part_data = part_data_json['partitiontable']['partitions']
        start_offset = part_data[part_number]['start']
    except KeyError as e:
        raise OgError(f'Error while trying to parse sfdisk: {e}') from e

    return start_offset