Coverage for libs/sm/lcache.py: 33%

191 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2026-07-10 07:59 +0000

1# Copyright (C) Citrix Systems Inc. 

2# 

3# This program is free software; you can redistribute it and/or modify 

4# it under the terms of the GNU Lesser General Public License as published 

5# by the Free Software Foundation; version 2.1 only. 

6# 

7# This program is distributed in the hope that it will be useful, 

8# but WITHOUT ANY WARRANTY; without even the implied warranty of 

9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

10# GNU Lesser General Public License for more details. 

11# 

12# You should have received a copy of the GNU Lesser General Public License 

13# along with this program; if not, write to the Free Software Foundation, Inc., 

14# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 

15 

16import os 

17import glob 

18import re 

19from stat import * # S_ISBLK(), ... 

20from sm import blktap2 

21 

22SECTOR_SHIFT = 9 

23 

24 

25class CachingTap(object): 

26 

27 def __init__(self, tapdisk, stats): 

28 self.tapdisk = tapdisk 

29 self.stats = stats 

30 

31 @classmethod 

32 def from_tapdisk(cls, tapdisk, stats): 

33 # pick the last image. if it's a VHD, we got a parent 

34 # cache. the leaf case is an aio node sitting on a 

35 # parent-caching tapdev. always checking the complementary 

36 # case, so we bail on unexpected chains. 

37 

38 images = stats['images'] 

39 image = images[-1] 

40 path = image['name'] 

41 _type = image['driver']['name'] 

42 

43 def __assert(cond): 

44 if not cond: 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true

45 raise cls.NotACachingTapdisk(tapdisk, stats) 

46 

47 if _type == 'vhd': 47 ↛ 50line 47 didn't jump to line 50, because the condition on line 47 was never true

48 # parent 

49 

50 return ParentCachingTap(tapdisk, stats) 

51 

52 elif _type == 'aio': 52 ↛ 54line 52 didn't jump to line 54, because the condition on line 52 was never true

53 # leaf 

54 st = os.stat(path) 

55 

56 __assert(S_ISBLK(st.st_mode)) 

57 

58 major = os.major(st.st_rdev) 

59 minor = os.minor(st.st_rdev) 

60 

61 __assert(major == tapdisk.major()) 

62 

63 return LeafCachingTap(tapdisk, stats, minor) 

64 elif _type == 'nbd' and 'run/blktap-control/nbd' in path: 64 ↛ 72line 64 didn't jump to line 72, because the condition on line 64 was never false

65 minor_matcher = re.compile(r'.*run/blktap-control/nbd\d+\.(\d+)$') 

66 match = minor_matcher.match(path) 

67 __assert(match is not None) 

68 

69 parent_minor = int(match.group(1)) 

70 return LeafCachingTap(tapdisk, stats, parent_minor) 

71 

72 __assert(0) 

73 

74 class NotACachingTapdisk(Exception): 

75 

76 def __init__(self, tapdisk, stats): 

77 self.tapdisk = tapdisk 

78 self.stats = stats 

79 

80 def __str__(self): 

81 return \ 

82 "Tapdisk %s in state '%s' not found caching." % \ 

83 (self.tapdisk, self.stats) 

84 

85 

86class ParentCachingTap(CachingTap): 

87 

88 def __init__(self, tapdisk, stats): 

89 CachingTap.__init__(self, tapdisk, stats) 

90 self.leaves = [] 

91 

92 def add_leaves(self, tapdisks): 

93 for t in tapdisks: 

94 if t.is_child_of(self): 

95 self.leaves.append(t) 

96 

97 def vdi_stats(self): 

98 """Parent caching hits/miss count.""" 

99 

100 images = self.stats['images'] 

101 total = self.stats['secs'][0] 

102 

103 rd_Gc = images[0]['hits'][0] 

104 rd_lc = images[1]['hits'][0] 

105 

106 rd_hits = rd_Gc 

107 rd_miss = total - rd_hits 

