blob: 91ae7211da63139f2627a6f16bf27185320d8849 [file] [log] [blame]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001#!/usr/bin/python -u
2# -*- coding: utf-8 -*-
3#
4# Copyright 2015 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
Joel Kitching22b89042015-08-06 18:23:29 +08008from __future__ import print_function
9
Wei-Ning Huang7d029b12015-03-06 10:32:15 +080010import argparse
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080011import contextlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080012import fcntl
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080013import hashlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080014import json
15import logging
16import os
17import Queue
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080018import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080019import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080020import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080021import socket
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080022import struct
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080023import subprocess
24import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080025import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080026import threading
27import time
Joel Kitching22b89042015-08-06 18:23:29 +080028import traceback
Wei-Ning Huang39169902015-09-19 06:00:23 +080029import tty
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080030import urllib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080031import uuid
32
Wei-Ning Huang2132de32015-04-13 17:24:38 +080033import jsonrpclib
34from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
35
36
37_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080038
39_OVERLORD_PORT = 4455
40_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080041_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080042
43_BUFSIZE = 8192
44_RETRY_INTERVAL = 2
45_SEPARATOR = '\r\n'
46_PING_TIMEOUT = 3
47_PING_INTERVAL = 5
48_REQUEST_TIMEOUT_SECS = 60
49_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080050_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080051
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080052_CONTROL_START = 128
53_CONTROL_END = 129
54
Wei-Ning Huanga301f572015-06-03 17:34:21 +080055_BLOCK_SIZE = 4096
56
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080057SUCCESS = 'success'
58FAILED = 'failed'
59DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080060
Joel Kitching22b89042015-08-06 18:23:29 +080061
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080062class PingTimeoutError(Exception):
63 pass
64
65
66class RequestError(Exception):
67 pass
68
69
Joel Kitching22b89042015-08-06 18:23:29 +080070class SSHPortForwarder(object):
71 """Create and maintain an SSH port forwarding connection.
72
73 This is meant to be a standalone class to maintain an SSH port forwarding
74 connection to a given server. It provides a fail/retry mechanism, and also
75 can report its current connection status.
76 """
77 _FAILED_STR = 'port forwarding failed'
78 _DEFAULT_CONNECT_TIMEOUT = 10
79 _DEFAULT_ALIVE_INTERVAL = 10
80 _DEFAULT_DISCONNECT_WAIT = 1
81 _DEFAULT_RETRIES = 5
82 _DEFAULT_EXP_FACTOR = 1
83 _DEBUG_INTERVAL = 2
84
85 CONNECTING = 1
86 INITIALIZED = 2
87 FAILED = 4
88
89 REMOTE = 1
90 LOCAL = 2
91
92 @classmethod
93 def ToRemote(cls, *args, **kwargs):
94 """Calls contructor with forward_to=REMOTE."""
95 return cls(*args, forward_to=cls.REMOTE, **kwargs)
96
97 @classmethod
98 def ToLocal(cls, *args, **kwargs):
99 """Calls contructor with forward_to=LOCAL."""
100 return cls(*args, forward_to=cls.LOCAL, **kwargs)
101
102 def __init__(self,
103 forward_to,
104 src_port,
105 dst_port,
106 user,
107 identity_file,
108 host,
109 port=22,
110 connect_timeout=_DEFAULT_CONNECT_TIMEOUT,
111 alive_interval=_DEFAULT_ALIVE_INTERVAL,
112 disconnect_wait=_DEFAULT_DISCONNECT_WAIT,
113 retries=_DEFAULT_RETRIES,
114 exp_factor=_DEFAULT_EXP_FACTOR):
115 """Constructor.
116
117 Args:
118 forward_to: Which direction to forward traffic: REMOTE or LOCAL.
119 src_port: Source port for forwarding.
120 dst_port: Destination port for forwarding.
121 user: Username on remote server.
122 identity_file: Identity file for passwordless authentication on remote
123 server.
124 host: Host of remote server.
125 port: Port of remote server.
126 connect_timeout: Time in seconds
127 alive_interval:
128 disconnect_wait: The number of seconds to wait before reconnecting after
129 the first disconnect.
130 retries: The number of times to retry before reporting a failed
131 connection.
132 exp_factor: After each reconnect, the disconnect wait time is multiplied
133 by 2^exp_factor.
134 """
135 # Internal use.
136 self._ssh_thread = None
137 self._ssh_output = None
138 self._exception = None
139 self._state = self.CONNECTING
140 self._poll = threading.Event()
141
142 # Connection arguments.
143 self._forward_to = forward_to
144 self._src_port = src_port
145 self._dst_port = dst_port
146 self._host = host
147 self._user = user
148 self._identity_file = identity_file
149 self._port = port
150
151 # Configuration arguments.
152 self._connect_timeout = connect_timeout
153 self._alive_interval = alive_interval
154 self._exp_factor = exp_factor
155
156 t = threading.Thread(
157 target=self._Run,
158 args=(disconnect_wait, retries))
159 t.daemon = True
160 t.start()
161
162 def __str__(self):
163 # State representation.
164 if self._state == self.CONNECTING:
165 state_str = 'connecting'
166 elif self._state == self.INITIALIZED:
167 state_str = 'initialized'
168 else:
169 state_str = 'failed'
170
171 # Port forward representation.
172 if self._forward_to == self.REMOTE:
173 fwd_str = '->%d' % self._dst_port
174 else:
175 fwd_str = '%d<-' % self._dst_port
176
177 return 'SSHPortForwarder(%s,%s)' % (state_str, fwd_str)
178
179 def _ForwardArgs(self):
180 if self._forward_to == self.REMOTE:
181 return ['-R', '%d:127.0.0.1:%d' % (self._dst_port, self._src_port)]
182 else:
183 return ['-L', '%d:127.0.0.1:%d' % (self._src_port, self._dst_port)]
184
185 def _RunSSHCmd(self):
186 """Runs the SSH command, storing the exception on failure."""
187 try:
188 cmd = [
189 'ssh',
190 '-o', 'StrictHostKeyChecking=no',
191 '-o', 'GlobalKnownHostsFile=/dev/null',
192 '-o', 'UserKnownHostsFile=/dev/null',
193 '-o', 'ExitOnForwardFailure=yes',
194 '-o', 'ConnectTimeout=%d' % self._connect_timeout,
195 '-o', 'ServerAliveInterval=%d' % self._alive_interval,
196 '-o', 'ServerAliveCountMax=1',
197 '-o', 'TCPKeepAlive=yes',
198 '-o', 'BatchMode=yes',
199 '-i', self._identity_file,
200 '-N',
201 '-p', str(self._port),
202 '%s@%s' % (self._user, self._host),
203 ] + self._ForwardArgs()
204 logging.info(' '.join(cmd))
205 self._ssh_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
206 except subprocess.CalledProcessError as e:
207 self._exception = e
208 finally:
209 pass
210
211 def _Run(self, disconnect_wait, retries):
212 """Wraps around the SSH command, detecting its connection status."""
213 assert retries > 0, '%s: _Run must be called with retries > 0' % self
214
215 logging.info('%s: Connecting to %s:%d',
216 self, self._host, self._port)
217
218 # Set identity file permissions. Need to only be user-readable for ssh to
219 # use the key.
220 try:
221 os.chmod(self._identity_file, 0600)
222 except OSError as e:
223 logging.error('%s: Error setting identity file permissions: %s',
224 self, e)
225 self._state = self.FAILED
226 return
227
228 # Start a thread. If it fails, deal with the failure. If it is still
229 # running after connect_timeout seconds, assume everything's working great,
230 # and tell the caller. Then, continue waiting for it to end.
231 self._ssh_thread = threading.Thread(target=self._RunSSHCmd)
232 self._ssh_thread.daemon = True
233 self._ssh_thread.start()
234
235 # See if the SSH thread is still working after connect_timeout.
236 self._ssh_thread.join(self._connect_timeout)
237 if self._ssh_thread.is_alive():
238 # Assumed to be working. Tell our caller that we are connected.
239 if self._state != self.INITIALIZED:
240 self._state = self.INITIALIZED
241 self._poll.set()
242 logging.info('%s: Still connected after timeout=%ds',
243 self, self._connect_timeout)
244
245 # Only for debug purposes. Keep showing connection status.
246 while self._ssh_thread.is_alive():
247 logging.debug('%s: Still connected', self)
248 self._ssh_thread.join(self._DEBUG_INTERVAL)
249
250 # Figure out what went wrong.
251 if not self._exception:
252 logging.info('%s: SSH unexpectedly exited: %s',
253 self, self._ssh_output.rstrip())
254 if self._exception and self._FAILED_STR in self._exception.output:
255 self._state = self.FAILED
256 self._poll.set()
257 logging.info('%s: Port forwarding failed', self)
258 return
259 elif retries == 1:
260 self._state = self.FAILED
261 self._poll.set()
262 logging.info('%s: Disconnected (0 retries left)', self)
263 return
264 else:
265 logging.info('%s: Disconnected, retrying (sleep %1ds, %d retries left)',
266 self, disconnect_wait, retries - 1)
267 time.sleep(disconnect_wait)
268 self._Run(disconnect_wait=disconnect_wait * (2 ** self._exp_factor),
269 retries=retries - 1)
270
271 def GetState(self):
272 """Returns the current connection state.
273
274 State may be one of:
275
276 CONNECTING: Still attempting to make the first successful connection.
277 INITIALIZED: Is either connected or is trying to make subsequent
278 connection.
279 FAILED: Has completed all connection attempts, or server has reported that
280 target port is in use.
281 """
282 return self._state
283
284 def GetDstPort(self):
285 """Returns the current target port."""
286 return self._dst_port
287
288 def Wait(self):
289 """Waits for a state change, and returns the new state."""
290 self._poll.wait()
291 self._poll.clear()
292 return self.GetState()
293
294
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800295class Ghost(object):
296 """Ghost implements the client protocol of Overlord.
297
298 Ghost provide terminal/shell/logcat functionality and manages the client
299 side connectivity.
300 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800301 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800302
303 MODE_NAME = {
304 NONE: 'NONE',
305 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800306 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800307 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800308 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800309 FILE: 'File',
310 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800311 }
312
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800313 RANDOM_MID = '##random_mid##'
314
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800315 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800316 prop_file=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800317 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800318 """Constructor.
319
320 Args:
321 overlord_addrs: a list of possible address of overlord.
322 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800323 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
324 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800325 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800326 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800327 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800328 terminal_sid: the terminal session ID associate with this client. This is
329 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800330 tty_device: the terminal device to open, if tty_device is None, as pseudo
331 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800332 command: the command to execute when we are in SHELL mode.
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800333 file_op: a tuple (action, filepath, perm). action is either 'download' or
334 'upload'. perm is the permission to set for the file.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800335 port: port number to forward.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800336 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800337 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
338 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800339 if mode == Ghost.SHELL:
340 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800341 if mode == Ghost.FILE:
342 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800343
344 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800345 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800346 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800347 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800348 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800349 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800350 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800351 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800352 self._ttyname_to_sid = {}
353 self._terminal_sid_to_pid = {}
354 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800355 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800356 self._register_status = DISCONNECTED
357 self._reset = threading.Event()
358
359 # RPC
360 self._buf = '' # Read buffer
361 self._requests = {}
362 self._queue = Queue.Queue()
363
364 # Protocol specific
365 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800366 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800367 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800368 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800369 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800370 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800371
372 # SSH Forwarding related
Joel Kitching22b89042015-08-06 18:23:29 +0800373 self._forward_ssh = False
374 self._ssh_port_forwarder = None
375 self._target_identity_file = os.path.join(os.path.dirname(
376 os.path.abspath(os.path.realpath(__file__))), 'ghost_rsa')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800377
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800378 def SetIgnoreChild(self, status):
379 # Only ignore child for Agent since only it could spawn child Ghost.
380 if self._mode == Ghost.AGENT:
381 signal.signal(signal.SIGCHLD,
382 signal.SIG_IGN if status else signal.SIG_DFL)
383
384 def GetFileSha1(self, filename):
385 with open(filename, 'r') as f:
386 return hashlib.sha1(f.read()).hexdigest()
387
Wei-Ning Huang58833882015-09-16 16:52:37 +0800388 def UseSSL(self):
389 """Determine if SSL is enabled on the Overlord server."""
390 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
391 try:
392 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
393 sock.send('GET\r\n')
394
395 data = sock.recv(16)
396 return 'HTTP' not in data
397 except Exception:
398 return False # For whatever reason above failed, assume HTTP
399
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800400 def Upgrade(self):
401 logging.info('Upgrade: initiating upgrade sequence...')
402
403 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800404 url = 'http%s://%s:%d/upgrade/ghost.py' % (
405 's' if self.UseSSL() else '', self._connected_addr[0],
406 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800407
408 # Download sha1sum for ghost.py for verification
409 try:
410 with contextlib.closing(urllib.urlopen(url + '.sha1')) as f:
411 if f.getcode() != 200:
412 raise RuntimeError('HTTP status %d' % f.getcode())
413 sha1sum = f.read().strip()
414 except Exception:
415 logging.error('Upgrade: failed to download sha1sum file, abort')
416 return
417
418 if self.GetFileSha1(scriptpath) == sha1sum:
419 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
420 return
421
422 # Download upgrade version of ghost.py
423 try:
424 with contextlib.closing(urllib.urlopen(url)) as f:
425 if f.getcode() != 200:
426 raise RuntimeError('HTTP status %d' % f.getcode())
427 data = f.read()
428 except Exception:
429 logging.error('Upgrade: failed to download upgrade, abort')
430 return
431
432 # Compare SHA1 sum
433 if hashlib.sha1(data).hexdigest() != sha1sum:
434 logging.error('Upgrade: sha1sum mismatch, abort')
435 return
436
437 python = os.readlink('/proc/self/exe')
438 try:
439 with open(scriptpath, 'w') as f:
440 f.write(data)
441 except Exception:
442 logging.error('Upgrade: failed to write upgrade onto disk, abort')
443 return
444
445 logging.info('Upgrade: restarting ghost...')
446 self.CloseSockets()
447 self.SetIgnoreChild(False)
448 os.execve(python, [python, scriptpath] + sys.argv[1:], os.environ)
449
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800450 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800451 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800452 if self._prop_file:
453 with open(self._prop_file, 'r') as f:
454 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800455 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800456 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800457
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800458 def CloseSockets(self):
459 # Close sockets opened by parent process, since we don't use it anymore.
460 for fd in os.listdir('/proc/self/fd/'):
461 try:
462 real_fd = os.readlink('/proc/self/fd/%s' % fd)
463 if real_fd.startswith('socket'):
464 os.close(int(fd))
465 except Exception:
466 pass
467
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800468 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800469 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800470 """Spawn a child ghost with specific mode.
471
472 Returns:
473 The spawned child process pid.
474 """
Joel Kitching22b89042015-08-06 18:23:29 +0800475 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800476 self.SetIgnoreChild(False)
477
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800478 pid = os.fork()
479 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800480 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800481 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800482 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800483 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800484 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800485 sys.exit(0)
486 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800487 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800488 return pid
489
490 def Timestamp(self):
491 return int(time.time())
492
493 def GetGateWayIP(self):
494 with open('/proc/net/route', 'r') as f:
495 lines = f.readlines()
496
497 ips = []
498 for line in lines:
499 parts = line.split('\t')
500 if parts[2] == '00000000':
501 continue
502
503 try:
504 h = parts[2].decode('hex')
505 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
506 except TypeError:
507 pass
508
509 return ips
510
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800511 def GetShopfloorIP(self):
512 try:
513 import factory_common # pylint: disable=W0612
514 from cros.factory.test import shopfloor
515
516 url = shopfloor.get_server_url()
517 match = re.match(r'^https?://(.*):.*$', url)
518 if match:
519 return [match.group(1)]
520 except Exception:
521 pass
522 return []
523
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800524 def GetMachineID(self):
525 """Generates machine-dependent ID string for a machine.
526 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800527 1. factory device_id
528 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800529 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
530 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800531 We follow the listed order to generate machine ID, and fallback to the next
532 alternative if the previous doesn't work.
533 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800534 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800535 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800536 elif self._mid:
537 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800538
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800539 # Try factory device id
540 try:
541 import factory_common # pylint: disable=W0612
542 from cros.factory.test import event_log
543 with open(event_log.DEVICE_ID_PATH) as f:
544 return f.read().strip()
545 except Exception:
546 pass
547
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800548 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800549 try:
550 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
551 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800552 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800553 stdout, _ = p.communicate()
554 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800555 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800556 return stdout.strip()
557 except Exception:
558 pass
559
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800560 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800561 try:
562 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
563 return f.read().strip()
564 except Exception:
565 pass
566
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800567 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800568 try:
569 macs = []
570 ifaces = sorted(os.listdir('/sys/class/net'))
571 for iface in ifaces:
572 if iface == 'lo':
573 continue
574
575 with open('/sys/class/net/%s/address' % iface, 'r') as f:
576 macs.append(f.read().strip())
577
578 return ';'.join(macs)
579 except Exception:
580 pass
581
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800582 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800583
584 def Reset(self):
585 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800586 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800587 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800588 self._last_ping = 0
589 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800590 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800591 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800592
593 def SendMessage(self, msg):
594 """Serialize the message and send it through the socket."""
595 self._sock.send(json.dumps(msg) + _SEPARATOR)
596
597 def SendRequest(self, name, args, handler=None,
598 timeout=_REQUEST_TIMEOUT_SECS):
599 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800600 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800601
602 rid = str(uuid.uuid4())
603 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800604 if timeout >= 0:
605 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800606 self.SendMessage(msg)
607
608 def SendResponse(self, omsg, status, params=None):
609 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
610 self.SendMessage(msg)
611
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800612 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800613 msg = json.loads(control_string)
614 command = msg['command']
615 params = msg['params']
616 if command == 'resize':
617 # some error happened on websocket
618 if len(params) != 2:
619 return
620 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
621 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
622 else:
623 logging.warn('Invalid request command "%s"', command)
624
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800625 def SpawnTTYServer(self, _):
626 """Spawn a TTY server and forward I/O to the TCP socket."""
627 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800628
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800629 try:
630 if self._tty_device is None:
631 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800632
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800633 if pid == 0:
634 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
635 try:
636 server = GhostRPCServer()
637 server.RegisterTTY(self._session_id, ttyname)
638 server.RegisterSession(self._session_id, os.getpid())
639 except Exception:
640 # If ghost is launched without RPC server, the call will fail but we
641 # can ignore it.
642 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800643
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800644 # The directory that contains the current running ghost script
645 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800646
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800647 env = os.environ.copy()
648 env['USER'] = os.getenv('USER', 'root')
649 env['HOME'] = os.getenv('HOME', '/root')
650 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
651 os.chdir(env['HOME'])
652 os.execve(_SHELL, [_SHELL], env)
653 else:
654 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800655 tty.setraw(fd)
656 attr = termios.tcgetattr(fd)
657 attr[0] &= ~(termios.IXON | termios.IXOFF)
658 attr[2] |= termios.CLOCAL
659 attr[2] &= ~termios.CRTSCTS
660 attr[4] = termios.B115200
661 attr[5] = termios.B115200
662 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800663
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800664 control_state = None
665 control_string = ''
666 write_buffer = ''
667 while True:
668 rd, _, _ = select.select([self._sock, fd], [], [])
669
670 if fd in rd:
671 self._sock.send(os.read(fd, _BUFSIZE))
672
673 if self._sock in rd:
674 ret = self._sock.recv(_BUFSIZE)
675 if len(ret) == 0:
676 raise RuntimeError('socket closed')
677 while ret:
678 if control_state:
679 if chr(_CONTROL_END) in ret:
680 index = ret.index(chr(_CONTROL_END))
681 control_string += ret[:index]
682 self.HandleTTYControl(fd, control_string)
683 control_state = None
684 control_string = ''
685 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800686 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800687 control_string += ret
688 ret = ''
689 else:
690 if chr(_CONTROL_START) in ret:
691 control_state = _CONTROL_START
692 index = ret.index(chr(_CONTROL_START))
693 write_buffer += ret[:index]
694 ret = ret[index+1:]
695 else:
696 write_buffer += ret
697 ret = ''
698 if write_buffer:
699 os.write(fd, write_buffer)
700 write_buffer = ''
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800701 except Exception as e:
702 logging.error('SpawnTTYServer: %s', e)
703 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800704 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800705
706 logging.info('SpawnTTYServer: terminated')
707 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800708
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800709 def SpawnShellServer(self, _):
710 """Spawn a shell server and forward input/output from/to the TCP socket."""
711 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800712
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800713 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800714 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
715 shell=True)
716
717 def make_non_block(fd):
718 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
719 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
720
721 make_non_block(p.stdout)
722 make_non_block(p.stderr)
723
724 try:
725 while True:
726 rd, _, _ = select.select([p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800727 if p.stdout in rd:
728 self._sock.send(p.stdout.read(_BUFSIZE))
729
730 if p.stderr in rd:
731 self._sock.send(p.stderr.read(_BUFSIZE))
732
733 if self._sock in rd:
734 ret = self._sock.recv(_BUFSIZE)
735 if len(ret) == 0:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800736 raise RuntimeError('socket closed')
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800737 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800738 p.poll()
739 if p.returncode != None:
740 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800741 except Exception as e:
742 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800743 finally:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800744 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800745
746 logging.info('SpawnShellServer: terminated')
747 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800748
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800749 def InitiateFileOperation(self, _):
750 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800751 try:
752 size = os.stat(self._file_op[1]).st_size
753 except OSError as e:
754 logging.error('InitiateFileOperation: download: %s', e)
755 sys.exit(1)
756
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800757 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800758 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800759 'filename': os.path.basename(self._file_op[1]),
760 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800761 elif self._file_op[0] == 'upload':
762 self.SendRequest('clear_to_upload', {}, timeout=-1)
763 self.StartUploadServer()
764 else:
765 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800766
767 def StartDownloadServer(self):
768 logging.info('StartDownloadServer: started')
769
770 try:
771 with open(self._file_op[1], 'rb') as f:
772 while True:
773 data = f.read(_BLOCK_SIZE)
774 if len(data) == 0:
775 break
776 self._sock.send(data)
777 except Exception as e:
778 logging.error('StartDownloadServer: %s', e)
779 finally:
780 self._sock.close()
781
782 logging.info('StartDownloadServer: terminated')
783 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800784
Wei-Ning Huange2981862015-08-03 15:03:08 +0800785 def StartUploadServer(self):
786 logging.info('StartUploadServer: started')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800787 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800788 filepath = self._file_op[1]
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800789 dirname = os.path.dirname(filepath)
790 if not os.path.exists(dirname):
791 try:
792 os.makedirs(dirname)
793 except Exception:
794 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800795
796 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800797 with open(filepath, 'wb') as f:
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800798 if self._file_op[2]:
799 os.fchmod(f.fileno(), self._file_op[2])
800
Wei-Ning Huange2981862015-08-03 15:03:08 +0800801 while True:
802 rd, _, _ = select.select([self._sock], [], [])
803 if self._sock in rd:
804 buf = self._sock.recv(_BLOCK_SIZE)
805 if len(buf) == 0:
806 break
807 f.write(buf)
808 except socket.error as e:
809 logging.error('StartUploadServer: socket error: %s', e)
810 except Exception as e:
811 logging.error('StartUploadServer: %s', e)
812 finally:
813 self._sock.close()
814
815 logging.info('StartUploadServer: terminated')
816 sys.exit(0)
817
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800818 def SpawnPortForwardServer(self, _):
819 """Spawn a port forwarding server and forward I/O to the TCP socket."""
820 logging.info('SpawnPortForwardServer: started')
821
822 src_sock = None
823 try:
824 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
825 src_sock.connect(('localhost', self._port))
826 src_sock.setblocking(False)
827
828 # Pass the leftovers of the previous buffer
829 if self._buf:
830 src_sock.send(self._buf)
831 self._buf = ''
832
833 while True:
834 rd, _, _ = select.select([self._sock, src_sock], [], [])
835
836 if self._sock in rd:
837 data = self._sock.recv(_BUFSIZE)
838 if not data:
839 break
840 src_sock.send(data)
841
842 if src_sock in rd:
843 data = src_sock.recv(_BUFSIZE)
844 if not data:
845 break
846 self._sock.send(data)
847 except Exception as e:
848 logging.error('SpawnPortForwardServer: %s', e)
849 finally:
850 if src_sock:
851 src_sock.close()
852 self._sock.close()
853
854 logging.info('SpawnPortForwardServer: terminated')
855 sys.exit(0)
856
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800857 def Ping(self):
858 def timeout_handler(x):
859 if x is None:
860 raise PingTimeoutError
861
862 self._last_ping = self.Timestamp()
863 self.SendRequest('ping', {}, timeout_handler, 5)
864
Wei-Ning Huangae923642015-09-24 14:08:09 +0800865 def HandleFileDownloadRequest(self, msg):
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800866 params = msg['params']
Wei-Ning Huangae923642015-09-24 14:08:09 +0800867 filepath = params['filename']
868 if not os.path.isabs(filepath):
869 filepath = os.path.join(os.getenv('HOME', '/tmp'), filepath)
870
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800871 try:
Wei-Ning Huang46a3fc92015-10-06 02:35:27 +0800872 with open(filepath, 'r') as f:
873 pass
874 except Exception as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800875 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800876
877 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800878 file_op=('download', filepath))
879 self.SendResponse(msg, SUCCESS)
880
881 def HandleFileUploadRequest(self, msg):
882 params = msg['params']
883
884 # Resolve upload filepath
885 filename = params['filename']
886 dest_path = filename
887
888 # If dest is specified, use it first
889 dest_path = params.get('dest', '')
890 if dest_path:
891 if not os.path.isabs(dest_path):
892 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
893
894 if os.path.isdir(dest_path):
895 dest_path = os.path.join(dest_path, filename)
896 else:
897 target_dir = os.getenv('HOME', '/tmp')
898
899 # Terminal session ID found, upload to it's current working directory
900 if params.has_key('terminal_sid'):
901 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
902 if pid:
903 target_dir = os.readlink('/proc/%d/cwd' % pid)
904
905 dest_path = os.path.join(target_dir, filename)
906
907 try:
908 os.makedirs(os.path.dirname(dest_path))
909 except Exception:
910 pass
911
912 try:
913 with open(dest_path, 'w') as _:
914 pass
915 except Exception as e:
916 return self.SendResponse(msg, str(e))
917
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800918 # If not check_only, spawn FILE mode ghost agent to handle upload
919 if not params.get('check_only', False):
920 self.SpawnGhost(self.FILE, params['sid'],
921 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800922 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800923
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800924 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800925 command = msg['name']
926 params = msg['params']
927
928 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800929 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800930 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800931 self.SpawnGhost(self.TERMINAL, params['sid'],
932 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800933 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800934 elif command == 'shell':
935 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800936 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800937 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800938 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800939 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800940 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800941 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800942 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800943 elif command == 'forward':
944 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
945 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800946
947 def HandleResponse(self, response):
948 rid = str(response['rid'])
949 if rid in self._requests:
950 handler = self._requests[rid][2]
951 del self._requests[rid]
952 if callable(handler):
953 handler(response)
954 else:
955 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800956 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800957
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800958 def ParseMessage(self, single=True):
959 if single:
960 index = self._buf.index(_SEPARATOR)
961 msgs_json = [self._buf[:index]]
962 self._buf = self._buf[index + 2:]
963 else:
964 msgs_json = self._buf.split(_SEPARATOR)
965 self._buf = msgs_json.pop()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800966
967 for msg_json in msgs_json:
968 try:
969 msg = json.loads(msg_json)
970 except ValueError:
971 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800972 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800973 continue
974
975 if 'name' in msg:
976 self.HandleRequest(msg)
977 elif 'response' in msg:
978 self.HandleResponse(msg)
979 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800980 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800981
982 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800983 """Scans for pending requests which have timed out.
984
985 If any timed-out requests are discovered, their handler is called with the
986 special response value of None.
987 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800988 for rid in self._requests.keys()[:]:
989 request_time, timeout, handler = self._requests[rid]
990 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800991 if callable(handler):
992 handler(None)
993 else:
994 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800995 del self._requests[rid]
996
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800997 def InitiateDownload(self):
998 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800999 sid = self._ttyname_to_sid[ttyname]
1000 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +08001001 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001002
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001003 def Listen(self):
1004 try:
1005 while True:
1006 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
1007
1008 if self._sock in rds:
1009 self._buf += self._sock.recv(_BUFSIZE)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +08001010 self.ParseMessage(self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001011
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001012 if (self._mode == self.AGENT and
1013 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001014 self.Ping()
1015 self.ScanForTimeoutRequests()
1016
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001017 if not self._download_queue.empty():
1018 self.InitiateDownload()
1019
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001020 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001021 self.Reset()
1022 break
1023 except socket.error:
1024 raise RuntimeError('Connection dropped')
1025 except PingTimeoutError:
1026 raise RuntimeError('Connection timeout')
1027 finally:
1028 self._sock.close()
1029
1030 self._queue.put('resume')
1031
1032 if self._mode != Ghost.AGENT:
1033 sys.exit(1)
1034
1035 def Register(self):
1036 non_local = {}
1037 for addr in self._overlord_addrs:
1038 non_local['addr'] = addr
1039 def registered(response):
1040 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001041 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001042 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001043
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001044 self._register_status = response['response']
1045 if response['response'] != SUCCESS:
1046 self._reset.set()
1047 raise RuntimeError('Reigster: ' + response['response'])
1048 else:
1049 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
1050 self._connected_addr = non_local['addr']
1051 self.Upgrade() # Check for upgrade
1052 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001053
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001054 if self._forward_ssh:
1055 logging.info('Starting target SSH port negotiation')
1056 self.NegotiateTargetSSHPort()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001057
1058 try:
1059 logging.info('Trying %s:%d ...', *addr)
1060 self.Reset()
1061 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1062 self._sock.settimeout(_PING_TIMEOUT)
1063 self._sock.connect(addr)
1064
1065 logging.info('Connection established, registering...')
1066 handler = {
1067 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +08001068 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001069 Ghost.SHELL: self.SpawnShellServer,
1070 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +08001071 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001072 }[self._mode]
1073
1074 # Machine ID may change if MAC address is used (USB-ethernet dongle
1075 # plugged/unplugged)
1076 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001077 self.SendRequest('register',
1078 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001079 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001080 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001081 except socket.error:
1082 pass
1083 else:
1084 self._sock.settimeout(None)
1085 self.Listen()
1086
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001087 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001088
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001089 def Reconnect(self):
1090 logging.info('Received reconnect request from RPC server, reconnecting...')
1091 self._reset.set()
1092
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001093 def GetStatus(self):
1094 return self._register_status
1095
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001096 def AddToDownloadQueue(self, ttyname, filename):
1097 self._download_queue.put((ttyname, filename))
1098
Wei-Ning Huangd521f282015-08-07 05:28:04 +08001099 def RegisterTTY(self, session_id, ttyname):
1100 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +08001101
1102 def RegisterSession(self, session_id, process_id):
1103 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001104
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001105 def StartLanDiscovery(self):
1106 """Start to listen to LAN discovery packet at
1107 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001108
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001109 def thread_func():
1110 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1111 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1112 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001113 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001114 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
1115 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001116 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001117 return
1118
1119 logging.info('LAN Discovery: started')
1120 while True:
1121 rd, _, _ = select.select([s], [], [], 1)
1122
1123 if s in rd:
1124 data, source_addr = s.recvfrom(_BUFSIZE)
1125 parts = data.split()
1126 if parts[0] == 'OVERLORD':
1127 ip, port = parts[1].split(':')
1128 if not ip:
1129 ip = source_addr[0]
1130 self._queue.put((ip, int(port)), True)
1131
1132 try:
1133 obj = self._queue.get(False)
1134 except Queue.Empty:
1135 pass
1136 else:
1137 if type(obj) is not str:
1138 self._queue.put(obj)
1139 elif obj == 'pause':
1140 logging.info('LAN Discovery: paused')
1141 while obj != 'resume':
1142 obj = self._queue.get(True)
1143 logging.info('LAN Discovery: resumed')
1144
1145 t = threading.Thread(target=thread_func)
1146 t.daemon = True
1147 t.start()
1148
1149 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +08001150 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001151 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
1152 logRequests=False)
1153 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001154 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001155 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001156 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001157 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001158 t = threading.Thread(target=rpc_server.serve_forever)
1159 t.daemon = True
1160 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001161
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001162 def ScanServer(self):
1163 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1164 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1165 if addr not in self._overlord_addrs:
1166 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001167
Joel Kitching22b89042015-08-06 18:23:29 +08001168 def NegotiateTargetSSHPort(self):
1169 """Request-receive target SSH port forwarding loop.
1170
1171 Repeatedly attempts to forward this machine's SSH port to target. It
1172 bounces back and forth between RequestPort and ReceivePort when a new port
1173 is required. ReceivePort starts a new thread so that the main ghost thread
1174 may continue running.
1175 """
1176 # Sanity check for identity file.
1177 if not os.path.isfile(self._target_identity_file):
1178 logging.info('No target host identity file: not negotiating '
1179 'target SSH port')
1180 return
1181
1182 def PollSSHPortForwarder():
1183 def ThreadFunc():
1184 while True:
1185 state = self._ssh_port_forwarder.GetState()
1186
1187 # Connected successfully.
1188 if state == SSHPortForwarder.INITIALIZED:
1189 # The SSH port forward has succeeded! Let's tell Overlord.
1190 port = self._ssh_port_forwarder.GetDstPort()
1191 RegisterPort(port)
1192
1193 # We've given up... continue to the next port.
1194 elif state == SSHPortForwarder.FAILED:
1195 break
1196
1197 # Either CONNECTING or INITIALIZED.
1198 self._ssh_port_forwarder.Wait()
1199
1200 # Only request a new port if we are still registered to Overlord.
1201 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1202 # which will take care of it.
1203 try:
1204 RequestPort()
1205 except Exception:
1206 logging.info('Failed to request port, will wait for next connection')
1207 self._ssh_port_forwarder = None
1208
1209 t = threading.Thread(target=ThreadFunc)
1210 t.daemon = True
1211 t.start()
1212
1213 def ReceivePort(response):
1214 # If the response times out, this version of Overlord may not support SSH
1215 # port negotiation. Give up on port negotiation process.
1216 if response is None:
1217 return
1218
1219 port = int(response['params']['port'])
1220 logging.info('Received target SSH port: %d', port)
1221
1222 if (self._ssh_port_forwarder and
1223 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1224 logging.info('Unexpectedly received a target SSH port')
1225 return
1226
1227 # Try forwarding SSH port to target.
1228 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1229 src_port=22,
1230 dst_port=port,
1231 user='ghost',
1232 identity_file=self._target_identity_file,
1233 host=self._connected_addr[0]) # Use Overlord host as target.
1234
1235 # Creates a new thread.
1236 PollSSHPortForwarder()
1237
1238 def RequestPort():
1239 logging.info('Requesting new target SSH port')
1240 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1241
1242 def RegisterPort(port):
1243 logging.info('Registering target SSH port %d', port)
1244 self.SendRequest(
1245 'register_target_ssh_port',
1246 {'port': port}, RegisterPortResponse, 5)
1247
1248 def RegisterPortResponse(response):
1249 # Overlord responded to request_port already. If register_port fails,
1250 # something might be in an inconsistent state, so trigger a reconnect
1251 # via PingTimeoutError.
1252 if response is None:
1253 raise PingTimeoutError
1254 logging.info('Registering target SSH port acknowledged')
1255
1256 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1257 # manually report the port to target, since SSHPortForwarder is currently
1258 # blocking.
1259 if (self._ssh_port_forwarder and
1260 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1261 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1262 if not self._ssh_port_forwarder:
1263 RequestPort()
1264
1265 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001266 logging.info('%s started', self.MODE_NAME[self._mode])
1267 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001268 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001269
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001270 # We don't care about child process's return code, not wait is needed. This
1271 # is used to prevent zombie process from lingering in the system.
1272 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001273
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001274 if lan_disc:
1275 self.StartLanDiscovery()
1276
1277 if rpc_server:
1278 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001279
Joel Kitching22b89042015-08-06 18:23:29 +08001280 self._forward_ssh = forward_ssh
1281
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001282 try:
1283 while True:
1284 try:
1285 addr = self._queue.get(False)
1286 except Queue.Empty:
1287 pass
1288 else:
1289 if type(addr) == tuple and addr not in self._overlord_addrs:
1290 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1291 self._overlord_addrs.append(addr)
1292
1293 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001294 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001295 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001296 # Don't show stack trace for RuntimeError, which we use in this file for
1297 # plausible and expected errors (such as can't connect to server).
1298 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001299 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +08001300 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001301 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001302 _, _, exc_traceback = sys.exc_info()
1303 traceback.print_tb(exc_traceback)
1304 logging.info('%s: %s, retrying in %ds',
1305 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001306 time.sleep(_RETRY_INTERVAL)
1307
1308 self.Reset()
1309 except KeyboardInterrupt:
1310 logging.error('Received keyboard interrupt, quit')
1311 sys.exit(0)
1312
1313
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001314def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001315 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001316 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1317
1318
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001319def ForkToBackground():
1320 """Fork process to run in background."""
1321 pid = os.fork()
1322 if pid != 0:
1323 logging.info('Ghost(%d) running in background.', pid)
1324 sys.exit(0)
1325
1326
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001327def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001328 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001329 filepath = os.path.abspath(filename)
1330 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001331 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001332 sys.exit(1)
1333
1334 # Check if we actually have permission to read the file
1335 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001336 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001337 sys.exit(1)
1338
1339 server = GhostRPCServer()
1340 server.AddToDownloadQueue(os.ttyname(0), filepath)
1341 sys.exit(0)
1342
1343
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001344def main():
1345 logger = logging.getLogger()
1346 logger.setLevel(logging.INFO)
1347
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001348 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001349 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1350 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001351 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1352 default=None, help='use MID as machine ID')
1353 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1354 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001355 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1356 default=True, help='disable LAN discovery')
1357 parser.add_argument('--no-rpc-server', dest='rpc_server',
1358 action='store_false', default=True,
1359 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001360 parser.add_argument('--forward-ssh', dest='forward_ssh',
1361 action='store_true', default=False,
1362 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001363 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001364 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001365 help='file containing the JSON representation of client '
1366 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001367 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1368 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001369 parser.add_argument('--reset', dest='reset', default=False,
1370 action='store_true',
1371 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001372 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1373 nargs='*', help='overlord server address')
1374 args = parser.parse_args()
1375
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001376 if args.fork:
1377 ForkToBackground()
1378
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001379 if args.reset:
1380 GhostRPCServer().Reconnect()
1381 sys.exit()
1382
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001383 if args.download:
1384 DownloadFile(args.download)
1385
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001386 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001387 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001388
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001389 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001390 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1391
1392
1393def _SigtermHandler(*_):
1394 """Ensure that SSH processes also get killed on a sigterm signal.
1395
1396 By also passing the sigterm signal onto the process group, we ensure that any
1397 child SSH processes will also get killed.
1398
1399 Source:
1400 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1401 """
1402 logging.info('SIGTERM handler: shutting down')
1403 if not _SigtermHandler.SIGTERM_SENT:
1404 _SigtermHandler.SIGTERM_SENT = True
1405 logging.info('Sending TERM to process group')
1406 os.killpg(0, signal.SIGTERM)
1407 sys.exit()
1408_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001409
1410
1411if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001412 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001413 main()