blob: 998a02a341cc556a0124405166edfeefdf228c0c [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
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08008import argparse
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08009import contextlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080010import fcntl
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080011import hashlib
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080012import json
13import logging
14import os
15import Queue
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080016import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080017import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080018import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080019import socket
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080020import struct
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080021import subprocess
22import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080023import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080024import threading
25import time
Joel Kitching22b89042015-08-06 18:23:29 +080026import traceback
Wei-Ning Huang39169902015-09-19 06:00:23 +080027import tty
Wei-Ning Huange0def6a2015-11-05 15:41:24 +080028import urllib2
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080029import uuid
30
Wei-Ning Huang2132de32015-04-13 17:24:38 +080031import jsonrpclib
32from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
33
34
35_GHOST_RPC_PORT = 4499
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080036
37_OVERLORD_PORT = 4455
38_OVERLORD_LAN_DISCOVERY_PORT = 4456
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080039_OVERLORD_HTTP_PORT = 9000
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080040
41_BUFSIZE = 8192
42_RETRY_INTERVAL = 2
43_SEPARATOR = '\r\n'
44_PING_TIMEOUT = 3
45_PING_INTERVAL = 5
46_REQUEST_TIMEOUT_SECS = 60
47_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang2132de32015-04-13 17:24:38 +080048_DEFAULT_BIND_ADDRESS = '0.0.0.0'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080049
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080050_CONTROL_START = 128
51_CONTROL_END = 129
52
Wei-Ning Huanga301f572015-06-03 17:34:21 +080053_BLOCK_SIZE = 4096
Wei-Ning Huange0def6a2015-11-05 15:41:24 +080054_CONNECT_TIMEOUT = 3
Wei-Ning Huanga301f572015-06-03 17:34:21 +080055
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080056# Stream control
57_STDIN_CLOSED = '##STDIN_CLOSED##'
58
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080059SUCCESS = 'success'
60FAILED = 'failed'
61DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080062
Joel Kitching22b89042015-08-06 18:23:29 +080063
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080064class PingTimeoutError(Exception):
65 pass
66
67
68class RequestError(Exception):
69 pass
70
71
72class Ghost(object):
73 """Ghost implements the client protocol of Overlord.
74
75 Ghost provide terminal/shell/logcat functionality and manages the client
76 side connectivity.
77 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080078 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080079
80 MODE_NAME = {
81 NONE: 'NONE',
82 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +080083 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080084 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +080085 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080086 FILE: 'File',
87 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080088 }
89
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +080090 RANDOM_MID = '##random_mid##'
91
Wei-Ning Huangd521f282015-08-07 05:28:04 +080092 def __init__(self, overlord_addrs, mode=AGENT, mid=None, sid=None,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +080093 prop_file=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +080094 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080095 """Constructor.
96
97 Args:
98 overlord_addrs: a list of possible address of overlord.
99 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800100 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
101 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800102 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800103 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800104 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800105 terminal_sid: the terminal session ID associate with this client. This is
106 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800107 tty_device: the terminal device to open, if tty_device is None, as pseudo
108 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800109 command: the command to execute when we are in SHELL mode.
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800110 file_op: a tuple (action, filepath, perm). action is either 'download' or
111 'upload'. perm is the permission to set for the file.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800112 port: port number to forward.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800113 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800114 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
115 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800116 if mode == Ghost.SHELL:
117 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800118 if mode == Ghost.FILE:
119 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800120
121 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800122 self._connected_addr = None
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800123 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800124 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800125 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800126 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800127 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800128 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800129 self._ttyname_to_sid = {}
130 self._terminal_sid_to_pid = {}
131 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800132 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800133 self._register_status = DISCONNECTED
134 self._reset = threading.Event()
135
136 # RPC
137 self._buf = '' # Read buffer
138 self._requests = {}
139 self._queue = Queue.Queue()
140
141 # Protocol specific
142 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800143 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800144 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800145 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800146 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800147 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800148
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800149 def SetIgnoreChild(self, status):
150 # Only ignore child for Agent since only it could spawn child Ghost.
151 if self._mode == Ghost.AGENT:
152 signal.signal(signal.SIGCHLD,
153 signal.SIG_IGN if status else signal.SIG_DFL)
154
155 def GetFileSha1(self, filename):
156 with open(filename, 'r') as f:
157 return hashlib.sha1(f.read()).hexdigest()
158
Wei-Ning Huang58833882015-09-16 16:52:37 +0800159 def UseSSL(self):
160 """Determine if SSL is enabled on the Overlord server."""
161 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
162 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800163 sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huang58833882015-09-16 16:52:37 +0800164 sock.connect((self._connected_addr[0], _OVERLORD_HTTP_PORT))
165 sock.send('GET\r\n')
166
167 data = sock.recv(16)
168 return 'HTTP' not in data
169 except Exception:
170 return False # For whatever reason above failed, assume HTTP
171
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800172 def Upgrade(self):
173 logging.info('Upgrade: initiating upgrade sequence...')
174
175 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800176 url = 'http%s://%s:%d/upgrade/ghost.py' % (
177 's' if self.UseSSL() else '', self._connected_addr[0],
178 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800179
180 # Download sha1sum for ghost.py for verification
181 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800182 with contextlib.closing(
183 urllib2.urlopen(url + '.sha1', timeout=_CONNECT_TIMEOUT)) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800184 if f.getcode() != 200:
185 raise RuntimeError('HTTP status %d' % f.getcode())
186 sha1sum = f.read().strip()
187 except Exception:
188 logging.error('Upgrade: failed to download sha1sum file, abort')
189 return
190
191 if self.GetFileSha1(scriptpath) == sha1sum:
192 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
193 return
194
195 # Download upgrade version of ghost.py
196 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800197 with contextlib.closing(
198 urllib2.urlopen(url, timeout=_CONNECT_TIMEOUT)) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800199 if f.getcode() != 200:
200 raise RuntimeError('HTTP status %d' % f.getcode())
201 data = f.read()
202 except Exception:
203 logging.error('Upgrade: failed to download upgrade, abort')
204 return
205
206 # Compare SHA1 sum
207 if hashlib.sha1(data).hexdigest() != sha1sum:
208 logging.error('Upgrade: sha1sum mismatch, abort')
209 return
210
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800211 try:
212 with open(scriptpath, 'w') as f:
213 f.write(data)
214 except Exception:
215 logging.error('Upgrade: failed to write upgrade onto disk, abort')
216 return
217
218 logging.info('Upgrade: restarting ghost...')
219 self.CloseSockets()
220 self.SetIgnoreChild(False)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800221 os.execve(scriptpath, [scriptpath] + sys.argv[1:], os.environ)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800222
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800223 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800224 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800225 if self._prop_file:
226 with open(self._prop_file, 'r') as f:
227 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800228 except Exception as e:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800229 logging.exception('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800230
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800231 def CloseSockets(self):
232 # Close sockets opened by parent process, since we don't use it anymore.
233 for fd in os.listdir('/proc/self/fd/'):
234 try:
235 real_fd = os.readlink('/proc/self/fd/%s' % fd)
236 if real_fd.startswith('socket'):
237 os.close(int(fd))
238 except Exception:
239 pass
240
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800241 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800242 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800243 """Spawn a child ghost with specific mode.
244
245 Returns:
246 The spawned child process pid.
247 """
Joel Kitching22b89042015-08-06 18:23:29 +0800248 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800249 self.SetIgnoreChild(False)
250
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800251 pid = os.fork()
252 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800253 self.CloseSockets()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800254 g = Ghost([self._connected_addr], mode, Ghost.RANDOM_MID, sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800255 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800256 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800257 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800258 sys.exit(0)
259 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800260 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800261 return pid
262
263 def Timestamp(self):
264 return int(time.time())
265
266 def GetGateWayIP(self):
267 with open('/proc/net/route', 'r') as f:
268 lines = f.readlines()
269
270 ips = []
271 for line in lines:
272 parts = line.split('\t')
273 if parts[2] == '00000000':
274 continue
275
276 try:
277 h = parts[2].decode('hex')
278 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
279 except TypeError:
280 pass
281
282 return ips
283
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800284 def GetShopfloorIP(self):
285 try:
286 import factory_common # pylint: disable=W0612
287 from cros.factory.test import shopfloor
288
289 url = shopfloor.get_server_url()
290 match = re.match(r'^https?://(.*):.*$', url)
291 if match:
292 return [match.group(1)]
293 except Exception:
294 pass
295 return []
296
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800297 def GetMachineID(self):
298 """Generates machine-dependent ID string for a machine.
299 There are many ways to generate a machine ID:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800300 1. factory device_id
301 2. factory device-data
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800302 3. /sys/class/dmi/id/product_uuid (only available on intel machines)
303 4. MAC address
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800304 We follow the listed order to generate machine ID, and fallback to the next
305 alternative if the previous doesn't work.
306 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800307 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800308 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800309 elif self._mid:
310 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800311
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800312 # Try factory device id
313 try:
314 import factory_common # pylint: disable=W0612
315 from cros.factory.test import event_log
316 with open(event_log.DEVICE_ID_PATH) as f:
317 return f.read().strip()
318 except Exception:
319 pass
320
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800321 # Try factory device data
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800322 try:
323 p = subprocess.Popen('factory device-data | grep mlb_serial_number | '
324 'cut -d " " -f 2', stdout=subprocess.PIPE,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800325 stderr=subprocess.PIPE, shell=True)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800326 stdout, unused_stderr = p.communicate()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800327 if stdout == '':
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800328 raise RuntimeError('empty mlb number')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800329 return stdout.strip()
330 except Exception:
331 pass
332
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800333 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800334 try:
335 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
336 return f.read().strip()
337 except Exception:
338 pass
339
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800340 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800341 try:
342 macs = []
343 ifaces = sorted(os.listdir('/sys/class/net'))
344 for iface in ifaces:
345 if iface == 'lo':
346 continue
347
348 with open('/sys/class/net/%s/address' % iface, 'r') as f:
349 macs.append(f.read().strip())
350
351 return ';'.join(macs)
352 except Exception:
353 pass
354
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800355 raise RuntimeError('can\'t generate machine ID')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800356
357 def Reset(self):
358 """Reset state and clear request handlers."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800359 self._reset.clear()
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800360 self._buf = ''
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800361 self._last_ping = 0
362 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800363 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800364 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800365
366 def SendMessage(self, msg):
367 """Serialize the message and send it through the socket."""
368 self._sock.send(json.dumps(msg) + _SEPARATOR)
369
370 def SendRequest(self, name, args, handler=None,
371 timeout=_REQUEST_TIMEOUT_SECS):
372 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800373 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800374
375 rid = str(uuid.uuid4())
376 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800377 if timeout >= 0:
378 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800379 self.SendMessage(msg)
380
381 def SendResponse(self, omsg, status, params=None):
382 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
383 self.SendMessage(msg)
384
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800385 def HandleTTYControl(self, fd, control_string):
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800386 msg = json.loads(control_string)
387 command = msg['command']
388 params = msg['params']
389 if command == 'resize':
390 # some error happened on websocket
391 if len(params) != 2:
392 return
393 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
394 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
395 else:
396 logging.warn('Invalid request command "%s"', command)
397
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800398 def SpawnTTYServer(self, unused_var):
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800399 """Spawn a TTY server and forward I/O to the TCP socket."""
400 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800401
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800402 try:
403 if self._tty_device is None:
404 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800405
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800406 if pid == 0:
407 ttyname = os.readlink('/proc/%d/fd/0' % os.getpid())
408 try:
409 server = GhostRPCServer()
410 server.RegisterTTY(self._session_id, ttyname)
411 server.RegisterSession(self._session_id, os.getpid())
412 except Exception:
413 # If ghost is launched without RPC server, the call will fail but we
414 # can ignore it.
415 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800416
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800417 # The directory that contains the current running ghost script
418 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800419
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800420 env = os.environ.copy()
421 env['USER'] = os.getenv('USER', 'root')
422 env['HOME'] = os.getenv('HOME', '/root')
423 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
424 os.chdir(env['HOME'])
425 os.execve(_SHELL, [_SHELL], env)
426 else:
427 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800428 tty.setraw(fd)
429 attr = termios.tcgetattr(fd)
430 attr[0] &= ~(termios.IXON | termios.IXOFF)
431 attr[2] |= termios.CLOCAL
432 attr[2] &= ~termios.CRTSCTS
433 attr[4] = termios.B115200
434 attr[5] = termios.B115200
435 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800436
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800437 control_state = None
438 control_string = ''
439 write_buffer = ''
440 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800441 rd, unused_wd, unused_xd = select.select([self._sock, fd], [], [])
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800442
443 if fd in rd:
444 self._sock.send(os.read(fd, _BUFSIZE))
445
446 if self._sock in rd:
447 ret = self._sock.recv(_BUFSIZE)
448 if len(ret) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800449 raise RuntimeError('connection terminated')
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800450 while ret:
451 if control_state:
452 if chr(_CONTROL_END) in ret:
453 index = ret.index(chr(_CONTROL_END))
454 control_string += ret[:index]
455 self.HandleTTYControl(fd, control_string)
456 control_state = None
457 control_string = ''
458 ret = ret[index+1:]
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800459 else:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800460 control_string += ret
461 ret = ''
462 else:
463 if chr(_CONTROL_START) in ret:
464 control_state = _CONTROL_START
465 index = ret.index(chr(_CONTROL_START))
466 write_buffer += ret[:index]
467 ret = ret[index+1:]
468 else:
469 write_buffer += ret
470 ret = ''
471 if write_buffer:
472 os.write(fd, write_buffer)
473 write_buffer = ''
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800474 except Exception as e:
475 logging.error('SpawnTTYServer: %s', e)
476 finally:
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800477 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800478
479 logging.info('SpawnTTYServer: terminated')
480 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800481
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800482 def SpawnShellServer(self, unused_var):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800483 """Spawn a shell server and forward input/output from/to the TCP socket."""
484 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800485
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800486 # Add ghost executable to PATH
487 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
488 env = os.environ.copy()
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800489 env['PATH'] = '%s:%s' % (script_dir, os.getenv('PATH'))
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800490
491 # Execute shell command from HOME directory
492 os.chdir(os.getenv('HOME', '/tmp'))
493
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800494 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800495 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800496 shell=True, env=env)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800497
498 def make_non_block(fd):
499 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
500 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
501
502 make_non_block(p.stdout)
503 make_non_block(p.stderr)
504
505 try:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800506
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800507 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800508 rd, unused_wd, unused_xd = select.select(
509 [p.stdout, p.stderr, self._sock], [], [self._sock])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800510 if p.stdout in rd:
511 self._sock.send(p.stdout.read(_BUFSIZE))
512
513 if p.stderr in rd:
514 self._sock.send(p.stderr.read(_BUFSIZE))
515
516 if self._sock in rd:
517 ret = self._sock.recv(_BUFSIZE)
518 if len(ret) == 0:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800519 raise RuntimeError('connection terminated')
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800520
521 try:
522 idx = ret.index(_STDIN_CLOSED * 2)
523 p.stdin.write(ret[:idx])
524 p.stdin.close()
525 except ValueError:
526 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800527 p.poll()
528 if p.returncode != None:
529 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800530 except Exception as e:
531 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800532 finally:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800533 # Check if the process is terminated. If not, Send SIGTERM to process,
534 # then wait for 1 second. Send another SIGKILL to make sure the process is
535 # terminated.
536 p.poll()
537 if p.returncode is None:
538 try:
539 p.terminate()
540 time.sleep(1)
541 p.kill()
542 except Exception:
543 pass
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800544
545 p.wait()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800546 self._sock.close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800547
548 logging.info('SpawnShellServer: terminated')
549 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800550
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800551 def InitiateFileOperation(self, unused_var):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800552 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800553 try:
554 size = os.stat(self._file_op[1]).st_size
555 except OSError as e:
556 logging.error('InitiateFileOperation: download: %s', e)
557 sys.exit(1)
558
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800559 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800560 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800561 'filename': os.path.basename(self._file_op[1]),
562 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800563 elif self._file_op[0] == 'upload':
564 self.SendRequest('clear_to_upload', {}, timeout=-1)
565 self.StartUploadServer()
566 else:
567 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800568
569 def StartDownloadServer(self):
570 logging.info('StartDownloadServer: started')
571
572 try:
573 with open(self._file_op[1], 'rb') as f:
574 while True:
575 data = f.read(_BLOCK_SIZE)
576 if len(data) == 0:
577 break
578 self._sock.send(data)
579 except Exception as e:
580 logging.error('StartDownloadServer: %s', e)
581 finally:
582 self._sock.close()
583
584 logging.info('StartDownloadServer: terminated')
585 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800586
Wei-Ning Huange2981862015-08-03 15:03:08 +0800587 def StartUploadServer(self):
588 logging.info('StartUploadServer: started')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800589 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800590 filepath = self._file_op[1]
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800591 dirname = os.path.dirname(filepath)
592 if not os.path.exists(dirname):
593 try:
594 os.makedirs(dirname)
595 except Exception:
596 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800597
598 self._sock.setblocking(False)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800599 with open(filepath, 'wb') as f:
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800600 if self._file_op[2]:
601 os.fchmod(f.fileno(), self._file_op[2])
602
Wei-Ning Huange2981862015-08-03 15:03:08 +0800603 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800604 rd, unused_wd, unused_xd = select.select([self._sock], [], [])
Wei-Ning Huange2981862015-08-03 15:03:08 +0800605 if self._sock in rd:
606 buf = self._sock.recv(_BLOCK_SIZE)
607 if len(buf) == 0:
608 break
609 f.write(buf)
610 except socket.error as e:
611 logging.error('StartUploadServer: socket error: %s', e)
612 except Exception as e:
613 logging.error('StartUploadServer: %s', e)
614 finally:
615 self._sock.close()
616
617 logging.info('StartUploadServer: terminated')
618 sys.exit(0)
619
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800620 def SpawnPortForwardServer(self, unused_var):
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800621 """Spawn a port forwarding server and forward I/O to the TCP socket."""
622 logging.info('SpawnPortForwardServer: started')
623
624 src_sock = None
625 try:
626 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800627 src_sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800628 src_sock.connect(('localhost', self._port))
629 src_sock.setblocking(False)
630
631 # Pass the leftovers of the previous buffer
632 if self._buf:
633 src_sock.send(self._buf)
634 self._buf = ''
635
636 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800637 rd, unused_wd, unused_xd = select.select([self._sock, src_sock], [], [])
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800638
639 if self._sock in rd:
640 data = self._sock.recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800641 if len(data) == 0:
642 raise RuntimeError('connection terminated')
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800643 src_sock.send(data)
644
645 if src_sock in rd:
646 data = src_sock.recv(_BUFSIZE)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800647 if len(data) == 0:
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800648 break
649 self._sock.send(data)
650 except Exception as e:
651 logging.error('SpawnPortForwardServer: %s', e)
652 finally:
653 if src_sock:
654 src_sock.close()
655 self._sock.close()
656
657 logging.info('SpawnPortForwardServer: terminated')
658 sys.exit(0)
659
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800660 def Ping(self):
661 def timeout_handler(x):
662 if x is None:
663 raise PingTimeoutError
664
665 self._last_ping = self.Timestamp()
666 self.SendRequest('ping', {}, timeout_handler, 5)
667
Wei-Ning Huangae923642015-09-24 14:08:09 +0800668 def HandleFileDownloadRequest(self, msg):
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800669 params = msg['params']
Wei-Ning Huangae923642015-09-24 14:08:09 +0800670 filepath = params['filename']
671 if not os.path.isabs(filepath):
672 filepath = os.path.join(os.getenv('HOME', '/tmp'), filepath)
673
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800674 try:
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800675 with open(filepath, 'r') as _:
Wei-Ning Huang46a3fc92015-10-06 02:35:27 +0800676 pass
677 except Exception as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800678 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800679
680 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800681 file_op=('download', filepath))
682 self.SendResponse(msg, SUCCESS)
683
684 def HandleFileUploadRequest(self, msg):
685 params = msg['params']
686
687 # Resolve upload filepath
688 filename = params['filename']
689 dest_path = filename
690
691 # If dest is specified, use it first
692 dest_path = params.get('dest', '')
693 if dest_path:
694 if not os.path.isabs(dest_path):
695 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
696
697 if os.path.isdir(dest_path):
698 dest_path = os.path.join(dest_path, filename)
699 else:
700 target_dir = os.getenv('HOME', '/tmp')
701
702 # Terminal session ID found, upload to it's current working directory
703 if params.has_key('terminal_sid'):
704 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
705 if pid:
706 target_dir = os.readlink('/proc/%d/cwd' % pid)
707
708 dest_path = os.path.join(target_dir, filename)
709
710 try:
711 os.makedirs(os.path.dirname(dest_path))
712 except Exception:
713 pass
714
715 try:
716 with open(dest_path, 'w') as _:
717 pass
718 except Exception as e:
719 return self.SendResponse(msg, str(e))
720
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800721 # If not check_only, spawn FILE mode ghost agent to handle upload
722 if not params.get('check_only', False):
723 self.SpawnGhost(self.FILE, params['sid'],
724 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800725 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800726
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800727 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800728 command = msg['name']
729 params = msg['params']
730
731 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800732 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800733 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800734 self.SpawnGhost(self.TERMINAL, params['sid'],
735 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800736 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800737 elif command == 'shell':
738 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800739 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800740 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800741 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800742 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800743 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800744 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800745 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800746 elif command == 'forward':
747 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
748 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800749
750 def HandleResponse(self, response):
751 rid = str(response['rid'])
752 if rid in self._requests:
753 handler = self._requests[rid][2]
754 del self._requests[rid]
755 if callable(handler):
756 handler(response)
757 else:
Joel Kitching22b89042015-08-06 18:23:29 +0800758 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800759
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800760 def ParseMessage(self, single=True):
761 if single:
762 index = self._buf.index(_SEPARATOR)
763 msgs_json = [self._buf[:index]]
764 self._buf = self._buf[index + 2:]
765 else:
766 msgs_json = self._buf.split(_SEPARATOR)
767 self._buf = msgs_json.pop()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800768
769 for msg_json in msgs_json:
770 try:
771 msg = json.loads(msg_json)
772 except ValueError:
773 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800774 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800775 continue
776
777 if 'name' in msg:
778 self.HandleRequest(msg)
779 elif 'response' in msg:
780 self.HandleResponse(msg)
781 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800782 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800783
784 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800785 """Scans for pending requests which have timed out.
786
787 If any timed-out requests are discovered, their handler is called with the
788 special response value of None.
789 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800790 for rid in self._requests.keys()[:]:
791 request_time, timeout, handler = self._requests[rid]
792 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800793 if callable(handler):
794 handler(None)
795 else:
796 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800797 del self._requests[rid]
798
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800799 def InitiateDownload(self):
800 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800801 sid = self._ttyname_to_sid[ttyname]
802 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +0800803 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800804
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800805 def Listen(self):
806 try:
807 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800808 rds, unused_wd, unused_xd = select.select([self._sock], [], [],
809 _PING_INTERVAL / 2)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800810
811 if self._sock in rds:
Wei-Ning Huang09c19612015-11-24 16:29:09 +0800812 data = self._sock.recv(_BUFSIZE)
813
814 # Socket is closed
815 if len(data) == 0:
816 self.Reset()
817 break
818
819 self._buf += data
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800820 self.ParseMessage(self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800821
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800822 if (self._mode == self.AGENT and
823 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800824 self.Ping()
825 self.ScanForTimeoutRequests()
826
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800827 if not self._download_queue.empty():
828 self.InitiateDownload()
829
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800830 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800831 self.Reset()
832 break
833 except socket.error:
834 raise RuntimeError('Connection dropped')
835 except PingTimeoutError:
836 raise RuntimeError('Connection timeout')
837 finally:
838 self._sock.close()
839
840 self._queue.put('resume')
841
842 if self._mode != Ghost.AGENT:
843 sys.exit(1)
844
845 def Register(self):
846 non_local = {}
847 for addr in self._overlord_addrs:
848 non_local['addr'] = addr
849 def registered(response):
850 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800851 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800852 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800853
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800854 self._register_status = response['response']
855 if response['response'] != SUCCESS:
856 self._reset.set()
857 raise RuntimeError('Reigster: ' + response['response'])
858 else:
859 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
860 self._connected_addr = non_local['addr']
861 self.Upgrade() # Check for upgrade
862 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +0800863
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800864 try:
865 logging.info('Trying %s:%d ...', *addr)
866 self.Reset()
867 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800868 self._sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800869 self._sock.connect(addr)
870
871 logging.info('Connection established, registering...')
872 handler = {
873 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800874 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800875 Ghost.SHELL: self.SpawnShellServer,
876 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800877 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800878 }[self._mode]
879
880 # Machine ID may change if MAC address is used (USB-ethernet dongle
881 # plugged/unplugged)
882 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800883 self.SendRequest('register',
884 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800885 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800886 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800887 except socket.error:
888 pass
889 else:
890 self._sock.settimeout(None)
891 self.Listen()
892
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800893 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800894
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800895 def Reconnect(self):
896 logging.info('Received reconnect request from RPC server, reconnecting...')
897 self._reset.set()
898
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800899 def GetStatus(self):
900 return self._register_status
901
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800902 def AddToDownloadQueue(self, ttyname, filename):
903 self._download_queue.put((ttyname, filename))
904
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800905 def RegisterTTY(self, session_id, ttyname):
906 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +0800907
908 def RegisterSession(self, session_id, process_id):
909 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800910
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800911 def StartLanDiscovery(self):
912 """Start to listen to LAN discovery packet at
913 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800914
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800915 def thread_func():
916 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
917 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
918 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800919 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800920 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
921 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800922 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800923 return
924
925 logging.info('LAN Discovery: started')
926 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800927 rd, unused_wd, unused_xd = select.select([s], [], [], 1)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800928
929 if s in rd:
930 data, source_addr = s.recvfrom(_BUFSIZE)
931 parts = data.split()
932 if parts[0] == 'OVERLORD':
933 ip, port = parts[1].split(':')
934 if not ip:
935 ip = source_addr[0]
936 self._queue.put((ip, int(port)), True)
937
938 try:
939 obj = self._queue.get(False)
940 except Queue.Empty:
941 pass
942 else:
943 if type(obj) is not str:
944 self._queue.put(obj)
945 elif obj == 'pause':
946 logging.info('LAN Discovery: paused')
947 while obj != 'resume':
948 obj = self._queue.get(True)
949 logging.info('LAN Discovery: resumed')
950
951 t = threading.Thread(target=thread_func)
952 t.daemon = True
953 t.start()
954
955 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800956 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800957 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
958 logRequests=False)
959 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800960 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800961 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800962 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800963 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800964 t = threading.Thread(target=rpc_server.serve_forever)
965 t.daemon = True
966 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800967
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800968 def ScanServer(self):
969 for meth in [self.GetGateWayIP, self.GetShopfloorIP]:
970 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
971 if addr not in self._overlord_addrs:
972 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800973
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800974 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800975 logging.info('%s started', self.MODE_NAME[self._mode])
976 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800977 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800978
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800979 # We don't care about child process's return code, not wait is needed. This
980 # is used to prevent zombie process from lingering in the system.
981 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800982
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800983 if lan_disc:
984 self.StartLanDiscovery()
985
986 if rpc_server:
987 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800988
989 try:
990 while True:
991 try:
992 addr = self._queue.get(False)
993 except Queue.Empty:
994 pass
995 else:
996 if type(addr) == tuple and addr not in self._overlord_addrs:
997 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
998 self._overlord_addrs.append(addr)
999
1000 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001001 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001002 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001003 # Don't show stack trace for RuntimeError, which we use in this file for
1004 # plausible and expected errors (such as can't connect to server).
1005 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001006 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +08001007 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001008 except Exception as e:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001009 unused_x, unused_y, exc_traceback = sys.exc_info()
Joel Kitching22b89042015-08-06 18:23:29 +08001010 traceback.print_tb(exc_traceback)
1011 logging.info('%s: %s, retrying in %ds',
1012 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001013 time.sleep(_RETRY_INTERVAL)
1014
1015 self.Reset()
1016 except KeyboardInterrupt:
1017 logging.error('Received keyboard interrupt, quit')
1018 sys.exit(0)
1019
1020
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001021def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001022 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001023 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1024
1025
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001026def ForkToBackground():
1027 """Fork process to run in background."""
1028 pid = os.fork()
1029 if pid != 0:
1030 logging.info('Ghost(%d) running in background.', pid)
1031 sys.exit(0)
1032
1033
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001034def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001035 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001036 filepath = os.path.abspath(filename)
1037 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001038 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001039 sys.exit(1)
1040
1041 # Check if we actually have permission to read the file
1042 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001043 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001044 sys.exit(1)
1045
1046 server = GhostRPCServer()
1047 server.AddToDownloadQueue(os.ttyname(0), filepath)
1048 sys.exit(0)
1049
1050
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001051def main():
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001052 # Setup logging format
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001053 logger = logging.getLogger()
1054 logger.setLevel(logging.INFO)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001055 handler = logging.StreamHandler()
1056 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1057 handler.setFormatter(formatter)
1058 logger.addHandler(handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001059
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001060 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001061 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1062 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001063 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1064 default=None, help='use MID as machine ID')
1065 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1066 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001067 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1068 default=True, help='disable LAN discovery')
1069 parser.add_argument('--no-rpc-server', dest='rpc_server',
1070 action='store_false', default=True,
1071 help='disable RPC server')
Joel Kitching22b89042015-08-06 18:23:29 +08001072 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001073 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001074 help='file containing the JSON representation of client '
1075 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001076 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1077 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001078 parser.add_argument('--reset', dest='reset', default=False,
1079 action='store_true',
1080 help='reset ghost and reload all configs')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001081 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1082 nargs='*', help='overlord server address')
1083 args = parser.parse_args()
1084
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001085 if args.fork:
1086 ForkToBackground()
1087
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001088 if args.reset:
1089 GhostRPCServer().Reconnect()
1090 sys.exit()
1091
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001092 if args.download:
1093 DownloadFile(args.download)
1094
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001095 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001096 addrs += [(x, _OVERLORD_PORT) for x in args.overlord_ip]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001097
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001098 g = Ghost(addrs, Ghost.AGENT, args.mid, prop_file=args.prop_file)
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001099 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001100
1101
1102if __name__ == '__main__':
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001103 main()