Skip to content

Commit da82fa0

Browse files
committed
feat(lvmsr): support vdi_revert on lvm
* Support `vdi_revert` * Add journals for rollback in case of failure * Rewrite lvm journal creation with security feature against writing too much inside allocated space * Read more in lvm journal * Support CBT on vdi revert Signed-off-by: Antoine Bartuccio <antoine.bartuccio@vates.tech>
1 parent 8f4395d commit da82fa0

8 files changed

Lines changed: 611 additions & 117 deletions

File tree

drivers/LVMSR.py

Lines changed: 505 additions & 45 deletions
Large diffs are not rendered by default.

drivers/LVMoFCoESR.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"VDI_GENERATE_CONFIG", "VDI_SNAPSHOT", "VDI_CLONE",
3535
"VDI_RESIZE", "ATOMIC_PAUSE", "VDI_RESET_ON_BOOT/2",
3636
"VDI_UPDATE", "VDI_MIRROR", "VDI_CONFIG_CBT", "VDI_ACTIVATE",
37-
"VDI_DEACTIVATE"]
37+
"VDI_DEACTIVATE", "VDI_REVERT"]
3838

3939
CONFIGURATION = [['SCSIid', 'The scsi_id of the destination LUN'],
4040
['allocation', 'Valid values are thick or thin(optional,\

drivers/LVMoHBASR.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
4242
"VDI_GENERATE_CONFIG", "VDI_SNAPSHOT", "VDI_CLONE", "VDI_MIRROR",
4343
"VDI_RESIZE", "ATOMIC_PAUSE", "VDI_RESET_ON_BOOT/2",
44-
"VDI_UPDATE", "VDI_CONFIG_CBT", "VDI_ACTIVATE", "VDI_DEACTIVATE"]
44+
"VDI_UPDATE", "VDI_CONFIG_CBT", "VDI_ACTIVATE", "VDI_DEACTIVATE", "VDI_REVERT"]
4545

4646
CONFIGURATION = [['SCSIid', 'The scsi_id of the destination LUN'], \
4747
['allocation', 'Valid values are thick or thin (optional, defaults to thick)']]

drivers/LVMoISCSISR.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"VDI_GENERATE_CONFIG", "VDI_CLONE", "VDI_SNAPSHOT",
4747
"VDI_RESIZE", "ATOMIC_PAUSE", "VDI_RESET_ON_BOOT/2",
4848
"VDI_UPDATE", "VDI_MIRROR", "VDI_CONFIG_CBT",
49-
"VDI_ACTIVATE", "VDI_DEACTIVATE"]
49+
"VDI_ACTIVATE", "VDI_DEACTIVATE", "VDI_REVERT"]
5050

