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

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 

18 

19from sm.core import util 

20from sm.core import xs_errors 

21from sm.srmetadata import open_file, file_read_wrapper, file_write_wrapper 

22 

23LVM_MAX_NAME_LEN = 127 

24 

25 

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.""" 

30 

31 LV_SIZE = 4 * 1024 * 1024 # minimum size 

32 LV_TAG = "journaler" 

33 SEPARATOR = "_" 

34 JRN_CLONE = "clone" 

35 JRN_LEAF = "leaf" 

36 

37 def __init__(self, lvmCache): 

38 self.vgName = lvmCache.vgName 

39 self.lvmCache = lvmCache 

40 

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) 

49 

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 

56 

57 self.lvmCache.create(lvName, self.LV_SIZE, self.LV_TAG) 

58 

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) 

83 

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) 

92 

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) 

100 

101 mapperDevice = self._getLVMapperName(lvName) 

102 if len(mapperDevice) > LVM_MAX_NAME_LEN: 

103 lvName = self._getNameLV(type, id) 

104 self.lvmCache.remove(lvName) 

105 

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) 

113 

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] 

120 

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 

129 

130 def _getNameLV(self, type, id, val=1): 

131 return "%s%s%s%s%s" % (type, self.SEPARATOR, id, self.SEPARATOR, val) 

132 

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 

165 

166 def _getLVMapperName(self, lvName): 

167 return '%s-%s' % (self.vgName.replace("-", "--"), lvName) 

168 

169########################################################################### 

170# 

171# Unit tests 

172# 

173from sm import lvutil 

174from sm import lvmcache 

175 

176 

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 

186 

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 

231 

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)