Coverage for drivers/lcache.py : 0%

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
23from stat import * # S_ISBLK(), ...
25SECTOR_SHIFT = 9
28class CachingTap(object):
30 def __init__(self, tapdisk, stats):
31 self.tapdisk = tapdisk
32 self.stats = stats
34 @classmethod
35 def from_tapdisk(cls, tapdisk, stats):
36 # pick the last image. if it's a VHD, we got a parent
37 # cache. the leaf case is an aio node sitting on a
38 # parent-caching tapdev. always checking the complementary
39 # case, so we bail on unexpected chains.
41 images = stats['images']
42 image = images[-1]
43 path = image['name']
44 _type = image['driver']['name']
46 def __assert(cond):
47 if not cond:
48 raise cls.NotACachingTapdisk(tapdisk, stats)
50 if _type == 'vhd':
51 # parent
53 return ParentCachingTap(tapdisk, stats)
55 elif _type == 'aio':
56 # leaf
57 st = os.stat(path)
59 __assert(S_ISBLK(st.st_mode))
61 major = os.major(st.st_rdev)
62 minor = os.minor(st.st_rdev)
64 __assert(major == tapdisk.major())
66 return LeafCachingTap(tapdisk, stats, minor)
68 __assert(0)
70 class NotACachingTapdisk(Exception):
72 def __init__(self, tapdisk, stats):
73 self.tapdisk = tapdisk
74 self.stats = stats
76 @override
77 def __str__(self) -> str:
78 return \
79 "Tapdisk %s in state '%s' not found caching." % \
80 (self.tapdisk, self.stats)
83class ParentCachingTap(CachingTap):
85 def __init__(self, tapdisk, stats):
86 CachingTap.__init__(self, tapdisk, stats)
87 self.leaves = []
89 def add_leaves(self, tapdisks):
90 for t in tapdisks:
91 if t.is_child_of(self):
92 self.leaves.append(t)
94 def vdi_stats(self):
95 """Parent caching hits/miss count."""
97 images = self.stats['images']
98 total = self.stats['secs'][0]
100 rd_Gc = images[0]['hits'][0]
101 rd_lc = images[1]['hits'][0]
103 rd_hits = rd_Gc
104 rd_miss = total - rd_hits
106 return (rd_hits, rd_miss)
108 def vdi_stats_total(self):
109 """VDI total stats, including leaf hits/miss counts."""
111 rd_hits, rd_miss = self.vdi_stats()
112 wr_rdir = 0
114 for leaf in self.leaves:
115 l_rd_hits, l_rd_miss, l_wr_rdir = leaf.vdi_stats()
116 rd_hits += l_rd_hits
117 rd_miss += l_rd_miss
118 wr_rdir += l_wr_rdir
120 return rd_hits, rd_miss, wr_rdir
122 @override
123 def __str__(self) -> str:
124 return "%s(%s, minor=%s)" % \
125 (self.__class__.__name__,
126 self.tapdisk.path, self.tapdisk.minor)
129class LeafCachingTap(CachingTap):
131 def __init__(self, tapdisk, stats, parent_minor):
132 CachingTap.__init__(self, tapdisk, stats)
133 self.parent_minor = parent_minor
135 def is_child_of(self, parent):
136 return parent.tapdisk.minor == self.parent_minor
138 def vdi_stats(self):
139 images = self.stats['images']
140 total = self.stats['secs'][0]
142 rd_Ac = images[0]['hits'][0]
143 rd_A = images[1]['hits'][0]
145 rd_hits = rd_Ac
146 rd_miss = rd_A
147 wr_rdir = self.stats['FIXME_enospc_redirect_count']
149 return rd_hits, rd_miss, wr_rdir
151 @override
152 def __str__(self) -> str:
153 return "%s(%s, minor=%s)" % \
154 (self.__class__.__name__,
155 self.tapdisk.path, self.tapdisk.minor)
158class CacheFileSR(object):
160 CACHE_NODE_EXT = '.vhdcache'
162 def __init__(self, sr_path):
163 self.sr_path = sr_path
165 def is_mounted(self):
166 # NB. a basic check should do, currently only for CLI usage.
167 return os.path.exists(self.sr_path)
169 class NotAMountPoint(Exception):
171 def __init__(self, path):
172 self.path = path
174 @override
175 def __str__(self) -> str:
176 return "Not a mount point: %s" % self.path
178 @classmethod
179 def from_uuid(cls, sr_uuid):
180 import SR
181 sr_path = "%s/%s" % (SR.MOUNT_BASE, sr_uuid)
183 cache_sr = cls(sr_path)
185 if not cache_sr.is_mounted():
186 raise cls.NotAMountPoint(sr_path)
188 return cache_sr
190 @classmethod
191 def from_session(cls, session):
192 import util
193 import SR as sm
195 host_ref = util.get_localhost_ref(session)
197 _host = session.xenapi.host
198 sr_ref = _host.get_local_cache_sr(host_ref)
199 if not sr_ref:
200 raise util.SMException("Local cache SR not specified")
202 if sr_ref == 'OpaqueRef:NULL':
203 raise util.SMException("Local caching not enabled.")
205 _SR = session.xenapi.SR
206 sr_uuid = _SR.get_uuid(sr_ref)
208 target = sm.SR.from_uuid(session, sr_uuid)
210 return cls(target.path)
212 @classmethod
213 def from_cli(cls):
214 import XenAPI # pylint: disable=import-error
216 session = XenAPI.xapi_local()
217 session.xenapi.login_with_password('root', '', '', 'SM')
219 return cls.from_session(session)
221 def statvfs(self):
222 return os.statvfs(self.sr_path)
224 def _fast_find_nodes(self):
225 pattern = "%s/*%s" % (self.sr_path, self.CACHE_NODE_EXT)
227 found = glob.glob(pattern)
229 return list(found)
231 def xapi_vfs_stats(self):
232 import util
234 f = self.statvfs()
235 if not f.f_frsize:
236 raise util.SMException("Cache FS does not report utilization.")
238 fs_size = f.f_frsize * f.f_blocks
239 fs_free = f.f_frsize * f.f_bfree
241 fs_cache_total = 0
242 for path in self._fast_find_nodes():
243 st = os.stat(path)
244 fs_cache_total += st.st_size
246 return {
247 'FREE_CACHE_SPACE_AVAILABLE':
248 fs_free,
249 'TOTAL_CACHE_UTILISATION':
250 fs_cache_total,
251 'TOTAL_UTILISATION_BY_NON_CACHE_DATA':
252 fs_size - fs_free - fs_cache_total
253 }
255 @classmethod
256 def _fast_find_tapdisks(cls):
257 import errno
258 # NB. we're only about to gather stats here, so take the
259 # fastpath, bypassing agent based VBD[currently-attached] ->
260 # VDI[allow-caching] -> Tap resolution altogether. Instead, we
261 # list all tapdisk and match by path suffix.
263 tapdisks = []
265 for tapdisk in blktap2.Tapdisk.list():
266 try:
267 ext = os.path.splitext(tapdisk.path)[1]
268 except:
269 continue
271 if ext != cls.CACHE_NODE_EXT:
272 continue
274 try:
275 stats = tapdisk.stats()
276 except blktap2.TapCtl.CommandFailure as e:
277 if e.errno != errno.ENOENT:
278 raise
279 continue # shut down
281 caching = CachingTap.from_tapdisk(tapdisk, stats)
282 tapdisks.append(caching)
284 return tapdisks
286 def fast_scan_topology(self):
287 # NB. gather all tapdisks. figure out which ones are leaves
288 # and which ones cache parents.
290 parents = []
291 leaves = []
293 for caching in self._fast_find_tapdisks():
294 if type(caching) == ParentCachingTap:
295 parents.append(caching)
296 else:
297 leaves.append(caching)
299 for parent in parents:
300 parent.add_leaves(leaves)
302 return parents
304 def vdi_stats_total(self):
306 parents = self.fast_scan_topology()
308 rd_hits, rd_miss, wr_rdir = 0, 0, 0
310 for parent in parents:
311 p_rd_hits, p_rd_miss, p_wr_rdir = parent.vdi_stats_total()
312 rd_hits += p_rd_hits
313 rd_miss += p_rd_miss
314 wr_rdir += p_wr_rdir
316 return rd_hits, rd_miss, wr_rdir
318 def xapi_vdi_stats(self):
319 rd_hits, rd_miss, wr_rdir = self.vdi_stats_total()
321 return {
322 'TOTAL_CACHE_HITS':
323 rd_hits << SECTOR_SHIFT,
324 'TOTAL_CACHE_MISSES':
325 rd_miss << SECTOR_SHIFT,
326 'TOTAL_CACHE_ENOSPACE_REDIRECTS':
327 wr_rdir << SECTOR_SHIFT,
328 }
330 def xapi_stats(self):
332 vfs = self.xapi_vfs_stats()
333 vdi = self.xapi_vdi_stats()
335 vfs.update(vdi)
336 return vfs
338CacheSR = CacheFileSR
340if __name__ == '__main__':
342 import sys
343 from pprint import pprint
345 args = list(sys.argv)
346 prog = args.pop(0)
347 prog = os.path.basename(prog)
350 def usage(stream):
351 if prog == 'tapdisk-cache-stats':
352 print("usage: tapdisk-cache-stats [<sr-uuid>]", file=stream)
353 else:
354 print("usage: %s sr.{stats|topology} [<sr-uuid>]" % prog, file=stream)
357 def usage_error():
358 usage(sys.stderr)
359 sys.exit(1)
361 if prog == 'tapdisk-cache-stats':
362 cmd = 'sr.stats'
363 else:
364 try:
365 cmd = args.pop(0)
366 except IndexError:
367 usage_error()
369 try:
370 _class, method = cmd.split('.')
371 except:
372 usage(sys.stderr)
373 sys.exit(1)
375 if _class == 'sr':
376 try:
377 uuid = args.pop(0)
378 except IndexError:
379 cache_sr = CacheSR.from_cli()
380 else:
381 cache_sr = CacheSR.from_uuid(uuid)
383 if method == 'stats':
385 d = cache_sr.xapi_stats()
386 for item in d.items():
387 print("%s=%s" % item)
389 elif method == 'topology':
390 parents = cache_sr.fast_scan_topology()
392 for parent in parents:
393 print(parent, "hits/miss=%s total=%s" % \
394 (parent.vdi_stats(), parent.vdi_stats_total()))
395 pprint(parent.stats)
397 for leaf in parent.leaves:
398 print(leaf, "hits/miss=%s" % str(leaf.vdi_stats()))
399 pprint(leaf.stats)
401 print("sr.total=%s" % str(cache_sr.vdi_stats_total()))
403 else:
404 usage_error()
405 else:
406 usage_error()