blob: 94af7c173bf5b8162aa6eab7b8dcfb82f6b5f7c7 [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 Huang23ed0162015-09-18 14:42:03 +0800313 prop_file=None, terminal_sid=None, tty_device=None,
314 command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800315 """Constructor.
316
317 Args:
318 overlord_addrs: a list of possible address of overlord.
319 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800320 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
321 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800322 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800323 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800324 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800325 terminal_sid: the terminal session ID associate with this client. This is
326 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800327 tty_device: the terminal device to open, if tty_device is None, as pseudo
328 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800329 command: the command to execute when we are in SHELL mode.
Wei-Ning Huange2981862015-08-03 15:03:08 +0800330 file_op: a tuple (action, filepath, pid). action is either 'download' or
331 'upload'. pid is the pid of the target shell, used to determine where
332 the current working is and thus where to upload to.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800333 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800334 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800335 if mode == Ghost.SHELL:
336 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800337 if mode == Ghost.FILE:
338 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800339
340 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800341 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800342 self._mode = mode
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800343 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800344 self._sock = None
345 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800346 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800347 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800348 self._ttyname_to_sid = {}
349 self._terminal_sid_to_pid = {}
350 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800351 self._properties = {}
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800352 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800353 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800354 self._file_op = file_op
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800355 self._buf = ''
356 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800357 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800358 self._last_ping = 0
359 self._queue = Queue.Queue()
Joel Kitching22b89042015-08-06 18:23:29 +0800360 self._forward_ssh = False
361 self._ssh_port_forwarder = None
362 self._target_identity_file = os.path.join(os.path.dirname(
363 os.path.abspath(os.path.realpath(__file__))), 'ghost_rsa')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800364 self._download_queue = Queue.Queue()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800365
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800366 def SetIgnoreChild(self, status):
367 # Only ignore child for Agent since only it could spawn child Ghost.
368 if self._mode == Ghost.AGENT:
369 signal.signal(signal.SIGCHLD,
370 signal.SIG_IGN if status else signal.SIG_DFL)
371
372 def GetFileSha1(self, filename):
373 with open(filename, 'r') as f:
374 return hashlib.sha1(f.read()).hexdigest()
375
Wei-Ning Huang58833882015-09-16 16:52:37 +0800376 def UseSSL(self):
377 """Determine if SSL is enabled on the Overlord server."""
378 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
379 try:
380 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
381 sock.send('GET\r\n')
382
383 data = sock.recv(16)
384 return 'HTTP' not in data
385 except Exception:
386 return False # For whatever reason above failed, assume HTTP
387
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800388 def Upgrade(self):
389 logging.info('Upgrade: initiating upgrade sequence...')
390
391 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800392 url = 'http%s://%s:%d/upgrade/ghost.py' % (
393 's' if self.UseSSL() else '', self._connected_addr[0],
394 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800395
396 # Download sha1sum for ghost.py for verification
397 try:
398 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
399 if f.getcode() != 200:
400 raise RuntimeError('HTTP status %d' % f.getcode())
401 sha1sum = f.read().strip()
402 except Exception:
403 logging.error('Upgrade: failed to download sha1sum file, abort')
404 return
405
406 if self.GetFileSha1(scriptpath) == sha1sum:
407 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
408 return
409
410 # Download upgrade version of ghost.py
411 try:
412 with contextlib.closing(urllib.urlopen(url)) as f:
413 if f.getcode() != 200:
414 raise RuntimeError('HTTP status %d' % f.getcode())
415 data = f.read()
416 except Exception:
417 logging.error('Upgrade: failed to download upgrade, abort')
418 return
419
420 # Compare SHA1 sum
421 if hashlib.sha1(data).hexdigest() != sha1sum:
422 logging.error('Upgrade: sha1sum mismatch, abort')
423 return
424
425 python = os.readlink('/proc/self/exe')
426 try:
427 with open(scriptpath, 'w') as f:
428 f.write(data)
429 except Exception:
430 logging.error('Upgrade: failed to write upgrade onto disk, abort')
431 return
432
433 logging.info('Upgrade: restarting ghost...')
434 self.CloseSockets()
435 self.SetIgnoreChild(False)
436 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
437
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800438 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800439 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800440 if self._prop_file:
441 with open(self._prop_file, 'r') as f:
442 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800443 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800444 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800445
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800446 def CloseSockets(self):
447 # Close sockets opened by parent process, since we don't use it anymore.
448 for fd in os.listdir('/proc/self/fd/'):
449 try:
450 real_fd = os.readlink('/proc/self/fd/%s' % fd)
451 if real_fd.startswith('socket'):
452 os.close(int(fd))
453 except Exception:
454 pass
455
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800456 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
457 command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800458 """Spawn a child ghost with specific mode.
459
460 Returns:
461 The spawned child process pid.
462 """
Joel Kitching22b89042015-08-06 18:23:29 +0800463 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800464 self.SetIgnoreChild(False)
465
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800466 pid = os.fork()
467 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800468 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800469 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800470 terminal_sid=terminal_sid, tty_device=tty_device,
471 command=command, file_op=file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800472 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800473 sys.exit(0)
474 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800475 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800476 return pid
477
478 def Timestamp(self):
479 return int(time.time())
480
481 def GetGateWayIP(self):
482 with open('/proc/net/route', 'r') as f:
483 lines = f.readlines()
484
485 ips = []
486 for line in lines:
487 parts = line.split('\t')
488 if parts[2] == '00000000':
489 continue
490
491 try:
492 h = parts[2].decode('hex')
493 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
494 except TypeError:
495 pass
496
497 return ips
498
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800499 def GetShopfloorIP(self):
500 try:
501 import factory_common # pylint: disable=W0612
502 from cros.factory.test import shopfloor
503
504 url = shopfloor.get_server_url()
505 match = re.match(r'^https?://(.*):.*$', url)
506 if match:
507 return [match.group(1)]
508 except Exception:
509 pass
510 return []
511
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800512 def GetMachineID(self):
513 """Generates machine-dependent ID string for a machine.
514 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800515 1. factory device_id
516 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800517 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
518 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800519 We follow the listed order to generate machine ID, and fallback to the next
520 alternative if the previous doesn't work.
521 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800522 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800523 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800524 elif self._mid:
525 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800526
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800527 # Try factory device id
528 try:
529 import factory_common # pylint: disable=W0612
530 from cros.factory.test import event_log
531 with open(event_log.DEVICE_ID_PATH) as f:
532 return f.read().strip()
533 except Exception:
534 pass
535
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800536 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800537 try:
538 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
539 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800540 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800541 stdout, _ = p.communicate()
542 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800543 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800544 return stdout.strip()
545 except Exception:
546 pass
547
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800548 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800549 try:
550 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
551 return f.read().strip()
552 except Exception:
553 pass
554
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800555 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800556 try:
557 macs = []
558 ifaces = sorted(os.listdir('/sys/class/net'))
559 for iface in ifaces:
560 if iface == 'lo':
561 continue
562
563 with open('/sys/class/net/%s/address' % iface, 'r') as f:
564 macs.append(f.read().strip())
565
566 return ';'.join(macs)
567 except Exception:
568 pass
569
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800570 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800571
572 def Reset(self):
573 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800574 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800575 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800576 self._last_ping = 0
577 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800578 self.LoadProperties()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800579
580 def SendMessage(self, msg):
581 """Serialize the message and send it through the socket."""
582 self._sock.send(json.dumps(msg) + _SEPARATOR)
583
584 def SendRequest(self, name, args, handler=None,
585 timeout=_REQUEST_TIMEOUT_SECS):
586 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800587 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800588
589 rid = str(uuid.uuid4())
590 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800591 if timeout >= 0:
592 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800593 self.SendMessage(msg)
594
595 def SendResponse(self, omsg, status, params=None):
596 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
597 self.SendMessage(msg)
598
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800599 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800600 msg = json.loads(control_string)
601 command = msg['command']
602 params = msg['params']
603 if command == 'resize':
604 # some error happened on websocket
605 if len(params) != 2:
606 return
607 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
608 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
609 else:
610 logging.warn('Invalid request command "%s"', command)
611
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800612 def SpawnTTYServer(self, _):
613 """Spawn a TTY server and forward I/O to the TCP socket."""
614 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800615
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800616 try:
617 if self._tty_device is None:
618 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800619
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800620 if pid == 0:
621 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
622 try:
623 server = GhostRPCServer()
624 server.RegisterTTY(self._session_id, ttyname)
625 server.RegisterSession(self._session_id, os.getpid())
626 except Exception:
627 # If ghost is launched without RPC server, the call will fail but we
628 # can ignore it.
629 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800630
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800631 # The directory that contains the current running ghost script
632 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800633
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800634 env = os.environ.copy()
635 env['USER'] = os.getenv('USER', 'root')
636 env['HOME'] = os.getenv('HOME', '/root')
637 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
638 os.chdir(env['HOME'])
639 os.execve(_SHELL, [_SHELL], env)
640 else:
641 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800642
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800643 control_state = None
644 control_string = ''
645 write_buffer = ''
646 while True:
647 rd, _, _ = select.select([self._sock, fd], [], [])
648
649 if fd in rd:
650 self._sock.send(os.read(fd, _BUFSIZE))
651
652 if self._sock in rd:
653 ret = self._sock.recv(_BUFSIZE)
654 if len(ret) == 0:
655 raise RuntimeError('socket closed')
656 while ret:
657 if control_state:
658 if chr(_CONTROL_END) in ret:
659 index = ret.index(chr(_CONTROL_END))
660 control_string += ret[:index]
661 self.HandleTTYControl(fd, control_string)
662 control_state = None
663 control_string = ''
664 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800665 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800666 control_string += ret
667 ret = ''
668 else:
669 if chr(_CONTROL_START) in ret:
670 control_state = _CONTROL_START
671 index = ret.index(chr(_CONTROL_START))
672 write_buffer += ret[:index]
673 ret = ret[index+1:]
674 else:
675 write_buffer += ret
676 ret = ''
677 if write_buffer:
678 os.write(fd, write_buffer)
679 write_buffer = ''
680 except (OSError, socket.error, RuntimeError) as e:
681 self._sock.close()
682 logging.info('SpawnTTYServer: %s' % str(e))
683 logging.info('SpawnTTYServer: terminated')
684 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800685
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800686 def SpawnShellServer(self, _):
687 """Spawn a shell server and forward input/output from/to the TCP socket."""
688 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800689
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800690 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800691 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
692 shell=True)
693
694 def make_non_block(fd):
695 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
696 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
697
698 make_non_block(p.stdout)
699 make_non_block(p.stderr)
700
701 try:
702 while True:
703 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800704 if p.stdout in rd:
705 self._sock.send(p.stdout.read(_BUFSIZE))
706
707 if p.stderr in rd:
708 self._sock.send(p.stderr.read(_BUFSIZE))
709
710 if self._sock in rd:
711 ret = self._sock.recv(_BUFSIZE)
712 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800713 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800714 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800715 p.poll()
716 if p.returncode != None:
717 break
718 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800719 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800720 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800721 sys.exit(0)
722
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800723 def InitiateFileOperation(self, _):
724 if self._file_op[0] == 'download':
725 size = os.stat(self._file_op[1]).st_size
726 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800727 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800728 'filename': os.path.basename(self._file_op[1]),
729 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800730 elif self._file_op[0] == 'upload':
731 self.SendRequest('clear_to_upload', {}, timeout=-1)
732 self.StartUploadServer()
733 else:
734 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800735
736 def StartDownloadServer(self):
737 logging.info('StartDownloadServer: started')
738
739 try:
740 with open(self._file_op[1], 'rb') as f:
741 while True:
742 data = f.read(_BLOCK_SIZE)
743 if len(data) == 0:
744 break
745 self._sock.send(data)
746 except Exception as e:
747 logging.error('StartDownloadServer: %s', e)
748 finally:
749 self._sock.close()
750
751 logging.info('StartDownloadServer: terminated')
752 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800753
Wei-Ning Huange2981862015-08-03 15:03:08 +0800754 def StartUploadServer(self):
755 logging.info('StartUploadServer: started')
756
757 try:
758 target_dir = os.getenv('HOME', '/tmp')
759
760 # Get the client's working dir, which is our target upload dir
761 if self._file_op[2]:
762 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
763
764 self._sock.setblocking(False)
765 with open(os.path.join(target_dir, self._file_op[1]), 'wb') as f:
766 while True:
767 rd, _, _ = select.select([self._sock], [], [])
768 if self._sock in rd:
769 buf = self._sock.recv(_BLOCK_SIZE)
770 if len(buf) == 0:
771 break
772 f.write(buf)
773 except socket.error as e:
774 logging.error('StartUploadServer: socket error: %s', e)
775 except Exception as e:
776 logging.error('StartUploadServer: %s', e)
777 finally:
778 self._sock.close()
779
780 logging.info('StartUploadServer: terminated')
781 sys.exit(0)
782
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800783 def Ping(self):
784 def timeout_handler(x):
785 if x is None:
786 raise PingTimeoutError
787
788 self._last_ping = self.Timestamp()
789 self.SendRequest('ping', {}, timeout_handler, 5)
790
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800791 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800792 command = msg['name']
793 params = msg['params']
794
795 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800796 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800797 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800798 self.SpawnGhost(self.TERMINAL, params['sid'],
799 tty_device=params['tty_device'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800800 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800801 elif command == 'shell':
802 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800803 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800804 elif command == 'file_download':
805 self.SpawnGhost(self.FILE, params['sid'],
806 file_op=('download', params['filename'], None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800807 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800808 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800809 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800810 elif command == 'file_upload':
811 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
812 self.SpawnGhost(self.FILE, params['sid'],
813 file_op=('upload', params['filename'], pid))
814 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800815
816 def HandleResponse(self, response):
817 rid = str(response['rid'])
818 if rid in self._requests:
819 handler = self._requests[rid][2]
820 del self._requests[rid]
821 if callable(handler):
822 handler(response)
823 else:
824 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800825 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800826
827 def ParseMessage(self):
828 msgs_json = self._buf.split(_SEPARATOR)
829 self._buf = msgs_json.pop()
830
831 for msg_json in msgs_json:
832 try:
833 msg = json.loads(msg_json)
834 except ValueError:
835 # Ignore mal-formed message.
836 continue
837
838 if 'name' in msg:
839 self.HandleRequest(msg)
840 elif 'response' in msg:
841 self.HandleResponse(msg)
842 else: # Ingnore mal-formed message.
843 pass
844
845 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800846 """Scans for pending requests which have timed out.
847
848 If any timed-out requests are discovered, their handler is called with the
849 special response value of None.
850 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800851 for rid in self._requests.keys()[:]:
852 request_time, timeout, handler = self._requests[rid]
853 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800854 if callable(handler):
855 handler(None)
856 else:
857 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800858 del self._requests[rid]
859
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800860 def InitiateDownload(self):
861 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800862 sid = self._ttyname_to_sid[ttyname]
863 self.SpawnGhost(self.FILE, terminal_sid=sid,
864 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800865
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800866 def Listen(self):
867 try:
868 while True:
869 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
870
871 if self._sock in rds:
872 self._buf += self._sock.recv(_BUFSIZE)
873 self.ParseMessage()
874
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800875 if (self._mode == self.AGENT and
876 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800877 self.Ping()
878 self.ScanForTimeoutRequests()
879
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800880 if not self._download_queue.empty():
881 self.InitiateDownload()
882
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800883 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800884 self.Reset()
885 break
886 except socket.error:
887 raise RuntimeError('Connection dropped')
888 except PingTimeoutError:
889 raise RuntimeError('Connection timeout')
890 finally:
891 self._sock.close()
892
893 self._queue.put('resume')
894
895 if self._mode != Ghost.AGENT:
896 sys.exit(1)
897
898 def Register(self):
899 non_local = {}
900 for addr in self._overlord_addrs:
901 non_local['addr'] = addr
902 def registered(response):
903 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800904 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800905 raise RuntimeError('Register request timeout')
906 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Joel Kitching22b89042015-08-06 18:23:29 +0800907 if self._forward_ssh:
908 logging.info('Starting target SSH port negotiation')
909 self.NegotiateTargetSSHPort()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800910 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800911
912 try:
913 logging.info('Trying %s:%d ...', *addr)
914 self.Reset()
915 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
916 self._sock.settimeout(_PING_TIMEOUT)
917 self._sock.connect(addr)
918
919 logging.info('Connection established, registering...')
920 handler = {
921 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800922 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800923 Ghost.SHELL: self.SpawnShellServer,
924 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800925 }[self._mode]
926
927 # Machine ID may change if MAC address is used (USB-ethernet dongle
928 # plugged/unplugged)
929 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800930 self.SendRequest('register',
931 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800932 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800933 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800934 except socket.error:
935 pass
936 else:
937 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800938 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800939 self.Listen()
940
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800941 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800942
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800943 def Reconnect(self):
944 logging.info('Received reconnect request from RPC server, reconnecting...')
945 self._reset.set()
946
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800947 def AddToDownloadQueue(self, ttyname, filename):
948 self._download_queue.put((ttyname, filename))
949
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800950 def RegisterTTY(self, session_id, ttyname):
951 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800952
953 def RegisterSession(self, session_id, process_id):
954 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800955
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800956 def StartLanDiscovery(self):
957 """Start to listen to LAN discovery packet at
958 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800959
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800960 def thread_func():
961 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
962 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
963 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800964 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800965 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
966 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800967 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800968 return
969
970 logging.info('LAN Discovery: started')
971 while True:
972 rd, _, _ = select.select([s], [], [], 1)
973
974 if s in rd:
975 data, source_addr = s.recvfrom(_BUFSIZE)
976 parts = data.split()
977 if parts[0] == 'OVERLORD':
978 ip, port = parts[1].split(':')
979 if not ip:
980 ip = source_addr[0]
981 self._queue.put((ip, int(port)), True)
982
983 try:
984 obj = self._queue.get(False)
985 except Queue.Empty:
986 pass
987 else:
988 if type(obj) is not str:
989 self._queue.put(obj)
990 elif obj == 'pause':
991 logging.info('LAN Discovery: paused')
992 while obj != 'resume':
993 obj = self._queue.get(True)
994 logging.info('LAN Discovery: resumed')
995
996 t = threading.Thread(target=thread_func)
997 t.daemon = True
998 t.start()
999
1000 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +08001001 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001002 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
1003 logRequests=False)
1004 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001005 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001006 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001007 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001008 t = threading.Thread(target=rpc_server.serve_forever)
1009 t.daemon = True
1010 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001011
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001012 def ScanServer(self):
1013 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1014 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1015 if addr not in self._overlord_addrs:
1016 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001017
Joel Kitching22b89042015-08-06 18:23:29 +08001018 def NegotiateTargetSSHPort(self):
1019 """Request-receive target SSH port forwarding loop.
1020
1021 Repeatedly attempts to forward this machine's SSH port to target. It
1022 bounces back and forth between RequestPort and ReceivePort when a new port
1023 is required. ReceivePort starts a new thread so that the main ghost thread
1024 may continue running.
1025 """
1026 # Sanity check for identity file.
1027 if not os.path.isfile(self._target_identity_file):
1028 logging.info('No target host identity file: not negotiating '
1029 'target SSH port')
1030 return
1031
1032 def PollSSHPortForwarder():
1033 def ThreadFunc():
1034 while True:
1035 state = self._ssh_port_forwarder.GetState()
1036
1037 # Connected successfully.
1038 if state == SSHPortForwarder.INITIALIZED:
1039 # The SSH port forward has succeeded! Let's tell Overlord.
1040 port = self._ssh_port_forwarder.GetDstPort()
1041 RegisterPort(port)
1042
1043 # We've given up... continue to the next port.
1044 elif state == SSHPortForwarder.FAILED:
1045 break
1046
1047 # Either CONNECTING or INITIALIZED.
1048 self._ssh_port_forwarder.Wait()
1049
1050 # Only request a new port if we are still registered to Overlord.
1051 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1052 # which will take care of it.
1053 try:
1054 RequestPort()
1055 except Exception:
1056 logging.info('Failed to request port, will wait for next connection')
1057 self._ssh_port_forwarder = None
1058
1059 t = threading.Thread(target=ThreadFunc)
1060 t.daemon = True
1061 t.start()
1062
1063 def ReceivePort(response):
1064 # If the response times out, this version of Overlord may not support SSH
1065 # port negotiation. Give up on port negotiation process.
1066 if response is None:
1067 return
1068
1069 port = int(response['params']['port'])
1070 logging.info('Received target SSH port: %d', port)
1071
1072 if (self._ssh_port_forwarder and
1073 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1074 logging.info('Unexpectedly received a target SSH port')
1075 return
1076
1077 # Try forwarding SSH port to target.
1078 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1079 src_port=22,
1080 dst_port=port,
1081 user='ghost',
1082 identity_file=self._target_identity_file,
1083 host=self._connected_addr[0]) # Use Overlord host as target.
1084
1085 # Creates a new thread.
1086 PollSSHPortForwarder()
1087
1088 def RequestPort():
1089 logging.info('Requesting new target SSH port')
1090 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1091
1092 def RegisterPort(port):
1093 logging.info('Registering target SSH port %d', port)
1094 self.SendRequest(
1095 'register_target_ssh_port',
1096 {'port': port}, RegisterPortResponse, 5)
1097
1098 def RegisterPortResponse(response):
1099 # Overlord responded to request_port already. If register_port fails,
1100 # something might be in an inconsistent state, so trigger a reconnect
1101 # via PingTimeoutError.
1102 if response is None:
1103 raise PingTimeoutError
1104 logging.info('Registering target SSH port acknowledged')
1105
1106 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1107 # manually report the port to target, since SSHPortForwarder is currently
1108 # blocking.
1109 if (self._ssh_port_forwarder and
1110 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1111 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1112 if not self._ssh_port_forwarder:
1113 RequestPort()
1114
1115 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001116 logging.info('%s started', self.MODE_NAME[self._mode])
1117 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001118 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001119
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001120 # We don't care about child process's return code, not wait is needed. This
1121 # is used to prevent zombie process from lingering in the system.
1122 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001123
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001124 if lan_disc:
1125 self.StartLanDiscovery()
1126
1127 if rpc_server:
1128 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001129
Joel Kitching22b89042015-08-06 18:23:29 +08001130 self._forward_ssh = forward_ssh
1131
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001132 try:
1133 while True:
1134 try:
1135 addr = self._queue.get(False)
1136 except Queue.Empty:
1137 pass
1138 else:
1139 if type(addr) == tuple and addr not in self._overlord_addrs:
1140 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1141 self._overlord_addrs.append(addr)
1142
1143 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001144 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001145 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001146 # Don't show stack trace for RuntimeError, which we use in this file for
1147 # plausible and expected errors (such as can't connect to server).
1148 except RuntimeError as e:
1149 logging.info('%s: %s, retrying in %ds',
1150 e.__class__.__name__, e.message, _RETRY_INTERVAL)
1151 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001152 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001153 _, _, exc_traceback = sys.exc_info()
1154 traceback.print_tb(exc_traceback)
1155 logging.info('%s: %s, retrying in %ds',
1156 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001157 time.sleep(_RETRY_INTERVAL)
1158
1159 self.Reset()
1160 except KeyboardInterrupt:
1161 logging.error('Received keyboard interrupt, quit')
1162 sys.exit(0)
1163
1164
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001165def GhostRPCServer():
1166 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1167
1168
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001169def DownloadFile(filename):
1170 filepath = os.path.abspath(filename)
1171 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001172 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001173 sys.exit(1)
1174
1175 # Check if we actually have permission to read the file
1176 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001177 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001178 sys.exit(1)
1179
1180 server = GhostRPCServer()
1181 server.AddToDownloadQueue(os.ttyname(0), filepath)
1182 sys.exit(0)
1183
1184
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001185def main():
1186 logger = logging.getLogger()
1187 logger.setLevel(logging.INFO)
1188
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001189 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001190 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1191 default=None, help='use MID as machine ID')
1192 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1193 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001194 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1195 default=True, help='disable LAN discovery')
1196 parser.add_argument('--no-rpc-server', dest='rpc_server',
1197 action='store_false', default=True,
1198 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001199 parser.add_argument('--forward-ssh', dest='forward_ssh',
1200 action='store_true', default=False,
1201 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001202 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001203 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001204 help='file containing the JSON representation of client '
1205 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001206 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1207 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001208 parser.add_argument('--reset', dest='reset', default=False,
1209 action='store_true',
1210 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001211 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1212 nargs='*', help='overlord server address')
1213 args = parser.parse_args()
1214
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001215 if args.reset:
1216 GhostRPCServer().Reconnect()
1217 sys.exit()
1218
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001219 if args.download:
1220 DownloadFile(args.download)
1221
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001222 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001223 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001224
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001225 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001226 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1227
1228
1229def _SigtermHandler(*_):
1230 """Ensure that SSH processes also get killed on a sigterm signal.
1231
1232 By also passing the sigterm signal onto the process group, we ensure that any
1233 child SSH processes will also get killed.
1234
1235 Source:
1236 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1237 """
1238 logging.info('SIGTERM handler: shutting down')
1239 if not _SigtermHandler.SIGTERM_SENT:
1240 _SigtermHandler.SIGTERM_SENT = True
1241 logging.info('Sending TERM to process group')
1242 os.killpg(0, signal.SIGTERM)
1243 sys.exit()
1244_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001245
1246
1247if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001248 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001249 main()