Coverage for drivers/lcache.py : 24%
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(), ...
26SECTOR_SHIFT = 9
29class CachingTap(object):
31 def __init__(self, tapdisk, stats):
32 self.tapdisk = tapdisk
33 self.stats = stats
35 @classmethod
36 def from_tapdisk(cls, tapdisk, stats):
37 # pick the last image. if it's a VHD, we got a parent
38 # cache. the leaf case is an aio node sitting on a
39 # parent-caching tapdev. always checking the complementary
40 # case, so we bail on unexpected chains.
42 images = stats['images']
43 image = images[-1]
44 path = image['name']
45 _type = image['driver']['name']
47 def __assert(cond):
48 if not cond: 48 ↛ 49line 48 didn't jump to line 49, because the condition on line 48 was never true
49 raise cls.NotACachingTapdisk(tapdisk, stats)
51 if _type == 'vhd': 51 ↛ 54line 51 didn't jump to line 54, because the condition on line 51 was never true
52 # parent
54 return ParentCachingTap(tapdisk, stats)
56 elif _type == 'aio': 56 ↛ 58line 56 didn't jump to line 58, because the condition on line 56 was never true
57 # leaf
58 st = os.stat(path)
60 __assert(S_ISBLK(st.st_mode))
62 major = os.major(st.st_rdev)
63 minor = os.minor(st.st_rdev)
65 __assert(major == tapdisk.major())
67 return LeafCachingTap(tapdisk, stats, minor)
68 elif _type == 'nbd' and 'run/blktap-control/nbd' in path: 68 ↛ 76line 68 didn't jump to line 76, because the condition on line 68 was never false
69 minor_matcher = re.compile(r'.*run/blktap-control/nbd\d+\.(\d+)$')
70 match = minor_matcher.match(path)
71 __assert(match is not None)
73 parent_minor = int(match.group(1))
74 return LeafCachingTap(tapdisk, stats, parent_minor)
76 __assert(0)
78 class NotACachingTapdisk(Exception):
80 def __init__(self, tapdisk, stats):
81 self.tapdisk = tapdisk
82 self.stats = stats
84 @override
85 def __str__(self) -> str:
86 return \
87 "Tapdisk %s in state '%s' not found caching." % \
88 (self.tapdisk, self.stats)
91class ParentCachingTap(CachingTap):
93 def __init__(self, tapdisk, stats):
94 CachingTap.__init__(self, tapdisk, stats)
95 self.leaves = []
97 def add_leaves(self, tapdisks):
98 for t in tapdisks:
99 if t.is_child_of(self):
100 self.leaves.append(t)
102 def vdi_stats(self):
103 """Parent caching hits/miss count."""
105 images = self.stats['images']
106 total = self.stats['secs'][0]
108 rd_Gc = images[0]['hits'][0]
109 rd_lc = images[1]['hits'][0]
111 rd_hits = rd_Gc
112 rd_miss = total - rd_hits
114 return (rd_hits, rd_miss)
116 def vdi_stats_total(self):
117 """VDI total stats, including leaf hits/miss counts."""
119 rd_hits, rd_miss = self.vdi_stats()
120 wr_rdir = 0
122 for leaf in self.leaves:
123 l_rd_hits, l_rd_miss, l_wr_rdir = leaf.vdi_stats()
124 rd_hits += l_rd_hits
125 rd_miss += l_rd_miss
126 wr_rdir += l_wr_rdir
128 return rd_hits, rd_miss, wr_rdir
130 @override
131 def __str__(self) -> str:
132 return "%s(%s, minor=%s)" % \
133 (self.__class__.__name__,
134 self.tapdisk.path, self.tapdisk.minor)
137class LeafCachingTap(CachingTap):
139 def __init__(self, tapdisk, stats, parent_minor):
140 CachingTap.__init__(self, tapdisk, stats)
141 self.parent_minor = parent_minor
143 def is_child_of(self, parent):
144 return parent.tapdisk.minor == self.parent_minor
146 def vdi_stats(self):
147 images = self.stats['images']
148 total = self.stats['secs'][0]
150 rd_Ac = images[0]['hits'][0]
151 rd_A = images[1]['hits'][0]
153 rd_hits = rd_Ac
154 rd_miss = rd_A
155 wr_rdir = self.stats['FIXME_enospc_redirect_count']
157 return rd_hits, rd_miss, wr_rdir
159 @override
160 def __str__(self) -> str:
161 return "%s(%s, minor=%s)" % \
162 (self.__class__.__name__,
163 self.tapdisk.path, self.tapdisk.minor)
166class CacheFileSR(object):
168 CACHE_NODE_EXT = '.vhdcache'
170 def __init__(self, sr_path):
171 self.sr_path = sr_path
173 def is_mounted(self):
174 # NB. a basic check should do, currently only for CLI usage.
175 return os.path.exists(self.sr_path)
177 class NotAMountPoint(Exception):
179 def __init__(self, path):
180 self.path = path
182 @override
183 def __str__(self) -> str:
184 return "Not a mount point: %s" % self.path
186 @classmethod
187 def from_uuid(cls, sr_uuid):
188 import SR
189 sr_path = "%s/%s" % (SR.MOUNT_BASE, sr_uuid)
191 cache_sr = cls(sr_path)
193 if not cache_sr.is_mounted():
194 raise cls.NotAMountPoint(sr_path)
196 return cache_sr
198 @classmethod
199 def from_session(cls, session):
200 import util
201 import SR as sm
203 host_ref = util.get_localhost_ref(session)
205 _host = session.xenapi.host
206 sr_ref = _host.get_local_cache_sr(host_ref)
207 if not sr_ref:
208 raise util.SMException("Local cache SR not specified")
210 if sr_ref == 'OpaqueRef:NULL':
211 raise util.SMException("Local caching not enabled.")
213 _SR = session.xenapi.SR
214 sr_uuid = _SR.get_uuid(sr_ref)
216 target = sm.SR.from_uuid(session, sr_uuid)
218 return cls(target.path)
220 @classmethod
221 def from_cli(cls):
222 import XenAPI # pylint: disable=import-error
224 session = XenAPI.xapi_local()
225 session.xenapi.login_with_password('root', '', '', 'SM')
227 return cls.from_session(session)
229 def statvfs(self):
230 return os.statvfs(self.sr_path)
232 def _fast_find_nodes(self):
233 pattern = "%s/*%s" % (self.sr_path, self.CACHE_NODE_EXT)
235 found = glob.glob(pattern)
237 return list(found)
239 def xapi_vfs_stats(self):
240 import util
242 f = self.statvfs()
243 if not f.f_frsize:
244 raise util.SMException("Cache FS does not report utilization.")
246 fs_size = f.f_frsize * f.f_blocks
247 fs_free = f.f_frsize * f.f_bfree
249 fs_cache_total = 0
250 for path in self._fast_find_nodes():
251 st = os.stat(path)
252 fs_cache_total += st.st_size
254 return {
255 'FREE_CACHE_SPACE_AVAILABLE':
256 fs_free,
257 'TOTAL_CACHE_UTILISATION':
258 fs_cache_total,
259 'TOTAL_UTILISATION_BY_NON_CACHE_DATA':
260 fs_size - fs_free - fs_cache_total
261 }
263 @classmethod
264 def _fast_find_tapdisks(cls):
265 import errno
266 # NB. we're only about to gather stats here, so take the
267 # fastpath, bypassing agent based VBD[currently-attached] ->
268 # VDI[allow-caching] -> Tap resolution altogether. Instead, we
269 # list all tapdisk and match by path suffix.
271 tapdisks = []
273 for tapdisk in blktap2.Tapdisk.list():
274 try:
275 ext = os.path.splitext(tapdisk.path)[1]
276 except:
277 continue
279 if ext != cls.CACHE_NODE_EXT:
280 continue
282 try:
283 stats = tapdisk.stats()
284 except blktap2.TapCtl.CommandFailure as e:
285 if e.errno != errno.ENOENT:
286 raise
287 continue # shut down
289 caching = CachingTap.from_tapdisk(tapdisk, stats)
290 tapdisks.append(caching)
292 return tapdisks
294 def fast_scan_topology(self):
295 # NB. gather all tapdisks. figure out which ones are leaves
296 # and which ones cache parents.
298 parents = []
299 leaves = []
301 for caching in self._fast_find_tapdisks():
302 if type(caching) == ParentCachingTap:
303 parents.append(caching)
304 else:
305 leaves.append(caching)
307 for parent in parents:
308 parent.add_leaves(leaves)
310 return parents
312 def vdi_stats_total(self):
314 parents = self.fast_scan_topology()
316 rd_hits, rd_miss, wr_rdir = 0, 0, 0
318 for parent in parents:
319 p_rd_hits, p_rd_miss, p_wr_rdir = parent.vdi_stats_total()
320 rd_hits += p_rd_hits
321 rd_miss += p_rd_miss
322 wr_rdir += p_wr_rdir
324 return rd_hits, rd_miss, wr_rdir
326 def xapi_vdi_stats(self):
327 rd_hits, rd_miss, wr_rdir = self.vdi_stats_total()
329 return {
330 'TOTAL_CACHE_HITS':
331 rd_hits << SECTOR_SHIFT,
332 'TOTAL_CACHE_MISSES':
333 rd_miss << SECTOR_SHIFT,
334 'TOTAL_CACHE_ENOSPACE_REDIRECTS':
335 wr_rdir << SECTOR_SHIFT,
336 }
338 def xapi_stats(self):
340 vfs = self.xapi_vfs_stats()
341 vdi = self.xapi_vdi_stats()
343 vfs.update(vdi)
344 return vfs
346CacheSR = CacheFileSR
348if __name__ == '__main__': 348 ↛ 350line 348 didn't jump to line 350, because the condition on line 348 was never true
350 import sys
351 from pprint import pprint
353 args = list(sys.argv)
354 prog = args.pop(0)
355 prog = os.path.basename(prog)
358 def usage(stream):
359 if prog == 'tapdisk-cache-stats':
360 print("usage: tapdisk-cache-stats [<sr-uuid>]", file=stream)
361 else:
362 print("usage: %s sr.{stats|topology} [<sr-uuid>]" % prog, file=stream)
365 def usage_error():
366 usage(sys.stderr)
367 sys.exit(1)
369 if prog == 'tapdisk-cache-stats':
370 cmd = 'sr.stats'
371 else:
372 try:
373 cmd = args.pop(0)
374 except IndexError:
375 usage_error()
377 try:
378 _class, method = cmd.split('.')
379 except:
380 usage(sys.stderr)
381 sys.exit(1)
383 if _class == 'sr':
384 try:
385 uuid = args.pop(0)
386 except IndexError:
387 cache_sr = CacheSR.from_cli()
388 else:
389 cache_sr = CacheSR.from_uuid(uuid)
391 if method == 'stats':
393 d = cache_sr.xapi_stats()
394 for item in d.items():
395 print("%s=%s" % item)
397 elif method == 'topology':
398 parents = cache_sr.fast_scan_topology()
400 for parent in parents:
401 print(parent, "hits/miss=%s total=%s" % \
402 (parent.vdi_stats(), parent.vdi_stats_total()))
403 pprint(parent.stats)
405 for leaf in parent.leaves:
406 print(leaf, "hits/miss=%s" % str(leaf.vdi_stats()))
407 pprint(leaf.stats)
409 print("sr.total=%s" % str(cache_sr.vdi_stats_total()))
411 else:
412 usage_error()
413 else:
414 usage_error()