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 

26from vditype import VdiType 

27 

28SECTOR_SHIFT = 9 

29 

30 

31class CachingTap(object): 

32 

33 def __init__(self, tapdisk, stats): 

34 self.tapdisk = tapdisk 

35 self.stats = stats 

36 

37 @classmethod 

38 def from_tapdisk(cls, tapdisk, stats): 

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

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

41 # parent-caching tapdev. always checking the complementary 

42 # case, so we bail on unexpected chains. 

43 

44 images = stats['images'] 

45 image = images[-1] 

46 path = image['name'] 

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

48 

49 def __assert(cond): 

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

51 raise cls.NotACachingTapdisk(tapdisk, stats) 

52 

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

54 # parent 

55 

56 return ParentCachingTap(tapdisk, stats) 

57 

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

59 # leaf 

60 st = os.stat(path) 

61 

62 __assert(S_ISBLK(st.st_mode)) 

63 

64 major = os.major(st.st_rdev) 

65 minor = os.minor(st.st_rdev) 

66 

67 __assert(major == tapdisk.major()) 

68 

69 return LeafCachingTap(tapdisk, stats, minor) 

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

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

72 match = minor_matcher.match(path) 

73 __assert(match is not None) 

74 

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

76 return LeafCachingTap(tapdisk, stats, parent_minor) 

77 

78 __assert(0) 

79 

80 class NotACachingTapdisk(Exception): 

81 

82 def __init__(self, tapdisk, stats): 

83 self.tapdisk = tapdisk 

84 self.stats = stats 

85 

86 @override 

87 def __str__(self) -> str: 

88 return \ 

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

90 (self.tapdisk, self.stats) 

91 

92 

93class ParentCachingTap(CachingTap): 

94 

95 def __init__(self, tapdisk, stats): 

96 CachingTap.__init__(self, tapdisk, stats) 

97 self.leaves = [] 

98 

99 def add_leaves(self, tapdisks): 

100 for t in tapdisks: 

101 if t.is_child_of(self): 

102 self.leaves.append(t) 

103 

104 def vdi_stats(self): 

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

106 

107 images = self.stats['images'] 

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

109 

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

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

112 

113 rd_hits = rd_Gc 

114 rd_miss = total - rd_hits 

115 

116 return (rd_hits, rd_miss) 

117 

118 def vdi_stats_total(self): 

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

120 

121 rd_hits, rd_miss = self.vdi_stats() 

122 wr_rdir = 0 

123 

124 for leaf in self.leaves: 

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

126 rd_hits += l_rd_hits 

127 rd_miss += l_rd_miss 

128 wr_rdir += l_wr_rdir 

129 

130 return rd_hits, rd_miss, wr_rdir 

131 

132 @override 

133 def __str__(self) -> str: 

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

135 (self.__class__.__name__, 

136 self.tapdisk.path, self.tapdisk.minor) 

137 

138 

139class LeafCachingTap(CachingTap): 

140 

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

142 CachingTap.__init__(self, tapdisk, stats) 

143 self.parent_minor = parent_minor 

144 

145 def is_child_of(self, parent): 

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

147 

148 def vdi_stats(self): 

149 images = self.stats['images'] 

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

151 

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

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

154 

155 rd_hits = rd_Ac 

156 rd_miss = rd_A 

157 wr_rdir = self.stats['FIXME_enospc_redirect_count'] 

158 

159 return rd_hits, rd_miss, wr_rdir 

160 

161 @override 

162 def __str__(self) -> str: 

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

164 (self.__class__.__name__, 

165 self.tapdisk.path, self.tapdisk.minor) 

166 

167 

168class CacheFileSR(object): 

169 

170 CACHE_NODE_EXT = '.vhdcache' 

171 

172 def __init__(self, sr_path): 

173 self.sr_path = sr_path 

174 

175 def is_mounted(self): 

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

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

178 

179 class NotAMountPoint(Exception): 

180 

181 def __init__(self, path): 

182 self.path = path 

183 

184 @override 

185 def __str__(self) -> str: 

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

187 

188 @classmethod 

189 def from_uuid(cls, sr_uuid): 

190 import SR 

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

192 

193 cache_sr = cls(sr_path) 

194 

195 if not cache_sr.is_mounted(): 

196 raise cls.NotAMountPoint(sr_path) 

197 

198 return cache_sr 

199 

200 @classmethod 

201 def from_session(cls, session): 

202 import util 

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 import XenAPI # pylint: disable=import-error 

225 

226 session = XenAPI.xapi_local() 

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

228 

229 return cls.from_session(session) 

230 

231 def statvfs(self): 

232 return os.statvfs(self.sr_path) 

233 

