blob: 2e2643dd3eea4b13f25578c6203b372bc26029ef [file] [log] [blame]
Yilin Yang19da6932019-12-10 13:39:28 +08001#!/usr/bin/env python3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +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
Yilin Yang14d02a22019-11-01 11:32:03 +08006from __future__ import division
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08007from __future__ import print_function
8
9import argparse
10import ast
11import base64
12import fcntl
Peter Shih13e78c52018-01-23 12:57:07 +080013import functools
Wei-Ning Huangba768ab2016-02-07 14:38:06 +080014import getpass
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080015import hashlib
Yilin Yang752db712019-09-27 15:42:38 +080016import http.client
Yilin Yang8d4f9d02019-11-28 17:12:11 +080017from io import StringIO
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080018import json
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080019import logging
20import os
21import re
22import select
23import signal
24import socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080025import ssl
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080026import struct
27import subprocess
28import sys
29import tempfile
30import termios
31import threading
32import time
33import tty
Peter Shih99b73ec2017-06-16 17:54:15 +080034import unicodedata # required by pyinstaller, pylint: disable=unused-import
Yilin Yangf54fb912020-01-08 11:42:38 +080035import urllib.error
36import urllib.parse
37import urllib.request
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080038
Peter Shih99b73ec2017-06-16 17:54:15 +080039import jsonrpclib
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080040from jsonrpclib.config import Config
Peter Shih99b73ec2017-06-16 17:54:15 +080041from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080042from ws4py.client import WebSocketBaseClient
Peter Shih99b73ec2017-06-16 17:54:15 +080043import yaml
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080044
Yilin Yang83c8f442020-05-05 13:46:51 +080045from cros.factory.utils import process_utils
46
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080047
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080048_CERT_DIR = os.path.expanduser('~/.config/ovl')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080049
50_ESCAPE = '~'
51_BUFSIZ = 8192
52_OVERLORD_PORT = 4455
53_OVERLORD_HTTP_PORT = 9000
54_OVERLORD_CLIENT_DAEMON_PORT = 4488
55_OVERLORD_CLIENT_DAEMON_RPC_ADDR = ('127.0.0.1', _OVERLORD_CLIENT_DAEMON_PORT)
56
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080057_CONNECT_TIMEOUT = 3
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080058_DEFAULT_HTTP_TIMEOUT = 30
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080059_LIST_CACHE_TIMEOUT = 2
60_DEFAULT_TERMINAL_WIDTH = 80
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080061_RETRY_TIMES = 3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080062
63# echo -n overlord | md5sum
64_HTTP_BOUNDARY_MAGIC = '9246f080c855a69012707ab53489b921'
65
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080066# Terminal resize control
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080067_CONTROL_START = 128
68_CONTROL_END = 129
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080069
70# Stream control
71_STDIN_CLOSED = '##STDIN_CLOSED##'
72
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080073_SSH_CONTROL_SOCKET_PREFIX = os.path.join(tempfile.gettempdir(),
74 'ovl-ssh-control-')
75
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080076_TLS_CERT_FAILED_WARNING = """
77@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
78@ WARNING: REMOTE HOST VERIFICATION HAS FAILED! @
79@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
80IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
81Someone could be eavesdropping on you right now (man-in-the-middle attack)!
82It is also possible that the server is using a self-signed certificate.
83The fingerprint for the TLS host certificate sent by the remote host is
84
85%s
86
87Do you want to trust this certificate and proceed? [Y/n] """
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080088
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080089_TLS_CERT_CHANGED_WARNING = """
90@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
91@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
92@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
93IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
94Someone could be eavesdropping on you right now (man-in-the-middle attack)!
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080095It is also possible that the TLS host certificate has just been changed.
96The fingerprint for the TLS host certificate sent by the remote host is
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080097
98%s
99
100Remove '%s' if you still want to proceed.
101SSL Certificate verification failed."""
102
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800103
104def GetVersionDigest():
105 """Return the sha1sum of the current executing script."""
Wei-Ning Huang31609662016-08-11 00:22:25 +0800106 # Check python script by default
107 filename = __file__
108
109 # If we are running from a frozen binary, we should calculate the checksum
110 # against that binary instead of the python script.
111 # See: https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
112 if getattr(sys, 'frozen', False):
113 filename = sys.executable
114
Yilin Yang0412c272019-12-05 16:57:40 +0800115 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800116 return hashlib.sha1(f.read()).hexdigest()
117
118
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800119def GetTLSCertPath(host):
120 return os.path.join(_CERT_DIR, '%s.cert' % host)
121
122
123def UrlOpen(state, url):
Yilin Yangf54fb912020-01-08 11:42:38 +0800124 """Wrapper for urllib.request.urlopen.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800125
126 It selects correct HTTP scheme according to self._state.ssl, add HTTP
127 basic auth headers, and add specify correct SSL context.
128 """
129 url = MakeRequestUrl(state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +0800130 request = urllib.request.Request(url)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800131 if state.username is not None and state.password is not None:
132 request.add_header(*BasicAuthHeader(state.username, state.password))
Yilin Yangf54fb912020-01-08 11:42:38 +0800133 return urllib.request.urlopen(request, timeout=_DEFAULT_HTTP_TIMEOUT,
134 context=state.ssl_context)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800135
136
137def GetTLSCertificateSHA1Fingerprint(cert_pem):
138 beg = cert_pem.index('\n')
139 end = cert_pem.rindex('\n', 0, len(cert_pem) - 2)
140 cert_pem = cert_pem[beg:end] # Remove BEGIN/END CERTIFICATE boundary
141 cert_der = base64.b64decode(cert_pem)
142 return hashlib.sha1(cert_der).hexdigest()
143
144
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800145def KillGraceful(pid, wait_secs=1):
146 """Kill a process gracefully by first sending SIGTERM, wait for some time,
147 then send SIGKILL to make sure it's killed."""
148 try:
149 os.kill(pid, signal.SIGTERM)
150 time.sleep(wait_secs)
151 os.kill(pid, signal.SIGKILL)
152 except OSError:
153 pass
154
155
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800156def AutoRetry(action_name, retries):
157 """Decorator for retry function call."""
158 def Wrap(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800159 @functools.wraps(func)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800160 def Loop(*args, **kwargs):
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800161 for unused_i in range(retries):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800162 try:
163 func(*args, **kwargs)
164 except Exception as e:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800165 print('error: %s: %s: retrying ...' % (args[0], e))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800166 else:
167 break
168 else:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800169 print('error: failed to %s %s' % (action_name, args[0]))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800170 return Loop
171 return Wrap
172
173
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800174def BasicAuthHeader(user, password):
175 """Return HTTP basic auth header."""
176 credential = base64.b64encode('%s:%s' % (user, password))
177 return ('Authorization', 'Basic %s' % credential)
178
179
180def GetTerminalSize():
181 """Retrieve terminal window size."""
182 ws = struct.pack('HHHH', 0, 0, 0, 0)
183 ws = fcntl.ioctl(0, termios.TIOCGWINSZ, ws)
184 lines, columns, unused_x, unused_y = struct.unpack('HHHH', ws)
185 return lines, columns
186
187
188def MakeRequestUrl(state, url):
189 return 'http%s://%s' % ('s' if state.ssl else '', url)
190
191
Fei Shaobd07c9a2020-06-15 19:04:50 +0800192class ProgressBar:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800193 SIZE_WIDTH = 11
194 SPEED_WIDTH = 10
195 DURATION_WIDTH = 6
196 PERCENTAGE_WIDTH = 8
197
198 def __init__(self, name):
199 self._start_time = time.time()
200 self._name = name
201 self._size = 0
202 self._width = 0
203 self._name_width = 0
204 self._name_max = 0
205 self._stat_width = 0
206 self._max = 0
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800207 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800208 self.SetProgress(0)
209
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800210 def _CalculateSize(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800211 self._width = GetTerminalSize()[1] or _DEFAULT_TERMINAL_WIDTH
212 self._name_width = int(self._width * 0.3)
213 self._name_max = self._name_width
214 self._stat_width = self.SIZE_WIDTH + self.SPEED_WIDTH + self.DURATION_WIDTH
215 self._max = (self._width - self._name_width - self._stat_width -
216 self.PERCENTAGE_WIDTH)
217
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800218 def _SizeToHuman(self, size_in_bytes):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800219 if size_in_bytes < 1024:
220 unit = 'B'
221 value = size_in_bytes
222 elif size_in_bytes < 1024 ** 2:
223 unit = 'KiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800224 value = size_in_bytes / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800225 elif size_in_bytes < 1024 ** 3:
226 unit = 'MiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800227 value = size_in_bytes / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800228 elif size_in_bytes < 1024 ** 4:
229 unit = 'GiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800230 value = size_in_bytes / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800231 return ' %6.1f %3s' % (value, unit)
232
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800233 def _SpeedToHuman(self, speed_in_bs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800234 if speed_in_bs < 1024:
235 unit = 'B'
236 value = speed_in_bs
237 elif speed_in_bs < 1024 ** 2:
238 unit = 'K'
Yilin Yang14d02a22019-11-01 11:32:03 +0800239 value = speed_in_bs / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800240 elif speed_in_bs < 1024 ** 3:
241 unit = 'M'
Yilin Yang14d02a22019-11-01 11:32:03 +0800242 value = speed_in_bs / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800243 elif speed_in_bs < 1024 ** 4:
244 unit = 'G'
Yilin Yang14d02a22019-11-01 11:32:03 +0800245 value = speed_in_bs / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800246 return ' %6.1f%s/s' % (value, unit)
247
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800248 def _DurationToClock(self, duration):
Yilin Yang14d02a22019-11-01 11:32:03 +0800249 return ' %02d:%02d' % (duration // 60, duration % 60)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800250
251 def SetProgress(self, percentage, size=None):
252 current_width = GetTerminalSize()[1]
253 if self._width != current_width:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800254 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800255
256 if size is not None:
257 self._size = size
258
259 elapse_time = time.time() - self._start_time
Yilin Yang14d02a22019-11-01 11:32:03 +0800260 speed = self._size / elapse_time
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800261
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800262 size_str = self._SizeToHuman(self._size)
263 speed_str = self._SpeedToHuman(speed)
264 elapse_str = self._DurationToClock(elapse_time)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800265
266 width = int(self._max * percentage / 100.0)
267 sys.stdout.write(
268 '%*s' % (- self._name_max,
269 self._name if len(self._name) <= self._name_max else
270 self._name[:self._name_max - 4] + ' ...') +
271 size_str + speed_str + elapse_str +
272 ((' [' + '#' * width + ' ' * (self._max - width) + ']' +
273 '%4d%%' % int(percentage)) if self._max > 2 else '') + '\r')
274 sys.stdout.flush()
275
276 def End(self):
277 self.SetProgress(100.0)
278 sys.stdout.write('\n')
279 sys.stdout.flush()
280
281
Fei Shaobd07c9a2020-06-15 19:04:50 +0800282class DaemonState:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800283 """DaemonState is used for storing Overlord state info."""
284 def __init__(self):
285 self.version_sha1sum = GetVersionDigest()
286 self.host = None
287 self.port = None
288 self.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800289 self.ssl_self_signed = False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800290 self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800291 self.ssh = False
292 self.orig_host = None
293 self.ssh_pid = None
294 self.username = None
295 self.password = None
296 self.selected_mid = None
297 self.forwards = {}
298 self.listing = []
299 self.last_list = 0
300
301
Fei Shaobd07c9a2020-06-15 19:04:50 +0800302class OverlordClientDaemon:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800303 """Overlord Client Daemon."""
304 def __init__(self):
305 self._state = DaemonState()
306 self._server = None
307
308 def Start(self):
309 self.StartRPCServer()
310
311 def StartRPCServer(self):
312 self._server = SimpleJSONRPCServer(_OVERLORD_CLIENT_DAEMON_RPC_ADDR,
313 logRequests=False)
314 exports = [
315 (self.State, 'State'),
316 (self.Ping, 'Ping'),
317 (self.GetPid, 'GetPid'),
318 (self.Connect, 'Connect'),
319 (self.Clients, 'Clients'),
320 (self.SelectClient, 'SelectClient'),
321 (self.AddForward, 'AddForward'),
322 (self.RemoveForward, 'RemoveForward'),
323 (self.RemoveAllForward, 'RemoveAllForward'),
324 ]
325 for func, name in exports:
326 self._server.register_function(func, name)
327
328 pid = os.fork()
329 if pid == 0:
Peter Shih4d55ded2017-07-03 17:19:01 +0800330 for fd in range(3):
331 os.close(fd)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800332 self._server.serve_forever()
333
334 @staticmethod
335 def GetRPCServer():
336 """Returns the Overlord client daemon RPC server."""
337 server = jsonrpclib.Server('http://%s:%d' %
338 _OVERLORD_CLIENT_DAEMON_RPC_ADDR)
339 try:
340 server.Ping()
341 except Exception:
342 return None
343 return server
344
345 def State(self):
346 return self._state
347
348 def Ping(self):
349 return True
350
351 def GetPid(self):
352 return os.getpid()
353
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800354 def _GetJSON(self, path):
355 url = '%s:%d%s' % (self._state.host, self._state.port, path)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800356 return json.loads(UrlOpen(self._state, url).read())
357
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800358 def _TLSEnabled(self):
359 """Determine if TLS is enabled on given server address."""
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800360 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
361 try:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800362 # Allow any certificate since we only want to check if server talks TLS.
363 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
364 context.verify_mode = ssl.CERT_NONE
365
366 sock = context.wrap_socket(sock, server_hostname=self._state.host)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800367 sock.settimeout(_CONNECT_TIMEOUT)
368 sock.connect((self._state.host, self._state.port))
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800369 return True
Wei-Ning Huangecc80b82016-07-01 16:55:10 +0800370 except ssl.SSLError:
371 return False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800372 except socket.error: # Connect refused or timeout
373 raise
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800374 except Exception:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800375 return False # For whatever reason above failed, assume False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800376
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800377 def _CheckTLSCertificate(self, check_hostname=True):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800378 """Check TLS certificate.
379
380 Returns:
381 A tupple (check_result, if_certificate_is_loaded)
382 """
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800383 def _DoConnect(context):
384 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
385 try:
386 sock.settimeout(_CONNECT_TIMEOUT)
387 sock = context.wrap_socket(sock, server_hostname=self._state.host)
388 sock.connect((self._state.host, self._state.port))
389 except ssl.SSLError:
390 return False
391 finally:
392 sock.close()
393
394 # Save SSLContext for future use.
395 self._state.ssl_context = context
396 return True
397
398 # First try connect with built-in certificates
399 tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
400 if _DoConnect(tls_context):
401 return True
402
403 # Try with already saved certificate, if any.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800404 tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
405 tls_context.verify_mode = ssl.CERT_REQUIRED
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800406 tls_context.check_hostname = check_hostname
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800407
408 tls_cert_path = GetTLSCertPath(self._state.host)
409 if os.path.exists(tls_cert_path):
410 tls_context.load_verify_locations(tls_cert_path)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800411 self._state.ssl_self_signed = True
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800412
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800413 return _DoConnect(tls_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800414
415 def Connect(self, host, port=_OVERLORD_HTTP_PORT, ssh_pid=None,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800416 username=None, password=None, orig_host=None,
417 check_hostname=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800418 self._state.username = username
419 self._state.password = password
420 self._state.host = host
421 self._state.port = port
422 self._state.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800423 self._state.ssl_self_signed = False
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800424 self._state.orig_host = orig_host
425 self._state.ssh_pid = ssh_pid
426 self._state.selected_mid = None
427
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800428 tls_enabled = self._TLSEnabled()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800429 if tls_enabled:
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800430 result = self._CheckTLSCertificate(check_hostname)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800431 if not result:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800432 if self._state.ssl_self_signed:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800433 return ('SSLCertificateChanged', ssl.get_server_certificate(
434 (self._state.host, self._state.port)))
Yilin Yang15a3f8f2020-01-03 17:49:00 +0800435 return ('SSLVerifyFailed', ssl.get_server_certificate(
436 (self._state.host, self._state.port)))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800437
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800438 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800439 self._state.ssl = tls_enabled
440 UrlOpen(self._state, '%s:%d' % (host, port))
Yilin Yangf54fb912020-01-08 11:42:38 +0800441 except urllib.error.HTTPError as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800442 return ('HTTPError', e.getcode(), str(e), e.read().strip())
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800443 except Exception as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800444 return str(e)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800445 else:
446 return True
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800447
448 def Clients(self):
449 if time.time() - self._state.last_list <= _LIST_CACHE_TIMEOUT:
450 return self._state.listing
451
Peter Shihcf0f3b22017-06-19 15:59:22 +0800452 self._state.listing = self._GetJSON('/api/agents/list')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800453 self._state.last_list = time.time()
454 return self._state.listing
455
456 def SelectClient(self, mid):
457 self._state.selected_mid = mid
458
459 def AddForward(self, mid, remote, local, pid):
460 self._state.forwards[local] = (mid, remote, pid)
461
462 def RemoveForward(self, local_port):
463 try:
464 unused_mid, unused_remote, pid = self._state.forwards[local_port]
465 KillGraceful(pid)
466 del self._state.forwards[local_port]
467 except (KeyError, OSError):
468 pass
469
470 def RemoveAllForward(self):
471 for unused_mid, unused_remote, pid in self._state.forwards.values():
472 try:
473 KillGraceful(pid)
474 except OSError:
475 pass
476 self._state.forwards = {}
477
478
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800479class SSLEnabledWebSocketBaseClient(WebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800480 def __init__(self, state, *args, **kwargs):
481 cafile = ssl.get_default_verify_paths().openssl_cafile
482 # For some system / distribution, python can not detect system cafile path.
483 # In such case we fallback to the default path.
484 if not os.path.exists(cafile):
485 cafile = '/etc/ssl/certs/ca-certificates.crt'
486
487 if state.ssl_self_signed:
488 cafile = GetTLSCertPath(state.host)
489
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800490 ssl_options = {
491 'cert_reqs': ssl.CERT_REQUIRED,
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800492 'ca_certs': cafile
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800493 }
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800494 # ws4py does not allow you to specify SSLContext, but rather passing in the
495 # argument of ssl.wrap_socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800496 super(SSLEnabledWebSocketBaseClient, self).__init__(
497 ssl_options=ssl_options, *args, **kwargs)
498
499
500class TerminalWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800501 def __init__(self, state, mid, escape, *args, **kwargs):
502 super(TerminalWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800503 self._mid = mid
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800504 self._escape = escape
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800505 self._stdin_fd = sys.stdin.fileno()
506 self._old_termios = None
507
508 def handshake_ok(self):
509 pass
510
511 def opened(self):
512 nonlocals = {'size': (80, 40)}
513
514 def _ResizeWindow():
515 size = GetTerminalSize()
516 if size != nonlocals['size']: # Size not changed, ignore
517 control = {'command': 'resize', 'params': list(size)}
518 payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END)
519 nonlocals['size'] = size
520 try:
521 self.send(payload, binary=True)
522 except Exception:
523 pass
524
525 def _FeedInput():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800526 self._old_termios = termios.tcgetattr(self._stdin_fd)
527 tty.setraw(self._stdin_fd)
528
529 READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3)
530
531 try:
532 state = READY
533 while True:
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800534 # Check if terminal is resized
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800535 _ResizeWindow()
536
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800537 ch = sys.stdin.read(1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800538
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800539 # Scan for escape sequence
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800540 if self._escape:
541 if state == READY:
542 state = ENTER_PRESSED if ch == chr(0x0d) else READY
543 elif state == ENTER_PRESSED:
544 state = ESCAPE_PRESSED if ch == self._escape else READY
545 elif state == ESCAPE_PRESSED:
546 if ch == '.':
547 self.close()
548 break
549 else:
550 state = READY
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800551
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800552 self.send(ch)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800553 except (KeyboardInterrupt, RuntimeError):
554 pass
555
556 t = threading.Thread(target=_FeedInput)
557 t.daemon = True
558 t.start()
559
560 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800561 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800562 termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios)
563 print('Connection to %s closed.' % self._mid)
564
Yilin Yangf64670b2020-01-06 11:22:18 +0800565 def received_message(self, message):
566 if message.is_binary:
567 sys.stdout.write(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800568 sys.stdout.flush()
569
570
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800571class ShellWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800572 def __init__(self, state, output, *args, **kwargs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800573 """Constructor.
574
575 Args:
576 output: output file object.
577 """
578 self.output = output
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800579 super(ShellWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800580
581 def handshake_ok(self):
582 pass
583
584 def opened(self):
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800585 def _FeedInput():
586 try:
587 while True:
588 data = sys.stdin.read(1)
589
Peter Shihf84a8972017-06-19 15:18:24 +0800590 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800591 self.send(_STDIN_CLOSED * 2)
592 break
593 self.send(data, binary=True)
594 except (KeyboardInterrupt, RuntimeError):
595 pass
596
597 t = threading.Thread(target=_FeedInput)
598 t.daemon = True
599 t.start()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800600
601 def closed(self, code, reason=None):
602 pass
603
Yilin Yangf64670b2020-01-06 11:22:18 +0800604 def received_message(self, message):
605 if message.is_binary:
606 self.output.write(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800607 self.output.flush()
608
609
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800610class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800611 def __init__(self, state, sock, *args, **kwargs):
612 super(ForwarderWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800613 self._sock = sock
614 self._stop = threading.Event()
615
616 def handshake_ok(self):
617 pass
618
619 def opened(self):
620 def _FeedInput():
621 try:
622 self._sock.setblocking(False)
623 while True:
624 rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5)
625 if self._stop.is_set():
626 break
627 if self._sock in rd:
628 data = self._sock.recv(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +0800629 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800630 self.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800631 break
632 self.send(data, binary=True)
633 except Exception:
634 pass
635 finally:
636 self._sock.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800637
638 t = threading.Thread(target=_FeedInput)
639 t.daemon = True
640 t.start()
641
642 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800643 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800644 self._stop.set()
645 sys.exit(0)
646
Yilin Yangf64670b2020-01-06 11:22:18 +0800647 def received_message(self, message):
648 if message.is_binary:
649 self._sock.send(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800650
651
652def Arg(*args, **kwargs):
653 return (args, kwargs)
654
655
656def Command(command, help_msg=None, args=None):
657 """Decorator for adding argparse parameter for a method."""
658 if args is None:
659 args = []
660 def WrapFunc(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800661 @functools.wraps(func)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800662 def Wrapped(*args, **kwargs):
663 return func(*args, **kwargs)
Peter Shih99b73ec2017-06-16 17:54:15 +0800664 # pylint: disable=protected-access
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800665 Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args}
666 return Wrapped
667 return WrapFunc
668
669
670def ParseMethodSubCommands(cls):
671 """Decorator for a class using the @Command decorator.
672
673 This decorator retrieve command info from each method and append it in to the
674 SUBCOMMANDS class variable, which is later used to construct parser.
675 """
Yilin Yang879fbda2020-05-14 13:52:30 +0800676 for unused_key, method in cls.__dict__.items():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800677 if hasattr(method, '__arg_attr'):
Peter Shih99b73ec2017-06-16 17:54:15 +0800678 # pylint: disable=protected-access
679 cls.SUBCOMMANDS.append(method.__arg_attr)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800680 return cls
681
682
683@ParseMethodSubCommands
Fei Shaobd07c9a2020-06-15 19:04:50 +0800684class OverlordCLIClient:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800685 """Overlord command line interface client."""
686
687 SUBCOMMANDS = []
688
689 def __init__(self):
690 self._parser = self._BuildParser()
691 self._selected_mid = None
692 self._server = None
693 self._state = None
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800694 self._escape = None
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800695
696 def _BuildParser(self):
697 root_parser = argparse.ArgumentParser(prog='ovl')
698 subparsers = root_parser.add_subparsers(help='sub-command')
699
700 root_parser.add_argument('-s', dest='selected_mid', action='store',
701 default=None,
702 help='select target to execute command on')
703 root_parser.add_argument('-S', dest='select_mid_before_action',
704 action='store_true', default=False,
705 help='select target before executing command')
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800706 root_parser.add_argument('-e', dest='escape', metavar='ESCAPE_CHAR',
707 action='store', default=_ESCAPE, type=str,
708 help='set shell escape character, \'none\' to '
709 'disable escape completely')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800710
711 for attr in self.SUBCOMMANDS:
712 parser = subparsers.add_parser(attr['command'], help=attr['help'])
713 parser.set_defaults(which=attr['command'])
714 for arg in attr['args']:
715 parser.add_argument(*arg[0], **arg[1])
716
717 return root_parser
718
719 def Main(self):
720 # We want to pass the rest of arguments after shell command directly to the
721 # function without parsing it.
722 try:
723 index = sys.argv.index('shell')
724 except ValueError:
725 args = self._parser.parse_args()
726 else:
727 args = self._parser.parse_args(sys.argv[1:index + 1])
728
729 command = args.which
730 self._selected_mid = args.selected_mid
731
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800732 if args.escape and args.escape != 'none':
733 self._escape = args.escape[0]
734
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800735 if command == 'start-server':
736 self.StartServer()
737 return
738 elif command == 'kill-server':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800739 self.KillServer()
740 return
741
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800742 self.CheckDaemon()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800743 if command == 'status':
744 self.Status()
745 return
746 elif command == 'connect':
747 self.Connect(args)
748 return
749
750 # The following command requires connection to the server
751 self.CheckConnection()
752
753 if args.select_mid_before_action:
754 self.SelectClient(store=False)
755
756 if command == 'select':
757 self.SelectClient(args)
758 elif command == 'ls':
Peter Shih99b73ec2017-06-16 17:54:15 +0800759 self.ListClients(args)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800760 elif command == 'shell':
761 command = sys.argv[sys.argv.index('shell') + 1:]
762 self.Shell(command)
763 elif command == 'push':
764 self.Push(args)
765 elif command == 'pull':
766 self.Pull(args)
767 elif command == 'forward':
768 self.Forward(args)
769
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800770 def _SaveTLSCertificate(self, host, cert_pem):
771 try:
772 os.makedirs(_CERT_DIR)
773 except Exception:
774 pass
775 with open(GetTLSCertPath(host), 'w') as f:
776 f.write(cert_pem)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800777
778 def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None):
779 """Perform HTTP POST and upload file to Overlord.
780
781 To minimize the external dependencies, we construct the HTTP post request
782 by ourselves.
783 """
784 url = MakeRequestUrl(self._state, url)
785 size = os.stat(filename).st_size
786 boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC
787 CRLF = '\r\n'
Yilin Yangf54fb912020-01-08 11:42:38 +0800788 parse = urllib.parse.urlparse(url)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800789
790 part_headers = [
791 '--' + boundary,
792 'Content-Disposition: form-data; name="file"; '
793 'filename="%s"' % os.path.basename(filename),
794 'Content-Type: application/octet-stream',
795 '', ''
796 ]
797 part_header = CRLF.join(part_headers)
798 end_part = CRLF + '--' + boundary + '--' + CRLF
799
800 content_length = len(part_header) + size + len(end_part)
801 if parse.scheme == 'http':
Yilin Yang752db712019-09-27 15:42:38 +0800802 h = http.client.HTTP(parse.netloc)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800803 else:
Yilin Yang752db712019-09-27 15:42:38 +0800804 h = http.client.HTTPS(parse.netloc, context=self._state.ssl_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800805
806 post_path = url[url.index(parse.netloc) + len(parse.netloc):]
807 h.putrequest('POST', post_path)
808 h.putheader('Content-Length', content_length)
809 h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
810
811 if user and passwd:
812 h.putheader(*BasicAuthHeader(user, passwd))
813 h.endheaders()
814 h.send(part_header)
815
816 count = 0
817 with open(filename, 'r') as f:
818 while True:
819 data = f.read(_BUFSIZ)
820 if not data:
821 break
822 count += len(data)
823 if progress:
Yilin Yang14d02a22019-11-01 11:32:03 +0800824 progress(count * 100 // size, count)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800825 h.send(data)
826
827 h.send(end_part)
828 progress(100)
829
830 if count != size:
831 logging.warning('file changed during upload, upload may be truncated.')
832
833 errcode, unused_x, unused_y = h.getreply()
834 return errcode == 200
835
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800836 def CheckDaemon(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800837 self._server = OverlordClientDaemon.GetRPCServer()
838 if self._server is None:
839 print('* daemon not running, starting it now on port %d ... *' %
840 _OVERLORD_CLIENT_DAEMON_PORT)
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800841 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800842
843 self._state = self._server.State()
844 sha1sum = GetVersionDigest()
845
846 if sha1sum != self._state.version_sha1sum:
847 print('ovl server is out of date. killing...')
848 KillGraceful(self._server.GetPid())
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800849 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800850
851 def GetSSHControlFile(self, host):
852 return _SSH_CONTROL_SOCKET_PREFIX + host
853
854 def SSHTunnel(self, user, host, port):
855 """SSH forward the remote overlord server.
856
857 Overlord server may not have port 9000 open to the public network, in such
858 case we can SSH forward the port to localhost.
859 """
860
861 control_file = self.GetSSHControlFile(host)
862 try:
863 os.unlink(control_file)
864 except Exception:
865 pass
866
867 subprocess.Popen([
868 'ssh', '-Nf',
869 '-M', # Enable master mode
870 '-S', control_file,
871 '-L', '9000:localhost:9000',
872 '-p', str(port),
873 '%s%s' % (user + '@' if user else '', host)
874 ]).wait()
875
Yilin Yang83c8f442020-05-05 13:46:51 +0800876 p = process_utils.Spawn([
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800877 'ssh',
878 '-S', control_file,
879 '-O', 'check', host,
Yilin Yang83c8f442020-05-05 13:46:51 +0800880 ], read_stderr=True, ignore_stdout=True)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800881
Yilin Yang83c8f442020-05-05 13:46:51 +0800882 s = re.search(r'pid=(\d+)', p.stderr_data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800883 if s:
884 return int(s.group(1))
885
886 raise RuntimeError('can not establish ssh connection')
887
888 def CheckConnection(self):
889 if self._state.host is None:
890 raise RuntimeError('not connected to any server, abort')
891
892 try:
893 self._server.Clients()
894 except Exception:
895 raise RuntimeError('remote server disconnected, abort')
896
897 if self._state.ssh_pid is not None:
898 ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)],
899 stdout=subprocess.PIPE,
900 stderr=subprocess.PIPE).wait()
901 if ret != 0:
902 raise RuntimeError('ssh tunnel disconnected, please re-connect')
903
904 def CheckClient(self):
905 if self._selected_mid is None:
906 if self._state.selected_mid is None:
907 raise RuntimeError('No client is selected')
908 self._selected_mid = self._state.selected_mid
909
Peter Shihcf0f3b22017-06-19 15:59:22 +0800910 if not any(client['mid'] == self._selected_mid
911 for client in self._server.Clients()):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800912 raise RuntimeError('client %s disappeared' % self._selected_mid)
913
914 def CheckOutput(self, command):
915 headers = []
916 if self._state.username is not None and self._state.password is not None:
917 headers.append(BasicAuthHeader(self._state.username,
918 self._state.password))
919
920 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Yilin Yang8d4f9d02019-11-28 17:12:11 +0800921 sio = StringIO()
Peter Shihe03450b2017-06-14 14:02:08 +0800922 ws = ShellWebSocketClient(
923 self._state, sio, scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
924 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +0800925 urllib.parse.quote(self._selected_mid),
926 urllib.parse.quote(command)),
Peter Shihe03450b2017-06-14 14:02:08 +0800927 headers=headers)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800928 ws.connect()
929 ws.run()
930 return sio.getvalue()
931
932 @Command('status', 'show Overlord connection status')
933 def Status(self):
934 if self._state.host is None:
935 print('Not connected to any host.')
936 else:
937 if self._state.ssh_pid is not None:
938 print('Connected to %s with SSH tunneling.' % self._state.orig_host)
939 else:
940 print('Connected to %s:%d.' % (self._state.host, self._state.port))
941
942 if self._selected_mid is None:
943 self._selected_mid = self._state.selected_mid
944
945 if self._selected_mid is None:
946 print('No client is selected.')
947 else:
948 print('Client %s selected.' % self._selected_mid)
949
950 @Command('connect', 'connect to Overlord server', [
951 Arg('host', metavar='HOST', type=str, default='localhost',
952 help='Overlord hostname/IP'),
953 Arg('port', metavar='PORT', type=int,
954 default=_OVERLORD_HTTP_PORT, help='Overlord port'),
955 Arg('-f', '--forward', dest='ssh_forward', default=False,
956 action='store_true',
957 help='connect with SSH forwarding to the host'),
958 Arg('-p', '--ssh-port', dest='ssh_port', default=22,
959 type=int, help='SSH server port for SSH forwarding'),
960 Arg('-l', '--ssh-login', dest='ssh_login', default='',
961 type=str, help='SSH server login name for SSH forwarding'),
962 Arg('-u', '--user', dest='user', default=None,
963 type=str, help='Overlord HTTP auth username'),
964 Arg('-w', '--passwd', dest='passwd', default=None, type=str,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800965 help='Overlord HTTP auth password'),
966 Arg('-i', '--no-check-hostname', dest='check_hostname',
967 default=True, action='store_false',
968 help='Ignore SSL cert hostname check')])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800969 def Connect(self, args):
970 ssh_pid = None
971 host = args.host
972 orig_host = args.host
973
974 if args.ssh_forward:
975 # Kill previous SSH tunnel
976 self.KillSSHTunnel()
977
978 ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port)
979 host = 'localhost'
980
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800981 username_provided = args.user is not None
982 password_provided = args.passwd is not None
983 prompt = False
984
Peter Shih533566a2018-09-05 17:48:03 +0800985 for unused_i in range(3): # pylint: disable=too-many-nested-blocks
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800986 try:
987 if prompt:
988 if not username_provided:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800989 args.user = input('Username: ')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800990 if not password_provided:
991 args.passwd = getpass.getpass('Password: ')
992
993 ret = self._server.Connect(host, args.port, ssh_pid, args.user,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800994 args.passwd, orig_host,
995 args.check_hostname)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800996 if isinstance(ret, list):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800997 if ret[0].startswith('SSL'):
998 cert_pem = ret[1]
999 fp = GetTLSCertificateSHA1Fingerprint(cert_pem)
1000 fp_text = ':'.join([fp[i:i+2] for i in range(0, len(fp), 2)])
1001
1002 if ret[0] == 'SSLCertificateChanged':
1003 print(_TLS_CERT_CHANGED_WARNING % (fp_text, GetTLSCertPath(host)))
1004 return
1005 elif ret[0] == 'SSLVerifyFailed':
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001006 print(_TLS_CERT_FAILED_WARNING % (fp_text), end='')
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001007 response = input()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001008 if response.lower() in ['y', 'ye', 'yes']:
1009 self._SaveTLSCertificate(host, cert_pem)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001010 print('TLS host Certificate trusted, you will not be prompted '
1011 'next time.\n')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001012 continue
Fei Shaof91ab8f2020-06-23 17:54:03 +08001013 print('connection aborted.')
1014 return
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001015 elif ret[0] == 'HTTPError':
1016 code, except_str, body = ret[1:]
1017 if code == 401:
1018 print('connect: %s' % body)
1019 prompt = True
1020 if not username_provided or not password_provided:
1021 continue
Fei Shaof91ab8f2020-06-23 17:54:03 +08001022 break
Fei Shao0e4e2c62020-06-23 18:22:26 +08001023 logging.error('%s; %s', except_str, body)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001024
1025 if ret is not True:
1026 print('can not connect to %s: %s' % (host, ret))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001027 else:
1028 print('connection to %s:%d established.' % (host, args.port))
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001029 except Exception as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001030 logging.error(e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001031 else:
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001032 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001033
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001034 @Command('start-server', 'start overlord CLI client server')
1035 def StartServer(self):
1036 self._server = OverlordClientDaemon.GetRPCServer()
1037 if self._server is None:
1038 OverlordClientDaemon().Start()
1039 time.sleep(1)
1040 self._server = OverlordClientDaemon.GetRPCServer()
1041 if self._server is not None:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001042 print('* daemon started successfully *\n')
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001043
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001044 @Command('kill-server', 'kill overlord CLI client server')
1045 def KillServer(self):
1046 self._server = OverlordClientDaemon.GetRPCServer()
1047 if self._server is None:
1048 return
1049
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001050 self._state = self._server.State()
1051
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001052 # Kill SSH Tunnel
1053 self.KillSSHTunnel()
1054
1055 # Kill server daemon
1056 KillGraceful(self._server.GetPid())
1057
1058 def KillSSHTunnel(self):
1059 if self._state.ssh_pid is not None:
1060 KillGraceful(self._state.ssh_pid)
1061
Peter Shihcf0f3b22017-06-19 15:59:22 +08001062 def _FilterClients(self, clients, prop_filters, mid=None):
1063 def _ClientPropertiesMatch(client, key, regex):
1064 try:
1065 return bool(re.search(regex, client['properties'][key]))
1066 except KeyError:
1067 return False
1068
1069 for prop_filter in prop_filters:
1070 key, sep, regex = prop_filter.partition('=')
1071 if not sep:
1072 # The filter doesn't contains =.
1073 raise ValueError('Invalid filter condition %r' % filter)
1074 clients = [c for c in clients if _ClientPropertiesMatch(c, key, regex)]
1075
1076 if mid is not None:
1077 client = next((c for c in clients if c['mid'] == mid), None)
1078 if client:
1079 return [client]
1080 clients = [c for c in clients if c['mid'].startswith(mid)]
1081 return clients
1082
1083 @Command('ls', 'list clients', [
1084 Arg('-f', '--filter', default=[], dest='filters', action='append',
1085 help=('Conditions to filter clients by properties. '
1086 'Should be in form "key=regex", where regex is the regular '
1087 'expression that should be found in the value. '
1088 'Multiple --filter arguments would be ANDed.')),
Peter Shih99b73ec2017-06-16 17:54:15 +08001089 Arg('-v', '--verbose', default=False, action='store_true',
1090 help='Print properties of each client.')
1091 ])
1092 def ListClients(self, args):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001093 clients = self._FilterClients(self._server.Clients(), args.filters)
1094 for client in clients:
1095 if args.verbose:
Peter Shih99b73ec2017-06-16 17:54:15 +08001096 print(yaml.safe_dump(client, default_flow_style=False))
Peter Shihcf0f3b22017-06-19 15:59:22 +08001097 else:
1098 print(client['mid'])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001099
1100 @Command('select', 'select default client', [
Peter Shihcf0f3b22017-06-19 15:59:22 +08001101 Arg('-f', '--filter', default=[], dest='filters', action='append',
1102 help=('Conditions to filter clients by properties. '
1103 'Should be in form "key=regex", where regex is the regular '
1104 'expression that should be found in the value. '
1105 'Multiple --filter arguments would be ANDed.')),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001106 Arg('mid', metavar='mid', nargs='?', default=None)])
1107 def SelectClient(self, args=None, store=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001108 mid = args.mid if args is not None else None
Wei-Ning Huang6796a132017-06-22 14:46:32 +08001109 filters = args.filters if args is not None else []
1110 clients = self._FilterClients(self._server.Clients(), filters, mid=mid)
Peter Shihcf0f3b22017-06-19 15:59:22 +08001111
1112 if not clients:
1113 raise RuntimeError('select: client not found')
Fei Shao0e4e2c62020-06-23 18:22:26 +08001114 if len(clients) == 1:
Peter Shihcf0f3b22017-06-19 15:59:22 +08001115 mid = clients[0]['mid']
1116 else:
1117 # This case would not happen when args.mid is specified.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001118 print('Select from the following clients:')
1119 for i, client in enumerate(clients):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001120 print(' %d. %s' % (i + 1, client['mid']))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001121
1122 print('\nSelection: ', end='')
1123 try:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001124 choice = int(input()) - 1
Peter Shihcf0f3b22017-06-19 15:59:22 +08001125 mid = clients[choice]['mid']
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001126 except ValueError:
1127 raise RuntimeError('select: invalid selection')
1128 except IndexError:
1129 raise RuntimeError('select: selection out of range')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001130
1131 self._selected_mid = mid
1132 if store:
1133 self._server.SelectClient(mid)
1134 print('Client %s selected' % mid)
1135
1136 @Command('shell', 'open a shell or execute a shell command', [
1137 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1138 def Shell(self, command=None):
1139 if command is None:
1140 command = []
1141 self.CheckClient()
1142
1143 headers = []
1144 if self._state.username is not None and self._state.password is not None:
1145 headers.append(BasicAuthHeader(self._state.username,
1146 self._state.password))
1147
1148 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Peter Shihe03450b2017-06-14 14:02:08 +08001149 if command:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001150 cmd = ' '.join(command)
Peter Shihe03450b2017-06-14 14:02:08 +08001151 ws = ShellWebSocketClient(
1152 self._state, sys.stdout,
1153 scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
1154 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001155 urllib.parse.quote(self._selected_mid), urllib.parse.quote(cmd)),
Peter Shihe03450b2017-06-14 14:02:08 +08001156 headers=headers)
1157 else:
1158 ws = TerminalWebSocketClient(
1159 self._state, self._selected_mid, self._escape,
1160 scheme + '%s:%d/api/agent/tty/%s' % (
1161 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001162 urllib.parse.quote(self._selected_mid)),
Peter Shihe03450b2017-06-14 14:02:08 +08001163 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001164 try:
1165 ws.connect()
1166 ws.run()
1167 except socket.error as e:
1168 if e.errno == 32: # Broken pipe
1169 pass
1170 else:
1171 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001172
1173 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001174 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001175 Arg('dst', metavar='DESTINATION')])
1176 def Push(self, args):
1177 self.CheckClient()
1178
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001179 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001180 def _push(src, dst):
1181 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001182
1183 # Local file is a link
1184 if os.path.islink(src):
1185 pbar = ProgressBar(src_base)
1186 link_path = os.readlink(src)
1187 self.CheckOutput('mkdir -p %(dirname)s; '
1188 'if [ -d "%(dst)s" ]; then '
1189 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1190 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1191 dict(dirname=os.path.dirname(dst),
1192 link_path=link_path, dst=dst,
1193 link_name=src_base))
1194 pbar.End()
1195 return
1196
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001197 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1198 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001199 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001200 urllib.parse.quote(self._selected_mid), dst, mode))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001201 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001202 UrlOpen(self._state, url + '&filename=%s' % src_base)
Yilin Yangf54fb912020-01-08 11:42:38 +08001203 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001204 msg = json.loads(e.read()).get('error', None)
1205 raise RuntimeError('push: %s' % msg)
1206
1207 pbar = ProgressBar(src_base)
1208 self._HTTPPostFile(url, src, pbar.SetProgress,
1209 self._state.username, self._state.password)
1210 pbar.End()
1211
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001212 def _push_single_target(src, dst):
1213 if os.path.isdir(src):
1214 dst_exists = ast.literal_eval(self.CheckOutput(
1215 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1216 for root, unused_x, files in os.walk(src):
1217 # If destination directory does not exist, we should strip the first
1218 # layer of directory. For example: src_dir contains a single file 'A'
1219 #
1220 # push src_dir dest_dir
1221 #
1222 # If dest_dir exists, the resulting directory structure should be:
1223 # dest_dir/src_dir/A
1224 # If dest_dir does not exist, the resulting directory structure should
1225 # be:
1226 # dest_dir/A
1227 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1228 for name in files:
1229 _push(os.path.join(root, name),
1230 os.path.join(dst, dst_root, name))
1231 else:
1232 _push(src, dst)
1233
1234 if len(args.srcs) > 1:
1235 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1236 '2>/dev/null' % args.dst).strip()
1237 if not dst_type:
1238 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1239 if dst_type != 'directory':
1240 raise RuntimeError('push: %s: Not a directory' % args.dst)
1241
1242 for src in args.srcs:
1243 if not os.path.exists(src):
1244 raise RuntimeError('push: can not stat "%s": no such file or directory'
1245 % src)
1246 if not os.access(src, os.R_OK):
1247 raise RuntimeError('push: can not open "%s" for reading' % src)
1248
1249 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001250
1251 @Command('pull', 'pull a file or directory from remote', [
1252 Arg('src', metavar='SOURCE'),
1253 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1254 def Pull(self, args):
1255 self.CheckClient()
1256
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001257 @AutoRetry('pull', _RETRY_TIMES)
Peter Shihe6afab32018-09-11 17:16:48 +08001258 def _pull(src, dst, ftype, perm=0o644, link=None):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001259 try:
1260 os.makedirs(os.path.dirname(dst))
1261 except Exception:
1262 pass
1263
1264 src_base = os.path.basename(src)
1265
1266 # Remote file is a link
1267 if ftype == 'l':
1268 pbar = ProgressBar(src_base)
1269 if os.path.exists(dst):
1270 os.remove(dst)
1271 os.symlink(link, dst)
1272 pbar.End()
1273 return
1274
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001275 url = ('%s:%d/api/agent/download/%s?filename=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001276 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001277 urllib.parse.quote(self._selected_mid), urllib.parse.quote(src)))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001278 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001279 h = UrlOpen(self._state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +08001280 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001281 msg = json.loads(e.read()).get('error', 'unkown error')
1282 raise RuntimeError('pull: %s' % msg)
1283 except KeyboardInterrupt:
1284 return
1285
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001286 pbar = ProgressBar(src_base)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001287 with open(dst, 'w') as f:
1288 os.fchmod(f.fileno(), perm)
1289 total_size = int(h.headers.get('Content-Length'))
1290 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001291
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001292 while True:
1293 data = h.read(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +08001294 if not data:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001295 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001296 downloaded_size += len(data)
Yilin Yang14d02a22019-11-01 11:32:03 +08001297 pbar.SetProgress(downloaded_size * 100 / total_size,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001298 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001299 f.write(data)
1300 pbar.End()
1301
1302 # Use find to get a listing of all files under a root directory. The 'stat'
1303 # command is used to retrieve the filename and it's filemode.
1304 output = self.CheckOutput(
1305 'cd $HOME; '
1306 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001307 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1308 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001309 % {'src': args.src})
1310
1311 # We got error from the stat command
1312 if output.startswith('stat: '):
1313 sys.stderr.write(output)
1314 return
1315
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001316 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001317 common_prefix = os.path.dirname(args.src)
1318
1319 if len(entries) == 1:
1320 entry = entries[0]
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001321 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001322 if os.path.isdir(args.dst):
1323 dst = os.path.join(args.dst, os.path.basename(src_path))
1324 else:
1325 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001326 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001327 else:
1328 if not os.path.exists(args.dst):
1329 common_prefix = args.src
1330
1331 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001332 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001333 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001334 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1335 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001336
1337 @Command('forward', 'forward remote port to local port', [
1338 Arg('--list', dest='list_all', action='store_true', default=False,
1339 help='list all port forwarding sessions'),
1340 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1341 default=None,
1342 help='remove port forwarding for local port LOCAL_PORT'),
1343 Arg('--remove-all', dest='remove_all', action='store_true',
1344 default=False, help='remove all port forwarding'),
1345 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1346 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1347 def Forward(self, args):
1348 if args.list_all:
1349 max_len = 10
Peter Shihf84a8972017-06-19 15:18:24 +08001350 if self._state.forwards:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001351 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1352
1353 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1354 for local in sorted(self._state.forwards.keys()):
1355 value = self._state.forwards[local]
1356 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1357 return
1358
1359 if args.remove_all:
1360 self._server.RemoveAllForward()
1361 return
1362
1363 if args.remove:
1364 self._server.RemoveForward(args.remove)
1365 return
1366
1367 self.CheckClient()
1368
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001369 if args.remote is None:
1370 raise RuntimeError('remote port not specified')
1371
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001372 if args.local is None:
1373 args.local = args.remote
1374 remote = int(args.remote)
1375 local = int(args.local)
1376
1377 def HandleConnection(conn):
1378 headers = []
1379 if self._state.username is not None and self._state.password is not None:
1380 headers.append(BasicAuthHeader(self._state.username,
1381 self._state.password))
1382
1383 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1384 ws = ForwarderWebSocketClient(
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001385 self._state, conn,
Peter Shihe03450b2017-06-14 14:02:08 +08001386 scheme + '%s:%d/api/agent/forward/%s?port=%d' % (
1387 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001388 urllib.parse.quote(self._selected_mid), remote),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001389 headers=headers)
1390 try:
1391 ws.connect()
1392 ws.run()
1393 except Exception as e:
1394 print('error: %s' % e)
1395 finally:
1396 ws.close()
1397
1398 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1399 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1400 server.bind(('0.0.0.0', local))
1401 server.listen(5)
1402
1403 pid = os.fork()
1404 if pid == 0:
1405 while True:
1406 conn, unused_addr = server.accept()
1407 t = threading.Thread(target=HandleConnection, args=(conn,))
1408 t.daemon = True
1409 t.start()
1410 else:
1411 self._server.AddForward(self._selected_mid, remote, local, pid)
1412
1413
1414def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001415 # Setup logging format
1416 logger = logging.getLogger()
1417 logger.setLevel(logging.INFO)
1418 handler = logging.StreamHandler()
1419 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1420 handler.setFormatter(formatter)
1421 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001422
1423 # Add DaemonState to JSONRPC lib classes
1424 Config.instance().classes.add(DaemonState)
1425
1426 ovl = OverlordCLIClient()
1427 try:
1428 ovl.Main()
1429 except KeyboardInterrupt:
1430 print('Ctrl-C received, abort')
1431 except Exception as e:
1432 print('error: %s' % e)
1433
1434
1435if __name__ == '__main__':
1436 main()