]> git.ipfire.org Git - people/ms/pakfire.git/blame - python/pakfire/system.py
daemon: Check if build root is read-write mounted.
[people/ms/pakfire.git] / python / pakfire / system.py
CommitLineData
8fe16710
MT
1#!/usr/bin/python
2###############################################################################
3# #
4# Pakfire - The IPFire package management system #
5# Copyright (C) 2011 Pakfire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
21
22from __future__ import division
23
c62d93f1 24import multiprocessing
8fe16710 25import os
c62d93f1 26import socket
d251d8e7 27import tempfile
c62d93f1 28
aa14071d 29import distro
d251d8e7 30import shell
aa14071d 31
c62d93f1
MT
32from i18n import _
33
34class System(object):
35 """
36 Class that grants access to several information about
37 the system this software is running on.
38 """
39 @property
40 def hostname(self):
a579a34f
MT
41 hn = socket.gethostname()
42
43 # If a host has got no domain part, we add one.
44 if not "." in hn:
45 hn = "%s.localdomain" % hn
46
47 return hn
c62d93f1 48
aa14071d
MT
49 @property
50 def distro(self):
51 if not hasattr(self, "_distro"):
52 self._distro = distro.Distribution()
53
54 return self._distro
55
790a44cc
MT
56 @property
57 def native_arch(self):
58 """
59 Return the native architecture of the host we
60 are running on.
61 """
62 return os.uname()[4]
63
c62d93f1
MT
64 @property
65 def arch(self):
66 """
67 Return the architecture of the host we are running on.
68 """
b0b48ca3 69 if self.supported_arches and not self.native_arch in self.supported_arches:
790a44cc
MT
70 return self.supported_arches[0]
71
72 return self.native_arch
c62d93f1
MT
73
74 @property
75 def supported_arches(self):
76 """
77 Check what architectures can be built on this host.
78 """
79 host_can_build = {
80 # Host arch : Can build these arches.
81
82 # x86
83 "x86_64" : ["x86_64", "i686",],
84 "i686" : ["i686",],
85
86 # ARM
87 "armv5tel" : ["armv5tel",],
88 "armv5tejl" : ["armv5tel",],
f711a054 89 "armv6l" : ["armv5tel",],
790a44cc 90 "armv7l" : ["armv7hl", "armv5tel",],
c62d93f1
MT
91 "armv7hl" : ["armv7hl", "armv5tel",],
92 }
93
94 try:
790a44cc 95 return host_can_build[self.native_arch]
c62d93f1
MT
96 except KeyError:
97 return []
98
99 def host_supports_arch(self, arch):
100 """
101 Check if this host can build for the target architecture "arch".
102 """
103 return arch in self.supported_arches
104
105 @property
106 def cpu_count(self):
107 """
108 Count the number of CPU cores.
109 """
110 return multiprocessing.cpu_count()
111
aa14071d
MT
112 def parse_cpuinfo(self):
113 ret = {}
114
c62d93f1
MT
115 with open("/proc/cpuinfo") as f:
116 for line in f.readlines():
c62d93f1 117 try:
aa14071d
MT
118 # Split the lines by colons.
119 a, b = line.split(":")
120
121 # Strip whitespace.
122 a = a.strip()
123 b = b.strip()
124
125 ret[a] = b
c62d93f1 126 except:
aa14071d 127 pass
c62d93f1 128
aa14071d
MT
129 return ret
130
131 @property
132 def cpu_model(self):
133 cpuinfo = self.parse_cpuinfo()
c62d93f1 134
90c9f249
MT
135 ret = cpuinfo.get("model name", None)
136
137 # Some ARM platforms do not provide "model name", so we
138 # try an other way.
139 if ret is None:
c62d93f1
MT
140 try:
141 ret = "%(Hardware)s - %(Processor)s" % cpuinfo
142 except KeyError:
143 pass
c62d93f1
MT
144
145 # Remove too many spaces.
90c9f249
MT
146 if ret:
147 ret = " ".join(ret.split())
c62d93f1
MT
148
149 return ret or _("Could not be determined")
150
151 @property
aa14071d
MT
152 def cpu_bogomips(self):
153 cpuinfo = self.parse_cpuinfo()
154
726c2908
MT
155 for key in ("bogomips", "BogoMIPS"):
156 bogomips = cpuinfo.get(key, None)
157
158 if bogomips is None:
159 continue
160
aa14071d 161 return float(bogomips) * self.cpu_count
aa14071d
MT
162
163 def get_loadavg(self):
164 return os.getloadavg()
165
166 @property
167 def loadavg1(self):
168 return self.get_loadavg()[0]
169
170 @property
171 def loadavg5(self):
172 return self.get_loadavg()[1]
173
174 @property
175 def loadavg15(self):
176 return self.get_loadavg()[2]
177
178 def has_overload(self):
179 """
180 Checks, if the load average is not too high.
181
182 On this is to be decided if a new job is taken.
183 """
184 # If there are more than 2 processes in the process queue per CPU
185 # core we will assume that the system has heavy load and to not request
186 # a new job.
187 return self.loadavg5 >= self.cpu_count * 2
188
189 def parse_meminfo(self):
190 ret = {}
191
c62d93f1 192 with open("/proc/meminfo") as f:
aa14071d
MT
193 for line in f.readlines():
194 try:
195 a, b, c = line.split()
c62d93f1 196
aa14071d
MT
197 a = a.strip()
198 a = a.replace(":", "")
199 b = int(b)
200
201 ret[a] = b * 1024
202 except:
203 pass
204
205 return ret
206
207 @property
208 def memory_total(self):
209 meminfo = self.parse_meminfo()
210
211 return meminfo.get("MemTotal", None)
212
213 # For compatibility
214 memory = memory_total
215
216 @property
217 def memory_free(self):
218 meminfo = self.parse_meminfo()
219
7811590a
MT
220 free = meminfo.get("MemFree", None)
221 if free:
222 buffers = meminfo.get("Buffers")
223 cached = meminfo.get("Cached")
224
225 return free + buffers + cached
aa14071d
MT
226
227 @property
228 def swap_total(self):
229 meminfo = self.parse_meminfo()
230
231 return meminfo.get("SwapTotal", None)
232
233 @property
234 def swap_free(self):
235 meminfo = self.parse_meminfo()
c62d93f1 236
aa14071d 237 return meminfo.get("SwapFree", None)
c62d93f1 238
2ad59b6e
MT
239 def get_mountpoint(self, path):
240 return Mountpoint(path)
241
cae35096
MT
242 @property
243 def parallelism(self):
244 """
245 Calculates how many processes should be run
246 simulatneously when compiling.
247 """
879abfa1 248 # Check how many processes would fit into the
f7a632ab
MT
249 # memory when each process takes up to 128MB.
250 multiplicator = self.memory / (128 * 1024 * 1024)
879abfa1
MT
251 multiplicator = round(multiplicator)
252
cae35096 253 # Count the number of online CPU cores.
f7a632ab
MT
254 cpucount = os.sysconf("SC_NPROCESSORS_CONF") * 2
255 cpucount += 1
cae35096 256
f7a632ab 257 return min(multiplicator, cpucount)
cae35096 258
c62d93f1
MT
259
260# Create an instance of this class to only keep it once in memory.
261system = System()
8fe16710
MT
262
263class Mountpoints(object):
2ad59b6e 264 def __init__(self, root="/"):
8fe16710
MT
265 self._mountpoints = []
266
267 # Scan for all mountpoints on the system.
268 self._scan(root)
269
270 def __iter__(self):
271 return iter(self._mountpoints)
272
273 def _scan(self, root):
274 # Get the real path of root.
275 root = os.path.realpath(root)
276
277 # If root is not equal to /, we are in a chroot and
278 # our root must be a mountpoint to count files.
279 if not root == "/":
2ad59b6e 280 mp = Mountpoint("/", root=root)
8fe16710
MT
281 self._mountpoints.append(mp)
282
283 f = open("/proc/mounts")
284
285 for line in f.readlines():
286 line = line.split()
287
288 # The mountpoint is the second argument.
289 mountpoint = line[1]
290
291 # Skip all mountpoints that are not in our root directory.
292 if not mountpoint.startswith(root):
293 continue
294
295 mountpoint = os.path.relpath(mountpoint, root)
296 if mountpoint == ".":
297 mountpoint = "/"
298 else:
299 mountpoint = os.path.join("/", mountpoint)
300
2ad59b6e 301 mp = Mountpoint(mountpoint, root=root)
8fe16710
MT
302
303 if not mp in self._mountpoints:
304 self._mountpoints.append(mp)
305
306 f.close()
307
308 # Sort all mountpoints for better searching.
309 self._mountpoints.sort()
310
311 def add_pkg(self, pkg):
312 for file in pkg.filelist:
313 self.add(file)
314
315 def rem_pkg(self, pkg):
316 for file in pkg.filelist:
317 self.rem(file)
318
319 def add(self, file):
320 for mp in reversed(self._mountpoints):
321 # Check if the file is located on this mountpoint.
322 if not file.name.startswith(mp.path):
323 continue
324
325 # Add file to this mountpoint.
326 mp.add(file)
327 break
328
329 def rem(self, file):
330 for mp in reversed(self._mountpoints):
331 # Check if the file is located on this mountpoint.
332 if not file.name.startswith(mp.path):
333 continue
334
335 # Remove file from this mountpoint.
336 mp.rem(file)
337 break
338
339
340class Mountpoint(object):
2ad59b6e 341 def __init__(self, path, root="/"):
8fe16710
MT
342 self.path = path
343 self.root = root
344
345 # Cache the statvfs call of the mountpoint.
346 self.__stat = None
347
348 # Save the amount of data that is used or freed.
349 self.disk_usage = 0
350
d251d8e7
MT
351 def __repr__(self):
352 return "<%s %s>" % (self.__class__.__name__, self.fullpath)
353
8fe16710
MT
354 def __cmp__(self, other):
355 return cmp(self.fullpath, other.fullpath)
356
357 @property
358 def fullpath(self):
359 path = self.path
360 while path.startswith("/"):
361 path = path[1:]
362
363 return os.path.join(self.root, path)
364
365 @property
366 def stat(self):
367 if self.__stat is None:
368 # Find the next mountpoint, because we cannot
369 # statvfs any path in the FS.
370 path = os.path.realpath(self.fullpath)
371
372 # Walk to root until we find a mountpoint.
373 while not os.path.ismount(path):
374 path = os.path.dirname(path)
375
376 # See what we can get.
377 self.__stat = os.statvfs(path)
378
379 return self.__stat
380
381 @property
382 def free(self):
383 return self.stat.f_bavail * self.stat.f_bsize
384
385 @property
386 def space_needed(self):
387 if self.disk_usage > 0:
388 return self.disk_usage
389
390 return 0
391
392 @property
393 def space_left(self):
394 return self.free - self.space_needed
395
396 def add(self, file):
397 assert file.name.startswith(self.path)
398
399 # Round filesize to 4k blocks.
400 block_size = 4096
401
402 blocks = file.size // block_size
403 if file.size % block_size:
404 blocks += 1
405
406 self.disk_usage += blocks * block_size
407
408 def rem(self, file):
409 assert file.name.startswith(self.path)
410
cd99871f 411 self.disk_usage -= file.size
c62d93f1 412
d251d8e7
MT
413 def is_readonly(self):
414 """
415 Returns True if the mountpoint is mounted read-only.
416 Otherwise False.
417 """
418 # Using the statvfs output does not really work, so we use
419 # a very naive approach here, were we just try to create a
420 # new file. If that works, it's writable.
421
422 try:
423 handle, path = tempfile.mkstemp(prefix="ro-test-", dir=self.fullpath)
424 except OSError, e:
425 # Read-only file system.
426 if e.errno == 30:
427 return True
428
429 # Raise all other exceptions.
430 raise
431 else:
432 # Close the file and remove it.
433 os.close(handle)
434 os.unlink(path)
435
436 return False
437
438 def remount(self, rorw=None):
439 options = "remount"
440 if rorw in ("ro", "rw"):
441 options = "%s,%s" % (options, rorw)
442
443 try:
444 shellenv = shell.ShellExecuteEnvironment(
445 ["mount", "-o", options, self.fullpath],
446 shell=False,
447 )
448 shellenv.execute()
449 except ShellEnvironmentError, e:
450 raise OSError
451
c62d93f1
MT
452
453if __name__ == "__main__":
454 print "Hostname", system.hostname
455 print "Arch", system.arch
456 print "Supported arches", system.supported_arches
457
458 print "CPU Model", system.cpu_model
459 print "CPU count", system.cpu_count
460 print "Memory", system.memory