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 

25from vditype import VdiType 

26 

27SECTOR_SHIFT = 9 

28 

29 

30class CachingTap(object): 

31 

32 def __init__(self, tapdisk, stats): 

33 self.tapdisk = tapdisk 

34 self.stats = stats 

35 

36 @classmethod 

37 def from_tapdisk(cls, tapdisk, stats): 

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

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

40 # parent-caching tapdev. always checking the complementary 

41 # case, so we bail on unexpected chains. 

42 

43 images = stats['images'] 

44 image = images[-1] 

45 path = image['name'] 

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

47 

48 def __assert(cond): 

49 if not cond: 

50 raise cls.NotACachingTapdisk(tapdisk, stats) 

51 

52 if VdiType.isCowImage(_type): 

53 # parent 

54 

55 return ParentCachingTap(tapdisk, stats) 

56 

57 elif _type == 'aio': 

58 # leaf 

59 st = os.stat(path) 

60 

61 __assert(S_ISBLK(st.st_mode)) 

62 

63 major = os.major(st.st_rdev) 

64 minor = os.minor(st.st_rdev) 

65 

66 __assert(major == tapdisk.major()) 

67 

68 return LeafCachingTap(tapdisk, stats, minor) 

69 

70 __assert(0) 

71 

72 class NotACachingTapdisk(Exception): 

73 

74 def __init__(self, tapdisk, stats): 

75 self.tapdisk = tapdisk 

76 self.stats = stats 

77 

78 @override 

79 def __str__(self) -> str: 

80 return \ 

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

82 (self.tapdisk, self.stats) 

83 

84 

85class ParentCachingTap(CachingTap): 

86 

87 def __init__(self, tapdisk, stats): 

88 CachingTap.__init__(self, tapdisk, stats) 

89 self.leaves = [] 

90 

91 def add_leaves(self, tapdisks): 

92 for t in tapdisks: 

93 if t.is_child_of(self): 

94 self.leaves.append(t) 

95 

96 def vdi_stats(self): 

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

98 

99 images = self.stats['images'] 

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

101 

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

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

104 

105 rd_hits = rd_Gc 

106 rd_miss = total - rd_hits 

107 

108 return (rd_hits, rd_miss) 

109 

110 def vdi_stats_total(self): 

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

112 

113 rd_hits, rd_miss = self.vdi_stats() 

114 wr_rdir = 0 

115 

116 for leaf in self.leaves: 

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

118 rd_hits += l_rd_hits 

119 rd_miss += l_rd_miss 

120 wr_rdir += l_wr_rdir 

121 

122 return rd_hits, rd_miss, wr_rdir 

123 

124 @override 

125 def __str__(self) -> str: 

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 @override 

154 def __str__(self) -> str: 

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

156 (self.__class__.__name__, 

157 self.tapdisk.path, self.tapdisk.minor) 

158 

159 

160class CacheFileSR(object): 

161 

162 CACHE_NODE_EXT = '.vhdcache' 

163 

164 def __init__(self, sr_path): 

165 self.sr_path = sr_path 

166 

167 def is_mounted(self): 

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

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

170 

171 class NotAMountPoint(Exception): 

172 

173 def __init__(self, path): 

174 self.path = path 

175 

176 @override 

177 def __str__(self) -> str: 

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

179 

180 @classmethod 

181 def from_uuid(cls, sr_uuid): 

182 import SR 

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

184 

185 cache_sr = cls(sr_path) 

186 

187 if not cache_sr.is_mounted(): 

188 raise cls.NotAMountPoint(sr_path) 

189 

190 return cache_sr 

191 

192 @classmethod 

193 def from_session(cls, session): 

194 import util 

195 import SR as sm 

196 

197 host_ref = util.get_localhost_ref(session) 

198 

199 _host = session.xenapi.host 

200 sr_ref = _host.get_local_cache_sr(host_ref) 

201 if not sr_ref: 

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

203 

204 if sr_ref == 'OpaqueRef:NULL': 

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

206 

207 _SR = session.xenapi.SR 

208 sr_uuid = _SR.get_uuid(sr_ref) 

209 

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

211 

212 return cls(target.path) 

213 

214 @classmethod 

215 def from_cli(cls): 

216 import XenAPI # pylint: disable=import-error 

217 

218 session = XenAPI.xapi_local() 

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

220 

221 return cls.from_session(session) 

222 

223 def statvfs(self): 

224 return os.statvfs(self.sr_path) 

225 

226 def _fast_find_nodes(self): 

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

228 

229 found = glob.glob(pattern) 

230 

231 return list(found) 

