blob: da58ead881658fa83885f415b166e00d7727f512 [file] [log] [blame]
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +08001#!/usr/bin/env python
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08002# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Peter Shih5cafebb2017-06-30 16:36:22 +08006from __future__ import print_function
7
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08008import argparse
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08009import contextlib
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +080010import ctypes
11import ctypes.util
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
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +080017import platform
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080018import Queue
Wei-Ning Huang829e0c82015-05-26 14:37:23 +080019import re
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080020import select
Wei-Ning Huanga301f572015-06-03 17:34:21 +080021import signal
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080022import socket
Wei-Ning Huangf5311a02016-02-04 15:23:46 +080023import ssl
Wei-Ning Huangb05cde32015-08-01 09:48:41 +080024import struct
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080025import subprocess
26import sys
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080027import termios
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080028import threading
29import time
Joel Kitching22b89042015-08-06 18:23:29 +080030import traceback
Wei-Ning Huang39169902015-09-19 06:00:23 +080031import tty
Wei-Ning Huange0def6a2015-11-05 15:41:24 +080032import urllib2
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080033import uuid
34
Wei-Ning Huang2132de32015-04-13 17:24:38 +080035import jsonrpclib
36from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
37
38
Peter Shihc56c5b62016-12-22 12:30:57 +080039_GHOST_RPC_PORT = int(os.getenv('GHOST_RPC_PORT', 4499))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080040
Peter Shihc56c5b62016-12-22 12:30:57 +080041_OVERLORD_PORT = int(os.getenv('OVERLORD_PORT', 4455))
42_OVERLORD_LAN_DISCOVERY_PORT = int(os.getenv('OVERLORD_LD_PORT', 4456))
43_OVERLORD_HTTP_PORT = int(os.getenv('OVERLORD_HTTP_PORT', 9000))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080044
45_BUFSIZE = 8192
46_RETRY_INTERVAL = 2
47_SEPARATOR = '\r\n'
48_PING_TIMEOUT = 3
49_PING_INTERVAL = 5
50_REQUEST_TIMEOUT_SECS = 60
51_SHELL = os.getenv('SHELL', '/bin/bash')
Wei-Ning Huang7dbf4a72016-03-02 20:16:20 +080052_DEFAULT_BIND_ADDRESS = 'localhost'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080053
Moja Hsuc9ecc8b2015-07-13 11:39:17 +080054_CONTROL_START = 128
55_CONTROL_END = 129
56
Wei-Ning Huanga301f572015-06-03 17:34:21 +080057_BLOCK_SIZE = 4096
Wei-Ning Huange0def6a2015-11-05 15:41:24 +080058_CONNECT_TIMEOUT = 3
Wei-Ning Huanga301f572015-06-03 17:34:21 +080059
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080060# Stream control
61_STDIN_CLOSED = '##STDIN_CLOSED##'
62
Wei-Ning Huang7ec55342015-09-17 08:46:06 +080063SUCCESS = 'success'
64FAILED = 'failed'
65DISCONNECTED = 'disconnected'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080066
Joel Kitching22b89042015-08-06 18:23:29 +080067
Wei-Ning Huang1cea6112015-03-02 12:45:34 +080068class PingTimeoutError(Exception):
69 pass
70
71
72class RequestError(Exception):
73 pass
74
75
Wei-Ning Huangf5311a02016-02-04 15:23:46 +080076class BufferedSocket(object):
Wei-Ning Huanga28cd232016-01-27 15:04:41 +080077 """A buffered socket that supports unrecv.
78
79 Allow putting back data back to the socket for the next recv() call.
80 """
Wei-Ning Huangf5311a02016-02-04 15:23:46 +080081 def __init__(self, sock):
82 self.sock = sock
Wei-Ning Huanga28cd232016-01-27 15:04:41 +080083 self._buf = ''
84
Wei-Ning Huangf5311a02016-02-04 15:23:46 +080085 def fileno(self):
86 return self.sock.fileno()
87
Wei-Ning Huanga28cd232016-01-27 15:04:41 +080088 def Recv(self, bufsize, flags=0):
89 if self._buf:
90 if len(self._buf) >= bufsize:
91 ret = self._buf[:bufsize]
92 self._buf = self._buf[bufsize:]
93 return ret
94 else:
95 ret = self._buf
96 self._buf = ''
Wei-Ning Huangf5311a02016-02-04 15:23:46 +080097 return ret + self.sock.recv(bufsize - len(ret), flags)
Wei-Ning Huanga28cd232016-01-27 15:04:41 +080098 else:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +080099 return self.sock.recv(bufsize, flags)
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800100
101 def UnRecv(self, buf):
102 self._buf = buf + self._buf
103
104 def Send(self, *args, **kwargs):
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800105 return self.sock.send(*args, **kwargs)
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800106
107 def RecvBuf(self):
108 """Only recive from buffer."""
109 ret = self._buf
110 self._buf = ''
111 return ret
112
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800113 def Close(self):
114 self.sock.close()
115
116
117class TLSSettings(object):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800118 def __init__(self, tls_cert_file, verify):
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800119 """Constructor.
120
121 Args:
122 tls_cert_file: TLS certificate in PEM format.
123 enable_tls_without_verify: enable TLS but don't verify certificate.
124 """
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800125 self._enabled = False
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800126 self._tls_cert_file = tls_cert_file
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800127 self._verify = verify
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800128 self._tls_context = None
129
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800130 def _UpdateContext(self):
131 if not self._enabled:
132 self._tls_context = None
133 return
134
135 self._tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
136 self._tls_context.verify_mode = ssl.CERT_REQUIRED
137
138 if self._verify:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800139 if self._tls_cert_file:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800140 self._tls_context.check_hostname = True
141 try:
142 self._tls_context.load_verify_locations(self._tls_cert_file)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800143 logging.info('TLSSettings: using user-supplied ca-certificate')
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800144 except IOError as e:
145 logging.error('TLSSettings: %s: %s', self._tls_cert_file, e)
146 sys.exit(1)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800147 else:
148 self._tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
149 logging.info('TLSSettings: using built-in ca-certificates')
150 else:
151 self._tls_context.verify_mode = ssl.CERT_NONE
152 logging.info('TLSSettings: skipping TLS verification!!!')
153
154 def SetEnabled(self, enabled):
155 logging.info('TLSSettings: enabled: %s', enabled)
156
157 if self._enabled != enabled:
158 self._enabled = enabled
159 self._UpdateContext()
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800160
161 def Enabled(self):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800162 return self._enabled
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800163
164 def Context(self):
165 return self._tls_context
166
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800167
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800168class Ghost(object):
169 """Ghost implements the client protocol of Overlord.
170
171 Ghost provide terminal/shell/logcat functionality and manages the client
172 side connectivity.
173 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800174 NONE, AGENT, TERMINAL, SHELL, LOGCAT, FILE, FORWARD = range(7)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800175
176 MODE_NAME = {
177 NONE: 'NONE',
178 AGENT: 'Agent',
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800179 TERMINAL: 'Terminal',
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800180 SHELL: 'Shell',
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800181 LOGCAT: 'Logcat',
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800182 FILE: 'File',
183 FORWARD: 'Forward'
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800184 }
185
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800186 RANDOM_MID = '##random_mid##'
187
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800188 def __init__(self, overlord_addrs, tls_settings=None, mode=AGENT, mid=None,
189 sid=None, prop_file=None, terminal_sid=None, tty_device=None,
Peter Shih220a96d2016-12-22 17:02:16 +0800190 command=None, file_op=None, port=None, tls_mode=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800191 """Constructor.
192
193 Args:
194 overlord_addrs: a list of possible address of overlord.
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800195 tls_settings: a TLSSetting object.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800196 mode: client mode, either AGENT, SHELL or LOGCAT
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800197 mid: a str to set for machine ID. If mid equals Ghost.RANDOM_MID, machine
198 id is randomly generated.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800199 sid: session ID. If the connection is requested by overlord, sid should
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800200 be set to the corresponding session id assigned by overlord.
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800201 prop_file: properties file filename.
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800202 terminal_sid: the terminal session ID associate with this client. This is
203 use for file download.
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800204 tty_device: the terminal device to open, if tty_device is None, as pseudo
205 terminal will be opened instead.
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800206 command: the command to execute when we are in SHELL mode.
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800207 file_op: a tuple (action, filepath, perm). action is either 'download' or
208 'upload'. perm is the permission to set for the file.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800209 port: port number to forward.
Peter Shih220a96d2016-12-22 17:02:16 +0800210 tls_mode: can be [True, False, None]. if not None, skip detection of
211 TLS and assume whether server use TLS or not.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800212 """
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800213 assert mode in [Ghost.AGENT, Ghost.TERMINAL, Ghost.SHELL, Ghost.FILE,
214 Ghost.FORWARD]
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800215 if mode == Ghost.SHELL:
216 assert command is not None
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800217 if mode == Ghost.FILE:
218 assert file_op is not None
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800219
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800220 self._platform = platform.system()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800221 self._overlord_addrs = overlord_addrs
Wei-Ning Huangad330c52015-03-12 20:34:18 +0800222 self._connected_addr = None
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800223 self._tls_settings = tls_settings
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800224 self._mid = mid
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800225 self._sock = None
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800226 self._mode = mode
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800227 self._machine_id = self.GetMachineID()
Wei-Ning Huangfed95862015-08-07 03:17:11 +0800228 self._session_id = sid if sid is not None else str(uuid.uuid4())
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800229 self._terminal_session_id = terminal_sid
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800230 self._ttyname_to_sid = {}
231 self._terminal_sid_to_pid = {}
232 self._prop_file = prop_file
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800233 self._properties = {}
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800234 self._register_status = DISCONNECTED
235 self._reset = threading.Event()
Peter Shih220a96d2016-12-22 17:02:16 +0800236 self._tls_mode = tls_mode
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800237
238 # RPC
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800239 self._requests = {}
240 self._queue = Queue.Queue()
241
242 # Protocol specific
243 self._last_ping = 0
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800244 self._tty_device = tty_device
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800245 self._shell_command = command
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800246 self._file_op = file_op
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800247 self._download_queue = Queue.Queue()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800248 self._port = port
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800249
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800250 def SetIgnoreChild(self, status):
251 # Only ignore child for Agent since only it could spawn child Ghost.
252 if self._mode == Ghost.AGENT:
253 signal.signal(signal.SIGCHLD,
254 signal.SIG_IGN if status else signal.SIG_DFL)
255
256 def GetFileSha1(self, filename):
257 with open(filename, 'r') as f:
258 return hashlib.sha1(f.read()).hexdigest()
259
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800260 def TLSEnabled(self, host, port):
261 """Determine if TLS is enabled on given server address."""
Wei-Ning Huang58833882015-09-16 16:52:37 +0800262 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
263 try:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800264 # Allow any certificate since we only want to check if server talks TLS.
265 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
266 context.verify_mode = ssl.CERT_NONE
Wei-Ning Huang58833882015-09-16 16:52:37 +0800267
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800268 sock = context.wrap_socket(sock, server_hostname=host)
269 sock.settimeout(_CONNECT_TIMEOUT)
270 sock.connect((host, port))
271 return True
Wei-Ning Huangb6605d22016-06-22 17:33:37 +0800272 except ssl.SSLError:
273 return False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800274 except socket.error: # Connect refused or timeout
275 raise
Wei-Ning Huang58833882015-09-16 16:52:37 +0800276 except Exception:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800277 return False # For whatever reason above failed, assume False
Wei-Ning Huang58833882015-09-16 16:52:37 +0800278
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800279 def Upgrade(self):
280 logging.info('Upgrade: initiating upgrade sequence...')
281
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800282 try:
Wei-Ning Huangb6605d22016-06-22 17:33:37 +0800283 https_enabled = self.TLSEnabled(self._connected_addr[0],
284 _OVERLORD_HTTP_PORT)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800285 except socket.error:
Wei-Ning Huangb6605d22016-06-22 17:33:37 +0800286 logging.error('Upgrade: failed to connect to Overlord HTTP server, '
287 'abort')
288 return
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800289
290 if self._tls_settings.Enabled() and not https_enabled:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800291 logging.error('Upgrade: TLS enforced but found Overlord HTTP server '
292 'without TLS enabled! Possible mis-configuration or '
293 'DNS/IP spoofing detected, abort')
294 return
295
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800296 scriptpath = os.path.abspath(sys.argv[0])
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800297 url = 'http%s://%s:%d/upgrade/ghost.py' % (
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800298 's' if https_enabled else '', self._connected_addr[0],
Wei-Ning Huang03f9f762015-09-16 21:51:35 +0800299 _OVERLORD_HTTP_PORT)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800300
301 # Download sha1sum for ghost.py for verification
302 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800303 with contextlib.closing(
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800304 urllib2.urlopen(url + '.sha1', timeout=_CONNECT_TIMEOUT,
305 context=self._tls_settings.Context())) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800306 if f.getcode() != 200:
307 raise RuntimeError('HTTP status %d' % f.getcode())
308 sha1sum = f.read().strip()
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800309 except (ssl.SSLError, ssl.CertificateError) as e:
310 logging.error('Upgrade: %s: %s', e.__class__.__name__, e)
311 return
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800312 except Exception:
313 logging.error('Upgrade: failed to download sha1sum file, abort')
314 return
315
316 if self.GetFileSha1(scriptpath) == sha1sum:
317 logging.info('Upgrade: ghost is already up-to-date, skipping upgrade')
318 return
319
320 # Download upgrade version of ghost.py
321 try:
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800322 with contextlib.closing(
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800323 urllib2.urlopen(url, timeout=_CONNECT_TIMEOUT,
324 context=self._tls_settings.Context())) as f:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800325 if f.getcode() != 200:
326 raise RuntimeError('HTTP status %d' % f.getcode())
327 data = f.read()
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800328 except (ssl.SSLError, ssl.CertificateError) as e:
329 logging.error('Upgrade: %s: %s', e.__class__.__name__, e)
330 return
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800331 except Exception:
332 logging.error('Upgrade: failed to download upgrade, abort')
333 return
334
335 # Compare SHA1 sum
336 if hashlib.sha1(data).hexdigest() != sha1sum:
337 logging.error('Upgrade: sha1sum mismatch, abort')
338 return
339
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800340 try:
341 with open(scriptpath, 'w') as f:
342 f.write(data)
343 except Exception:
344 logging.error('Upgrade: failed to write upgrade onto disk, abort')
345 return
346
347 logging.info('Upgrade: restarting ghost...')
348 self.CloseSockets()
349 self.SetIgnoreChild(False)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800350 os.execve(scriptpath, [scriptpath] + sys.argv[1:], os.environ)
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800351
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800352 def LoadProperties(self):
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800353 try:
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800354 if self._prop_file:
355 with open(self._prop_file, 'r') as f:
356 self._properties = json.loads(f.read())
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800357 except Exception as e:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800358 logging.error('LoadProperties: ' + str(e))
Wei-Ning Huang7d029b12015-03-06 10:32:15 +0800359
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800360 def CloseSockets(self):
361 # Close sockets opened by parent process, since we don't use it anymore.
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800362 if self._platform == 'Linux':
363 for fd in os.listdir('/proc/self/fd/'):
364 try:
365 real_fd = os.readlink('/proc/self/fd/%s' % fd)
366 if real_fd.startswith('socket'):
367 os.close(int(fd))
368 except Exception:
369 pass
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800370
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800371 def SpawnGhost(self, mode, sid=None, terminal_sid=None, tty_device=None,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800372 command=None, file_op=None, port=None):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800373 """Spawn a child ghost with specific mode.
374
375 Returns:
376 The spawned child process pid.
377 """
Joel Kitching22b89042015-08-06 18:23:29 +0800378 # Restore the default signal handler, so our child won't have problems.
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800379 self.SetIgnoreChild(False)
380
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800381 pid = os.fork()
382 if pid == 0:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800383 self.CloseSockets()
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800384 g = Ghost([self._connected_addr], tls_settings=self._tls_settings,
385 mode=mode, mid=Ghost.RANDOM_MID, sid=sid,
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800386 terminal_sid=terminal_sid, tty_device=tty_device,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800387 command=command, file_op=file_op, port=port)
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800388 g.Start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800389 sys.exit(0)
390 else:
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800391 self.SetIgnoreChild(True)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800392 return pid
393
394 def Timestamp(self):
395 return int(time.time())
396
397 def GetGateWayIP(self):
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800398 if self._platform == 'Darwin':
399 output = subprocess.check_output(['route', '-n', 'get', 'default'])
400 ret = re.search('gateway: (.*)', output)
401 if ret:
402 return [ret.group(1)]
403 elif self._platform == 'Linux':
404 with open('/proc/net/route', 'r') as f:
405 lines = f.readlines()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800406
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800407 ips = []
408 for line in lines:
409 parts = line.split('\t')
410 if parts[2] == '00000000':
411 continue
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800412
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800413 try:
414 h = parts[2].decode('hex')
415 ips.append('%d.%d.%d.%d' % tuple(ord(x) for x in reversed(h)))
416 except TypeError:
417 pass
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800418
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800419 return ips
420 else:
421 logging.warning('GetGateWayIP: unsupported platform')
422 return []
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800423
Hung-Te Lin41ff8f32017-08-30 08:10:39 +0800424 def GetFactoryServerIP(self):
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800425 try:
Peter Shihcb0e5512017-06-14 16:59:46 +0800426 import factory_common # pylint: disable=unused-variable
Hung-Te Lin41ff8f32017-08-30 08:10:39 +0800427 from cros.factory.test import server_proxy
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800428
Hung-Te Lin41ff8f32017-08-30 08:10:39 +0800429 url = server_proxy.GetServerURL()
Wei-Ning Huang829e0c82015-05-26 14:37:23 +0800430 match = re.match(r'^https?://(.*):.*$', url)
431 if match:
432 return [match.group(1)]
433 except Exception:
434 pass
435 return []
436
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800437 def GetMachineID(self):
438 """Generates machine-dependent ID string for a machine.
439 There are many ways to generate a machine ID:
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800440 Linux:
441 1. factory device_id
Peter Shih5f1f48c2017-06-26 14:12:00 +0800442 2. /sys/class/dmi/id/product_uuid (only available on intel machines)
443 3. MAC address
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800444 We follow the listed order to generate machine ID, and fallback to the
445 next alternative if the previous doesn't work.
446
447 Darwin:
448 All Darwin system should have the IOPlatformSerialNumber attribute.
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800449 """
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800450 if self._mid == Ghost.RANDOM_MID:
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800451 return str(uuid.uuid4())
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +0800452 elif self._mid:
453 return self._mid
Wei-Ning Huangaed90452015-03-23 17:50:21 +0800454
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800455 # Darwin
456 if self._platform == 'Darwin':
457 output = subprocess.check_output(['ioreg', '-rd1', '-c',
458 'IOPlatformExpertDevice'])
459 ret = re.search('"IOPlatformSerialNumber" = "(.*)"', output)
460 if ret:
461 return ret.group(1)
462
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800463 # Try factory device id
464 try:
Peter Shihcb0e5512017-06-14 16:59:46 +0800465 import factory_common # pylint: disable=unused-variable
Peter Shihdf6ee852017-07-03 18:28:45 +0800466 from cros.factory.test import testlog_goofy
467 return testlog_goofy.GetDeviceID()
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800468 except Exception:
469 pass
470
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800471 # Try DMI product UUID
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800472 try:
473 with open('/sys/class/dmi/id/product_uuid', 'r') as f:
474 return f.read().strip()
475 except Exception:
476 pass
477
Wei-Ning Huang1d7603b2015-07-03 17:38:56 +0800478 # Use MAC address if non is available
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800479 try:
480 macs = []
481 ifaces = sorted(os.listdir('/sys/class/net'))
482 for iface in ifaces:
483 if iface == 'lo':
484 continue
485
486 with open('/sys/class/net/%s/address' % iface, 'r') as f:
487 macs.append(f.read().strip())
488
489 return ';'.join(macs)
490 except Exception:
491 pass
492
Peter Shihcb0e5512017-06-14 16:59:46 +0800493 raise RuntimeError("can't generate machine ID")
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800494
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800495 def GetProcessWorkingDirectory(self, pid):
496 if self._platform == 'Linux':
497 return os.readlink('/proc/%d/cwd' % pid)
498 elif self._platform == 'Darwin':
499 PROC_PIDVNODEPATHINFO = 9
500 proc_vnodepathinfo_size = 2352
501 vid_path_offset = 152
502
503 proc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('libproc'))
504 buf = ctypes.create_string_buffer('\0' * proc_vnodepathinfo_size)
505 proc.proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0,
506 ctypes.byref(buf), proc_vnodepathinfo_size)
507 buf = buf.raw[vid_path_offset:]
508 n = buf.index('\0')
509 return buf[:n]
510 else:
511 raise RuntimeError('GetProcessWorkingDirectory: unsupported platform')
512
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800513 def Reset(self):
514 """Reset state and clear request handlers."""
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800515 if self._sock is not None:
516 self._sock.Close()
517 self._sock = None
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800518 self._reset.clear()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800519 self._last_ping = 0
520 self._requests = {}
Wei-Ning Huang23ed0162015-09-18 14:42:03 +0800521 self.LoadProperties()
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800522 self._register_status = DISCONNECTED
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800523
524 def SendMessage(self, msg):
525 """Serialize the message and send it through the socket."""
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800526 self._sock.Send(json.dumps(msg) + _SEPARATOR)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800527
528 def SendRequest(self, name, args, handler=None,
529 timeout=_REQUEST_TIMEOUT_SECS):
530 if handler and not callable(handler):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800531 raise RequestError('Invalid request handler for msg "%s"' % name)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800532
533 rid = str(uuid.uuid4())
534 msg = {'rid': rid, 'timeout': timeout, 'name': name, 'params': args}
Wei-Ning Huange2981862015-08-03 15:03:08 +0800535 if timeout >= 0:
536 self._requests[rid] = [self.Timestamp(), timeout, handler]
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800537 self.SendMessage(msg)
538
539 def SendResponse(self, omsg, status, params=None):
540 msg = {'rid': omsg['rid'], 'response': status, 'params': params}
541 self.SendMessage(msg)
542
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800543 def HandleTTYControl(self, fd, control_str):
544 msg = json.loads(control_str)
Moja Hsuc9ecc8b2015-07-13 11:39:17 +0800545 command = msg['command']
546 params = msg['params']
547 if command == 'resize':
548 # some error happened on websocket
549 if len(params) != 2:
550 return
551 winsize = struct.pack('HHHH', params[0], params[1], 0, 0)
552 fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
553 else:
554 logging.warn('Invalid request command "%s"', command)
555
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800556 def SpawnTTYServer(self, unused_var):
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800557 """Spawn a TTY server and forward I/O to the TCP socket."""
558 logging.info('SpawnTTYServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800559
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800560 try:
561 if self._tty_device is None:
562 pid, fd = os.forkpty()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800563
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800564 if pid == 0:
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800565 ttyname = os.ttyname(sys.stdout.fileno())
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800566 try:
567 server = GhostRPCServer()
568 server.RegisterTTY(self._session_id, ttyname)
569 server.RegisterSession(self._session_id, os.getpid())
570 except Exception:
571 # If ghost is launched without RPC server, the call will fail but we
572 # can ignore it.
573 pass
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800574
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800575 # The directory that contains the current running ghost script
576 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800577
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800578 env = os.environ.copy()
579 env['USER'] = os.getenv('USER', 'root')
580 env['HOME'] = os.getenv('HOME', '/root')
581 env['PATH'] = os.getenv('PATH') + ':%s' % script_dir
582 os.chdir(env['HOME'])
583 os.execve(_SHELL, [_SHELL], env)
584 else:
585 fd = os.open(self._tty_device, os.O_RDWR)
Wei-Ning Huang39169902015-09-19 06:00:23 +0800586 tty.setraw(fd)
587 attr = termios.tcgetattr(fd)
588 attr[0] &= ~(termios.IXON | termios.IXOFF)
589 attr[2] |= termios.CLOCAL
590 attr[2] &= ~termios.CRTSCTS
591 attr[4] = termios.B115200
592 attr[5] = termios.B115200
593 termios.tcsetattr(fd, termios.TCSANOW, attr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800594
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800595 nonlocals = {'control_state': None, 'control_str': ''}
596
597 def _ProcessBuffer(buf):
598 write_buffer = ''
599 while buf:
600 if nonlocals['control_state']:
601 if chr(_CONTROL_END) in buf:
602 index = buf.index(chr(_CONTROL_END))
603 nonlocals['control_str'] += buf[:index]
604 self.HandleTTYControl(fd, nonlocals['control_str'])
605 nonlocals['control_state'] = None
606 nonlocals['control_str'] = ''
607 buf = buf[index+1:]
608 else:
609 nonlocals['control_str'] += buf
610 buf = ''
611 else:
612 if chr(_CONTROL_START) in buf:
613 nonlocals['control_state'] = _CONTROL_START
614 index = buf.index(chr(_CONTROL_START))
615 write_buffer += buf[:index]
616 buf = buf[index+1:]
617 else:
618 write_buffer += buf
619 buf = ''
620
621 if write_buffer:
622 os.write(fd, write_buffer)
623
624 _ProcessBuffer(self._sock.RecvBuf())
625
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800626 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800627 rd, unused_wd, unused_xd = select.select([self._sock, fd], [], [])
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800628
629 if fd in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800630 self._sock.Send(os.read(fd, _BUFSIZE))
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800631
632 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800633 buf = self._sock.Recv(_BUFSIZE)
Peter Shihaacbc2f2017-06-16 14:39:29 +0800634 if not buf:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800635 raise RuntimeError('connection terminated')
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800636 _ProcessBuffer(buf)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800637 except Exception as e:
638 logging.error('SpawnTTYServer: %s', e)
639 finally:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800640 self._sock.Close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800641
642 logging.info('SpawnTTYServer: terminated')
643 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800644
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800645 def SpawnShellServer(self, unused_var):
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800646 """Spawn a shell server and forward input/output from/to the TCP socket."""
647 logging.info('SpawnShellServer: started')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800648
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800649 # Add ghost executable to PATH
650 script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
651 env = os.environ.copy()
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800652 env['PATH'] = '%s:%s' % (script_dir, os.getenv('PATH'))
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800653
654 # Execute shell command from HOME directory
655 os.chdir(os.getenv('HOME', '/tmp'))
656
Wei-Ning Huang0f4a5372015-03-09 15:12:07 +0800657 p = subprocess.Popen(self._shell_command, stdin=subprocess.PIPE,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800658 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800659 shell=True, env=env)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800660
661 def make_non_block(fd):
662 fl = fcntl.fcntl(fd, fcntl.F_GETFL)
663 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
664
665 make_non_block(p.stdout)
666 make_non_block(p.stderr)
667
668 try:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800669 p.stdin.write(self._sock.RecvBuf())
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800670
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800671 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800672 rd, unused_wd, unused_xd = select.select(
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800673 [p.stdout, p.stderr, self._sock], [], [])
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800674 if p.stdout in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800675 self._sock.Send(p.stdout.read(_BUFSIZE))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800676
677 if p.stderr in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800678 self._sock.Send(p.stderr.read(_BUFSIZE))
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800679
680 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800681 ret = self._sock.Recv(_BUFSIZE)
Peter Shihaacbc2f2017-06-16 14:39:29 +0800682 if not ret:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800683 raise RuntimeError('connection terminated')
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800684
685 try:
686 idx = ret.index(_STDIN_CLOSED * 2)
687 p.stdin.write(ret[:idx])
688 p.stdin.close()
689 except ValueError:
690 p.stdin.write(ret)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800691 p.poll()
692 if p.returncode != None:
693 break
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800694 except Exception as e:
695 logging.error('SpawnShellServer: %s', e)
Wei-Ning Huangf14c84e2015-08-03 15:03:08 +0800696 finally:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800697 # Check if the process is terminated. If not, Send SIGTERM to process,
698 # then wait for 1 second. Send another SIGKILL to make sure the process is
699 # terminated.
700 p.poll()
701 if p.returncode is None:
702 try:
703 p.terminate()
704 time.sleep(1)
705 p.kill()
706 except Exception:
707 pass
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800708
709 p.wait()
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800710 self._sock.Close()
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800711
712 logging.info('SpawnShellServer: terminated')
713 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800714
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800715 def InitiateFileOperation(self, unused_var):
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800716 if self._file_op[0] == 'download':
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800717 try:
718 size = os.stat(self._file_op[1]).st_size
719 except OSError as e:
720 logging.error('InitiateFileOperation: download: %s', e)
721 sys.exit(1)
722
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800723 self.SendRequest('request_to_download',
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800724 {'terminal_sid': self._terminal_session_id,
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800725 'filename': os.path.basename(self._file_op[1]),
726 'size': size})
Wei-Ning Huange2981862015-08-03 15:03:08 +0800727 elif self._file_op[0] == 'upload':
728 self.SendRequest('clear_to_upload', {}, timeout=-1)
729 self.StartUploadServer()
730 else:
731 logging.error('InitiateFileOperation: unknown file operation, ignored')
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800732
733 def StartDownloadServer(self):
734 logging.info('StartDownloadServer: started')
735
736 try:
737 with open(self._file_op[1], 'rb') as f:
738 while True:
739 data = f.read(_BLOCK_SIZE)
Peter Shihaacbc2f2017-06-16 14:39:29 +0800740 if not data:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800741 break
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800742 self._sock.Send(data)
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800743 except Exception as e:
744 logging.error('StartDownloadServer: %s', e)
745 finally:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800746 self._sock.Close()
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800747
748 logging.info('StartDownloadServer: terminated')
749 sys.exit(0)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800750
Wei-Ning Huange2981862015-08-03 15:03:08 +0800751 def StartUploadServer(self):
752 logging.info('StartUploadServer: started')
Wei-Ning Huange2981862015-08-03 15:03:08 +0800753 try:
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800754 filepath = self._file_op[1]
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800755 dirname = os.path.dirname(filepath)
756 if not os.path.exists(dirname):
757 try:
758 os.makedirs(dirname)
759 except Exception:
760 pass
Wei-Ning Huange2981862015-08-03 15:03:08 +0800761
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800762 with open(filepath, 'wb') as f:
Wei-Ning Huang8ee3bcd2015-10-01 17:10:01 +0800763 if self._file_op[2]:
764 os.fchmod(f.fileno(), self._file_op[2])
765
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800766 f.write(self._sock.RecvBuf())
767
Wei-Ning Huange2981862015-08-03 15:03:08 +0800768 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800769 rd, unused_wd, unused_xd = select.select([self._sock], [], [])
Wei-Ning Huange2981862015-08-03 15:03:08 +0800770 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800771 buf = self._sock.Recv(_BLOCK_SIZE)
Peter Shihaacbc2f2017-06-16 14:39:29 +0800772 if not buf:
Wei-Ning Huange2981862015-08-03 15:03:08 +0800773 break
774 f.write(buf)
775 except socket.error as e:
776 logging.error('StartUploadServer: socket error: %s', e)
777 except Exception as e:
778 logging.error('StartUploadServer: %s', e)
779 finally:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800780 self._sock.Close()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800781
782 logging.info('StartUploadServer: terminated')
783 sys.exit(0)
784
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800785 def SpawnPortForwardServer(self, unused_var):
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800786 """Spawn a port forwarding server and forward I/O to the TCP socket."""
787 logging.info('SpawnPortForwardServer: started')
788
789 src_sock = None
790 try:
791 src_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Wei-Ning Huange0def6a2015-11-05 15:41:24 +0800792 src_sock.settimeout(_CONNECT_TIMEOUT)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800793 src_sock.connect(('localhost', self._port))
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800794
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800795 src_sock.send(self._sock.RecvBuf())
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800796
797 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800798 rd, unused_wd, unused_xd = select.select([self._sock, src_sock], [], [])
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800799
800 if self._sock in rd:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800801 data = self._sock.Recv(_BUFSIZE)
Peter Shihaacbc2f2017-06-16 14:39:29 +0800802 if not data:
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +0800803 raise RuntimeError('connection terminated')
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800804 src_sock.send(data)
805
806 if src_sock in rd:
807 data = src_sock.recv(_BUFSIZE)
Peter Shihaacbc2f2017-06-16 14:39:29 +0800808 if not data:
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800809 break
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800810 self._sock.Send(data)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800811 except Exception as e:
812 logging.error('SpawnPortForwardServer: %s', e)
813 finally:
814 if src_sock:
815 src_sock.close()
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800816 self._sock.Close()
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800817
818 logging.info('SpawnPortForwardServer: terminated')
819 sys.exit(0)
820
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800821 def Ping(self):
822 def timeout_handler(x):
823 if x is None:
824 raise PingTimeoutError
825
826 self._last_ping = self.Timestamp()
827 self.SendRequest('ping', {}, timeout_handler, 5)
828
Wei-Ning Huangae923642015-09-24 14:08:09 +0800829 def HandleFileDownloadRequest(self, msg):
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800830 params = msg['params']
Wei-Ning Huangae923642015-09-24 14:08:09 +0800831 filepath = params['filename']
832 if not os.path.isabs(filepath):
833 filepath = os.path.join(os.getenv('HOME', '/tmp'), filepath)
834
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800835 try:
Wei-Ning Huang11c35022015-10-21 16:52:32 +0800836 with open(filepath, 'r') as _:
Wei-Ning Huang46a3fc92015-10-06 02:35:27 +0800837 pass
838 except Exception as e:
Wei-Ning Huangae923642015-09-24 14:08:09 +0800839 return self.SendResponse(msg, str(e))
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800840
841 self.SpawnGhost(self.FILE, params['sid'],
Wei-Ning Huangae923642015-09-24 14:08:09 +0800842 file_op=('download', filepath))
843 self.SendResponse(msg, SUCCESS)
844
845 def HandleFileUploadRequest(self, msg):
846 params = msg['params']
847
848 # Resolve upload filepath
849 filename = params['filename']
850 dest_path = filename
851
852 # If dest is specified, use it first
853 dest_path = params.get('dest', '')
854 if dest_path:
855 if not os.path.isabs(dest_path):
856 dest_path = os.path.join(os.getenv('HOME', '/tmp'), dest_path)
857
858 if os.path.isdir(dest_path):
859 dest_path = os.path.join(dest_path, filename)
860 else:
861 target_dir = os.getenv('HOME', '/tmp')
862
863 # Terminal session ID found, upload to it's current working directory
864 if params.has_key('terminal_sid'):
865 pid = self._terminal_sid_to_pid.get(params['terminal_sid'], None)
866 if pid:
Wei-Ning Huanga0e55b82016-02-10 14:32:07 +0800867 try:
868 target_dir = self.GetProcessWorkingDirectory(pid)
869 except Exception as e:
870 logging.error(e)
Wei-Ning Huangae923642015-09-24 14:08:09 +0800871
872 dest_path = os.path.join(target_dir, filename)
873
874 try:
875 os.makedirs(os.path.dirname(dest_path))
876 except Exception:
877 pass
878
879 try:
880 with open(dest_path, 'w') as _:
881 pass
882 except Exception as e:
883 return self.SendResponse(msg, str(e))
884
Wei-Ning Huangd6f69762015-10-01 21:02:07 +0800885 # If not check_only, spawn FILE mode ghost agent to handle upload
886 if not params.get('check_only', False):
887 self.SpawnGhost(self.FILE, params['sid'],
888 file_op=('upload', dest_path, params.get('perm', None)))
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800889 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang552cd702015-08-12 16:11:13 +0800890
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800891 def HandleRequest(self, msg):
Wei-Ning Huange2981862015-08-03 15:03:08 +0800892 command = msg['name']
893 params = msg['params']
894
895 if command == 'upgrade':
Wei-Ning Huangb05cde32015-08-01 09:48:41 +0800896 self.Upgrade()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800897 elif command == 'terminal':
Wei-Ning Huangb8461202015-09-01 20:07:41 +0800898 self.SpawnGhost(self.TERMINAL, params['sid'],
899 tty_device=params['tty_device'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800900 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800901 elif command == 'shell':
902 self.SpawnGhost(self.SHELL, params['sid'], command=params['command'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +0800903 self.SendResponse(msg, SUCCESS)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800904 elif command == 'file_download':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800905 self.HandleFileDownloadRequest(msg)
Wei-Ning Huange2981862015-08-03 15:03:08 +0800906 elif command == 'clear_to_download':
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800907 self.StartDownloadServer()
Wei-Ning Huange2981862015-08-03 15:03:08 +0800908 elif command == 'file_upload':
Wei-Ning Huangae923642015-09-24 14:08:09 +0800909 self.HandleFileUploadRequest(msg)
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800910 elif command == 'forward':
911 self.SpawnGhost(self.FORWARD, params['sid'], port=params['port'])
912 self.SendResponse(msg, SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800913
914 def HandleResponse(self, response):
915 rid = str(response['rid'])
916 if rid in self._requests:
917 handler = self._requests[rid][2]
918 del self._requests[rid]
919 if callable(handler):
920 handler(response)
921 else:
Joel Kitching22b89042015-08-06 18:23:29 +0800922 logging.warning('Received unsolicited response, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800923
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800924 def ParseMessage(self, buf, single=True):
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800925 if single:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +0800926 try:
927 index = buf.index(_SEPARATOR)
928 except ValueError:
929 self._sock.UnRecv(buf)
930 return
931
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800932 msgs_json = [buf[:index]]
933 self._sock.UnRecv(buf[index + 2:])
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800934 else:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800935 msgs_json = buf.split(_SEPARATOR)
936 self._sock.UnRecv(msgs_json.pop())
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800937
938 for msg_json in msgs_json:
939 try:
940 msg = json.loads(msg_json)
941 except ValueError:
942 # Ignore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800943 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800944 continue
945
946 if 'name' in msg:
947 self.HandleRequest(msg)
948 elif 'response' in msg:
949 self.HandleResponse(msg)
950 else: # Ingnore mal-formed message.
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +0800951 logging.error('mal-formed JSON request, ignored')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800952
953 def ScanForTimeoutRequests(self):
Joel Kitching22b89042015-08-06 18:23:29 +0800954 """Scans for pending requests which have timed out.
955
956 If any timed-out requests are discovered, their handler is called with the
957 special response value of None.
958 """
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800959 for rid in self._requests.keys()[:]:
960 request_time, timeout, handler = self._requests[rid]
961 if self.Timestamp() - request_time > timeout:
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800962 if callable(handler):
963 handler(None)
964 else:
965 logging.error('Request %s timeout', rid)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800966 del self._requests[rid]
967
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800968 def InitiateDownload(self):
969 ttyname, filename = self._download_queue.get()
Wei-Ning Huangd521f282015-08-07 05:28:04 +0800970 sid = self._ttyname_to_sid[ttyname]
971 self.SpawnGhost(self.FILE, terminal_sid=sid,
Wei-Ning Huangae923642015-09-24 14:08:09 +0800972 file_op=('download', filename))
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800973
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800974 def Listen(self):
975 try:
976 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800977 rds, unused_wd, unused_xd = select.select([self._sock], [], [],
978 _PING_INTERVAL / 2)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800979
980 if self._sock in rds:
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800981 data = self._sock.Recv(_BUFSIZE)
Wei-Ning Huang09c19612015-11-24 16:29:09 +0800982
983 # Socket is closed
Peter Shihaacbc2f2017-06-16 14:39:29 +0800984 if not data:
Wei-Ning Huang09c19612015-11-24 16:29:09 +0800985 break
986
Wei-Ning Huanga28cd232016-01-27 15:04:41 +0800987 self.ParseMessage(data, self._register_status != SUCCESS)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800988
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800989 if (self._mode == self.AGENT and
990 self.Timestamp() - self._last_ping > _PING_INTERVAL):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800991 self.Ping()
992 self.ScanForTimeoutRequests()
993
Wei-Ning Huanga301f572015-06-03 17:34:21 +0800994 if not self._download_queue.empty():
995 self.InitiateDownload()
996
Wei-Ning Huang2132de32015-04-13 17:24:38 +0800997 if self._reset.is_set():
Wei-Ning Huang1cea6112015-03-02 12:45:34 +0800998 break
999 except socket.error:
1000 raise RuntimeError('Connection dropped')
1001 except PingTimeoutError:
1002 raise RuntimeError('Connection timeout')
1003 finally:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001004 self.Reset()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001005
1006 self._queue.put('resume')
1007
1008 if self._mode != Ghost.AGENT:
1009 sys.exit(1)
1010
1011 def Register(self):
1012 non_local = {}
1013 for addr in self._overlord_addrs:
1014 non_local['addr'] = addr
1015 def registered(response):
1016 if response is None:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001017 self._reset.set()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001018 raise RuntimeError('Register request timeout')
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001019
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001020 self._register_status = response['response']
1021 if response['response'] != SUCCESS:
1022 self._reset.set()
Peter Shih220a96d2016-12-22 17:02:16 +08001023 raise RuntimeError('Register: ' + response['response'])
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001024 else:
1025 logging.info('Registered with Overlord at %s:%d', *non_local['addr'])
1026 self._connected_addr = non_local['addr']
1027 self.Upgrade() # Check for upgrade
1028 self._queue.put('pause', True)
Wei-Ning Huang63c16092015-09-18 16:20:27 +08001029
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001030 try:
1031 logging.info('Trying %s:%d ...', *addr)
1032 self.Reset()
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001033
Peter Shih220a96d2016-12-22 17:02:16 +08001034 # Check if server has TLS enabled. Only check if self._tls_mode is
1035 # None.
Wei-Ning Huangb6605d22016-06-22 17:33:37 +08001036 # Only control channel needs to determine if TLS is enabled. Other mode
1037 # should use the TLSSettings passed in when it was spawned.
1038 if self._mode == Ghost.AGENT:
Peter Shih220a96d2016-12-22 17:02:16 +08001039 self._tls_settings.SetEnabled(
1040 self.TLSEnabled(*addr) if self._tls_mode is None
1041 else self._tls_mode)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001042
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001043 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1044 sock.settimeout(_CONNECT_TIMEOUT)
1045
1046 try:
1047 if self._tls_settings.Enabled():
1048 tls_context = self._tls_settings.Context()
1049 sock = tls_context.wrap_socket(sock, server_hostname=addr[0])
1050
1051 sock.connect(addr)
1052 except (ssl.SSLError, ssl.CertificateError) as e:
1053 logging.error('%s: %s', e.__class__.__name__, e)
1054 continue
1055 except IOError as e:
1056 if e.errno == 2: # No such file or directory
1057 logging.error('%s: %s', e.__class__.__name__, e)
1058 continue
1059 raise
1060
1061 self._sock = BufferedSocket(sock)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001062
1063 logging.info('Connection established, registering...')
1064 handler = {
1065 Ghost.AGENT: registered,
Wei-Ning Huangb8461202015-09-01 20:07:41 +08001066 Ghost.TERMINAL: self.SpawnTTYServer,
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001067 Ghost.SHELL: self.SpawnShellServer,
1068 Ghost.FILE: self.InitiateFileOperation,
Wei-Ning Huangdadbeb62015-09-20 00:38:27 +08001069 Ghost.FORWARD: self.SpawnPortForwardServer,
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001070 }[self._mode]
1071
1072 # Machine ID may change if MAC address is used (USB-ethernet dongle
1073 # plugged/unplugged)
1074 self._machine_id = self.GetMachineID()
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001075 self.SendRequest('register',
1076 {'mode': self._mode, 'mid': self._machine_id,
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001077 'sid': self._session_id,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001078 'properties': self._properties}, handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001079 except socket.error:
1080 pass
1081 else:
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001082 sock.settimeout(None)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001083 self.Listen()
1084
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001085 raise RuntimeError('Cannot connect to any server')
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001086
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001087 def Reconnect(self):
1088 logging.info('Received reconnect request from RPC server, reconnecting...')
1089 self._reset.set()
1090
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001091 def GetStatus(self):
Peter Shih5cafebb2017-06-30 16:36:22 +08001092 status = self._register_status
1093 if self._register_status == SUCCESS:
1094 ip, port = self._sock.sock.getpeername()
1095 status += ' %s:%d' % (ip, port)
1096 return status
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001097
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001098 def AddToDownloadQueue(self, ttyname, filename):
1099 self._download_queue.put((ttyname, filename))
1100
Wei-Ning Huangd521f282015-08-07 05:28:04 +08001101 def RegisterTTY(self, session_id, ttyname):
1102 self._ttyname_to_sid[ttyname] = session_id
Wei-Ning Huange2981862015-08-03 15:03:08 +08001103
1104 def RegisterSession(self, session_id, process_id):
1105 self._terminal_sid_to_pid[session_id] = process_id
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001106
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001107 def StartLanDiscovery(self):
1108 """Start to listen to LAN discovery packet at
1109 _OVERLORD_LAN_DISCOVERY_PORT."""
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001110
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001111 def thread_func():
1112 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1113 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1114 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001115 try:
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001116 s.bind(('0.0.0.0', _OVERLORD_LAN_DISCOVERY_PORT))
1117 except socket.error as e:
Moja Hsuc9ecc8b2015-07-13 11:39:17 +08001118 logging.error('LAN discovery: %s, abort', e)
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001119 return
1120
1121 logging.info('LAN Discovery: started')
1122 while True:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001123 rd, unused_wd, unused_xd = select.select([s], [], [], 1)
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001124
1125 if s in rd:
1126 data, source_addr = s.recvfrom(_BUFSIZE)
1127 parts = data.split()
1128 if parts[0] == 'OVERLORD':
1129 ip, port = parts[1].split(':')
1130 if not ip:
1131 ip = source_addr[0]
1132 self._queue.put((ip, int(port)), True)
1133
1134 try:
1135 obj = self._queue.get(False)
1136 except Queue.Empty:
1137 pass
1138 else:
Peter Shihaacbc2f2017-06-16 14:39:29 +08001139 if not isinstance(obj, str):
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001140 self._queue.put(obj)
1141 elif obj == 'pause':
1142 logging.info('LAN Discovery: paused')
1143 while obj != 'resume':
1144 obj = self._queue.get(True)
1145 logging.info('LAN Discovery: resumed')
1146
1147 t = threading.Thread(target=thread_func)
1148 t.daemon = True
1149 t.start()
1150
1151 def StartRPCServer(self):
Joel Kitching22b89042015-08-06 18:23:29 +08001152 logging.info('RPC Server: started')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001153 rpc_server = SimpleJSONRPCServer((_DEFAULT_BIND_ADDRESS, _GHOST_RPC_PORT),
1154 logRequests=False)
1155 rpc_server.register_function(self.Reconnect, 'Reconnect')
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001156 rpc_server.register_function(self.GetStatus, 'GetStatus')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001157 rpc_server.register_function(self.RegisterTTY, 'RegisterTTY')
Wei-Ning Huange2981862015-08-03 15:03:08 +08001158 rpc_server.register_function(self.RegisterSession, 'RegisterSession')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001159 rpc_server.register_function(self.AddToDownloadQueue, 'AddToDownloadQueue')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001160 t = threading.Thread(target=rpc_server.serve_forever)
1161 t.daemon = True
1162 t.start()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001163
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001164 def ScanServer(self):
Hung-Te Lin41ff8f32017-08-30 08:10:39 +08001165 for meth in [self.GetGateWayIP, self.GetFactoryServerIP]:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001166 for addr in [(x, _OVERLORD_PORT) for x in meth()]:
1167 if addr not in self._overlord_addrs:
1168 self._overlord_addrs.append(addr)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001169
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001170 def Start(self, lan_disc=False, rpc_server=False):
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001171 logging.info('%s started', self.MODE_NAME[self._mode])
1172 logging.info('MID: %s', self._machine_id)
Wei-Ning Huangfed95862015-08-07 03:17:11 +08001173 logging.info('SID: %s', self._session_id)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001174
Wei-Ning Huangb05cde32015-08-01 09:48:41 +08001175 # We don't care about child process's return code, not wait is needed. This
1176 # is used to prevent zombie process from lingering in the system.
1177 self.SetIgnoreChild(True)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001178
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001179 if lan_disc:
1180 self.StartLanDiscovery()
1181
1182 if rpc_server:
1183 self.StartRPCServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001184
1185 try:
1186 while True:
1187 try:
1188 addr = self._queue.get(False)
1189 except Queue.Empty:
1190 pass
1191 else:
Peter Shihaacbc2f2017-06-16 14:39:29 +08001192 if isinstance(addr, tuple) and addr not in self._overlord_addrs:
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001193 logging.info('LAN Discovery: got overlord address %s:%d', *addr)
1194 self._overlord_addrs.append(addr)
1195
1196 try:
Wei-Ning Huang829e0c82015-05-26 14:37:23 +08001197 self.ScanServer()
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001198 self.Register()
Joel Kitching22b89042015-08-06 18:23:29 +08001199 # Don't show stack trace for RuntimeError, which we use in this file for
1200 # plausible and expected errors (such as can't connect to server).
1201 except RuntimeError as e:
Wei-Ning Huang7ec55342015-09-17 08:46:06 +08001202 logging.info('%s, retrying in %ds', e.message, _RETRY_INTERVAL)
Joel Kitching22b89042015-08-06 18:23:29 +08001203 time.sleep(_RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001204 except Exception as e:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001205 unused_x, unused_y, exc_traceback = sys.exc_info()
Joel Kitching22b89042015-08-06 18:23:29 +08001206 traceback.print_tb(exc_traceback)
1207 logging.info('%s: %s, retrying in %ds',
1208 e.__class__.__name__, e.message, _RETRY_INTERVAL)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001209 time.sleep(_RETRY_INTERVAL)
1210
1211 self.Reset()
1212 except KeyboardInterrupt:
1213 logging.error('Received keyboard interrupt, quit')
1214 sys.exit(0)
1215
1216
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001217def GhostRPCServer():
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001218 """Returns handler to Ghost's JSON RPC server."""
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001219 return jsonrpclib.Server('http://localhost:%d' % _GHOST_RPC_PORT)
1220
1221
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001222def ForkToBackground():
1223 """Fork process to run in background."""
1224 pid = os.fork()
1225 if pid != 0:
1226 logging.info('Ghost(%d) running in background.', pid)
1227 sys.exit(0)
1228
1229
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001230def DownloadFile(filename):
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001231 """Initiate a client-initiated file download."""
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001232 filepath = os.path.abspath(filename)
1233 if not os.path.exists(filepath):
Joel Kitching22b89042015-08-06 18:23:29 +08001234 logging.error('file `%s\' does not exist', filename)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001235 sys.exit(1)
1236
1237 # Check if we actually have permission to read the file
1238 if not os.access(filepath, os.R_OK):
Joel Kitching22b89042015-08-06 18:23:29 +08001239 logging.error('can not open %s for reading', filepath)
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001240 sys.exit(1)
1241
1242 server = GhostRPCServer()
1243 server.AddToDownloadQueue(os.ttyname(0), filepath)
1244 sys.exit(0)
1245
1246
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001247def main():
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001248 # Setup logging format
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001249 logger = logging.getLogger()
1250 logger.setLevel(logging.INFO)
Wei-Ning Huang5f3fa8f2015-10-24 15:08:48 +08001251 handler = logging.StreamHandler()
1252 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1253 handler.setFormatter(formatter)
1254 logger.addHandler(handler)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001255
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001256 parser = argparse.ArgumentParser()
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001257 parser.add_argument('--fork', dest='fork', action='store_true', default=False,
1258 help='fork procecess to run in background')
Wei-Ning Huangc9c97f02015-05-19 15:05:42 +08001259 parser.add_argument('--mid', metavar='MID', dest='mid', action='store',
1260 default=None, help='use MID as machine ID')
1261 parser.add_argument('--rand-mid', dest='mid', action='store_const',
1262 const=Ghost.RANDOM_MID, help='use random machine ID')
Wei-Ning Huang2132de32015-04-13 17:24:38 +08001263 parser.add_argument('--no-lan-disc', dest='lan_disc', action='store_false',
1264 default=True, help='disable LAN discovery')
1265 parser.add_argument('--no-rpc-server', dest='rpc_server',
1266 action='store_false', default=True,
1267 help='disable RPC server')
Peter Shih220a96d2016-12-22 17:02:16 +08001268 parser.add_argument('--tls', dest='tls_mode', default='detect',
1269 choices=('y', 'n', 'detect'),
1270 help="specify 'y' or 'n' to force enable/disable TLS")
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001271 parser.add_argument('--tls-cert-file', metavar='TLS_CERT_FILE',
1272 dest='tls_cert_file', type=str, default=None,
1273 help='file containing the server TLS certificate in PEM '
1274 'format')
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001275 parser.add_argument('--tls-no-verify', dest='tls_no_verify',
1276 action='store_true', default=False,
1277 help='do not verify certificate if TLS is enabled')
Joel Kitching22b89042015-08-06 18:23:29 +08001278 parser.add_argument('--prop-file', metavar='PROP_FILE', dest='prop_file',
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001279 type=str, default=None,
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001280 help='file containing the JSON representation of client '
1281 'properties')
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001282 parser.add_argument('--download', metavar='FILE', dest='download', type=str,
1283 default=None, help='file to download')
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001284 parser.add_argument('--reset', dest='reset', default=False,
1285 action='store_true',
1286 help='reset ghost and reload all configs')
Peter Shih5cafebb2017-06-30 16:36:22 +08001287 parser.add_argument('--status', dest='status', default=False,
1288 action='store_true',
1289 help='show status of the client')
Wei-Ning Huang7d029b12015-03-06 10:32:15 +08001290 parser.add_argument('overlord_ip', metavar='OVERLORD_IP', type=str,
1291 nargs='*', help='overlord server address')
1292 args = parser.parse_args()
1293
Peter Shih5cafebb2017-06-30 16:36:22 +08001294 if args.status:
1295 print(GhostRPCServer().GetStatus())
1296 sys.exit()
1297
Wei-Ning Huang8037c182015-09-19 04:41:50 +08001298 if args.fork:
1299 ForkToBackground()
1300
Wei-Ning Huang23ed0162015-09-18 14:42:03 +08001301 if args.reset:
1302 GhostRPCServer().Reconnect()
1303 sys.exit()
1304
Wei-Ning Huanga301f572015-06-03 17:34:21 +08001305 if args.download:
1306 DownloadFile(args.download)
1307
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001308 addrs = [('localhost', _OVERLORD_PORT)]
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001309 addrs = [(x, _OVERLORD_PORT) for x in args.overlord_ip] + addrs
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001310
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001311 prop_file = os.path.abspath(args.prop_file) if args.prop_file else None
1312
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001313 tls_settings = TLSSettings(args.tls_cert_file, not args.tls_no_verify)
Peter Shih220a96d2016-12-22 17:02:16 +08001314 tls_mode = args.tls_mode
1315 tls_mode = {'y': True, 'n': False, 'detect': None}[tls_mode]
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001316 g = Ghost(addrs, tls_settings, Ghost.AGENT, args.mid,
Peter Shih220a96d2016-12-22 17:02:16 +08001317 prop_file=prop_file, tls_mode=tls_mode)
Wei-Ning Huang11c35022015-10-21 16:52:32 +08001318 g.Start(args.lan_disc, args.rpc_server)
Wei-Ning Huang1cea6112015-03-02 12:45:34 +08001319
1320
1321if __name__ == '__main__':
Wei-Ning Huangf5311a02016-02-04 15:23:46 +08001322 try:
1323 main()
1324 except Exception as e:
1325 logging.error(e)