Hide keyboard shortcuts

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/python3 

2# 

3# Copyright (C) Citrix Systems Inc. 

4# 

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

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

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

8# 

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 Lesser General Public License for more details. 

13# 

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

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

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

17 

18import util 

19from sm_typing import override 

20 

21import os 

22import blktap2 

23import glob 

24import re 

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

26 

27from vditype import VdiType 

28 

29SECTOR_SHIFT = 9 

30 

31 

32class CachingTap(object): 

33 

34 def __init__(self, tapdisk, stats): 

35 self.tapdisk = tapdisk 

36 self.stats = stats 

37 

38 @classmethod 

39 def from_tapdisk(cls, tapdisk, stats): 

40 # pick the last image. if it's a COW, we got a parent 

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

42 # parent-caching tapdev. always checking the complementary 

43 # case, so we bail on unexpected chains. 

44 

45 images = stats['images'] 

46 image = images[-1] 

47 path = image['name'] 

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

49 

50 def __assert(cond): 

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

52 raise cls.NotACachingTapdisk(tapdisk, stats) 

53 

54 if VdiType.isCowImage(_type): 54 ↛ 57line 54 didn't jump to line 57, because the condition on line 54 was never true

55 # parent 

56 

57 return ParentCachingTap(tapdisk, stats) 

58 

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

60 # leaf 

61 st = os.stat(path) 

62 

63 __assert(S_ISBLK(st.st_mode)) 

64 

65 major = os.major(st.st_rdev) 

66 minor = os.minor(st.st_rdev) 

67 

68 __assert(major == tapdisk.major()) 

69 

70 return LeafCachingTap(tapdisk, stats, minor) 

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

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

73 match = minor_matcher.match(path) 

74 __assert(match is not None) 

75 

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

77 return LeafCachingTap(tapdisk, stats, parent_minor) 

78 

79 __assert(0) 

80 

81 class NotACachingTapdisk(Exception): 

82 

83 def __init__(self, tapdisk, stats): 

84 self.tapdisk = tapdisk 

85 self.stats = stats 

86 

87 @override 

88 def __str__(self) -> str: 

89 return \ 

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

91 (self.tapdisk, self.stats) 

92 

93 

94class ParentCachingTap(CachingTap): 

95 

96 def __init__(self, tapdisk, stats): 

97 CachingTap.__init__(self, tapdisk, stats) 

98 self.leaves = [] 

99 

100 def add_leaves(self, tapdisks): 

101 for t in tapdisks: 

102 if t.is_child_of(self): 

103 self.leaves.append(t) 

104 

105 def vdi_stats(self): 

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

107 

108 images = self.stats['images'] 

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

110 

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

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

113 

114 rd_hits = rd_Gc 

115 rd_miss = total - rd_hits 

116 

117 return (rd_hits, rd_miss) 

118 

119 def vdi_stats_total(self): 

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

121 

122 rd_hits, rd_miss = self.vdi_stats() 

123 wr_rdir = 0 

124 

125 for leaf in self.leaves: 

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

127 rd_hits += l_rd_hits 

128 rd_miss += l_rd_miss 

129 wr_rdir += l_wr_rdir 

130 

131 return rd_hits, rd_miss, wr_rdir 

132 

133 @override 

134 def __str__(self) -> str: 

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

136 (self.__class__.__name__, 

137 self.tapdisk.path, self.tapdisk.minor) 

138 

139 

140class LeafCachingTap(CachingTap): 

141 

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

143 CachingTap.__init__(self, tapdisk, stats) 

144 self.parent_minor = parent_minor 

145 

146 def is_child_of(self, parent): 

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

148 

149 def vdi_stats(self): 

150 images = self.stats['images'] 

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

152 

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

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

155 

156 rd_hits = rd_Ac 

157 rd_miss = rd_A 

158 wr_rdir = self.stats['FIXME_enospc_redirect_count'] 

159 

160 return rd_hits, rd_miss, wr_rdir 

161 

162 @override 

163 def __str__(self) -> str: 

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

165 (self.__class__.__name__, 

166 self.tapdisk.path, self.tapdisk.minor) 

167 

168 

169class CacheFileSR(object): 

170 

171 CACHE_NODE_EXT = '.vhdcache' 

172 

173 def __init__(self, sr_path): 

174 self.sr_path = sr_path 

175 

176 def is_mounted(self): 

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

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

179 

180 class NotAMountPoint(Exception): 

181 

182 def __init__(self, path): 

183 self.path = path 

184 

185 @override 

186 def __str__(self) -> str: 

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

188 

189 @classmethod 

190 def from_uuid(cls, sr_uuid): 

191 import SR 

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

193 

194 cache_sr = cls(sr_path) 

195 

196 if not cache_sr.is_mounted(): 

197 raise cls.NotAMountPoint(sr_path) 

198 

199 return cache_sr 

200 

201 @classmethod 

202 def from_session(cls, session): 

203 import SR as sm 

204 

205 host_ref = util.get_localhost_ref(session) 

206 

207 _host = session.xenapi.host 

208 sr_ref = _host.get_local_cache_sr(host_ref) 

209 if not sr_ref: 

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

211 

212 if sr_ref == 'OpaqueRef:NULL': 

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

214 

215 _SR = session.xenapi.SR 

216 sr_uuid = _SR.get_uuid(sr_ref) 

217 

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

219 

220 return cls(target.path) 

221 

222 @classmethod 

223 def from_cli(cls): 

