blob: 093c32ec1ac06c4e788ae850b762eee5509a573f [file] [log] [blame]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001#!/usr/bin/python -u
2# -*- coding: utf-8 -*-
3#
4# Copyright 2015 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
Joel Kitching22b89042015-08-06 18:23:29 +08008from __future__ import print_function
9
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080010import argparse
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080011import contextlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080012import fcntl
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080013import hashlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080014import json
15import logging
16import os
17import Queue
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080018import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080019import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080020import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080021import socket
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080022import struct
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080023import subprocess
24import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080025import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080026import threading
27import time
Joel Kitching22b89042015-08-06 18:23:29 +080028import traceback
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080029import urllib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080030import uuid
31
Wei-Ning Huang2132de32015-04-13 17:24:38 +080032import jsonrpclib
33from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
34
35
36_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080037
38_OVERLORD_PORT = 4455
39_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080040_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080041
42_BUFSIZE = 8192
43_RETRY_INTERVAL = 2
44_SEPARATOR = '\r\n'
45_PING_TIMEOUT = 3
46_PING_INTERVAL = 5
47_REQUEST_TIMEOUT_SECS = 60
48_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080049_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080050
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080051_CONTROL_START = 128
52_CONTROL_END = 129
53
Wei-Ning Huanga301f572015-06-03 17:34:21 +080054_BLOCK_SIZE = 4096
55
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080056RESPONSE_SUCCESS = 'success'
57RESPONSE_FAILED = 'failed'
58
Joel Kitching22b89042015-08-06 18:23:29 +080059
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080060class PingTimeoutError(Exception):
61 pass
62
63
64class RequestError(Exception):
65 pass
66
67
Joel Kitching22b89042015-08-06 18:23:29 +080068class SSHPortForwarder(object):
69 """Create and maintain an SSH port forwarding connection.
70
71 This is meant to be a standalone class to maintain an SSH port forwarding
72 connection to a given server. It provides a fail/retry mechanism, and also
73 can report its current connection status.
74 """
75 _FAILED_STR = 'port forwarding failed'
76 _DEFAULT_CONNECT_TIMEOUT = 10
77 _DEFAULT_ALIVE_INTERVAL = 10
78 _DEFAULT_DISCONNECT_WAIT = 1
79 _DEFAULT_RETRIES = 5
80 _DEFAULT_EXP_FACTOR = 1
81 _DEBUG_INTERVAL = 2
82
83 CONNECTING = 1
84 INITIALIZED = 2
85 FAILED = 4
86
87 REMOTE = 1
88 LOCAL = 2
89
90 @classmethod
91 def ToRemote(cls, *args, **kwargs):
92 """Calls contructor with forward_to=REMOTE."""
93 return cls(*args, forward_to=cls.REMOTE, **kwargs)
94
95 @classmethod
96 def ToLocal(cls, *args, **kwargs):
97 """Calls contructor with forward_to=LOCAL."""
98 return cls(*args, forward_to=cls.LOCAL, **kwargs)
99
100 def __init__(self,
101 forward_to,
102 src_port,
103 dst_port,
104 user,
105 identity_file,
106 host,
107 port=22,
108 connect_timeout=_DEFAULT_CONNECT_TIMEOUT,
109 alive_interval=_DEFAULT_ALIVE_INTERVAL,
110 disconnect_wait=_DEFAULT_DISCONNECT_WAIT,
111 retries=_DEFAULT_RETRIES,
112 exp_factor=_DEFAULT_EXP_FACTOR):
113 """Constructor.
114
115 Args:
116 forward_to: Which direction to forward traffic: REMOTE or LOCAL.
117 src_port: Source port for forwarding.
118 dst_port: Destination port for forwarding.
119 user: Username on remote server.
120 identity_file: Identity file for passwordless authentication on remote
121 server.
122 host: Host of remote server.
123 port: Port of remote server.
124 connect_timeout: Time in seconds
125 alive_interval:
126 disconnect_wait: The number of seconds to wait before reconnecting after
127 the first disconnect.
128 retries: The number of times to retry before reporting a failed
129 connection.
130 exp_factor: After each reconnect, the disconnect wait time is multiplied
131 by 2^exp_factor.
132 """
133 # Internal use.
134 self._ssh_thread = None
135 self._ssh_output = None
136 self._exception = None
137 self._state = self.CONNECTING
138 self._poll = threading.Event()
139
140 # Connection arguments.
141 self._forward_to = forward_to
142 self._src_port = src_port
143 self._dst_port = dst_port
144 self._host = host
145 self._user = user
146 self._identity_file = identity_file
147 self._port = port
148
149 # Configuration arguments.
150 self._connect_timeout = connect_timeout
151 self._alive_interval = alive_interval
152 self._exp_factor = exp_factor
153
154 t = threading.Thread(
155 target=self._Run,
156 args=(disconnect_wait, retries))
157 t.daemon = True
158 t.start()
159
160 def __str__(self):
161 # State representation.
162 if self._state == self.CONNECTING:
163 state_str = 'connecting'
164 elif self._state == self.INITIALIZED:
165 state_str = 'initialized'
166 else:
167 state_str = 'failed'
168
169 # Port forward representation.
170 if self._forward_to == self.REMOTE:
171 fwd_str = '->%d' % self._dst_port
172 else:
173 fwd_str = '%d<-' % self._dst_port
174
175 return 'SSHPortForwarder(%s,%s)' % (state_str, fwd_str)
176
177 def _ForwardArgs(self):
178 if self._forward_to == self.REMOTE:
179 return ['-R', '%d:127.0.0.1:%d' % (self._dst_port, self._src_port)]
180 else:
181 return ['-L', '%d:127.0.0.1:%d' % (self._src_port, self._dst_port)]
182
183 def _RunSSHCmd(self):
184 """Runs the SSH command, storing the exception on failure."""
185 try:
186 cmd = [
187 'ssh',
188 '-o', 'StrictHostKeyChecking=no',
189 '-o', 'GlobalKnownHostsFile=/dev/null',
190 '-o', 'UserKnownHostsFile=/dev/null',
191 '-o', 'ExitOnForwardFailure=yes',
192 '-o', 'ConnectTimeout=%d' % self._connect_timeout,
193 '-o', 'ServerAliveInterval=%d' % self._alive_interval,
194 '-o', 'ServerAliveCountMax=1',
195 '-o', 'TCPKeepAlive=yes',
196 '-o', 'BatchMode=yes',
197 '-i', self._identity_file,
198 '-N',
199 '-p', str(self._port),
200 '%s@%s' % (self._user, self._host),
201 ] + self._ForwardArgs()
202 logging.info(' '.join(cmd))
203 self._ssh_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
204 except subprocess.CalledProcessError as e:
205 self._exception = e
206 finally:
207 pass
208
209 def _Run(self, disconnect_wait, retries):
210 """Wraps around the SSH command, detecting its connection status."""
211 assert retries > 0, '%s: _Run must be called with retries > 0' % self
212
213 logging.info('%s: Connecting to %s:%d',
214 self, self._host, self._port)
215
216 # Set identity file permissions. Need to only be user-readable for ssh to
217 # use the key.
218 try:
219 os.chmod(self._identity_file, 0600)
220 except OSError as e:
221 logging.error('%s: Error setting identity file permissions: %s',
222 self, e)
223 self._state = self.FAILED
224 return
225
226 # Start a thread. If it fails, deal with the failure. If it is still
227 # running after connect_timeout seconds, assume everything's working great,
228 # and tell the caller. Then, continue waiting for it to end.
229 self._ssh_thread = threading.Thread(target=self._RunSSHCmd)
230 self._ssh_thread.daemon = True
231 self._ssh_thread.start()
232
233 # See if the SSH thread is still working after connect_timeout.
234 self._ssh_thread.join(self._connect_timeout)
235 if self._ssh_thread.is_alive():
236 # Assumed to be working. Tell our caller that we are connected.
237 if self._state != self.INITIALIZED:
238 self._state = self.INITIALIZED
239 self._poll.set()
240 logging.info('%s: Still connected after timeout=%ds',
241 self, self._connect_timeout)
242
243 # Only for debug purposes. Keep showing connection status.
244 while self._ssh_thread.is_alive():
245 logging.debug('%s: Still connected', self)
246 self._ssh_thread.join(self._DEBUG_INTERVAL)
247
248 # Figure out what went wrong.
249 if not self._exception:
250 logging.info('%s: SSH unexpectedly exited: %s',
251 self, self._ssh_output.rstrip())
252 if self._exception and self._FAILED_STR in self._exception.output:
253 self._state = self.FAILED
254 self._poll.set()
255 logging.info('%s: Port forwarding failed', self)
256 return
257 elif retries == 1:
258 self._state = self.FAILED
259 self._poll.set()
260 logging.info('%s: Disconnected (0 retries left)', self)
261 return
262 else:
263 logging.info('%s: Disconnected, retrying (sleep %1ds, %d retries left)',
264 self, disconnect_wait, retries - 1)
265 time.sleep(disconnect_wait)
266 self._Run(disconnect_wait=disconnect_wait * (2 ** self._exp_factor),
267 retries=retries - 1)
268
269 def GetState(self):
270 """Returns the current connection state.
271
272 State may be one of:
273
274 CONNECTING: Still attempting to make the first successful connection.
275 INITIALIZED: Is either connected or is trying to make subsequent
276 connection.
277 FAILED: Has completed all connection attempts, or server has reported that
278 target port is in use.
279 """
280 return self._state
281
282 def GetDstPort(self):
283 """Returns the current target port."""
284 return self._dst_port
285
286 def Wait(self):
287 """Waits for a state change, and returns the new state."""
288 self._poll.wait()
289 self._poll.clear()
290 return self.GetState()
291
292
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800293class Ghost(object):
294 """Ghost implements the client protocol of Overlord.
295
296 Ghost provide terminal/shell/logcat functionality and manages the client
297 side connectivity.
298 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800299 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE = range(6)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800300
301 MODE_NAME = {
302 NONE: 'NONE',
303 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800304 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800305 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800306 LOGCAT: 'Logcat',
307 FILE: 'File'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800308 }
309
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800310 RANDOM_MID = '##random_mid##'
311
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800312 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
313 terminal_sid=None, command=None, file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800314 """Constructor.
315
316 Args:
317 overlord_addrs: a list of possible address of overlord.
318 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800319 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
320 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800321 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800322 be set to the corresponding session id assigned by overlord.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800323 terminal_sid: the terminal session ID associate with this client. This is
324 use for file download.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800325 command: the command to execute when we are in SHELL mode.
Wei-Ning Huange2981862015-08-03 15:03:08 +0800326 file_op: a tuple (action, filepath, pid). action is either 'download' or
327 'upload'. pid is the pid of the target shell, used to determine where
328 the current working is and thus where to upload to.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800329 """
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800330 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800331 if mode == Ghost.SHELL:
332 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800333 if mode == Ghost.FILE:
334 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800335
336 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800337 self._connected_addr = None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800338 self._mode = mode
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800339 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800340 self._sock = None
341 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800342 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800343 self._terminal_session_id = terminal_sid
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800344 self._properties = {}
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800345 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800346 self._file_op = file_op
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800347 self._buf = ''
348 self._requests = {}
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800349 self._reset = threading.Event()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800350 self._last_ping = 0
351 self._queue = Queue.Queue()
Joel Kitching22b89042015-08-06 18:23:29 +0800352 self._forward_ssh = False
353 self._ssh_port_forwarder = None
354 self._target_identity_file = os.path.join(os.path.dirname(
355 os.path.abspath(os.path.realpath(__file__))), 'ghost_rsa')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800356 self._download_queue = Queue.Queue()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800357 self._ttyname_to_sid = {}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800358 self._terminal_sid_to_pid = {}
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800359
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800360 def SetIgnoreChild(self, status):
361 # Only ignore child for Agent since only it could spawn child Ghost.
362 if self._mode == Ghost.AGENT:
363 signal.signal(signal.SIGCHLD,
364 signal.SIG_IGN if status else signal.SIG_DFL)
365
366 def GetFileSha1(self, filename):
367 with open(filename, 'r') as f:
368 return hashlib.sha1(f.read()).hexdigest()
369
Wei-Ning Huang58833882015-09-16 16:52:37 +0800370 def UseSSL(self):
371 """Determine if SSL is enabled on the Overlord server."""
372 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
373 try:
374 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
375 sock.send('GET\r\n')
376
377 data = sock.recv(16)
378 return 'HTTP' not in data
379 except Exception:
380 return False # For whatever reason above failed, assume HTTP
381
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800382 def Upgrade(self):
383 logging.info('Upgrade: initiating upgrade sequence...')
384
385 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800386 url = 'http%s://%s:%d/upgrade/ghost.py' % (
387 's' if self.UseSSL() else '', self._connected_addr[0],
388 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800389
390 # Download sha1sum for ghost.py for verification
391 try:
392 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
393 if f.getcode() != 200:
394 raise RuntimeError('HTTP status %d' % f.getcode())
395 sha1sum = f.read().strip()
396 except Exception:
397 logging.error('Upgrade: failed to download sha1sum file, abort')
398 return
399
400 if self.GetFileSha1(scriptpath) == sha1sum:
401 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
402 return
403
404 # Download upgrade version of ghost.py
405 try:
406 with contextlib.closing(urllib.urlopen(url)) as f:
407 if f.getcode() != 200:
408 raise RuntimeError('HTTP status %d' % f.getcode())
409 data = f.read()
410 except Exception:
411 logging.error('Upgrade: failed to download upgrade, abort')
412 return
413
414 # Compare SHA1 sum
415 if hashlib.sha1(data).hexdigest() != sha1sum:
416 logging.error('Upgrade: sha1sum mismatch, abort')
417 return
418
419 python = os.readlink('/proc/self/exe')
420 try:
421 with open(scriptpath, 'w') as f:
422 f.write(data)
423 except Exception:
424 logging.error('Upgrade: failed to write upgrade onto disk, abort')
425 return
426
427 logging.info('Upgrade: restarting ghost...')
428 self.CloseSockets()
429 self.SetIgnoreChild(False)
430 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
431
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800432 def LoadPropertiesFromFile(self, filename):
433 try:
434 with open(filename, 'r') as f:
435 self._properties = json.loads(f.read())
436 except Exception as e:
437 logging.exception('LoadPropertiesFromFile: ' + str(e))
438
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800439 def CloseSockets(self):
440 # Close sockets opened by parent process, since we don't use it anymore.
441 for fd in os.listdir('/proc/self/fd/'):
442 try:
443 real_fd = os.readlink('/proc/self/fd/%s' % fd)
444 if real_fd.startswith('socket'):
445 os.close(int(fd))
446 except Exception:
447 pass
448
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800449 def SpawnGhost(self, mode, sid=None, terminal_sid=None, command=None,
450 file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800451 """Spawn a child ghost with specific mode.
452
453 Returns:
454 The spawned child process pid.
455 """
Joel Kitching22b89042015-08-06 18:23:29 +0800456 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800457 self.SetIgnoreChild(False)
458
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800459 pid = os.fork()
460 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800461 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800462 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
463 terminal_sid, command, file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800464 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800465 sys.exit(0)
466 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800467 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800468 return pid
469
470 def Timestamp(self):
471 return int(time.time())
472
473 def GetGateWayIP(self):
474 with open('/proc/net/route', 'r') as f:
475 lines = f.readlines()
476
477 ips = []
478 for line in lines:
479 parts = line.split('\t')
480 if parts[2] == '00000000':
481 continue
482
483 try:
484 h = parts[2].decode('hex')
485 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
486 except TypeError:
487 pass
488
489 return ips
490
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800491 def GetShopfloorIP(self):
492 try:
493 import factory_common # pylint: disable=W0612
494 from cros.factory.test import shopfloor
495
496 url = shopfloor.get_server_url()
497 match = re.match(r'^https?://(.*):.*$', url)
498 if match:
499 return [match.group(1)]
500 except Exception:
501 pass
502 return []
503
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800504 def GetMachineID(self):
505 """Generates machine-dependent ID string for a machine.
506 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800507 1. factory device_id
508 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800509 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
510 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800511 We follow the listed order to generate machine ID, and fallback to the next
512 alternative if the previous doesn't work.
513 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800514 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800515 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800516 elif self._mid:
517 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800518
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800519 # Try factory device id
520 try:
521 import factory_common # pylint: disable=W0612
522 from cros.factory.test import event_log
523 with open(event_log.DEVICE_ID_PATH) as f:
524 return f.read().strip()
525 except Exception:
526 pass
527
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800528 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800529 try:
530 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
531 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800532 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800533 stdout, _ = p.communicate()
534 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800535 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800536 return stdout.strip()
537 except Exception:
538 pass
539
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800540 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800541 try:
542 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
543 return f.read().strip()
544 except Exception:
545 pass
546
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800547 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800548 try:
549 macs = []
550 ifaces = sorted(os.listdir('/sys/class/net'))
551 for iface in ifaces:
552 if iface == 'lo':
553 continue
554
555 with open('/sys/class/net/%s/address' % iface, 'r') as f:
556 macs.append(f.read().strip())
557
558 return ';'.join(macs)
559 except Exception:
560 pass
561
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800562 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800563
564 def Reset(self):
565 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800566 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800567 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800568 self._last_ping = 0
569 self._requests = {}
570
571 def SendMessage(self, msg):
572 """Serialize the message and send it through the socket."""
573 self._sock.send(json.dumps(msg) + _SEPARATOR)
574
575 def SendRequest(self, name, args, handler=None,
576 timeout=_REQUEST_TIMEOUT_SECS):
577 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800578 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800579
580 rid = str(uuid.uuid4())
581 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800582 if timeout >= 0:
583 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800584 self.SendMessage(msg)
585
586 def SendResponse(self, omsg, status, params=None):
587 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
588 self.SendMessage(msg)
589
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800590 def HandlePTYControl(self, fd, control_string):
591 msg = json.loads(control_string)
592 command = msg['command']
593 params = msg['params']
594 if command == 'resize':
595 # some error happened on websocket
596 if len(params) != 2:
597 return
598 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
599 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
600 else:
601 logging.warn('Invalid request command "%s"', command)
602
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800603 def SpawnPTYServer(self, _):
604 """Spawn a PTY server and forward I/O to the TCP socket."""
605 logging.info('SpawnPTYServer: started')
606
607 pid, fd = os.forkpty()
608 if pid == 0:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800609 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
610 try:
611 server = GhostRPCServer()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800612 server.RegisterTTY(self._session_id, ttyname)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800613 server.RegisterSession(self._session_id, os.getpid())
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800614 except Exception:
615 # If ghost is launched without RPC server, the call will fail but we
616 # can ignore it.
617 pass
618
619 # The directory that contains the current running ghost script
620 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
621
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800622 env = os.environ.copy()
623 env['USER'] = os.getenv('USER', 'root')
624 env['HOME'] = os.getenv('HOME', '/root')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800625 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800626 os.chdir(env['HOME'])
627 os.execve(_SHELL, [_SHELL], env)
628 else:
629 try:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800630 control_state = None
631 control_string = ''
632 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800633 while True:
634 rd, _, _ = select.select([self._sock, fd], [], [])
635
636 if fd in rd:
637 self._sock.send(os.read(fd, _BUFSIZE))
638
639 if self._sock in rd:
640 ret = self._sock.recv(_BUFSIZE)
641 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800642 raise RuntimeError('socket closed')
643 while ret:
644 if control_state:
645 if chr(_CONTROL_END) in ret:
646 index = ret.index(chr(_CONTROL_END))
647 control_string += ret[:index]
648 self.HandlePTYControl(fd, control_string)
649 control_state = None
650 control_string = ''
651 ret = ret[index+1:]
652 else:
653 control_string += ret
654 ret = ''
655 else:
656 if chr(_CONTROL_START) in ret:
657 control_state = _CONTROL_START
658 index = ret.index(chr(_CONTROL_START))
659 write_buffer += ret[:index]
660 ret = ret[index+1:]
661 else:
662 write_buffer += ret
663 ret = ''
664 if write_buffer:
665 os.write(fd, write_buffer)
666 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800667 except (OSError, socket.error, RuntimeError):
668 self._sock.close()
669 logging.info('SpawnPTYServer: terminated')
670 sys.exit(0)
671
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800672 def SpawnShellServer(self, _):
673 """Spawn a shell server and forward input/output from/to the TCP socket."""
674 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800675
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800676 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800677 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
678 shell=True)
679
680 def make_non_block(fd):
681 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
682 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
683
684 make_non_block(p.stdout)
685 make_non_block(p.stderr)
686
687 try:
688 while True:
689 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800690 if p.stdout in rd:
691 self._sock.send(p.stdout.read(_BUFSIZE))
692
693 if p.stderr in rd:
694 self._sock.send(p.stderr.read(_BUFSIZE))
695
696 if self._sock in rd:
697 ret = self._sock.recv(_BUFSIZE)
698 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800699 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800700 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800701 p.poll()
702 if p.returncode != None:
703 break
704 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800705 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800706 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800707 sys.exit(0)
708
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800709 def InitiateFileOperation(self, _):
710 if self._file_op[0] == 'download':
711 size = os.stat(self._file_op[1]).st_size
712 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800713 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800714 'filename': os.path.basename(self._file_op[1]),
715 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800716 elif self._file_op[0] == 'upload':
717 self.SendRequest('clear_to_upload', {}, timeout=-1)
718 self.StartUploadServer()
719 else:
720 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800721
722 def StartDownloadServer(self):
723 logging.info('StartDownloadServer: started')
724
725 try:
726 with open(self._file_op[1], 'rb') as f:
727 while True:
728 data = f.read(_BLOCK_SIZE)
729 if len(data) == 0:
730 break
731 self._sock.send(data)
732 except Exception as e:
733 logging.error('StartDownloadServer: %s', e)
734 finally:
735 self._sock.close()
736
737 logging.info('StartDownloadServer: terminated')
738 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800739
Wei-Ning Huange2981862015-08-03 15:03:08 +0800740 def StartUploadServer(self):
741 logging.info('StartUploadServer: started')
742
743 try:
744 target_dir = os.getenv('HOME', '/tmp')
745
746 # Get the client's working dir, which is our target upload dir
747 if self._file_op[2]:
748 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
749
750 self._sock.setblocking(False)
751 with open(os.path.join(target_dir, self._file_op[1]), 'wb') as f:
752 while True:
753 rd, _, _ = select.select([self._sock], [], [])
754 if self._sock in rd:
755 buf = self._sock.recv(_BLOCK_SIZE)
756 if len(buf) == 0:
757 break
758 f.write(buf)
759 except socket.error as e:
760 logging.error('StartUploadServer: socket error: %s', e)
761 except Exception as e:
762 logging.error('StartUploadServer: %s', e)
763 finally:
764 self._sock.close()
765
766 logging.info('StartUploadServer: terminated')
767 sys.exit(0)
768
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800769 def Ping(self):
770 def timeout_handler(x):
771 if x is None:
772 raise PingTimeoutError
773
774 self._last_ping = self.Timestamp()
775 self.SendRequest('ping', {}, timeout_handler, 5)
776
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800777 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800778 command = msg['name']
779 params = msg['params']
780
781 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800782 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800783 elif command == 'terminal':
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800784 self.SpawnGhost(self.TERMINAL, params['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800785 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800786 elif command == 'shell':
787 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800788 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800789 elif command == 'file_download':
790 self.SpawnGhost(self.FILE, params['sid'],
791 file_op=('download', params['filename'], None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800792 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800793 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800794 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800795 elif command == 'file_upload':
796 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
797 self.SpawnGhost(self.FILE, params['sid'],
798 file_op=('upload', params['filename'], pid))
799 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800800
801 def HandleResponse(self, response):
802 rid = str(response['rid'])
803 if rid in self._requests:
804 handler = self._requests[rid][2]
805 del self._requests[rid]
806 if callable(handler):
807 handler(response)
808 else:
809 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800810 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800811
812 def ParseMessage(self):
813 msgs_json = self._buf.split(_SEPARATOR)
814 self._buf = msgs_json.pop()
815
816 for msg_json in msgs_json:
817 try:
818 msg = json.loads(msg_json)
819 except ValueError:
820 # Ignore mal-formed message.
821 continue
822
823 if 'name' in msg:
824 self.HandleRequest(msg)
825 elif 'response' in msg:
826 self.HandleResponse(msg)
827 else: # Ingnore mal-formed message.
828 pass
829
830 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800831 """Scans for pending requests which have timed out.
832
833 If any timed-out requests are discovered, their handler is called with the
834 special response value of None.
835 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800836 for rid in self._requests.keys()[:]:
837 request_time, timeout, handler = self._requests[rid]
838 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800839 if callable(handler):
840 handler(None)
841 else:
842 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800843 del self._requests[rid]
844
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800845 def InitiateDownload(self):
846 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800847 sid = self._ttyname_to_sid[ttyname]
848 self.SpawnGhost(self.FILE, terminal_sid=sid,
849 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800850
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800851 def Listen(self):
852 try:
853 while True:
854 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
855
856 if self._sock in rds:
857 self._buf += self._sock.recv(_BUFSIZE)
858 self.ParseMessage()
859
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800860 if (self._mode == self.AGENT and
861 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800862 self.Ping()
863 self.ScanForTimeoutRequests()
864
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800865 if not self._download_queue.empty():
866 self.InitiateDownload()
867
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800868 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800869 self.Reset()
870 break
871 except socket.error:
872 raise RuntimeError('Connection dropped')
873 except PingTimeoutError:
874 raise RuntimeError('Connection timeout')
875 finally:
876 self._sock.close()
877
878 self._queue.put('resume')
879
880 if self._mode != Ghost.AGENT:
881 sys.exit(1)
882
883 def Register(self):
884 non_local = {}
885 for addr in self._overlord_addrs:
886 non_local['addr'] = addr
887 def registered(response):
888 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800889 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800890 raise RuntimeError('Register request timeout')
891 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Joel Kitching22b89042015-08-06 18:23:29 +0800892 if self._forward_ssh:
893 logging.info('Starting target SSH port negotiation')
894 self.NegotiateTargetSSHPort()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800895 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800896
897 try:
898 logging.info('Trying %s:%d ...', *addr)
899 self.Reset()
900 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
901 self._sock.settimeout(_PING_TIMEOUT)
902 self._sock.connect(addr)
903
904 logging.info('Connection established, registering...')
905 handler = {
906 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800907 Ghost.TERMINAL: self.SpawnPTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800908 Ghost.SHELL: self.SpawnShellServer,
909 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800910 }[self._mode]
911
912 # Machine ID may change if MAC address is used (USB-ethernet dongle
913 # plugged/unplugged)
914 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800915 self.SendRequest('register',
916 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800917 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800918 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800919 except socket.error:
920 pass
921 else:
922 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800923 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800924 self.Listen()
925
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800926 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800927
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800928 def Reconnect(self):
929 logging.info('Received reconnect request from RPC server, reconnecting...')
930 self._reset.set()
931
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800932 def AddToDownloadQueue(self, ttyname, filename):
933 self._download_queue.put((ttyname, filename))
934
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800935 def RegisterTTY(self, session_id, ttyname):
936 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800937
938 def RegisterSession(self, session_id, process_id):
939 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800940
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800941 def StartLanDiscovery(self):
942 """Start to listen to LAN discovery packet at
943 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800944
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800945 def thread_func():
946 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
947 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
948 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800949 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800950 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
951 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800952 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800953 return
954
955 logging.info('LAN Discovery: started')
956 while True:
957 rd, _, _ = select.select([s], [], [], 1)
958
959 if s in rd:
960 data, source_addr = s.recvfrom(_BUFSIZE)
961 parts = data.split()
962 if parts[0] == 'OVERLORD':
963 ip, port = parts[1].split(':')
964 if not ip:
965 ip = source_addr[0]
966 self._queue.put((ip, int(port)), True)
967
968 try:
969 obj = self._queue.get(False)
970 except Queue.Empty:
971 pass
972 else:
973 if type(obj) is not str:
974 self._queue.put(obj)
975 elif obj == 'pause':
976 logging.info('LAN Discovery: paused')
977 while obj != 'resume':
978 obj = self._queue.get(True)
979 logging.info('LAN Discovery: resumed')
980
981 t = threading.Thread(target=thread_func)
982 t.daemon = True
983 t.start()
984
985 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800986 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800987 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
988 logRequests=False)
989 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800990 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800991 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800992 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800993 t = threading.Thread(target=rpc_server.serve_forever)
994 t.daemon = True
995 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800996
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800997 def ScanServer(self):
998 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
999 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1000 if addr not in self._overlord_addrs:
1001 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001002
Joel Kitching22b89042015-08-06 18:23:29 +08001003 def NegotiateTargetSSHPort(self):
1004 """Request-receive target SSH port forwarding loop.
1005
1006 Repeatedly attempts to forward this machine's SSH port to target. It
1007 bounces back and forth between RequestPort and ReceivePort when a new port
1008 is required. ReceivePort starts a new thread so that the main ghost thread
1009 may continue running.
1010 """
1011 # Sanity check for identity file.
1012 if not os.path.isfile(self._target_identity_file):
1013 logging.info('No target host identity file: not negotiating '
1014 'target SSH port')
1015 return
1016
1017 def PollSSHPortForwarder():
1018 def ThreadFunc():
1019 while True:
1020 state = self._ssh_port_forwarder.GetState()
1021
1022 # Connected successfully.
1023 if state == SSHPortForwarder.INITIALIZED:
1024 # The SSH port forward has succeeded! Let's tell Overlord.
1025 port = self._ssh_port_forwarder.GetDstPort()
1026 RegisterPort(port)
1027
1028 # We've given up... continue to the next port.
1029 elif state == SSHPortForwarder.FAILED:
1030 break
1031
1032 # Either CONNECTING or INITIALIZED.
1033 self._ssh_port_forwarder.Wait()
1034
1035 # Only request a new port if we are still registered to Overlord.
1036 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1037 # which will take care of it.
1038 try:
1039 RequestPort()
1040 except Exception:
1041 logging.info('Failed to request port, will wait for next connection')
1042 self._ssh_port_forwarder = None
1043
1044 t = threading.Thread(target=ThreadFunc)
1045 t.daemon = True
1046 t.start()
1047
1048 def ReceivePort(response):
1049 # If the response times out, this version of Overlord may not support SSH
1050 # port negotiation. Give up on port negotiation process.
1051 if response is None:
1052 return
1053
1054 port = int(response['params']['port'])
1055 logging.info('Received target SSH port: %d', port)
1056
1057 if (self._ssh_port_forwarder and
1058 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1059 logging.info('Unexpectedly received a target SSH port')
1060 return
1061
1062 # Try forwarding SSH port to target.
1063 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1064 src_port=22,
1065 dst_port=port,
1066 user='ghost',
1067 identity_file=self._target_identity_file,
1068 host=self._connected_addr[0]) # Use Overlord host as target.
1069
1070 # Creates a new thread.
1071 PollSSHPortForwarder()
1072
1073 def RequestPort():
1074 logging.info('Requesting new target SSH port')
1075 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1076
1077 def RegisterPort(port):
1078 logging.info('Registering target SSH port %d', port)
1079 self.SendRequest(
1080 'register_target_ssh_port',
1081 {'port': port}, RegisterPortResponse, 5)
1082
1083 def RegisterPortResponse(response):
1084 # Overlord responded to request_port already. If register_port fails,
1085 # something might be in an inconsistent state, so trigger a reconnect
1086 # via PingTimeoutError.
1087 if response is None:
1088 raise PingTimeoutError
1089 logging.info('Registering target SSH port acknowledged')
1090
1091 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1092 # manually report the port to target, since SSHPortForwarder is currently
1093 # blocking.
1094 if (self._ssh_port_forwarder and
1095 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1096 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1097 if not self._ssh_port_forwarder:
1098 RequestPort()
1099
1100 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001101 logging.info('%s started', self.MODE_NAME[self._mode])
1102 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001103 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001104
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001105 # We don't care about child process's return code, not wait is needed. This
1106 # is used to prevent zombie process from lingering in the system.
1107 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001108
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001109 if lan_disc:
1110 self.StartLanDiscovery()
1111
1112 if rpc_server:
1113 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001114
Joel Kitching22b89042015-08-06 18:23:29 +08001115 self._forward_ssh = forward_ssh
1116
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001117 try:
1118 while True:
1119 try:
1120 addr = self._queue.get(False)
1121 except Queue.Empty:
1122 pass
1123 else:
1124 if type(addr) == tuple and addr not in self._overlord_addrs:
1125 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1126 self._overlord_addrs.append(addr)
1127
1128 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001129 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001130 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001131 # Don't show stack trace for RuntimeError, which we use in this file for
1132 # plausible and expected errors (such as can't connect to server).
1133 except RuntimeError as e:
1134 logging.info('%s: %s, retrying in %ds',
1135 e.__class__.__name__, e.message, _RETRY_INTERVAL)
1136 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001137 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001138 _, _, exc_traceback = sys.exc_info()
1139 traceback.print_tb(exc_traceback)
1140 logging.info('%s: %s, retrying in %ds',
1141 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001142 time.sleep(_RETRY_INTERVAL)
1143
1144 self.Reset()
1145 except KeyboardInterrupt:
1146 logging.error('Received keyboard interrupt, quit')
1147 sys.exit(0)
1148
1149
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001150def GhostRPCServer():
1151 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1152
1153
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001154def DownloadFile(filename):
1155 filepath = os.path.abspath(filename)
1156 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001157 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001158 sys.exit(1)
1159
1160 # Check if we actually have permission to read the file
1161 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001162 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001163 sys.exit(1)
1164
1165 server = GhostRPCServer()
1166 server.AddToDownloadQueue(os.ttyname(0), filepath)
1167 sys.exit(0)
1168
1169
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001170def main():
1171 logger = logging.getLogger()
1172 logger.setLevel(logging.INFO)
1173
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001174 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001175 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1176 default=None, help='use MID as machine ID')
1177 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1178 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001179 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1180 default=True, help='disable LAN discovery')
1181 parser.add_argument('--no-rpc-server', dest='rpc_server',
1182 action='store_false', default=True,
1183 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001184 parser.add_argument('--forward-ssh', dest='forward_ssh',
1185 action='store_true', default=False,
1186 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001187 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001188 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001189 help='file containing the JSON representation of client '
1190 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001191 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1192 default=None, help='file to download')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001193 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1194 nargs='*', help='overlord server address')
1195 args = parser.parse_args()
1196
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001197 if args.download:
1198 DownloadFile(args.download)
1199
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001200 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001201 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001202
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001203 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001204 if args.prop_file:
1205 g.LoadPropertiesFromFile(args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001206 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1207
1208
1209def _SigtermHandler(*_):
1210 """Ensure that SSH processes also get killed on a sigterm signal.
1211
1212 By also passing the sigterm signal onto the process group, we ensure that any
1213 child SSH processes will also get killed.
1214
1215 Source:
1216 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1217 """
1218 logging.info('SIGTERM handler: shutting down')
1219 if not _SigtermHandler.SIGTERM_SENT:
1220 _SigtermHandler.SIGTERM_SENT = True
1221 logging.info('Sending TERM to process group')
1222 os.killpg(0, signal.SIGTERM)
1223 sys.exit()
1224_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001225
1226
1227if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001228 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001229 main()