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 

18from sm_typing import override 

19 

20import os 

21import blktap2 

22import glob 

23import re 

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

25 

26SECTOR_SHIFT = 9 

27 

28 

29class CachingTap(object): 

30 

31 def __init__(self, tapdisk, stats): 

32 self.tapdisk = tapdisk 

33 self.stats = stats 

34 

35 @classmethod 

36 def from_tapdisk(cls, tapdisk, stats): 

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

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

39 # parent-caching tapdev. always checking the complementary 

40 # case, so we bail on unexpected chains. 

41 

42 images = stats['images'] 

43 image = images[-1] 

44 path = image['name'] 

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

46 

47 def __assert(cond): 

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

49 raise cls.NotACachingTapdisk(tapdisk, stats) 

50 

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

52 # parent 

53 

54 return ParentCachingTap(tapdisk, stats) 

55 

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

57 # leaf 

58 st = os.stat(path) 

59 

60 __assert(S_ISBLK(st.st_mode)) 

61 

62 major = os.major(st.st_rdev) 

63 minor = os.minor(st.st_rdev) 

64 

65 __assert(major == tapdisk.major()) 

66 

67 return LeafCachingTap(tapdisk, stats, minor) 

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

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

70 match = minor_matcher.match(path) 

71 __assert(match is not None) 

72 

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

74 return LeafCachingTap(tapdisk, stats, parent_minor) 

75 

76 __assert(0) 

77 

78 class NotACachingTapdisk(Exception): 

79 

80 def __init__(self, tapdisk, stats): 

81 self.tapdisk = tapdisk 

82 self.stats = stats 

83 

84 @override 

85 def __str__(self) -> str: 

86 return \ 

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

88 (self.tapdisk, self.stats) 

89 

90 

91class ParentCachingTap(CachingTap): 

92 

93 def __init__(self, tapdisk, stats): 

94 CachingTap.__init__(self, tapdisk, stats) 

95 self.leaves = [] 

96 

97 def add_leaves(self, tapdisks): 

98 for t in tapdisks: 

99 if t.is_child_of(self): 

100 self.leaves.append(t) 

101 

102 def vdi_stats(self): 

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

104 

105 images = self.stats['images'] 

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

107 

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

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

110 

111 rd_hits = rd_Gc 

112 rd_miss = total - rd_hits 

113 

114 return (rd_hits, rd_miss) 

115 

116 def vdi_stats_total(self): 

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

118 

119 rd_hits, rd_miss = self.vdi_stats() 

120 wr_rdir = 0 

121 

122 for leaf in self.leaves: 

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

124 rd_hits += l_rd_hits 

125 rd_miss += l_rd_miss 

126 wr_rdir += l_wr_rdir 

127 

128 return rd_hits, rd_miss, wr_rdir 

129 

130 @override 

131 def __str__(self) -> str: 

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

133 (self.__class__.__name__, 

134 self.tapdisk.path, self.tapdisk.minor) 

135 

136 

137class LeafCachingTap(CachingTap): 

138 

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

140 CachingTap.__init__(self, tapdisk, stats) 

141 self.parent_minor = parent_minor 

142 

143 def is_child_of(self, parent): 

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

145 

146 def vdi_stats(self): 

147 images = self.stats['images'] 

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

149 

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

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

152 

153 rd_hits = rd_Ac 

154 rd_miss = rd_A 

155 wr_rdir = self.stats['FIXME_enospc_redirect_count'] 

156 

157 return rd_hits, rd_miss, wr_rdir 

158 

159 @override 

160 def __str__(self) -> str: 

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

162 (self.__class__.__name__, 

163 self.tapdisk.path, self.tapdisk.minor) 

164 

165 

166class CacheFileSR(object): 

167 

168 CACHE_NODE_EXT = '.vhdcache' 

169 

170 def __init__(self, sr_path): 

171 self.sr_path = sr_path 

172 

173 def is_mounted(self): 

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

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

176 

177 class NotAMountPoint(Exception): 

178 

179 def __init__(self, path): 

180 self.path = path 

181 

182 @override 

183 def __str__(self) -> str: 

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

185 

186 @classmethod 

187 def from_uuid(cls, sr_uuid): 

188 import SR 

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

190 

191 cache_sr = cls(sr_path) 

192 

193 if not cache_sr.is_mounted(): 

194 raise cls.NotAMountPoint(sr_path) 

195 

196 return cache_sr 

197 

198 @classmethod 

199 def from_session(cls, session): 

200 import util 

201 import SR as sm 

202 

203 host_ref = util.get_localhost_ref(session) 

204 

205 _host = session.xenapi.host 

206 sr_ref = _host.get_local_cache_sr(host_ref) 

207 if not sr_ref: 

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

209 

210 if sr_ref == 'OpaqueRef:NULL': 

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

212 

213 _SR = session.xenapi.SR 

214 sr_uuid = _SR.get_uuid(sr_ref) 

215 

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

217 

218 return cls(target.path) 

