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 

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

24 

25SECTOR_SHIFT = 9 

26 

27 

28class CachingTap(object): 

29 

30 def __init__(self, tapdisk, stats): 

31 self.tapdisk = tapdisk 

32 self.stats = stats 

33 

34 @classmethod 

35 def from_tapdisk(cls, tapdisk, stats): 

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

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

38 # parent-caching tapdev. always checking the complementary 

39 # case, so we bail on unexpected chains. 

40 

41 images = stats['images'] 

42 image = images[-1] 

43 path = image['name'] 

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

45 

46 def __assert(cond): 

47 if not cond: 

48 raise cls.NotACachingTapdisk(tapdisk, stats) 

49 

50 if _type == 'vhd': 

51 # parent 

52 

53 return ParentCachingTap(tapdisk, stats) 

54 

55 elif _type == 'aio': 

56 # leaf 

57 st = os.stat(path) 

58 

59 __assert(S_ISBLK(st.st_mode)) 

60 

61 major = os.major(st.st_rdev) 

62 minor = os.minor(st.st_rdev) 

63 

64 __assert(major == tapdisk.major()) 

65 

66 return LeafCachingTap(tapdisk, stats, minor) 

67 

68 __assert(0) 

69 

70 class NotACachingTapdisk(Exception): 

71 

72 def __init__(self, tapdisk, stats): 

73 self.tapdisk = tapdisk 

74 self.stats = stats 

75 

76 @override 

77 def __str__(self) -> str: 

78 return \ 

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

80 (self.tapdisk, self.stats) 

81 

82 

83class ParentCachingTap(CachingTap): 

84 

85 def __init__(self, tapdisk, stats): 

86 CachingTap.__init__(self, tapdisk, stats) 

87 self.leaves = [] 

88 

89 def add_leaves(self, tapdisks): 

90 for t in tapdisks: 

91 if t.is_child_of(self): 

92 self.leaves.append(t) 

93 

94 def vdi_stats(self): 

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

96 

97 images = self.stats['images'] 

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

99 

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

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

102 

103 rd_hits = rd_Gc 

104 rd_miss = total - rd_hits 

105 

106 return (rd_hits, rd_miss) 

107 

108 def vdi_stats_total(self): 

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

110 

111 rd_hits, rd_miss = self.vdi_stats() 

112 wr_rdir = 0 

113 

114 for leaf in self.leaves: 

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

116 rd_hits += l_rd_hits 

117 rd_miss += l_rd_miss 

118 wr_rdir += l_wr_rdir 

119 

120 return rd_hits, rd_miss, wr_rdir 

121 

122 @override 

123 def __str__(self) -> str: 

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

125 (self.__class__.__name__, 

126 self.tapdisk.path, self.tapdisk.minor) 

127 

128 

129class LeafCachingTap(CachingTap): 

130 

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

132 CachingTap.__init__(self, tapdisk, stats) 

133 self.parent_minor = parent_minor 

134 

135 def is_child_of(self, parent): 

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

137 

138 def vdi_stats(self): 

139 images = self.stats['images'] 

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

141 

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

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

144 

145 rd_hits = rd_Ac 

146 rd_miss = rd_A 

147 wr_rdir = self.stats['FIXME_enospc_redirect_count'] 

148 

149 return rd_hits, rd_miss, wr_rdir 

150 

151 @override 

152 def __str__(self) -> str: 

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

154 (self.__class__.__name__, 

155 self.tapdisk.path, self.tapdisk.minor) 

156 

157 

158class CacheFileSR(object): 

159 

160 CACHE_NODE_EXT = '.vhdcache' 

161 

162 def __init__(self, sr_path): 

163 self.sr_path = sr_path 

164 

165 def is_mounted(self): 

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

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

168 

169 class NotAMountPoint(Exception): 

170 

171 def __init__(self, path): 

172 self.path = path 

173 

174 @override 

175 def __str__(self) -> str: 

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

177 

178 @classmethod 

179 def from_uuid(cls, sr_uuid): 

180 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 import util 

193 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 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 

340if __name__ == '__main__': 

341 

342 import sys 

343 from pprint import pprint 

344 

345 args = list(sys.argv) 

346 prog = args.pop(0) 

347 prog = os.path.basename(prog) 

348 

349 

350 def usage(stream): 

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

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

353 else: 

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

355 

356 

357 def usage_error(): 

358 usage(sys.stderr) 

359 sys.exit(1) 

360 

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

362 cmd = 'sr.stats' 

363 else: 

364 try: 

365 cmd = args.pop(0) 

366 except IndexError: 

367 usage_error() 

368 

369 try: 

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

371 except: 

372 usage(sys.stderr) 

373 sys.exit(1) 

374 

375 if _class == 'sr': 

376 try: 

377 uuid = args.pop(0) 

378 except IndexError: 

379 cache_sr = CacheSR.from_cli() 

380 else: 

381 cache_sr = CacheSR.from_uuid(uuid) 

382 

383 if method == 'stats': 

384 

385 d = cache_sr.xapi_stats() 

386 for item in d.items(): 

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

388 

389 elif method == 'topology': 

390 parents = cache_sr.fast_scan_topology() 

391 

392 for parent in parents: 

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

394 (parent.vdi_stats(), parent.vdi_stats_total())) 

395 pprint(parent.stats) 

396 

397 for leaf in parent.leaves: 

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

399 pprint(leaf.stats) 

400 

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

402 

403 else: 

404 usage_error() 

405 else: 

406 usage_error()