123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- #!/usr/bin/env python
- # ex:ts=4:sw=4:sts=4:et
- # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
- #
- # Copyright (C) 2012 Robert Yang
- #
- # SPDX-License-Identifier: GPL-2.0-only
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License version 2 as
- # published by the Free Software Foundation.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License along
- # with this program; if not, write to the Free Software Foundation, Inc.,
- # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- import os, logging, re, sys
- import bb
- logger = logging.getLogger("BitBake.Monitor")
- def printErr(info):
- logger.error("%s\n Disk space monitor will NOT be enabled" % info)
- def convertGMK(unit):
- """ Convert the space unit G, M, K, the unit is case-insensitive """
- unitG = re.match(r'([1-9][0-9]*)[gG]\s?$', unit)
- if unitG:
- return int(unitG.group(1)) * (1024 ** 3)
- unitM = re.match(r'([1-9][0-9]*)[mM]\s?$', unit)
- if unitM:
- return int(unitM.group(1)) * (1024 ** 2)
- unitK = re.match(r'([1-9][0-9]*)[kK]\s?$', unit)
- if unitK:
- return int(unitK.group(1)) * 1024
- unitN = re.match(r'([1-9][0-9]*)\s?$', unit)
- if unitN:
- return int(unitN.group(1))
- else:
- return None
- def getMountedDev(path):
- """ Get the device mounted at the path, uses /proc/mounts """
- # Get the mount point of the filesystem containing path
- # st_dev is the ID of device containing file
- parentDev = os.stat(path).st_dev
- currentDev = parentDev
- # When the current directory's device is different from the
- # parent's, then the current directory is a mount point
- while parentDev == currentDev:
- mountPoint = path
- # Use dirname to get the parent's directory
- path = os.path.dirname(path)
- # Reach the "/"
- if path == mountPoint:
- break
- parentDev= os.stat(path).st_dev
- try:
- with open("/proc/mounts", "r") as ifp:
- for line in ifp:
- procLines = line.rstrip('\n').split()
- if procLines[1] == mountPoint:
- return procLines[0]
- except EnvironmentError:
- pass
- return None
- def getDiskData(BBDirs, configuration):
- """Prepare disk data for disk space monitor"""
- # Save the device IDs, need the ID to be unique (the dictionary's key is
- # unique), so that when more than one directory is located on the same
- # device, we just monitor it once
- devDict = {}
- for pathSpaceInode in BBDirs.split():
- # The input format is: "dir,space,inode", dir is a must, space
- # and inode are optional
- pathSpaceInodeRe = re.match(r'([^,]*),([^,]*),([^,]*),?(.*)', pathSpaceInode)
- if not pathSpaceInodeRe:
- printErr("Invalid value in BB_DISKMON_DIRS: %s" % pathSpaceInode)
- return None
- action = pathSpaceInodeRe.group(1)
- if action not in ("ABORT", "STOPTASKS", "WARN"):
- printErr("Unknown disk space monitor action: %s" % action)
- return None
- path = os.path.realpath(pathSpaceInodeRe.group(2))
- if not path:
- printErr("Invalid path value in BB_DISKMON_DIRS: %s" % pathSpaceInode)
- return None
- # The disk space or inode is optional, but it should have a correct
- # value once it is specified
- minSpace = pathSpaceInodeRe.group(3)
- if minSpace:
- minSpace = convertGMK(minSpace)
- if not minSpace:
- printErr("Invalid disk space value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(3))
- return None
- else:
- # None means that it is not specified
- minSpace = None
- minInode = pathSpaceInodeRe.group(4)
- if minInode:
- minInode = convertGMK(minInode)
- if not minInode:
- printErr("Invalid inode value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(4))
- return None
- else:
- # None means that it is not specified
- minInode = None
- if minSpace is None and minInode is None:
- printErr("No disk space or inode value in found BB_DISKMON_DIRS: %s" % pathSpaceInode)
- return None
- # mkdir for the directory since it may not exist, for example the
- # DL_DIR may not exist at the very beginning
- if not os.path.exists(path):
- bb.utils.mkdirhier(path)
- dev = getMountedDev(path)
- # Use path/action as the key
- devDict[(path, action)] = [dev, minSpace, minInode]
- return devDict
- def getInterval(configuration):
- """ Get the disk space interval """
- # The default value is 50M and 5K.
- spaceDefault = 50 * 1024 * 1024
- inodeDefault = 5 * 1024
- interval = configuration.getVar("BB_DISKMON_WARNINTERVAL")
- if not interval:
- return spaceDefault, inodeDefault
- else:
- # The disk space or inode interval is optional, but it should
- # have a correct value once it is specified
- intervalRe = re.match(r'([^,]*),?\s*(.*)', interval)
- if intervalRe:
- intervalSpace = intervalRe.group(1)
- if intervalSpace:
- intervalSpace = convertGMK(intervalSpace)
- if not intervalSpace:
- printErr("Invalid disk space interval value in BB_DISKMON_WARNINTERVAL: %s" % intervalRe.group(1))
- return None, None
- else:
- intervalSpace = spaceDefault
- intervalInode = intervalRe.group(2)
- if intervalInode:
- intervalInode = convertGMK(intervalInode)
- if not intervalInode:
- printErr("Invalid disk inode interval value in BB_DISKMON_WARNINTERVAL: %s" % intervalRe.group(2))
- return None, None
- else:
- intervalInode = inodeDefault
- return intervalSpace, intervalInode
- else:
- printErr("Invalid interval value in BB_DISKMON_WARNINTERVAL: %s" % interval)
- return None, None
- class diskMonitor:
- """Prepare the disk space monitor data"""
- def __init__(self, configuration):
- self.enableMonitor = False
- self.configuration = configuration
- BBDirs = configuration.getVar("BB_DISKMON_DIRS") or None
- if BBDirs:
- self.devDict = getDiskData(BBDirs, configuration)
- if self.devDict:
- self.spaceInterval, self.inodeInterval = getInterval(configuration)
- if self.spaceInterval and self.inodeInterval:
- self.enableMonitor = True
- # These are for saving the previous disk free space and inode, we
- # use them to avoid printing too many warning messages
- self.preFreeS = {}
- self.preFreeI = {}
- # This is for STOPTASKS and ABORT, to avoid printing the message
- # repeatedly while waiting for the tasks to finish
- self.checked = {}
- for k in self.devDict:
- self.preFreeS[k] = 0
- self.preFreeI[k] = 0
- self.checked[k] = False
- if self.spaceInterval is None and self.inodeInterval is None:
- self.enableMonitor = False
- def check(self, rq):
- """ Take action for the monitor """
- if self.enableMonitor:
- diskUsage = {}
- for k, attributes in self.devDict.items():
- path, action = k
- dev, minSpace, minInode = attributes
- st = os.statvfs(path)
- # The available free space, integer number
- freeSpace = st.f_bavail * st.f_frsize
- # Send all relevant information in the event.
- freeSpaceRoot = st.f_bfree * st.f_frsize
- totalSpace = st.f_blocks * st.f_frsize
- diskUsage[dev] = bb.event.DiskUsageSample(freeSpace, freeSpaceRoot, totalSpace)
- if minSpace and freeSpace < minSpace:
- # Always show warning, the self.checked would always be False if the action is WARN
- if self.preFreeS[k] == 0 or self.preFreeS[k] - freeSpace > self.spaceInterval and not self.checked[k]:
- logger.warning("The free space of %s (%s) is running low (%.3fGB left)" % \
- (path, dev, freeSpace / 1024 / 1024 / 1024.0))
- self.preFreeS[k] = freeSpace
- if action == "STOPTASKS" and not self.checked[k]:
- logger.error("No new tasks can be executed since the disk space monitor action is \"STOPTASKS\"!")
- self.checked[k] = True
- rq.finish_runqueue(False)
- bb.event.fire(bb.event.DiskFull(dev, 'disk', freeSpace, path), self.configuration)
- elif action == "ABORT" and not self.checked[k]:
- logger.error("Immediately abort since the disk space monitor action is \"ABORT\"!")
- self.checked[k] = True
- rq.finish_runqueue(True)
- bb.event.fire(bb.event.DiskFull(dev, 'disk', freeSpace, path), self.configuration)
- # The free inodes, integer number
- freeInode = st.f_favail
- if minInode and freeInode < minInode:
- # Some filesystems use dynamic inodes so can't run out
- # (e.g. btrfs). This is reported by the inode count being 0.
- if st.f_files == 0:
- self.devDict[k][2] = None
- continue
- # Always show warning, the self.checked would always be False if the action is WARN
- if self.preFreeI[k] == 0 or self.preFreeI[k] - freeInode > self.inodeInterval and not self.checked[k]:
- logger.warning("The free inode of %s (%s) is running low (%.3fK left)" % \
- (path, dev, freeInode / 1024.0))
- self.preFreeI[k] = freeInode
- if action == "STOPTASKS" and not self.checked[k]:
- logger.error("No new tasks can be executed since the disk space monitor action is \"STOPTASKS\"!")
- self.checked[k] = True
- rq.finish_runqueue(False)
- bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration)
- elif action == "ABORT" and not self.checked[k]:
- logger.error("Immediately abort since the disk space monitor action is \"ABORT\"!")
- self.checked[k] = True
- rq.finish_runqueue(True)
- bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration)
- bb.event.fire(bb.event.MonitorDiskEvent(diskUsage), self.configuration)
- return
|