108 

109 return (rd_hits, rd_miss) 

110 

111 def vdi_stats_total(self): 

112 """VDI total stats, including leaf hits/miss counts.""" 

113 

114 rd_hits, rd_miss = self.vdi_stats() 

115 wr_rdir = 0 

116 

117 for leaf in self.leaves: 

118 l_rd_hits, l_rd_miss, l_wr_rdir = leaf.vdi_stats() 

119 rd_hits += l_rd_hits 

120 rd_miss += l_rd_miss 

121 wr_rdir += l_wr_rdir 

122 

123 return rd_hits, rd_miss, wr_rdir 

124 

125 def __str__(self): 

126 return "%s(%s, minor=%s)" % \ 

127 (self.__class__.__name__, 

128 self.tapdisk.path, self.tapdisk.minor) 

129 

130 

131class LeafCachingTap(CachingTap): 

132 

133 def __init__(self, tapdisk, stats, parent_minor): 

134 CachingTap.__init__(self, tapdisk, stats) 

135 self.parent_minor = parent_minor 

136 

137 def is_child_of(self, parent): 

138 return parent.tapdisk.minor == self.parent_minor 

139 

140 def vdi_stats(self): 

141 images = self.stats['images'] 

142 total = self.stats['secs'][0] 

143 

144 rd_Ac = images[0]['hits'][0] 

145 rd_A = images[1]['hits'][0] 

146 

147 rd_hits = rd_Ac 

148 rd_miss = rd_A 

149 wr_rdir = self.stats['FIXME_enospc_redirect_count'] 

150 

151 return rd_hits, rd_miss, wr_rdir 

152 

153 def __str__(self): 

154 return "%s(%s, minor=%s)" % \ 

155 (self.__class__.__name__, 

156 self.tapdisk.path, self.tapdisk.minor) 

157 

158 

159class CacheFileSR(object): 

160 

161 CACHE_NODE_EXT = '.vhdcache' 

162 

163 def __init__(self, sr_path): 

164 self.sr_path = sr_path 

165 

166 def is_mounted(self): 

167 # NB. a basic check should do, currently only for CLI usage. 

168 return os.path.exists(self.sr_path) 

169 

170 class NotAMountPoint(Exception): 

171 

172 def __init__(self, path): 

173 self.path = path 

174 

175 def __str__(self): 

176 return "Not a mount point: %s" % self.path 

177 

178 @classmethod 

179 def from_uuid(cls, sr_uuid): 

180 from sm import SR 

181 sr_path = "%s/%s" % (SR.MOUNT_BASE, sr_uuid) 

182 

183 cache_sr = cls(sr_path) 

184 

185 if not cache_sr.is_mounted(): 

186 raise cls.NotAMountPoint(sr_path) 

187 

188 return cache_sr 

189 

190 @classmethod 

191 def from_session(cls, session): 

192 from sm.core import util 

193 from sm import SR as sm 

194 

195 host_ref = util.get_localhost_ref(session) 

196 

197 _host = session.xenapi.host 

198 sr_ref = _host.get_local_cache_sr(host_ref) 

199 if not sr_ref: 

200 raise util.SMException("Local cache SR not specified") 

201 

202 if sr_ref == 'OpaqueRef:NULL': 

203 raise util.SMException("Local caching not enabled.") 

204 

205 _SR = session.xenapi.SR 

206 sr_uuid = _SR.get_uuid(sr_ref) 

207 

208 target = sm.SR.from_uuid(session, sr_uuid) 

209 

210 return cls(target.path) 

211 

212 @classmethod 

213 def from_cli(cls): 

214 import XenAPI # pylint: disable=import-error 

215 

216 session = XenAPI.xapi_local() 

217 session.xenapi.login_with_password('root', '', '', 'SM') 

218 

219 return cls.from_session(session) 

220 

221 def statvfs(self): 

222 return os.statvfs(self.sr_path) 

223 

