1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# Copyright (C) 2012 Robert Yang 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun 7*4882a593Smuzhiyunimport os, logging, re 8*4882a593Smuzhiyunimport bb 9*4882a593Smuzhiyunlogger = logging.getLogger("BitBake.Monitor") 10*4882a593Smuzhiyun 11*4882a593Smuzhiyundef printErr(info): 12*4882a593Smuzhiyun logger.error("%s\n Disk space monitor will NOT be enabled" % info) 13*4882a593Smuzhiyun 14*4882a593Smuzhiyundef convertGMK(unit): 15*4882a593Smuzhiyun 16*4882a593Smuzhiyun """ Convert the space unit G, M, K, the unit is case-insensitive """ 17*4882a593Smuzhiyun 18*4882a593Smuzhiyun unitG = re.match(r'([1-9][0-9]*)[gG]\s?$', unit) 19*4882a593Smuzhiyun if unitG: 20*4882a593Smuzhiyun return int(unitG.group(1)) * (1024 ** 3) 21*4882a593Smuzhiyun unitM = re.match(r'([1-9][0-9]*)[mM]\s?$', unit) 22*4882a593Smuzhiyun if unitM: 23*4882a593Smuzhiyun return int(unitM.group(1)) * (1024 ** 2) 24*4882a593Smuzhiyun unitK = re.match(r'([1-9][0-9]*)[kK]\s?$', unit) 25*4882a593Smuzhiyun if unitK: 26*4882a593Smuzhiyun return int(unitK.group(1)) * 1024 27*4882a593Smuzhiyun unitN = re.match(r'([1-9][0-9]*)\s?$', unit) 28*4882a593Smuzhiyun if unitN: 29*4882a593Smuzhiyun return int(unitN.group(1)) 30*4882a593Smuzhiyun else: 31*4882a593Smuzhiyun return None 32*4882a593Smuzhiyun 33*4882a593Smuzhiyundef getMountedDev(path): 34*4882a593Smuzhiyun 35*4882a593Smuzhiyun """ Get the device mounted at the path, uses /proc/mounts """ 36*4882a593Smuzhiyun 37*4882a593Smuzhiyun # Get the mount point of the filesystem containing path 38*4882a593Smuzhiyun # st_dev is the ID of device containing file 39*4882a593Smuzhiyun parentDev = os.stat(path).st_dev 40*4882a593Smuzhiyun currentDev = parentDev 41*4882a593Smuzhiyun # When the current directory's device is different from the 42*4882a593Smuzhiyun # parent's, then the current directory is a mount point 43*4882a593Smuzhiyun while parentDev == currentDev: 44*4882a593Smuzhiyun mountPoint = path 45*4882a593Smuzhiyun # Use dirname to get the parent's directory 46*4882a593Smuzhiyun path = os.path.dirname(path) 47*4882a593Smuzhiyun # Reach the "/" 48*4882a593Smuzhiyun if path == mountPoint: 49*4882a593Smuzhiyun break 50*4882a593Smuzhiyun parentDev= os.stat(path).st_dev 51*4882a593Smuzhiyun 52*4882a593Smuzhiyun try: 53*4882a593Smuzhiyun with open("/proc/mounts", "r") as ifp: 54*4882a593Smuzhiyun for line in ifp: 55*4882a593Smuzhiyun procLines = line.rstrip('\n').split() 56*4882a593Smuzhiyun if procLines[1] == mountPoint: 57*4882a593Smuzhiyun return procLines[0] 58*4882a593Smuzhiyun except EnvironmentError: 59*4882a593Smuzhiyun pass 60*4882a593Smuzhiyun return None 61*4882a593Smuzhiyun 62*4882a593Smuzhiyundef getDiskData(BBDirs): 63*4882a593Smuzhiyun 64*4882a593Smuzhiyun """Prepare disk data for disk space monitor""" 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun # Save the device IDs, need the ID to be unique (the dictionary's key is 67*4882a593Smuzhiyun # unique), so that when more than one directory is located on the same 68*4882a593Smuzhiyun # device, we just monitor it once 69*4882a593Smuzhiyun devDict = {} 70*4882a593Smuzhiyun for pathSpaceInode in BBDirs.split(): 71*4882a593Smuzhiyun # The input format is: "dir,space,inode", dir is a must, space 72*4882a593Smuzhiyun # and inode are optional 73*4882a593Smuzhiyun pathSpaceInodeRe = re.match(r'([^,]*),([^,]*),([^,]*),?(.*)', pathSpaceInode) 74*4882a593Smuzhiyun if not pathSpaceInodeRe: 75*4882a593Smuzhiyun printErr("Invalid value in BB_DISKMON_DIRS: %s" % pathSpaceInode) 76*4882a593Smuzhiyun return None 77*4882a593Smuzhiyun 78*4882a593Smuzhiyun action = pathSpaceInodeRe.group(1) 79*4882a593Smuzhiyun if action == "ABORT": 80*4882a593Smuzhiyun # Emit a deprecation warning 81*4882a593Smuzhiyun logger.warnonce("The BB_DISKMON_DIRS \"ABORT\" action has been renamed to \"HALT\", update configuration") 82*4882a593Smuzhiyun action = "HALT" 83*4882a593Smuzhiyun 84*4882a593Smuzhiyun if action not in ("HALT", "STOPTASKS", "WARN"): 85*4882a593Smuzhiyun printErr("Unknown disk space monitor action: %s" % action) 86*4882a593Smuzhiyun return None 87*4882a593Smuzhiyun 88*4882a593Smuzhiyun path = os.path.realpath(pathSpaceInodeRe.group(2)) 89*4882a593Smuzhiyun if not path: 90*4882a593Smuzhiyun printErr("Invalid path value in BB_DISKMON_DIRS: %s" % pathSpaceInode) 91*4882a593Smuzhiyun return None 92*4882a593Smuzhiyun 93*4882a593Smuzhiyun # The disk space or inode is optional, but it should have a correct 94*4882a593Smuzhiyun # value once it is specified 95*4882a593Smuzhiyun minSpace = pathSpaceInodeRe.group(3) 96*4882a593Smuzhiyun if minSpace: 97*4882a593Smuzhiyun minSpace = convertGMK(minSpace) 98*4882a593Smuzhiyun if not minSpace: 99*4882a593Smuzhiyun printErr("Invalid disk space value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(3)) 100*4882a593Smuzhiyun return None 101*4882a593Smuzhiyun else: 102*4882a593Smuzhiyun # None means that it is not specified 103*4882a593Smuzhiyun minSpace = None 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun minInode = pathSpaceInodeRe.group(4) 106*4882a593Smuzhiyun if minInode: 107*4882a593Smuzhiyun minInode = convertGMK(minInode) 108*4882a593Smuzhiyun if not minInode: 109*4882a593Smuzhiyun printErr("Invalid inode value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(4)) 110*4882a593Smuzhiyun return None 111*4882a593Smuzhiyun else: 112*4882a593Smuzhiyun # None means that it is not specified 113*4882a593Smuzhiyun minInode = None 114*4882a593Smuzhiyun 115*4882a593Smuzhiyun if minSpace is None and minInode is None: 116*4882a593Smuzhiyun printErr("No disk space or inode value in found BB_DISKMON_DIRS: %s" % pathSpaceInode) 117*4882a593Smuzhiyun return None 118*4882a593Smuzhiyun # mkdir for the directory since it may not exist, for example the 119*4882a593Smuzhiyun # DL_DIR may not exist at the very beginning 120*4882a593Smuzhiyun if not os.path.exists(path): 121*4882a593Smuzhiyun bb.utils.mkdirhier(path) 122*4882a593Smuzhiyun dev = getMountedDev(path) 123*4882a593Smuzhiyun # Use path/action as the key 124*4882a593Smuzhiyun devDict[(path, action)] = [dev, minSpace, minInode] 125*4882a593Smuzhiyun 126*4882a593Smuzhiyun return devDict 127*4882a593Smuzhiyun 128*4882a593Smuzhiyundef getInterval(configuration): 129*4882a593Smuzhiyun 130*4882a593Smuzhiyun """ Get the disk space interval """ 131*4882a593Smuzhiyun 132*4882a593Smuzhiyun # The default value is 50M and 5K. 133*4882a593Smuzhiyun spaceDefault = 50 * 1024 * 1024 134*4882a593Smuzhiyun inodeDefault = 5 * 1024 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun interval = configuration.getVar("BB_DISKMON_WARNINTERVAL") 137*4882a593Smuzhiyun if not interval: 138*4882a593Smuzhiyun return spaceDefault, inodeDefault 139*4882a593Smuzhiyun else: 140*4882a593Smuzhiyun # The disk space or inode interval is optional, but it should 141*4882a593Smuzhiyun # have a correct value once it is specified 142*4882a593Smuzhiyun intervalRe = re.match(r'([^,]*),?\s*(.*)', interval) 143*4882a593Smuzhiyun if intervalRe: 144*4882a593Smuzhiyun intervalSpace = intervalRe.group(1) 145*4882a593Smuzhiyun if intervalSpace: 146*4882a593Smuzhiyun intervalSpace = convertGMK(intervalSpace) 147*4882a593Smuzhiyun if not intervalSpace: 148*4882a593Smuzhiyun printErr("Invalid disk space interval value in BB_DISKMON_WARNINTERVAL: %s" % intervalRe.group(1)) 149*4882a593Smuzhiyun return None, None 150*4882a593Smuzhiyun else: 151*4882a593Smuzhiyun intervalSpace = spaceDefault 152*4882a593Smuzhiyun intervalInode = intervalRe.group(2) 153*4882a593Smuzhiyun if intervalInode: 154*4882a593Smuzhiyun intervalInode = convertGMK(intervalInode) 155*4882a593Smuzhiyun if not intervalInode: 156*4882a593Smuzhiyun printErr("Invalid disk inode interval value in BB_DISKMON_WARNINTERVAL: %s" % intervalRe.group(2)) 157*4882a593Smuzhiyun return None, None 158*4882a593Smuzhiyun else: 159*4882a593Smuzhiyun intervalInode = inodeDefault 160*4882a593Smuzhiyun return intervalSpace, intervalInode 161*4882a593Smuzhiyun else: 162*4882a593Smuzhiyun printErr("Invalid interval value in BB_DISKMON_WARNINTERVAL: %s" % interval) 163*4882a593Smuzhiyun return None, None 164*4882a593Smuzhiyun 165*4882a593Smuzhiyunclass diskMonitor: 166*4882a593Smuzhiyun 167*4882a593Smuzhiyun """Prepare the disk space monitor data""" 168*4882a593Smuzhiyun 169*4882a593Smuzhiyun def __init__(self, configuration): 170*4882a593Smuzhiyun 171*4882a593Smuzhiyun self.enableMonitor = False 172*4882a593Smuzhiyun self.configuration = configuration 173*4882a593Smuzhiyun 174*4882a593Smuzhiyun BBDirs = configuration.getVar("BB_DISKMON_DIRS") or None 175*4882a593Smuzhiyun if BBDirs: 176*4882a593Smuzhiyun self.devDict = getDiskData(BBDirs) 177*4882a593Smuzhiyun if self.devDict: 178*4882a593Smuzhiyun self.spaceInterval, self.inodeInterval = getInterval(configuration) 179*4882a593Smuzhiyun if self.spaceInterval and self.inodeInterval: 180*4882a593Smuzhiyun self.enableMonitor = True 181*4882a593Smuzhiyun # These are for saving the previous disk free space and inode, we 182*4882a593Smuzhiyun # use them to avoid printing too many warning messages 183*4882a593Smuzhiyun self.preFreeS = {} 184*4882a593Smuzhiyun self.preFreeI = {} 185*4882a593Smuzhiyun # This is for STOPTASKS and HALT, to avoid printing the message 186*4882a593Smuzhiyun # repeatedly while waiting for the tasks to finish 187*4882a593Smuzhiyun self.checked = {} 188*4882a593Smuzhiyun for k in self.devDict: 189*4882a593Smuzhiyun self.preFreeS[k] = 0 190*4882a593Smuzhiyun self.preFreeI[k] = 0 191*4882a593Smuzhiyun self.checked[k] = False 192*4882a593Smuzhiyun if self.spaceInterval is None and self.inodeInterval is None: 193*4882a593Smuzhiyun self.enableMonitor = False 194*4882a593Smuzhiyun 195*4882a593Smuzhiyun def check(self, rq): 196*4882a593Smuzhiyun 197*4882a593Smuzhiyun """ Take action for the monitor """ 198*4882a593Smuzhiyun 199*4882a593Smuzhiyun if self.enableMonitor: 200*4882a593Smuzhiyun diskUsage = {} 201*4882a593Smuzhiyun for k, attributes in self.devDict.items(): 202*4882a593Smuzhiyun path, action = k 203*4882a593Smuzhiyun dev, minSpace, minInode = attributes 204*4882a593Smuzhiyun 205*4882a593Smuzhiyun st = os.statvfs(path) 206*4882a593Smuzhiyun 207*4882a593Smuzhiyun # The available free space, integer number 208*4882a593Smuzhiyun freeSpace = st.f_bavail * st.f_frsize 209*4882a593Smuzhiyun 210*4882a593Smuzhiyun # Send all relevant information in the event. 211*4882a593Smuzhiyun freeSpaceRoot = st.f_bfree * st.f_frsize 212*4882a593Smuzhiyun totalSpace = st.f_blocks * st.f_frsize 213*4882a593Smuzhiyun diskUsage[dev] = bb.event.DiskUsageSample(freeSpace, freeSpaceRoot, totalSpace) 214*4882a593Smuzhiyun 215*4882a593Smuzhiyun if minSpace and freeSpace < minSpace: 216*4882a593Smuzhiyun # Always show warning, the self.checked would always be False if the action is WARN 217*4882a593Smuzhiyun if self.preFreeS[k] == 0 or self.preFreeS[k] - freeSpace > self.spaceInterval and not self.checked[k]: 218*4882a593Smuzhiyun logger.warning("The free space of %s (%s) is running low (%.3fGB left)" % \ 219*4882a593Smuzhiyun (path, dev, freeSpace / 1024 / 1024 / 1024.0)) 220*4882a593Smuzhiyun self.preFreeS[k] = freeSpace 221*4882a593Smuzhiyun 222*4882a593Smuzhiyun if action == "STOPTASKS" and not self.checked[k]: 223*4882a593Smuzhiyun logger.error("No new tasks can be executed since the disk space monitor action is \"STOPTASKS\"!") 224*4882a593Smuzhiyun self.checked[k] = True 225*4882a593Smuzhiyun rq.finish_runqueue(False) 226*4882a593Smuzhiyun bb.event.fire(bb.event.DiskFull(dev, 'disk', freeSpace, path), self.configuration) 227*4882a593Smuzhiyun elif action == "HALT" and not self.checked[k]: 228*4882a593Smuzhiyun logger.error("Immediately halt since the disk space monitor action is \"HALT\"!") 229*4882a593Smuzhiyun self.checked[k] = True 230*4882a593Smuzhiyun rq.finish_runqueue(True) 231*4882a593Smuzhiyun bb.event.fire(bb.event.DiskFull(dev, 'disk', freeSpace, path), self.configuration) 232*4882a593Smuzhiyun 233*4882a593Smuzhiyun # The free inodes, integer number 234*4882a593Smuzhiyun freeInode = st.f_favail 235*4882a593Smuzhiyun 236*4882a593Smuzhiyun if minInode and freeInode < minInode: 237*4882a593Smuzhiyun # Some filesystems use dynamic inodes so can't run out 238*4882a593Smuzhiyun # (e.g. btrfs). This is reported by the inode count being 0. 239*4882a593Smuzhiyun if st.f_files == 0: 240*4882a593Smuzhiyun self.devDict[k][2] = None 241*4882a593Smuzhiyun continue 242*4882a593Smuzhiyun # Always show warning, the self.checked would always be False if the action is WARN 243*4882a593Smuzhiyun if self.preFreeI[k] == 0 or self.preFreeI[k] - freeInode > self.inodeInterval and not self.checked[k]: 244*4882a593Smuzhiyun logger.warning("The free inode of %s (%s) is running low (%.3fK left)" % \ 245*4882a593Smuzhiyun (path, dev, freeInode / 1024.0)) 246*4882a593Smuzhiyun self.preFreeI[k] = freeInode 247*4882a593Smuzhiyun 248*4882a593Smuzhiyun if action == "STOPTASKS" and not self.checked[k]: 249*4882a593Smuzhiyun logger.error("No new tasks can be executed since the disk space monitor action is \"STOPTASKS\"!") 250*4882a593Smuzhiyun self.checked[k] = True 251*4882a593Smuzhiyun rq.finish_runqueue(False) 252*4882a593Smuzhiyun bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration) 253*4882a593Smuzhiyun elif action == "HALT" and not self.checked[k]: 254*4882a593Smuzhiyun logger.error("Immediately halt since the disk space monitor action is \"HALT\"!") 255*4882a593Smuzhiyun self.checked[k] = True 256*4882a593Smuzhiyun rq.finish_runqueue(True) 257*4882a593Smuzhiyun bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration) 258*4882a593Smuzhiyun 259*4882a593Smuzhiyun bb.event.fire(bb.event.MonitorDiskEvent(diskUsage), self.configuration) 260*4882a593Smuzhiyun return 261