xref: /OK3568_Linux_fs/yocto/poky/bitbake/lib/bb/monitordisk.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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