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 os 

19import blktap2 

20import glob 

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

22 

23SECTOR_SHIFT = 9 

24 

25 

26class CachingTap(object): 

27 

28 def __init__(self, tapdisk, stats): 

29 self.tapdisk = tapdisk 

30 self.stats = stats 

31 

32 @classmethod 

33 def from_tapdisk(cls, tapdisk, stats): 

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

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

36 # parent-caching tapdev. always checking the complementary 

37 # case, so we bail on unexpected chains. 

38 

39 images = stats['images'] 

40 image = images[-1] 

41 path = image['name'] 

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

43 

44 def __assert(cond): 

45 if not cond: 

46 raise cls.NotACachingTapdisk(tapdisk, stats) 

47 

48 if _type == 'vhd': 

49 # parent 

50 

51 return ParentCachingTap(tapdisk, stats) 

52 

53 elif _type == 'aio': 

54 # leaf 

55 st = os.stat(path) 

56 

57 __assert(S_ISBLK(st.st_mode)) 

58 

59 major = os.major(st.st_rdev) 

60 minor = os.minor(st.st_rdev) 

61 

62 __assert(major == tapdisk.major()) 

63 

64 return LeafCachingTap(tapdisk, stats, minor) 

65 

66 __assert(0) 

67 

68 class NotACachingTapdisk(Exception): 

69 

70 def __init__(self, tapdisk, stats): 

71 self.tapdisk = tapdisk 

72 self.stats = stats 

73 

74 def __str__(self): 

75 return \ 

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

77 (self.tapdisk, self.stats) 

78 

79 

80class ParentCachingTap(CachingTap): 

81 

82 def __init__(self, tapdisk, stats): 

83 CachingTap.__init__(self, tapdisk, stats) 

84 self.leaves = [] 

85 

86 def add_leaves(self, tapdisks): 

87 for t in tapdisks: 

88 if t.is_child_of(self): 

89 self.leaves.append(t) 

90 

91 def vdi_stats(self): 

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

93 

94 images = self.stats['images'] 

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

96 

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

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

99 

100 rd_hits = rd_Gc 

101 rd_miss = total - rd_hits 

102 

103 return (rd_hits, rd_miss) 

104 

105 def vdi_stats_total(self): 

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

107 

108 rd_hits, rd_miss = self.vdi_stats() 

109 wr_rdir = 0 

110 

111 for leaf in self.leaves: 

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

113 rd_hits += l_rd_hits 

114 rd_miss += l_rd_miss 

115 wr_rdir += l_wr_rdir 

116 

117 return rd_hits, rd_miss, wr_rdir 

118 

119 def __str__(self): 

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

121 (self.__class__.__name__, 

122 self.tapdisk.path, self.tapdisk.minor) 

123 

124 

125class LeafCachingTap(CachingTap): 

126 

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

128 CachingTap.__init__(self, tapdisk, stats) 

129 self.parent_minor = parent_minor 

130 

131 def is_child_of(self, parent): 

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

133 

134 def vdi_stats(self): 

135 images = self.stats['images'] 

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

137 

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

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

140 

141 rd_hits = rd_Ac 

142 rd_miss = rd_A 

143 wr_rdir = self.stats['FIXME_enospc_redirect_count'] 

144 

145 return rd_hits, rd_miss, wr_rdir 

146 

147 def __str__(self): 

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

149 (self.__class__.__name__, 

150 self.tapdisk.path, self.tapdisk.minor) 

151 

152 

153class CacheFileSR(object): 

154 

155 CACHE_NODE_EXT = '.vhdcache' 

156 

157 def __init__(self, sr_path): 

158 self.sr_path = sr_path 

159 

160 def is_mounted(self): 

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

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

163 

164 class NotAMountPoint(Exception): 

165 

166 def __init__(self, path): 

167 self.path = path 

168 

169 def __str__(self): 

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

171 

172 @classmethod 

173 def from_uuid(cls, sr_uuid): 

174 import SR 

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

176 

177 cache_sr = cls(sr_path) 

178 

179 if not cache_sr.is_mounted(): 

180 raise cls.NotAMountPoint(sr_path) 

181 

182 return cache_sr 

183 

184 @classmethod 

185 def from_session(cls, session): 

186 import util 

187 import SR as sm 

188 

189 host_ref = util.get_localhost_ref(session) 

190 

191 _host = session.xenapi.host 

192 sr_ref = _host.get_local_cache_sr(host_ref) 

193 if not sr_ref: 

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

195 

196 if sr_ref == 'OpaqueRef:NULL': 

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

198 

199 _SR = session.xenapi.SR 

200 sr_uuid = _SR.get_uuid(sr_ref) 

201 

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

203 

204 return cls(target.path) 

205 

206 @classmethod 

207 def from_cli(cls): 

208 import XenAPI # pylint: disable=import-error 

209 