232 

233 def xapi_vfs_stats(self): 

234 import util 

235 

236 f = self.statvfs() 

237 if not f.f_frsize: 

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

239 

240 fs_size = f.f_frsize * f.f_blocks 

241 fs_free = f.f_frsize * f.f_bfree 

242 

243 fs_cache_total = 0 

244 for path in self._fast_find_nodes(): 

245 st = os.stat(path) 

246 fs_cache_total += st.st_size 

247 

248 return { 

249 'FREE_CACHE_SPACE_AVAILABLE': 

250 fs_free, 

251 'TOTAL_CACHE_UTILISATION': 

252 fs_cache_total, 

253 'TOTAL_UTILISATION_BY_NON_CACHE_DATA': 

254 fs_size - fs_free - fs_cache_total 

255 } 

256 

257 @classmethod 

258 def _fast_find_tapdisks(cls): 

259 import errno 

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

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

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

263 # list all tapdisk and match by path suffix. 

264 

265 tapdisks = [] 

266 

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

268 try: 

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

270 except: 

271 continue 

272 

273 if ext != cls.CACHE_NODE_EXT: 

274 continue 

275 

276 try: 

277 stats = tapdisk.stats() 

278 except blktap2.TapCtl.CommandFailure as e: 

279 if e.errno != errno.ENOENT: 

280 raise 

281 continue # shut down 

282 

283 caching = CachingTap.from_tapdisk(tapdisk, stats) 

284 tapdisks.append(caching) 

285 

286 return tapdisks 

287 

288 def fast_scan_topology(self): 

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

290 # and which ones cache parents. 

291 

292 parents = [] 

293 leaves = [] 

294 

295 for caching in self._fast_find_tapdisks(): 

296 if type(caching) == ParentCachingTap: 

297 parents.append(caching) 

298 else: 

299 leaves.append(caching) 

300 

301 for parent in parents: 

302 parent.add_leaves(leaves) 

303 

304 return parents 

305 

306 def vdi_stats_total(self): 

307 

308 parents = self.fast_scan_topology() 

309 

310 rd_hits, rd_miss, wr_rdir = 0, 0, 0 

311 

312 for parent in parents: 

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

314 rd_hits += p_rd_hits 

315 rd_miss += p_rd_miss 

316 wr_rdir += p_wr_rdir 

317 

318 return rd_hits, rd_miss, wr_rdir 

319 

320 def xapi_vdi_stats(self): 

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

322 

323 return { 

324 'TOTAL_CACHE_HITS': 

325 rd_hits << SECTOR_SHIFT, 

326 'TOTAL_CACHE_MISSES': 

327 rd_miss << SECTOR_SHIFT, 

328 'TOTAL_CACHE_ENOSPACE_REDIRECTS': 

329 wr_rdir << SECTOR_SHIFT, 

330 } 

331 

332 def xapi_stats(self): 

333 

334 vfs = self.xapi_vfs_stats() 

335 vdi = self.xapi_vdi_stats() 

336 

337 vfs.update(vdi) 

338 return vfs 

339 

340CacheSR = CacheFileSR 

341 

342if __name__ == '__main__': 

343 

344 import sys 

345 from pprint import pprint 

346 

347 args = list(sys.argv) 

348 prog = args.pop(0) 

349 prog = os.path.basename(prog) 

350 

351 

352 def usage(stream): 

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

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

355 else: 

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

357 

358 

359 def usage_error(): 

360 usage(sys.stderr) 

361 sys.exit(1) 

362 

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

364 cmd = 'sr.stats' 

365 else: 

366 try: 

367 cmd = args.pop(0) 

368 except IndexError: 

369 usage_error() 

370 

371 try: 

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

373 except: 

374 usage(sys.stderr) 

375 sys.exit(1) 

376 

377 if _class == 'sr': 

378 try: 

379 uuid = args.pop(0) 

380 except IndexError: 

381 cache_sr = CacheSR.from_cli() 

382 else: 

383 cache_sr = CacheSR.from_uuid(uuid) 

384 

385 if method == 'stats': 

386 

387 d = cache_sr.xapi_stats() 

388 for item in d.items(): 

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

390 

391 elif method == 'topology': 

392 parents = cache_sr.fast_scan_topology() 

393 

394 for parent in parents: 

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

396 (parent.vdi_stats(), parent.vdi_stats_total())) 

397 pprint(parent.stats) 

398 

399 for leaf in parent.leaves: 

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

401 pprint(leaf.stats) 

402 

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

404 

405 else: 

406 usage_error() 

407 else: 

408 usage_error()