monitordisk.py 10 KB

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