blob: ef6c91ee61facdaec98a9508ad3fa744344ab427 [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 Huangae923642015-09-24 14:08:09 +0800872 os.stat(filepath)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800873 except OSError as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800874 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800875
876 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800877 file_op=('download', filepath))
878 self.SendResponse(msg, SUCCESS)
879
880 def HandleFileUploadRequest(self, msg):
881 params = msg['params']
882
883 # Resolve upload filepath
884 filename = params['filename']
885 dest_path = filename
886
887 # If dest is specified, use it first
888 dest_path = params.get('dest', '')
889 if dest_path:
890 if not os.path.isabs(dest_path):
891 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
892
893 if os.path.isdir(dest_path):
894 dest_path = os.path.join(dest_path, filename)
895 else:
896 target_dir = os.getenv('HOME', '/tmp')
897
898 # Terminal session ID found, upload to it's current working directory
899 if params.has_key('terminal_sid'):
900 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
901 if pid:
902 target_dir = os.readlink('/proc/%d/cwd' % pid)
903
904 dest_path = os.path.join(target_dir, filename)
905
906 try:
907 os.makedirs(os.path.dirname(dest_path))
908 except Exception:
909 pass
910
911 try:
912 with open(dest_path, 'w') as _:
913 pass
914 except Exception as e:
915 return self.SendResponse(msg, str(e))
916
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800917 # If not check_only, spawn FILE mode ghost agent to handle upload
918 if not params.get('check_only', False):
919 self.SpawnGhost(self.FILE, params['sid'],
920 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800921 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800922
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800923 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800924 command = msg['name']
925 params = msg['params']
926
927 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800928 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800929 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800930 self.SpawnGhost(self.TERMINAL, params['sid'],
931 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800932 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800933 elif command == 'shell':
934 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800935 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800936 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800937 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800938 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800939 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800940 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800941 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800942 elif command == 'forward':
943 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
944 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800945
946 def HandleResponse(self, response):
947 rid = str(response['rid'])
948 if rid in self._requests:
949 handler = self._requests[rid][2]
950 del self._requests[rid]
951 if callable(handler):
952 handler(response)
953 else:
954 print(response, self._requests.keys())
Joel Kitching22b89042015-08-06 18:23:29 +0800955 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800956
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800957 def ParseMessage(self, single=True):
958 if single:
959 index = self._buf.index(_SEPARATOR)
960 msgs_json = [self._buf[:index]]
961 self._buf = self._buf[index + 2:]
962 else:
963 msgs_json = self._buf.split(_SEPARATOR)
964 self._buf = msgs_json.pop()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800965
966 for msg_json in msgs_json:
967 try:
968 msg = json.loads(msg_json)
969 except ValueError:
970 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800971 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800972 continue
973
974 if 'name' in msg:
975 self.HandleRequest(msg)
976 elif 'response' in msg:
977 self.HandleResponse(msg)
978 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800979 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800980
981 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800982 """Scans for pending requests which have timed out.
983
984 If any timed-out requests are discovered, their handler is called with the
985 special response value of None.
986 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800987 for rid in self._requests.keys()[:]:
988 request_time, timeout, handler = self._requests[rid]
989 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800990 if callable(handler):
991 handler(None)
992 else:
993 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800994 del self._requests[rid]
995
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800996 def InitiateDownload(self):
997 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800998 sid = self._ttyname_to_sid[ttyname]
999 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +08001000 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001001
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001002 def Listen(self):
1003 try:
1004 while True:
1005 rds, _, _ = select.select([self._sock], [], [], _PING_INTERVAL / 2)
1006
1007 if self._sock in rds:
1008 self._buf += self._sock.recv(_BUFSIZE)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +08001009 self.ParseMessage(self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001010
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001011 if (self._mode == self.AGENT and
1012 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001013 self.Ping()
1014 self.ScanForTimeoutRequests()
1015
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001016 if not self._download_queue.empty():
1017 self.InitiateDownload()
1018
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001019 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001020 self.Reset()
1021 break
1022 except socket.error:
1023 raise RuntimeError('Connection dropped')
1024 except PingTimeoutError:
1025 raise RuntimeError('Connection timeout')
1026 finally:
1027 self._sock.close()
1028
1029 self._queue.put('resume')
1030
1031 if self._mode != Ghost.AGENT:
1032 sys.exit(1)
1033
1034 def Register(self):
1035 non_local = {}
1036 for addr in self._overlord_addrs:
1037 non_local['addr'] = addr
1038 def registered(response):
1039 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001040 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001041 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001042
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001043 self._register_status = response['response']
1044 if response['response'] != SUCCESS:
1045 self._reset.set()
1046 raise RuntimeError('Reigster: ' + response['response'])
1047 else:
1048 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
1049 self._connected_addr = non_local['addr']
1050 self.Upgrade() # Check for upgrade
1051 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001052
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001053 if self._forward_ssh:
1054 logging.info('Starting target SSH port negotiation')
1055 self.NegotiateTargetSSHPort()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001056
1057 try:
1058 logging.info('Trying %s:%d ...', *addr)
1059 self.Reset()
1060 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1061 self._sock.settimeout(_PING_TIMEOUT)
1062 self._sock.connect(addr)
1063
1064 logging.info('Connection established, registering...')
1065 handler = {
1066 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +08001067 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001068 Ghost.SHELL: self.SpawnShellServer,
1069 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +08001070 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001071 }[self._mode]
1072
1073 # Machine ID may change if MAC address is used (USB-ethernet dongle
1074 # plugged/unplugged)
1075 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001076 self.SendRequest('register',
1077 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001078 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001079 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001080 except socket.error:
1081 pass
1082 else:
1083 self._sock.settimeout(None)
1084 self.Listen()
1085
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001086 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001087
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001088 def Reconnect(self):
1089 logging.info('Received reconnect request from RPC server, reconnecting...')
1090 self._reset.set()
1091
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001092 def GetStatus(self):
1093 return self._register_status
1094
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001095 def AddToDownloadQueue(self, ttyname, filename):
1096 self._download_queue.put((ttyname, filename))
1097
Wei-Ning Huangd521f282015-08-07 05:28:04 +08001098 def RegisterTTY(self, session_id, ttyname):
1099 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +08001100
1101 def RegisterSession(self, session_id, process_id):
1102 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001103
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001104 def StartLanDiscovery(self):
1105 """Start to listen to LAN discovery packet at
1106 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001107
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001108 def thread_func():
1109 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1110 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1111 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001112 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001113 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
1114 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001115 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001116 return
1117
1118 logging.info('LAN Discovery: started')
1119 while True:
1120 rd, _, _ = select.select([s], [], [], 1)
1121
1122 if s in rd:
1123 data, source_addr = s.recvfrom(_BUFSIZE)
1124 parts = data.split()
1125 if parts[0] == 'OVERLORD':
1126 ip, port = parts[1].split(':')
1127 if not ip:
1128 ip = source_addr[0]
1129 self._queue.put((ip, int(port)), True)
1130
1131 try:
1132 obj = self._queue.get(False)
1133 except Queue.Empty:
1134 pass
1135 else:
1136 if type(obj) is not str:
1137 self._queue.put(obj)
1138 elif obj == 'pause':
1139 logging.info('LAN Discovery: paused')
1140 while obj != 'resume':
1141 obj = self._queue.get(True)
1142 logging.info('LAN Discovery: resumed')
1143
1144 t = threading.Thread(target=thread_func)
1145 t.daemon = True
1146 t.start()
1147
1148 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +08001149 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001150 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
1151 logRequests=False)
1152 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001153 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001154 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001155 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001156 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001157 t = threading.Thread(target=rpc_server.serve_forever)
1158 t.daemon = True
1159 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001160
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001161 def ScanServer(self):
1162 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
1163 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1164 if addr not in self._overlord_addrs:
1165 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001166
Joel Kitching22b89042015-08-06 18:23:29 +08001167 def NegotiateTargetSSHPort(self):
1168 """Request-receive target SSH port forwarding loop.
1169
1170 Repeatedly attempts to forward this machine's SSH port to target. It
1171 bounces back and forth between RequestPort and ReceivePort when a new port
1172 is required. ReceivePort starts a new thread so that the main ghost thread
1173 may continue running.
1174 """
1175 # Sanity check for identity file.
1176 if not os.path.isfile(self._target_identity_file):
1177 logging.info('No target host identity file: not negotiating '
1178 'target SSH port')
1179 return
1180
1181 def PollSSHPortForwarder():
1182 def ThreadFunc():
1183 while True:
1184 state = self._ssh_port_forwarder.GetState()
1185
1186 # Connected successfully.
1187 if state == SSHPortForwarder.INITIALIZED:
1188 # The SSH port forward has succeeded! Let's tell Overlord.
1189 port = self._ssh_port_forwarder.GetDstPort()
1190 RegisterPort(port)
1191
1192 # We've given up... continue to the next port.
1193 elif state == SSHPortForwarder.FAILED:
1194 break
1195
1196 # Either CONNECTING or INITIALIZED.
1197 self._ssh_port_forwarder.Wait()
1198
1199 # Only request a new port if we are still registered to Overlord.
1200 # Otherwise, a new call to NegotiateTargetSSHPort will be made,
1201 # which will take care of it.
1202 try:
1203 RequestPort()
1204 except Exception:
1205 logging.info('Failed to request port, will wait for next connection')
1206 self._ssh_port_forwarder = None
1207
1208 t = threading.Thread(target=ThreadFunc)
1209 t.daemon = True
1210 t.start()
1211
1212 def ReceivePort(response):
1213 # If the response times out, this version of Overlord may not support SSH
1214 # port negotiation. Give up on port negotiation process.
1215 if response is None:
1216 return
1217
1218 port = int(response['params']['port'])
1219 logging.info('Received target SSH port: %d', port)
1220
1221 if (self._ssh_port_forwarder and
1222 self._ssh_port_forwarder.GetState() != SSHPortForwarder.FAILED):
1223 logging.info('Unexpectedly received a target SSH port')
1224 return
1225
1226 # Try forwarding SSH port to target.
1227 self._ssh_port_forwarder = SSHPortForwarder.ToRemote(
1228 src_port=22,
1229 dst_port=port,
1230 user='ghost',
1231 identity_file=self._target_identity_file,
1232 host=self._connected_addr[0]) # Use Overlord host as target.
1233
1234 # Creates a new thread.
1235 PollSSHPortForwarder()
1236
1237 def RequestPort():
1238 logging.info('Requesting new target SSH port')
1239 self.SendRequest('request_target_ssh_port', {}, ReceivePort, 5)
1240
1241 def RegisterPort(port):
1242 logging.info('Registering target SSH port %d', port)
1243 self.SendRequest(
1244 'register_target_ssh_port',
1245 {'port': port}, RegisterPortResponse, 5)
1246
1247 def RegisterPortResponse(response):
1248 # Overlord responded to request_port already. If register_port fails,
1249 # something might be in an inconsistent state, so trigger a reconnect
1250 # via PingTimeoutError.
1251 if response is None:
1252 raise PingTimeoutError
1253 logging.info('Registering target SSH port acknowledged')
1254
1255 # If the SSHPortForwarder is already in a INITIALIZED state, we need to
1256 # manually report the port to target, since SSHPortForwarder is currently
1257 # blocking.
1258 if (self._ssh_port_forwarder and
1259 self._ssh_port_forwarder.GetState() == SSHPortForwarder.INITIALIZED):
1260 RegisterPort(self._ssh_port_forwarder.GetDstPort())
1261 if not self._ssh_port_forwarder:
1262 RequestPort()
1263
1264 def Start(self, lan_disc=False, rpc_server=False, forward_ssh=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001265 logging.info('%s started', self.MODE_NAME[self._mode])
1266 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001267 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001268
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001269 # We don't care about child process's return code, not wait is needed. This
1270 # is used to prevent zombie process from lingering in the system.
1271 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001272
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001273 if lan_disc:
1274 self.StartLanDiscovery()
1275
1276 if rpc_server:
1277 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001278
Joel Kitching22b89042015-08-06 18:23:29 +08001279 self._forward_ssh = forward_ssh
1280
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001281 try:
1282 while True:
1283 try:
1284 addr = self._queue.get(False)
1285 except Queue.Empty:
1286 pass
1287 else:
1288 if type(addr) == tuple and addr not in self._overlord_addrs:
1289 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1290 self._overlord_addrs.append(addr)
1291
1292 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001293 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001294 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001295 # Don't show stack trace for RuntimeError, which we use in this file for
1296 # plausible and expected errors (such as can't connect to server).
1297 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001298 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +08001299 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001300 except Exception as e:
Joel Kitching22b89042015-08-06 18:23:29 +08001301 _, _, exc_traceback = sys.exc_info()
1302 traceback.print_tb(exc_traceback)
1303 logging.info('%s: %s, retrying in %ds',
1304 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001305 time.sleep(_RETRY_INTERVAL)
1306
1307 self.Reset()
1308 except KeyboardInterrupt:
1309 logging.error('Received keyboard interrupt, quit')
1310 sys.exit(0)
1311
1312
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001313def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001314 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001315 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1316
1317
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001318def ForkToBackground():
1319 """Fork process to run in background."""
1320 pid = os.fork()
1321 if pid != 0:
1322 logging.info('Ghost(%d) running in background.', pid)
1323 sys.exit(0)
1324
1325
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001326def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001327 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001328 filepath = os.path.abspath(filename)
1329 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001330 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001331 sys.exit(1)
1332
1333 # Check if we actually have permission to read the file
1334 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001335 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001336 sys.exit(1)
1337
1338 server = GhostRPCServer()
1339 server.AddToDownloadQueue(os.ttyname(0), filepath)
1340 sys.exit(0)
1341
1342
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001343def main():
1344 logger = logging.getLogger()
1345 logger.setLevel(logging.INFO)
1346
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001347 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001348 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1349 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001350 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1351 default=None, help='use MID as machine ID')
1352 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1353 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001354 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1355 default=True, help='disable LAN discovery')
1356 parser.add_argument('--no-rpc-server', dest='rpc_server',
1357 action='store_false', default=True,
1358 help='disable RPC server')
Wei-Ning Huang03f9f762015-09-16 21:51:35 +08001359 parser.add_argument('--forward-ssh', dest='forward_ssh',
1360 action='store_true', default=False,
1361 help='enable target SSH port forwarding')
Joel Kitching22b89042015-08-06 18:23:29 +08001362 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001363 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001364 help='file containing the JSON representation of client '
1365 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001366 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1367 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001368 parser.add_argument('--reset', dest='reset', default=False,
1369 action='store_true',
1370 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001371 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1372 nargs='*', help='overlord server address')
1373 args = parser.parse_args()
1374
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001375 if args.fork:
1376 ForkToBackground()
1377
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001378 if args.reset:
1379 GhostRPCServer().Reconnect()
1380 sys.exit()
1381
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001382 if args.download:
1383 DownloadFile(args.download)
1384
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001385 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001386 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001387
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001388 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Joel Kitching22b89042015-08-06 18:23:29 +08001389 g.Start(args.lan_disc, args.rpc_server, args.forward_ssh)
1390
1391
1392def _SigtermHandler(*_):
1393 """Ensure that SSH processes also get killed on a sigterm signal.
1394
1395 By also passing the sigterm signal onto the process group, we ensure that any
1396 child SSH processes will also get killed.
1397
1398 Source:
1399 http://www.tsheffler.com/blog/2010/11/21/python-multithreaded-daemon-with-sigterm-support-a-recipe/
1400 """
1401 logging.info('SIGTERM handler: shutting down')
1402 if not _SigtermHandler.SIGTERM_SENT:
1403 _SigtermHandler.SIGTERM_SENT = True
1404 logging.info('Sending TERM to process group')
1405 os.killpg(0, signal.SIGTERM)
1406 sys.exit()
1407_SigtermHandler.SIGTERM_SENT = False
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001408
1409
1410if __name__ == '__main__':
Joel Kitching22b89042015-08-06 18:23:29 +08001411 signal.signal(signal.SIGTERM, _SigtermHandler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001412 main()