219 

220 @classmethod 

221 def from_cli(cls): 

222 import XenAPI # pylint: disable=import-error 

223 

224 session = XenAPI.xapi_local() 

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

226 

227 return cls.from_session(session) 

228 

229 def statvfs(self): 

230 return os.statvfs(self.sr_path) 

231 

232 def _fast_find_nodes(self): 

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

234 

235 found = glob.glob(pattern) 

236 

237 return list(found) 

238 

239 def xapi_vfs_stats(self): 

240 import util 

241 

242 f = self.statvfs() 

243 if not f.f_frsize: 

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

245 

246 fs_size = f.f_frsize * f.f_blocks 

247 fs_free = f.f_frsize * f.f_bfree 

248 

249 fs_cache_total = 0 

250 for path in self._fast_find_nodes(): 

251 st = os.stat(path) 

252 fs_cache_total += st.st_size 

253 

254 return { 

255 'FREE_CACHE_SPACE_AVAILABLE': 

256 fs_free, 

257 'TOTAL_CACHE_UTILISATION': 

258 fs_cache_total, 

259 'TOTAL_UTILISATION_BY_NON_CACHE_DATA': 

260 fs_size - fs_free - fs_cache_total 

261 } 

262 

263 @classmethod 

264 def _fast_find_tapdisks(cls): 

265 import errno 

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

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

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

269 # list all tapdisk and match by path suffix. 

270 

271 tapdisks = [] 

272 

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

274 try: 

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

276 except: 

277 continue 

278 

279 if ext != cls.CACHE_NODE_EXT: 

280 continue 

281 

282 try: 

283 stats = tapdisk.stats() 

284 except blktap2.TapCtl.CommandFailure as e: 

285 if e.errno != errno.ENOENT: 

286 raise 

287 continue # shut down 

288 

289 caching = CachingTap.from_tapdisk(tapdisk, stats) 

290 tapdisks.append(caching) 

291 

292 return tapdisks 

293 

294 def fast_scan_topology(self): 

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

296 # and which ones cache parents. 

297 

298 parents = [] 

299 leaves = [] 

300 

301 for caching in self._fast_find_tapdisks(): 

302 if type(caching) == ParentCachingTap: 

303 parents.append(caching) 

304 else: 

305 leaves.append(caching) 

306 

307 for parent in parents: 

308 parent.add_leaves(leaves) 

309 

310 return parents 

311 

312 def vdi_stats_total(self): 

313 

314 parents = self.fast_scan_topology() 

315 

316 rd_hits, rd_miss, wr_rdir = 0, 0, 0 

317 

318 for parent in parents: 

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

320 rd_hits += p_rd_hits 

321 rd_miss += p_rd_miss 

322 wr_rdir += p_wr_rdir 

323 

324 return rd_hits, rd_miss, wr_rdir 

325 

326 def xapi_vdi_stats(self): 

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

328 

329 return { 

330 'TOTAL_CACHE_HITS': 

331 rd_hits << SECTOR_SHIFT, 

332 'TOTAL_CACHE_MISSES': 

333 rd_miss << SECTOR_SHIFT, 

334 'TOTAL_CACHE_ENOSPACE_REDIRECTS': 

335 wr_rdir << SECTOR_SHIFT, 

336 } 

337 

338 def xapi_stats(self): 

339 

340 vfs = self.xapi_vfs_stats() 

341 vdi = self.xapi_vdi_stats() 

342 

343 vfs.update(vdi) 

344 return vfs 

345 

346CacheSR = CacheFileSR 

347 

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

349 

350 import sys 

351 from pprint import pprint 

352 

353 args = list(sys.argv) 

354 prog = args.pop(0) 

355 prog = os.path.basename(prog) 

356 

357 

358 def usage(stream): 

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

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

361 else: 

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

363 

364 

365 def usage_error(): 

366 usage(sys.stderr) 

367 sys.exit(1) 

368 

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

370 cmd = 'sr.stats' 

371 else: 

372 try: 

373 cmd = args.pop(0) 

374 except IndexError: 

375 usage_error() 

376 

377 try: 

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

379 except: 

380 usage(sys.stderr) 

381 sys.exit(1) 

382 

383 if _class == 'sr': 

384 try: 

385 uuid = args.pop(0) 

386 except IndexError: 

387 cache_sr = CacheSR.from_cli() 

388 else: 

389 cache_sr = CacheSR.from_uuid(uuid) 

390 

391 if method == 'stats': 

392 

393 d = cache_sr.xapi_stats() 

394 for item in d.items(): 

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

396 

397 elif method == 'topology': 

398 parents = cache_sr.fast_scan_topology() 

399 

400 for parent in parents: 

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

402 (parent.vdi_stats(), parent.vdi_stats_total())) 

403 pprint(parent.stats) 

404 

405 for leaf in parent.leaves: 

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

407 pprint(leaf.stats) 

408 

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

410 

411 else: 

412 usage_error() 

413 else: 

414 usage_error()