224 with util.ApiSession("SM-local-cache") as session: 

225 return cls.from_session(session) 

226 

227 def statvfs(self): 

228 return os.statvfs(self.sr_path) 

229 

230 def _fast_find_nodes(self): 

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

232 

233 found = glob.glob(pattern) 

234 

235 return list(found) 

236 

237 def xapi_vfs_stats(self): 

238 f = self.statvfs() 

239 if not f.f_frsize: 

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

241 

242 fs_size = f.f_frsize * f.f_blocks 

243 fs_free = f.f_frsize * f.f_bfree 

244 

245 fs_cache_total = 0 

246 for path in self._fast_find_nodes(): 

247 st = os.stat(path) 

248 fs_cache_total += st.st_size 

249 

250 return { 

251 'FREE_CACHE_SPACE_AVAILABLE': 

252 fs_free, 

253 'TOTAL_CACHE_UTILISATION': 

254 fs_cache_total, 

255 'TOTAL_UTILISATION_BY_NON_CACHE_DATA': 

256 fs_size - fs_free - fs_cache_total 

257 } 

258 

259 @classmethod 

260 def _fast_find_tapdisks(cls): 

261 import errno 

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

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

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

265 # list all tapdisk and match by path suffix. 

266 

267 tapdisks = [] 

268 

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

270 try: 

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

272 except: 

273 continue 

274 

275 if ext != cls.CACHE_NODE_EXT: 

276 continue 

277 

278 try: 

279 stats = tapdisk.stats() 

280 except blktap2.TapCtl.CommandFailure as e: 

281 if e.errno != errno.ENOENT: 

282 raise 

283 continue # shut down 

284 

285 caching = CachingTap.from_tapdisk(tapdisk, stats) 

286 tapdisks.append(caching) 

287 

288 return tapdisks 

289 

290 def fast_scan_topology(self): 

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

292 # and which ones cache parents. 

293 

294 parents = [] 

295 leaves = [] 

296 

297 for caching in self._fast_find_tapdisks(): 

298 if type(caching) == ParentCachingTap: 

299 parents.append(caching) 

300 else: 

301 leaves.append(caching) 

302 

303 for parent in parents: 

304 parent.add_leaves(leaves) 

305 

306 return parents 

307 

308 def vdi_stats_total(self): 

309 

310 parents = self.fast_scan_topology() 

311 

312 rd_hits, rd_miss, wr_rdir = 0, 0, 0 

313 

314 for parent in parents: 

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

316 rd_hits += p_rd_hits 

317 rd_miss += p_rd_miss 

318 wr_rdir += p_wr_rdir 

319 

320 return rd_hits, rd_miss, wr_rdir 

321 

322 def xapi_vdi_stats(self): 

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

324 

325 return { 

326 'TOTAL_CACHE_HITS': 

327 rd_hits << SECTOR_SHIFT, 

328 'TOTAL_CACHE_MISSES': 

329 rd_miss << SECTOR_SHIFT, 

330 'TOTAL_CACHE_ENOSPACE_REDIRECTS': 

331 wr_rdir << SECTOR_SHIFT, 

332 } 

333 

334 def xapi_stats(self): 

335 

336 vfs = self.xapi_vfs_stats() 

337 vdi = self.xapi_vdi_stats() 

338 

339 vfs.update(vdi) 

340 return vfs 

341 

342CacheSR = CacheFileSR 

343 

344if __name__ == '__main__': 344 ↛ 346line 344 didn't jump to line 346, because the condition on line 344 was never true

345 

346 import sys 

347 from pprint import pprint 

348 

349 args = list(sys.argv) 

350 prog = args.pop(0) 

351 prog = os.path.basename(prog) 

352 

353 

354 def usage(stream): 

355 if prog == 'tapdisk-cache-stats': 

356 print("usage: tapdisk-cache-stats [<sr-uuid>]", file=stream) 

357 else: 

358 print("usage: %s sr.{stats|topology} [<sr-uuid>]" % prog, file=stream) 

359 

360 

361 def usage_error(): 

362 usage(sys.stderr) 

363 sys.exit(1) 

364 

365 if prog == 'tapdisk-cache-stats': 

366 cmd = 'sr.stats' 

367 else: 

368 try: 

369 cmd = args.pop(0) 

370 except IndexError: 

371 usage_error() 

372 

373 try: 

374 _class, method = cmd.split('.') 

375 except: 

376 usage(sys.stderr) 

377 sys.exit(1) 

378 

379 if _class == 'sr': 

380 try: 

381 uuid = args.pop(0) 

382 except IndexError: 

383 cache_sr = CacheSR.from_cli() 

384 else: 

385 cache_sr = CacheSR.from_uuid(uuid) 

386 

387 if method == 'stats': 

388 

389 d = cache_sr.xapi_stats() 

390 for item in d.items(): 

391 print("%s=%s" % item) 

392 

393 elif method == 'topology': 

394 parents = cache_sr.fast_scan_topology() 

395 

396 for parent in parents: 

397 print(parent, "hits/miss=%s total=%s" % \ 

398 (parent.vdi_stats(), parent.vdi_stats_total())) 

399 pprint(parent.stats) 

400 

401 for leaf in parent.leaves: 

402 print(leaf, "hits/miss=%s" % str(leaf.vdi_stats())) 

403 pprint(leaf.stats) 

404 

405 print("sr.total=%s" % str(cache_sr.vdi_stats_total())) 

406 

407 else: 

408 usage_error() 

409 else: 

410 usage_error()