| # Copyright 2015 The LUCI Authors. All rights reserved. |
| # Use of this source code is governed under the Apache License, Version 2.0 |
| # that can be found in the LICENSE file. |
| |
| """GNU/Linux specific utility functions.""" |
| |
| import ctypes |
| import ctypes.util |
| import logging |
| import multiprocessing |
| import os |
| import pipes |
| import platform |
| import re |
| import shlex |
| import subprocess |
| |
| from utils import tools |
| |
| import common |
| import gpu |
| |
| |
| ## Private stuff. |
| |
| |
| libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c')) |
| |
| cpu_mask_t = ctypes.c_ulong |
| |
| CPU_SETSIZE = 1024 |
| NCPUBITS = 8 * ctypes.sizeof(cpu_mask_t) |
| |
| pid_t = ctypes.c_uint64 |
| |
| class cpu_set_t(ctypes.Structure): |
| _fields_ = [('__bits', cpu_mask_t * (CPU_SETSIZE / NCPUBITS))] |
| |
| def get_cpus(self): |
| """Convert bits in list len == CPU_SETSIZE |
| Use 1 / 0 per cpu |
| """ |
| cpus = [] |
| for bitmask in getattr(self, '__bits'): |
| for i in range(NCPUBITS): |
| if bitmask & 1: |
| cpus.append(i) |
| bitmask >>= 1 |
| if not bitmask: |
| break |
| return cpus |
| |
| |
| @tools.cached |
| def get_num_processors(): |
| # Multiprocessing cpu_count() returns number of processors on the system, |
| # not the number of processors process can actually use. These numbers |
| # may differ for example in Docker containers. Use sched_getaffinity to |
| # find out the number of usable processors instead. |
| cpu_set = cpu_set_t() |
| err = libc.sched_getaffinity( |
| pid_t(os.getpid()), |
| ctypes.sizeof(cpu_set_t), |
| ctypes.pointer(cpu_set)) |
| if err != 0: |
| # This is not a big deal, fallback onto multiprocessing. This happens on |
| # MIPS. |
| return multiprocessing.cpu_count() |
| return len(cpu_set.get_cpus()) |
| |
| |
| def _lspci(): |
| """Returns list of PCI devices found. |
| |
| list(Bus, Type, Vendor [ID], Device [ID], extra...) |
| """ |
| try: |
| lines = subprocess.check_output( |
| ['lspci', '-mm', '-nn'], stderr=subprocess.PIPE).splitlines() |
| except (OSError, subprocess.CalledProcessError): |
| # It normally happens on Google Compute Engine as lspci is not installed by |
| # default and on ARM since they do not have a PCI bus. |
| return None |
| return [shlex.split(l) for l in lines] |
| |
| |
| @tools.cached |
| def _get_nvidia_version(): |
| try: |
| with open('/sys/module/nvidia/version', 'rb') as f: |
| # Looks like '367.27'. |
| return f.read().strip() |
| except (IOError, OSError): |
| return None |
| |
| |
| @tools.cached |
| def _get_intel_version(): |
| """Retrieves the Mesa DRI driver version, which is usable as the Intel GPU |
| driver version. |
| """ |
| try: |
| out = subprocess.check_output(['dpkg', '-s', 'libgl1-mesa-dri']) |
| # Looks like 'Version: 17.2.8-0ubuntu0~17.10.1', and we need '17.2.8'. |
| for line in out.splitlines(): |
| match = re.match(r'^Version: (\d+.\d+.\d+)', line) |
| if match: |
| return match.group(1) |
| return None |
| except (OSError, subprocess.CalledProcessError): |
| return None |
| |
| |
| def _read_cpuinfo(): |
| with open('/proc/cpuinfo', 'rb') as f: |
| return f.read() |
| |
| |
| def _read_dmi_file(filename): |
| try: |
| with open('/sys/devices/virtual/dmi/id/' + filename, 'rb') as f: |
| return f.read().strip() |
| except (IOError, OSError): |
| return None |
| |
| |
| ## Public API. |
| |
| |
| @tools.cached |
| def get_os_version_number(): |
| """Returns the normalized OS version number as a string. |
| |
| Returns: |
| - 12.04, 10.04, etc. |
| """ |
| # On Ubuntu it will return a string like '12.04'. On Raspbian, it will look |
| # like '7.6'. |
| return unicode(platform.linux_distribution()[1]) |
| |
| |
| def get_temperatures(): |
| """Returns the temperatures measured via thermal_zones |
| |
| Returns: |
| - dict of temperatures found on device in degrees C |
| (e.g. {'thermal_zone0': 43.0, 'thermal_zone1': 46.117}) |
| """ |
| temps = {} |
| path = '/sys/class/thermal' |
| if os.path.isdir(path): |
| for filename in os.listdir(path): |
| if re.match(r'^thermal_zone\d+', filename): |
| try: |
| with open(os.path.join(path, filename, 'temp'), 'rb') as f: |
| # Convert from milli-C to Celsius |
| temps[filename] = int(f.read()) / 1000.0 |
| except (IOError, OSError): |
| pass |
| return temps |
| |
| |
| @tools.cached |
| def get_audio(): |
| """Returns information about the audio.""" |
| pci_devices = _lspci() |
| if not pci_devices: |
| return None |
| # Join columns 'Vendor' and 'Device'. 'man lspci' for more details. |
| return [ |
| u': '.join(l[2:4]) for l in pci_devices if l[1] == 'Audio device [0403]' |
| ] |
| |
| |
| @tools.cached |
| def get_cpuinfo(): |
| values = common._safe_parse(_read_cpuinfo()) |
| cpu_info = {} |
| if u'vendor_id' in values: |
| # Intel. |
| cpu_info[u'flags'] = values[u'flags'] |
| cpu_info[u'model'] = [ |
| int(values[u'cpu family']), int(values[u'model']), |
| int(values[u'stepping']), int(values[u'microcode'], 0), |
| ] |
| cpu_info[u'name'] = values[u'model name'] |
| cpu_info[u'vendor'] = values[u'vendor_id'] |
| elif u'mips' in values.get('isa', ''): |
| # MIPS. |
| cpu_info[u'flags'] = values[u'isa'] |
| cpu_info[u'name'] = values[u'cpu model'] |
| else: |
| # CPU implementer == 0x41 means ARM. |
| cpu_info[u'flags'] = values[u'Features'] |
| cpu_info[u'model'] = ( |
| int(values[u'CPU variant'], 0), int(values[u'CPU part'], 0), |
| int(values[u'CPU revision']), |
| ) |
| # ARM CPUs have a serial number embedded. Intel did try on the Pentium III |
| # but gave up after backlash; |
| # http://www.wired.com/1999/01/intel-on-privacy-whoops/ |
| # http://www.theregister.co.uk/2000/05/04/intel_processor_serial_number_q/ |
| # It is very ironic that ARM based manufacturers are getting away with. |
| if u'Serial' in values: |
| cpu_info[u'serial'] = values[u'Serial'].lstrip(u'0') |
| if u'Revision' in values: |
| cpu_info[u'revision'] = values[u'Revision'] |
| |
| # 'Hardware' field has better content so use it instead of 'model name' / |
| # 'Processor' field. |
| if u'Hardware' in values: |
| cpu_info[u'name'] = values[u'Hardware'] |
| # Samsung felt this was useful information. Strip that. |
| suffix = ' (Flattened Device Tree)' |
| if cpu_info[u'name'].endswith(suffix): |
| cpu_info[u'name'] = cpu_info[u'name'][:-len(suffix)] |
| # SAMSUNG EXYNOS5 uses 'Processor' instead of 'model name' as the key for |
| # its name <insert exasperation meme here>. |
| cpu_info[u'vendor'] = ( |
| values.get(u'model name') or values.get(u'Processor') or u'N/A') |
| |
| # http://unix.stackexchange.com/questions/43539/what-do-the-flags-in-proc-cpuinfo-mean |
| cpu_info[u'flags'] = sorted(i for i in cpu_info[u'flags'].split()) |
| return cpu_info |
| |
| |
| def get_gpu(): |
| """Returns video device as listed by 'lspci'. See get_gpu(). |
| |
| Not cached as the GPU driver may change underneat. |
| """ |
| pci_devices = _lspci() |
| if not pci_devices: |
| # It normally happens on Google Compute Engine as lspci is not installed by |
| # default and on ARM since they do not have a PCI bus. In either case, we |
| # don't care about the GPU. |
| return None, None |
| |
| dimensions = set() |
| state = set() |
| re_id = re.compile(r'^(.+?) \[([0-9a-f]{4})\]$') |
| for line in pci_devices: |
| # Look for display class as noted at http://wiki.osdev.org/PCI |
| dev_type = re_id.match(line[1]).group(2) |
| if not dev_type or not dev_type.startswith('03'): |
| continue |
| vendor = re_id.match(line[2]) |
| device = re_id.match(line[3]) |
| if not vendor or not device: |
| continue |
| ven_name = vendor.group(1) |
| ven_id = vendor.group(2) |
| dev_name = device.group(1) |
| dev_id = device.group(2) |
| |
| # TODO(maruel): Implement for AMD once needed. |
| version = u'' |
| if ven_id == gpu.NVIDIA: |
| version = _get_nvidia_version() |
| elif ven_id == gpu.INTEL: |
| version = _get_intel_version() |
| ven_name, dev_name = gpu.ids_to_names(ven_id, ven_name, dev_id, dev_name) |
| |
| dimensions.add(unicode(ven_id)) |
| dimensions.add(u'%s:%s' % (ven_id, dev_id)) |
| if version: |
| dimensions.add(u'%s:%s-%s' % (ven_id, dev_id, version)) |
| state.add(u'%s %s %s' % (ven_name, dev_name, version)) |
| else: |
| state.add(u'%s %s' % (ven_name, dev_name)) |
| return sorted(dimensions), sorted(state) |
| |
| |
| def get_uptime(): |
| """Returns uptime in seconds since system startup. |
| |
| Includes sleep time. |
| """ |
| try: |
| with open('/proc/uptime', 'rb') as f: |
| return float(f.read().split()[0]) |
| except (IOError, OSError, ValueError): |
| return 0. |
| |
| |
| def get_reboot_required(): |
| """Returns True if the system should be rebooted to apply updates. |
| |
| This only checks /var/run/reboot-required, which not all distros support and |
| which is not set for all conditions requiring reboot. |
| """ |
| return os.path.exists('/var/run/reboot-required') |
| |
| |
| @tools.cached |
| def get_ssd(): |
| """Returns a list of SSD disks.""" |
| try: |
| out = subprocess.check_output( |
| ['lsblk', '-d', '-o', 'name,rota']).splitlines() |
| ssd = [] |
| for line in out: |
| match = re.match(r'(\w+)\s+(0|1)', line) |
| if match and match.group(2) == '0': |
| ssd.append(match.group(1).decode('utf-8')) |
| return tuple(sorted(ssd)) |
| except (OSError, subprocess.CalledProcessError) as e: |
| logging.error('Failed to read disk info: %s', e) |
| return () |
| |
| |
| @tools.cached |
| def get_kvm(): |
| """Check whether KVM is available.""" |
| # We only check the file existence, not whether we can access it. This avoids |
| # the race condition between swarming_bot and udev which is responsible for |
| # setting the correct ACL of /dev/kvm. |
| return os.path.exists('/dev/kvm') |
| |
| |
| @tools.cached |
| def get_inside_docker(): |
| """Returns a strings representing if running inside docker, and which type. |
| |
| Returns: |
| - None if not run in docker. |
| - u'stock' if running in standard docker. |
| - u'nvidia' if running in nvidia-docker. |
| """ |
| if not os.path.isfile('/.docker_env') and not os.path.isfile('/.dockerenv'): |
| return None |
| # TODO(maruel): Detect nvidia-docker. |
| return u'stock' |
| |
| |
| @tools.cached |
| def get_device_tree_compatible(): |
| """Returns the devicetree/compatible data, if available. |
| |
| This is generally used on ARM based hardware. |
| """ |
| try: |
| with open('/sys/firmware/devicetree/base/compatible', 'rb') as f: |
| items = f.read().strip().split(',') |
| except IOError: |
| return None |
| # The data could contain nul byte or other invalid data, we've observed this |
| # on ODROID-C2. |
| for i, item in enumerate(items): |
| if '\x00' in item: |
| items[i] = None |
| else: |
| try: |
| items[i] = item.decode('utf-8', 'replace') |
| except UnicodeDecodeError: |
| items[i] = None |
| return sorted(i for i in items if i) |
| |
| |
| def get_cpu_scaling_governor(cpu_num): |
| """Returns the current CPU scaling governor, if available.""" |
| files = [ |
| '/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor' % cpu_num, |
| '/sys/devices/system/cpu/cpufreq/policy%d/scaling_governor' % cpu_num, |
| ] |
| for p in files: |
| try: |
| with open(p, 'rb') as f: |
| return [unicode(f.read().strip())] |
| except IOError: |
| continue |
| return None |
| |
| |
| ## Mutating code. |
| |
| |
| def generate_initd(command, cwd, user): |
| """Returns a valid init.d script for use for Swarming. |
| |
| Author is lazy so he copy-pasted. |
| Source: https://github.com/fhd/init-script-template |
| |
| Copyright (C) 2012-2014 Felix H. Dahlke |
| This is open source software, licensed under the MIT License. See the file |
| LICENSE for details. |
| |
| LICENSE is at https://github.com/fhd/init-script-template/blob/master/LICENSE |
| """ |
| return """#!/bin/sh |
| ### BEGIN INIT INFO |
| # Provides: swarming |
| # Required-Start: $remote_fs $syslog |
| # Required-Stop: $remote_fs $syslog |
| # Default-Start: 2 3 4 5 |
| # Default-Stop: 0 1 6 |
| # Short-Description: Start daemon at boot time |
| # Description: Enable service provided by daemon. |
| ### END INIT INFO |
| |
| dir='%(cwd)s' |
| user='%(user)s' |
| cmd='%(cmd)s' |
| |
| name=`basename $0` |
| pid_file="/var/run/$name.pid" |
| stdout_log="/var/log/$name.log" |
| stderr_log="/var/log/$name.err" |
| |
| get_pid() { |
| cat "$pid_file" |
| } |
| |
| is_running() { |
| [ -f "$pid_file" ] && ps `get_pid` > /dev/null 2>&1 |
| } |
| |
| case "$1" in |
| start) |
| if is_running; then |
| echo "Already started" |
| else |
| echo "Starting $name" |
| cd "$dir" |
| sudo -u "$user" $cmd >> "$stdout_log" 2>> "$stderr_log" & |
| echo $! > "$pid_file" |
| if ! is_running; then |
| echo "Unable to start, see $stdout_log and $stderr_log" |
| exit 1 |
| fi |
| fi |
| ;; |
| stop) |
| if is_running; then |
| echo -n "Stopping $name.." |
| kill `get_pid` |
| for i in {1..10} |
| do |
| if ! is_running; then |
| break |
| fi |
| |
| echo -n "." |
| sleep 1 |
| done |
| echo |
| |
| if is_running; then |
| echo "Not stopped; may still be shutting down or shutdown may have failed" |
| exit 1 |
| else |
| echo "Stopped" |
| if [ -f "$pid_file" ]; then |
| rm "$pid_file" |
| fi |
| fi |
| else |
| echo "Not running" |
| fi |
| ;; |
| restart) |
| $0 stop |
| if is_running; then |
| echo "Unable to stop, will not attempt to start" |
| exit 1 |
| fi |
| $0 start |
| ;; |
| status) |
| if is_running; then |
| echo "Running" |
| else |
| echo "Stopped" |
| exit 1 |
| fi |
| ;; |
| *) |
| echo "Usage: $0 {start|stop|restart|status}" |
| exit 1 |
| ;; |
| esac |
| |
| exit 0 |
| """ % { |
| 'cmd': ' '.join(pipes.quote(c) for c in command), |
| 'cwd': pipes.quote(cwd), |
| 'user': pipes.quote(user), |
| } |
| |
| |
| def generate_autostart_desktop(command, name): |
| """Returns a valid .desktop for use with Swarming bot. |
| |
| http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html |
| """ |
| return ( |
| '[Desktop Entry]\n' |
| 'Type=Application\n' |
| 'Name=%(name)s\n' |
| 'Exec=%(cmd)s\n' |
| 'Hidden=false\n' |
| 'NoDisplay=false\n' |
| 'Comment=Created by os_utilities.py in swarming_bot.zip\n' |
| 'X-GNOME-Autostart-enabled=true\n') % { |
| 'cmd': ' '.join(pipes.quote(c) for c in command), |
| 'name': name, |
| } |
| |
| |
| @tools.cached |
| def get_computer_system_info(): |
| """Return a named tuple, which lists the following params: |
| |
| name, vendor, version, uuid |
| """ |
| return common.ComputerSystemInfo( |
| name=_read_dmi_file('product_name'), |
| vendor=_read_dmi_file('sys_vendor'), |
| version=_read_dmi_file('product_version'), |
| serial=_read_dmi_file('product_serial')) |