blob: 789cf632d5cc2334cd92416299bf8db68a7ac7f2 [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
370 def Upgrade(self):
371 logging.info('Upgrade: initiating upgrade sequence...')
372
373 scriptpath = os.path.abspath(sys.argv[0])
374 url = 'http://%s:%d/upgrade/ghost.py' % (
375 self._connected_addr[0], _OVERLORD_HTTP_PORT)
376
377 # Download sha1sum for ghost.py for verification
378 try:
379 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
380 if f.getcode() != 200:
381 raise RuntimeError('HTTP status %d' % f.getcode())
382 sha1sum = f.read().strip()
383 except Exception:
384 logging.error('Upgrade: failed to download sha1sum file, abort')
385 return
386
387 if self.GetFileSha1(scriptpath) == sha1sum:
388 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
389 return
390
391 # Download upgrade version of ghost.py
392 try:
393 with contextlib.closing(urllib.urlopen(url)) as f:
394 if f.getcode() != 200:
395 raise RuntimeError('HTTP status %d' % f.getcode())
396 data = f.read()
397 except Exception:
398 logging.error('Upgrade: failed to download upgrade, abort')
399 return
400
401 # Compare SHA1 sum
402 if hashlib.sha1(data).hexdigest() != sha1sum:
403 logging.error('Upgrade: sha1sum mismatch, abort')
404 return
405
406 python = os.readlink('/proc/self/exe')
407 try:
408 with open(scriptpath, 'w') as f:
409 f.write(data)
410 except Exception:
411 logging.error('Upgrade: failed to write upgrade onto disk, abort')
412 return
413
414 logging.info('Upgrade: restarting ghost...')
415 self.CloseSockets()
416 self.SetIgnoreChild(False)
417 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
418
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800419 def LoadPropertiesFromFile(self, filename):
420 try:
421 with open(filename, 'r') as f:
422 self._properties = json.loads(f.read())
423 except Exception as e:
424 logging.exception('LoadPropertiesFromFile: ' + str(e))
425
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800426 def CloseSockets(self):
427 # Close sockets opened by parent process, since we don't use it anymore.
428 for fd in os.listdir('/proc/self/fd/'):
429 try:
430 real_fd = os.readlink('/proc/self/fd/%s' % fd)
431 if real_fd.startswith('socket'):
432 os.close(int(fd))
433 except Exception:
434 pass
435
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800436 def SpawnGhost(self, mode, sid=None, terminal_sid=None, command=None,
437 file_op=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800438 """Spawn a child ghost with specific mode.
439
440 Returns:
441 The spawned child process pid.
442 """
Joel Kitching22b89042015-08-06 18:23:29 +0800443 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800444 self.SetIgnoreChild(False)
445
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800446 pid = os.fork()
447 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800448 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800449 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
450 terminal_sid, command, file_op)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800451 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800452 sys.exit(0)
453 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800454 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800455 return pid
456
457 def Timestamp(self):
458 return int(time.time())
459
460 def GetGateWayIP(self):
461 with open('/proc/net/route', 'r') as f:
462 lines = f.readlines()
463
464 ips = []
465 for line in lines:
466 parts = line.split('\t')
467 if parts[2] == '00000000':
468 continue
469
470 try:
471 h = parts[2].decode('hex')
472 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
473 except TypeError:
474 pass
475
476 return ips
477
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800478 def GetShopfloorIP(self):
479 try:
480 import factory_common # pylint: disable=W0612
481 from cros.factory.test import shopfloor
482
483 url = shopfloor.get_server_url()
484 match = re.match(r'^https?://(.*):.*$', url)
485 if match:
486 return [match.group(1)]
487 except Exception:
488 pass
489 return []
490
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800491 def GetMachineID(self):
492 """Generates machine-dependent ID string for a machine.
493 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800494 1. factory device_id
495 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800496 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
497 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800498 We follow the listed order to generate machine ID, and fallback to the next
499 alternative if the previous doesn't work.
500 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800501 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800502 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800503 elif self._mid:
504 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800505
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800506 # Try factory device id
507 try:
508 import factory_common # pylint: disable=W0612
509 from cros.factory.test import event_log
510 with open(event_log.DEVICE_ID_PATH) as f:
511 return f.read().strip()
512 except Exception:
513 pass
514
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800515 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800516 try:
517 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
518 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800519 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800520 stdout, _ = p.communicate()
521 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800522 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800523 return stdout.strip()
524 except Exception:
525 pass
526
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800527 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800528 try:
529 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
530 return f.read().strip()
531 except Exception:
532 pass
533
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800534 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800535 try:
536 macs = []
537 ifaces = sorted(os.listdir('/sys/class/net'))
538 for iface in ifaces:
539 if iface == 'lo':
540 continue
541
542 with open('/sys/class/net/%s/address' % iface, 'r') as f:
543 macs.append(f.read().strip())
544
545 return ';'.join(macs)
546 except Exception:
547 pass
548
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800549 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800550
551 def Reset(self):
552 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800553 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800554 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800555 self._last_ping = 0
556 self._requests = {}
557
558 def SendMessage(self, msg):
559 """Serialize the message and send it through the socket."""
560 self._sock.send(json.dumps(msg) + _SEPARATOR)
561
562 def SendRequest(self, name, args, handler=None,
563 timeout=_REQUEST_TIMEOUT_SECS):
564 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800565 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800566
567 rid = str(uuid.uuid4())
568 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800569 if timeout >= 0:
570 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800571 self.SendMessage(msg)
572
573 def SendResponse(self, omsg, status, params=None):
574 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
575 self.SendMessage(msg)
576
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800577 def HandlePTYControl(self, fd, control_string):
578 msg = json.loads(control_string)
579 command = msg['command']
580 params = msg['params']
581 if command == 'resize':
582 # some error happened on websocket
583 if len(params) != 2:
584 return
585 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
586 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
587 else:
588 logging.warn('Invalid request command "%s"', command)
589
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800590 def SpawnPTYServer(self, _):
591 """Spawn a PTY server and forward I/O to the TCP socket."""
592 logging.info('SpawnPTYServer: started')
593
594 pid, fd = os.forkpty()
595 if pid == 0:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800596 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
597 try:
598 server = GhostRPCServer()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800599 server.RegisterTTY(self._session_id, ttyname)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800600 server.RegisterSession(self._session_id, os.getpid())
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800601 except Exception:
602 # If ghost is launched without RPC server, the call will fail but we
603 # can ignore it.
604 pass
605
606 # The directory that contains the current running ghost script
607 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
608
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800609 env = os.environ.copy()
610 env['USER'] = os.getenv('USER', 'root')
611 env['HOME'] = os.getenv('HOME', '/root')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800612 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800613 os.chdir(env['HOME'])
614 os.execve(_SHELL, [_SHELL], env)
615 else:
616 try:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800617 control_state = None
618 control_string = ''
619 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800620 while True:
621 rd, _, _ = select.select([self._sock, fd], [], [])
622
623 if fd in rd:
624 self._sock.send(os.read(fd, _BUFSIZE))
625
626 if self._sock in rd:
627 ret = self._sock.recv(_BUFSIZE)
628 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800629 raise RuntimeError('socket closed')
630 while ret:
631 if control_state:
632 if chr(_CONTROL_END) in ret:
633 index = ret.index(chr(_CONTROL_END))
634 control_string += ret[:index]
635 self.HandlePTYControl(fd, control_string)
636 control_state = None
637 control_string = ''
638 ret = ret[index+1:]
639 else:
640 control_string += ret
641 ret = ''
642 else:
643 if chr(_CONTROL_START) in ret:
644 control_state = _CONTROL_START
645 index = ret.index(chr(_CONTROL_START))
646 write_buffer += ret[:index]
647 ret = ret[index+1:]
648 else:
649 write_buffer += ret
650 ret = ''
651 if write_buffer:
652 os.write(fd, write_buffer)
653 write_buffer = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800654 except (OSError, socket.error, RuntimeError):
655 self._sock.close()
656 logging.info('SpawnPTYServer: terminated')
657 sys.exit(0)
658
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800659 def SpawnShellServer(self, _):
660 """Spawn a shell server and forward input/output from/to the TCP socket."""
661 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800662
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800663 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800664 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
665 shell=True)
666
667 def make_non_block(fd):
668 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
669 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
670
671 make_non_block(p.stdout)
672 make_non_block(p.stderr)
673
674 try:
675 while True:
676 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800677 if p.stdout in rd:
678 self._sock.send(p.stdout.read(_BUFSIZE))
679
680 if p.stderr in rd:
681 self._sock.send(p.stderr.read(_BUFSIZE))
682
683 if self._sock in rd:
684 ret = self._sock.recv(_BUFSIZE)
685 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800686 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800687 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800688 p.poll()
689 if p.returncode != None:
690 break
691 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800692 self._sock.close()
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800693 logging.info('SpawnShellServer: terminated')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800694 sys.exit(0)
695
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800696 def InitiateFileOperation(self, _):
697 if self._file_op[0] == 'download':
698 size = os.stat(self._file_op[1]).st_size
699 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800700 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800701 'filename': os.path.basename(self._file_op[1]),
702 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800703 elif self._file_op[0] == 'upload':
704 self.SendRequest('clear_to_upload', {}, timeout=-1)
705 self.StartUploadServer()
706 else:
707 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800708
709 def StartDownloadServer(self):
710 logging.info('StartDownloadServer: started')
711
712 try:
713 with open(self._file_op[1], 'rb') as f:
714 while True:
715 data = f.read(_BLOCK_SIZE)
716 if len(data) == 0:
717 break
718 self._sock.send(data)
719 except Exception as e:
720 logging.error('StartDownloadServer: %s', e)
721 finally:
722 self._sock.close()
723
724 logging.info('StartDownloadServer: terminated')
725 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800726
Wei-Ning Huange2981862015-08-03 15:03:08 +0800727 def StartUploadServer(self):
728 logging.info('StartUploadServer: started')
729
730 try:
731 target_dir = os.getenv('HOME', '/tmp')
732
733 # Get the client's working dir, which is our target upload dir
734 if self._file_op[2]:
735 target_dir = os.readlink('/proc/%d/cwd' % self._file_op[2])
736
737 self._sock.setblocking(False)
738 with open(os.path.join(target_dir, self._file_op[1]), 'wb') as f:
739 while True:
740 rd, _, _ = select.select([self._sock], [], [])
741 if self._sock in rd:
742 buf = self._sock.recv(_BLOCK_SIZE)
743 if len(buf) == 0:
744 break
745 f.write(buf)
746 except socket.error as e:
747 logging.error('StartUploadServer: socket error: %s', e)
748 except Exception as e:
749 logging.error('StartUploadServer: %s', e)
750 finally:
751 self._sock.close()
752
753 logging.info('StartUploadServer: terminated')
754 sys.exit(0)
755
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800756 def Ping(self):
757 def timeout_handler(x):
758 if x is None:
759 raise PingTimeoutError
760
761 self._last_ping = self.Timestamp()
762 self.SendRequest('ping', {}, timeout_handler, 5)
763
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800764 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800765 command = msg['name']
766 params = msg['params']
767
768 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800769 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800770 elif command == 'terminal':
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800771 self.SpawnGhost(self.TERMINAL, params['sid'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800772 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800773 elif command == 'shell':
774 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800775 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800776 elif command == 'file_download':
777 self.SpawnGhost(self.FILE, params['sid'],
778 file_op=('download', params['filename'], None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800779 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800780 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800781 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800782 elif command == 'file_upload':
783 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
784 self.SpawnGhost(self.FILE, params['sid'],
785 file_op=('upload', params['filename'], pid))
786 self.SendResponse(msg, RESPONSE_SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800787
788 def HandleResponse(self, response):
789 rid = str(response['rid'])
790 if rid in self._requests:
791 handler = self._requests[rid][2]
792 del self._requests[rid]
793 if callable(handler):
794 handler(response)
795 else:
796 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800797 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800798
799 def ParseMessage(self):
800 msgs_json = self._buf.split(_SEPARATOR)
801 self._buf = msgs_json.pop()
802
803 for msg_json in msgs_json:
804 try:
805 msg = json.loads(msg_json)
806 except ValueError:
807 # Ignore mal-formed message.
808 continue
809
810 if 'name' in msg:
811 self.HandleRequest(msg)
812 elif 'response' in msg:
813 self.HandleResponse(msg)
814 else: # Ingnore mal-formed message.
815 pass
816
817 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800818 """Scans for pending requests which have timed out.
819
820 If any timed-out requests are discovered, their handler is called with the
821 special response value of None.
822 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800823 for rid in self._requests.keys()[:]:
824 request_time, timeout, handler = self._requests[rid]
825 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800826 if callable(handler):
827 handler(None)
828 else:
829 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800830 del self._requests[rid]
831
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800832 def InitiateDownload(self):
833 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800834 sid = self._ttyname_to_sid[ttyname]
835 self.SpawnGhost(self.FILE, terminal_sid=sid,
836 file_op=('download', filename, None))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800837
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800838 def Listen(self):
839 try:
840 while True:
841 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
842
843 if self._sock in rds:
844 self._buf += self._sock.recv(_BUFSIZE)
845 self.ParseMessage()
846
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800847 if (self._mode == self.AGENT and
848 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800849 self.Ping()
850 self.ScanForTimeoutRequests()
851
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800852 if not self._download_queue.empty():
853 self.InitiateDownload()
854
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800855 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800856 self.Reset()
857 break
858 except socket.error:
859 raise RuntimeError('Connection dropped')
860 except PingTimeoutError:
861 raise RuntimeError('Connection timeout')
862 finally:
863 self._sock.close()
864
865 self._queue.put('resume')
866
867 if self._mode != Ghost.AGENT:
868 sys.exit(1)
869
870 def Register(self):
871 non_local = {}
872 for addr in self._overlord_addrs:
873 non_local['addr'] = addr
874 def registered(response):
875 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800876 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800877 raise RuntimeError('Register request timeout')
878 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
Joel Kitching22b89042015-08-06 18:23:29 +0800879 if self._forward_ssh:
880 logging.info('Starting target SSH port negotiation')
881 self.NegotiateTargetSSHPort()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800882 self._queue.put('pause', True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800883
884 try:
885 logging.info('Trying %s:%d ...', *addr)
886 self.Reset()
887 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
888 self._sock.settimeout(_PING_TIMEOUT)
889 self._sock.connect(addr)
890
891 logging.info('Connection established, registering...')
892 handler = {
893 Ghost.AGENT: registered,
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800894 Ghost.TERMINAL: self.SpawnPTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800895 Ghost.SHELL: self.SpawnShellServer,
896 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800897 }[self._mode]
898
899 # Machine ID may change if MAC address is used (USB-ethernet dongle
900 # plugged/unplugged)
901 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800902 self.SendRequest('register',
903 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800904 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800905 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800906 except socket.error:
907 pass
908 else:
909 self._sock.settimeout(None)
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800910 self._connected_addr = addr
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800911 self.Listen()
912
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800913 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800914
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800915 def Reconnect(self):
916 logging.info('Received reconnect request from RPC server, reconnecting...')
917 self._reset.set()
918
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800919 def AddToDownloadQueue(self, ttyname, filename):
920 self._download_queue.put((ttyname, filename))
921
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800922 def RegisterTTY(self, session_id, ttyname):
923 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800924
925 def RegisterSession(self, session_id, process_id):
926 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800927
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800928 def StartLanDiscovery(self):
929 """Start to listen to LAN discovery packet at
930 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800931
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800932 def thread_func():
933 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
934 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
935 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800936 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800937 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
938 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800939 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800940 return
941
942 logging.info('LAN Discovery: started')
943 while True:
944 rd, _, _ = select.select([s], [], [], 1)
945
946 if s in rd:
947 data, source_addr = s.recvfrom(_BUFSIZE)
948 parts = data.split()
949 if parts[0] == 'OVERLORD':
950 ip, port = parts[1].split(':')
951 if not ip:
952 ip = source_addr[0]
953 self._queue.put((ip, int(port)), True)
954
955 try:
956 obj = self._queue.get(False)
957 except Queue.Empty:
958 pass
959 else:
960 if type(obj) is not str:
961 self._queue.put(obj)
962 elif obj == 'pause':
963 logging.info('LAN Discovery: paused')
964 while obj != 'resume':
965 obj = self._queue.get(True)
966 logging.info('LAN Discovery: resumed')
967
968 t = threading.Thread(target=thread_func)
969 t.daemon = True
970 t.start()
971
972 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800973 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800974 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
975 logRequests=False)
976 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800977 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800978 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800979 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800980 t = threading.Thread(target=rpc_server.serve_forever)
981 t.daemon = True
982 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800983
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800984 def ScanServer(self):
985 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
986 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
987 if addr not in self._overlord_addrs:
988 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800989
Joel Kitching22b89042015-08-06 18:23:29 +0800990 def NegotiateTargetSSHPort(self):
991 """Request-receive target SSH port forwarding loop.
992
993 Repeatedly attempts to forward this machine's SSH port to target. It
994 bounces back and forth between RequestPort and ReceivePort when a new port
995 is required. ReceivePort starts a new thread so that the main ghost thread
996 may continue running.
997 """
998 # Sanity check for identity file.
999 if not os.path.isfile(self._target_identity_file):
1000 logging.info('No target host identity file: not negotiating '
1001 'target SSH port')
1002 return
1003
1004 def PollSSHPortForwarder():
1005 def ThreadFunc():
1006 while True:
1007 state = self._ssh_port_forwarder.GetState()
1008
1009 # Connected successfully.
1010 if state == SSHPortForwarder.INITIALIZED:
1011 # The SSH port forward has succeeded! Let's tell Overlord.
1012 port = self._ssh_port_forwarder.GetDstPort()
1013 RegisterPort(port)
1014
1015 # We've given up... continue to the next port.
1016 elif state == SSHPortForwarder.FAILED:
1017 break
1018
1019 # Either CONNECTING or INITIALIZED.
1020 self._ssh_port_forwarder.Wait()
1021
1022 # Only request a new port if we are still registered to Overlord.
1023 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1024 # which will take care of it.
1025 try:
1026 RequestPort()
1027 except Exception:
1028 logging.info('Failed to request port, will wait for next connection')
1029 self._ssh_port_forwarder = None
1030
1031 t = threading.Thread(target=ThreadFunc)
1032 t.daemon = True
1033 t.start()
1034
1035 def ReceivePort(response):
1036 # If the response times out, this version of Overlord may not support SSH
1037 # port negotiation. Give up on port negotiation process.
1038 if response is None:
1039 return
1040
1041 port = int(response['params']['port'])
1042 logging.info('Received target SSH port: %d', port)
1043
1044 if (self._ssh_port_forwarder and
1045 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1046 logging.info('Unexpectedly received a target SSH port')
1047 return
1048
1049 # Try forwarding SSH port to target.
1050 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1051 src_port=22,
1052 dst_port=port,
1053 user='ghost',
1054 identity_file=self._target_identity_file,
1055 host=self._connected_addr[0]) # Use Overlord host as target.
1056
1057 # Creates a new thread.
1058 PollSSHPortForwarder()
1059
1060 def RequestPort():
1061 logging.info('Requesting new target SSH port')
1062 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1063
1064 def RegisterPort(port):
1065 logging.info('Registering target SSH port %d', port)
1066 self.SendRequest(
1067 'register_target_ssh_port',
1068 {'port': port}, RegisterPortResponse, 5)
1069
1070 def RegisterPortResponse(response):
1071 # Overlord responded to request_port already. If register_port fails,
1072 # something might be in an inconsistent state, so trigger a reconnect
1073 # via PingTimeoutError.
1074 if response is None:
1075 raise PingTimeoutError
1076 logging.info('Registering target SSH port acknowledged')
1077
1078 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1079 # manually report the port to target, since SSHPortForwarder is currently
1080 # blocking.
1081 if (self._ssh_port_forwarder and
1082 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1083 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1084 if not self._ssh_port_forwarder:
1085 RequestPort()
1086
1087 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001088 logging.info('%s started', self.MODE_NAME[self._mode])
1089 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001090 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001091
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001092 # We don't care about child process's return code, not wait is needed. This
1093 # is used to prevent zombie process from lingering in the system.
1094 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001095
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001096 if lan_disc:
1097 self.StartLanDiscovery()
1098
1099 if rpc_server:
1100 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001101
Joel Kitching22b89042015-08-06 18:23:29 +08001102 self._forward_ssh = forward_ssh
1103
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001104 try:
1105 while True:
1106 try:
1107 addr = self._queue.get(False)
1108 except Queue.Empty:
1109 pass
1110 else:
1111 if type(addr) == tuple and addr not in self._overlord_addrs:
1112 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1113 self._overlord_addrs.append(addr)
1114
1115 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001116 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001117 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001118 # Don't show stack trace for RuntimeError, which we use in this file for
1119 # plausible and expected errors (such as can't connect to server).
1120 except RuntimeError as e:
1121 logging.info('%s: %s, retrying in %ds',
1122 e.__class__.__name__, e.message, _RETRY_INTERVAL)
1123 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001124 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001125 _, _, exc_traceback = sys.exc_info()
1126 traceback.print_tb(exc_traceback)
1127 logging.info('%s: %s, retrying in %ds',
1128 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001129 time.sleep(_RETRY_INTERVAL)
1130
1131 self.Reset()
1132 except KeyboardInterrupt:
1133 logging.error('Received keyboard interrupt, quit')
1134 sys.exit(0)
1135
1136
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001137def GhostRPCServer():
1138 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1139
1140
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001141def DownloadFile(filename):
1142 filepath = os.path.abspath(filename)
1143 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001144 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001145 sys.exit(1)
1146
1147 # Check if we actually have permission to read the file
1148 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001149 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001150 sys.exit(1)
1151
1152 server = GhostRPCServer()
1153 server.AddToDownloadQueue(os.ttyname(0), filepath)
1154 sys.exit(0)
1155
1156
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001157def main():
1158 logger = logging.getLogger()
1159 logger.setLevel(logging.INFO)
1160
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001161 parser = argparse.ArgumentParser()
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001162 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1163 default=None, help='use MID as machine ID')
1164 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1165 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001166 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1167 default=True, help='disable LAN discovery')
1168 parser.add_argument('--no-rpc-server', dest='rpc_server',
1169 action='store_false', default=True,
1170 help='disable RPC server')
Joel Kitching22b89042015-08-06 18:23:29 +08001171 parser.add_argument('--no-forward-ssh', dest='forward_ssh',
1172 action='store_false', default=True,
1173 help='disable target SSH port forwarding')
1174 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001175 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001176 help='file containing the JSON representation of client '
1177 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001178 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1179 default=None, help='file to download')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001180 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1181 nargs='*', help='overlord server address')
1182 args = parser.parse_args()
1183
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001184 if args.download:
1185 DownloadFile(args.download)
1186
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001187 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001188 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001189
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001190 g = Ghost(addrs, Ghost.AGENT, args.mid)
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001191 if args.prop_file:
1192 g.LoadPropertiesFromFile(args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001193 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1194
1195
1196def _SigtermHandler(*_):
1197 """Ensure that SSH processes also get killed on a sigterm signal.
1198
1199 By also passing the sigterm signal onto the process group, we ensure that any
1200 child SSH processes will also get killed.
1201
1202 Source:
1203 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1204 """
1205 logging.info('SIGTERM handler: shutting down')
1206 if not _SigtermHandler.SIGTERM_SENT:
1207 _SigtermHandler.SIGTERM_SENT = True
1208 logging.info('Sending TERM to process group')
1209 os.killpg(0, signal.SIGTERM)
1210 sys.exit()
1211_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001212
1213
1214if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001215 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001216 main()