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