blob: a8425b9b08d2ee17b3746a613b4864724e59ded6 [file] [log] [blame]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001#!/usr/bin/python -u
2# -*- coding: utf-8 -*-
3#
4# Copyright 2015 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
Joel Kitching22b89042015-08-06 18:23:29 +08008from __future__ import print_function
9
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080010import argparse
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080011import contextlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080012import fcntl
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080013import hashlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080014import json
15import logging
16import os
17import Queue
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080018import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080019import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080020import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080021import socket
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080022import struct
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080023import subprocess
24import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080025import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080026import threading
27import time
Joel Kitching22b89042015-08-06 18:23:29 +080028import traceback
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080029import urllib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080030import uuid
31
Wei-Ning Huang2132de32015-04-13 17:24:38 +080032import jsonrpclib
33from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
34
35
36_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080037
38_OVERLORD_PORT = 4455
39_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080040_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080041
42_BUFSIZE = 8192
43_RETRY_INTERVAL = 2
44_SEPARATOR = '\r\n'
45_PING_TIMEOUT = 3
46_PING_INTERVAL = 5
47_REQUEST_TIMEOUT_SECS = 60
48_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080049_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080050
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080051_CONTROL_START = 128
52_CONTROL_END = 129
53
Wei-Ning Huanga301f572015-06-03 17:34:21 +080054_BLOCK_SIZE = 4096
55
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080056RESPONSE_SUCCESS = 'success'
57RESPONSE_FAILED = 'failed'
58
Joel Kitching22b89042015-08-06 18:23:29 +080059
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080060class PingTimeoutError(Exception):
61 pass
62
63
64class RequestError(Exception):
65 pass
66
67
Joel Kitching22b89042015-08-06 18:23:29 +080068class SSHPortForwarder(object):
69 """Create and maintain an SSH port forwarding connection.
70
71 This is meant to be a standalone class to maintain an SSH port forwarding
72 connection to a given server. It provides a fail/retry mechanism, and also
73 can report its current connection status.
74 """
75 _FAILED_STR = 'port forwarding failed'
76 _DEFAULT_CONNECT_TIMEOUT = 10
77 _DEFAULT_ALIVE_INTERVAL = 10
78 _DEFAULT_DISCONNECT_WAIT = 1
79 _DEFAULT_RETRIES = 5
80 _DEFAULT_EXP_FACTOR = 1
81 _DEBUG_INTERVAL = 2
82
83 CONNECTING = 1
84 INITIALIZED = 2
85 FAILED = 4
86
87 REMOTE = 1
88 LOCAL = 2
89
90 @classmethod
91 def ToRemote(cls, *args, **kwargs):
92 """Calls contructor with forward_to=REMOTE."""
93 return cls(*args, forward_to=cls.REMOTE, **kwargs)
94
95 @classmethod
96 def ToLocal(cls, *args, **kwargs):
97 """Calls contructor with forward_to=LOCAL."""
98 return cls(*args, forward_to=cls.LOCAL, **kwargs)
99
100 def __init__(self,
101 forward_to,
102 src_port,
103 dst_port,
104 user,
105 identity_file,
106 host,
107 port=22,
108 connect_timeout=_DEFAULT_CONNECT_TIMEOUT,
109 alive_interval=_DEFAULT_ALIVE_INTERVAL,
110 disconnect_wait=_DEFAULT_DISCONNECT_WAIT,
111 retries=_DEFAULT_RETRIES,
112 exp_factor=_DEFAULT_EXP_FACTOR):
113 """Constructor.
114
115 Args:
116 forward_to: Which direction to forward traffic: REMOTE or LOCAL.
117 src_port: Source port for forwarding.
118 dst_port: Destination port for forwarding.
119 user: Username on remote server.
120 identity_file: Identity file for passwordless authentication on remote
121 server.
122 host: Host of remote server.
123 port: Port of remote server.
124 connect_timeout: Time in seconds
125 alive_interval:
126 disconnect_wait: The number of seconds to wait before reconnecting after
127 the first disconnect.
128 retries: The number of times to retry before reporting a failed
129 connection.
130 exp_factor: After each reconnect, the disconnect wait time is multiplied
131 by 2^exp_factor.
132 """
133 # Internal use.
134 self._ssh_thread = None
135 self._ssh_output = None
136 self._exception = None
137 self._state = self.CONNECTING
138 self._poll = threading.Event()
139
140 # Connection arguments.
141 self._forward_to = forward_to
142 self._src_port = src_port
143 self._dst_port = dst_port
144 self._host = host
145 self._user = user
146 self._identity_file = identity_file
147 self._port = port
148
149 # Configuration arguments.
150 self._connect_timeout = connect_timeout
151 self._alive_interval = alive_interval
152 self._exp_factor = exp_factor
153
154 t = threading.Thread(
155 target=self._Run,
156 args=(disconnect_wait, retries))
157 t.daemon = True
158 t.start()
159
160 def __str__(self):
161 # State representation.
162 if self._state == self.CONNECTING:
163 state_str = 'connecting'
164 elif self._state == self.INITIALIZED:
165 state_str = 'initialized'
166 else:
167 state_str = 'failed'
168
169 # Port forward representation.
170 if self._forward_to == self.REMOTE:
171 fwd_str = '->%d' % self._dst_port
172 else:
173 fwd_str = '%d<-' % self._dst_port
174
175 return 'SSHPortForwarder(%s,%s)' % (state_str, fwd_str)
176
177 def _ForwardArgs(self):
178 if self._forward_to == self.REMOTE:
179 return ['-R', '%d:127.0.0.1:%d' % (self._dst_port, self._src_port)]
180 else:
181 return ['-L', '%d:127.0.0.1:%d' % (self._src_port, self._dst_port)]
182
183 def _RunSSHCmd(self):
184 """Runs the SSH command, storing the exception on failure."""
185 try:
186 cmd = [
187 'ssh',
188 '-o', 'StrictHostKeyChecking=no',
189 '-o', 'GlobalKnownHostsFile=/dev/null',
190 '-o', 'UserKnownHostsFile=/dev/null',
191 '-o', 'ExitOnForwardFailure=yes',
192 '-o', 'ConnectTimeout=%d' % self._connect_timeout,
193 '-o', 'ServerAliveInterval=%d' % self._alive_interval,
194 '-o', 'ServerAliveCountMax=1',
195 '-o', 'TCPKeepAlive=yes',
196 '-o', 'BatchMode=yes',
197 '-i', self._identity_file,
198 '-N',
199 '-p', str(self._port),
200 '%s@%s' % (self._user, self._host),
201 ] + self._ForwardArgs()
202 logging.info(' '.join(cmd))
203 self._ssh_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
204 except subprocess.CalledProcessError as e:
205 self._exception = e
206 finally:
207 pass
208
209 def _Run(self, disconnect_wait, retries):
210 """Wraps around the SSH command, detecting its connection status."""
211 assert retries > 0, '%s: _Run must be called with retries > 0' % self
212
213 logging.info('%s: Connecting to %s:%d',
214 self, self._host, self._port)
215
216 # Set identity file permissions. Need to only be user-readable for ssh to
217 # use the key.
218 try:
219 os.chmod(self._identity_file, 0600)
220 except OSError as e:
221 logging.error('%s: Error setting identity file permissions: %s',
222 self, e)
223 self._state = self.FAILED
224 return
225
226 # Start a thread. If it fails, deal with the failure. If it is still
227 # running after connect_timeout seconds, assume everything's working great,
228 # and tell the caller. Then, continue waiting for it to end.
229 self._ssh_thread = threading.Thread(target=self._RunSSHCmd)
230 self._ssh_thread.daemon = True
231 self._ssh_thread.start()
232
233 # See if the SSH thread is still working after connect_timeout.
234 self._ssh_thread.join(self._connect_timeout)
235 if self._ssh_thread.is_alive():
236 # Assumed to be working. Tell our caller that we are connected.
237 if self._state != self.INITIALIZED:
238 self._state = self.INITIALIZED
239 self._poll.set()
240 logging.info('%s: Still connected after timeout=%ds',
241 self, self._connect_timeout)
242
243 # Only for debug purposes. Keep showing connection status.
244 while self._ssh_thread.is_alive():
245 logging.debug('%s: Still connected', self)
246 self._ssh_thread.join(self._DEBUG_INTERVAL)
247
248 # Figure out what went wrong.
249 if not self._exception:
250 logging.info('%s: SSH unexpectedly exited: %s',
251 self, self._ssh_output.rstrip())
252 if self._exception and self._FAILED_STR in self._exception.output:
253 self._state = self.FAILED
254 self._poll.set()
255 logging.info('%s: Port forwarding failed', self)
256 return
257 elif retries == 1:
258 self._state = self.FAILED
259 self._poll.set()
260 logging.info('%s: Disconnected (0 retries left)', self)
261 return
262 else:
263 logging.info('%s: Disconnected, retrying (sleep %1ds, %d retries left)',
264 self, disconnect_wait, retries - 1)
265 time.sleep(disconnect_wait)
266 self._Run(disconnect_wait=disconnect_wait * (2 ** self._exp_factor),
267 retries=retries - 1)
268
269 def GetState(self):
270 """Returns the current connection state.
271
272 State may be one of:
273
274 CONNECTING: Still attempting to make the first successful connection.
275 INITIALIZED: Is either connected or is trying to make subsequent
276 connection.
277 FAILED: Has completed all connection attempts, or server has reported that
278 target port is in use.
279 """
280 return self._state
281
282 def GetDstPort(self):
283 """Returns the current target port."""
284 return self._dst_port
285
286 def Wait(self):
287 """Waits for a state change, and returns the new state."""
288 self._poll.wait()
289 self._poll.clear()
290 return self.GetState()
291
292
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800293class Ghost(object):
294 """Ghost implements the client protocol of Overlord.
295
296 Ghost provide terminal/shell/logcat functionality and manages the client
297 side connectivity.
298 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800299 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE = range(6)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800300
301 MODE_NAME = {
302 NONE: 'NONE',
303 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800304 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800305 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800306 LOGCAT: 'Logcat',
307 FILE: 'File'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800308 }
309
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800310 RANDOM_MID = '##random_mid##'
311
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800312 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800313 terminal_sid=None, tty_device=None, command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800314 """Constructor.
315
316 Args:
317 overlord_addrs: a list of possible address of overlord.
318 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800319 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
320 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800321 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800322 be set to the corresponding session id assigned by overlord.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800323 terminal_sid: the terminal session ID associate with this client. This is
324 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800325 tty_device: the terminal device to open, if tty_device is None, as pseudo
326 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800327 command: the command to execute when we are in SHELL mode.
Wei-Ning Huange2981862015-08-03 15:03:08 +0800328 file_op: a tuple (action, filepath, pid). action is either 'download' or
329 'upload'. pid is the pid of the target shell, used to determine where
330 the current working is and thus where to upload to.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800331 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800332 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800333 if mode == Ghost.SHELL:
334 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800335 if mode == Ghost.FILE:
336 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800337
338 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800339 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800340 self._mode = mode
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800341 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800342 self._sock = None
343 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800344 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800345 self._terminal_session_id = terminal_sid
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800346 self._properties = {}
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800347 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800348 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800349 self._file_op = file_op
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800350 self._buf = ''
351 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800352 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800353 self._last_ping = 0
354 self._queue = Queue.Queue()
Joel Kitching22b89042015-08-06 18:23:29 +0800355 self._forward_ssh = False
356 self._ssh_port_forwarder = None
357 self._target_identity_file = os.path.join(os.path.dirname(
358 os.path.abspath(os.path.realpath(__file__))), 'ghost_rsa')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800359 self._download_queue = Queue.Queue()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800360 self._ttyname_to_sid = {}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800361 self._terminal_sid_to_pid = {}
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800362
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800363 def SetIgnoreChild(self, status):
364 # Only ignore child for Agent since only it could spawn child Ghost.
365 if self._mode == Ghost.AGENT:
366 signal.signal(signal.SIGCHLD,
367 signal.SIG_IGN if status else signal.SIG_DFL)
368
369 def GetFileSha1(self, filename):
370 with open(filename, 'r') as f:
371 return hashlib.sha1(f.read()).hexdigest()
372
Wei-Ning Huang58833882015-09-16 16:52:37 +0800373 def UseSSL(self):
374 """Determine if SSL is enabled on the Overlord server."""
375 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
376 try:
377 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
378 sock.send('GET\r\n')
379
380 data = sock.recv(16)
381 return 'HTTP' not in data
382 except Exception:
383 return False # For whatever reason above failed, assume HTTP
384
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800385 def Upgrade(self):
386 logging.info('Upgrade: initiating upgrade sequence...')
387
388 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800389 url = 'http%s://%s:%d/upgrade/ghost.py' % (
390 's' if self.UseSSL() else '', self._connected_addr[0],
391 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800392
393 # Download sha1sum for ghost.py for verification
394 try:
395 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
396 if f.getcode() != 200:
397 raise RuntimeError('HTTP status %d' % f.getcode())
398 sha1sum = f.read().strip()
399 except Exception:
400 logging.error('Upgrade: failed to download sha1sum file, abort')
401 return
402
403 if self.GetFileSha1(scriptpath) == sha1sum:
404 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
405 return
406
407 # Download upgrade version of ghost.py
408 try:
409 with contextlib.closing(urllib.urlopen(url)) as f:
410 if f.getcode() != 200:
411 raise RuntimeError('HTTP status %d' % f.getcode())
412 data = f.read()
413 except Exception:
414 logging.error('Upgrade: failed to download upgrade, abort')
415 return
416
417 # Compare SHA1 sum
418 if hashlib.sha1(data).hexdigest() != sha1sum:
419 logging.error('Upgrade: sha1sum mismatch, abort')
420 return
421
422 python = os.readlink('/proc/self/exe')
423 try:
424 with open(scriptpath, 'w') as f:
425 f.write(data)
426 except Exception:
427 logging.error('Upgrade: failed to write upgrade onto disk, abort')
428 return
429
430 logging.info('Upgrade: restarting ghost...')
431 self.CloseSockets()
432 self.SetIgnoreChild(False)
433 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
434
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800435 def LoadPropertiesFromFile(self, filename):
436 try:
437 with open(filename, 'r') as f:
438 self._properties = json.loads(f.read())
439 except Exception as e:
440 logging.exception('LoadPropertiesFromFile: ' + str(e))
441
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800442 def CloseSockets(self):
443 # Close sockets opened by parent process, since we don't use it anymore.
444 for fd in os.listdir('/proc/self/fd/'):
445 try:
446 real_fd = os.readlink('/proc/self/fd/%s' % fd)
447 if real_fd.startswith('socket'):
448 os.close(int(fd))
449 except Exception:
450 pass
451
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800452 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
453 command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800454 """Spawn a child ghost with specific mode.
455
456 Returns:
457 The spawned child process pid.
458 """
Joel Kitching22b89042015-08-06 18:23:29 +0800459 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800460 self.SetIgnoreChild(False)
461
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800462 pid = os.fork()
463 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800464 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800465 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800466 terminal_sid, tty_device, command, file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800467 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800468 sys.exit(0)
469 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800470 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800471 return pid
472
473 def Timestamp(self):
474 return int(time.time())
475
476 def GetGateWayIP(self):
477 with open('/proc/net/route', 'r') as f:
478 lines = f.readlines()
479
480 ips = []
481 for line in lines:
482 parts = line.split('\t')
483 if parts[2] == '00000000':
484 continue
485
486 try:
487 h = parts[2].decode('hex')
488 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
489 except TypeError:
490 pass
491
492 return ips
493
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800494 def GetShopfloorIP(self):
495 try:
496 import factory_common # pylint: disable=W0612
497 from cros.factory.test import shopfloor
498
499 url = shopfloor.get_server_url()
500 match = re.match(r'^https?://(.*):.*$', url)
501 if match:
502 return [match.group(1)]
503 except Exception:
504 pass
505 return []
506
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800507 def GetMachineID(self):
508 """Generates machine-dependent ID string for a machine.
509 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800510 1. factory device_id
511 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800512 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
513 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800514 We follow the listed order to generate machine ID, and fallback to the next
515 alternative if the previous doesn't work.
516 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800517 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800518 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800519 elif self._mid:
520 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800521
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800522 # Try factory device id
523 try:
524 import factory_common # pylint: disable=W0612
525 from cros.factory.test import event_log
526 with open(event_log.DEVICE_ID_PATH) as f:
527 return f.read().strip()
528 except Exception:
529 pass
530
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800531 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532 try:
533 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
534 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800535 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800536 stdout, _ = p.communicate()
537 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800538 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800539 return stdout.strip()
540 except Exception:
541 pass
542
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800543 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800544 try:
545 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
546 return f.read().strip()
547 except Exception:
548 pass
549
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800550 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800551 try:
552 macs = []
553 ifaces = sorted(os.listdir('/sys/class/net'))
554 for iface in ifaces:
555 if iface == 'lo':
556 continue
557
558 with open('/sys/class/net/%s/address' % iface, 'r') as f:
559 macs.append(f.read().strip())
560
561 return ';'.join(macs)
562 except Exception:
563 pass
564
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800565 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800566
567 def Reset(self):
568 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800569 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800570 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800571 self._last_ping = 0
572 self._requests = {}
573
574 def SendMessage(self, msg):
575 """Serialize the message and send it through the socket."""
576 self._sock.send(json.dumps(msg) + _SEPARATOR)
577
578 def SendRequest(self, name, args, handler=None,
579 timeout=_REQUEST_TIMEOUT_SECS):
580 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800581 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800582
583 rid = str(uuid.uuid4())
584 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800585 if timeout >= 0:
586 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800587 self.SendMessage(msg)
588
589 def SendResponse(self, omsg, status, params=None):
590 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
591 self.SendMessage(msg)
592
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800593 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800594 msg = json.loads(control_string)
595 command = msg['command']
596 params = msg['params']
597 if command == 'resize':
598 # some error happened on websocket
599 if len(params) != 2:
600 return
601 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
602 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
603 else:
604 logging.warn('Invalid request command "%s"', command)
605
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800606 def SpawnTTYServer(self, _):
607 """Spawn a TTY server and forward I/O to the TCP socket."""
608 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800609
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800610 try:
611 if self._tty_device is None:
612 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800613
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800614 if pid == 0:
615 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
616 try:
617 server = GhostRPCServer()
618 server.RegisterTTY(self._session_id, ttyname)
619 server.RegisterSession(self._session_id, os.getpid())
620 except Exception:
621 # If ghost is launched without RPC server, the call will fail but we
622 # can ignore it.
623 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800624
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800625 # The directory that contains the current running ghost script
626 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800627
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800628 env = os.environ.copy()
629 env['USER'] = os.getenv('USER', 'root')
630 env['HOME'] = os.getenv('HOME', '/root')
631 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
632 os.chdir(env['HOME'])
633 os.execve(_SHELL, [_SHELL], env)
634 else:
635 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800636
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800637 control_state = None
638 control_string = ''
639 write_buffer = ''
640 while True:
641 rd, _, _ = select.select([self._sock, fd], [], [])
642
643 if fd in rd:
644 self._sock.send(os.read(fd, _BUFSIZE))
645
646 if self._sock in rd:
647 ret = self._sock.recv(_BUFSIZE)
648 if len(ret) == 0:
649 raise RuntimeError('socket closed')
650 while ret:
651 if control_state:
652 if chr(_CONTROL_END) in ret:
653 index = ret.index(chr(_CONTROL_END))
654 control_string += ret[:index]
655 self.HandleTTYControl(fd, control_string)
656 control_state = None
657 control_string = ''
658 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800659 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800660 control_string += ret
661 ret = ''
662 else:
663 if chr(_CONTROL_START) in ret:
664 control_state = _CONTROL_START
665 index = ret.index(chr(_CONTROL_START))
666 write_buffer += ret[:index]
667 ret = ret[index+1:]
668 else:
669 write_buffer += ret
670 ret = ''
671 if write_buffer:
672 os.write(fd, write_buffer)
673 write_buffer = ''
674 except (OSError, socket.error, RuntimeError) as e:
675 self._sock.close()
676 logging.info('SpawnTTYServer: %s' % str(e))
677 logging.info('SpawnTTYServer: terminated')
678 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800679
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800680 def SpawnShellServer(self, _):
681 """Spawn a shell server and forward input/output from/to the TCP socket."""
682 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800683
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800684 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800685 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
686 shell=True)
687
688 def make_non_block(fd):
689 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
690 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
691
692 make_non_block(p.stdout)
693 make_non_block(p.stderr)
694
695 try:
696 while True:
697 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800698 if p.stdout in rd:
699 self._sock.send(p.stdout.read(_BUFSIZE))
700
701 if p.stderr in rd:
702 self._sock.send(p.stderr.read(_BUFSIZE))
703
704 if self._sock in rd:
705 ret = self._sock.recv(_BUFSIZE)
706 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800707 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800708 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800709 p.poll()
710 if p.returncode != None:
711 break
712 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800713 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800714 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800715 sys.exit(0)
716
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800717 def InitiateFileOperation(self, _):
718 if self._file_op[0] == 'download':
719 size = os.stat(self._file_op[1]).st_size
720 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800721 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800722 'filename': os.path.basename(self._file_op[1]),
723 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800724 elif self._file_op[0] == 'upload':
725 self.SendRequest('clear_to_upload', {}, timeout=-1)
726 self.StartUploadServer()
727 else:
728 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800729
730 def StartDownloadServer(self):
731 logging.info('StartDownloadServer: started')
732
733 try:
734 with open(self._file_op[1], 'rb') as f:
735 while True:
736 data = f.read(_BLOCK_SIZE)
737 if len(data) == 0:
738 break
739 self._sock.send(data)
740 except Exception as e:
741 logging.error('StartDownloadServer: %s', e)
742 finally:
743 self._sock.close()
744
745 logging.info('StartDownloadServer: terminated')
746 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800747
Wei-Ning Huange2981862015-08-03 15:03:08 +0800748 def StartUploadServer(self):
749 logging.info('StartUploadServer: started')
750
751 try:
752 target_dir = os.getenv('HOME', '/tmp')
753
754 # Get the client's working dir, which is our target upload dir
755 if self._file_op[2]:
756 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
757
758 self._sock.setblocking(False)
759 with open(os.path.join(target_dir, self._file_op[1]), 'wb') as f:
760 while True:
761 rd, _, _ = select.select([self._sock], [], [])
762 if self._sock in rd:
763 buf = self._sock.recv(_BLOCK_SIZE)
764 if len(buf) == 0:
765 break
766 f.write(buf)
767 except socket.error as e:
768 logging.error('StartUploadServer: socket error: %s', e)
769 except Exception as e:
770 logging.error('StartUploadServer: %s', e)
771 finally:
772 self._sock.close()
773
774 logging.info('StartUploadServer: terminated')
775 sys.exit(0)
776
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800777 def Ping(self):
778 def timeout_handler(x):
779 if x is None:
780 raise PingTimeoutError
781
782 self._last_ping = self.Timestamp()
783 self.SendRequest('ping', {}, timeout_handler, 5)
784
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800785 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800786 command = msg['name']
787 params = msg['params']
788
789 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800790 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800791 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800792 self.SpawnGhost(self.TERMINAL, params['sid'],
793 tty_device=params['tty_device'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800794 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800795 elif command == 'shell':
796 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800797 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800798 elif command == 'file_download':
799 self.SpawnGhost(self.FILE, params['sid'],
800 file_op=('download', params['filename'], None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800801 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800802 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800803 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800804 elif command == 'file_upload':
805 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
806 self.SpawnGhost(self.FILE, params['sid'],
807 file_op=('upload', params['filename'], pid))
808 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800809
810 def HandleResponse(self, response):
811 rid = str(response['rid'])
812 if rid in self._requests:
813 handler = self._requests[rid][2]
814 del self._requests[rid]
815 if callable(handler):
816 handler(response)
817 else:
818 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800819 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800820
821 def ParseMessage(self):
822 msgs_json = self._buf.split(_SEPARATOR)
823 self._buf = msgs_json.pop()
824
825 for msg_json in msgs_json:
826 try:
827 msg = json.loads(msg_json)
828 except ValueError:
829 # Ignore mal-formed message.
830 continue
831
832 if 'name' in msg:
833 self.HandleRequest(msg)
834 elif 'response' in msg:
835 self.HandleResponse(msg)
836 else: # Ingnore mal-formed message.
837 pass
838
839 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800840 """Scans for pending requests which have timed out.
841
842 If any timed-out requests are discovered, their handler is called with the
843 special response value of None.
844 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800845 for rid in self._requests.keys()[:]:
846 request_time, timeout, handler = self._requests[rid]
847 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800848 if callable(handler):
849 handler(None)
850 else:
851 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800852 del self._requests[rid]
853
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800854 def InitiateDownload(self):
855 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800856 sid = self._ttyname_to_sid[ttyname]
857 self.SpawnGhost(self.FILE, terminal_sid=sid,
858 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800859
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800860 def Listen(self):
861 try:
862 while True:
863 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
864
865 if self._sock in rds:
866 self._buf += self._sock.recv(_BUFSIZE)
867 self.ParseMessage()
868
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800869 if (self._mode == self.AGENT and
870 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800871 self.Ping()
872 self.ScanForTimeoutRequests()
873
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800874 if not self._download_queue.empty():
875 self.InitiateDownload()
876
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800877 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800878 self.Reset()
879 break
880 except socket.error:
881 raise RuntimeError('Connection dropped')
882 except PingTimeoutError:
883 raise RuntimeError('Connection timeout')
884 finally:
885 self._sock.close()
886
887 self._queue.put('resume')
888
889 if self._mode != Ghost.AGENT:
890 sys.exit(1)
891
892 def Register(self):
893 non_local = {}
894 for addr in self._overlord_addrs:
895 non_local['addr'] = addr
896 def registered(response):
897 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800898 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800899 raise RuntimeError('Register request timeout')
900 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Joel Kitching22b89042015-08-06 18:23:29 +0800901 if self._forward_ssh:
902 logging.info('Starting target SSH port negotiation')
903 self.NegotiateTargetSSHPort()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800904 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800905
906 try:
907 logging.info('Trying %s:%d ...', *addr)
908 self.Reset()
909 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
910 self._sock.settimeout(_PING_TIMEOUT)
911 self._sock.connect(addr)
912
913 logging.info('Connection established, registering...')
914 handler = {
915 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800916 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800917 Ghost.SHELL: self.SpawnShellServer,
918 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800919 }[self._mode]
920
921 # Machine ID may change if MAC address is used (USB-ethernet dongle
922 # plugged/unplugged)
923 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800924 self.SendRequest('register',
925 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800926 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800927 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800928 except socket.error:
929 pass
930 else:
931 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800932 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800933 self.Listen()
934
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800935 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800936
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800937 def Reconnect(self):
938 logging.info('Received reconnect request from RPC server, reconnecting...')
939 self._reset.set()
940
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800941 def AddToDownloadQueue(self, ttyname, filename):
942 self._download_queue.put((ttyname, filename))
943
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800944 def RegisterTTY(self, session_id, ttyname):
945 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800946
947 def RegisterSession(self, session_id, process_id):
948 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800949
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800950 def StartLanDiscovery(self):
951 """Start to listen to LAN discovery packet at
952 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800953
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800954 def thread_func():
955 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
956 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
957 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800958 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800959 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
960 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800961 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800962 return
963
964 logging.info('LAN Discovery: started')
965 while True:
966 rd, _, _ = select.select([s], [], [], 1)
967
968 if s in rd:
969 data, source_addr = s.recvfrom(_BUFSIZE)
970 parts = data.split()
971 if parts[0] == 'OVERLORD':
972 ip, port = parts[1].split(':')
973 if not ip:
974 ip = source_addr[0]
975 self._queue.put((ip, int(port)), True)
976
977 try:
978 obj = self._queue.get(False)
979 except Queue.Empty:
980 pass
981 else:
982 if type(obj) is not str:
983 self._queue.put(obj)
984 elif obj == 'pause':
985 logging.info('LAN Discovery: paused')
986 while obj != 'resume':
987 obj = self._queue.get(True)
988 logging.info('LAN Discovery: resumed')
989
990 t = threading.Thread(target=thread_func)
991 t.daemon = True
992 t.start()
993
994 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800995 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800996 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
997 logRequests=False)
998 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800999 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001000 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001001 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001002 t = threading.Thread(target=rpc_server.serve_forever)
1003 t.daemon = True
1004 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001005
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001006 def ScanServer(self):
1007 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1008 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1009 if addr not in self._overlord_addrs:
1010 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001011
Joel Kitching22b89042015-08-06 18:23:29 +08001012 def NegotiateTargetSSHPort(self):
1013 """Request-receive target SSH port forwarding loop.
1014
1015 Repeatedly attempts to forward this machine's SSH port to target. It
1016 bounces back and forth between RequestPort and ReceivePort when a new port
1017 is required. ReceivePort starts a new thread so that the main ghost thread
1018 may continue running.
1019 """
1020 # Sanity check for identity file.
1021 if not os.path.isfile(self._target_identity_file):
1022 logging.info('No target host identity file: not negotiating '
1023 'target SSH port')
1024 return
1025
1026 def PollSSHPortForwarder():
1027 def ThreadFunc():
1028 while True:
1029 state = self._ssh_port_forwarder.GetState()
1030
1031 # Connected successfully.
1032 if state == SSHPortForwarder.INITIALIZED:
1033 # The SSH port forward has succeeded! Let's tell Overlord.
1034 port = self._ssh_port_forwarder.GetDstPort()
1035 RegisterPort(port)
1036
1037 # We've given up... continue to the next port.
1038 elif state == SSHPortForwarder.FAILED:
1039 break
1040
1041 # Either CONNECTING or INITIALIZED.
1042 self._ssh_port_forwarder.Wait()
1043
1044 # Only request a new port if we are still registered to Overlord.
1045 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1046 # which will take care of it.
1047 try:
1048 RequestPort()
1049 except Exception:
1050 logging.info('Failed to request port, will wait for next connection')
1051 self._ssh_port_forwarder = None
1052
1053 t = threading.Thread(target=ThreadFunc)
1054 t.daemon = True
1055 t.start()
1056
1057 def ReceivePort(response):
1058 # If the response times out, this version of Overlord may not support SSH
1059 # port negotiation. Give up on port negotiation process.
1060 if response is None:
1061 return
1062
1063 port = int(response['params']['port'])
1064 logging.info('Received target SSH port: %d', port)
1065
1066 if (self._ssh_port_forwarder and
1067 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1068 logging.info('Unexpectedly received a target SSH port')
1069 return
1070
1071 # Try forwarding SSH port to target.
1072 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1073 src_port=22,
1074 dst_port=port,
1075 user='ghost',
1076 identity_file=self._target_identity_file,
1077 host=self._connected_addr[0]) # Use Overlord host as target.
1078
1079 # Creates a new thread.
1080 PollSSHPortForwarder()
1081
1082 def RequestPort():
1083 logging.info('Requesting new target SSH port')
1084 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1085
1086 def RegisterPort(port):
1087 logging.info('Registering target SSH port %d', port)
1088 self.SendRequest(
1089 'register_target_ssh_port',
1090 {'port': port}, RegisterPortResponse, 5)
1091
1092 def RegisterPortResponse(response):
1093 # Overlord responded to request_port already. If register_port fails,
1094 # something might be in an inconsistent state, so trigger a reconnect
1095 # via PingTimeoutError.
1096 if response is None:
1097 raise PingTimeoutError
1098 logging.info('Registering target SSH port acknowledged')
1099
1100 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1101 # manually report the port to target, since SSHPortForwarder is currently
1102 # blocking.
1103 if (self._ssh_port_forwarder and
1104 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1105 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1106 if not self._ssh_port_forwarder:
1107 RequestPort()
1108
1109 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001110 logging.info('%s started', self.MODE_NAME[self._mode])
1111 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001112 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001113
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001114 # We don't care about child process's return code, not wait is needed. This
1115 # is used to prevent zombie process from lingering in the system.
1116 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001117
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001118 if lan_disc:
1119 self.StartLanDiscovery()
1120
1121 if rpc_server:
1122 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001123
Joel Kitching22b89042015-08-06 18:23:29 +08001124 self._forward_ssh = forward_ssh
1125
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001126 try:
1127 while True:
1128 try:
1129 addr = self._queue.get(False)
1130 except Queue.Empty:
1131 pass
1132 else:
1133 if type(addr) == tuple and addr not in self._overlord_addrs:
1134 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1135 self._overlord_addrs.append(addr)
1136
1137 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001138 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001139 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001140 # Don't show stack trace for RuntimeError, which we use in this file for
1141 # plausible and expected errors (such as can't connect to server).
1142 except RuntimeError as e:
1143 logging.info('%s: %s, retrying in %ds',
1144 e.__class__.__name__, e.message, _RETRY_INTERVAL)
1145 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001146 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001147 _, _, exc_traceback = sys.exc_info()
1148 traceback.print_tb(exc_traceback)
1149 logging.info('%s: %s, retrying in %ds',
1150 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001151 time.sleep(_RETRY_INTERVAL)
1152
1153 self.Reset()
1154 except KeyboardInterrupt:
1155 logging.error('Received keyboard interrupt, quit')
1156 sys.exit(0)
1157
1158
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001159def GhostRPCServer():
1160 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1161
1162
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001163def DownloadFile(filename):
1164 filepath = os.path.abspath(filename)
1165 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001166 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001167 sys.exit(1)
1168
1169 # Check if we actually have permission to read the file
1170 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001171 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001172 sys.exit(1)
1173
1174 server = GhostRPCServer()
1175 server.AddToDownloadQueue(os.ttyname(0), filepath)
1176 sys.exit(0)
1177
1178
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001179def main():
1180 logger = logging.getLogger()
1181 logger.setLevel(logging.INFO)
1182
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001183 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001184 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1185 default=None, help='use MID as machine ID')
1186 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1187 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001188 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1189 default=True, help='disable LAN discovery')
1190 parser.add_argument('--no-rpc-server', dest='rpc_server',
1191 action='store_false', default=True,
1192 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001193 parser.add_argument('--forward-ssh', dest='forward_ssh',
1194 action='store_true', default=False,
1195 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001196 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001197 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001198 help='file containing the JSON representation of client '
1199 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001200 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1201 default=None, help='file to download')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001202 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1203 nargs='*', help='overlord server address')
1204 args = parser.parse_args()
1205
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001206 if args.download:
1207 DownloadFile(args.download)
1208
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001209 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001210 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001211
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001212 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001213 if args.prop_file:
1214 g.LoadPropertiesFromFile(args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001215 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1216
1217
1218def _SigtermHandler(*_):
1219 """Ensure that SSH processes also get killed on a sigterm signal.
1220
1221 By also passing the sigterm signal onto the process group, we ensure that any
1222 child SSH processes will also get killed.
1223
1224 Source:
1225 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1226 """
1227 logging.info('SIGTERM handler: shutting down')
1228 if not _SigtermHandler.SIGTERM_SENT:
1229 _SigtermHandler.SIGTERM_SENT = True
1230 logging.info('Sending TERM to process group')
1231 os.killpg(0, signal.SIGTERM)
1232 sys.exit()
1233_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001234
1235
1236if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001237 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001238 main()