234 def _fast_find_nodes(self): 

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

236 

237 found = glob.glob(pattern) 

238 

239 return list(found) 

240 

241 def xapi_vfs_stats(self): 

242 import util 

243 

244 f = self.statvfs() 

245 if not f.f_frsize: 

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

247 

248 fs_size = f.f_frsize * f.f_blocks 

249 fs_free = f.f_frsize * f.f_bfree 

250 

251 fs_cache_total = 0 

252 for path in self._fast_find_nodes(): 

253 st = os.stat(path) 

254 fs_cache_total += st.st_size 

255 

256 return { 

257 'FREE_CACHE_SPACE_AVAILABLE': 

258 fs_free, 

259 'TOTAL_CACHE_UTILISATION': 

260 fs_cache_total, 

261 'TOTAL_UTILISATION_BY_NON_CACHE_DATA': 

262 fs_size - fs_free - fs_cache_total 

263 } 

264 

265 @classmethod 

266 def _fast_find_tapdisks(cls): 

267 import errno 

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

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

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

271 # list all tapdisk and match by path suffix. 

272 

273 tapdisks = [] 

274 

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

276 try: 

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

278 except: 

279 continue 

280 

281 if ext != cls.CACHE_NODE_EXT: 

282 continue 

283 

284 try: 

285 stats = tapdisk.stats() 

286 except blktap2.TapCtl.CommandFailure as e: 

287 if e.errno != errno.ENOENT: 

288 raise 

289 continue # shut down 

290 

291 caching = CachingTap.from_tapdisk(tapdisk, stats) 

292 tapdisks.append(caching) 

293 

294 return tapdisks 

295 

296 def fast_scan_topology(self): 

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

298 # and which ones cache parents. 

299 

300 parents = [] 

301 leaves = [] 

302 

303 for caching in self._fast_find_tapdisks(): 

304 if type(caching) == ParentCachingTap: 

305 parents.append(caching) 

306 else: 

307 leaves.append(caching) 

308 

309 for parent in parents: 

310 parent.add_leaves(leaves) 

311 

312 return parents 

313 

314 def vdi_stats_total(self): 

315 

316 parents = self.fast_scan_topology() 

317 

318 rd_hits, rd_miss, wr_rdir = 0, 0, 0 

319 

320 for parent in parents: 

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

322 rd_hits += p_rd_hits 

323 rd_miss += p_rd_miss 

324 wr_rdir += p_wr_rdir 

325 

326 return rd_hits, rd_miss, wr_rdir 

327 

328 def xapi_vdi_stats(self): 

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

330 

331 return { 

332 'TOTAL_CACHE_HITS': 

333 rd_hits << SECTOR_SHIFT, 

334 'TOTAL_CACHE_MISSES': 

335 rd_miss << SECTOR_SHIFT, 

336 'TOTAL_CACHE_ENOSPACE_REDIRECTS': 

337 wr_rdir << SECTOR_SHIFT, 

338 } 

339 

340 def xapi_stats(self): 

341 

342 vfs = self.xapi_vfs_stats() 

343 vdi = self.xapi_vdi_stats() 

344 

345 vfs.update(vdi) 

346 return vfs 

347 

348CacheSR = CacheFileSR 

349 

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

351 

352 import sys 

353 from pprint import pprint 

354 

355 args = list(sys.argv) 

356 prog = args.pop(0) 

357 prog = os.path.basename(prog) 

358 

359 

360 def usage(stream): 

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

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

363 else: 

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

365 

366 

367 def usage_error(): 

368 usage(sys.stderr) 

369 sys.exit(1) 

370 

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

372 cmd = 'sr.stats' 

373 else: 

374 try: 

375 cmd = args.pop(0) 

376 except IndexError: 

377 usage_error() 

378 

379 try: 

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

381 except: 

382 usage(sys.stderr) 

383 sys.exit(1) 

384 

385 if _class == 'sr': 

386 try: 

387 uuid = args.pop(0) 

388 except IndexError: 

389 cache_sr = CacheSR.from_cli() 

390 else: 

391 cache_sr = CacheSR.from_uuid(uuid) 

392 

393 if method == 'stats': 

394 

395 d = cache_sr.xapi_stats() 

396 for item in d.items(): 

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

398 

399 elif method == 'topology': 

400 parents = cache_sr.fast_scan_topology() 

401 

402 for parent in parents: 

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

404 (parent.vdi_stats(), parent.vdi_stats_total())) 

405 pprint(parent.stats) 

406 

407 for leaf in parent.leaves: 

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

409 pprint(leaf.stats) 

410 

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

412 

413 else: 

414 usage_error() 

415 else: 

416 usage_error()