blob: fea267b353007da706e6881700d942a664f08f64 [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')
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800906
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800907 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800908 self._connected_addr = addr
909 self.Upgrade() # Check for upgrade
910 self._queue.put('pause', True)
911
Joel Kitching22b89042015-08-06 18:23:29 +0800912 if self._forward_ssh:
913 logging.info('Starting target SSH port negotiation')
914 self.NegotiateTargetSSHPort()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800915
916 try:
917 logging.info('Trying %s:%d ...', *addr)
918 self.Reset()
919 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
920 self._sock.settimeout(_PING_TIMEOUT)
921 self._sock.connect(addr)
922
923 logging.info('Connection established, registering...')
924 handler = {
925 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800926 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800927 Ghost.SHELL: self.SpawnShellServer,
928 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800929 }[self._mode]
930
931 # Machine ID may change if MAC address is used (USB-ethernet dongle
932 # plugged/unplugged)
933 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800934 self.SendRequest('register',
935 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800936 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800937 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800938 except socket.error:
939 pass
940 else:
941 self._sock.settimeout(None)
942 self.Listen()
943
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800944 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800945
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800946 def Reconnect(self):
947 logging.info('Received reconnect request from RPC server, reconnecting...')
948 self._reset.set()
949
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800950 def AddToDownloadQueue(self, ttyname, filename):
951 self._download_queue.put((ttyname, filename))
952
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800953 def RegisterTTY(self, session_id, ttyname):
954 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800955
956 def RegisterSession(self, session_id, process_id):
957 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800958
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800959 def StartLanDiscovery(self):
960 """Start to listen to LAN discovery packet at
961 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800962
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800963 def thread_func():
964 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
965 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
966 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800967 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800968 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
969 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800970 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800971 return
972
973 logging.info('LAN Discovery: started')
974 while True:
975 rd, _, _ = select.select([s], [], [], 1)
976
977 if s in rd:
978 data, source_addr = s.recvfrom(_BUFSIZE)
979 parts = data.split()
980 if parts[0] == 'OVERLORD':
981 ip, port = parts[1].split(':')
982 if not ip:
983 ip = source_addr[0]
984 self._queue.put((ip, int(port)), True)
985
986 try:
987 obj = self._queue.get(False)
988 except Queue.Empty:
989 pass
990 else:
991 if type(obj) is not str:
992 self._queue.put(obj)
993 elif obj == 'pause':
994 logging.info('LAN Discovery: paused')
995 while obj != 'resume':
996 obj = self._queue.get(True)
997 logging.info('LAN Discovery: resumed')
998
999 t = threading.Thread(target=thread_func)
1000 t.daemon = True
1001 t.start()
1002
1003 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +08001004 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001005 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
1006 logRequests=False)
1007 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001008 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001009 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001010 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001011 t = threading.Thread(target=rpc_server.serve_forever)
1012 t.daemon = True
1013 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001014
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001015 def ScanServer(self):
1016 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1017 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1018 if addr not in self._overlord_addrs:
1019 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001020
Joel Kitching22b89042015-08-06 18:23:29 +08001021 def NegotiateTargetSSHPort(self):
1022 """Request-receive target SSH port forwarding loop.
1023
1024 Repeatedly attempts to forward this machine's SSH port to target. It
1025 bounces back and forth between RequestPort and ReceivePort when a new port
1026 is required. ReceivePort starts a new thread so that the main ghost thread
1027 may continue running.
1028 """
1029 # Sanity check for identity file.
1030 if not os.path.isfile(self._target_identity_file):
1031 logging.info('No target host identity file: not negotiating '
1032 'target SSH port')
1033 return
1034
1035 def PollSSHPortForwarder():
1036 def ThreadFunc():
1037 while True:
1038 state = self._ssh_port_forwarder.GetState()
1039
1040 # Connected successfully.
1041 if state == SSHPortForwarder.INITIALIZED:
1042 # The SSH port forward has succeeded! Let's tell Overlord.
1043 port = self._ssh_port_forwarder.GetDstPort()
1044 RegisterPort(port)
1045
1046 # We've given up... continue to the next port.
1047 elif state == SSHPortForwarder.FAILED:
1048 break
1049
1050 # Either CONNECTING or INITIALIZED.
1051 self._ssh_port_forwarder.Wait()
1052
1053 # Only request a new port if we are still registered to Overlord.
1054 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1055 # which will take care of it.
1056 try:
1057 RequestPort()
1058 except Exception:
1059 logging.info('Failed to request port, will wait for next connection')
1060 self._ssh_port_forwarder = None
1061
1062 t = threading.Thread(target=ThreadFunc)
1063 t.daemon = True
1064 t.start()
1065
1066 def ReceivePort(response):
1067 # If the response times out, this version of Overlord may not support SSH
1068 # port negotiation. Give up on port negotiation process.
1069 if response is None:
1070 return
1071
1072 port = int(response['params']['port'])
1073 logging.info('Received target SSH port: %d', port)
1074
1075 if (self._ssh_port_forwarder and
1076 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1077 logging.info('Unexpectedly received a target SSH port')
1078 return
1079
1080 # Try forwarding SSH port to target.
1081 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1082 src_port=22,
1083 dst_port=port,
1084 user='ghost',
1085 identity_file=self._target_identity_file,
1086 host=self._connected_addr[0]) # Use Overlord host as target.
1087
1088 # Creates a new thread.
1089 PollSSHPortForwarder()
1090
1091 def RequestPort():
1092 logging.info('Requesting new target SSH port')
1093 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1094
1095 def RegisterPort(port):
1096 logging.info('Registering target SSH port %d', port)
1097 self.SendRequest(
1098 'register_target_ssh_port',
1099 {'port': port}, RegisterPortResponse, 5)
1100
1101 def RegisterPortResponse(response):
1102 # Overlord responded to request_port already. If register_port fails,
1103 # something might be in an inconsistent state, so trigger a reconnect
1104 # via PingTimeoutError.
1105 if response is None:
1106 raise PingTimeoutError
1107 logging.info('Registering target SSH port acknowledged')
1108
1109 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1110 # manually report the port to target, since SSHPortForwarder is currently
1111 # blocking.
1112 if (self._ssh_port_forwarder and
1113 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1114 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1115 if not self._ssh_port_forwarder:
1116 RequestPort()
1117
1118 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001119 logging.info('%s started', self.MODE_NAME[self._mode])
1120 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001121 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001122
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001123 # We don't care about child process's return code, not wait is needed. This
1124 # is used to prevent zombie process from lingering in the system.
1125 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001126
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001127 if lan_disc:
1128 self.StartLanDiscovery()
1129
1130 if rpc_server:
1131 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001132
Joel Kitching22b89042015-08-06 18:23:29 +08001133 self._forward_ssh = forward_ssh
1134
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001135 try:
1136 while True:
1137 try:
1138 addr = self._queue.get(False)
1139 except Queue.Empty:
1140 pass
1141 else:
1142 if type(addr) == tuple and addr not in self._overlord_addrs:
1143 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1144 self._overlord_addrs.append(addr)
1145
1146 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001147 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001148 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001149 # Don't show stack trace for RuntimeError, which we use in this file for
1150 # plausible and expected errors (such as can't connect to server).
1151 except RuntimeError as e:
1152 logging.info('%s: %s, retrying in %ds',
1153 e.__class__.__name__, e.message, _RETRY_INTERVAL)
1154 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001155 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001156 _, _, exc_traceback = sys.exc_info()
1157 traceback.print_tb(exc_traceback)
1158 logging.info('%s: %s, retrying in %ds',
1159 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001160 time.sleep(_RETRY_INTERVAL)
1161
1162 self.Reset()
1163 except KeyboardInterrupt:
1164 logging.error('Received keyboard interrupt, quit')
1165 sys.exit(0)
1166
1167
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001168def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001169 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001170 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1171
1172
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001173def ForkToBackground():
1174 """Fork process to run in background."""
1175 pid = os.fork()
1176 if pid != 0:
1177 logging.info('Ghost(%d) running in background.', pid)
1178 sys.exit(0)
1179
1180
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001181def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001182 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001183 filepath = os.path.abspath(filename)
1184 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001185 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001186 sys.exit(1)
1187
1188 # Check if we actually have permission to read the file
1189 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001190 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001191 sys.exit(1)
1192
1193 server = GhostRPCServer()
1194 server.AddToDownloadQueue(os.ttyname(0), filepath)
1195 sys.exit(0)
1196
1197
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001198def main():
1199 logger = logging.getLogger()
1200 logger.setLevel(logging.INFO)
1201
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001202 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001203 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1204 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001205 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1206 default=None, help='use MID as machine ID')
1207 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1208 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001209 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1210 default=True, help='disable LAN discovery')
1211 parser.add_argument('--no-rpc-server', dest='rpc_server',
1212 action='store_false', default=True,
1213 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001214 parser.add_argument('--forward-ssh', dest='forward_ssh',
1215 action='store_true', default=False,
1216 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001217 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001218 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001219 help='file containing the JSON representation of client '
1220 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001221 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1222 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001223 parser.add_argument('--reset', dest='reset', default=False,
1224 action='store_true',
1225 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001226 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1227 nargs='*', help='overlord server address')
1228 args = parser.parse_args()
1229
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001230 if args.fork:
1231 ForkToBackground()
1232
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001233 if args.reset:
1234 GhostRPCServer().Reconnect()
1235 sys.exit()
1236
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001237 if args.download:
1238 DownloadFile(args.download)
1239
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001240 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001241 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001242
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001243 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001244 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1245
1246
1247def _SigtermHandler(*_):
1248 """Ensure that SSH processes also get killed on a sigterm signal.
1249
1250 By also passing the sigterm signal onto the process group, we ensure that any
1251 child SSH processes will also get killed.
1252
1253 Source:
1254 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1255 """
1256 logging.info('SIGTERM handler: shutting down')
1257 if not _SigtermHandler.SIGTERM_SENT:
1258 _SigtermHandler.SIGTERM_SENT = True
1259 logging.info('Sending TERM to process group')
1260 os.killpg(0, signal.SIGTERM)
1261 sys.exit()
1262_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001263
1264
1265if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001266 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001267 main()