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
18import util
19from sm_typing import override
21import os
22import blktap2
23import glob
24import re
25from stat import * # S_ISBLK(), ...
27from vditype import VdiType
29SECTOR_SHIFT = 9
32class CachingTap(object):
34 def __init__(self, tapdisk, stats):
35 self.tapdisk = tapdisk
36 self.stats = stats
38 @classmethod
39 def from_tapdisk(cls, tapdisk, stats):
40 # pick the last image. if it's a COW, we got a parent
41 # cache. the leaf case is an aio node sitting on a
42 # parent-caching tapdev. always checking the complementary
43 # case, so we bail on unexpected chains.
45 images = stats['images']
46 image = images[-1]
47 path = image['name']
48 _type = image['driver']['name']
50 def __assert(cond):
51 if not cond: 51 ↛ 52line 51 didn't jump to line 52, because the condition on line 51 was never true
52 raise cls.NotACachingTapdisk(tapdisk, stats)
54 if VdiType.isCowImage(_type): 54 ↛ 57line 54 didn't jump to line 57, because the condition on line 54 was never true
55 # parent
57 return ParentCachingTap(tapdisk, stats)
59 elif _type == 'aio': 59 ↛ 61line 59 didn't jump to line 61, because the condition on line 59 was never true
60 # leaf
61 st = os.stat(path)
63 __assert(S_ISBLK(st.st_mode))
65 major = os.major(st.st_rdev)
66 minor = os.minor(st.st_rdev)
68 __assert(major == tapdisk.major())
70 return LeafCachingTap(tapdisk, stats, minor)
71 elif _type == 'nbd' and 'run/blktap-control/nbd' in path: 71 ↛ 79line 71 didn't jump to line 79, because the condition on line 71 was never false
72 minor_matcher = re.compile(r'.*run/blktap-control/nbd\d+\.(\d+)$')
73 match = minor_matcher.match(path)
74 __assert(match is not None)
76 parent_minor = int(match.group(1))
77 return LeafCachingTap(tapdisk, stats, parent_minor)
79 __assert(0)
81 class NotACachingTapdisk(Exception):
83 def __init__(self, tapdisk, stats):
84 self.tapdisk = tapdisk
85 self.stats = stats
87 @override
88 def __str__(self) -> str:
89 return \
90 "Tapdisk %s in state '%s' not found caching." % \
91 (self.tapdisk, self.stats)
94class ParentCachingTap(CachingTap):
96 def __init__(self, tapdisk, stats):
97 CachingTap.__init__(self, tapdisk, stats)
98 self.leaves = []
100 def add_leaves(self, tapdisks):
101 for t in tapdisks:
102 if t.is_child_of(self):
103 self.leaves.append(t)
105 def vdi_stats(self):
106 """Parent caching hits/miss count."""
108 images = self.stats['images']
109 total = self.stats['secs'][0]
111 rd_Gc = images[0]['hits'][0]
112 rd_lc = images[1]['hits'][0]
114 rd_hits = rd_Gc
115 rd_miss = total - rd_hits
117 return (rd_hits, rd_miss)
119 def vdi_stats_total(self):
120 """VDI total stats, including leaf hits/miss counts."""
122 rd_hits, rd_miss = self.vdi_stats()
123 wr_rdir = 0
125 for leaf in self.leaves:
126 l_rd_hits, l_rd_miss, l_wr_rdir = leaf.vdi_stats()
127 rd_hits += l_rd_hits
128 rd_miss += l_rd_miss
129 wr_rdir += l_wr_rdir
131 return rd_hits, rd_miss, wr_rdir
133 @override
134 def __str__(self) -> str:
135 return "%s(%s, minor=%s)" % \
136 (self.__class__.__name__,
137 self.tapdisk.path, self.tapdisk.minor)
140class LeafCachingTap(CachingTap):
142 def __init__(self, tapdisk, stats, parent_minor):
143 CachingTap.__init__(self, tapdisk, stats)
144 self.parent_minor = parent_minor
146 def is_child_of(self, parent):
147 return parent.tapdisk.minor == self.parent_minor
149 def vdi_stats(self):
150 images = self.stats['images']
151 total = self.stats['secs'][0]
153 rd_Ac = images[0]['hits'][0]
154 rd_A = images[1]['hits'][0]
156 rd_hits = rd_Ac
157 rd_miss = rd_A
158 wr_rdir = self.stats['FIXME_enospc_redirect_count']
160 return rd_hits, rd_miss, wr_rdir
162 @override
163 def __str__(self) -> str:
164 return "%s(%s, minor=%s)" % \
165 (self.__class__.__name__,
166 self.tapdisk.path, self.tapdisk.minor)
169class CacheFileSR(object):
171 CACHE_NODE_EXT = '.vhdcache'
173 def __init__(self, sr_path):
174 self.sr_path = sr_path
176 def is_mounted(self):
177 # NB. a basic check should do, currently only for CLI usage.
178 return os.path.exists(self.sr_path)
180 class NotAMountPoint(Exception):
182 def __init__(self, path):
183 self.path = path
185 @override
186 def __str__(self) -> str:
187 return "Not a mount point: %s" % self.path
189 @classmethod
190 def from_uuid(cls, sr_uuid):
191 import SR
192 sr_path = "%s/%s" % (SR.MOUNT_BASE, sr_uuid)
194 cache_sr = cls(sr_path)
196 if not cache_sr.is_mounted():
197 raise cls.NotAMountPoint(sr_path)
199 return cache_sr
201 @classmethod
202 def from_session(cls, session):
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 with util.ApiSession("SM-local-cache") as session:
225 return cls.from_session(session)
227 def statvfs(self):
228 return os.statvfs(self.sr_path)
230 def _fast_find_nodes(self):
231 pattern = "%s/*%s" % (self.sr_path, self.CACHE_NODE_EXT)
233 found = glob.glob(pattern)
235 return list(found)
237 def xapi_vfs_stats(self):
238 f = self.statvfs()
239 if not f.f_frsize:
240 raise util.SMException("Cache FS does not report utilization.")
242 fs_size = f.f_frsize * f.f_blocks
243 fs_free = f.f_frsize * f.f_bfree
245 fs_cache_total = 0
246 for path in self._fast_find_nodes():
247 st = os.stat(path)
248 fs_cache_total += st.st_size
250 return {
251 'FREE_CACHE_SPACE_AVAILABLE':
252 fs_free,
253 'TOTAL_CACHE_UTILISATION':
254 fs_cache_total,
255 'TOTAL_UTILISATION_BY_NON_CACHE_DATA':
256 fs_size - fs_free - fs_cache_total
257 }
259 @classmethod
260 def _fast_find_tapdisks(cls):
261 import errno
262 # NB. we're only about to gather stats here, so take the
263 # fastpath, bypassing agent based VBD[currently-attached] ->
264 # VDI[allow-caching] -> Tap resolution altogether. Instead, we
265 # list all tapdisk and match by path suffix.
267 tapdisks = []
269 for tapdisk in blktap2.Tapdisk.list():
270 try:
271 ext = os.path.splitext(tapdisk.path)[1]
272 except:
273 continue
275 if ext != cls.CACHE_NODE_EXT:
276 continue
278 try:
279 stats = tapdisk.stats()
280 except blktap2.TapCtl.CommandFailure as e:
281 if e.errno != errno.ENOENT:
282 raise
283 continue # shut down
285 caching = CachingTap.from_tapdisk(tapdisk, stats)
286 tapdisks.append(caching)
288 return tapdisks
290 def fast_scan_topology(self):
291 # NB. gather all tapdisks. figure out which ones are leaves
292 # and which ones cache parents.
294 parents = []
295 leaves = []
297 for caching in self._fast_find_tapdisks():
298 if type(caching) == ParentCachingTap:
299 parents.append(caching)
300 else:
301 leaves.append(caching)
303 for parent in parents:
304 parent.add_leaves(leaves)
306 return parents
308 def vdi_stats_total(self):
310 parents = self.fast_scan_topology()
312 rd_hits, rd_miss, wr_rdir = 0, 0, 0
314 for parent in parents:
315 p_rd_hits, p_rd_miss, p_wr_rdir = parent.vdi_stats_total()
316 rd_hits += p_rd_hits
317 rd_miss += p_rd_miss
318 wr_rdir += p_wr_rdir
320 return rd_hits, rd_miss, wr_rdir
322 def xapi_vdi_stats(self):
323 rd_hits, rd_miss, wr_rdir = self.vdi_stats_total()
325 return {
326 'TOTAL_CACHE_HITS':
327 rd_hits << SECTOR_SHIFT,
328 'TOTAL_CACHE_MISSES':
329 rd_miss << SECTOR_SHIFT,
330 'TOTAL_CACHE_ENOSPACE_REDIRECTS':
331 wr_rdir << SECTOR_SHIFT,
332 }
334 def xapi_stats(self):
336 vfs = self.xapi_vfs_stats()
337 vdi = self.xapi_vdi_stats()
339 vfs.update(vdi)
340 return vfs
342CacheSR = CacheFileSR
344if __name__ == '__main__': 344 ↛ 346line 344 didn't jump to line 346, because the condition on line 344 was never true
346 import sys
347 from pprint import pprint
349 args = list(sys.argv)
350 prog = args.pop(0)
351 prog = os.path.basename(prog)
354 def usage(stream):
355 if prog == 'tapdisk-cache-stats':
356 print("usage: tapdisk-cache-stats [<sr-uuid>]", file=stream)
357 else:
358 print("usage: %s sr.{stats|topology} [<sr-uuid>]" % prog, file=stream)
361 def usage_error():
362 usage(sys.stderr)
363 sys.exit(1)
365 if prog == 'tapdisk-cache-stats':
366 cmd = 'sr.stats'
367 else:
368 try:
369 cmd = args.pop(0)
370 except IndexError:
371 usage_error()
373 try:
374 _class, method = cmd.split('.')
375 except:
376 usage(sys.stderr)
377 sys.exit(1)
379 if _class == 'sr':
380 try:
381 uuid = args.pop(0)
382 except IndexError:
383 cache_sr = CacheSR.from_cli()
384 else:
385 cache_sr = CacheSR.from_uuid(uuid)
387 if method == 'stats':
389 d = cache_sr.xapi_stats()
390 for item in d.items():
391 print("%s=%s" % item)
393 elif method == 'topology':
394 parents = cache_sr.fast_scan_topology()
396 for parent in parents:
397 print(parent, "hits/miss=%s total=%s" % \
398 (parent.vdi_stats(), parent.vdi_stats_total()))
399 pprint(parent.stats)
401 for leaf in parent.leaves:
402 print(leaf, "hits/miss=%s" % str(leaf.vdi_stats()))
403 pprint(leaf.stats)
405 print("sr.total=%s" % str(cache_sr.vdi_stats_total()))
407 else:
408 usage_error()
409 else:
410 usage_error()