From patchwork Mon Feb 20 08:01:12 2012 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: [bitbake-devel,1/2] V3 Disk space monitoring Date: Mon, 20 Feb 2012 08:01:12 -0000 From: Robert Yang X-Patchwork-Id: 21435 Message-Id: <98f5d9f3d6f9070ad88ce4869b5632802c539d85.1329724458.git.liezhi.yang@windriver.com> To: Monitor disk availability and take action when the free disk space or amount of free inode is running low, it is enabled when BB_DISKMON_DIRS is set. * Variable meanings(from meta-yocto/conf/local.conf.sample): # Set the directories to monitor for disk usage, if more than one # directories are mounted in the same device, then only one directory # would be monitored since the monitor is based on the device. # The format are: "directory,minimum space,minimum free inode", # either space or inode (Or both of them) should be specified, otherwise # the monitor would not be enabled, the unit can be G, M, K or none, # but do NOT use GB, MB or KB (B is not needed). #BB_DISKMON_DIRS = "${TMPDIR},1G,1K ${SSTATE_DIR},500M,1K" # # Set the action when the space is running low, the action is one of: # ABORT: Immediately abort # NO_NEW_TASK: no new tasks # WARN: show warnings # The default is WARN #BB_DISKMON_ACTION = "WARN" # # Set disk space and inode interval, the unit can be G, M, or K, but do # NOT use the GB, MB or KB (B is not needed), the format is: # "disk space interval, disk inode interval", the default value is # "10M, 50" which means that it would warn when the free space is # lower than the minimum space(or inode), and would repeat the action # when the disk space reduces 10M (or the amount of inode reduces 50) # again. #BB_DISKMON_INTERVAL = "10M,10K" [YOCTO #1589] Signed-off-by: Robert Yang --- bitbake/bin/bitbake | 1 + bitbake/lib/bb/monitordisk.py | 177 +++++++++++++++++++++++++++++++++++++++++ bitbake/lib/bb/runqueue.py | 65 ++++++++++++++- 3 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 bitbake/lib/bb/monitordisk.py diff --git a/bitbake/bin/bitbake b/bitbake/bin/bitbake index 6da4980..d295229 100755 --- a/bitbake/bin/bitbake +++ b/bitbake/bin/bitbake @@ -240,6 +240,7 @@ Default BBFILES are the .bb files in the current directory.""") else: print("server address: %s, server port: %s" % (server.serverinfo.host, server.serverinfo.port)) + return 1 if __name__ == "__main__": diff --git a/bitbake/lib/bb/monitordisk.py b/bitbake/lib/bb/monitordisk.py new file mode 100644 index 0000000..2ea7bdb --- /dev/null +++ b/bitbake/lib/bb/monitordisk.py @@ -0,0 +1,177 @@ +#!/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 +# +# 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 time, os, logging, re, sys +import bb +logger = logging.getLogger("BitBake.Monitor") + +def errRet(info): + logger.error("%s" % info) + logger.error("Disk space monitor will NOT be enabled") + return None + +def errRetTwo(info): + logger.error("%s" % info) + logger.error("Disk space monitor will NOT be enabled") + return None, None + +def convertGMK(unit): + + """ Convert the space unit G, M, K, the unit is case-insensitive """ + + unitG = re.match('([1-9][0-9]*)[gG]\s?$', unit) + if unitG: + return int(unitG.group(1)) * (1024 ** 3) + unitM = re.match('([1-9][0-9]*)[mM]\s?$', unit) + if unitM: + return int(unitM.group(1)) * (1024 ** 2) + unitK = re.match('([1-9][0-9]*)[kK]\s?$', unit) + if unitK: + return int(unitK.group(1)) * 1024 + unitN = re.match('([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 + # parrent's, then the current directory is a mount point + while parentDev == currentDev: + mountPoint = path + # Use dirname to get the parrent'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 directories are located in 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('([^,]*),([^,]*),?(.*)', pathSpaceInode) + if not pathSpaceInodeRe: + errRet("Invalid value in BB_DISKMON_DIRS: %s" % pathSpaceInode) + path = os.path.realpath(pathSpaceInodeRe.group(1)) + if not path: + errRet("Invalid path value in BB_DISKMON_DIRS: %s" % pathSpaceInode) + minSpace = pathSpaceInodeRe.group(2) + # The disk space or inode is optional, but it should have a correct + # value once it is specified + if minSpace: + minSpace = convertGMK(minSpace) + if not minSpace: + errRet("Invalid disk space value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(2)) + else: + # 0 means that it is not specified + minSpace = None + + minInode = pathSpaceInodeRe.group(3) + if minInode: + minInode = convertGMK(minInode) + if not minInode: + errRet("Invalid inode value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(3)) + else: + # 0 means that it is not specified + minInode = None + + if minSpace is None and minInode is None: + errRet("No disk space or inode value in found BB_DISKMON_DIRS: %s" % pathSpaceInode) + # 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) + mountedDev = getMountedDev(path) + devDict[mountedDev] = path, minSpace, minInode + return devDict + +def getAction(configuration): + + """ Get the disk space monitor action""" + + action = configuration.getVar("BB_DISKMON_ACTION", 1) or "WARN" + if action not in ("ABORT", "NO_NEW_TASK", "WARN"): + errRet("Unknown disk space monitor action: %s" % self.action) + else: + return action + +def getInterval(configuration): + + """ Get the disk space interval """ + interval = configuration.getVar("BB_DISKMON_INTERVAL", 1) + if not interval: + # The default value is 10M and 50. + return 10 * 1024 * 1024, 50 + else: + # The disk space or inode is optional, but it should have a + # correct value once it is specified + intervalRe = re.match('([^,]*),?\s*(.*)', interval) + if intervalRe: + intervalSpace = intervalRe.group(1) + if intervalSpace: + intervalSpace = convertGMK(intervalSpace) + if not intervalSpace: + errRetTwo("Invalid disk space interval value in BB_DISKMON_INTERVAL: %s" % intervalRe.group(1)) + intervalInode = intervalRe.group(2) + if intervalInode: + intervalInode = convertGMK(intervalInode) + if not intervalInode: + errRetTwo("Invalid disk inode interval value in BB_DISKMON_INTERVAL: %s" % intervalRe.group(2)) + return intervalSpace, intervalInode + else: + errRetTwo("Invalid interval value in BB_DISKMON_INTERVAL: %s" % interval) + +class diskMonitor: + + """Prepare the disk space monitor data""" + + def __init__(self, BBDirs, configuration): + self.enableMonitor = True + self.devDict = getDiskData(BBDirs, configuration) + self.action = getAction(configuration) + self.spaceInterval, self.inodeInterval = getInterval(configuration) + if self.devDict is None or self.action is None: + self.enableMonitor = False + if self.spaceInterval is None and self.inodeInterval is None: + self.enableMonitor = False diff --git a/bitbake/lib/bb/runqueue.py b/bitbake/lib/bb/runqueue.py index 7bf4320..e185f15 100644 --- a/bitbake/lib/bb/runqueue.py +++ b/bitbake/lib/bb/runqueue.py @@ -31,6 +31,7 @@ import fcntl import logging import bb from bb import msg, data, event +from bb import monitordisk bblogger = logging.getLogger("BitBake") logger = logging.getLogger("BitBake.RunQueue") @@ -771,6 +772,16 @@ class RunQueue: self.state = runQueuePrepare + # For disk space monitor + self.diskMonDirs = cfgData.getVar("BB_DISKMON_DIRS", 1) or None + if self.diskMonDirs: + self.dm = monitordisk.diskMonitor(self.diskMonDirs, cfgData) or None + self.preFreeSpace = {} + self.preFreeInode = {} + for dev in self.dm.devDict: + self.preFreeSpace[dev] = 0 + self.preFreeInode[dev] = 0 + def check_stamps(self): unchecked = {} current = [] @@ -923,6 +934,40 @@ class RunQueue: return iscurrent + def disk_monitor_action (self, devDict): + + """ Tack action for the monitor """ + + for dev in devDict: + st = os.statvfs(devDict[dev][0]) + # The free space, float point number + freeSpace = st.f_bavail * st.f_frsize + if devDict[dev][1] is not None and freeSpace < devDict[dev][1]: + # Always show warning, and this is the default "WARN" action + if self.preFreeSpace[dev] == 0 or self.preFreeSpace[dev] - freeSpace > self.dm.spaceInterval: + logger.warn("The free space of %s is running low (%.3fGB left)" % (dev, freeSpace / 1024 / 1024 / 1024.0)) + self.preFreeSpace[dev] = freeSpace + if self.dm.action == "NO_NEW_TASK": + logger.warn("No new tasks can be excuted since BB_DISKMON_ACTION = \"NO_NEW_TASK\"!") + return 1 + elif self.dm.action == "ABORT": + logger.error("Immediately abort since BB_DISKMON_ACTION = \"ABORT\"!") + sys.exit(1) + # The free inodes, float point number + freeInode = st.f_favail + if devDict[dev][2] is not None and freeInode < devDict[dev][2]: + # Always show warning, and this is the default "WARN" action + if self.preFreeInode[dev] == 0 or self.preFreeInode[dev] - freeInode > self.dm.inodeInterval: + logger.warn("The free inode of %s is running low (%.3fK left)" % (dev, freeInode / 1024.0)) + self.preFreeInode[dev] = freeInode + elif self.dm.action == "NO_NEW_TASK": + logger.warn("No new tasks can be excuted since BB_DISKMON_ACTION = \"NO_NEW_TASK\"!") + return 1 + elif self.dm.action == "ABORT": + logger.error("Immediately abort since BB_DISKMON_ACTION = \"ABORT\"!") + sys.exit(1) + return 0 + def execute_runqueue(self): """ Run the tasks in a queue prepared by rqdata.prepare() @@ -946,7 +991,14 @@ class RunQueue: self.rqexe = RunQueueExecuteScenequeue(self) if self.state is runQueueSceneRun: - retval = self.rqexe.execute() + if self.dm.enableMonitor: + dm_action = self.disk_monitor_action(self.dm.devDict) + if dm_action == 1: + self.rqexe.finish() + else: + retval = self.rqexe.execute() + else: + retval = self.rqexe.execute() if self.state is runQueueRunInit: logger.info("Executing RunQueue Tasks") @@ -954,10 +1006,17 @@ class RunQueue: self.state = runQueueRunning if self.state is runQueueRunning: - retval = self.rqexe.execute() + if self.dm.enableMonitor: + dm_action = self.disk_monitor_action(self.dm.devDict) + if dm_action == 1: + self.rqexe.finish() + else: + retval = self.rqexe.execute() + else: + retval = self.rqexe.execute() if self.state is runQueueCleanUp: - self.rqexe.finish() + self.rqexe.finish() if self.state is runQueueComplete or self.state is runQueueFailed: if self.rqexe.stats.failed: