blob: f928210351652b7896a2d4ecd1f7c067314e062c [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
2# Copyright (C) 2012 Robert Yang
3#
Brad Bishopc342db32019-05-15 21:57:59 -04004# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc124f4f2015-09-15 14:41:29 -05005#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05006
Andrew Geissler82c905d2020-04-13 13:39:40 -05007import os, logging, re
Patrick Williamsc124f4f2015-09-15 14:41:29 -05008import 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
Brad Bishop19323692019-04-05 15:28:33 -040018 unitG = re.match(r'([1-9][0-9]*)[gG]\s?$', unit)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050019 if unitG:
20 return int(unitG.group(1)) * (1024 ** 3)
Brad Bishop19323692019-04-05 15:28:33 -040021 unitM = re.match(r'([1-9][0-9]*)[mM]\s?$', unit)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050022 if unitM:
23 return int(unitM.group(1)) * (1024 ** 2)
Brad Bishop19323692019-04-05 15:28:33 -040024 unitK = re.match(r'([1-9][0-9]*)[kK]\s?$', unit)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050025 if unitK:
26 return int(unitK.group(1)) * 1024
Brad Bishop19323692019-04-05 15:28:33 -040027 unitN = re.match(r'([1-9][0-9]*)\s?$', unit)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050028 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
Andrew Geissler6ce62a22020-11-30 19:58:47 -060062def getDiskData(BBDirs):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050063
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
Brad Bishop19323692019-04-05 15:28:33 -040073 pathSpaceInodeRe = re.match(r'([^,]*),([^,]*),([^,]*),?(.*)', pathSpaceInode)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050074 if not pathSpaceInodeRe:
75 printErr("Invalid value in BB_DISKMON_DIRS: %s" % pathSpaceInode)
76 return None
77
78 action = pathSpaceInodeRe.group(1)
Andrew Geissler7e0e3c02022-02-25 20:34:39 +000079 if action == "ABORT":
80 # Emit a deprecation warning
Andrew Geissler9aee5002022-03-30 16:27:02 +000081 logger.warnonce("The BB_DISKMON_DIRS \"ABORT\" action has been renamed to \"HALT\", update configuration")
Andrew Geissler7e0e3c02022-02-25 20:34:39 +000082 action = "HALT"
83
84 if action not in ("HALT", "STOPTASKS", "WARN"):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050085 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500124 devDict[(path, action)] = [dev, minSpace, minInode]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500125
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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500136 interval = configuration.getVar("BB_DISKMON_WARNINTERVAL")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500137 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
Brad Bishop19323692019-04-05 15:28:33 -0400142 intervalRe = re.match(r'([^,]*),?\s*(.*)', interval)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500143 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500174 BBDirs = configuration.getVar("BB_DISKMON_DIRS") or None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500175 if BBDirs:
Andrew Geissler6ce62a22020-11-30 19:58:47 -0600176 self.devDict = getDiskData(BBDirs)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500177 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 = {}
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000185 # This is for STOPTASKS and HALT, to avoid printing the message
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500186 # 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:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500200 diskUsage = {}
201 for k, attributes in self.devDict.items():
202 path, action = k
203 dev, minSpace, minInode = attributes
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500204
205 st = os.statvfs(path)
206
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500207 # The available free space, integer number
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500208 freeSpace = st.f_bavail * st.f_frsize
209
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500210 # 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500215 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]:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600218 logger.warning("The free space of %s (%s) is running low (%.3fGB left)" % \
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500219 (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)
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000227 elif action == "HALT" and not self.checked[k]:
228 logger.error("Immediately halt since the disk space monitor action is \"HALT\"!")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500229 self.checked[k] = True
230 rq.finish_runqueue(True)
231 bb.event.fire(bb.event.DiskFull(dev, 'disk', freeSpace, path), self.configuration)
232
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500233 # The free inodes, integer number
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500234 freeInode = st.f_favail
235
236 if minInode and freeInode < minInode:
Andrew Geissler5082cc72023-09-11 08:41:39 -0400237 # Some filesystems use dynamic inodes so can't run out.
238 # This is reported by the inode count being 0 (btrfs) or the free
239 # inode count being -1 (cephfs).
240 if st.f_files == 0 or st.f_favail == -1:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500241 self.devDict[k][2] = None
242 continue
243 # Always show warning, the self.checked would always be False if the action is WARN
244 if self.preFreeI[k] == 0 or self.preFreeI[k] - freeInode > self.inodeInterval and not self.checked[k]:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600245 logger.warning("The free inode of %s (%s) is running low (%.3fK left)" % \
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500246 (path, dev, freeInode / 1024.0))
247 self.preFreeI[k] = freeInode
248
249 if action == "STOPTASKS" and not self.checked[k]:
250 logger.error("No new tasks can be executed since the disk space monitor action is \"STOPTASKS\"!")
251 self.checked[k] = True
252 rq.finish_runqueue(False)
253 bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration)
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000254 elif action == "HALT" and not self.checked[k]:
255 logger.error("Immediately halt since the disk space monitor action is \"HALT\"!")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500256 self.checked[k] = True
257 rq.finish_runqueue(True)
258 bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500259
260 bb.event.fire(bb.event.MonitorDiskEvent(diskUsage), self.configuration)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500261 return