blob: 622eb52af7e6d6fc31d2633a8158dbc5c46434aa [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 Huang58833882015-09-16 16:52:37 +0800386 url = 'http%s://%s:%d/upgrade/ghost.py' % ('s' if self.UseSSL() else '',
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800387 self._connected_addr[0], _OVERLORD_HTTP_PORT)
388
389 # Download sha1sum for ghost.py for verification
390 try:
391 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
392 if f.getcode() != 200:
393 raise RuntimeError('HTTP status %d' % f.getcode())
394 sha1sum = f.read().strip()
395 except Exception:
396 logging.error('Upgrade: failed to download sha1sum file, abort')
397 return
398
399 if self.GetFileSha1(scriptpath) == sha1sum:
400 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
401 return
402
403 # Download upgrade version of ghost.py
404 try:
405 with contextlib.closing(urllib.urlopen(url)) as f:
406 if f.getcode() != 200:
407 raise RuntimeError('HTTP status %d' % f.getcode())
408 data = f.read()
409 except Exception:
410 logging.error('Upgrade: failed to download upgrade, abort')
411 return
412
413 # Compare SHA1 sum
414 if hashlib.sha1(data).hexdigest() != sha1sum:
415 logging.error('Upgrade: sha1sum mismatch, abort')
416 return
417
418 python = os.readlink('/proc/self/exe')
419 try:
420 with open(scriptpath, 'w') as f:
421 f.write(data)
422 except Exception:
423 logging.error('Upgrade: failed to write upgrade onto disk, abort')
424 return
425
426 logging.info('Upgrade: restarting ghost...')
427 self.CloseSockets()
428 self.SetIgnoreChild(False)
429 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
430
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800431 def LoadPropertiesFromFile(self, filename):
432 try:
433 with open(filename, 'r') as f:
434 self._properties = json.loads(f.read())
435 except Exception as e:
436 logging.exception('LoadPropertiesFromFile: ' + str(e))
437
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800438 def CloseSockets(self):
439 # Close sockets opened by parent process, since we don't use it anymore.
440 for fd in os.listdir('/proc/self/fd/'):
441 try:
442 real_fd = os.readlink('/proc/self/fd/%s' % fd)
443 if real_fd.startswith('socket'):
444 os.close(int(fd))
445 except Exception:
446 pass
447
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800448 def SpawnGhost(self, mode, sid=None, terminal_sid=None, command=None,
449 file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800450 """Spawn a child ghost with specific mode.
451
452 Returns:
453 The spawned child process pid.
454 """
Joel Kitching22b89042015-08-06 18:23:29 +0800455 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800456 self.SetIgnoreChild(False)
457
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800458 pid = os.fork()
459 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800460 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800461 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
462 terminal_sid, command, file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800463 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800464 sys.exit(0)
465 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800466 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800467 return pid
468
469 def Timestamp(self):
470 return int(time.time())
471
472 def GetGateWayIP(self):
473 with open('/proc/net/route', 'r') as f:
474 lines = f.readlines()
475
476 ips = []
477 for line in lines:
478 parts = line.split('\t')
479 if parts[2] == '00000000':
480 continue
481
482 try:
483 h = parts[2].decode('hex')
484 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
485 except TypeError:
486 pass
487
488 return ips
489
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800490 def GetShopfloorIP(self):
491 try:
492 import factory_common # pylint: disable=W0612
493 from cros.factory.test import shopfloor
494
495 url = shopfloor.get_server_url()
496 match = re.match(r'^https?://(.*):.*$', url)
497 if match:
498 return [match.group(1)]
499 except Exception:
500 pass
501 return []
502
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800503 def GetMachineID(self):
504 """Generates machine-dependent ID string for a machine.
505 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800506 1. factory device_id
507 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800508 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
509 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800510 We follow the listed order to generate machine ID, and fallback to the next
511 alternative if the previous doesn't work.
512 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800513 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800514 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800515 elif self._mid:
516 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800517
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800518 # Try factory device id
519 try:
520 import factory_common # pylint: disable=W0612
521 from cros.factory.test import event_log
522 with open(event_log.DEVICE_ID_PATH) as f:
523 return f.read().strip()
524 except Exception:
525 pass
526
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800527 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800528 try:
529 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
530 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800531 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532 stdout, _ = p.communicate()
533 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800534 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800535 return stdout.strip()
536 except Exception:
537 pass
538
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800539 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800540 try:
541 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
542 return f.read().strip()
543 except Exception:
544 pass
545
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800546 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800547 try:
548 macs = []
549 ifaces = sorted(os.listdir('/sys/class/net'))
550 for iface in ifaces:
551 if iface == 'lo':
552 continue
553
554 with open('/sys/class/net/%s/address' % iface, 'r') as f:
555 macs.append(f.read().strip())
556
557 return ';'.join(macs)
558 except Exception:
559 pass
560
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800561 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800562
563 def Reset(self):
564 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800565 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800566 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800567 self._last_ping = 0
568 self._requests = {}
569
570 def SendMessage(self, msg):
571 """Serialize the message and send it through the socket."""
572 self._sock.send(json.dumps(msg) + _SEPARATOR)
573
574 def SendRequest(self, name, args, handler=None,
575 timeout=_REQUEST_TIMEOUT_SECS):
576 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800577 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800578
579 rid = str(uuid.uuid4())
580 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800581 if timeout >= 0:
582 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800583 self.SendMessage(msg)
584
585 def SendResponse(self, omsg, status, params=None):
586 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
587 self.SendMessage(msg)
588
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800589 def HandlePTYControl(self, fd, control_string):
590 msg = json.loads(control_string)
591 command = msg['command']
592 params = msg['params']
593 if command == 'resize':
594 # some error happened on websocket
595 if len(params) != 2:
596 return
597 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
598 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
599 else:
600 logging.warn('Invalid request command "%s"', command)
601
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800602 def SpawnPTYServer(self, _):
603 """Spawn a PTY server and forward I/O to the TCP socket."""
604 logging.info('SpawnPTYServer: started')
605
606 pid, fd = os.forkpty()
607 if pid == 0:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800608 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
609 try:
610 server = GhostRPCServer()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800611 server.RegisterTTY(self._session_id, ttyname)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800612 server.RegisterSession(self._session_id, os.getpid())
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800613 except Exception:
614 # If ghost is launched without RPC server, the call will fail but we
615 # can ignore it.
616 pass
617
618 # The directory that contains the current running ghost script
619 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
620
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800621 env = os.environ.copy()
622 env['USER'] = os.getenv('USER', 'root')
623 env['HOME'] = os.getenv('HOME', '/root')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800624 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800625 os.chdir(env['HOME'])
626 os.execve(_SHELL, [_SHELL], env)
627 else:
628 try:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800629 control_state = None
630 control_string = ''
631 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800632 while True:
633 rd, _, _ = select.select([self._sock, fd], [], [])
634
635 if fd in rd:
636 self._sock.send(os.read(fd, _BUFSIZE))
637
638 if self._sock in rd:
639 ret = self._sock.recv(_BUFSIZE)
640 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800641 raise RuntimeError('socket closed')
642 while ret:
643 if control_state:
644 if chr(_CONTROL_END) in ret:
645 index = ret.index(chr(_CONTROL_END))
646 control_string += ret[:index]
647 self.HandlePTYControl(fd, control_string)
648 control_state = None
649 control_string = ''
650 ret = ret[index+1:]
651 else:
652 control_string += ret
653 ret = ''
654 else:
655 if chr(_CONTROL_START) in ret:
656 control_state = _CONTROL_START
657 index = ret.index(chr(_CONTROL_START))
658 write_buffer += ret[:index]
659 ret = ret[index+1:]
660 else:
661 write_buffer += ret
662 ret = ''
663 if write_buffer:
664 os.write(fd, write_buffer)
665 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800666 except (OSError, socket.error, RuntimeError):
667 self._sock.close()
668 logging.info('SpawnPTYServer: terminated')
669 sys.exit(0)
670
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800671 def SpawnShellServer(self, _):
672 """Spawn a shell server and forward input/output from/to the TCP socket."""
673 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800674
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800675 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800676 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
677 shell=True)
678
679 def make_non_block(fd):
680 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
681 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
682
683 make_non_block(p.stdout)
684 make_non_block(p.stderr)
685
686 try:
687 while True:
688 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800689 if p.stdout in rd:
690 self._sock.send(p.stdout.read(_BUFSIZE))
691
692 if p.stderr in rd:
693 self._sock.send(p.stderr.read(_BUFSIZE))
694
695 if self._sock in rd:
696 ret = self._sock.recv(_BUFSIZE)
697 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800698 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800699 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800700 p.poll()
701 if p.returncode != None:
702 break
703 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800704 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800705 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800706 sys.exit(0)
707
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800708 def InitiateFileOperation(self, _):
709 if self._file_op[0] == 'download':
710 size = os.stat(self._file_op[1]).st_size
711 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800712 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800713 'filename': os.path.basename(self._file_op[1]),
714 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800715 elif self._file_op[0] == 'upload':
716 self.SendRequest('clear_to_upload', {}, timeout=-1)
717 self.StartUploadServer()
718 else:
719 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800720
721 def StartDownloadServer(self):
722 logging.info('StartDownloadServer: started')
723
724 try:
725 with open(self._file_op[1], 'rb') as f:
726 while True:
727 data = f.read(_BLOCK_SIZE)
728 if len(data) == 0:
729 break
730 self._sock.send(data)
731 except Exception as e:
732 logging.error('StartDownloadServer: %s', e)
733 finally:
734 self._sock.close()
735
736 logging.info('StartDownloadServer: terminated')
737 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800738
Wei-Ning Huange2981862015-08-03 15:03:08 +0800739 def StartUploadServer(self):
740 logging.info('StartUploadServer: started')
741
742 try:
743 target_dir = os.getenv('HOME', '/tmp')
744
745 # Get the client's working dir, which is our target upload dir
746 if self._file_op[2]:
747 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
748
749 self._sock.setblocking(False)
750 with open(os.path.join(target_dir, self._file_op[1]), 'wb') as f:
751 while True:
752 rd, _, _ = select.select([self._sock], [], [])
753 if self._sock in rd:
754 buf = self._sock.recv(_BLOCK_SIZE)
755 if len(buf) == 0:
756 break
757 f.write(buf)
758 except socket.error as e:
759 logging.error('StartUploadServer: socket error: %s', e)
760 except Exception as e:
761 logging.error('StartUploadServer: %s', e)
762 finally:
763 self._sock.close()
764
765 logging.info('StartUploadServer: terminated')
766 sys.exit(0)
767
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800768 def Ping(self):
769 def timeout_handler(x):
770 if x is None:
771 raise PingTimeoutError
772
773 self._last_ping = self.Timestamp()
774 self.SendRequest('ping', {}, timeout_handler, 5)
775
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800776 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800777 command = msg['name']
778 params = msg['params']
779
780 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800781 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800782 elif command == 'terminal':
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800783 self.SpawnGhost(self.TERMINAL, params['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800784 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800785 elif command == 'shell':
786 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800787 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800788 elif command == 'file_download':
789 self.SpawnGhost(self.FILE, params['sid'],
790 file_op=('download', params['filename'], None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800791 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800792 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800793 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800794 elif command == 'file_upload':
795 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
796 self.SpawnGhost(self.FILE, params['sid'],
797 file_op=('upload', params['filename'], pid))
798 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800799
800 def HandleResponse(self, response):
801 rid = str(response['rid'])
802 if rid in self._requests:
803 handler = self._requests[rid][2]
804 del self._requests[rid]
805 if callable(handler):
806 handler(response)
807 else:
808 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800809 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800810
811 def ParseMessage(self):
812 msgs_json = self._buf.split(_SEPARATOR)
813 self._buf = msgs_json.pop()
814
815 for msg_json in msgs_json:
816 try:
817 msg = json.loads(msg_json)
818 except ValueError:
819 # Ignore mal-formed message.
820 continue
821
822 if 'name' in msg:
823 self.HandleRequest(msg)
824 elif 'response' in msg:
825 self.HandleResponse(msg)
826 else: # Ingnore mal-formed message.
827 pass
828
829 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800830 """Scans for pending requests which have timed out.
831
832 If any timed-out requests are discovered, their handler is called with the
833 special response value of None.
834 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800835 for rid in self._requests.keys()[:]:
836 request_time, timeout, handler = self._requests[rid]
837 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800838 if callable(handler):
839 handler(None)
840 else:
841 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800842 del self._requests[rid]
843
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800844 def InitiateDownload(self):
845 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800846 sid = self._ttyname_to_sid[ttyname]
847 self.SpawnGhost(self.FILE, terminal_sid=sid,
848 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800849
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800850 def Listen(self):
851 try:
852 while True:
853 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
854
855 if self._sock in rds:
856 self._buf += self._sock.recv(_BUFSIZE)
857 self.ParseMessage()
858
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800859 if (self._mode == self.AGENT and
860 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800861 self.Ping()
862 self.ScanForTimeoutRequests()
863
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800864 if not self._download_queue.empty():
865 self.InitiateDownload()
866
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800867 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800868 self.Reset()
869 break
870 except socket.error:
871 raise RuntimeError('Connection dropped')
872 except PingTimeoutError:
873 raise RuntimeError('Connection timeout')
874 finally:
875 self._sock.close()
876
877 self._queue.put('resume')
878
879 if self._mode != Ghost.AGENT:
880 sys.exit(1)
881
882 def Register(self):
883 non_local = {}
884 for addr in self._overlord_addrs:
885 non_local['addr'] = addr
886 def registered(response):
887 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800888 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800889 raise RuntimeError('Register request timeout')
890 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Joel Kitching22b89042015-08-06 18:23:29 +0800891 if self._forward_ssh:
892 logging.info('Starting target SSH port negotiation')
893 self.NegotiateTargetSSHPort()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800894 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800895
896 try:
897 logging.info('Trying %s:%d ...', *addr)
898 self.Reset()
899 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
900 self._sock.settimeout(_PING_TIMEOUT)
901 self._sock.connect(addr)
902
903 logging.info('Connection established, registering...')
904 handler = {
905 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800906 Ghost.TERMINAL: self.SpawnPTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800907 Ghost.SHELL: self.SpawnShellServer,
908 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800909 }[self._mode]
910
911 # Machine ID may change if MAC address is used (USB-ethernet dongle
912 # plugged/unplugged)
913 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800914 self.SendRequest('register',
915 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800916 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800917 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800918 except socket.error:
919 pass
920 else:
921 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800922 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800923 self.Listen()
924
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800925 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800926
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800927 def Reconnect(self):
928 logging.info('Received reconnect request from RPC server, reconnecting...')
929 self._reset.set()
930
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800931 def AddToDownloadQueue(self, ttyname, filename):
932 self._download_queue.put((ttyname, filename))
933
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800934 def RegisterTTY(self, session_id, ttyname):
935 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800936
937 def RegisterSession(self, session_id, process_id):
938 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800939
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800940 def StartLanDiscovery(self):
941 """Start to listen to LAN discovery packet at
942 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800943
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800944 def thread_func():
945 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
946 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
947 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800948 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800949 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
950 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800951 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800952 return
953
954 logging.info('LAN Discovery: started')
955 while True:
956 rd, _, _ = select.select([s], [], [], 1)
957
958 if s in rd:
959 data, source_addr = s.recvfrom(_BUFSIZE)
960 parts = data.split()
961 if parts[0] == 'OVERLORD':
962 ip, port = parts[1].split(':')
963 if not ip:
964 ip = source_addr[0]
965 self._queue.put((ip, int(port)), True)
966
967 try:
968 obj = self._queue.get(False)
969 except Queue.Empty:
970 pass
971 else:
972 if type(obj) is not str:
973 self._queue.put(obj)
974 elif obj == 'pause':
975 logging.info('LAN Discovery: paused')
976 while obj != 'resume':
977 obj = self._queue.get(True)
978 logging.info('LAN Discovery: resumed')
979
980 t = threading.Thread(target=thread_func)
981 t.daemon = True
982 t.start()
983
984 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800985 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800986 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
987 logRequests=False)
988 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800989 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800990 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800991 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800992 t = threading.Thread(target=rpc_server.serve_forever)
993 t.daemon = True
994 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800995
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800996 def ScanServer(self):
997 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
998 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
999 if addr not in self._overlord_addrs:
1000 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001001
Joel Kitching22b89042015-08-06 18:23:29 +08001002 def NegotiateTargetSSHPort(self):
1003 """Request-receive target SSH port forwarding loop.
1004
1005 Repeatedly attempts to forward this machine's SSH port to target. It
1006 bounces back and forth between RequestPort and ReceivePort when a new port
1007 is required. ReceivePort starts a new thread so that the main ghost thread
1008 may continue running.
1009 """
1010 # Sanity check for identity file.
1011 if not os.path.isfile(self._target_identity_file):
1012 logging.info('No target host identity file: not negotiating '
1013 'target SSH port')
1014 return
1015
1016 def PollSSHPortForwarder():
1017 def ThreadFunc():
1018 while True:
1019 state = self._ssh_port_forwarder.GetState()
1020
1021 # Connected successfully.
1022 if state == SSHPortForwarder.INITIALIZED:
1023 # The SSH port forward has succeeded! Let's tell Overlord.
1024 port = self._ssh_port_forwarder.GetDstPort()
1025 RegisterPort(port)
1026
1027 # We've given up... continue to the next port.
1028 elif state == SSHPortForwarder.FAILED:
1029 break
1030
1031 # Either CONNECTING or INITIALIZED.
1032 self._ssh_port_forwarder.Wait()
1033
1034 # Only request a new port if we are still registered to Overlord.
1035 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1036 # which will take care of it.
1037 try:
1038 RequestPort()
1039 except Exception:
1040 logging.info('Failed to request port, will wait for next connection')
1041 self._ssh_port_forwarder = None
1042
1043 t = threading.Thread(target=ThreadFunc)
1044 t.daemon = True
1045 t.start()
1046
1047 def ReceivePort(response):
1048 # If the response times out, this version of Overlord may not support SSH
1049 # port negotiation. Give up on port negotiation process.
1050 if response is None:
1051 return
1052
1053 port = int(response['params']['port'])
1054 logging.info('Received target SSH port: %d', port)
1055
1056 if (self._ssh_port_forwarder and
1057 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1058 logging.info('Unexpectedly received a target SSH port')
1059 return
1060
1061 # Try forwarding SSH port to target.
1062 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1063 src_port=22,
1064 dst_port=port,
1065 user='ghost',
1066 identity_file=self._target_identity_file,
1067 host=self._connected_addr[0]) # Use Overlord host as target.
1068
1069 # Creates a new thread.
1070 PollSSHPortForwarder()
1071
1072 def RequestPort():
1073 logging.info('Requesting new target SSH port')
1074 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1075
1076 def RegisterPort(port):
1077 logging.info('Registering target SSH port %d', port)
1078 self.SendRequest(
1079 'register_target_ssh_port',
1080 {'port': port}, RegisterPortResponse, 5)
1081
1082 def RegisterPortResponse(response):
1083 # Overlord responded to request_port already. If register_port fails,
1084 # something might be in an inconsistent state, so trigger a reconnect
1085 # via PingTimeoutError.
1086 if response is None:
1087 raise PingTimeoutError
1088 logging.info('Registering target SSH port acknowledged')
1089
1090 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1091 # manually report the port to target, since SSHPortForwarder is currently
1092 # blocking.
1093 if (self._ssh_port_forwarder and
1094 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1095 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1096 if not self._ssh_port_forwarder:
1097 RequestPort()
1098
1099 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001100 logging.info('%s started', self.MODE_NAME[self._mode])
1101 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001102 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001103
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001104 # We don't care about child process's return code, not wait is needed. This
1105 # is used to prevent zombie process from lingering in the system.
1106 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001107
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001108 if lan_disc:
1109 self.StartLanDiscovery()
1110
1111 if rpc_server:
1112 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001113
Joel Kitching22b89042015-08-06 18:23:29 +08001114 self._forward_ssh = forward_ssh
1115
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001116 try:
1117 while True:
1118 try:
1119 addr = self._queue.get(False)
1120 except Queue.Empty:
1121 pass
1122 else:
1123 if type(addr) == tuple and addr not in self._overlord_addrs:
1124 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1125 self._overlord_addrs.append(addr)
1126
1127 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001128 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001129 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001130 # Don't show stack trace for RuntimeError, which we use in this file for
1131 # plausible and expected errors (such as can't connect to server).
1132 except RuntimeError as e:
1133 logging.info('%s: %s, retrying in %ds',
1134 e.__class__.__name__, e.message, _RETRY_INTERVAL)
1135 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001136 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001137 _, _, exc_traceback = sys.exc_info()
1138 traceback.print_tb(exc_traceback)
1139 logging.info('%s: %s, retrying in %ds',
1140 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001141 time.sleep(_RETRY_INTERVAL)
1142
1143 self.Reset()
1144 except KeyboardInterrupt:
1145 logging.error('Received keyboard interrupt, quit')
1146 sys.exit(0)
1147
1148
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001149def GhostRPCServer():
1150 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1151
1152
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001153def DownloadFile(filename):
1154 filepath = os.path.abspath(filename)
1155 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001156 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001157 sys.exit(1)
1158
1159 # Check if we actually have permission to read the file
1160 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001161 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001162 sys.exit(1)
1163
1164 server = GhostRPCServer()
1165 server.AddToDownloadQueue(os.ttyname(0), filepath)
1166 sys.exit(0)
1167
1168
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001169def main():
1170 logger = logging.getLogger()
1171 logger.setLevel(logging.INFO)
1172
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001173 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001174 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1175 default=None, help='use MID as machine ID')
1176 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1177 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001178 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1179 default=True, help='disable LAN discovery')
1180 parser.add_argument('--no-rpc-server', dest='rpc_server',
1181 action='store_false', default=True,
1182 help='disable RPC server')
Joel Kitching22b89042015-08-06 18:23:29 +08001183 parser.add_argument('--no-forward-ssh', dest='forward_ssh',
1184 action='store_false', default=True,
1185 help='disable target SSH port forwarding')
1186 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001187 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001188 help='file containing the JSON representation of client '
1189 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001190 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1191 default=None, help='file to download')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001192 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1193 nargs='*', help='overlord server address')
1194 args = parser.parse_args()
1195
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001196 if args.download:
1197 DownloadFile(args.download)
1198
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001199 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001200 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001201
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001202 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001203 if args.prop_file:
1204 g.LoadPropertiesFromFile(args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001205 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1206
1207
1208def _SigtermHandler(*_):
1209 """Ensure that SSH processes also get killed on a sigterm signal.
1210
1211 By also passing the sigterm signal onto the process group, we ensure that any
1212 child SSH processes will also get killed.
1213
1214 Source:
1215 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1216 """
1217 logging.info('SIGTERM handler: shutting down')
1218 if not _SigtermHandler.SIGTERM_SENT:
1219 _SigtermHandler.SIGTERM_SENT = True
1220 logging.info('Sending TERM to process group')
1221 os.killpg(0, signal.SIGTERM)
1222 sys.exit()
1223_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001224
1225
1226if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001227 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001228 main()