blob: ac5a30fbf84a2e007cc4413b3c67b9bf617288d9 [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
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08006import argparse
7import ast
8import base64
9import fcntl
Peter Shih13e78c52018-01-23 12:57:07 +080010import functools
Wei-Ning Huangba768ab2016-02-07 14:38:06 +080011import getpass
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080012import hashlib
Yilin Yang752db712019-09-27 15:42:38 +080013import http.client
Yilin Yang8d4f9d02019-11-28 17:12:11 +080014from io import StringIO
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080015import json
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080016import logging
17import os
18import re
19import select
Yilin Yang3db92cc2020-10-26 15:31:47 +080020import shutil
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080021import signal
22import socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080023import ssl
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080024import struct
25import subprocess
26import sys
27import tempfile
28import termios
29import threading
30import time
31import tty
Peter Shih99b73ec2017-06-16 17:54:15 +080032import unicodedata # required by pyinstaller, pylint: disable=unused-import
Yilin Yangf54fb912020-01-08 11:42:38 +080033import urllib.error
34import urllib.parse
35import urllib.request
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080036
Peter Shih99b73ec2017-06-16 17:54:15 +080037import jsonrpclib
Stimim Chena30447c2020-10-06 10:04:00 +080038from jsonrpclib import config
Peter Shih99b73ec2017-06-16 17:54:15 +080039from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080040from ws4py.client import WebSocketBaseClient
Peter Shih99b73ec2017-06-16 17:54:15 +080041import yaml
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080042
Yilin Yang83c8f442020-05-05 13:46:51 +080043from cros.factory.utils import process_utils
44
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080045
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080046_CERT_DIR = os.path.expanduser('~/.config/ovl')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080047
48_ESCAPE = '~'
49_BUFSIZ = 8192
50_OVERLORD_PORT = 4455
51_OVERLORD_HTTP_PORT = 9000
52_OVERLORD_CLIENT_DAEMON_PORT = 4488
53_OVERLORD_CLIENT_DAEMON_RPC_ADDR = ('127.0.0.1', _OVERLORD_CLIENT_DAEMON_PORT)
54
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080055_CONNECT_TIMEOUT = 3
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080056_DEFAULT_HTTP_TIMEOUT = 30
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080057_LIST_CACHE_TIMEOUT = 2
58_DEFAULT_TERMINAL_WIDTH = 80
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080059_RETRY_TIMES = 3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080060
61# echo -n overlord | md5sum
62_HTTP_BOUNDARY_MAGIC = '9246f080c855a69012707ab53489b921'
63
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080064# Terminal resize control
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080065_CONTROL_START = 128
66_CONTROL_END = 129
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080067
68# Stream control
69_STDIN_CLOSED = '##STDIN_CLOSED##'
70
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080071_SSH_CONTROL_SOCKET_PREFIX = os.path.join(tempfile.gettempdir(),
72 'ovl-ssh-control-')
73
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080074_TLS_CERT_FAILED_WARNING = """
75@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
76@ WARNING: REMOTE HOST VERIFICATION HAS FAILED! @
77@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Yilin Yang3db92cc2020-10-26 15:31:47 +080078Failed Reason: %s.
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080079
Yilin Yang3db92cc2020-10-26 15:31:47 +080080Please use -c option to specify path of root CA certificate.
81This root CA certificate should be the one that signed the certificate used by
82overlord server."""
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080083
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080084
85def GetVersionDigest():
86 """Return the sha1sum of the current executing script."""
Wei-Ning Huang31609662016-08-11 00:22:25 +080087 # Check python script by default
88 filename = __file__
89
90 # If we are running from a frozen binary, we should calculate the checksum
91 # against that binary instead of the python script.
92 # See: https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
93 if getattr(sys, 'frozen', False):
94 filename = sys.executable
95
Yilin Yang0412c272019-12-05 16:57:40 +080096 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080097 return hashlib.sha1(f.read()).hexdigest()
98
99
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800100def GetTLSCertPath(host):
101 return os.path.join(_CERT_DIR, '%s.cert' % host)
102
103
104def UrlOpen(state, url):
Yilin Yangf54fb912020-01-08 11:42:38 +0800105 """Wrapper for urllib.request.urlopen.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800106
107 It selects correct HTTP scheme according to self._state.ssl, add HTTP
108 basic auth headers, and add specify correct SSL context.
109 """
110 url = MakeRequestUrl(state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +0800111 request = urllib.request.Request(url)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800112 if state.username is not None and state.password is not None:
113 request.add_header(*BasicAuthHeader(state.username, state.password))
Yilin Yangf54fb912020-01-08 11:42:38 +0800114 return urllib.request.urlopen(request, timeout=_DEFAULT_HTTP_TIMEOUT,
115 context=state.ssl_context)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800116
117
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800118def KillGraceful(pid, wait_secs=1):
119 """Kill a process gracefully by first sending SIGTERM, wait for some time,
120 then send SIGKILL to make sure it's killed."""
121 try:
122 os.kill(pid, signal.SIGTERM)
123 time.sleep(wait_secs)
124 os.kill(pid, signal.SIGKILL)
125 except OSError:
126 pass
127
128
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800129def AutoRetry(action_name, retries):
130 """Decorator for retry function call."""
131 def Wrap(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800132 @functools.wraps(func)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800133 def Loop(*args, **kwargs):
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800134 for unused_i in range(retries):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800135 try:
136 func(*args, **kwargs)
137 except Exception as e:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800138 print('error: %s: %s: retrying ...' % (args[0], e))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800139 else:
140 break
141 else:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800142 print('error: failed to %s %s' % (action_name, args[0]))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800143 return Loop
144 return Wrap
145
146
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800147def BasicAuthHeader(user, password):
148 """Return HTTP basic auth header."""
Stimim Chena30447c2020-10-06 10:04:00 +0800149 credential = base64.b64encode(
150 b'%s:%s' % (user.encode('utf-8'), password.encode('utf-8')))
151 return ('Authorization', 'Basic %s' % credential.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800152
153
154def GetTerminalSize():
155 """Retrieve terminal window size."""
156 ws = struct.pack('HHHH', 0, 0, 0, 0)
157 ws = fcntl.ioctl(0, termios.TIOCGWINSZ, ws)
158 lines, columns, unused_x, unused_y = struct.unpack('HHHH', ws)
159 return lines, columns
160
161
162def MakeRequestUrl(state, url):
163 return 'http%s://%s' % ('s' if state.ssl else '', url)
164
165
Fei Shaobd07c9a2020-06-15 19:04:50 +0800166class ProgressBar:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800167 SIZE_WIDTH = 11
168 SPEED_WIDTH = 10
169 DURATION_WIDTH = 6
170 PERCENTAGE_WIDTH = 8
171
172 def __init__(self, name):
173 self._start_time = time.time()
174 self._name = name
175 self._size = 0
176 self._width = 0
177 self._name_width = 0
178 self._name_max = 0
179 self._stat_width = 0
180 self._max = 0
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800181 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800182 self.SetProgress(0)
183
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800184 def _CalculateSize(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800185 self._width = GetTerminalSize()[1] or _DEFAULT_TERMINAL_WIDTH
186 self._name_width = int(self._width * 0.3)
187 self._name_max = self._name_width
188 self._stat_width = self.SIZE_WIDTH + self.SPEED_WIDTH + self.DURATION_WIDTH
189 self._max = (self._width - self._name_width - self._stat_width -
190 self.PERCENTAGE_WIDTH)
191
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800192 def _SizeToHuman(self, size_in_bytes):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800193 if size_in_bytes < 1024:
194 unit = 'B'
195 value = size_in_bytes
196 elif size_in_bytes < 1024 ** 2:
197 unit = 'KiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800198 value = size_in_bytes / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800199 elif size_in_bytes < 1024 ** 3:
200 unit = 'MiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800201 value = size_in_bytes / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800202 elif size_in_bytes < 1024 ** 4:
203 unit = 'GiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800204 value = size_in_bytes / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800205 return ' %6.1f %3s' % (value, unit)
206
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800207 def _SpeedToHuman(self, speed_in_bs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800208 if speed_in_bs < 1024:
209 unit = 'B'
210 value = speed_in_bs
211 elif speed_in_bs < 1024 ** 2:
212 unit = 'K'
Yilin Yang14d02a22019-11-01 11:32:03 +0800213 value = speed_in_bs / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800214 elif speed_in_bs < 1024 ** 3:
215 unit = 'M'
Yilin Yang14d02a22019-11-01 11:32:03 +0800216 value = speed_in_bs / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800217 elif speed_in_bs < 1024 ** 4:
218 unit = 'G'
Yilin Yang14d02a22019-11-01 11:32:03 +0800219 value = speed_in_bs / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800220 return ' %6.1f%s/s' % (value, unit)
221
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800222 def _DurationToClock(self, duration):
Yilin Yang14d02a22019-11-01 11:32:03 +0800223 return ' %02d:%02d' % (duration // 60, duration % 60)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800224
225 def SetProgress(self, percentage, size=None):
226 current_width = GetTerminalSize()[1]
227 if self._width != current_width:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800228 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800229
230 if size is not None:
231 self._size = size
232
233 elapse_time = time.time() - self._start_time
Yilin Yang14d02a22019-11-01 11:32:03 +0800234 speed = self._size / elapse_time
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800235
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800236 size_str = self._SizeToHuman(self._size)
237 speed_str = self._SpeedToHuman(speed)
238 elapse_str = self._DurationToClock(elapse_time)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800239
240 width = int(self._max * percentage / 100.0)
241 sys.stdout.write(
242 '%*s' % (- self._name_max,
243 self._name if len(self._name) <= self._name_max else
244 self._name[:self._name_max - 4] + ' ...') +
245 size_str + speed_str + elapse_str +
246 ((' [' + '#' * width + ' ' * (self._max - width) + ']' +
247 '%4d%%' % int(percentage)) if self._max > 2 else '') + '\r')
248 sys.stdout.flush()
249
250 def End(self):
251 self.SetProgress(100.0)
252 sys.stdout.write('\n')
253 sys.stdout.flush()
254
255
Fei Shaobd07c9a2020-06-15 19:04:50 +0800256class DaemonState:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800257 """DaemonState is used for storing Overlord state info."""
258 def __init__(self):
259 self.version_sha1sum = GetVersionDigest()
260 self.host = None
261 self.port = None
262 self.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800263 self.ssl_self_signed = False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800264 self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800265 self.ssh = False
266 self.orig_host = None
267 self.ssh_pid = None
268 self.username = None
269 self.password = None
270 self.selected_mid = None
271 self.forwards = {}
272 self.listing = []
273 self.last_list = 0
274
275
Fei Shaobd07c9a2020-06-15 19:04:50 +0800276class OverlordClientDaemon:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800277 """Overlord Client Daemon."""
278 def __init__(self):
Stimim Chena30447c2020-10-06 10:04:00 +0800279 # Use full module path for jsonrpclib to resolve.
280 import cros.factory.tools.ovl
281 self._state = cros.factory.tools.ovl.DaemonState()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800282 self._server = None
283
284 def Start(self):
285 self.StartRPCServer()
286
287 def StartRPCServer(self):
288 self._server = SimpleJSONRPCServer(_OVERLORD_CLIENT_DAEMON_RPC_ADDR,
289 logRequests=False)
290 exports = [
291 (self.State, 'State'),
292 (self.Ping, 'Ping'),
293 (self.GetPid, 'GetPid'),
294 (self.Connect, 'Connect'),
295 (self.Clients, 'Clients'),
296 (self.SelectClient, 'SelectClient'),
297 (self.AddForward, 'AddForward'),
298 (self.RemoveForward, 'RemoveForward'),
299 (self.RemoveAllForward, 'RemoveAllForward'),
300 ]
301 for func, name in exports:
302 self._server.register_function(func, name)
303
304 pid = os.fork()
305 if pid == 0:
Peter Shih4d55ded2017-07-03 17:19:01 +0800306 for fd in range(3):
307 os.close(fd)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800308 self._server.serve_forever()
309
310 @staticmethod
311 def GetRPCServer():
312 """Returns the Overlord client daemon RPC server."""
313 server = jsonrpclib.Server('http://%s:%d' %
314 _OVERLORD_CLIENT_DAEMON_RPC_ADDR)
315 try:
316 server.Ping()
317 except Exception:
318 return None
319 return server
320
321 def State(self):
322 return self._state
323
324 def Ping(self):
325 return True
326
327 def GetPid(self):
328 return os.getpid()
329
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800330 def _GetJSON(self, path):
331 url = '%s:%d%s' % (self._state.host, self._state.port, path)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800332 return json.loads(UrlOpen(self._state, url).read())
333
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800334 def _TLSEnabled(self):
335 """Determine if TLS is enabled on given server address."""
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800336 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
337 try:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800338 # Allow any certificate since we only want to check if server talks TLS.
339 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
340 context.verify_mode = ssl.CERT_NONE
341
342 sock = context.wrap_socket(sock, server_hostname=self._state.host)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800343 sock.settimeout(_CONNECT_TIMEOUT)
344 sock.connect((self._state.host, self._state.port))
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800345 return True
Wei-Ning Huangecc80b82016-07-01 16:55:10 +0800346 except ssl.SSLError:
347 return False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800348 except socket.error: # Connect refused or timeout
349 raise
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800350 except Exception:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800351 return False # For whatever reason above failed, assume False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800352
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800353 def _CheckTLSCertificate(self, check_hostname=True):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800354 """Check TLS certificate.
355
356 Returns:
357 A tupple (check_result, if_certificate_is_loaded)
358 """
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800359 def _DoConnect(context):
360 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
361 try:
362 sock.settimeout(_CONNECT_TIMEOUT)
363 sock = context.wrap_socket(sock, server_hostname=self._state.host)
364 sock.connect((self._state.host, self._state.port))
365 except ssl.SSLError:
366 return False
367 finally:
368 sock.close()
369
370 # Save SSLContext for future use.
371 self._state.ssl_context = context
372 return True
373
374 # First try connect with built-in certificates
375 tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
376 if _DoConnect(tls_context):
377 return True
378
379 # Try with already saved certificate, if any.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800380 tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
381 tls_context.verify_mode = ssl.CERT_REQUIRED
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800382 tls_context.check_hostname = check_hostname
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800383
384 tls_cert_path = GetTLSCertPath(self._state.host)
385 if os.path.exists(tls_cert_path):
386 tls_context.load_verify_locations(tls_cert_path)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800387 self._state.ssl_self_signed = True
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800388
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800389 return _DoConnect(tls_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800390
391 def Connect(self, host, port=_OVERLORD_HTTP_PORT, ssh_pid=None,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800392 username=None, password=None, orig_host=None,
393 check_hostname=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800394 self._state.username = username
395 self._state.password = password
396 self._state.host = host
397 self._state.port = port
398 self._state.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800399 self._state.ssl_self_signed = False
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800400 self._state.orig_host = orig_host
401 self._state.ssh_pid = ssh_pid
402 self._state.selected_mid = None
403
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800404 tls_enabled = self._TLSEnabled()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800405 if tls_enabled:
Yilin Yang3db92cc2020-10-26 15:31:47 +0800406 if not os.path.exists(os.path.join(_CERT_DIR, '%s.cert' % host)):
407 return 'SSLCertificateNotExisted'
408
409 if not self._CheckTLSCertificate(check_hostname):
410 return 'SSLVerifyFailed'
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800411
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800412 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800413 self._state.ssl = tls_enabled
414 UrlOpen(self._state, '%s:%d' % (host, port))
Yilin Yangf54fb912020-01-08 11:42:38 +0800415 except urllib.error.HTTPError as e:
Yilin Yang3db92cc2020-10-26 15:31:47 +0800416 return ('HTTPError', e.getcode(), str(e),
417 e.read().strip().decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800418 except Exception as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800419 return str(e)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800420 else:
421 return True
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800422
423 def Clients(self):
424 if time.time() - self._state.last_list <= _LIST_CACHE_TIMEOUT:
425 return self._state.listing
426
Peter Shihcf0f3b22017-06-19 15:59:22 +0800427 self._state.listing = self._GetJSON('/api/agents/list')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800428 self._state.last_list = time.time()
429 return self._state.listing
430
431 def SelectClient(self, mid):
432 self._state.selected_mid = mid
433
434 def AddForward(self, mid, remote, local, pid):
435 self._state.forwards[local] = (mid, remote, pid)
436
437 def RemoveForward(self, local_port):
438 try:
439 unused_mid, unused_remote, pid = self._state.forwards[local_port]
440 KillGraceful(pid)
441 del self._state.forwards[local_port]
442 except (KeyError, OSError):
443 pass
444
445 def RemoveAllForward(self):
446 for unused_mid, unused_remote, pid in self._state.forwards.values():
447 try:
448 KillGraceful(pid)
449 except OSError:
450 pass
451 self._state.forwards = {}
452
453
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800454class SSLEnabledWebSocketBaseClient(WebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800455 def __init__(self, state, *args, **kwargs):
456 cafile = ssl.get_default_verify_paths().openssl_cafile
457 # For some system / distribution, python can not detect system cafile path.
458 # In such case we fallback to the default path.
459 if not os.path.exists(cafile):
460 cafile = '/etc/ssl/certs/ca-certificates.crt'
461
462 if state.ssl_self_signed:
463 cafile = GetTLSCertPath(state.host)
464
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800465 ssl_options = {
466 'cert_reqs': ssl.CERT_REQUIRED,
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800467 'ca_certs': cafile
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800468 }
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800469 # ws4py does not allow you to specify SSLContext, but rather passing in the
470 # argument of ssl.wrap_socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800471 super(SSLEnabledWebSocketBaseClient, self).__init__(
472 ssl_options=ssl_options, *args, **kwargs)
473
474
475class TerminalWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800476 def __init__(self, state, mid, escape, *args, **kwargs):
477 super(TerminalWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800478 self._mid = mid
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800479 self._escape = escape
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800480 self._stdin_fd = sys.stdin.fileno()
481 self._old_termios = None
482
483 def handshake_ok(self):
484 pass
485
486 def opened(self):
487 nonlocals = {'size': (80, 40)}
488
489 def _ResizeWindow():
490 size = GetTerminalSize()
491 if size != nonlocals['size']: # Size not changed, ignore
492 control = {'command': 'resize', 'params': list(size)}
493 payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END)
494 nonlocals['size'] = size
495 try:
496 self.send(payload, binary=True)
497 except Exception:
498 pass
499
500 def _FeedInput():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800501 self._old_termios = termios.tcgetattr(self._stdin_fd)
502 tty.setraw(self._stdin_fd)
503
504 READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3)
505
506 try:
507 state = READY
508 while True:
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800509 # Check if terminal is resized
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800510 _ResizeWindow()
511
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800512 ch = sys.stdin.read(1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800513
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800514 # Scan for escape sequence
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800515 if self._escape:
516 if state == READY:
517 state = ENTER_PRESSED if ch == chr(0x0d) else READY
518 elif state == ENTER_PRESSED:
519 state = ESCAPE_PRESSED if ch == self._escape else READY
520 elif state == ESCAPE_PRESSED:
521 if ch == '.':
522 self.close()
523 break
524 else:
525 state = READY
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800526
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800527 self.send(ch)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800528 except (KeyboardInterrupt, RuntimeError):
529 pass
530
531 t = threading.Thread(target=_FeedInput)
532 t.daemon = True
533 t.start()
534
535 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800536 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800537 termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios)
538 print('Connection to %s closed.' % self._mid)
539
Yilin Yangf64670b2020-01-06 11:22:18 +0800540 def received_message(self, message):
541 if message.is_binary:
Stimim Chena30447c2020-10-06 10:04:00 +0800542 sys.stdout.write(message.data.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800543 sys.stdout.flush()
544
545
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800546class ShellWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800547 def __init__(self, state, output, *args, **kwargs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800548 """Constructor.
549
550 Args:
551 output: output file object.
552 """
553 self.output = output
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800554 super(ShellWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800555
556 def handshake_ok(self):
557 pass
558
559 def opened(self):
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800560 def _FeedInput():
561 try:
562 while True:
Stimim Chena30447c2020-10-06 10:04:00 +0800563 data = sys.stdin.buffer.read(1)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800564
Peter Shihf84a8972017-06-19 15:18:24 +0800565 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800566 self.send(_STDIN_CLOSED * 2)
567 break
568 self.send(data, binary=True)
569 except (KeyboardInterrupt, RuntimeError):
570 pass
571
572 t = threading.Thread(target=_FeedInput)
573 t.daemon = True
574 t.start()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800575
576 def closed(self, code, reason=None):
577 pass
578
Yilin Yangf64670b2020-01-06 11:22:18 +0800579 def received_message(self, message):
580 if message.is_binary:
Stimim Chena30447c2020-10-06 10:04:00 +0800581 self.output.write(message.data.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800582 self.output.flush()
583
584
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800585class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800586 def __init__(self, state, sock, *args, **kwargs):
587 super(ForwarderWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800588 self._sock = sock
589 self._stop = threading.Event()
590
591 def handshake_ok(self):
592 pass
593
594 def opened(self):
595 def _FeedInput():
596 try:
597 self._sock.setblocking(False)
598 while True:
599 rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5)
600 if self._stop.is_set():
601 break
602 if self._sock in rd:
603 data = self._sock.recv(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +0800604 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800605 self.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800606 break
607 self.send(data, binary=True)
608 except Exception:
609 pass
610 finally:
611 self._sock.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800612
613 t = threading.Thread(target=_FeedInput)
614 t.daemon = True
615 t.start()
616
617 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800618 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800619 self._stop.set()
620 sys.exit(0)
621
Yilin Yangf64670b2020-01-06 11:22:18 +0800622 def received_message(self, message):
623 if message.is_binary:
624 self._sock.send(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800625
626
627def Arg(*args, **kwargs):
628 return (args, kwargs)
629
630
631def Command(command, help_msg=None, args=None):
632 """Decorator for adding argparse parameter for a method."""
633 if args is None:
634 args = []
635 def WrapFunc(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800636 @functools.wraps(func)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800637 def Wrapped(*args, **kwargs):
638 return func(*args, **kwargs)
Peter Shih99b73ec2017-06-16 17:54:15 +0800639 # pylint: disable=protected-access
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800640 Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args}
641 return Wrapped
642 return WrapFunc
643
644
645def ParseMethodSubCommands(cls):
646 """Decorator for a class using the @Command decorator.
647
648 This decorator retrieve command info from each method and append it in to the
649 SUBCOMMANDS class variable, which is later used to construct parser.
650 """
Yilin Yang879fbda2020-05-14 13:52:30 +0800651 for unused_key, method in cls.__dict__.items():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800652 if hasattr(method, '__arg_attr'):
Peter Shih99b73ec2017-06-16 17:54:15 +0800653 # pylint: disable=protected-access
654 cls.SUBCOMMANDS.append(method.__arg_attr)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800655 return cls
656
657
658@ParseMethodSubCommands
Fei Shaobd07c9a2020-06-15 19:04:50 +0800659class OverlordCLIClient:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800660 """Overlord command line interface client."""
661
662 SUBCOMMANDS = []
663
664 def __init__(self):
665 self._parser = self._BuildParser()
666 self._selected_mid = None
667 self._server = None
668 self._state = None
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800669 self._escape = None
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800670
671 def _BuildParser(self):
672 root_parser = argparse.ArgumentParser(prog='ovl')
673 subparsers = root_parser.add_subparsers(help='sub-command')
674
675 root_parser.add_argument('-s', dest='selected_mid', action='store',
676 default=None,
677 help='select target to execute command on')
678 root_parser.add_argument('-S', dest='select_mid_before_action',
679 action='store_true', default=False,
680 help='select target before executing command')
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800681 root_parser.add_argument('-e', dest='escape', metavar='ESCAPE_CHAR',
682 action='store', default=_ESCAPE, type=str,
683 help='set shell escape character, \'none\' to '
684 'disable escape completely')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800685
686 for attr in self.SUBCOMMANDS:
687 parser = subparsers.add_parser(attr['command'], help=attr['help'])
688 parser.set_defaults(which=attr['command'])
689 for arg in attr['args']:
690 parser.add_argument(*arg[0], **arg[1])
691
692 return root_parser
693
694 def Main(self):
695 # We want to pass the rest of arguments after shell command directly to the
696 # function without parsing it.
697 try:
698 index = sys.argv.index('shell')
699 except ValueError:
700 args = self._parser.parse_args()
701 else:
702 args = self._parser.parse_args(sys.argv[1:index + 1])
703
704 command = args.which
705 self._selected_mid = args.selected_mid
706
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800707 if args.escape and args.escape != 'none':
708 self._escape = args.escape[0]
709
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800710 if command == 'start-server':
711 self.StartServer()
712 return
Fei Shao12ecf382020-06-23 18:32:26 +0800713 if command == 'kill-server':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800714 self.KillServer()
715 return
716
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800717 self.CheckDaemon()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800718 if command == 'status':
719 self.Status()
720 return
Fei Shao12ecf382020-06-23 18:32:26 +0800721 if command == 'connect':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800722 self.Connect(args)
723 return
724
725 # The following command requires connection to the server
726 self.CheckConnection()
727
728 if args.select_mid_before_action:
729 self.SelectClient(store=False)
730
731 if command == 'select':
732 self.SelectClient(args)
733 elif command == 'ls':
Peter Shih99b73ec2017-06-16 17:54:15 +0800734 self.ListClients(args)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800735 elif command == 'shell':
736 command = sys.argv[sys.argv.index('shell') + 1:]
737 self.Shell(command)
738 elif command == 'push':
739 self.Push(args)
740 elif command == 'pull':
741 self.Pull(args)
742 elif command == 'forward':
743 self.Forward(args)
744
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800745 def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None):
746 """Perform HTTP POST and upload file to Overlord.
747
748 To minimize the external dependencies, we construct the HTTP post request
749 by ourselves.
750 """
751 url = MakeRequestUrl(self._state, url)
752 size = os.stat(filename).st_size
753 boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC
754 CRLF = '\r\n'
Yilin Yangf54fb912020-01-08 11:42:38 +0800755 parse = urllib.parse.urlparse(url)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800756
757 part_headers = [
758 '--' + boundary,
759 'Content-Disposition: form-data; name="file"; '
760 'filename="%s"' % os.path.basename(filename),
761 'Content-Type: application/octet-stream',
762 '', ''
763 ]
764 part_header = CRLF.join(part_headers)
765 end_part = CRLF + '--' + boundary + '--' + CRLF
766
767 content_length = len(part_header) + size + len(end_part)
768 if parse.scheme == 'http':
Stimim Chena30447c2020-10-06 10:04:00 +0800769 h = http.client.HTTPConnection(parse.netloc)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800770 else:
Stimim Chena30447c2020-10-06 10:04:00 +0800771 h = http.client.HTTPSConnection(parse.netloc,
772 context=self._state.ssl_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800773
774 post_path = url[url.index(parse.netloc) + len(parse.netloc):]
775 h.putrequest('POST', post_path)
776 h.putheader('Content-Length', content_length)
777 h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
778
779 if user and passwd:
780 h.putheader(*BasicAuthHeader(user, passwd))
781 h.endheaders()
Stimim Chena30447c2020-10-06 10:04:00 +0800782 h.send(part_header.encode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800783
784 count = 0
Stimim Chena30447c2020-10-06 10:04:00 +0800785 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800786 while True:
787 data = f.read(_BUFSIZ)
788 if not data:
789 break
790 count += len(data)
791 if progress:
Yilin Yang14d02a22019-11-01 11:32:03 +0800792 progress(count * 100 // size, count)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800793 h.send(data)
794
Stimim Chena30447c2020-10-06 10:04:00 +0800795 h.send(end_part.encode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800796 progress(100)
797
798 if count != size:
799 logging.warning('file changed during upload, upload may be truncated.')
800
Stimim Chena30447c2020-10-06 10:04:00 +0800801 resp = h.getresponse()
802 return resp.status == 200
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800803
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800804 def CheckDaemon(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800805 self._server = OverlordClientDaemon.GetRPCServer()
806 if self._server is None:
807 print('* daemon not running, starting it now on port %d ... *' %
808 _OVERLORD_CLIENT_DAEMON_PORT)
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800809 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800810
811 self._state = self._server.State()
812 sha1sum = GetVersionDigest()
813
814 if sha1sum != self._state.version_sha1sum:
815 print('ovl server is out of date. killing...')
816 KillGraceful(self._server.GetPid())
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800817 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800818
819 def GetSSHControlFile(self, host):
820 return _SSH_CONTROL_SOCKET_PREFIX + host
821
822 def SSHTunnel(self, user, host, port):
823 """SSH forward the remote overlord server.
824
825 Overlord server may not have port 9000 open to the public network, in such
826 case we can SSH forward the port to localhost.
827 """
828
829 control_file = self.GetSSHControlFile(host)
830 try:
831 os.unlink(control_file)
832 except Exception:
833 pass
834
835 subprocess.Popen([
836 'ssh', '-Nf',
837 '-M', # Enable master mode
838 '-S', control_file,
839 '-L', '9000:localhost:9000',
840 '-p', str(port),
841 '%s%s' % (user + '@' if user else '', host)
842 ]).wait()
843
Yilin Yang83c8f442020-05-05 13:46:51 +0800844 p = process_utils.Spawn([
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800845 'ssh',
846 '-S', control_file,
847 '-O', 'check', host,
Yilin Yang83c8f442020-05-05 13:46:51 +0800848 ], read_stderr=True, ignore_stdout=True)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800849
Yilin Yang83c8f442020-05-05 13:46:51 +0800850 s = re.search(r'pid=(\d+)', p.stderr_data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800851 if s:
852 return int(s.group(1))
853
854 raise RuntimeError('can not establish ssh connection')
855
856 def CheckConnection(self):
857 if self._state.host is None:
858 raise RuntimeError('not connected to any server, abort')
859
860 try:
861 self._server.Clients()
862 except Exception:
863 raise RuntimeError('remote server disconnected, abort')
864
865 if self._state.ssh_pid is not None:
866 ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)],
867 stdout=subprocess.PIPE,
868 stderr=subprocess.PIPE).wait()
869 if ret != 0:
870 raise RuntimeError('ssh tunnel disconnected, please re-connect')
871
872 def CheckClient(self):
873 if self._selected_mid is None:
874 if self._state.selected_mid is None:
875 raise RuntimeError('No client is selected')
876 self._selected_mid = self._state.selected_mid
877
Peter Shihcf0f3b22017-06-19 15:59:22 +0800878 if not any(client['mid'] == self._selected_mid
879 for client in self._server.Clients()):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800880 raise RuntimeError('client %s disappeared' % self._selected_mid)
881
882 def CheckOutput(self, command):
883 headers = []
884 if self._state.username is not None and self._state.password is not None:
885 headers.append(BasicAuthHeader(self._state.username,
886 self._state.password))
887
888 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Yilin Yang8d4f9d02019-11-28 17:12:11 +0800889 sio = StringIO()
Peter Shihe03450b2017-06-14 14:02:08 +0800890 ws = ShellWebSocketClient(
891 self._state, sio, scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
892 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +0800893 urllib.parse.quote(self._selected_mid),
894 urllib.parse.quote(command)),
Peter Shihe03450b2017-06-14 14:02:08 +0800895 headers=headers)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800896 ws.connect()
897 ws.run()
898 return sio.getvalue()
899
900 @Command('status', 'show Overlord connection status')
901 def Status(self):
902 if self._state.host is None:
903 print('Not connected to any host.')
904 else:
905 if self._state.ssh_pid is not None:
906 print('Connected to %s with SSH tunneling.' % self._state.orig_host)
907 else:
908 print('Connected to %s:%d.' % (self._state.host, self._state.port))
909
910 if self._selected_mid is None:
911 self._selected_mid = self._state.selected_mid
912
913 if self._selected_mid is None:
914 print('No client is selected.')
915 else:
916 print('Client %s selected.' % self._selected_mid)
917
918 @Command('connect', 'connect to Overlord server', [
919 Arg('host', metavar='HOST', type=str, default='localhost',
920 help='Overlord hostname/IP'),
Yilin Yang3db92cc2020-10-26 15:31:47 +0800921 Arg('port', metavar='PORT', type=int, default=_OVERLORD_HTTP_PORT,
922 help='Overlord port'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800923 Arg('-f', '--forward', dest='ssh_forward', default=False,
Yilin Yang3db92cc2020-10-26 15:31:47 +0800924 action='store_true', help='connect with SSH forwarding to the host'),
925 Arg('-p', '--ssh-port', dest='ssh_port', default=22, type=int,
926 help='SSH server port for SSH forwarding'),
927 Arg('-l', '--ssh-login', dest='ssh_login', default='', type=str,
928 help='SSH server login name for SSH forwarding'),
929 Arg('-u', '--user', dest='user', default=None, type=str,
930 help='Overlord HTTP auth username'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800931 Arg('-w', '--passwd', dest='passwd', default=None, type=str,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800932 help='Overlord HTTP auth password'),
Yilin Yang3db92cc2020-10-26 15:31:47 +0800933 Arg('-c', '--root-CA', dest='cert', default=None, type=str,
934 help='Path to root CA certificate, only assign at the first time'),
935 Arg('-i', '--no-check-hostname', dest='check_hostname', default=True,
936 action='store_false', help='Ignore SSL cert hostname check')
937 ])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800938 def Connect(self, args):
939 ssh_pid = None
940 host = args.host
941 orig_host = args.host
942
Yilin Yang3db92cc2020-10-26 15:31:47 +0800943 if args.cert and os.path.exists(args.cert):
944 os.makedirs(_CERT_DIR, exist_ok=True)
945 shutil.copy(args.cert, os.path.join(_CERT_DIR, '%s.cert' % host))
946
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800947 if args.ssh_forward:
948 # Kill previous SSH tunnel
949 self.KillSSHTunnel()
950
951 ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port)
952 host = 'localhost'
953
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800954 username_provided = args.user is not None
955 password_provided = args.passwd is not None
956 prompt = False
957
Peter Shih533566a2018-09-05 17:48:03 +0800958 for unused_i in range(3): # pylint: disable=too-many-nested-blocks
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800959 try:
960 if prompt:
961 if not username_provided:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800962 args.user = input('Username: ')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800963 if not password_provided:
964 args.passwd = getpass.getpass('Password: ')
965
966 ret = self._server.Connect(host, args.port, ssh_pid, args.user,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800967 args.passwd, orig_host,
968 args.check_hostname)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800969 if isinstance(ret, list):
Fei Shao12ecf382020-06-23 18:32:26 +0800970 if ret[0] == 'HTTPError':
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800971 code, except_str, body = ret[1:]
972 if code == 401:
973 print('connect: %s' % body)
974 prompt = True
975 if not username_provided or not password_provided:
976 continue
Fei Shaof91ab8f2020-06-23 17:54:03 +0800977 break
Fei Shao0e4e2c62020-06-23 18:22:26 +0800978 logging.error('%s; %s', except_str, body)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800979
Yilin Yang3db92cc2020-10-26 15:31:47 +0800980 if ret in ('SSLCertificateNotExisted', 'SSLVerifyFailed'):
981 print(_TLS_CERT_FAILED_WARNING % ret)
982 return
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800983 if ret is not True:
984 print('can not connect to %s: %s' % (host, ret))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800985 else:
986 print('connection to %s:%d established.' % (host, args.port))
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800987 except Exception as e:
Yilin Yang3db92cc2020-10-26 15:31:47 +0800988 logging.exception(e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800989 else:
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800990 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800991
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800992 @Command('start-server', 'start overlord CLI client server')
993 def StartServer(self):
994 self._server = OverlordClientDaemon.GetRPCServer()
995 if self._server is None:
996 OverlordClientDaemon().Start()
997 time.sleep(1)
998 self._server = OverlordClientDaemon.GetRPCServer()
999 if self._server is not None:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001000 print('* daemon started successfully *\n')
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001001
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001002 @Command('kill-server', 'kill overlord CLI client server')
1003 def KillServer(self):
1004 self._server = OverlordClientDaemon.GetRPCServer()
1005 if self._server is None:
1006 return
1007
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001008 self._state = self._server.State()
1009
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001010 # Kill SSH Tunnel
1011 self.KillSSHTunnel()
1012
1013 # Kill server daemon
1014 KillGraceful(self._server.GetPid())
1015
1016 def KillSSHTunnel(self):
1017 if self._state.ssh_pid is not None:
1018 KillGraceful(self._state.ssh_pid)
1019
Peter Shihcf0f3b22017-06-19 15:59:22 +08001020 def _FilterClients(self, clients, prop_filters, mid=None):
1021 def _ClientPropertiesMatch(client, key, regex):
1022 try:
1023 return bool(re.search(regex, client['properties'][key]))
1024 except KeyError:
1025 return False
1026
1027 for prop_filter in prop_filters:
1028 key, sep, regex = prop_filter.partition('=')
1029 if not sep:
1030 # The filter doesn't contains =.
1031 raise ValueError('Invalid filter condition %r' % filter)
1032 clients = [c for c in clients if _ClientPropertiesMatch(c, key, regex)]
1033
1034 if mid is not None:
1035 client = next((c for c in clients if c['mid'] == mid), None)
1036 if client:
1037 return [client]
1038 clients = [c for c in clients if c['mid'].startswith(mid)]
1039 return clients
1040
1041 @Command('ls', 'list clients', [
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001042 Arg(
1043 '-f', '--filter', default=[], dest='filters', action='append',
Peter Shihcf0f3b22017-06-19 15:59:22 +08001044 help=('Conditions to filter clients by properties. '
1045 'Should be in form "key=regex", where regex is the regular '
1046 'expression that should be found in the value. '
1047 'Multiple --filter arguments would be ANDed.')),
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001048 Arg('-m', '--mid-only', default=False, action='store_true',
1049 help='Print mid only.'),
Peter Shih99b73ec2017-06-16 17:54:15 +08001050 Arg('-v', '--verbose', default=False, action='store_true',
1051 help='Print properties of each client.')
1052 ])
1053 def ListClients(self, args):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001054 clients = self._FilterClients(self._server.Clients(), args.filters)
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001055
1056 if args.verbose:
1057 for client in clients:
Peter Shih99b73ec2017-06-16 17:54:15 +08001058 print(yaml.safe_dump(client, default_flow_style=False))
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001059 return
1060
1061 # Used in station_setup to ckeck if there is duplicate mid.
1062 if args.mid_only:
1063 for client in clients:
Peter Shihcf0f3b22017-06-19 15:59:22 +08001064 print(client['mid'])
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001065 return
1066
1067 def FormatPrint(length, string):
1068 print('%*s' % (length + 2, string), end='|')
1069
1070 columns = ['mid', 'status', 'pytest', 'model', 'ip']
1071 columns_max_len = {column: len(column)
1072 for column in columns}
1073
1074 for client in clients:
1075 for column in columns:
1076 columns_max_len[column] = max(columns_max_len[column],
1077 len(str(client[column])))
1078
1079 for column in columns:
1080 FormatPrint(columns_max_len[column], column)
1081 print()
1082
1083 for client in clients:
1084 for column in columns:
1085 FormatPrint(columns_max_len[column], str(client[column]))
1086 print()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001087
1088 @Command('select', 'select default client', [
Peter Shihcf0f3b22017-06-19 15:59:22 +08001089 Arg('-f', '--filter', default=[], dest='filters', action='append',
1090 help=('Conditions to filter clients by properties. '
1091 'Should be in form "key=regex", where regex is the regular '
1092 'expression that should be found in the value. '
1093 'Multiple --filter arguments would be ANDed.')),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001094 Arg('mid', metavar='mid', nargs='?', default=None)])
1095 def SelectClient(self, args=None, store=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001096 mid = args.mid if args is not None else None
Wei-Ning Huang6796a132017-06-22 14:46:32 +08001097 filters = args.filters if args is not None else []
1098 clients = self._FilterClients(self._server.Clients(), filters, mid=mid)
Peter Shihcf0f3b22017-06-19 15:59:22 +08001099
1100 if not clients:
1101 raise RuntimeError('select: client not found')
Fei Shao0e4e2c62020-06-23 18:22:26 +08001102 if len(clients) == 1:
Peter Shihcf0f3b22017-06-19 15:59:22 +08001103 mid = clients[0]['mid']
1104 else:
1105 # This case would not happen when args.mid is specified.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001106 print('Select from the following clients:')
1107 for i, client in enumerate(clients):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001108 print(' %d. %s' % (i + 1, client['mid']))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001109
1110 print('\nSelection: ', end='')
1111 try:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001112 choice = int(input()) - 1
Peter Shihcf0f3b22017-06-19 15:59:22 +08001113 mid = clients[choice]['mid']
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001114 except ValueError:
1115 raise RuntimeError('select: invalid selection')
1116 except IndexError:
1117 raise RuntimeError('select: selection out of range')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001118
1119 self._selected_mid = mid
1120 if store:
1121 self._server.SelectClient(mid)
1122 print('Client %s selected' % mid)
1123
1124 @Command('shell', 'open a shell or execute a shell command', [
1125 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1126 def Shell(self, command=None):
1127 if command is None:
1128 command = []
1129 self.CheckClient()
1130
1131 headers = []
1132 if self._state.username is not None and self._state.password is not None:
1133 headers.append(BasicAuthHeader(self._state.username,
1134 self._state.password))
1135
1136 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Peter Shihe03450b2017-06-14 14:02:08 +08001137 if command:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001138 cmd = ' '.join(command)
Peter Shihe03450b2017-06-14 14:02:08 +08001139 ws = ShellWebSocketClient(
1140 self._state, sys.stdout,
1141 scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
1142 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001143 urllib.parse.quote(self._selected_mid), urllib.parse.quote(cmd)),
Peter Shihe03450b2017-06-14 14:02:08 +08001144 headers=headers)
1145 else:
1146 ws = TerminalWebSocketClient(
1147 self._state, self._selected_mid, self._escape,
1148 scheme + '%s:%d/api/agent/tty/%s' % (
1149 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001150 urllib.parse.quote(self._selected_mid)),
Peter Shihe03450b2017-06-14 14:02:08 +08001151 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001152 try:
1153 ws.connect()
1154 ws.run()
1155 except socket.error as e:
1156 if e.errno == 32: # Broken pipe
1157 pass
1158 else:
1159 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001160
1161 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001162 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001163 Arg('dst', metavar='DESTINATION')])
1164 def Push(self, args):
1165 self.CheckClient()
1166
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001167 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001168 def _push(src, dst):
1169 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001170
1171 # Local file is a link
1172 if os.path.islink(src):
1173 pbar = ProgressBar(src_base)
1174 link_path = os.readlink(src)
1175 self.CheckOutput('mkdir -p %(dirname)s; '
1176 'if [ -d "%(dst)s" ]; then '
1177 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1178 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1179 dict(dirname=os.path.dirname(dst),
1180 link_path=link_path, dst=dst,
1181 link_name=src_base))
1182 pbar.End()
1183 return
1184
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001185 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1186 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001187 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001188 urllib.parse.quote(self._selected_mid), dst, mode))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001189 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001190 UrlOpen(self._state, url + '&filename=%s' % src_base)
Yilin Yangf54fb912020-01-08 11:42:38 +08001191 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001192 msg = json.loads(e.read()).get('error', None)
1193 raise RuntimeError('push: %s' % msg)
1194
1195 pbar = ProgressBar(src_base)
1196 self._HTTPPostFile(url, src, pbar.SetProgress,
1197 self._state.username, self._state.password)
1198 pbar.End()
1199
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001200 def _push_single_target(src, dst):
1201 if os.path.isdir(src):
1202 dst_exists = ast.literal_eval(self.CheckOutput(
1203 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1204 for root, unused_x, files in os.walk(src):
1205 # If destination directory does not exist, we should strip the first
1206 # layer of directory. For example: src_dir contains a single file 'A'
1207 #
1208 # push src_dir dest_dir
1209 #
1210 # If dest_dir exists, the resulting directory structure should be:
1211 # dest_dir/src_dir/A
1212 # If dest_dir does not exist, the resulting directory structure should
1213 # be:
1214 # dest_dir/A
1215 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1216 for name in files:
1217 _push(os.path.join(root, name),
1218 os.path.join(dst, dst_root, name))
1219 else:
1220 _push(src, dst)
1221
1222 if len(args.srcs) > 1:
1223 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1224 '2>/dev/null' % args.dst).strip()
1225 if not dst_type:
1226 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1227 if dst_type != 'directory':
1228 raise RuntimeError('push: %s: Not a directory' % args.dst)
1229
1230 for src in args.srcs:
1231 if not os.path.exists(src):
1232 raise RuntimeError('push: can not stat "%s": no such file or directory'
1233 % src)
1234 if not os.access(src, os.R_OK):
1235 raise RuntimeError('push: can not open "%s" for reading' % src)
1236
1237 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001238
1239 @Command('pull', 'pull a file or directory from remote', [
1240 Arg('src', metavar='SOURCE'),
1241 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1242 def Pull(self, args):
1243 self.CheckClient()
1244
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001245 @AutoRetry('pull', _RETRY_TIMES)
Peter Shihe6afab32018-09-11 17:16:48 +08001246 def _pull(src, dst, ftype, perm=0o644, link=None):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001247 try:
1248 os.makedirs(os.path.dirname(dst))
1249 except Exception:
1250 pass
1251
1252 src_base = os.path.basename(src)
1253
1254 # Remote file is a link
1255 if ftype == 'l':
1256 pbar = ProgressBar(src_base)
1257 if os.path.exists(dst):
1258 os.remove(dst)
1259 os.symlink(link, dst)
1260 pbar.End()
1261 return
1262
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001263 url = ('%s:%d/api/agent/download/%s?filename=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001264 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001265 urllib.parse.quote(self._selected_mid), urllib.parse.quote(src)))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001266 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001267 h = UrlOpen(self._state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +08001268 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001269 msg = json.loads(e.read()).get('error', 'unkown error')
1270 raise RuntimeError('pull: %s' % msg)
1271 except KeyboardInterrupt:
1272 return
1273
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001274 pbar = ProgressBar(src_base)
Stimim Chena30447c2020-10-06 10:04:00 +08001275 with open(dst, 'wb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001276 os.fchmod(f.fileno(), perm)
1277 total_size = int(h.headers.get('Content-Length'))
1278 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001279
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001280 while True:
1281 data = h.read(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +08001282 if not data:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001283 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001284 downloaded_size += len(data)
Yilin Yang14d02a22019-11-01 11:32:03 +08001285 pbar.SetProgress(downloaded_size * 100 / total_size,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001286 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001287 f.write(data)
1288 pbar.End()
1289
1290 # Use find to get a listing of all files under a root directory. The 'stat'
1291 # command is used to retrieve the filename and it's filemode.
1292 output = self.CheckOutput(
1293 'cd $HOME; '
1294 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001295 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1296 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001297 % {'src': args.src})
1298
1299 # We got error from the stat command
1300 if output.startswith('stat: '):
1301 sys.stderr.write(output)
1302 return
1303
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001304 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001305 common_prefix = os.path.dirname(args.src)
1306
1307 if len(entries) == 1:
1308 entry = entries[0]
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001309 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001310 if os.path.isdir(args.dst):
1311 dst = os.path.join(args.dst, os.path.basename(src_path))
1312 else:
1313 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001314 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001315 else:
1316 if not os.path.exists(args.dst):
1317 common_prefix = args.src
1318
1319 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001320 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001321 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001322 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1323 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001324
1325 @Command('forward', 'forward remote port to local port', [
1326 Arg('--list', dest='list_all', action='store_true', default=False,
1327 help='list all port forwarding sessions'),
1328 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1329 default=None,
1330 help='remove port forwarding for local port LOCAL_PORT'),
1331 Arg('--remove-all', dest='remove_all', action='store_true',
1332 default=False, help='remove all port forwarding'),
1333 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1334 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1335 def Forward(self, args):
1336 if args.list_all:
1337 max_len = 10
Peter Shihf84a8972017-06-19 15:18:24 +08001338 if self._state.forwards:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001339 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1340
1341 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1342 for local in sorted(self._state.forwards.keys()):
1343 value = self._state.forwards[local]
1344 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1345 return
1346
1347 if args.remove_all:
1348 self._server.RemoveAllForward()
1349 return
1350
1351 if args.remove:
1352 self._server.RemoveForward(args.remove)
1353 return
1354
1355 self.CheckClient()
1356
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001357 if args.remote is None:
1358 raise RuntimeError('remote port not specified')
1359
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001360 if args.local is None:
1361 args.local = args.remote
1362 remote = int(args.remote)
1363 local = int(args.local)
1364
1365 def HandleConnection(conn):
1366 headers = []
1367 if self._state.username is not None and self._state.password is not None:
1368 headers.append(BasicAuthHeader(self._state.username,
1369 self._state.password))
1370
1371 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1372 ws = ForwarderWebSocketClient(
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001373 self._state, conn,
Peter Shihe03450b2017-06-14 14:02:08 +08001374 scheme + '%s:%d/api/agent/forward/%s?port=%d' % (
1375 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001376 urllib.parse.quote(self._selected_mid), remote),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001377 headers=headers)
1378 try:
1379 ws.connect()
1380 ws.run()
1381 except Exception as e:
1382 print('error: %s' % e)
1383 finally:
1384 ws.close()
1385
1386 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1387 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1388 server.bind(('0.0.0.0', local))
1389 server.listen(5)
1390
1391 pid = os.fork()
1392 if pid == 0:
1393 while True:
1394 conn, unused_addr = server.accept()
1395 t = threading.Thread(target=HandleConnection, args=(conn,))
1396 t.daemon = True
1397 t.start()
1398 else:
1399 self._server.AddForward(self._selected_mid, remote, local, pid)
1400
1401
1402def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001403 # Setup logging format
1404 logger = logging.getLogger()
Stimim Chena30447c2020-10-06 10:04:00 +08001405 logger.setLevel(logging.DEBUG)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001406 handler = logging.StreamHandler()
1407 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1408 handler.setFormatter(formatter)
1409 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001410
1411 # Add DaemonState to JSONRPC lib classes
Stimim Chena30447c2020-10-06 10:04:00 +08001412 config.DEFAULT.classes.add(DaemonState)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001413
1414 ovl = OverlordCLIClient()
1415 try:
1416 ovl.Main()
1417 except KeyboardInterrupt:
1418 print('Ctrl-C received, abort')
1419 except Exception as e:
Stimim Chena30447c2020-10-06 10:04:00 +08001420 logging.exception('exit with error [%s]', e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001421
1422
1423if __name__ == '__main__':
1424 main()