210 session = XenAPI.xapi_local() 

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

212 

213 return cls.from_session(session) 

214 

215 def statvfs(self): 

216 return os.statvfs(self.sr_path) 

217 

218 def _fast_find_nodes(self): 

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

220 

221 found = glob.glob(pattern) 

222 

223 return list(found) 

224 

225 def xapi_vfs_stats(self): 

226 import util 

227 

228 f = self.statvfs() 

229 if not f.f_frsize: 

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

231 

232 fs_size = f.f_frsize * f.f_blocks 

233 fs_free = f.f_frsize * f.f_bfree 

234 

235 fs_cache_total = 0 

236 for path in self._fast_find_nodes(): 

237 st = os.stat(path) 

238 fs_cache_total += st.st_size 

239 

240 return { 

241 'FREE_CACHE_SPACE_AVAILABLE': 

242 fs_free, 

243 'TOTAL_CACHE_UTILISATION': 

244 fs_cache_total, 

245 'TOTAL_UTILISATION_BY_NON_CACHE_DATA': 

246 fs_size - fs_free - fs_cache_total 

247 } 

248 

249 @classmethod 

250 def _fast_find_tapdisks(cls): 

251 import errno 

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

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

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

255 # list all tapdisk and match by path suffix. 

256 

257 tapdisks = [] 

258 

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

260 try: 

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

262 except: 

263 continue 

264 

265 if ext != cls.CACHE_NODE_EXT: 

266 continue 

267 

268 try: 

269 stats = tapdisk.stats() 

270 except blktap2.TapCtl.CommandFailure as e: 

271 if e.errno != errno.ENOENT: 

272 raise 

273 continue # shut down 

274 

275 caching = CachingTap.from_tapdisk(tapdisk, stats) 

276 tapdisks.append(caching) 

277 

278 return tapdisks 

279 

280 def fast_scan_topology(self): 

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

282 # and which ones cache parents. 

283 

284 parents = [] 

285 leaves = [] 

286 

287 for caching in self._fast_find_tapdisks(): 

288 if type(caching) == ParentCachingTap: 

289 parents.append(caching) 

290 else: 

291 leaves.append(caching) 

292 

293 for parent in parents: 

294 parent.add_leaves(leaves) 

295 

296 return parents 

297 

298 def vdi_stats_total(self): 

299 

300 parents = self.fast_scan_topology() 

301 

302 rd_hits, rd_miss, wr_rdir = 0, 0, 0 

303 

304 for parent in parents: 

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

306 rd_hits += p_rd_hits 

307 rd_miss += p_rd_miss 

308 wr_rdir += p_wr_rdir 

309 

310 return rd_hits, rd_miss, wr_rdir 

311 

312 def xapi_vdi_stats(self): 

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

314 

315 return { 

316 'TOTAL_CACHE_HITS': 

317 rd_hits << SECTOR_SHIFT, 

318 'TOTAL_CACHE_MISSES': 

319 rd_miss << SECTOR_SHIFT, 

320 'TOTAL_CACHE_ENOSPACE_REDIRECTS': 

321 wr_rdir << SECTOR_SHIFT, 

322 } 

323 

324 def xapi_stats(self): 

325 

326 vfs = self.xapi_vfs_stats() 

327 vdi = self.xapi_vdi_stats() 

328 

329 vfs.update(vdi) 

330 return vfs 

331 

332CacheSR = CacheFileSR 

333 

334if __name__ == '__main__': 

335 

336 import sys 

337 from pprint import pprint 

338 

339 args = list(sys.argv) 

340 prog = args.pop(0) 

341 prog = os.path.basename(prog) 

342 

343 

344 def usage(stream): 

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

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

347 else: 

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

349 

350 

351 def usage_error(): 

352 usage(sys.stderr) 

353 sys.exit(1) 

354 

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

356 cmd = 'sr.stats' 

357 else: 

358 try: 

359 cmd = args.pop(0) 

360 except IndexError: 

361 usage_error() 

362 

363 try: 

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

365 except: 

366 usage(sys.stderr) 

367 sys.exit(1) 

368 

369 if _class == 'sr': 

370 try: 

371 uuid = args.pop(0) 

372 except IndexError: 

373 cache_sr = CacheSR.from_cli() 

374 else: 

375 cache_sr = CacheSR.from_uuid(uuid) 

376 

377 if method == 'stats': 

378 

379 d = cache_sr.xapi_stats() 

380 for item in d.items(): 

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

382 

383 elif method == 'topology': 

384 parents = cache_sr.fast_scan_topology() 

385 

386 for parent in parents: 

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

388 (parent.vdi_stats(), parent.vdi_stats_total())) 

389 pprint(parent.stats) 

390 

391 for leaf in parent.leaves: 

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

393 pprint(leaf.stats) 

394 

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

396 

397 else: 

398 usage_error() 

399 else: 

400 usage_error()