5151
CONFIGURATION = [['SCSIid', 'The scsi_id of the destination LUN'], \
5252
['target', 'IP address or hostname of the iSCSI target'], \

drivers/journaler.py

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import util
2121
import xs_errors
22-
from srmetadata import open_file, file_read_wrapper, file_write_wrapper
22+
from srmetadata import open_file, file_read_wrapper, file_write_wrapper, align_data_to_file
2323

2424
LVM_MAX_NAME_LEN = 127
2525

@@ -34,6 +34,16 @@ class Journaler:
3434
SEPARATOR = "_"
3535
JRN_CLONE = "clone"
3636
JRN_LEAF = "leaf"
37+
JRN_REVERT = "revert"
38+
39+
@classmethod
40+
def has_additional_data(cls, type: str) -> bool:
41+
"""Return True if journal type contains additional data inside of it"""
42+
return type in [
43+
cls.JRN_CLONE,
44+
cls.JRN_LEAF,
45+
cls.JRN_REVERT,
46+
]
3747

3848
def __init__(self, lvmCache):
3949
self.vgName = lvmCache.vgName
@@ -42,54 +52,63 @@ def __init__(self, lvmCache):
4252
def create(self, type, id, val):
4353
"""Create an entry of type "type" for "id" with the value "val".
4454
Error if such an entry already exists."""
45-
valExisting = self.get(type, id)
46-
writeData = False
47-
if valExisting or util.fistpoint.is_active("LVM_journaler_exists"):
48-
raise xs_errors.XenError('LVMCreate', opterr="Journal already exists for '%s:%s': %s" % (type, id, valExisting))
49-
lvName = self._getNameLV(type, id, val)
55+
to_write = None
56+
journal_exists = self.get(type, id)
57+
if journal_exists or util.fistpoint.is_active("LVM_journaler_exists"):
58+
raise xs_errors.XenError('LVMCreate', opterr=f"Journal already exists for '{type}:{id}': {journal_exists}")
59+
lv_name = self._getNameLV(type, id, val)
60+
61+
mapper_device = self._getLVMapperName(lv_name)
62+
if len(mapper_device) > LVM_MAX_NAME_LEN:
63+
lv_name = self._getNameLV(type, id)
64+
mapper_device = self._getLVMapperName(lv_name)
65+
assert len(mapper_device) <= LVM_MAX_NAME_LEN
5066

51-
mapperDevice = self._getLVMapperName(lvName)
52-
if len(mapperDevice) > LVM_MAX_NAME_LEN:
53-
lvName = self._getNameLV(type, id)
54-
writeData = True
55-
mapperDevice = self._getLVMapperName(lvName)
56-
assert len(mapperDevice) <= LVM_MAX_NAME_LEN
67+
try:
68+
to_write = ("%d %s" % (len(val), val)).encode()
69+
except UnicodeEncodeError as e:
70+
util.logException("journaler.create")
71+
raise xs_errors.XenError("LVMWrite", opterr=f"Failed to encode data for journal {lv_name}") from e
72+
used_size = len(align_data_to_file(to_write))
73+
if used_size > self.LV_SIZE:
74+
raise xs_errors.XenError("LVMWrite", opterr=f"Size of encoded journal {used_size} exceeds limit of {self.LV_SIZE}")
75+
76+
77+
self.lvmCache.create(lv_name, self.LV_SIZE, self.LV_TAG)
5778

58-
self.lvmCache.create(lvName, self.LV_SIZE, self.LV_TAG)
79+
if not to_write:
80+
return
5981

60-
if writeData:
61-
fullPath = self.lvmCache._getPath(lvName)
62-
journal_file = open_file(fullPath, True)
82+
full_path = self.lvmCache._getPath(lv_name)
83+
journal_file = open_file(full_path, True)
84+
try:
85+
raised_exception = None
6386
try:
64-
e = None
87+
file_write_wrapper(journal_file, 0, to_write)
88+
if util.fistpoint.is_active("LVM_journaler_writefail"):
89+
raise ValueError("LVM_journaler_writefail FistPoint active")
90+
except Exception as e:
91+
raised_exception = e
92+
raise
93+
finally:
6594
try:
66-
data = ("%d %s" % (len(val), val)).encode()
67-
file_write_wrapper(journal_file, 0, data)
68-
if util.fistpoint.is_active("LVM_journaler_writefail"):
69-
raise ValueError("LVM_journaler_writefail FistPoint active")
95+
journal_file.close()
96+
self.lvmCache.deactivateNoRefcount(lv_name)
7097
except Exception as e:
71-
raise
72-
finally:
73-
try:
74-
journal_file.close()
75-
self.lvmCache.deactivateNoRefcount(lvName)
76-
except Exception as e2:
77-
msg = 'failed to close/deactivate %s: %s' \
78-
% (lvName, e2)
79-
if not e:
80-
util.SMlog(msg)
81-
raise e2
82-
else:
83-
util.SMlog('WARNING: %s (error ignored)' % msg)
84-
85-
except:
86-
util.logException("journaler.create")
87-
try:
88-
self.lvmCache.remove(lvName)
89-
except Exception as e:
90-
util.SMlog('WARNING: failed to clean up failed journal ' \
91-
' creation: %s (error ignored)' % e)
92-
raise xs_errors.XenError('LVMWrite', opterr="Failed to write to journal %s" % lvName)
98+
msg = f"failed to close/deactivate {lv_name}: {e}"
99+
if not raised_exception:
100+
util.SMlog(msg)
101+
raise e
102+
else:
103+
util.SMlog(f"WARNING: {msg} (error ignored)")
104+
105+
except:
106+
util.logException("journaler.create")
107+
try:
108+
self.lvmCache.remove(lv_name)
109+
except Exception as e:
110+
util.SMlog(f"WARNING: failed to clean up failed journal creation: {e} (error ignored)")
111+
raise xs_errors.XenError('LVMWrite', opterr=f"Failed to write to journal {lv_name}")
93112

94113
def remove(self, type, id):
95114
"""Remove the entry of type "type" for "id". Error if the entry doesn't
@@ -139,26 +158,23 @@ def _getAllEntries(self, readFile=True):
139158
if len(parts) != 3 or util.fistpoint.is_active("LVM_journaler_badname"):
140159
raise xs_errors.XenError('LVMNoVolume', opterr="Bad LV name: %s" % lvName)
141160
type, id, val = parts
142-
if readFile:
143-
# For clone and leaf journals, additional
144-
# data is written inside file
145-
# TODO: Remove dependency on journal type
146-
if type == self.JRN_CLONE or type == self.JRN_LEAF:
147-
fullPath = self.lvmCache._getPath(lvName)
148-
self.lvmCache.activateNoRefcount(lvName, False)
149-
journal_file = open_file(fullPath)
161+
# TODO: Remove dependency on journal type
162+
if readFile and self.has_additional_data(type):
163+
fullPath = self.lvmCache._getPath(lvName)
164+
self.lvmCache.activateNoRefcount(lvName, False)
165+
journal_file = open_file(fullPath)
166+
try:
150167
try:
151-
try:
152-
data = file_read_wrapper(journal_file, 0)
153-
length, val = data.decode().split(" ", 1)
154-
val = val[:int(length)]
155-
if util.fistpoint.is_active("LVM_journaler_readfail"):
156-
raise ValueError("LVM_journaler_readfail FistPoint active")
157-
except:
158-
raise xs_errors.XenError('LVMRead', opterr="Failed to read from journal %s" % lvName)
159-
finally:
160-
journal_file.close()
161-
self.lvmCache.deactivateNoRefcount(lvName)
168+
data = file_read_wrapper(journal_file, 0, -1)
169+
length, val = data.decode().split(" ", 1)
170+
val = val[:int(length)]
171+
if util.fistpoint.is_active("LVM_journaler_readfail"):
172+
raise ValueError("LVM_journaler_readfail FistPoint active")
173+
except:
174+
raise xs_errors.XenError('LVMRead', opterr="Failed to read from journal %s" % lvName)
175+
finally:
176+
journal_file.close()
177+
self.lvmCache.deactivateNoRefcount(lvName)
162178
if not entries.get(type):
163179
entries[type] = dict()
164180
entries[type][id] = val

drivers/srmetadata.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,25 @@ def open_file(path, write=False):
102102
(path, e.errno))
103103
return file_p
104104

105+
def align_data_to_file(data: bytes) -> bytes:
106+
"""Add space padding to data to ensure that a complete blocks can be written."""
107+
blocksize = METADATA_BLK_SIZE
108+
length = len(data)
109+
newlength = length
110+
if length % blocksize:
111+
newlength = length + (blocksize - length % blocksize)
112+
return data + b' ' * (newlength - length)
105113

106114
def file_write_wrapper(fd, offset, data):
107115
"""
108116
Writes data to a file at a given offset. Padding (consisting of spaces)
109117
may be written out after the given data to ensure that complete blocks are
110118
written.
111119
"""
120+
blocksize = METADATA_BLK_SIZE
112121
try:
113-
blocksize = METADATA_BLK_SIZE
114-
length = len(data)
115-
newlength = length
116-
if length % blocksize:
117-
newlength = length + (blocksize - length % blocksize)
122+
to_write = align_data_to_file(data)
118123
fd.seek(offset, SEEK_SET)
119-
to_write = data + b' ' * (newlength - length)
120124
return fd.write(to_write)
121125
except OSError as e:
122126
raise OSError(
@@ -127,10 +131,16 @@ def file_write_wrapper(fd, offset, data):
127131
def file_read_wrapper(fd, offset, bytesToRead=METADATA_BLK_SIZE):
128132
"""
129133
Reads data from a file at a given offset. If not specified, the amount of
130-
data to read defaults to one block.
134+
data to read defaults to one block. Using -1 reads the whole file.
131135
"""
132136
try:
133137
fd.seek(offset, SEEK_SET)
138+
if bytesToRead == -1:
139+
# The most reliable solution I could find to read the whole file
140+
# is to read it to the end to find the size
141+
# This isn't super efficient but in most case where this is used, the file
142+
# is quite short and that shouldn't be an issue
143+
bytesToRead = len(fd.peek())
134144
return fd.read(bytesToRead)
135145
except OSError as e:
136146
raise OSError(

drivers/util.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,6 +1549,9 @@ def list_find(f, seq):
15491549
"FileSR_revert_create_insert",
15501550
"FileSR_revert_create_src",
15511551
"FileSR_revert_create_dest",
1552+
"LVM_revert_create_insert",
1553+
"LVM_revert_create_src",
1554+
"LVM_revert_create_dest",
15521555
"LVM_journaler_exists",
15531556
"LVM_journaler_none",
15541557
"LVM_journaler_badname",

tests/test_LVMSR.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import XenAPI
12
from sm_typing import override
23

34
import copy
@@ -698,6 +699,10 @@ def test_update_slaves_on_cbt_disable(self, mock_xenapi, mock_lock):
698699
Ensure we tell the supporter host when we disable CBT for one of its VMs
699700
"""
700701
# Arrange
702+
def xenapi_failure(_):
703+
raise XenAPI.Failure("No uuid")
704+
705+
mock_xenapi.xapi_local.return_value.xenapi.VDI.get_by_uuid.side_effect = xenapi_failure
701706
xapi_session = mock_xenapi.xapi_local.return_value
702707

703708
vdi_uuid = str(uuid.uuid4)

0 commit comments

Comments
 (0)