224 def _fast_find_nodes(self): 

225 pattern = "%s/*%s" % (self.sr_path, self.CACHE_NODE_EXT) 

226 

227 found = glob.glob(pattern) 

228 

229 return list(found) 

230 

231 def xapi_vfs_stats(self): 

232 from sm.core import util 

233 

234 f = self.statvfs() 

235 if not f.f_frsize: 

236 raise util.SMException("Cache FS does not report utilization.") 

237 

238 fs_size = f.f_frsize * f.f_blocks 

239 fs_free = f.f_frsize * f.f_bfree 

240 

241 fs_cache_total = 0 

242 for path in self._fast_find_nodes(): 

243 st = os.stat(path) 

244 fs_cache_total += st.st_size 

245 

246 return { 

247 'FREE_CACHE_SPACE_AVAILABLE': 

248 fs_free, 

249 'TOTAL_CACHE_UTILISATION': 

250 fs_cache_total, 

251 'TOTAL_UTILISATION_BY_NON_CACHE_DATA': 

252 fs_size - fs_free - fs_cache_total 

253 } 

254 

255 @classmethod 

256 def _fast_find_tapdisks(cls): 

257 import errno 

258 # NB. we're only about to gather stats here, so take the 

259 # fastpath, bypassing agent based VBD[currently-attached] -> 

260 # VDI[allow-caching] -> Tap resolution altogether. Instead, we 

261 # list all tapdisk and match by path suffix. 

262 

263 tapdisks = [] 

264 

265 for tapdisk in blktap2.Tapdisk.list(): 

266 try: 

267 ext = os.path.splitext(tapdisk.path)[1] 

268 except: 

269 continue 

270 

271 if ext != cls.CACHE_NODE_EXT: 

272 continue 

273 

274 try: 

275 stats = tapdisk.stats() 

276 except blktap2.TapCtl.CommandFailure as e: 

277 if e.errno != errno.ENOENT: 

278 raise 

279 continue # shut down 

280 

281 caching = CachingTap.from_tapdisk(tapdisk, stats) 

282 tapdisks.append(caching) 

283 

284 return tapdisks 

285 

286 def fast_scan_topology(self): 

287 # NB. gather all tapdisks. figure out which ones are leaves 

288 # and which ones cache parents. 

289 

290 parents = [] 

291 leaves = [] 

292 

293 for caching in self._fast_find_tapdisks(): 

294 if type(caching) == ParentCachingTap: 

295 parents.append(caching) 

296 else: 

297 leaves.append(caching) 

298 

299 for parent in parents: 

300 parent.add_leaves(leaves) 

301 

302 return parents 

303 

304 def vdi_stats_total(self): 

305 

306 parents = self.fast_scan_topology() 

307 

308 rd_hits, rd_miss, wr_rdir = 0, 0, 0 

309 

310 for parent in parents: 

311 p_rd_hits, p_rd_miss, p_wr_rdir = parent.vdi_stats_total() 

312 rd_hits += p_rd_hits 

313 rd_miss += p_rd_miss 

314 wr_rdir += p_wr_rdir 

315 

316 return rd_hits, rd_miss, wr_rdir 

317 

318 def xapi_vdi_stats(self): 

319 rd_hits, rd_miss, wr_rdir = self.vdi_stats_total() 

320 

321 return { 

322 'TOTAL_CACHE_HITS': 

323 rd_hits << SECTOR_SHIFT, 

324 'TOTAL_CACHE_MISSES': 

325 rd_miss << SECTOR_SHIFT, 

326 'TOTAL_CACHE_ENOSPACE_REDIRECTS': 

327 wr_rdir << SECTOR_SHIFT, 

328 } 

329 

330 def xapi_stats(self): 

331 

332 vfs = self.xapi_vfs_stats() 

333 vdi = self.xapi_vdi_stats() 

334 

335 vfs.update(vdi) 

336 return vfs 

337 

338CacheSR = CacheFileSR 

339