Coverage for drivers/lcache.py : 25%
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
18from sm_typing import override
20import os
21import blktap2
22import glob
23import re
24from stat import * # S_ISBLK(), ...
26from vditype import VdiType
28SECTOR_SHIFT = 9
31class CachingTap(object):
33 def __init__(self, tapdisk, stats):
34 self.tapdisk = tapdisk
35 self.stats = stats
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.
44 images = stats['images']
45 image = images[-1]
46 path = image['name']
47 _type = image['driver']['name']
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)
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
56 return ParentCachingTap(tapdisk, stats)
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)
62 __assert(S_ISBLK(st.st_mode))
64 major = os.major(st.st_rdev)
65 minor = os.minor(st.st_rdev)
67 __assert(major == tapdisk.major())
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)
75 parent_minor = int(match.group(1))
76 return LeafCachingTap(tapdisk, stats, parent_minor)
78 __assert(0)
80 class NotACachingTapdisk(Exception):
82 def __init__(self, tapdisk, stats):
83 self.tapdisk = tapdisk
84 self.stats = stats
86 @override
87 def __str__(self) -> str:
88 return \
89 "Tapdisk %s in state '%s' not found caching." % \
90 (self.tapdisk, self.stats)
93class ParentCachingTap(CachingTap):
95 def __init__(self, tapdisk, stats):
96 CachingTap.__init__(self, tapdisk, stats)
97 self.leaves = []
99 def add_leaves(self, tapdisks):
100 for t in tapdisks:
101 if t.is_child_of(self):
102 self.leaves.append(t)
104 def vdi_stats(self):
105 """Parent caching hits/miss count."""
107 images = self.stats['images']
108 total = self.stats['secs'][0]
110 rd_Gc = images[0]['hits'][0]
111 rd_lc = images[1]['hits'][0]
113 rd_hits = rd_Gc
114 rd_miss = total - rd_hits
116 return (rd_hits, rd_miss)
118 def vdi_stats_total(self):
119 """VDI total stats, including leaf hits/miss counts."""
121 rd_hits, rd_miss = self.vdi_stats()
122 wr_rdir = 0
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
130 return rd_hits, rd_miss, wr_rdir
132 @override
133 def __str__(self) -> str:
134 return "%s(%s, minor=%s)" % \
135 (self.__class__.__name__,
136 self.tapdisk.path, self.tapdisk.minor)
139class LeafCachingTap(CachingTap):
141 def __init__(self, tapdisk, stats, parent_minor):
142 CachingTap.__init__(self, tapdisk, stats)
143 self.parent_minor = parent_minor
145 def is_child_of(self, parent):
146 return parent.tapdisk.minor == self.parent_minor
148 def vdi_stats(self):
149 images = self.stats['images']
150 total = self.stats['secs'][0]
152 rd_Ac = images[0]['hits'][0]
153 rd_A = images[1]['hits'][0]
155 rd_hits = rd_Ac
156 rd_miss = rd_A
157 wr_rdir = self.stats['FIXME_enospc_redirect_count']
159 return rd_hits, rd_miss, wr_rdir
161 @override
162 def __str__(self) -> str:
163 return "%s(%s, minor=%s)" % \
164 (self.__class__.__name__,
165 self.tapdisk.path, self.tapdisk.minor)
168class CacheFileSR(object):
170 CACHE_NODE_EXT = '.vhdcache'
172 def __init__(self, sr_path):
173 self.sr_path = sr_path
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)
179 class NotAMountPoint(Exception):
181 def __init__(self, path):
182 self.path = path
184 @override
185 def __str__(self) -> str:
186 return "Not a mount point: %s" % self.path
188 @classmethod
189 def from_uuid(cls, sr_uuid):
190 import SR
191 sr_path = "%s/%s" % (SR.MOUNT_BASE, sr_uuid)
193 cache_sr = cls(sr_path)
195 if not cache_sr.is_mounted():
196 raise cls.NotAMountPoint(sr_path)
198 return cache_sr
200 @classmethod
201 def from_session(cls, session):
202 import util
203 import SR as sm
205 host_ref = util.get_localhost_ref(session)
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")
212 if sr_ref == 'OpaqueRef:NULL':
213 raise util.SMException("Local caching not enabled.")
215 _SR = session.xenapi.SR
216 sr_uuid = _SR.get_uuid(sr_ref)
218 target = sm.SR.from_uuid(session, sr_uuid)
220 return cls(target.path)
222 @classmethod
223 def from_cli(cls):
224 import XenAPI # pylint: disable=import-error
226 session = XenAPI.xapi_local()
227 session.xenapi.login_with_password('root', '', '', 'SM')
229 return cls.from_session(session)
231 def statvfs(self):
232 return os.statvfs(self.sr_path)
234 def _fast_find_nodes(self):
235 pattern = "%s/*%s" % (self.sr_path, self.CACHE_NODE_EXT)
237 found = glob.glob(pattern)
239 return list(found)
241 def xapi_vfs_stats(self):
242 import util
244 f = self.statvfs()
245 if not f.f_frsize:
246 raise util.SMException("Cache FS does not report utilization.")
248 fs_size = f.f_frsize * f.f_blocks
249 fs_free = f.f_frsize * f.f_bfree
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
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 }
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.
273 tapdisks = []
275 for tapdisk in blktap2.Tapdisk.list():
276 try:
277 ext = os.path.splitext(tapdisk.path)[1]
278 except:
279 continue
281 if ext != cls.CACHE_NODE_EXT:
282 continue
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
291 caching = CachingTap.from_tapdisk(tapdisk, stats)
292 tapdisks.append(caching)
294 return tapdisks
296 def fast_scan_topology(self):
297 # NB. gather all tapdisks. figure out which ones are leaves
298 # and which ones cache parents.
300 parents = []
301 leaves = []
303 for caching in self._fast_find_tapdisks():
304 if type(caching) == ParentCachingTap:
305 parents.append(caching)
306 else:
307 leaves.append(caching)
309 for parent in parents:
310 parent.add_leaves(leaves)
312 return parents
314 def vdi_stats_total(self):
316 parents = self.fast_scan_topology()
318 rd_hits, rd_miss, wr_rdir = 0, 0, 0
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
326 return rd_hits, rd_miss, wr_rdir
328 def xapi_vdi_stats(self):
329 rd_hits, rd_miss, wr_rdir = self.vdi_stats_total()
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 }
340 def xapi_stats(self):
342 vfs = self.xapi_vfs_stats()
343 vdi = self.xapi_vdi_stats()
345 vfs.update(vdi)
346 return vfs
348CacheSR = CacheFileSR
350if __name__ == '__main__': 350 ↛ 352line 350 didn't jump to line 352, because the condition on line 350 was never true
352 import sys
353 from pprint import pprint
355 args = list(sys.argv)
356 prog = args.pop(0)
357 prog = os.path.basename(prog)
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)
367 def usage_error():
368 usage(sys.stderr)
369 sys.exit(1)
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()
379 try:
380 _class, method = cmd.split('.')
381 except:
382 usage(sys.stderr)
383 sys.exit(1)
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)
393 if method == 'stats':
395 d = cache_sr.xapi_stats()
396 for item in d.items():
397 print("%s=%s" % item)
399 elif method == 'topology':
400 parents = cache_sr.fast_scan_topology()
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)
407 for leaf in parent.leaves:
408 print(leaf, "hits/miss=%s" % str(leaf.vdi_stats()))
409 pprint(leaf.stats)
411 print("sr.total=%s" % str(cache_sr.vdi_stats_total()))
413 else:
414 usage_error()
415 else:
416 usage_error()