Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 1 | # Copyright 2015 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Module to retrieve system information and status.""" |
| 6 | |
| 7 | from __future__ import print_function |
| 8 | |
| 9 | import collections |
| 10 | import glob |
| 11 | import logging |
| 12 | import netifaces |
| 13 | import os |
| 14 | import re |
| 15 | import subprocess |
| 16 | |
| 17 | import factory_common # pylint: disable=W0611 |
| 18 | from cros.factory import hwid |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 19 | from cros.factory.system import GetBoard |
| 20 | from cros.factory.system import partitions |
| 21 | from cros.factory import test |
Hung-Te Lin | b3a0964 | 2015-12-14 18:57:23 +0800 | [diff] [blame] | 22 | from cros.factory.test import dut |
Hung-Te Lin | 6a72c64 | 2015-12-13 22:09:09 +0800 | [diff] [blame] | 23 | from cros.factory.test.dut import power |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 24 | from cros.factory.test import factory |
| 25 | from cros.factory.utils.process_utils import Spawn |
| 26 | from cros.factory.utils.sys_utils import MountDeviceAndReadFile |
| 27 | |
| 28 | |
| 29 | class SystemInfo(object): |
| 30 | """Static information about the system. |
| 31 | |
| 32 | This is mostly static information that changes rarely if ever |
| 33 | (e.g., version numbers, serial numbers, etc.). |
| 34 | """ |
| 35 | # If not None, an update that is available from the update server. |
| 36 | update_md5sum = None |
| 37 | |
| 38 | # The cached release image version and channel. |
| 39 | release_image_version = None |
| 40 | release_image_channel = None |
| 41 | allowed_release_channels = ['dev', 'beta', 'stable'] |
| 42 | |
Hung-Te Lin | b3a0964 | 2015-12-14 18:57:23 +0800 | [diff] [blame] | 43 | def __init__(self, dut_instance=None): |
| 44 | self.dut = dut.Create() if dut_instance is None else dut_instance |
| 45 | |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 46 | self.mlb_serial_number = None |
| 47 | try: |
| 48 | self.mlb_serial_number = test.shopfloor.GetDeviceData()[ |
| 49 | 'mlb_serial_number'] |
| 50 | except: |
| 51 | pass |
| 52 | |
| 53 | self.serial_number = None |
| 54 | try: |
| 55 | self.serial_number = test.shopfloor.get_serial_number() |
| 56 | if self.serial_number is not None: |
| 57 | self.serial_number = str(self.serial_number) |
| 58 | except: |
| 59 | pass |
| 60 | |
| 61 | self.stage = None |
| 62 | try: |
| 63 | self.stage = test.shopfloor.GetDeviceData()['stage'] |
| 64 | except: |
| 65 | pass |
| 66 | |
| 67 | self.factory_image_version = None |
| 68 | try: |
| 69 | lsb_release = open('/etc/lsb-release').read() |
| 70 | match = re.search('^GOOGLE_RELEASE=(.+)$', lsb_release, |
| 71 | re.MULTILINE) |
| 72 | if match: |
| 73 | self.factory_image_version = match.group(1) |
| 74 | except: |
| 75 | pass |
| 76 | |
| 77 | self.toolkit_version = None |
| 78 | try: |
| 79 | with open('/usr/local/factory/TOOLKIT_VERSION') as f: |
| 80 | self.toolkit_version = f.read().strip() |
| 81 | except: |
| 82 | pass |
| 83 | |
| 84 | def _GetReleaseLSBValue(lsb_key): |
| 85 | """Gets the value from the lsb-release file on release image.""" |
| 86 | if not _GetReleaseLSBValue.lsb_content: |
| 87 | try: |
| 88 | release_rootfs = partitions.RELEASE_ROOTFS.path |
| 89 | _GetReleaseLSBValue.lsb_content = ( |
| 90 | MountDeviceAndReadFile(release_rootfs, '/etc/lsb-release')) |
| 91 | except: |
| 92 | pass |
| 93 | |
| 94 | match = re.search('^%s=(.+)$' % lsb_key, |
| 95 | _GetReleaseLSBValue.lsb_content, |
| 96 | re.MULTILINE) |
| 97 | if match: |
| 98 | return match.group(1) |
| 99 | else: |
| 100 | return None |
| 101 | # The cached content of lsb-release file. |
| 102 | _GetReleaseLSBValue.lsb_content = "" |
| 103 | |
| 104 | self.release_image_version = None |
| 105 | if SystemInfo.release_image_version: |
| 106 | self.release_image_version = SystemInfo.release_image_version |
| 107 | logging.debug('Obtained release image version from SystemInfo: %r', |
| 108 | self.release_image_version) |
| 109 | else: |
| 110 | logging.debug('Release image version does not exist in SystemInfo. ' |
| 111 | 'Try to get it from lsb-release from release partition.') |
| 112 | |
| 113 | self.release_image_version = _GetReleaseLSBValue('GOOGLE_RELEASE') |
| 114 | if self.release_image_version: |
| 115 | logging.debug('Release image version: %s', self.release_image_version) |
| 116 | logging.debug('Cache release image version to SystemInfo.') |
| 117 | SystemInfo.release_image_version = self.release_image_version |
| 118 | else: |
| 119 | logging.debug('Can not read release image version from lsb-release.') |
| 120 | |
| 121 | self.release_image_channel = None |
| 122 | if SystemInfo.release_image_channel: |
| 123 | self.release_image_channel = SystemInfo.release_image_channel |
| 124 | logging.debug('Obtained release image channel from SystemInfo: %r', |
| 125 | self.release_image_channel) |
| 126 | else: |
| 127 | logging.debug('Release image channel does not exist in SystemInfo. ' |
| 128 | 'Try to get it from lsb-release from release partition.') |
| 129 | |
| 130 | self.release_image_channel = _GetReleaseLSBValue('CHROMEOS_RELEASE_TRACK') |
| 131 | if self.release_image_channel: |
| 132 | logging.debug('Release image channel: %s', self.release_image_channel) |
| 133 | logging.debug('Cache release image channel to SystemInfo.') |
| 134 | SystemInfo.release_image_channel = self.release_image_channel |
| 135 | else: |
| 136 | logging.debug('Can not read release image channel from lsb-release.') |
| 137 | |
| 138 | self.wlan0_mac = None |
| 139 | try: |
| 140 | for wlan_interface in ['mlan0', 'wlan0']: |
| 141 | address_path = os.path.join('/sys/class/net/', |
| 142 | wlan_interface, 'address') |
| 143 | if os.path.exists(address_path): |
| 144 | self.wlan0_mac = open(address_path).read().strip() |
| 145 | except: |
| 146 | pass |
| 147 | |
| 148 | self.eth_macs = dict() |
| 149 | try: |
| 150 | eth_paths = glob.glob('/sys/class/net/eth*') |
| 151 | for eth_path in eth_paths: |
| 152 | address_path = os.path.join(eth_path, 'address') |
| 153 | if os.path.exists(address_path): |
| 154 | self.eth_macs[os.path.basename(eth_path)] = open( |
| 155 | address_path).read().strip() |
| 156 | except: |
| 157 | self.eth_macs = None |
| 158 | |
| 159 | self.kernel_version = None |
| 160 | try: |
| 161 | uname = subprocess.Popen(['uname', '-r'], stdout=subprocess.PIPE) |
| 162 | stdout, _ = uname.communicate() |
| 163 | self.kernel_version = stdout.strip() |
| 164 | except: |
| 165 | pass |
| 166 | |
| 167 | self.architecture = None |
| 168 | try: |
| 169 | self.architecture = Spawn(['uname', '-m'], |
| 170 | check_output=True).stdout_data.strip() |
| 171 | except: |
| 172 | pass |
| 173 | |
| 174 | self.ec_version = None |
| 175 | try: |
Hung-Te Lin | b3a0964 | 2015-12-14 18:57:23 +0800 | [diff] [blame] | 176 | self.ec_version = self.dut.ec.GetECVersion() |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 177 | except: |
| 178 | pass |
| 179 | |
| 180 | self.pd_version = None |
| 181 | try: |
Hung-Te Lin | b3a0964 | 2015-12-14 18:57:23 +0800 | [diff] [blame] | 182 | self.pd_version = self.dut.ec.GetPDVersion() |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 183 | except: |
| 184 | pass |
| 185 | |
| 186 | self.firmware_version = None |
| 187 | try: |
| 188 | crossystem = subprocess.Popen(['crossystem', 'fwid'], |
| 189 | stdout=subprocess.PIPE) |
| 190 | stdout, _ = crossystem.communicate() |
| 191 | self.firmware_version = stdout.strip() or None |
| 192 | except: |
| 193 | pass |
| 194 | |
| 195 | self.mainfw_type = None |
| 196 | try: |
| 197 | crossystem = subprocess.Popen(['crossystem', 'mainfw_type'], |
| 198 | stdout=subprocess.PIPE) |
| 199 | stdout, _ = crossystem.communicate() |
| 200 | self.mainfw_type = stdout.strip() or None |
| 201 | except: |
| 202 | pass |
| 203 | |
| 204 | self.root_device = None |
| 205 | try: |
| 206 | rootdev = Spawn(['rootdev', '-s'], |
| 207 | stdout=subprocess.PIPE, ignore_stderr=True) |
| 208 | stdout, _ = rootdev.communicate() |
| 209 | self.root_device = stdout.strip() |
| 210 | except: |
| 211 | pass |
| 212 | |
| 213 | self.factory_md5sum = factory.get_current_md5sum() |
| 214 | |
| 215 | # Uses checksum of hwid file as hwid database version. |
| 216 | self.hwid_database_version = None |
| 217 | try: |
| 218 | hwid_file_path = os.path.join(hwid.common.DEFAULT_HWID_DATA_PATH, |
| 219 | hwid.common.ProbeBoard().upper()) |
| 220 | if os.path.exists(hwid_file_path): |
| 221 | self.hwid_database_version = hwid.hwid_utils.ComputeDatabaseChecksum( |
| 222 | hwid_file_path) |
| 223 | except: |
| 224 | pass |
| 225 | |
| 226 | # update_md5sum is currently in SystemInfo's __dict__ but not this |
| 227 | # object's. Copy it from SystemInfo into this object's __dict__. |
| 228 | self.update_md5sum = SystemInfo.update_md5sum |
| 229 | |
| 230 | |
| 231 | # TODO(hungte) Move these functions to network module. |
| 232 | |
| 233 | def GetIPv4Interfaces(): |
| 234 | """Returns a list of IPv4 interfaces.""" |
| 235 | interfaces = sorted(netifaces.interfaces()) |
| 236 | return [x for x in interfaces if not x.startswith('lo')] |
| 237 | |
| 238 | |
| 239 | def GetIPv4InterfaceAddresses(interface): |
| 240 | """Returns a list of ips of an interface""" |
| 241 | try: |
| 242 | addresses = netifaces.ifaddresses(interface).get(netifaces.AF_INET, []) |
| 243 | except ValueError: |
| 244 | pass |
| 245 | ips = [x.get('addr') for x in addresses |
| 246 | if 'addr' in x] or ['none'] |
| 247 | return ips |
| 248 | |
| 249 | |
| 250 | def IsInterfaceConnected(prefix): |
| 251 | """Returns whether any interface starting with prefix is connected""" |
| 252 | ips = [] |
| 253 | for interface in GetIPv4Interfaces(): |
| 254 | if interface.startswith(prefix): |
| 255 | ips += [x for x in GetIPv4InterfaceAddresses(interface) if x != 'none'] |
| 256 | |
| 257 | return ips != [] |
| 258 | |
| 259 | |
| 260 | def GetIPv4Addresses(): |
| 261 | """Returns a string describing interfaces' IPv4 addresses. |
| 262 | |
| 263 | The returned string is of the format |
| 264 | |
| 265 | eth0=192.168.1.10, wlan0=192.168.16.14 |
| 266 | """ |
| 267 | ret = [] |
| 268 | interfaces = GetIPv4Interfaces() |
| 269 | for interface in interfaces: |
| 270 | ips = GetIPv4InterfaceAddresses(interface) |
| 271 | ret.append('%s=%s' % (interface, '+'.join(ips))) |
| 272 | |
| 273 | return ', '.join(ret) |
| 274 | |
| 275 | |
| 276 | _SysfsAttribute = collections.namedtuple('SysfsAttribute', |
| 277 | ['name', 'type', 'optional']) |
| 278 | _SysfsBatteryAttributes = [ |
| 279 | _SysfsAttribute('charge_full', int, False), |
| 280 | _SysfsAttribute('charge_full_design', int, False), |
| 281 | _SysfsAttribute('charge_now', int, False), |
| 282 | _SysfsAttribute('current_now', int, False), |
| 283 | _SysfsAttribute('present', bool, False), |
| 284 | _SysfsAttribute('status', str, False), |
| 285 | _SysfsAttribute('voltage_now', int, False), |
| 286 | _SysfsAttribute('voltage_min_design', int, True), |
| 287 | _SysfsAttribute('energy_full', int, True), |
| 288 | _SysfsAttribute('energy_full_design', int, True), |
| 289 | _SysfsAttribute('energy_now', int, True), |
| 290 | ] |
| 291 | |
| 292 | |
| 293 | class SystemStatus(object): |
| 294 | """Information about the current system status. |
| 295 | |
| 296 | This is information that changes frequently, e.g., load average |
| 297 | or battery information. |
| 298 | |
| 299 | We log a bunch of system status here. |
| 300 | """ |
| 301 | # Class variable: a charge_manager instance for checking force |
| 302 | # charge status. |
| 303 | charge_manager = None |
| 304 | |
Hung-Te Lin | b3a0964 | 2015-12-14 18:57:23 +0800 | [diff] [blame] | 305 | def __init__(self, dut_instance=None): |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 306 | def _CalculateBatteryFractionFull(battery): |
| 307 | for t in ['charge', 'energy']: |
| 308 | now = battery['%s_now' % t] |
| 309 | full = battery['%s_full' % t] |
| 310 | if (now is not None and full is not None and full > 0 and now >= 0): |
| 311 | return float(now) / full |
| 312 | return None |
| 313 | |
Hung-Te Lin | b3a0964 | 2015-12-14 18:57:23 +0800 | [diff] [blame] | 314 | self.dut = dut.Create() if dut_instance is None else dut_instance |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 315 | self.battery = {} |
| 316 | self.battery_sysfs_path = None |
| 317 | path_list = glob.glob('/sys/class/power_supply/*/type') |
| 318 | for p in path_list: |
| 319 | try: |
| 320 | if open(p).read().strip() == 'Battery': |
| 321 | self.battery_sysfs_path = os.path.dirname(p) |
| 322 | break |
| 323 | except: |
| 324 | logging.warning('sysfs path %s is unavailable', p) |
| 325 | |
| 326 | for k, item_type, optional in _SysfsBatteryAttributes: |
| 327 | self.battery[k] = None |
| 328 | try: |
| 329 | if self.battery_sysfs_path: |
| 330 | self.battery[k] = item_type( |
| 331 | open(os.path.join(self.battery_sysfs_path, k)).read().strip()) |
| 332 | except: |
| 333 | log_func = logging.error |
| 334 | if optional: |
| 335 | log_func = logging.debug |
| 336 | log_func('sysfs path %s is unavailable', |
| 337 | os.path.join(self.battery_sysfs_path, k)) |
| 338 | |
| 339 | self.battery['fraction_full'] = _CalculateBatteryFractionFull(self.battery) |
| 340 | |
| 341 | self.battery['force'] = False |
| 342 | if self.charge_manager: |
| 343 | force_status = { |
Hung-Te Lin | 6a72c64 | 2015-12-13 22:09:09 +0800 | [diff] [blame] | 344 | power.Power.ChargeState.DISCHARGE: 'Discharging', |
| 345 | power.Power.ChargeState.CHARGE: 'Charging', |
| 346 | power.Power.ChargeState.IDLE: 'Idle'}.get( |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 347 | self.charge_manager.state) |
| 348 | if force_status: |
| 349 | self.battery['status'] = force_status |
| 350 | self.battery['force'] = True |
| 351 | |
| 352 | # Get fan speed |
| 353 | try: |
Hung-Te Lin | 0326f85 | 2015-11-26 12:48:07 +0800 | [diff] [blame] | 354 | self.fan_rpm = self.dut.thermal.GetFanRPM() |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 355 | except: |
| 356 | self.fan_rpm = None |
| 357 | |
| 358 | # Get temperatures from sensors |
| 359 | try: |
Hung-Te Lin | 0326f85 | 2015-11-26 12:48:07 +0800 | [diff] [blame] | 360 | self.temperatures = self.dut.thermal.GetTemperatures() |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 361 | except: |
| 362 | self.temperatures = [] |
| 363 | |
| 364 | try: |
Hung-Te Lin | 0326f85 | 2015-11-26 12:48:07 +0800 | [diff] [blame] | 365 | self.main_temperature_index = self.dut.thermal.GetMainTemperatureIndex() |
Hung-Te Lin | 8fa2906 | 2015-11-25 18:00:59 +0800 | [diff] [blame] | 366 | except: |
| 367 | self.main_temperature_index = None |
| 368 | |
| 369 | try: |
| 370 | self.load_avg = map( |
| 371 | float, open('/proc/loadavg').read().split()[0:3]) |
| 372 | except: |
| 373 | self.load_avg = None |
| 374 | |
| 375 | try: |
| 376 | self.cpu = map(int, open('/proc/stat').readline().split()[1:]) |
| 377 | except: |
| 378 | self.cpu = None |
| 379 | |
| 380 | try: |
| 381 | self.ips = GetIPv4Addresses() |
| 382 | except: |
| 383 | self.ips = None |
| 384 | |
| 385 | try: |
| 386 | self.eth_on = IsInterfaceConnected('eth') |
| 387 | except: |
| 388 | self.eth_on = None |
| 389 | |
| 390 | try: |
| 391 | self.wlan_on = (IsInterfaceConnected('mlan') or |
| 392 | IsInterfaceConnected('wlan')) |
| 393 | except: |
| 394 | self.wlan_on = None |
| 395 | |
| 396 | |
| 397 | if __name__ == '__main__': |
| 398 | import yaml |
| 399 | print(yaml.dump(dict(system_info=SystemInfo(None, None).__dict__, |
| 400 | system_status=SystemStatus().__dict__), |
| 401 | default_flow_style=False)) |