blob: 30f7bd26640728ca3c5908686715f7f26959666f [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 Huanga301f572015-06-03 17:34:21 +0800301 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE = range(6)
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',
309 FILE: 'File'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800310 }
311
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800312 RANDOM_MID = '##random_mid##'
313
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800314 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800315 prop_file=None, terminal_sid=None, tty_device=None,
316 command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800317 """Constructor.
318
319 Args:
320 overlord_addrs: a list of possible address of overlord.
321 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800322 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
323 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800324 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800325 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800326 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800327 terminal_sid: the terminal session ID associate with this client. This is
328 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800329 tty_device: the terminal device to open, if tty_device is None, as pseudo
330 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800331 command: the command to execute when we are in SHELL mode.
Wei-Ning Huange2981862015-08-03 15:03:08 +0800332 file_op: a tuple (action, filepath, pid). action is either 'download' or
333 'upload'. pid is the pid of the target shell, used to determine where
334 the current working is and thus where to upload to.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800335 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800336 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800337 if mode == Ghost.SHELL:
338 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800339 if mode == Ghost.FILE:
340 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800341
342 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800343 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800344 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800345 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800346 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800347 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800348 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800349 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800350 self._ttyname_to_sid = {}
351 self._terminal_sid_to_pid = {}
352 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800353 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800354 self._register_status = DISCONNECTED
355 self._reset = threading.Event()
356
357 # RPC
358 self._buf = '' # Read buffer
359 self._requests = {}
360 self._queue = Queue.Queue()
361
362 # Protocol specific
363 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800364 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800365 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800366 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800367 self._download_queue = Queue.Queue()
368
369 # SSH Forwarding related
Joel Kitching22b89042015-08-06 18:23:29 +0800370 self._forward_ssh = False
371 self._ssh_port_forwarder = None
372 self._target_identity_file = os.path.join(os.path.dirname(
373 os.path.abspath(os.path.realpath(__file__))), 'ghost_rsa')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800374
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800375 def SetIgnoreChild(self, status):
376 # Only ignore child for Agent since only it could spawn child Ghost.
377 if self._mode == Ghost.AGENT:
378 signal.signal(signal.SIGCHLD,
379 signal.SIG_IGN if status else signal.SIG_DFL)
380
381 def GetFileSha1(self, filename):
382 with open(filename, 'r') as f:
383 return hashlib.sha1(f.read()).hexdigest()
384
Wei-Ning Huang58833882015-09-16 16:52:37 +0800385 def UseSSL(self):
386 """Determine if SSL is enabled on the Overlord server."""
387 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
388 try:
389 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
390 sock.send('GET\r\n')
391
392 data = sock.recv(16)
393 return 'HTTP' not in data
394 except Exception:
395 return False # For whatever reason above failed, assume HTTP
396
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800397 def Upgrade(self):
398 logging.info('Upgrade: initiating upgrade sequence...')
399
400 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800401 url = 'http%s://%s:%d/upgrade/ghost.py' % (
402 's' if self.UseSSL() else '', self._connected_addr[0],
403 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800404
405 # Download sha1sum for ghost.py for verification
406 try:
407 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
408 if f.getcode() != 200:
409 raise RuntimeError('HTTP status %d' % f.getcode())
410 sha1sum = f.read().strip()
411 except Exception:
412 logging.error('Upgrade: failed to download sha1sum file, abort')
413 return
414
415 if self.GetFileSha1(scriptpath) == sha1sum:
416 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
417 return
418
419 # Download upgrade version of ghost.py
420 try:
421 with contextlib.closing(urllib.urlopen(url)) as f:
422 if f.getcode() != 200:
423 raise RuntimeError('HTTP status %d' % f.getcode())
424 data = f.read()
425 except Exception:
426 logging.error('Upgrade: failed to download upgrade, abort')
427 return
428
429 # Compare SHA1 sum
430 if hashlib.sha1(data).hexdigest() != sha1sum:
431 logging.error('Upgrade: sha1sum mismatch, abort')
432 return
433
434 python = os.readlink('/proc/self/exe')
435 try:
436 with open(scriptpath, 'w') as f:
437 f.write(data)
438 except Exception:
439 logging.error('Upgrade: failed to write upgrade onto disk, abort')
440 return
441
442 logging.info('Upgrade: restarting ghost...')
443 self.CloseSockets()
444 self.SetIgnoreChild(False)
445 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
446
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800447 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800448 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800449 if self._prop_file:
450 with open(self._prop_file, 'r') as f:
451 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800452 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800453 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800454
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800455 def CloseSockets(self):
456 # Close sockets opened by parent process, since we don't use it anymore.
457 for fd in os.listdir('/proc/self/fd/'):
458 try:
459 real_fd = os.readlink('/proc/self/fd/%s' % fd)
460 if real_fd.startswith('socket'):
461 os.close(int(fd))
462 except Exception:
463 pass
464
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800465 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
466 command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800467 """Spawn a child ghost with specific mode.
468
469 Returns:
470 The spawned child process pid.
471 """
Joel Kitching22b89042015-08-06 18:23:29 +0800472 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800473 self.SetIgnoreChild(False)
474
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800475 pid = os.fork()
476 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800477 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800478 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800479 terminal_sid=terminal_sid, tty_device=tty_device,
480 command=command, file_op=file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800481 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800482 sys.exit(0)
483 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800484 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800485 return pid
486
487 def Timestamp(self):
488 return int(time.time())
489
490 def GetGateWayIP(self):
491 with open('/proc/net/route', 'r') as f:
492 lines = f.readlines()
493
494 ips = []
495 for line in lines:
496 parts = line.split('\t')
497 if parts[2] == '00000000':
498 continue
499
500 try:
501 h = parts[2].decode('hex')
502 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
503 except TypeError:
504 pass
505
506 return ips
507
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800508 def GetShopfloorIP(self):
509 try:
510 import factory_common # pylint: disable=W0612
511 from cros.factory.test import shopfloor
512
513 url = shopfloor.get_server_url()
514 match = re.match(r'^https?://(.*):.*$', url)
515 if match:
516 return [match.group(1)]
517 except Exception:
518 pass
519 return []
520
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800521 def GetMachineID(self):
522 """Generates machine-dependent ID string for a machine.
523 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800524 1. factory device_id
525 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800526 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
527 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800528 We follow the listed order to generate machine ID, and fallback to the next
529 alternative if the previous doesn't work.
530 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800531 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800532 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800533 elif self._mid:
534 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800535
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800536 # Try factory device id
537 try:
538 import factory_common # pylint: disable=W0612
539 from cros.factory.test import event_log
540 with open(event_log.DEVICE_ID_PATH) as f:
541 return f.read().strip()
542 except Exception:
543 pass
544
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800545 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800546 try:
547 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
548 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800549 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800550 stdout, _ = p.communicate()
551 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800552 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800553 return stdout.strip()
554 except Exception:
555 pass
556
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800557 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800558 try:
559 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
560 return f.read().strip()
561 except Exception:
562 pass
563
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800564 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800565 try:
566 macs = []
567 ifaces = sorted(os.listdir('/sys/class/net'))
568 for iface in ifaces:
569 if iface == 'lo':
570 continue
571
572 with open('/sys/class/net/%s/address' % iface, 'r') as f:
573 macs.append(f.read().strip())
574
575 return ';'.join(macs)
576 except Exception:
577 pass
578
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800579 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800580
581 def Reset(self):
582 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800583 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800584 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800585 self._last_ping = 0
586 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800587 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800588 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800589
590 def SendMessage(self, msg):
591 """Serialize the message and send it through the socket."""
592 self._sock.send(json.dumps(msg) + _SEPARATOR)
593
594 def SendRequest(self, name, args, handler=None,
595 timeout=_REQUEST_TIMEOUT_SECS):
596 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800597 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800598
599 rid = str(uuid.uuid4())
600 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800601 if timeout >= 0:
602 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800603 self.SendMessage(msg)
604
605 def SendResponse(self, omsg, status, params=None):
606 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
607 self.SendMessage(msg)
608
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800609 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800610 msg = json.loads(control_string)
611 command = msg['command']
612 params = msg['params']
613 if command == 'resize':
614 # some error happened on websocket
615 if len(params) != 2:
616 return
617 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
618 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
619 else:
620 logging.warn('Invalid request command "%s"', command)
621
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800622 def SpawnTTYServer(self, _):
623 """Spawn a TTY server and forward I/O to the TCP socket."""
624 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800625
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800626 try:
627 if self._tty_device is None:
628 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800629
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800630 if pid == 0:
631 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
632 try:
633 server = GhostRPCServer()
634 server.RegisterTTY(self._session_id, ttyname)
635 server.RegisterSession(self._session_id, os.getpid())
636 except Exception:
637 # If ghost is launched without RPC server, the call will fail but we
638 # can ignore it.
639 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800640
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800641 # The directory that contains the current running ghost script
642 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800643
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800644 env = os.environ.copy()
645 env['USER'] = os.getenv('USER', 'root')
646 env['HOME'] = os.getenv('HOME', '/root')
647 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
648 os.chdir(env['HOME'])
649 os.execve(_SHELL, [_SHELL], env)
650 else:
651 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800652 tty.setraw(fd)
653 attr = termios.tcgetattr(fd)
654 attr[0] &= ~(termios.IXON | termios.IXOFF)
655 attr[2] |= termios.CLOCAL
656 attr[2] &= ~termios.CRTSCTS
657 attr[4] = termios.B115200
658 attr[5] = termios.B115200
659 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800660
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800661 control_state = None
662 control_string = ''
663 write_buffer = ''
664 while True:
665 rd, _, _ = select.select([self._sock, fd], [], [])
666
667 if fd in rd:
668 self._sock.send(os.read(fd, _BUFSIZE))
669
670 if self._sock in rd:
671 ret = self._sock.recv(_BUFSIZE)
672 if len(ret) == 0:
673 raise RuntimeError('socket closed')
674 while ret:
675 if control_state:
676 if chr(_CONTROL_END) in ret:
677 index = ret.index(chr(_CONTROL_END))
678 control_string += ret[:index]
679 self.HandleTTYControl(fd, control_string)
680 control_state = None
681 control_string = ''
682 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800683 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800684 control_string += ret
685 ret = ''
686 else:
687 if chr(_CONTROL_START) in ret:
688 control_state = _CONTROL_START
689 index = ret.index(chr(_CONTROL_START))
690 write_buffer += ret[:index]
691 ret = ret[index+1:]
692 else:
693 write_buffer += ret
694 ret = ''
695 if write_buffer:
696 os.write(fd, write_buffer)
697 write_buffer = ''
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800698 except Exception as e:
699 logging.error('SpawnTTYServer: %s', e)
700 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800701 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800702
703 logging.info('SpawnTTYServer: terminated')
704 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800705
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800706 def SpawnShellServer(self, _):
707 """Spawn a shell server and forward input/output from/to the TCP socket."""
708 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800709
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800710 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800711 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
712 shell=True)
713
714 def make_non_block(fd):
715 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
716 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
717
718 make_non_block(p.stdout)
719 make_non_block(p.stderr)
720
721 try:
722 while True:
723 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800724 if p.stdout in rd:
725 self._sock.send(p.stdout.read(_BUFSIZE))
726
727 if p.stderr in rd:
728 self._sock.send(p.stderr.read(_BUFSIZE))
729
730 if self._sock in rd:
731 ret = self._sock.recv(_BUFSIZE)
732 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800733 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800734 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800735 p.poll()
736 if p.returncode != None:
737 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800738 except Exception as e:
739 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800740 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800741 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800742
743 logging.info('SpawnShellServer: terminated')
744 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800745
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800746 def InitiateFileOperation(self, _):
747 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800748 try:
749 size = os.stat(self._file_op[1]).st_size
750 except OSError as e:
751 logging.error('InitiateFileOperation: download: %s', e)
752 sys.exit(1)
753
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800754 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800755 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800756 'filename': os.path.basename(self._file_op[1]),
757 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800758 elif self._file_op[0] == 'upload':
759 self.SendRequest('clear_to_upload', {}, timeout=-1)
760 self.StartUploadServer()
761 else:
762 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800763
764 def StartDownloadServer(self):
765 logging.info('StartDownloadServer: started')
766
767 try:
768 with open(self._file_op[1], 'rb') as f:
769 while True:
770 data = f.read(_BLOCK_SIZE)
771 if len(data) == 0:
772 break
773 self._sock.send(data)
774 except Exception as e:
775 logging.error('StartDownloadServer: %s', e)
776 finally:
777 self._sock.close()
778
779 logging.info('StartDownloadServer: terminated')
780 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800781
Wei-Ning Huange2981862015-08-03 15:03:08 +0800782 def StartUploadServer(self):
783 logging.info('StartUploadServer: started')
784
785 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800786 filepath = self._file_op[1]
Wei-Ning Huange2981862015-08-03 15:03:08 +0800787
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800788 if not filepath.startswith('/'):
789 target_dir = os.getenv('HOME', '/tmp')
790
791 # Get the client's working dir, which is our target upload dir
792 if self._file_op[2]:
793 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
794
795 filepath = os.path.join(target_dir, filepath)
796
797 dirname = os.path.dirname(filepath)
798 if not os.path.exists(dirname):
799 try:
800 os.makedirs(dirname)
801 except Exception:
802 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800803
804 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800805 with open(filepath, 'wb') as f:
Wei-Ning Huange2981862015-08-03 15:03:08 +0800806 while True:
807 rd, _, _ = select.select([self._sock], [], [])
808 if self._sock in rd:
809 buf = self._sock.recv(_BLOCK_SIZE)
810 if len(buf) == 0:
811 break
812 f.write(buf)
813 except socket.error as e:
814 logging.error('StartUploadServer: socket error: %s', e)
815 except Exception as e:
816 logging.error('StartUploadServer: %s', e)
817 finally:
818 self._sock.close()
819
820 logging.info('StartUploadServer: terminated')
821 sys.exit(0)
822
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800823 def Ping(self):
824 def timeout_handler(x):
825 if x is None:
826 raise PingTimeoutError
827
828 self._last_ping = self.Timestamp()
829 self.SendRequest('ping', {}, timeout_handler, 5)
830
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800831
832 def HanldeFileDownloadRequest(self, msg):
833 params = msg['params']
834 try:
835 os.stat(params['filename'])
836 except OSError as e:
837 self.SendResponse(msg, str(e))
838 return
839
840 self.SpawnGhost(self.FILE, params['sid'],
841 file_op=('download', params['filename'], None))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800842 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800843
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800844 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800845 command = msg['name']
846 params = msg['params']
847
848 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800849 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800850 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800851 self.SpawnGhost(self.TERMINAL, params['sid'],
852 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800853 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800854 elif command == 'shell':
855 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800856 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800857 elif command == 'file_download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800858 self.HanldeFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800859 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800860 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800861 elif command == 'file_upload':
862 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
863 self.SpawnGhost(self.FILE, params['sid'],
864 file_op=('upload', params['filename'], pid))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800865 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800866
867 def HandleResponse(self, response):
868 rid = str(response['rid'])
869 if rid in self._requests:
870 handler = self._requests[rid][2]
871 del self._requests[rid]
872 if callable(handler):
873 handler(response)
874 else:
875 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800876 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800877
878 def ParseMessage(self):
879 msgs_json = self._buf.split(_SEPARATOR)
880 self._buf = msgs_json.pop()
881
882 for msg_json in msgs_json:
883 try:
884 msg = json.loads(msg_json)
885 except ValueError:
886 # Ignore mal-formed message.
887 continue
888
889 if 'name' in msg:
890 self.HandleRequest(msg)
891 elif 'response' in msg:
892 self.HandleResponse(msg)
893 else: # Ingnore mal-formed message.
894 pass
895
896 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800897 """Scans for pending requests which have timed out.
898
899 If any timed-out requests are discovered, their handler is called with the
900 special response value of None.
901 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800902 for rid in self._requests.keys()[:]:
903 request_time, timeout, handler = self._requests[rid]
904 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800905 if callable(handler):
906 handler(None)
907 else:
908 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800909 del self._requests[rid]
910
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800911 def InitiateDownload(self):
912 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800913 sid = self._ttyname_to_sid[ttyname]
914 self.SpawnGhost(self.FILE, terminal_sid=sid,
915 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800916
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800917 def Listen(self):
918 try:
919 while True:
920 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
921
922 if self._sock in rds:
923 self._buf += self._sock.recv(_BUFSIZE)
924 self.ParseMessage()
925
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800926 if (self._mode == self.AGENT and
927 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800928 self.Ping()
929 self.ScanForTimeoutRequests()
930
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800931 if not self._download_queue.empty():
932 self.InitiateDownload()
933
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800934 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800935 self.Reset()
936 break
937 except socket.error:
938 raise RuntimeError('Connection dropped')
939 except PingTimeoutError:
940 raise RuntimeError('Connection timeout')
941 finally:
942 self._sock.close()
943
944 self._queue.put('resume')
945
946 if self._mode != Ghost.AGENT:
947 sys.exit(1)
948
949 def Register(self):
950 non_local = {}
951 for addr in self._overlord_addrs:
952 non_local['addr'] = addr
953 def registered(response):
954 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800955 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800956 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800957
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800958 self._register_status = response['response']
959 if response['response'] != SUCCESS:
960 self._reset.set()
961 raise RuntimeError('Reigster: ' + response['response'])
962 else:
963 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
964 self._connected_addr = non_local['addr']
965 self.Upgrade() # Check for upgrade
966 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800967
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800968 if self._forward_ssh:
969 logging.info('Starting target SSH port negotiation')
970 self.NegotiateTargetSSHPort()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800971
972 try:
973 logging.info('Trying %s:%d ...', *addr)
974 self.Reset()
975 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
976 self._sock.settimeout(_PING_TIMEOUT)
977 self._sock.connect(addr)
978
979 logging.info('Connection established, registering...')
980 handler = {
981 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800982 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800983 Ghost.SHELL: self.SpawnShellServer,
984 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800985 }[self._mode]
986
987 # Machine ID may change if MAC address is used (USB-ethernet dongle
988 # plugged/unplugged)
989 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800990 self.SendRequest('register',
991 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800992 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800993 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800994 except socket.error:
995 pass
996 else:
997 self._sock.settimeout(None)
998 self.Listen()
999
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001000 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001001
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001002 def Reconnect(self):
1003 logging.info('Received reconnect request from RPC server, reconnecting...')
1004 self._reset.set()
1005
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001006 def GetStatus(self):
1007 return self._register_status
1008
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001009 def AddToDownloadQueue(self, ttyname, filename):
1010 self._download_queue.put((ttyname, filename))
1011
Wei-Ning Huangd521f282015-08-07 05:28:04 +08001012 def RegisterTTY(self, session_id, ttyname):
1013 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +08001014
1015 def RegisterSession(self, session_id, process_id):
1016 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001017
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001018 def StartLanDiscovery(self):
1019 """Start to listen to LAN discovery packet at
1020 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001021
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001022 def thread_func():
1023 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1024 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1025 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001026 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001027 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
1028 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001029 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001030 return
1031
1032 logging.info('LAN Discovery: started')
1033 while True:
1034 rd, _, _ = select.select([s], [], [], 1)
1035
1036 if s in rd:
1037 data, source_addr = s.recvfrom(_BUFSIZE)
1038 parts = data.split()
1039 if parts[0] == 'OVERLORD':
1040 ip, port = parts[1].split(':')
1041 if not ip:
1042 ip = source_addr[0]
1043 self._queue.put((ip, int(port)), True)
1044
1045 try:
1046 obj = self._queue.get(False)
1047 except Queue.Empty:
1048 pass
1049 else:
1050 if type(obj) is not str:
1051 self._queue.put(obj)
1052 elif obj == 'pause':
1053 logging.info('LAN Discovery: paused')
1054 while obj != 'resume':
1055 obj = self._queue.get(True)
1056 logging.info('LAN Discovery: resumed')
1057
1058 t = threading.Thread(target=thread_func)
1059 t.daemon = True
1060 t.start()
1061
1062 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +08001063 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001064 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
1065 logRequests=False)
1066 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001067 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001068 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001069 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001070 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001071 t = threading.Thread(target=rpc_server.serve_forever)
1072 t.daemon = True
1073 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001074
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001075 def ScanServer(self):
1076 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1077 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1078 if addr not in self._overlord_addrs:
1079 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001080
Joel Kitching22b89042015-08-06 18:23:29 +08001081 def NegotiateTargetSSHPort(self):
1082 """Request-receive target SSH port forwarding loop.
1083
1084 Repeatedly attempts to forward this machine's SSH port to target. It
1085 bounces back and forth between RequestPort and ReceivePort when a new port
1086 is required. ReceivePort starts a new thread so that the main ghost thread
1087 may continue running.
1088 """
1089 # Sanity check for identity file.
1090 if not os.path.isfile(self._target_identity_file):
1091 logging.info('No target host identity file: not negotiating '
1092 'target SSH port')
1093 return
1094
1095 def PollSSHPortForwarder():
1096 def ThreadFunc():
1097 while True:
1098 state = self._ssh_port_forwarder.GetState()
1099
1100 # Connected successfully.
1101 if state == SSHPortForwarder.INITIALIZED:
1102 # The SSH port forward has succeeded! Let's tell Overlord.
1103 port = self._ssh_port_forwarder.GetDstPort()
1104 RegisterPort(port)
1105
1106 # We've given up... continue to the next port.
1107 elif state == SSHPortForwarder.FAILED:
1108 break
1109
1110 # Either CONNECTING or INITIALIZED.
1111 self._ssh_port_forwarder.Wait()
1112
1113 # Only request a new port if we are still registered to Overlord.
1114 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1115 # which will take care of it.
1116 try:
1117 RequestPort()
1118 except Exception:
1119 logging.info('Failed to request port, will wait for next connection')
1120 self._ssh_port_forwarder = None
1121
1122 t = threading.Thread(target=ThreadFunc)
1123 t.daemon = True
1124 t.start()
1125
1126 def ReceivePort(response):
1127 # If the response times out, this version of Overlord may not support SSH
1128 # port negotiation. Give up on port negotiation process.
1129 if response is None:
1130 return
1131
1132 port = int(response['params']['port'])
1133 logging.info('Received target SSH port: %d', port)
1134
1135 if (self._ssh_port_forwarder and
1136 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1137 logging.info('Unexpectedly received a target SSH port')
1138 return
1139
1140 # Try forwarding SSH port to target.
1141 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1142 src_port=22,
1143 dst_port=port,
1144 user='ghost',
1145 identity_file=self._target_identity_file,
1146 host=self._connected_addr[0]) # Use Overlord host as target.
1147
1148 # Creates a new thread.
1149 PollSSHPortForwarder()
1150
1151 def RequestPort():
1152 logging.info('Requesting new target SSH port')
1153 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1154
1155 def RegisterPort(port):
1156 logging.info('Registering target SSH port %d', port)
1157 self.SendRequest(
1158 'register_target_ssh_port',
1159 {'port': port}, RegisterPortResponse, 5)
1160
1161 def RegisterPortResponse(response):
1162 # Overlord responded to request_port already. If register_port fails,
1163 # something might be in an inconsistent state, so trigger a reconnect
1164 # via PingTimeoutError.
1165 if response is None:
1166 raise PingTimeoutError
1167 logging.info('Registering target SSH port acknowledged')
1168
1169 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1170 # manually report the port to target, since SSHPortForwarder is currently
1171 # blocking.
1172 if (self._ssh_port_forwarder and
1173 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1174 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1175 if not self._ssh_port_forwarder:
1176 RequestPort()
1177
1178 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001179 logging.info('%s started', self.MODE_NAME[self._mode])
1180 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001181 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001182
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001183 # We don't care about child process's return code, not wait is needed. This
1184 # is used to prevent zombie process from lingering in the system.
1185 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001186
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001187 if lan_disc:
1188 self.StartLanDiscovery()
1189
1190 if rpc_server:
1191 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001192
Joel Kitching22b89042015-08-06 18:23:29 +08001193 self._forward_ssh = forward_ssh
1194
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001195 try:
1196 while True:
1197 try:
1198 addr = self._queue.get(False)
1199 except Queue.Empty:
1200 pass
1201 else:
1202 if type(addr) == tuple and addr not in self._overlord_addrs:
1203 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1204 self._overlord_addrs.append(addr)
1205
1206 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001207 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001208 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001209 # Don't show stack trace for RuntimeError, which we use in this file for
1210 # plausible and expected errors (such as can't connect to server).
1211 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001212 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +08001213 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001214 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001215 _, _, exc_traceback = sys.exc_info()
1216 traceback.print_tb(exc_traceback)
1217 logging.info('%s: %s, retrying in %ds',
1218 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001219 time.sleep(_RETRY_INTERVAL)
1220
1221 self.Reset()
1222 except KeyboardInterrupt:
1223 logging.error('Received keyboard interrupt, quit')
1224 sys.exit(0)
1225
1226
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001227def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001228 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001229 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1230
1231
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001232def ForkToBackground():
1233 """Fork process to run in background."""
1234 pid = os.fork()
1235 if pid != 0:
1236 logging.info('Ghost(%d) running in background.', pid)
1237 sys.exit(0)
1238
1239
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001240def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001241 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001242 filepath = os.path.abspath(filename)
1243 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001244 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001245 sys.exit(1)
1246
1247 # Check if we actually have permission to read the file
1248 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001249 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001250 sys.exit(1)
1251
1252 server = GhostRPCServer()
1253 server.AddToDownloadQueue(os.ttyname(0), filepath)
1254 sys.exit(0)
1255
1256
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001257def main():
1258 logger = logging.getLogger()
1259 logger.setLevel(logging.INFO)
1260
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001261 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001262 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1263 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001264 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1265 default=None, help='use MID as machine ID')
1266 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1267 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001268 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1269 default=True, help='disable LAN discovery')
1270 parser.add_argument('--no-rpc-server', dest='rpc_server',
1271 action='store_false', default=True,
1272 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001273 parser.add_argument('--forward-ssh', dest='forward_ssh',
1274 action='store_true', default=False,
1275 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001276 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001277 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001278 help='file containing the JSON representation of client '
1279 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001280 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1281 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001282 parser.add_argument('--reset', dest='reset', default=False,
1283 action='store_true',
1284 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001285 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1286 nargs='*', help='overlord server address')
1287 args = parser.parse_args()
1288
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001289 if args.fork:
1290 ForkToBackground()
1291
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001292 if args.reset:
1293 GhostRPCServer().Reconnect()
1294 sys.exit()
1295
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001296 if args.download:
1297 DownloadFile(args.download)
1298
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001299 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001300 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001301
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001302 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001303 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1304
1305
1306def _SigtermHandler(*_):
1307 """Ensure that SSH processes also get killed on a sigterm signal.
1308
1309 By also passing the sigterm signal onto the process group, we ensure that any
1310 child SSH processes will also get killed.
1311
1312 Source:
1313 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1314 """
1315 logging.info('SIGTERM handler: shutting down')
1316 if not _SigtermHandler.SIGTERM_SENT:
1317 _SigtermHandler.SIGTERM_SENT = True
1318 logging.info('Sending TERM to process group')
1319 os.killpg(0, signal.SIGTERM)
1320 sys.exit()
1321_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001322
1323
1324if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001325 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001326 main()