Coverage for drivers/LargeBlockSR.py : 18%

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/>.
17import SR
18from SR import deviceCheck
19import SRCommand
20import EXTSR
21import util
22import xs_errors
23import os
24import re
25import lvutil
27CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_SUPPORTS_LOCAL_CACHING",
28 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
29 "VDI_UPDATE", "VDI_CLONE", "VDI_SNAPSHOT", "VDI_RESIZE", "VDI_MIRROR",
30 "VDI_GENERATE_CONFIG",
31 "VDI_RESET_ON_BOOT/2", "ATOMIC_PAUSE", "VDI_CONFIG_CBT",
32 "VDI_ACTIVATE", "VDI_DEACTIVATE", "THIN_PROVISIONING", "VDI_READ_CACHING"]
34CONFIGURATION = [['device', 'local device path (required) (e.g. /dev/sda3)']]
36DRIVER_INFO = {
37 'name': 'Large Block SR',
38 'description': 'SR plugin which emulates a 512 bytes disk on top of a 4KiB device then create a EXT SR',
39 'vendor': 'Vates',
40 'copyright': '(C) 2024 Vates',
41 'driver_version': '1.0',
42 'required_api_version': '1.0',
43 'capabilities': CAPABILITIES,
44 'configuration': CONFIGURATION
45}
47LARGEBLOCK_PREFIX = "XSLocalLargeBlock-"
49class LargeBlockSR(EXTSR.EXTSR):
50 """Emulating 512b drives for EXT storage repository"""
52 DRIVER_TYPE = "largeblock"
53 LOOP_SECTOR_SIZE = 512
55 @staticmethod
56 def handles(srtype):
57 return srtype == LargeBlockSR.DRIVER_TYPE
59 def load(self, sr_uuid):
60 super(LargeBlockSR, self).load(sr_uuid)
61 self.is_deleting = False
62 self.vgname = LARGEBLOCK_PREFIX + sr_uuid
63 self.remotepath = os.path.join("/dev", self.vgname, sr_uuid)
65 def attach(self, sr_uuid):
66 if not self.is_deleting:
67 vg_device = self._get_device()
68 self.dconf["device"] = ",".join(vg_device)
69 self._create_emulated_device()
70 if not self._is_vg_connection_correct(): # Check if we need to redo the connection by parsing `vgs -o vg_name,devices self.vgname`
71 self._redo_vg_connection() # Call redo VG connection to connect it correctly to the loop device instead of the real 4KiB block device
72 super(LargeBlockSR, self).attach(sr_uuid)
74 def detach(self, sr_uuid):
75 if not self.is_deleting:
76 vg_device = self._get_device()
77 self.dconf["device"] = ",".join(vg_device)
78 super(LargeBlockSR, self).detach(sr_uuid)
79 if not self.is_deleting:
80 self._destroy_emulated_device()
82 @deviceCheck
83 def create(self, sr_uuid, size):
84 base_devices = self.dconf["device"].split(",")
85 if len(base_devices) > 1:
86 raise xs_errors.XenError("ConfigDeviceInvalid", opterr="Multiple devices configuration is not supported")
88 for dev in base_devices:
89 logical_blocksize = util.pread2(["blockdev", "--getss", dev]).strip()
90 if logical_blocksize == "512":
91 raise xs_errors.XenError("LargeBlockIncorrectBlocksize", opterr="The logical blocksize of the device {} is compatible with normal SR types".format(dev))
93 try:
94 self._create_emulated_device()
95 super(LargeBlockSR, self).create(sr_uuid, size)
96 finally:
97 self._destroy_emulated_device(base_devices)
99 def delete(self, sr_uuid):
100 base_devices = self._get_device()
101 self.dconf["device"] = ",".join(self._get_loopdev_from_device(base_devices))
103 self.is_deleting = True
104 try:
105 super(LargeBlockSR, self).delete(sr_uuid)
106 except xs_errors.SROSError:
107 # In case, the lvremove doesn't like the loop device, it will throw an error.
108 # We need to remove the device ourselves using the real device in this case.
109 for dev in base_devices:
110 util.pread2(["pvremove", dev])
111 finally:
112 self._destroy_emulated_device(base_devices)
113 self.is_deleting = False
115 @deviceCheck
116 def probe(self):
117 # We override EXTSR.probe because it uses EXT_PREFIX in this call
118 return lvutil.srlist_toxml(
119 lvutil.scan_srlist(LARGEBLOCK_PREFIX, self.dconf['device']),
120 LARGEBLOCK_PREFIX
121 )
123 def _create_loopdev(self, dev, emulated_path):
124 cmd = ["losetup", "-f", "-v", "--show", "--sector-size", str(self.LOOP_SECTOR_SIZE), dev]
125 loopdev = util.pread2(cmd).rstrip()
127 if os.path.exists(emulated_path) and os.path.islink(emulated_path):
128 os.unlink(emulated_path)
130 try:
131 os.symlink(loopdev, emulated_path)
132 except OSError:
133 raise xs_errors.XenError("LargeBlockSymlinkExist", opterr="Symlink {} couldn't be created".format(emulated_path))
135 def _delete_loopdev(self, dev, emulated_path):
136 if os.path.exists(emulated_path) and os.path.islink(emulated_path):
137 os.unlink(emulated_path)
139 # The backing file isn't a symlink if given by ID in device-config but the real device
140 dev = os.path.realpath(dev)
141 loopdevs = self._get_loopdev_from_device(dev)
143 if loopdevs != None:
144 try:
145 for lp in loopdevs:
146 cmd = ["losetup", "-d", lp] # Remove the loop device
147 util.pread2(cmd)
148 except xs_errors.SROSError:
149 util.SMlog("Couldn't removed losetup devices: {}".format(loopdevs))
150 else:
151 xs_errors.XenError("LargeBlockNoLosetup", opterr="Couldn't find loop device for {}".format(dev))
153 @staticmethod
154 def _get_loopdev_from_device(device):
155 lpdevs = []
156 output = util.pread2(["losetup", "--list"]).rstrip()
157 if output:
158 for line in output.split("\n"):
159 line = line.split()
160 loopdev = line[0]
161 dev = line[5].strip()
162 if dev in device:
163 lpdevs.append(loopdev)
164 return lpdevs
166 @staticmethod
167 def _get_device_from_loopdev(loopdevs):
168 devices = []
169 output = util.pread2(["losetup", "--list"]).rstrip()
170 if output:
171 for line in output.split("\n"):
172 line = line.split()
173 lpdev = line[0]
174 dev = line[5]
175 if lpdev in loopdevs:
176 devices.append(dev)
177 return devices
179 def _get_device_from_vg(self):
180 devices = []
181 output = util.pread2(["vgs", "--noheadings", "-o", "vg_name,devices", self.vgname]).splitlines()
182 for line in output:
183 line = line.split()
184 dev = line[1].split("(")[0]
185 if os.path.islink(dev):
186 dev = os.path.realpath(dev)
187 devices.append(dev)
188 return devices
190 def _get_device(self):
191 vg_device = self._get_device_from_vg()
192 for dev in vg_device:
193 if re.match(r"(.*\.512)|(/dev/loop[0-9]+)", dev):
194 lpdev = os.path.realpath(dev)
195 realdev = self._get_device_from_loopdev(lpdev)[0]
196 vg_device.remove(dev)
197 vg_device.append(realdev)
199 return vg_device
201 def _is_vg_connection_correct(self):
202 output = util.pread2(["vgs", "--noheadings", "-o", "vg_name,devices", self.vgname]).split()
203 output[1] = output[1].split("(")[0]
204 return bool(re.match(r"(.*\.512)|(/dev/loop[0-9]+)", output[1]))
206 def _redo_vg_connection(self):
207 """
208 In case of using a LargeBlockSR, the LVM scan at boot will find the LogicalVolume on the real block device.
209 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.
210 The PBD plug will succeed but then the SR will be accessed through the 4KiB device, returning to the erroneous behaviour on 4KiB device.
211 VM won't be able to run because vhd-util will fail to scan the VDI.
212 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.
213 """
215 util.SMlog("Reconnecting VG {} to use emulated device".format(self.vgname))
216 try:
217 lvutil.setActiveVG(self.vgname, False)
218 lvutil.setActiveVG(self.vgname, True, config="devices{ global_filter = [ \"r|^/dev/nvme.*|\", \"a|/dev/loop.*|\" ] }")
219 except util.CommandException as e:
220 xs_errors.XenError("LargeBlockVGReconnectFailed", opterr="Failed to reconnect the VolumeGroup {}, error: {}".format(self.vgname, e))
223 @classmethod
224 def _get_emulated_device_path(cls, dev):
225 return "{dev}.{bs}".format(dev=dev, bs=cls.LOOP_SECTOR_SIZE)
227 def _create_emulated_device(self):
228 base_devices = self.dconf["device"].split(",")
229 emulated_devices = []
230 for dev in base_devices:
231 emulated_path = self._get_emulated_device_path(dev)
232 self._create_loopdev(dev, emulated_path)
233 emulated_devices.append(emulated_path)
235 emulated_devices = ",".join(emulated_devices)
236 self.dconf["device"] = emulated_devices
238 def _destroy_emulated_device(self, devices=None):
239 if devices is None:
240 devices = self.dconf["device"].split(",")
242 for dev in devices:
243 emulated_path = self._get_emulated_device_path(dev)
244 self._delete_loopdev(dev, emulated_path)
246if __name__ == '__main__': 246 ↛ 247line 246 didn't jump to line 247, because the condition on line 246 was never true
247 SRCommand.run(LargeBlockSR, DRIVER_INFO)
248else:
249 SR.registerSR(LargeBlockSR)