Coverage for drivers/LargeBlockSR.py : 22%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2#
3# Copyright (C) 2024 Vates SAS - damien.thenot@vates.tech
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <https://www.gnu.org/licenses/>.
17from sm_typing import override
19import SR
20from SR import deviceCheck
21import SRCommand
22import EXTSR
23import util
24import xs_errors
25import os
26import re
27import lvutil
29CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_SUPPORTS_LOCAL_CACHING",
30 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
31 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR",
32 "VDI_GENERATE_CONFIG",
33 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT",
34 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"]
36CONFIGURATION = [['device', 'local device path (required) (e.g. /dev/sda3)']]
38DRIVER_INFO = {
39 'name': 'Large Block SR',
40 'description': 'SR plugin which emulates a 512 bytes disk on top of a 4KiB device then create a EXT SR',
41 'vendor': 'Vates',
42 'copyright': '(C) 2024 Vates',
43 'driver_version': '1.0',
44 'required_api_version': '1.0',
45 'capabilities': CAPABILITIES,
46 'configuration': CONFIGURATION
47}
49LARGEBLOCK_PREFIX = "XSLocalLargeBlock-"
51class LargeBlockSR(EXTSR.EXTSR):
52 """Emulating 512b drives for EXT storage repository"""
54 DRIVER_TYPE = "largeblock"
55 LOOP_SECTOR_SIZE = 512
57 @override
58 @staticmethod
59 def handles(srtype) -> bool:
60 return srtype == LargeBlockSR.DRIVER_TYPE
62 @override
63 def load(self, sr_uuid) -> None:
64 super(LargeBlockSR, self).load(sr_uuid)
65 self.is_deleting = False
66 self.vgname = LARGEBLOCK_PREFIX + sr_uuid
67 self.remotepath = os.path.join("/dev", self.vgname, sr_uuid)
69 @override
70 def attach(self, sr_uuid) -> None:
71 if not self.is_deleting:
72 vg_device = self._get_device()
73 self.dconf["device"] = ",".join(vg_device)
74 self._create_emulated_device()
75 if not self._is_vg_connection_correct(): # Check if we need to redo the connection by parsing `vgs -o vg_name,devices self.vgname`
76 self._redo_vg_connection() # Call redo VG connection to connect it correctly to the loop device instead of the real 4KiB block device
77 super(LargeBlockSR, self).attach(sr_uuid)
79 @override
80 def detach(self, sr_uuid) -> None:
81 if not self.is_deleting:
82 vg_device = self._get_device()
83 self.dconf["device"] = ",".join(vg_device)
84 super(LargeBlockSR, self).detach(sr_uuid)
85 if not self.is_deleting:
86 self._destroy_emulated_device()
88 @override
89 @deviceCheck
90 def create(self, sr_uuid, size) -> None:
91 base_devices = self.dconf["device"].split(",")
92 if len(base_devices) > 1:
93 raise xs_errors.XenError("ConfigDeviceInvalid", opterr="Multiple devices configuration is not supported")
95 for dev in base_devices:
96 logical_blocksize = util.pread2(["blockdev", "--getss", dev]).strip()
97 if logical_blocksize == "512":
98 raise xs_errors.XenError("LargeBlockIncorrectBlocksize", opterr="The logical blocksize of the device {} is compatible with normal SR types".format(dev))
100 try:
101 self._create_emulated_device()
102 super(LargeBlockSR, self).create(sr_uuid, size)
103 finally:
104 self._destroy_emulated_device(base_devices)
106 @override
107 def delete(self, sr_uuid) -> None:
108 base_devices = self._get_device()
109 self.dconf["device"] = ",".join(self._get_loopdev_from_device(base_devices))
111 self.is_deleting = True
112 try:
113 super(LargeBlockSR, self).delete(sr_uuid)
114 except xs_errors.SROSError:
115 # In case, the lvremove doesn't like the loop device, it will throw an error.
116 # We need to remove the device ourselves using the real device in this case.
117 for dev in base_devices:
118 util.pread2(["pvremove", dev])
119 finally:
120 self._destroy_emulated_device(base_devices)
121 self.is_deleting = False
123 @override
124 @deviceCheck
125 def probe(self) -> str:
126 # We override EXTSR.probe because it uses EXT_PREFIX in this call
127 return lvutil.srlist_toxml(
128 lvutil.scan_srlist(LARGEBLOCK_PREFIX, self.dconf['device']),
129 LARGEBLOCK_PREFIX
130 )
132 def _create_loopdev(self, dev, emulated_path):
133 cmd = ["losetup", "-f", "-v", "--show", "--sector-size", str(self.LOOP_SECTOR_SIZE), dev]
134 loopdev = util.pread2(cmd).rstrip()
136 if os.path.exists(emulated_path) and os.path.islink(emulated_path):
137 os.unlink(emulated_path)
139 try:
140 os.symlink(loopdev, emulated_path)
141 except OSError:
142 raise xs_errors.XenError("LargeBlockSymlinkExist", opterr="Symlink {} couldn't be created".format(emulated_path))
144 def _delete_loopdev(self, dev, emulated_path):
145 if os.path.exists(emulated_path) and os.path.islink(emulated_path):
146 os.unlink(emulated_path)
148 # The backing file isn't a symlink if given by ID in device-config but the real device
149 dev = os.path.realpath(dev)
150 loopdevs = self._get_loopdev_from_device(dev)
152 if loopdevs != None:
153 try:
154 for lp in loopdevs:
155 cmd = ["losetup", "-d", lp] # Remove the loop device
156 util.pread2(cmd)
157 except xs_errors.SROSError:
158 util.SMlog("Couldn't removed losetup devices: {}".format(loopdevs))
159 else:
160 xs_errors.XenError("LargeBlockNoLosetup", opterr="Couldn't find loop device for {}".format(dev))
162 @staticmethod
163 def _get_loopdev_from_device(device):
164 lpdevs = []
165 output = util.pread2(["losetup", "--list"]).rstrip()
166 if output:
167 for line in output.split("\n"):
168 line = line.split()
169 loopdev = line[0]
170 dev = line[5].strip()
171 if dev in device:
172 lpdevs.append(loopdev)
173 return lpdevs
175 @staticmethod
176 def _get_device_from_loopdev(loopdevs):
177 devices = []
178 output = util.pread2(["losetup", "--list"]).rstrip()
179 if output:
180 for line in output.split("\n"):
181 line = line.split()
182 lpdev = line[0]
183 dev = line[5]
184 if lpdev in loopdevs:
185 devices.append(dev)
186 return devices
188 def _get_device_from_vg(self):
189 devices = []
190 output = util.pread2(["vgs", "--noheadings", "-o", "vg_name,devices", self.vgname]).splitlines()
191 for line in output:
192 line = line.split()
193 dev = line[1].split("(")[0]
194 if os.path.islink(dev):
195 dev = os.path.realpath(dev)
196 devices.append(dev)
197 return devices
199 def _get_device(self):
200 vg_device = self._get_device_from_vg()
201 for dev in vg_device:
202 if re.match(r"(.*\.512)|(/dev/loop[0-9]+)", dev):
203 lpdev = os.path.realpath(dev)
204 realdev = self._get_device_from_loopdev(lpdev)[0]
205 vg_device.remove(dev)
206 vg_device.append(realdev)
208 return vg_device
210 def _is_vg_connection_correct(self):
211 output = util.pread2(["vgs", "--noheadings", "-o", "vg_name,devices", self.vgname]).split()
212 output[1] = output[1].split("(")[0]
213 return bool(re.match(r"(.*\.512)|(/dev/loop[0-9]+)", output[1]))
215 def _redo_vg_connection(self):
216 """
217 In case of using a LargeBlockSR, the LVM scan at boot will find the LogicalVolume on the real block device.
218 And when the PBD is connecting, it will mount from the original device instead of the loop device since LVM prefers real devices it has seen first.
219 The PBD plug will succeed but then the SR will be accessed through the 4KiB device, returning to the erroneous behaviour on 4KiB device.
220 VM won't be able to run because vhd-util will fail to scan the VDI.
221 This function force the LogicalVolume to be mounted on top of our emulation layer by disabling the VolumeGroup and re-enabling while applying a filter.
222 """
224 util.SMlog("Reconnecting VG {} to use emulated device".format(self.vgname))
225 try:
226 lvutil.setActiveVG(self.vgname, False)
227 lvutil.setActiveVG(self.vgname, True, config="devices{ global_filter = [ \"r|^/dev/nvme.*|\", \"a|/dev/loop.*|\" ] }")
228 except util.CommandException as e:
229 xs_errors.XenError("LargeBlockVGReconnectFailed", opterr="Failed to reconnect the VolumeGroup {}, error: {}".format(self.vgname, e))
232 @classmethod
233 def _get_emulated_device_path(cls, dev):
234 return "{dev}.{bs}".format(dev=dev, bs=cls.LOOP_SECTOR_SIZE)
236 def _create_emulated_device(self):
237 base_devices = self.dconf["device"].split(",")
238 emulated_devices = []
239 for dev in base_devices:
240 emulated_path = self._get_emulated_device_path(dev)
241 self._create_loopdev(dev, emulated_path)
242 emulated_devices.append(emulated_path)
244 emulated_devices = ",".join(emulated_devices)
245 self.dconf["device"] = emulated_devices
247 def _destroy_emulated_device(self, devices=None):
248 if devices is None:
249 devices = self.dconf["device"].split(",")
251 for dev in devices:
252 emulated_path = self._get_emulated_device_path(dev)
253 self._delete_loopdev(dev, emulated_path)
255if __name__ == '__main__': 255 ↛ 256line 255 didn't jump to line 256, because the condition on line 255 was never true
256 SRCommand.run(LargeBlockSR, DRIVER_INFO)
257else:
258 SR.registerSR(LargeBlockSR)