Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(919)

Side by Side Diff: appengine/swarming/swarming_bot/os_utilities.py

Issue 1306633002: Overhaul Android support and make Swarming bot use python-adb (Closed) Base URL: git@github.com:luci/luci-py.git@master
Patch Set: . Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2014 The Swarming Authors. All rights reserved. 2 # Copyright 2014 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed by the Apache v2.0 license that can be 3 # Use of this source code is governed by the Apache v2.0 license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """OS specific utility functions. 6 """OS specific utility functions.
7 7
8 Includes code: 8 Includes code:
9 - to declare the current system this code is running under. 9 - to declare the current system this code is running under.
10 - to run a command on user login. 10 - to run a command on user login.
11 - to restart the host. 11 - to restart the host.
12 12
13 This file serves as an API to bot_config.py. bot_config.py can be replaced on 13 This file serves as an API to bot_config.py. bot_config.py can be replaced on
14 the server to allow additional server-specific functionality. 14 the server to allow additional server-specific functionality.
15 """ 15 """
16 16
17 import cgi 17 import cgi
18 import ctypes 18 import ctypes
19 import getpass 19 import getpass
20 import glob 20 import glob
21 import hashlib
21 import json 22 import json
22 import logging 23 import logging
23 import multiprocessing 24 import multiprocessing
24 import os 25 import os
25 import pipes 26 import pipes
26 import platform 27 import platform
27 import re 28 import re
28 import shlex 29 import shlex
29 import signal 30 import signal
30 import socket 31 import socket
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after
105 106
106 # Used to calculated Swarming bot uptime. 107 # Used to calculated Swarming bot uptime.
107 _STARTED_TS = time.time() 108 _STARTED_TS = time.time()
108 109
109 110
110 # Cache of GCE OAuth2 token. 111 # Cache of GCE OAuth2 token.
111 _CACHED_OAUTH2_TOKEN_GCE = {} 112 _CACHED_OAUTH2_TOKEN_GCE = {}
112 _CACHED_OAUTH2_TOKEN_GCE_LOCK = threading.Lock() 113 _CACHED_OAUTH2_TOKEN_GCE_LOCK = threading.Lock()
113 114
114 115
116 # Set this to package adb and M2Crypto.RSA when Android support is initialized.
117 adb = None
118 RSA = None
119
120 # Set when ADB is initialized. It contains one or multiple key used to
121 # authenticate to Android debug protocol (adb).
122 _ADB_KEYS = []
123
124
125 # Cache of /system/build.prop on Android devices connected to this host.
126 _BUILD_PROP_ANDROID = {}
127
128
115 def _write(filepath, content): 129 def _write(filepath, content):
116 """Writes out a file and returns True on success.""" 130 """Writes out a file and returns True on success."""
117 logging.info('Writing in %s:\n%s', filepath, content) 131 logging.info('Writing in %s:\n%s', filepath, content)
118 try: 132 try:
119 with open(filepath, mode='wb') as f: 133 with open(filepath, mode='wb') as f:
120 f.write(content) 134 f.write(content)
121 return True 135 return True
122 except IOError as e: 136 except IOError as e:
123 logging.error('Failed to write %s: %s', filepath, e) 137 logging.error('Failed to write %s: %s', filepath, e)
124 return False 138 return False
(...skipping 1074 matching lines...) Expand 10 before | Expand all | Expand 10 after
1199 token_info.Label.Sid, p_sid_size.contents.value - 1) 1213 token_info.Label.Sid, p_sid_size.contents.value - 1)
1200 value = res.contents.value 1214 value = res.contents.value
1201 return mapping.get(value) or u'0x%04x' % value 1215 return mapping.get(value) or u'0x%04x' % value
1202 finally: 1216 finally:
1203 ctypes.windll.kernel32.CloseHandle(token) 1217 ctypes.windll.kernel32.CloseHandle(token)
1204 1218
1205 1219
1206 ### Android. 1220 ### Android.
1207 1221
1208 1222
1223 def initialize_android(pub_key, priv_key):
Vadim Sh. 2015/08/20 22:40:09 how do you feel about moving it to "android_utilit
M-A Ruel 2015/08/21 23:37:39 Done.
1224 """Initialize Android support through adb.
1225
1226 You can steal pub_key, priv_key pair from ~/.android/adbkey and
1227 ~/.android/adbkey.pub.
Vadim Sh. 2015/08/20 22:40:08 is it true only for chrome-infra bots?
M-A Ruel 2015/08/21 23:37:39 It's created by adb.
1228 """
1229 global adb
1230 global RSA
1231 assert bool(pub_key) == bool(priv_key)
1232
1233 if adb is None:
1234 try:
1235 # Workaround pylint lint errors.
Vadim Sh. 2015/08/20 22:40:09 just disable them, it will look less weird
M-A Ruel 2015/08/21 23:37:39 Not necessary anymore.
1236 import adb as adb2
1237 adb = adb2
1238 # This is needed otherwise adb_commands is not defined.
1239 from adb import adb_commands as _
1240 from M2Crypto import RSA as RSA2
1241 RSA = RSA2
1242 logging.info('adb fully loaded')
1243 except ImportError as e:
1244 adb = False
1245 logging.error('adb is unavailable: %s' % e)
1246 return False
1247
1248 if pub_key:
1249 _ADB_KEYS.append(M2CryptoSigner(pub_key, priv_key))
1250
1251 # Try to add local adb keys if available.
1252 path = os.path.expanduser('~/.android/adbkey')
Vadim Sh. 2015/08/20 22:40:08 is it intentionally different from priv_key? What
M-A Ruel 2015/08/21 23:37:39 The private and public keys for adb are stored in
1253 if os.path.isfile(path) and os.path.isfile(path+'.pub'):
1254 with open(path + '.pub', 'rb') as f:
1255 pub = f.read()
1256 with open(path, 'rb') as f:
1257 priv = f.read()
1258 _ADB_KEYS.append(M2CryptoSigner(pub, priv))
1259
1260 if not _ADB_KEYS:
1261 adb = False
1262 logging.error('No key is provided, android support is disabled.')
1263 return False
1264 else:
1265 if pub_key:
1266 logging.warning(
1267 'initialize_android() was called repeatedly: ignoring keys')
1268 return bool(adb)
1269
1270
1271 class M2CryptoSigner(object):
1272 """Implements adb_protocol.AuthSigner using
1273 https://github.com/martinpaljak/M2Crypto.
1274 """
1275 def __init__(self, pub, priv):
1276 self.priv_key = RSA.load_key_string(priv)
1277 self.pub_key = pub
1278
1279 def Sign(self, data):
1280 return self.priv_key.sign(data, 'sha1')
1281
1282 def GetPublicKey(self):
1283 return self.pub_key
1284
1285
1286 # TODO(maruel): M2Crypto is not included by default on Ubuntu.
1287 # rsa is included in client/third_party/rsa/rsa/ and
1288 # pycrypto is normally installed on Ubuntu. It would be preferable to use one of
1289 # these 2 but my skills failed up to now, authentication consistently fails.
1290 # Revisit later or delete the code.
1291 #
1292 #
1293 #sys.path.insert(0, os.path.join(THIS_FILE, 'third_party', 'rsa'))
1294 #import rsa
1295 #
1296 #class RSASigner(object):
1297 # """Implements adb_protocol.AuthSigner using http://stuvel.eu/rsa."""
1298 # def __init__(self):
1299 # self.privkey = rsa.PrivateKey.load_pkcs1(PRIV_CONVERTED_KEY)
1300 #
1301 # def Sign(self, data):
1302 # return rsa.sign(data, self.privkey, 'SHA-1')
1303 #
1304 # def GetPublicKey(self):
1305 # return PUB_KEY
1306 #
1307 #
1308 #try:
1309 # from Crypto.Hash import SHA
1310 # from Crypto.PublicKey import RSA
1311 # from Crypto.Signature import PKCS1_v1_5
1312 # from Crypto.Signature import PKCS1_PSS
1313 #except ImportError:
1314 # SHA = None
1315 #
1316 #
1317 #class CryptoSigner(object):
1318 # """Implements adb_protocol.AuthSigner using
1319 # https://www.dlitz.net/software/pycrypto/.
1320 # """
1321 # def __init__(self):
1322 # self.private_key = RSA.importKey(PRIV_KEY, None)
1323 # self._signer = PKCS1_v1_5.new(self.private_key)
1324 # #self.private_key = RSA.importKey(PRIV_CONVERTED_KEY, None)
1325 # #self._signer = PKCS1_PSS.new(self.private_key)
1326 #
1327 # def Sign(self, data):
1328 # h = SHA.new(data)
1329 # return self._signer.sign(h)
1330 #
1331 # def GetPublicKey(self):
1332 # return PUB_KEY
1333
1334
1335 def kill_adb_android():
1336 """adb sucks. Kill it with fire."""
1337 if not adb:
1338 return
1339 try:
1340 subprocess.call(['adb', 'kill-server'])
1341 except OSError:
1342 pass
1343 subprocess.call(['killall', 'adb'])
Vadim Sh. 2015/08/20 22:40:08 killall --exact adb (In general I'm not a fan of
M-A Ruel 2015/08/21 23:37:39 Added --exact. But that's adb, it needs to be kill
1344
1345
1346 def get_devices_android():
1347 """Returns the list of devices available.
1348
1349 Caller MUST call close_devices_android(cmds) on the return value.
1350
1351 Returns one of:
1352 - dict of {serial_number: adb.adb_commands.AdbCommands}. The value may be
1353 None if there was an Auth failure.
1354 - None if adb is unavailable.
1355 """
1356 if not adb:
1357 return None
1358
1359 cmds = {}
1360 for handle in adb.adb_commands.AdbCommands.Devices():
1361 logging.info('Trying Android %s', handle.serial_number)
1362 try:
1363 handle.Open()
1364 except adb.common.usb1.USBErrorBusy:
1365 logging.warning('Got USBErrorBusy. Killing adb')
1366 kill_adb_android()
1367 # TODO(maruel): Handle when it throws again, which means another process
1368 # holds an handle to the USB ports or group acl (plugdev) hasn't been
Vadim Sh. 2015/08/20 22:40:09 typo: a handle
M-A Ruel 2015/08/21 23:37:39 Done.
1369 # setup properly.
1370 handle.Open()
Vadim Sh. 2015/08/20 22:40:09 maybe at least log exception here and ignore this
M-A Ruel 2015/08/21 23:37:39 Done.
1371
1372 try:
1373 cmd = adb.adb_commands.AdbCommands.Connect(
1374 handle, banner='swarming', rsa_keys=_ADB_KEYS, auth_timeout_ms=100)
Vadim Sh. 2015/08/20 22:40:08 100ms? Is it really that fast?
M-A Ruel 2015/08/21 23:37:39 Yes.
1375 except adb.usb_exceptions.DeviceAuthError as e:
1376 logging.warning('AUTH FAILURE: %s: %s', handle.serial_number, e)
1377 cmd = None
1378 except adb.usb_exceptions.ReadFailedError as e:
1379 logging.warning('READ FAILURE: %s: %s', handle.serial_number, e)
1380 cmd = None
1381 except ValueError as e:
1382 logging.warning(
1383 'Trying unpluging and pluging it back: %s: %s',
1384 handle.serial_number, e)
1385 cmd = None
1386 cmds[handle.serial_number] = cmd
1387
1388 logging.info('FOUND %d devices', len(cmds))
1389 # Remove any /system/build.prop cache so if a device is disconnect, reflashed
1390 # then reconnected, it will likely be refresh properly. The main concern is
1391 # that the bot didn't have the time to loop once while this is being done.
1392 # Restarting the bot works fine too.
1393 for key in _BUILD_PROP_ANDROID.keys():
1394 if key not in cmds:
1395 _BUILD_PROP_ANDROID.pop(key)
1396 return cmds
1397
1398
1399 def close_devices_android(devices):
1400 """Closes all devices opened by get_devices_android()."""
1401 for device in (devices or {}).itervalues():
1402 if device:
1403 device.Close()
Vadim Sh. 2015/08/20 22:40:09 catch and log exceptions here? (so that all device
M-A Ruel 2015/08/21 23:37:39 I don't know which exception type would be thrown
1404
1405
1406 def get_build_prop_android(cmd):
1407 """Returns the system properties for a device.
1408
1409 This isn't really changing through the lifetime of a bot. One corner case is
1410 when the device is flashed or disconnected.
1411 """
1412 if cmd.handle.serial_number not in _BUILD_PROP_ANDROID:
1413 properties = {}
1414 try:
1415 out = cmd.Shell('cat /system/build.prop').decode('utf-8')
1416 except adb.usb_exceptions.ReadFailedError:
1417 # It's a bit annoying because it means timeout_ms was wasted. Blacklist
1418 # the device until it is disconnected and reconnected.
1419 properties = None
1420 else:
1421 for line in out.splitlines():
1422 if line.startswith(u'#') or not line:
1423 continue
1424 key, value = line.split(u'=', 1)
1425 properties[key] = value
1426 _BUILD_PROP_ANDROID[cmd.handle.serial_number] = properties
1427 return _BUILD_PROP_ANDROID[cmd.handle.serial_number]
1428
1429
1430 def get_temp_android(cmd):
1431 """Returns the device's 2 temperatures."""
1432 temps = []
1433 for i in xrange(2):
1434 try:
1435 temps.append(
1436 int(cmd.Shell('cat /sys/class/thermal/thermal_zone%d/temp' % i)))
1437 except ValueError:
1438 pass
1439 return temps
1440
1441
1442 def get_battery_android(cmd):
1443 """Returns details about the battery's state."""
1444 props = {}
1445 out = cmd.Shell('dumpsys battery').decode('utf-8', 'replace')
1446 for line in out.splitlines():
1447 if line.endswith(u':'):
1448 continue
1449 key, value = line.split(u': ', 2)
1450 props[key.lstrip()] = value
1451 out = {u'power': []}
1452 if props[u'AC powered'] == u'true':
1453 out[u'power'].append(u'AC')
1454 if props[u'USB powered'] == u'true':
1455 out[u'power'].append(u'USB')
1456 if props[u'Wireless powered'] == u'true':
1457 out[u'power'].append(u'Wireless')
1458 for key in (u'health', u'level', u'status', u'temperature', u'voltage'):
1459 out[key] = props[key]
1460 return out
1461
1462
1463 def get_disk_android(cmd):
1464 """Returns details about the battery's state."""
1465 props = {}
1466 out = cmd.Shell('dumpsys diskstats').decode('utf-8', 'replace')
1467 for line in out.splitlines():
1468 if line.endswith(u':'):
1469 continue
1470 key, value = line.split(u': ', 2)
1471 match = re.match(u'^(\d+)K / (\d+)K.*', value)
1472 if match:
1473 props[key.lstrip()] = {
1474 'free_mb': round(float(match.group(1)) / 1024., 1),
1475 'size_mb': round(float(match.group(2)) / 1024., 1),
1476 }
1477 return {
1478 u'cache': props[u'Cache-Free'],
1479 u'data': props[u'Data-Free'],
1480 u'system': props[u'System-Free'],
1481 }
1482
1483
1484 def get_dimensions_all_devices_android(cmds):
1485 """Returns the default dimensions for an host with multiple android devices.
1486 """
1487 dimensions = get_dimensions()
1488 if not cmds:
1489 return dimensions
1490
1491 # Pop a few dimensions otherwise there will be too many dimensions.
1492 del dimensions[u'cpu']
1493 del dimensions[u'cores']
1494 del dimensions[u'gpu']
1495 dimensions.pop(u'machine_type')
1496
1497 # Make sure all the devices use the same board.
1498 keys = (u'build.id', u'product.board')
1499 for key in keys:
1500 dimensions[key] = set()
1501 dimensions[u'android'] = []
1502 for serial_number, cmd in cmds.iteritems():
1503 if cmd:
1504 properties = get_build_prop_android(cmd)
1505 if properties:
1506 for key in keys:
1507 dimensions[key].add(properties[u'ro.' + key])
1508 dimensions[u'android'].append(serial_number)
1509 dimensions[u'android'].sort()
1510 for key in keys:
1511 if not dimensions[key]:
1512 del dimensions[key]
1513 else:
1514 dimensions[key] = sorted(dimensions[key])
1515 nb_android = len(dimensions[u'android'])
1516 dimensions[u'android_devices'] = map(
1517 str, range(nb_android, max(0, nb_android-2), -1))
1518 return dimensions
1519
1520
1521 def get_state_all_devices_android(cmds):
1522 """Returns state information about all the devices connected to the host.
1523 """
1524 state = get_state()
1525 if not cmds:
1526 return state
1527
1528 # Add a few values that were poped from dimensions.
1529 cpu_type = get_cpu_type()
1530 cpu_bitness = get_cpu_bitness()
1531 state[u'cpu'] = [
1532 cpu_type,
1533 cpu_type + u'-' + cpu_bitness,
1534 ]
1535 state[u'cores'] = [unicode(get_num_processors())]
1536 state[u'gpu'] = get_gpu()[0]
1537 machine_type = get_machine_type()
1538 if machine_type:
1539 state[u'machine_type'] = [machine_type]
1540
1541 keys = (
1542 u'board.platform', u'product.cpu.abi', u'product.cpu.abi2',
1543 u'build.tags', u'build.type', u'build.version.sdk')
1544 state['devices'] = {}
1545 for serial_number, cmd in cmds.iteritems():
1546 if cmd:
1547 properties = get_build_prop_android(cmd)
1548 if properties:
1549 # TODO(maruel): uptime, diskstats, wifi, power, throttle, etc.
1550 device = {
1551 u'build': {key: properties[u'ro.'+key] for key in keys},
1552 u'disk': get_disk_android(cmd),
1553 u'battery': get_battery_android(cmd),
1554 u'state': u'available',
1555 u'temp': get_temp_android(cmd),
1556 }
1557 else:
1558 device = {u'state': u'unavailable'}
1559 else:
1560 device = {u'state': 'unauthenticated'}
1561 state[u'devices'][serial_number] = device
1562 return state
1209 1563
1210 1564
1211 ### 1565 ###
1212 1566
1213 1567
1214 def get_dimensions(): 1568 def get_dimensions():
1215 """Returns the default dimensions.""" 1569 """Returns the default dimensions."""
1216 os_name = get_os_name() 1570 os_name = get_os_name()
1217 cpu_type = get_cpu_type() 1571 cpu_type = get_cpu_type()
1218 cpu_bitness = get_cpu_bitness() 1572 cpu_bitness = get_cpu_bitness()
(...skipping 310 matching lines...) Expand 10 before | Expand all | Expand 10 after
1529 os.remove(item) 1883 os.remove(item)
1530 for item in glob.iglob('%s.???' % name): 1884 for item in glob.iglob('%s.???' % name):
1531 os.remove(item) 1885 os.remove(item)
1532 except Exception as e: 1886 except Exception as e:
1533 logging.exception('trim_rolled_log(%s) failed: %s', name, e) 1887 logging.exception('trim_rolled_log(%s) failed: %s', name, e)
1534 1888
1535 1889
1536 def main(): 1890 def main():
1537 """Prints out the output of get_dimensions() and get_state().""" 1891 """Prints out the output of get_dimensions() and get_state()."""
1538 # Pass an empty tag, so pop it up since it has no significance. 1892 # Pass an empty tag, so pop it up since it has no significance.
1893 devices = get_devices_android()
1539 data = { 1894 data = {
1540 u'dimensions': get_dimensions(), 1895 u'dimensions': get_dimensions_all_devices_android(devices),
1541 u'state': get_state(), 1896 u'state': get_state_all_devices_android(devices),
1542 } 1897 }
1898 close_devices_android(devices)
Vadim Sh. 2015/08/20 22:40:08 put close_devices_android(...) in finally section.
M-A Ruel 2015/08/21 23:37:39 Done.
1543 json.dump(data, sys.stdout, indent=2, sort_keys=True, separators=(',', ': ')) 1899 json.dump(data, sys.stdout, indent=2, sort_keys=True, separators=(',', ': '))
1544 print('') 1900 print('')
1545 return 0 1901 return 0
1546 1902
1547 1903
1548 if __name__ == '__main__': 1904 if __name__ == '__main__':
1549 sys.exit(main()) 1905 sys.exit(main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698