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(), ...
25from vditype import VdiType
27SECTOR_SHIFT = 9
30class CachingTap(object):
32 def __init__(self, tapdisk, stats):
33 self.tapdisk = tapdisk
34 self.stats = stats
36 @classmethod
37 def from_tapdisk(cls, tapdisk, stats):
38 # pick the last image. if it's a COW, we got a parent
39 # cache. the leaf case is an aio node sitting on a
40 # parent-caching tapdev. always checking the complementary
41 # case, so we bail on unexpected chains.
43 images = stats['images']
44 image = images[-1]
45 path = image['name']
46 _type = image['driver']['name']
48 def __assert(cond):
49 if not cond:
50 raise cls.NotACachingTapdisk(tapdisk, stats)
52 if VdiType.isCowImage(_type):
53 # parent
55 return ParentCachingTap(tapdisk, stats)
57 elif _type == 'aio':
58 # leaf
59 st = os.stat(path)
61 __assert(S_ISBLK(st.st_mode))
63 major = os.major(st.st_rdev)
64 minor = os.minor(st.st_rdev)
66 __assert(major == tapdisk.major())
68 return LeafCachingTap(tapdisk, stats, minor)
70 __assert(0)
72 class NotACachingTapdisk(Exception):
74 def __init__(self, tapdisk, stats):
75 self.tapdisk = tapdisk
76 self.stats = stats
78 @override
79 def __str__(self) -> str:
80 return \
81 "Tapdisk %s in state '%s' not found caching." % \
82 (self.tapdisk, self.stats)
85class ParentCachingTap(CachingTap):
87 def __init__(self, tapdisk, stats):
88 CachingTap.__init__(self, tapdisk, stats)
89 self.leaves = []
91 def add_leaves(self, tapdisks):
92 for t in tapdisks:
93 if t.is_child_of(self):
94 self.leaves.append(t)
96 def vdi_stats(self):
97 """Parent caching hits/miss count."""
99 images = self.stats['images']
100 total = self.stats['secs'][0]
102 rd_Gc = images[0]['hits'][0]
103 rd_lc = images[1]['hits'][0]
105 rd_hits = rd_Gc
106 rd_miss = total - rd_hits
108 return (rd_hits, rd_miss)
110 def vdi_stats_total(self):
111 """VDI total stats, including leaf hits/miss counts."""
113 rd_hits, rd_miss = self.vdi_stats()
114 wr_rdir = 0
116 for leaf in self.leaves:
117 l_rd_hits, l_rd_miss, l_wr_rdir = leaf.vdi_stats()
118 rd_hits += l_rd_hits
119 rd_miss += l_rd_miss
120 wr_rdir += l_wr_rdir
122 return rd_hits, rd_miss, wr_rdir
124 @override
125 def __str__(self) -> str:
126 return "%s(%s, minor=%s)" % \
127 (self.__class__.__name__,
128 self.tapdisk.path, self.tapdisk.minor)
131class LeafCachingTap(CachingTap):
133 def __init__(self, tapdisk, stats, parent_minor):
134 CachingTap.__init__(self, tapdisk, stats)
135 self.parent_minor = parent_minor
137 def is_child_of(self, parent):
138 return parent.tapdisk.minor == self.parent_minor
140 def vdi_stats(self):
141 images = self.stats['images']
142 total = self.stats['secs'][0]
144 rd_Ac = images[0]['hits'][0]
145 rd_A = images[1]['hits'][0]
147 rd_hits = rd_Ac
148 rd_miss = rd_A
149 wr_rdir = self.stats['FIXME_enospc_redirect_count']
151 return rd_hits, rd_miss, wr_rdir
153 @override
154 def __str__(self) -> str:
155 return "%s(%s, minor=%s)" % \
156 (self.__class__.__name__,
157 self.tapdisk.path, self.tapdisk.minor)
160class CacheFileSR(object):
162 CACHE_NODE_EXT = '.vhdcache'
164 def __init__(self, sr_path):
165 self.sr_path = sr_path
167 def is_mounted(self):
168 # NB. a basic check should do, currently only for CLI usage.
169 return os.path.exists(self.sr_path)
171 class NotAMountPoint(Exception):
173 def __init__(self, path):
174 self.path = path
176 @override
177 def __str__(self) -> str:
178 return "Not a mount point: %s" % self.path
180 @classmethod
181 def from_uuid(cls, sr_uuid):
182 import SR
183 sr_path = "%s/%s" % (SR.MOUNT_BASE, sr_uuid)
185 cache_sr = cls(sr_path)
187 if not cache_sr.is_mounted():
188 raise cls.NotAMountPoint(sr_path)
190 return cache_sr
192 @classmethod
193 def from_session(cls, session):
194 import util
195 import SR as sm
197 host_ref = util.get_localhost_ref(session)
199 _host = session.xenapi.host
200 sr_ref = _host.get_local_cache_sr(host_ref)
201 if not sr_ref:
202 raise util.SMException("Local cache SR not specified")
204 if sr_ref == 'OpaqueRef:NULL':
205 raise util.SMException("Local caching not enabled.")
207 _SR = session.xenapi.SR
208 sr_uuid = _SR.get_uuid(sr_ref)
210 target = sm.SR.from_uuid(session, sr_uuid)
212 return cls(target.path)
214 @classmethod
215 def from_cli(cls):
216 import XenAPI # pylint: disable=import-error
218 session = XenAPI.xapi_local()
219 session.xenapi.login_with_password('root', '', '', 'SM')
221 return cls.from_session(session)
223 def statvfs(self):
224 return os.statvfs(self.sr_path)
226 def _fast_find_nodes(self):
227 pattern = "%s/*%s" % (self.sr_path, self.CACHE_NODE_EXT)
229 found = glob.glob(pattern)
231 return list(found)
233 def xapi_vfs_stats(self):
234 import util
236 f = self.statvfs()
237 if not f.f_frsize:
238 raise util.SMException("Cache FS does not report utilization.")
240 fs_size = f.f_frsize * f.f_blocks
241 fs_free = f.f_frsize * f.f_bfree
243 fs_cache_total = 0
244 for path in self._fast_find_nodes():
245 st = os.stat(path)
246 fs_cache_total += st.st_size
248 return {
249 'FREE_CACHE_SPACE_AVAILABLE':
250 fs_free,
251 'TOTAL_CACHE_UTILISATION':
252 fs_cache_total,
253 'TOTAL_UTILISATION_BY_NON_CACHE_DATA':
254 fs_size - fs_free - fs_cache_total
255 }
257 @classmethod
258 def _fast_find_tapdisks(cls):
259 import errno
260 # NB. we're only about to gather stats here, so take the
261 # fastpath, bypassing agent based VBD[currently-attached] ->
262 # VDI[allow-caching] -> Tap resolution altogether. Instead, we
263 # list all tapdisk and match by path suffix.
265 tapdisks = []
267 for tapdisk in blktap2.Tapdisk.list():
268 try:
269 ext = os.path.splitext(tapdisk.path)[1]
270 except:
271 continue
273 if ext != cls.CACHE_NODE_EXT:
274 continue
276 try:
277 stats = tapdisk.stats()
278 except blktap2.TapCtl.CommandFailure as e:
279 if e.errno != errno.ENOENT:
280 raise
281 continue # shut down
283 caching = CachingTap.from_tapdisk(tapdisk, stats)
284 tapdisks.append(caching)
286 return tapdisks
288 def fast_scan_topology(self):
289 # NB. gather all tapdisks. figure out which ones are leaves
290 # and which ones cache parents.
292 parents = []
293 leaves = []
295 for caching in self._fast_find_tapdisks():
296 if type(caching) == ParentCachingTap:
297 parents.append(caching)
298 else:
299 leaves.append(caching)
301 for parent in parents:
302 parent.add_leaves(leaves)
304 return parents
306 def vdi_stats_total(self):
308 parents = self.fast_scan_topology()
310 rd_hits, rd_miss, wr_rdir = 0, 0, 0
312 for parent in parents:
313 p_rd_hits, p_rd_miss, p_wr_rdir = parent.vdi_stats_total()
314 rd_hits += p_rd_hits
315 rd_miss += p_rd_miss
316 wr_rdir += p_wr_rdir
318 return rd_hits, rd_miss, wr_rdir
320 def xapi_vdi_stats(self):
321 rd_hits, rd_miss, wr_rdir = self.vdi_stats_total()
323 return {
324 'TOTAL_CACHE_HITS':
325 rd_hits << SECTOR_SHIFT,
326 'TOTAL_CACHE_MISSES':
327 rd_miss << SECTOR_SHIFT,
328 'TOTAL_CACHE_ENOSPACE_REDIRECTS':
329 wr_rdir << SECTOR_SHIFT,
330 }
332 def xapi_stats(self):
334 vfs = self.xapi_vfs_stats()
335 vdi = self.xapi_vdi_stats()
337 vfs.update(vdi)
338 return vfs
340CacheSR = CacheFileSR
342if __name__ == '__main__':
344 import sys
345 from pprint import pprint
347 args = list(sys.argv)
348 prog = args.pop(0)
349 prog = os.path.basename(prog)
352 def usage(stream):
353 if prog == 'tapdisk-cache-stats':
354 print("usage: tapdisk-cache-stats [<sr-uuid>]", file=stream)
355 else:
356 print("usage: %s sr.{stats|topology} [<sr-uuid>]" % prog, file=stream)
359 def usage_error():
360 usage(sys.stderr)
361 sys.exit(1)
363 if prog == 'tapdisk-cache-stats':
364 cmd = 'sr.stats'
365 else:
366 try:
367 cmd = args.pop(0)
368 except IndexError:
369 usage_error()
371 try:
372 _class, method = cmd.split('.')
373 except:
374 usage(sys.stderr)
375 sys.exit(1)
377 if _class == 'sr':
378 try:
379 uuid = args.pop(0)
380 except IndexError:
381 cache_sr = CacheSR.from_cli()
382 else:
383 cache_sr = CacheSR.from_uuid(uuid)
385 if method == 'stats':
387 d = cache_sr.xapi_stats()
388 for item in d.items():
389 print("%s=%s" % item)
391 elif method == 'topology':
392 parents = cache_sr.fast_scan_topology()
394 for parent in parents:
395 print(parent, "hits/miss=%s total=%s" % \
396 (parent.vdi_stats(), parent.vdi_stats_total()))
397 pprint(parent.stats)
399 for leaf in parent.leaves:
400 print(leaf, "hits/miss=%s" % str(leaf.vdi_stats()))
401 pprint(leaf.stats)
403 print("sr.total=%s" % str(cache_sr.vdi_stats_total()))
405 else:
406 usage_error()
407 else:
408 usage_error()