Coverage for libs/sm/journaler.py: 15%
171 statements
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
1#
2# Copyright (C) Citrix Systems Inc.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU Lesser General Public License as published
6# by the Free Software Foundation; version 2.1 only.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16#
17# LVM-based journaling
19from sm.core import util
20from sm.core import xs_errors
21from sm.srmetadata import open_file, file_read_wrapper, file_write_wrapper
23LVM_MAX_NAME_LEN = 127
26class Journaler:
27 """Simple journaler that uses LVM namespace for persistent "storage".
28 A journal is a id-value pair, and there can be only one journal for a
29 given id."""
31 LV_SIZE = 4 * 1024 * 1024 # minimum size
32 LV_TAG = "journaler"
33 SEPARATOR = "_"
34 JRN_CLONE = "clone"
35 JRN_LEAF = "leaf"
37 def __init__(self, lvmCache):
38 self.vgName = lvmCache.vgName
39 self.lvmCache = lvmCache
41 def create(self, type, id, val):
42 """Create an entry of type "type" for "id" with the value "val".
43 Error if such an entry already exists."""
44 valExisting = self.get(type, id)
45 writeData = False
46 if valExisting or util.fistpoint.is_active("LVM_journaler_exists"):
47 raise xs_errors.XenError('LVMCreate', opterr="Journal already exists for '%s:%s': %s" % (type, id, valExisting))
48 lvName = self._getNameLV(type, id, val)
50 mapperDevice = self._getLVMapperName(lvName)
51 if len(mapperDevice) > LVM_MAX_NAME_LEN:
52 lvName = self._getNameLV(type, id)
53 writeData = True
54 mapperDevice = self._getLVMapperName(lvName)
55 assert len(mapperDevice) <= LVM_MAX_NAME_LEN
57 self.lvmCache.create(lvName, self.LV_SIZE, self.LV_TAG)
59 if writeData:
60 fullPath = self.lvmCache._getPath(lvName)
61 journal_file = open_file(fullPath, True)
62 try:
63 e = None
64 try:
65 data = ("%d %s" % (len(val), val)).encode()
66 file_write_wrapper(journal_file, 0, data)
67 if util.fistpoint.is_active("LVM_journaler_writefail"):
68 raise ValueError("LVM_journaler_writefail FistPoint active")
69 except Exception as e:
70 raise
71 finally:
72 try:
73 journal_file.close()
74 self.lvmCache.deactivateNoRefcount(lvName)
75 except Exception as e2:
76 msg = 'failed to close/deactivate %s: %s' \
77 % (lvName, e2)
78 if not e:
79 util.SMlog(msg)
80 raise e2
81 else:
82 util.SMlog('WARNING: %s (error ignored)' % msg)
84 except:
85 util.logException("journaler.create")
86 try:
87 self.lvmCache.remove(lvName)
88 except Exception as e:
89 util.SMlog('WARNING: failed to clean up failed journal ' \
90 ' creation: %s (error ignored)' % e)
91 raise xs_errors.XenError('LVMWrite', opterr="Failed to write to journal %s" % lvName)
93 def remove(self, type, id):
94 """Remove the entry of type "type" for "id". Error if the entry doesn't
95 exist."""
96 val = self.get(type, id)
97 if not val or util.fistpoint.is_active("LVM_journaler_none"):
98 raise xs_errors.XenError('LVMNoVolume', opterr="No journal for '%s:%s'" % (type, id))
99 lvName = self._getNameLV(type, id, val)
101 mapperDevice = self._getLVMapperName(lvName)
102 if len(mapperDevice) > LVM_MAX_NAME_LEN:
103 lvName = self._getNameLV(type, id)
104 self.lvmCache.remove(lvName)
106 def get(self, type, id):
107 """Get the value for the journal entry of type "type" for "id".
108 Return None if no such entry exists"""
109 entries = self._getAllEntries()
110 if not entries.get(type):
111 return None
112 return entries[type].get(id)
114 def getAll(self, type):
115 """Get a mapping id->value for all entries of type "type"."""
116 entries = self._getAllEntries()
117 if not entries.get(type): 117 ↛ 119line 117 didn't jump to line 119, because the condition on line 117 was never false
118 return dict()
119 return entries[type]
121 def hasJournals(self, id):
122 """Return True if there any journals for "id", False otherwise"""
123 # Pass False as an argument to skip opening journal files
124 entries = self._getAllEntries(False)
125 for type, ids in entries.items():
126 if ids.get(id):
127 return True
128 return False
130 def _getNameLV(self, type, id, val=1):
131 return "%s%s%s%s%s" % (type, self.SEPARATOR, id, self.SEPARATOR, val)
133 def _getAllEntries(self, readFile=True):
134 lvList = self.lvmCache.getTagged(self.LV_TAG)
135 entries = dict()
136 for lvName in lvList: 136 ↛ 137line 136 didn't jump to line 137, because the loop on line 136 never started
137 parts = lvName.split(self.SEPARATOR, 2)
138 if len(parts) != 3 or util.fistpoint.is_active("LVM_journaler_badname"):
139 raise xs_errors.XenError('LVMNoVolume', opterr="Bad LV name: %s" % lvName)
140 type, id, val = parts
141 if readFile:
142 # For clone and leaf journals, additional
143 # data is written inside file
144 # TODO: Remove dependency on journal type
145 if type == self.JRN_CLONE or type == self.JRN_LEAF:
146 fullPath = self.lvmCache._getPath(lvName)
147 self.lvmCache.activateNoRefcount(lvName, False)
148 journal_file = open_file(fullPath)
149 try:
150 try:
151 data = file_read_wrapper(journal_file, 0)
152 length, val = data.decode().split(" ", 1)
153 val = val[:int(length)]
154 if util.fistpoint.is_active("LVM_journaler_readfail"):
155 raise ValueError("LVM_journaler_readfail FistPoint active")
156 except:
157 raise xs_errors.XenError('LVMRead', opterr="Failed to read from journal %s" % lvName)
158 finally:
159 journal_file.close()
160 self.lvmCache.deactivateNoRefcount(lvName)
161 if not entries.get(type):
162 entries[type] = dict()
163 entries[type][id] = val
164 return entries
166 def _getLVMapperName(self, lvName):
167 return '%s-%s' % (self.vgName.replace("-", "--"), lvName)
169###########################################################################
170#
171# Unit tests
172#
173from sm import lvutil
174from sm import lvmcache
177def _runTests(vgName):
178 """Unit testing"""
179 print("Running unit tests...")
180 if not vgName:
181 print("Error: missing VG name param")
182 return 1
183 if not lvutil._checkVG(vgName):
184 print("Error: VG %s not found" % vgName)
185 return 1
187 j = Journaler(lvmcache.LVMCache(vgName))
188 if j.get("clone", "1"):
189 print("get non-existing failed")
190 return 1
191 j.create("clone", "1", "a")
192 val = j.get("clone", "1")
193 if val != "a":
194 print("create-get failed")
195 return 1
196 j.remove("clone", "1")
197 if j.get("clone", "1"):
198 print("remove failed")
199 return 1
200 j.create("modify", "X", "831_3")
201 j.create("modify", "Z", "831_4")
202 j.create("modify", "Y", "53_0")
203 val = j.get("modify", "X")
204 if val != "831_3":
205 print("create underscore_val failed")
206 return 1
207 val = j.get("modify", "Y")
208 if val != "53_0":
209 print("create multiple id's failed")
210 return 1
211 entries = j.getAll("modify")
212 if not entries.get("X") or not entries.get("Y") or \
213 entries["X"] != "831_3" or entries["Y"] != "53_0":
214 print("getAll failed: %s" % entries)
215 return 1
216 j.remove("modify", "X")
217 val = j.getAll("modify")
218 if val.get("X") or not val.get("Y") or val["Y"] != "53_0":
219 print("remove(X) failed")
220 return 1
221 j.remove("modify", "Y")
222 j.remove("modify", "Z")
223 if j.get("modify", "Y"):
224 print("remove(Y) failed")
225 return 1
226 if j.get("modify", "Z"):
227 print("remove(Z) failed")
228 return 1
229 print("All tests passed")
230 return 0
232if __name__ == '__main__': 232 ↛ 233line 232 didn't jump to line 233, because the condition on line 232 was never true
233 import sys
234 vgName = None
235 if len(sys.argv) > 1:
236 vgName = sys.argv[1]
237 ret = _runTests(vgName)
238 sys.exit(ret)