blob: 8c68ce2805e8be64fa7b277c6d4079de4ce80f8e [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 Huang39169902015-09-19 06:00:23 +080029import tty
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080030import urllib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080031import uuid
32
Wei-Ning Huang2132de32015-04-13 17:24:38 +080033import jsonrpclib
34from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
35
36
37_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080038
39_OVERLORD_PORT = 4455
40_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080041_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080042
43_BUFSIZE = 8192
44_RETRY_INTERVAL = 2
45_SEPARATOR = '\r\n'
46_PING_TIMEOUT = 3
47_PING_INTERVAL = 5
48_REQUEST_TIMEOUT_SECS = 60
49_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080050_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080051
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080052_CONTROL_START = 128
53_CONTROL_END = 129
54
Wei-Ning Huanga301f572015-06-03 17:34:21 +080055_BLOCK_SIZE = 4096
56
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080057SUCCESS = 'success'
58FAILED = 'failed'
59DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080060
Joel Kitching22b89042015-08-06 18:23:29 +080061
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080062class PingTimeoutError(Exception):
63 pass
64
65
66class RequestError(Exception):
67 pass
68
69
Joel Kitching22b89042015-08-06 18:23:29 +080070class SSHPortForwarder(object):
71 """Create and maintain an SSH port forwarding connection.
72
73 This is meant to be a standalone class to maintain an SSH port forwarding
74 connection to a given server. It provides a fail/retry mechanism, and also
75 can report its current connection status.
76 """
77 _FAILED_STR = 'port forwarding failed'
78 _DEFAULT_CONNECT_TIMEOUT = 10
79 _DEFAULT_ALIVE_INTERVAL = 10
80 _DEFAULT_DISCONNECT_WAIT = 1
81 _DEFAULT_RETRIES = 5
82 _DEFAULT_EXP_FACTOR = 1
83 _DEBUG_INTERVAL = 2
84
85 CONNECTING = 1
86 INITIALIZED = 2
87 FAILED = 4
88
89 REMOTE = 1
90 LOCAL = 2
91
92 @classmethod
93 def ToRemote(cls, *args, **kwargs):
94 """Calls contructor with forward_to=REMOTE."""
95 return cls(*args, forward_to=cls.REMOTE, **kwargs)
96
97 @classmethod
98 def ToLocal(cls, *args, **kwargs):
99 """Calls contructor with forward_to=LOCAL."""
100 return cls(*args, forward_to=cls.LOCAL, **kwargs)
101
102 def __init__(self,
103 forward_to,
104 src_port,
105 dst_port,
106 user,
107 identity_file,
108 host,
109 port=22,
110 connect_timeout=_DEFAULT_CONNECT_TIMEOUT,
111 alive_interval=_DEFAULT_ALIVE_INTERVAL,
112 disconnect_wait=_DEFAULT_DISCONNECT_WAIT,
113 retries=_DEFAULT_RETRIES,
114 exp_factor=_DEFAULT_EXP_FACTOR):
115 """Constructor.
116
117 Args:
118 forward_to: Which direction to forward traffic: REMOTE or LOCAL.
119 src_port: Source port for forwarding.
120 dst_port: Destination port for forwarding.
121 user: Username on remote server.
122 identity_file: Identity file for passwordless authentication on remote
123 server.
124 host: Host of remote server.
125 port: Port of remote server.
126 connect_timeout: Time in seconds
127 alive_interval:
128 disconnect_wait: The number of seconds to wait before reconnecting after
129 the first disconnect.
130 retries: The number of times to retry before reporting a failed
131 connection.
132 exp_factor: After each reconnect, the disconnect wait time is multiplied
133 by 2^exp_factor.
134 """
135 # Internal use.
136 self._ssh_thread = None
137 self._ssh_output = None
138 self._exception = None
139 self._state = self.CONNECTING
140 self._poll = threading.Event()
141
142 # Connection arguments.
143 self._forward_to = forward_to
144 self._src_port = src_port
145 self._dst_port = dst_port
146 self._host = host
147 self._user = user
148 self._identity_file = identity_file
149 self._port = port
150
151 # Configuration arguments.
152 self._connect_timeout = connect_timeout
153 self._alive_interval = alive_interval
154 self._exp_factor = exp_factor
155
156 t = threading.Thread(
157 target=self._Run,
158 args=(disconnect_wait, retries))
159 t.daemon = True
160 t.start()
161
162 def __str__(self):
163 # State representation.
164 if self._state == self.CONNECTING:
165 state_str = 'connecting'
166 elif self._state == self.INITIALIZED:
167 state_str = 'initialized'
168 else:
169 state_str = 'failed'
170
171 # Port forward representation.
172 if self._forward_to == self.REMOTE:
173 fwd_str = '->%d' % self._dst_port
174 else:
175 fwd_str = '%d<-' % self._dst_port
176
177 return 'SSHPortForwarder(%s,%s)' % (state_str, fwd_str)
178
179 def _ForwardArgs(self):
180 if self._forward_to == self.REMOTE:
181 return ['-R', '%d:127.0.0.1:%d' % (self._dst_port, self._src_port)]
182 else:
183 return ['-L', '%d:127.0.0.1:%d' % (self._src_port, self._dst_port)]
184
185 def _RunSSHCmd(self):
186 """Runs the SSH command, storing the exception on failure."""
187 try:
188 cmd = [
189 'ssh',
190 '-o', 'StrictHostKeyChecking=no',
191 '-o', 'GlobalKnownHostsFile=/dev/null',
192 '-o', 'UserKnownHostsFile=/dev/null',
193 '-o', 'ExitOnForwardFailure=yes',
194 '-o', 'ConnectTimeout=%d' % self._connect_timeout,
195 '-o', 'ServerAliveInterval=%d' % self._alive_interval,
196 '-o', 'ServerAliveCountMax=1',
197 '-o', 'TCPKeepAlive=yes',
198 '-o', 'BatchMode=yes',
199 '-i', self._identity_file,
200 '-N',
201 '-p', str(self._port),
202 '%s@%s' % (self._user, self._host),
203 ] + self._ForwardArgs()
204 logging.info(' '.join(cmd))
205 self._ssh_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
206 except subprocess.CalledProcessError as e:
207 self._exception = e
208 finally:
209 pass
210
211 def _Run(self, disconnect_wait, retries):
212 """Wraps around the SSH command, detecting its connection status."""
213 assert retries > 0, '%s: _Run must be called with retries > 0' % self
214
215 logging.info('%s: Connecting to %s:%d',
216 self, self._host, self._port)
217
218 # Set identity file permissions. Need to only be user-readable for ssh to
219 # use the key.
220 try:
221 os.chmod(self._identity_file, 0600)
222 except OSError as e:
223 logging.error('%s: Error setting identity file permissions: %s',
224 self, e)
225 self._state = self.FAILED
226 return
227
228 # Start a thread. If it fails, deal with the failure. If it is still
229 # running after connect_timeout seconds, assume everything's working great,
230 # and tell the caller. Then, continue waiting for it to end.
231 self._ssh_thread = threading.Thread(target=self._RunSSHCmd)
232 self._ssh_thread.daemon = True
233 self._ssh_thread.start()
234
235 # See if the SSH thread is still working after connect_timeout.
236 self._ssh_thread.join(self._connect_timeout)
237 if self._ssh_thread.is_alive():
238 # Assumed to be working. Tell our caller that we are connected.
239 if self._state != self.INITIALIZED:
240 self._state = self.INITIALIZED
241 self._poll.set()
242 logging.info('%s: Still connected after timeout=%ds',
243 self, self._connect_timeout)
244
245 # Only for debug purposes. Keep showing connection status.
246 while self._ssh_thread.is_alive():
247 logging.debug('%s: Still connected', self)
248 self._ssh_thread.join(self._DEBUG_INTERVAL)
249
250 # Figure out what went wrong.
251 if not self._exception:
252 logging.info('%s: SSH unexpectedly exited: %s',
253 self, self._ssh_output.rstrip())
254 if self._exception and self._FAILED_STR in self._exception.output:
255 self._state = self.FAILED
256 self._poll.set()
257 logging.info('%s: Port forwarding failed', self)
258 return
259 elif retries == 1:
260 self._state = self.FAILED
261 self._poll.set()
262 logging.info('%s: Disconnected (0 retries left)', self)
263 return
264 else:
265 logging.info('%s: Disconnected, retrying (sleep %1ds, %d retries left)',
266 self, disconnect_wait, retries - 1)
267 time.sleep(disconnect_wait)
268 self._Run(disconnect_wait=disconnect_wait * (2 ** self._exp_factor),
269 retries=retries - 1)
270
271 def GetState(self):
272 """Returns the current connection state.
273
274 State may be one of:
275
276 CONNECTING: Still attempting to make the first successful connection.
277 INITIALIZED: Is either connected or is trying to make subsequent
278 connection.
279 FAILED: Has completed all connection attempts, or server has reported that
280 target port is in use.
281 """
282 return self._state
283
284 def GetDstPort(self):
285 """Returns the current target port."""
286 return self._dst_port
287
288 def Wait(self):
289 """Waits for a state change, and returns the new state."""
290 self._poll.wait()
291 self._poll.clear()
292 return self.GetState()
293
294
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800295class Ghost(object):
296 """Ghost implements the client protocol of Overlord.
297
298 Ghost provide terminal/shell/logcat functionality and manages the client
299 side connectivity.
300 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800301 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800302
303 MODE_NAME = {
304 NONE: 'NONE',
305 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800306 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800307 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800308 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800309 FILE: 'File',
310 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800311 }
312
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800313 RANDOM_MID = '##random_mid##'
314
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800315 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800316 prop_file=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800317 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800318 """Constructor.
319
320 Args:
321 overlord_addrs: a list of possible address of overlord.
322 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800323 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
324 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800325 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800326 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800327 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800328 terminal_sid: the terminal session ID associate with this client. This is
329 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800330 tty_device: the terminal device to open, if tty_device is None, as pseudo
331 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800332 command: the command to execute when we are in SHELL mode.
Wei-Ning Huange2981862015-08-03 15:03:08 +0800333 file_op: a tuple (action, filepath, pid). action is either 'download' or
334 'upload'. pid is the pid of the target shell, used to determine where
335 the current working is and thus where to upload to.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800336 port: port number to forward.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800337 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800338 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
339 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800340 if mode == Ghost.SHELL:
341 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800342 if mode == Ghost.FILE:
343 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800344
345 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800346 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800347 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800348 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800349 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800350 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800351 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800352 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800353 self._ttyname_to_sid = {}
354 self._terminal_sid_to_pid = {}
355 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800356 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800357 self._register_status = DISCONNECTED
358 self._reset = threading.Event()
359
360 # RPC
361 self._buf = '' # Read buffer
362 self._requests = {}
363 self._queue = Queue.Queue()
364
365 # Protocol specific
366 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800367 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800368 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800369 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800370 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800371 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800372
373 # SSH Forwarding related
Joel Kitching22b89042015-08-06 18:23:29 +0800374 self._forward_ssh = False
375 self._ssh_port_forwarder = None
376 self._target_identity_file = os.path.join(os.path.dirname(
377 os.path.abspath(os.path.realpath(__file__))), 'ghost_rsa')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800378
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800379 def SetIgnoreChild(self, status):
380 # Only ignore child for Agent since only it could spawn child Ghost.
381 if self._mode == Ghost.AGENT:
382 signal.signal(signal.SIGCHLD,
383 signal.SIG_IGN if status else signal.SIG_DFL)
384
385 def GetFileSha1(self, filename):
386 with open(filename, 'r') as f:
387 return hashlib.sha1(f.read()).hexdigest()
388
Wei-Ning Huang58833882015-09-16 16:52:37 +0800389 def UseSSL(self):
390 """Determine if SSL is enabled on the Overlord server."""
391 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
392 try:
393 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
394 sock.send('GET\r\n')
395
396 data = sock.recv(16)
397 return 'HTTP' not in data
398 except Exception:
399 return False # For whatever reason above failed, assume HTTP
400
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800401 def Upgrade(self):
402 logging.info('Upgrade: initiating upgrade sequence...')
403
404 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800405 url = 'http%s://%s:%d/upgrade/ghost.py' % (
406 's' if self.UseSSL() else '', self._connected_addr[0],
407 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800408
409 # Download sha1sum for ghost.py for verification
410 try:
411 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
412 if f.getcode() != 200:
413 raise RuntimeError('HTTP status %d' % f.getcode())
414 sha1sum = f.read().strip()
415 except Exception:
416 logging.error('Upgrade: failed to download sha1sum file, abort')
417 return
418
419 if self.GetFileSha1(scriptpath) == sha1sum:
420 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
421 return
422
423 # Download upgrade version of ghost.py
424 try:
425 with contextlib.closing(urllib.urlopen(url)) as f:
426 if f.getcode() != 200:
427 raise RuntimeError('HTTP status %d' % f.getcode())
428 data = f.read()
429 except Exception:
430 logging.error('Upgrade: failed to download upgrade, abort')
431 return
432
433 # Compare SHA1 sum
434 if hashlib.sha1(data).hexdigest() != sha1sum:
435 logging.error('Upgrade: sha1sum mismatch, abort')
436 return
437
438 python = os.readlink('/proc/self/exe')
439 try:
440 with open(scriptpath, 'w') as f:
441 f.write(data)
442 except Exception:
443 logging.error('Upgrade: failed to write upgrade onto disk, abort')
444 return
445
446 logging.info('Upgrade: restarting ghost...')
447 self.CloseSockets()
448 self.SetIgnoreChild(False)
449 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
450
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800451 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800452 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800453 if self._prop_file:
454 with open(self._prop_file, 'r') as f:
455 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800456 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800457 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800458
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800459 def CloseSockets(self):
460 # Close sockets opened by parent process, since we don't use it anymore.
461 for fd in os.listdir('/proc/self/fd/'):
462 try:
463 real_fd = os.readlink('/proc/self/fd/%s' % fd)
464 if real_fd.startswith('socket'):
465 os.close(int(fd))
466 except Exception:
467 pass
468
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800469 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800470 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800471 """Spawn a child ghost with specific mode.
472
473 Returns:
474 The spawned child process pid.
475 """
Joel Kitching22b89042015-08-06 18:23:29 +0800476 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800477 self.SetIgnoreChild(False)
478
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800479 pid = os.fork()
480 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800481 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800482 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800483 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800484 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800485 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800486 sys.exit(0)
487 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800488 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800489 return pid
490
491 def Timestamp(self):
492 return int(time.time())
493
494 def GetGateWayIP(self):
495 with open('/proc/net/route', 'r') as f:
496 lines = f.readlines()
497
498 ips = []
499 for line in lines:
500 parts = line.split('\t')
501 if parts[2] == '00000000':
502 continue
503
504 try:
505 h = parts[2].decode('hex')
506 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
507 except TypeError:
508 pass
509
510 return ips
511
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800512 def GetShopfloorIP(self):
513 try:
514 import factory_common # pylint: disable=W0612
515 from cros.factory.test import shopfloor
516
517 url = shopfloor.get_server_url()
518 match = re.match(r'^https?://(.*):.*$', url)
519 if match:
520 return [match.group(1)]
521 except Exception:
522 pass
523 return []
524
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800525 def GetMachineID(self):
526 """Generates machine-dependent ID string for a machine.
527 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800528 1. factory device_id
529 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800530 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
531 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532 We follow the listed order to generate machine ID, and fallback to the next
533 alternative if the previous doesn't work.
534 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800535 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800536 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800537 elif self._mid:
538 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800539
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800540 # Try factory device id
541 try:
542 import factory_common # pylint: disable=W0612
543 from cros.factory.test import event_log
544 with open(event_log.DEVICE_ID_PATH) as f:
545 return f.read().strip()
546 except Exception:
547 pass
548
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800549 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800550 try:
551 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
552 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800553 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800554 stdout, _ = p.communicate()
555 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800556 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800557 return stdout.strip()
558 except Exception:
559 pass
560
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800561 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800562 try:
563 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
564 return f.read().strip()
565 except Exception:
566 pass
567
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800568 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800569 try:
570 macs = []
571 ifaces = sorted(os.listdir('/sys/class/net'))
572 for iface in ifaces:
573 if iface == 'lo':
574 continue
575
576 with open('/sys/class/net/%s/address' % iface, 'r') as f:
577 macs.append(f.read().strip())
578
579 return ';'.join(macs)
580 except Exception:
581 pass
582
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800583 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800584
585 def Reset(self):
586 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800587 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800588 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800589 self._last_ping = 0
590 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800591 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800592 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800593
594 def SendMessage(self, msg):
595 """Serialize the message and send it through the socket."""
596 self._sock.send(json.dumps(msg) + _SEPARATOR)
597
598 def SendRequest(self, name, args, handler=None,
599 timeout=_REQUEST_TIMEOUT_SECS):
600 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800601 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800602
603 rid = str(uuid.uuid4())
604 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800605 if timeout >= 0:
606 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800607 self.SendMessage(msg)
608
609 def SendResponse(self, omsg, status, params=None):
610 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
611 self.SendMessage(msg)
612
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800613 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800614 msg = json.loads(control_string)
615 command = msg['command']
616 params = msg['params']
617 if command == 'resize':
618 # some error happened on websocket
619 if len(params) != 2:
620 return
621 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
622 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
623 else:
624 logging.warn('Invalid request command "%s"', command)
625
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800626 def SpawnTTYServer(self, _):
627 """Spawn a TTY server and forward I/O to the TCP socket."""
628 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800629
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800630 try:
631 if self._tty_device is None:
632 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800633
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800634 if pid == 0:
635 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
636 try:
637 server = GhostRPCServer()
638 server.RegisterTTY(self._session_id, ttyname)
639 server.RegisterSession(self._session_id, os.getpid())
640 except Exception:
641 # If ghost is launched without RPC server, the call will fail but we
642 # can ignore it.
643 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800644
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800645 # The directory that contains the current running ghost script
646 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800647
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800648 env = os.environ.copy()
649 env['USER'] = os.getenv('USER', 'root')
650 env['HOME'] = os.getenv('HOME', '/root')
651 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
652 os.chdir(env['HOME'])
653 os.execve(_SHELL, [_SHELL], env)
654 else:
655 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800656 tty.setraw(fd)
657 attr = termios.tcgetattr(fd)
658 attr[0] &= ~(termios.IXON | termios.IXOFF)
659 attr[2] |= termios.CLOCAL
660 attr[2] &= ~termios.CRTSCTS
661 attr[4] = termios.B115200
662 attr[5] = termios.B115200
663 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800664
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800665 control_state = None
666 control_string = ''
667 write_buffer = ''
668 while True:
669 rd, _, _ = select.select([self._sock, fd], [], [])
670
671 if fd in rd:
672 self._sock.send(os.read(fd, _BUFSIZE))
673
674 if self._sock in rd:
675 ret = self._sock.recv(_BUFSIZE)
676 if len(ret) == 0:
677 raise RuntimeError('socket closed')
678 while ret:
679 if control_state:
680 if chr(_CONTROL_END) in ret:
681 index = ret.index(chr(_CONTROL_END))
682 control_string += ret[:index]
683 self.HandleTTYControl(fd, control_string)
684 control_state = None
685 control_string = ''
686 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800687 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800688 control_string += ret
689 ret = ''
690 else:
691 if chr(_CONTROL_START) in ret:
692 control_state = _CONTROL_START
693 index = ret.index(chr(_CONTROL_START))
694 write_buffer += ret[:index]
695 ret = ret[index+1:]
696 else:
697 write_buffer += ret
698 ret = ''
699 if write_buffer:
700 os.write(fd, write_buffer)
701 write_buffer = ''
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800702 except Exception as e:
703 logging.error('SpawnTTYServer: %s', e)
704 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800705 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800706
707 logging.info('SpawnTTYServer: terminated')
708 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800709
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800710 def SpawnShellServer(self, _):
711 """Spawn a shell server and forward input/output from/to the TCP socket."""
712 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800713
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800714 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800715 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
716 shell=True)
717
718 def make_non_block(fd):
719 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
720 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
721
722 make_non_block(p.stdout)
723 make_non_block(p.stderr)
724
725 try:
726 while True:
727 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800728 if p.stdout in rd:
729 self._sock.send(p.stdout.read(_BUFSIZE))
730
731 if p.stderr in rd:
732 self._sock.send(p.stderr.read(_BUFSIZE))
733
734 if self._sock in rd:
735 ret = self._sock.recv(_BUFSIZE)
736 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800737 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800738 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800739 p.poll()
740 if p.returncode != None:
741 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800742 except Exception as e:
743 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800744 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800745 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800746
747 logging.info('SpawnShellServer: terminated')
748 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800749
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800750 def InitiateFileOperation(self, _):
751 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800752 try:
753 size = os.stat(self._file_op[1]).st_size
754 except OSError as e:
755 logging.error('InitiateFileOperation: download: %s', e)
756 sys.exit(1)
757
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800758 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800759 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800760 'filename': os.path.basename(self._file_op[1]),
761 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800762 elif self._file_op[0] == 'upload':
763 self.SendRequest('clear_to_upload', {}, timeout=-1)
764 self.StartUploadServer()
765 else:
766 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800767
768 def StartDownloadServer(self):
769 logging.info('StartDownloadServer: started')
770
771 try:
772 with open(self._file_op[1], 'rb') as f:
773 while True:
774 data = f.read(_BLOCK_SIZE)
775 if len(data) == 0:
776 break
777 self._sock.send(data)
778 except Exception as e:
779 logging.error('StartDownloadServer: %s', e)
780 finally:
781 self._sock.close()
782
783 logging.info('StartDownloadServer: terminated')
784 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800785
Wei-Ning Huange2981862015-08-03 15:03:08 +0800786 def StartUploadServer(self):
787 logging.info('StartUploadServer: started')
788
789 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800790 filepath = self._file_op[1]
Wei-Ning Huange2981862015-08-03 15:03:08 +0800791
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800792 if not filepath.startswith('/'):
793 target_dir = os.getenv('HOME', '/tmp')
794
795 # Get the client's working dir, which is our target upload dir
796 if self._file_op[2]:
797 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
798
799 filepath = os.path.join(target_dir, filepath)
800
801 dirname = os.path.dirname(filepath)
802 if not os.path.exists(dirname):
803 try:
804 os.makedirs(dirname)
805 except Exception:
806 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800807
808 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800809 with open(filepath, 'wb') as f:
Wei-Ning Huange2981862015-08-03 15:03:08 +0800810 while True:
811 rd, _, _ = select.select([self._sock], [], [])
812 if self._sock in rd:
813 buf = self._sock.recv(_BLOCK_SIZE)
814 if len(buf) == 0:
815 break
816 f.write(buf)
817 except socket.error as e:
818 logging.error('StartUploadServer: socket error: %s', e)
819 except Exception as e:
820 logging.error('StartUploadServer: %s', e)
821 finally:
822 self._sock.close()
823
824 logging.info('StartUploadServer: terminated')
825 sys.exit(0)
826
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800827 def SpawnPortForwardServer(self, _):
828 """Spawn a port forwarding server and forward I/O to the TCP socket."""
829 logging.info('SpawnPortForwardServer: started')
830
831 src_sock = None
832 try:
833 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
834 src_sock.connect(('localhost', self._port))
835 src_sock.setblocking(False)
836
837 # Pass the leftovers of the previous buffer
838 if self._buf:
839 src_sock.send(self._buf)
840 self._buf = ''
841
842 while True:
843 rd, _, _ = select.select([self._sock, src_sock], [], [])
844
845 if self._sock in rd:
846 data = self._sock.recv(_BUFSIZE)
847 if not data:
848 break
849 src_sock.send(data)
850
851 if src_sock in rd:
852 data = src_sock.recv(_BUFSIZE)
853 if not data:
854 break
855 self._sock.send(data)
856 except Exception as e:
857 logging.error('SpawnPortForwardServer: %s', e)
858 finally:
859 if src_sock:
860 src_sock.close()
861 self._sock.close()
862
863 logging.info('SpawnPortForwardServer: terminated')
864 sys.exit(0)
865
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800866 def Ping(self):
867 def timeout_handler(x):
868 if x is None:
869 raise PingTimeoutError
870
871 self._last_ping = self.Timestamp()
872 self.SendRequest('ping', {}, timeout_handler, 5)
873
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800874 def HanldeFileDownloadRequest(self, msg):
875 params = msg['params']
876 try:
877 os.stat(params['filename'])
878 except OSError as e:
879 self.SendResponse(msg, str(e))
880 return
881
882 self.SpawnGhost(self.FILE, params['sid'],
883 file_op=('download', params['filename'], None))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800884 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800885
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800886 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800887 command = msg['name']
888 params = msg['params']
889
890 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800891 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800892 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800893 self.SpawnGhost(self.TERMINAL, params['sid'],
894 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800895 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800896 elif command == 'shell':
897 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800898 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800899 elif command == 'file_download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800900 self.HanldeFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800901 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800902 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800903 elif command == 'file_upload':
904 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
905 self.SpawnGhost(self.FILE, params['sid'],
906 file_op=('upload', params['filename'], pid))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800907 self.SendResponse(msg, SUCCESS)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800908 elif command == 'forward':
909 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
910 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800911
912 def HandleResponse(self, response):
913 rid = str(response['rid'])
914 if rid in self._requests:
915 handler = self._requests[rid][2]
916 del self._requests[rid]
917 if callable(handler):
918 handler(response)
919 else:
920 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800921 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800922
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800923 def ParseMessage(self, single=True):
924 if single:
925 index = self._buf.index(_SEPARATOR)
926 msgs_json = [self._buf[:index]]
927 self._buf = self._buf[index + 2:]
928 else:
929 msgs_json = self._buf.split(_SEPARATOR)
930 self._buf = msgs_json.pop()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800931
932 for msg_json in msgs_json:
933 try:
934 msg = json.loads(msg_json)
935 except ValueError:
936 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800937 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800938 continue
939
940 if 'name' in msg:
941 self.HandleRequest(msg)
942 elif 'response' in msg:
943 self.HandleResponse(msg)
944 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800945 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800946
947 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800948 """Scans for pending requests which have timed out.
949
950 If any timed-out requests are discovered, their handler is called with the
951 special response value of None.
952 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800953 for rid in self._requests.keys()[:]:
954 request_time, timeout, handler = self._requests[rid]
955 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800956 if callable(handler):
957 handler(None)
958 else:
959 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800960 del self._requests[rid]
961
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800962 def InitiateDownload(self):
963 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800964 sid = self._ttyname_to_sid[ttyname]
965 self.SpawnGhost(self.FILE, terminal_sid=sid,
966 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800967
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800968 def Listen(self):
969 try:
970 while True:
971 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
972
973 if self._sock in rds:
974 self._buf += self._sock.recv(_BUFSIZE)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800975 self.ParseMessage(self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800976
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800977 if (self._mode == self.AGENT and
978 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800979 self.Ping()
980 self.ScanForTimeoutRequests()
981
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800982 if not self._download_queue.empty():
983 self.InitiateDownload()
984
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800985 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800986 self.Reset()
987 break
988 except socket.error:
989 raise RuntimeError('Connection dropped')
990 except PingTimeoutError:
991 raise RuntimeError('Connection timeout')
992 finally:
993 self._sock.close()
994
995 self._queue.put('resume')
996
997 if self._mode != Ghost.AGENT:
998 sys.exit(1)
999
1000 def Register(self):
1001 non_local = {}
1002 for addr in self._overlord_addrs:
1003 non_local['addr'] = addr
1004 def registered(response):
1005 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001006 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001007 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001008
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001009 self._register_status = response['response']
1010 if response['response'] != SUCCESS:
1011 self._reset.set()
1012 raise RuntimeError('Reigster: ' + response['response'])
1013 else:
1014 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
1015 self._connected_addr = non_local['addr']
1016 self.Upgrade() # Check for upgrade
1017 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001018
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001019 if self._forward_ssh:
1020 logging.info('Starting target SSH port negotiation')
1021 self.NegotiateTargetSSHPort()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001022
1023 try:
1024 logging.info('Trying %s:%d ...', *addr)
1025 self.Reset()
1026 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1027 self._sock.settimeout(_PING_TIMEOUT)
1028 self._sock.connect(addr)
1029
1030 logging.info('Connection established, registering...')
1031 handler = {
1032 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +08001033 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001034 Ghost.SHELL: self.SpawnShellServer,
1035 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +08001036 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001037 }[self._mode]
1038
1039 # Machine ID may change if MAC address is used (USB-ethernet dongle
1040 # plugged/unplugged)
1041 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001042 self.SendRequest('register',
1043 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001044 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001045 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001046 except socket.error:
1047 pass
1048 else:
1049 self._sock.settimeout(None)
1050 self.Listen()
1051
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001052 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001053
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001054 def Reconnect(self):
1055 logging.info('Received reconnect request from RPC server, reconnecting...')
1056 self._reset.set()
1057
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001058 def GetStatus(self):
1059 return self._register_status
1060
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001061 def AddToDownloadQueue(self, ttyname, filename):
1062 self._download_queue.put((ttyname, filename))
1063
Wei-Ning Huangd521f282015-08-07 05:28:04 +08001064 def RegisterTTY(self, session_id, ttyname):
1065 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +08001066
1067 def RegisterSession(self, session_id, process_id):
1068 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001069
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001070 def StartLanDiscovery(self):
1071 """Start to listen to LAN discovery packet at
1072 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001073
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001074 def thread_func():
1075 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1076 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1077 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001078 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001079 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
1080 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001081 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001082 return
1083
1084 logging.info('LAN Discovery: started')
1085 while True:
1086 rd, _, _ = select.select([s], [], [], 1)
1087
1088 if s in rd:
1089 data, source_addr = s.recvfrom(_BUFSIZE)
1090 parts = data.split()
1091 if parts[0] == 'OVERLORD':
1092 ip, port = parts[1].split(':')
1093 if not ip:
1094 ip = source_addr[0]
1095 self._queue.put((ip, int(port)), True)
1096
1097 try:
1098 obj = self._queue.get(False)
1099 except Queue.Empty:
1100 pass
1101 else:
1102 if type(obj) is not str:
1103 self._queue.put(obj)
1104 elif obj == 'pause':
1105 logging.info('LAN Discovery: paused')
1106 while obj != 'resume':
1107 obj = self._queue.get(True)
1108 logging.info('LAN Discovery: resumed')
1109
1110 t = threading.Thread(target=thread_func)
1111 t.daemon = True
1112 t.start()
1113
1114 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +08001115 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001116 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
1117 logRequests=False)
1118 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001119 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001120 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001121 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001122 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001123 t = threading.Thread(target=rpc_server.serve_forever)
1124 t.daemon = True
1125 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001126
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001127 def ScanServer(self):
1128 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1129 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1130 if addr not in self._overlord_addrs:
1131 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001132
Joel Kitching22b89042015-08-06 18:23:29 +08001133 def NegotiateTargetSSHPort(self):
1134 """Request-receive target SSH port forwarding loop.
1135
1136 Repeatedly attempts to forward this machine's SSH port to target. It
1137 bounces back and forth between RequestPort and ReceivePort when a new port
1138 is required. ReceivePort starts a new thread so that the main ghost thread
1139 may continue running.
1140 """
1141 # Sanity check for identity file.
1142 if not os.path.isfile(self._target_identity_file):
1143 logging.info('No target host identity file: not negotiating '
1144 'target SSH port')
1145 return
1146
1147 def PollSSHPortForwarder():
1148 def ThreadFunc():
1149 while True:
1150 state = self._ssh_port_forwarder.GetState()
1151
1152 # Connected successfully.
1153 if state == SSHPortForwarder.INITIALIZED:
1154 # The SSH port forward has succeeded! Let's tell Overlord.
1155 port = self._ssh_port_forwarder.GetDstPort()
1156 RegisterPort(port)
1157
1158 # We've given up... continue to the next port.
1159 elif state == SSHPortForwarder.FAILED:
1160 break
1161
1162 # Either CONNECTING or INITIALIZED.
1163 self._ssh_port_forwarder.Wait()
1164
1165 # Only request a new port if we are still registered to Overlord.
1166 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1167 # which will take care of it.
1168 try:
1169 RequestPort()
1170 except Exception:
1171 logging.info('Failed to request port, will wait for next connection')
1172 self._ssh_port_forwarder = None
1173
1174 t = threading.Thread(target=ThreadFunc)
1175 t.daemon = True
1176 t.start()
1177
1178 def ReceivePort(response):
1179 # If the response times out, this version of Overlord may not support SSH
1180 # port negotiation. Give up on port negotiation process.
1181 if response is None:
1182 return
1183
1184 port = int(response['params']['port'])
1185 logging.info('Received target SSH port: %d', port)
1186
1187 if (self._ssh_port_forwarder and
1188 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1189 logging.info('Unexpectedly received a target SSH port')
1190 return
1191
1192 # Try forwarding SSH port to target.
1193 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1194 src_port=22,
1195 dst_port=port,
1196 user='ghost',
1197 identity_file=self._target_identity_file,
1198 host=self._connected_addr[0]) # Use Overlord host as target.
1199
1200 # Creates a new thread.
1201 PollSSHPortForwarder()
1202
1203 def RequestPort():
1204 logging.info('Requesting new target SSH port')
1205 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1206
1207 def RegisterPort(port):
1208 logging.info('Registering target SSH port %d', port)
1209 self.SendRequest(
1210 'register_target_ssh_port',
1211 {'port': port}, RegisterPortResponse, 5)
1212
1213 def RegisterPortResponse(response):
1214 # Overlord responded to request_port already. If register_port fails,
1215 # something might be in an inconsistent state, so trigger a reconnect
1216 # via PingTimeoutError.
1217 if response is None:
1218 raise PingTimeoutError
1219 logging.info('Registering target SSH port acknowledged')
1220
1221 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1222 # manually report the port to target, since SSHPortForwarder is currently
1223 # blocking.
1224 if (self._ssh_port_forwarder and
1225 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1226 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1227 if not self._ssh_port_forwarder:
1228 RequestPort()
1229
1230 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001231 logging.info('%s started', self.MODE_NAME[self._mode])
1232 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001233 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001234
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001235 # We don't care about child process's return code, not wait is needed. This
1236 # is used to prevent zombie process from lingering in the system.
1237 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001238
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001239 if lan_disc:
1240 self.StartLanDiscovery()
1241
1242 if rpc_server:
1243 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001244
Joel Kitching22b89042015-08-06 18:23:29 +08001245 self._forward_ssh = forward_ssh
1246
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001247 try:
1248 while True:
1249 try:
1250 addr = self._queue.get(False)
1251 except Queue.Empty:
1252 pass
1253 else:
1254 if type(addr) == tuple and addr not in self._overlord_addrs:
1255 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1256 self._overlord_addrs.append(addr)
1257
1258 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001259 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001260 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001261 # Don't show stack trace for RuntimeError, which we use in this file for
1262 # plausible and expected errors (such as can't connect to server).
1263 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001264 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +08001265 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001266 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001267 _, _, exc_traceback = sys.exc_info()
1268 traceback.print_tb(exc_traceback)
1269 logging.info('%s: %s, retrying in %ds',
1270 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001271 time.sleep(_RETRY_INTERVAL)
1272
1273 self.Reset()
1274 except KeyboardInterrupt:
1275 logging.error('Received keyboard interrupt, quit')
1276 sys.exit(0)
1277
1278
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001279def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001280 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001281 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1282
1283
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001284def ForkToBackground():
1285 """Fork process to run in background."""
1286 pid = os.fork()
1287 if pid != 0:
1288 logging.info('Ghost(%d) running in background.', pid)
1289 sys.exit(0)
1290
1291
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001292def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001293 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001294 filepath = os.path.abspath(filename)
1295 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001296 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001297 sys.exit(1)
1298
1299 # Check if we actually have permission to read the file
1300 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001301 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001302 sys.exit(1)
1303
1304 server = GhostRPCServer()
1305 server.AddToDownloadQueue(os.ttyname(0), filepath)
1306 sys.exit(0)
1307
1308
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001309def main():
1310 logger = logging.getLogger()
1311 logger.setLevel(logging.INFO)
1312
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001313 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001314 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1315 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001316 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1317 default=None, help='use MID as machine ID')
1318 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1319 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001320 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1321 default=True, help='disable LAN discovery')
1322 parser.add_argument('--no-rpc-server', dest='rpc_server',
1323 action='store_false', default=True,
1324 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001325 parser.add_argument('--forward-ssh', dest='forward_ssh',
1326 action='store_true', default=False,
1327 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001328 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001329 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001330 help='file containing the JSON representation of client '
1331 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001332 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1333 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001334 parser.add_argument('--reset', dest='reset', default=False,
1335 action='store_true',
1336 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001337 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1338 nargs='*', help='overlord server address')
1339 args = parser.parse_args()
1340
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001341 if args.fork:
1342 ForkToBackground()
1343
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001344 if args.reset:
1345 GhostRPCServer().Reconnect()
1346 sys.exit()
1347
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001348 if args.download:
1349 DownloadFile(args.download)
1350
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001351 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001352 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001353
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001354 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001355 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1356
1357
1358def _SigtermHandler(*_):
1359 """Ensure that SSH processes also get killed on a sigterm signal.
1360
1361 By also passing the sigterm signal onto the process group, we ensure that any
1362 child SSH processes will also get killed.
1363
1364 Source:
1365 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1366 """
1367 logging.info('SIGTERM handler: shutting down')
1368 if not _SigtermHandler.SIGTERM_SENT:
1369 _SigtermHandler.SIGTERM_SENT = True
1370 logging.info('Sending TERM to process group')
1371 os.killpg(0, signal.SIGTERM)
1372 sys.exit()
1373_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001374
1375
1376if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001377 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001378 main()