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