blob: df7eaef1fd8170567835ad1a007622755e1b79cb [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 Yang77668412021-02-02 10:53:36 +080043from cros.factory.utils import net_utils
Yilin Yang83c8f442020-05-05 13:46:51 +080044from cros.factory.utils import process_utils
45
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080046
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080047_CERT_DIR = os.path.expanduser('~/.config/ovl')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080048
49_ESCAPE = '~'
50_BUFSIZ = 8192
51_OVERLORD_PORT = 4455
52_OVERLORD_HTTP_PORT = 9000
53_OVERLORD_CLIENT_DAEMON_PORT = 4488
54_OVERLORD_CLIENT_DAEMON_RPC_ADDR = ('127.0.0.1', _OVERLORD_CLIENT_DAEMON_PORT)
55
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080056_CONNECT_TIMEOUT = 3
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080057_DEFAULT_HTTP_TIMEOUT = 30
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080058_LIST_CACHE_TIMEOUT = 2
59_DEFAULT_TERMINAL_WIDTH = 80
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080060_RETRY_TIMES = 3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080061
62# echo -n overlord | md5sum
63_HTTP_BOUNDARY_MAGIC = '9246f080c855a69012707ab53489b921'
64
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080065# Terminal resize control
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080066_CONTROL_START = 128
67_CONTROL_END = 129
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080068
69# Stream control
70_STDIN_CLOSED = '##STDIN_CLOSED##'
71
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080072_SSH_CONTROL_SOCKET_PREFIX = os.path.join(tempfile.gettempdir(),
73 'ovl-ssh-control-')
74
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080075_TLS_CERT_FAILED_WARNING = """
76@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
77@ WARNING: REMOTE HOST VERIFICATION HAS FAILED! @
78@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Yilin Yang3db92cc2020-10-26 15:31:47 +080079Failed Reason: %s.
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080080
Yilin Yang3db92cc2020-10-26 15:31:47 +080081Please use -c option to specify path of root CA certificate.
82This root CA certificate should be the one that signed the certificate used by
83overlord server."""
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080084
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080085
86def GetVersionDigest():
87 """Return the sha1sum of the current executing script."""
Wei-Ning Huang31609662016-08-11 00:22:25 +080088 # Check python script by default
89 filename = __file__
90
91 # If we are running from a frozen binary, we should calculate the checksum
92 # against that binary instead of the python script.
93 # See: https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
94 if getattr(sys, 'frozen', False):
95 filename = sys.executable
96
Yilin Yang0412c272019-12-05 16:57:40 +080097 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080098 return hashlib.sha1(f.read()).hexdigest()
99
100
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800101def GetTLSCertPath(host):
102 return os.path.join(_CERT_DIR, '%s.cert' % host)
103
104
105def UrlOpen(state, url):
Yilin Yangf54fb912020-01-08 11:42:38 +0800106 """Wrapper for urllib.request.urlopen.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800107
108 It selects correct HTTP scheme according to self._state.ssl, add HTTP
109 basic auth headers, and add specify correct SSL context.
110 """
111 url = MakeRequestUrl(state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +0800112 request = urllib.request.Request(url)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800113 if state.username is not None and state.password is not None:
114 request.add_header(*BasicAuthHeader(state.username, state.password))
Yilin Yangf54fb912020-01-08 11:42:38 +0800115 return urllib.request.urlopen(request, timeout=_DEFAULT_HTTP_TIMEOUT,
116 context=state.ssl_context)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800117
118
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800119def KillGraceful(pid, wait_secs=1):
120 """Kill a process gracefully by first sending SIGTERM, wait for some time,
121 then send SIGKILL to make sure it's killed."""
122 try:
123 os.kill(pid, signal.SIGTERM)
124 time.sleep(wait_secs)
125 os.kill(pid, signal.SIGKILL)
126 except OSError:
127 pass
128
129
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800130def AutoRetry(action_name, retries):
131 """Decorator for retry function call."""
132 def Wrap(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800133 @functools.wraps(func)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800134 def Loop(*args, **kwargs):
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800135 for unused_i in range(retries):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800136 try:
137 func(*args, **kwargs)
138 except Exception as e:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800139 print('error: %s: %s: retrying ...' % (args[0], e))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800140 else:
141 break
142 else:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800143 print('error: failed to %s %s' % (action_name, args[0]))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800144 return Loop
145 return Wrap
146
147
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800148def BasicAuthHeader(user, password):
149 """Return HTTP basic auth header."""
Stimim Chena30447c2020-10-06 10:04:00 +0800150 credential = base64.b64encode(
151 b'%s:%s' % (user.encode('utf-8'), password.encode('utf-8')))
152 return ('Authorization', 'Basic %s' % credential.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800153
154
155def GetTerminalSize():
156 """Retrieve terminal window size."""
157 ws = struct.pack('HHHH', 0, 0, 0, 0)
158 ws = fcntl.ioctl(0, termios.TIOCGWINSZ, ws)
159 lines, columns, unused_x, unused_y = struct.unpack('HHHH', ws)
160 return lines, columns
161
162
163def MakeRequestUrl(state, url):
164 return 'http%s://%s' % ('s' if state.ssl else '', url)
165
166
Fei Shaobd07c9a2020-06-15 19:04:50 +0800167class ProgressBar:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800168 SIZE_WIDTH = 11
169 SPEED_WIDTH = 10
170 DURATION_WIDTH = 6
171 PERCENTAGE_WIDTH = 8
172
173 def __init__(self, name):
174 self._start_time = time.time()
175 self._name = name
176 self._size = 0
177 self._width = 0
178 self._name_width = 0
179 self._name_max = 0
180 self._stat_width = 0
181 self._max = 0
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800182 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800183 self.SetProgress(0)
184
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800185 def _CalculateSize(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800186 self._width = GetTerminalSize()[1] or _DEFAULT_TERMINAL_WIDTH
187 self._name_width = int(self._width * 0.3)
188 self._name_max = self._name_width
189 self._stat_width = self.SIZE_WIDTH + self.SPEED_WIDTH + self.DURATION_WIDTH
190 self._max = (self._width - self._name_width - self._stat_width -
191 self.PERCENTAGE_WIDTH)
192
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800193 def _SizeToHuman(self, size_in_bytes):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800194 if size_in_bytes < 1024:
195 unit = 'B'
196 value = size_in_bytes
197 elif size_in_bytes < 1024 ** 2:
198 unit = 'KiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800199 value = size_in_bytes / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800200 elif size_in_bytes < 1024 ** 3:
201 unit = 'MiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800202 value = size_in_bytes / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800203 elif size_in_bytes < 1024 ** 4:
204 unit = 'GiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800205 value = size_in_bytes / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800206 return ' %6.1f %3s' % (value, unit)
207
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800208 def _SpeedToHuman(self, speed_in_bs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800209 if speed_in_bs < 1024:
210 unit = 'B'
211 value = speed_in_bs
212 elif speed_in_bs < 1024 ** 2:
213 unit = 'K'
Yilin Yang14d02a22019-11-01 11:32:03 +0800214 value = speed_in_bs / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800215 elif speed_in_bs < 1024 ** 3:
216 unit = 'M'
Yilin Yang14d02a22019-11-01 11:32:03 +0800217 value = speed_in_bs / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800218 elif speed_in_bs < 1024 ** 4:
219 unit = 'G'
Yilin Yang14d02a22019-11-01 11:32:03 +0800220 value = speed_in_bs / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800221 return ' %6.1f%s/s' % (value, unit)
222
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800223 def _DurationToClock(self, duration):
Yilin Yang14d02a22019-11-01 11:32:03 +0800224 return ' %02d:%02d' % (duration // 60, duration % 60)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800225
226 def SetProgress(self, percentage, size=None):
227 current_width = GetTerminalSize()[1]
228 if self._width != current_width:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800229 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800230
231 if size is not None:
232 self._size = size
233
234 elapse_time = time.time() - self._start_time
Yilin Yang14d02a22019-11-01 11:32:03 +0800235 speed = self._size / elapse_time
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800236
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800237 size_str = self._SizeToHuman(self._size)
238 speed_str = self._SpeedToHuman(speed)
239 elapse_str = self._DurationToClock(elapse_time)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800240
241 width = int(self._max * percentage / 100.0)
242 sys.stdout.write(
243 '%*s' % (- self._name_max,
244 self._name if len(self._name) <= self._name_max else
245 self._name[:self._name_max - 4] + ' ...') +
246 size_str + speed_str + elapse_str +
247 ((' [' + '#' * width + ' ' * (self._max - width) + ']' +
248 '%4d%%' % int(percentage)) if self._max > 2 else '') + '\r')
249 sys.stdout.flush()
250
251 def End(self):
252 self.SetProgress(100.0)
253 sys.stdout.write('\n')
254 sys.stdout.flush()
255
256
Fei Shaobd07c9a2020-06-15 19:04:50 +0800257class DaemonState:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800258 """DaemonState is used for storing Overlord state info."""
259 def __init__(self):
260 self.version_sha1sum = GetVersionDigest()
261 self.host = None
262 self.port = None
263 self.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800264 self.ssl_self_signed = False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800265 self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800266 self.ssh = False
267 self.orig_host = None
268 self.ssh_pid = None
269 self.username = None
270 self.password = None
271 self.selected_mid = None
272 self.forwards = {}
273 self.listing = []
274 self.last_list = 0
275
276
Fei Shaobd07c9a2020-06-15 19:04:50 +0800277class OverlordClientDaemon:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800278 """Overlord Client Daemon."""
279 def __init__(self):
Stimim Chena30447c2020-10-06 10:04:00 +0800280 # Use full module path for jsonrpclib to resolve.
281 import cros.factory.tools.ovl
282 self._state = cros.factory.tools.ovl.DaemonState()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800283 self._server = None
284
285 def Start(self):
286 self.StartRPCServer()
287
288 def StartRPCServer(self):
289 self._server = SimpleJSONRPCServer(_OVERLORD_CLIENT_DAEMON_RPC_ADDR,
290 logRequests=False)
291 exports = [
292 (self.State, 'State'),
293 (self.Ping, 'Ping'),
294 (self.GetPid, 'GetPid'),
295 (self.Connect, 'Connect'),
296 (self.Clients, 'Clients'),
297 (self.SelectClient, 'SelectClient'),
298 (self.AddForward, 'AddForward'),
299 (self.RemoveForward, 'RemoveForward'),
300 (self.RemoveAllForward, 'RemoveAllForward'),
301 ]
302 for func, name in exports:
303 self._server.register_function(func, name)
304
305 pid = os.fork()
306 if pid == 0:
Peter Shih4d55ded2017-07-03 17:19:01 +0800307 for fd in range(3):
308 os.close(fd)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800309 self._server.serve_forever()
310
311 @staticmethod
312 def GetRPCServer():
313 """Returns the Overlord client daemon RPC server."""
314 server = jsonrpclib.Server('http://%s:%d' %
315 _OVERLORD_CLIENT_DAEMON_RPC_ADDR)
316 try:
317 server.Ping()
318 except Exception:
319 return None
320 return server
321
322 def State(self):
323 return self._state
324
325 def Ping(self):
326 return True
327
328 def GetPid(self):
329 return os.getpid()
330
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800331 def _GetJSON(self, path):
332 url = '%s:%d%s' % (self._state.host, self._state.port, path)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800333 return json.loads(UrlOpen(self._state, url).read())
334
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800335 def _TLSEnabled(self):
336 """Determine if TLS is enabled on given server address."""
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800337 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
338 try:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800339 # Allow any certificate since we only want to check if server talks TLS.
340 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
341 context.verify_mode = ssl.CERT_NONE
342
343 sock = context.wrap_socket(sock, server_hostname=self._state.host)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800344 sock.settimeout(_CONNECT_TIMEOUT)
345 sock.connect((self._state.host, self._state.port))
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800346 return True
Wei-Ning Huangecc80b82016-07-01 16:55:10 +0800347 except ssl.SSLError:
348 return False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800349 except socket.error: # Connect refused or timeout
350 raise
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800351 except Exception:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800352 return False # For whatever reason above failed, assume False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800353
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800354 def _CheckTLSCertificate(self, check_hostname=True):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800355 """Check TLS certificate.
356
357 Returns:
358 A tupple (check_result, if_certificate_is_loaded)
359 """
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800360 def _DoConnect(context):
361 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
362 try:
363 sock.settimeout(_CONNECT_TIMEOUT)
364 sock = context.wrap_socket(sock, server_hostname=self._state.host)
365 sock.connect((self._state.host, self._state.port))
366 except ssl.SSLError:
367 return False
368 finally:
369 sock.close()
370
371 # Save SSLContext for future use.
372 self._state.ssl_context = context
373 return True
374
375 # First try connect with built-in certificates
376 tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
377 if _DoConnect(tls_context):
378 return True
379
380 # Try with already saved certificate, if any.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800381 tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
382 tls_context.verify_mode = ssl.CERT_REQUIRED
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800383 tls_context.check_hostname = check_hostname
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800384
385 tls_cert_path = GetTLSCertPath(self._state.host)
386 if os.path.exists(tls_cert_path):
387 tls_context.load_verify_locations(tls_cert_path)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800388 self._state.ssl_self_signed = True
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800389
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800390 return _DoConnect(tls_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800391
392 def Connect(self, host, port=_OVERLORD_HTTP_PORT, ssh_pid=None,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800393 username=None, password=None, orig_host=None,
394 check_hostname=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800395 self._state.username = username
396 self._state.password = password
397 self._state.host = host
398 self._state.port = port
399 self._state.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800400 self._state.ssl_self_signed = False
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800401 self._state.orig_host = orig_host
402 self._state.ssh_pid = ssh_pid
403 self._state.selected_mid = None
404
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800405 tls_enabled = self._TLSEnabled()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800406 if tls_enabled:
Yilin Yang3db92cc2020-10-26 15:31:47 +0800407 if not os.path.exists(os.path.join(_CERT_DIR, '%s.cert' % host)):
408 return 'SSLCertificateNotExisted'
409
410 if not self._CheckTLSCertificate(check_hostname):
411 return 'SSLVerifyFailed'
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800412
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800413 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800414 self._state.ssl = tls_enabled
415 UrlOpen(self._state, '%s:%d' % (host, port))
Yilin Yangf54fb912020-01-08 11:42:38 +0800416 except urllib.error.HTTPError as e:
Yilin Yang3db92cc2020-10-26 15:31:47 +0800417 return ('HTTPError', e.getcode(), str(e),
418 e.read().strip().decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800419 except Exception as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800420 return str(e)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800421 else:
422 return True
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800423
424 def Clients(self):
425 if time.time() - self._state.last_list <= _LIST_CACHE_TIMEOUT:
426 return self._state.listing
427
Peter Shihcf0f3b22017-06-19 15:59:22 +0800428 self._state.listing = self._GetJSON('/api/agents/list')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800429 self._state.last_list = time.time()
430 return self._state.listing
431
432 def SelectClient(self, mid):
433 self._state.selected_mid = mid
434
435 def AddForward(self, mid, remote, local, pid):
436 self._state.forwards[local] = (mid, remote, pid)
437
438 def RemoveForward(self, local_port):
439 try:
440 unused_mid, unused_remote, pid = self._state.forwards[local_port]
441 KillGraceful(pid)
442 del self._state.forwards[local_port]
443 except (KeyError, OSError):
444 pass
445
446 def RemoveAllForward(self):
447 for unused_mid, unused_remote, pid in self._state.forwards.values():
448 try:
449 KillGraceful(pid)
450 except OSError:
451 pass
452 self._state.forwards = {}
453
454
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800455class SSLEnabledWebSocketBaseClient(WebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800456 def __init__(self, state, *args, **kwargs):
457 cafile = ssl.get_default_verify_paths().openssl_cafile
458 # For some system / distribution, python can not detect system cafile path.
459 # In such case we fallback to the default path.
460 if not os.path.exists(cafile):
461 cafile = '/etc/ssl/certs/ca-certificates.crt'
462
463 if state.ssl_self_signed:
464 cafile = GetTLSCertPath(state.host)
465
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800466 ssl_options = {
467 'cert_reqs': ssl.CERT_REQUIRED,
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800468 'ca_certs': cafile
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800469 }
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800470 # ws4py does not allow you to specify SSLContext, but rather passing in the
471 # argument of ssl.wrap_socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800472 super(SSLEnabledWebSocketBaseClient, self).__init__(
473 ssl_options=ssl_options, *args, **kwargs)
474
475
476class TerminalWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800477 def __init__(self, state, mid, escape, *args, **kwargs):
478 super(TerminalWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800479 self._mid = mid
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800480 self._escape = escape
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800481 self._stdin_fd = sys.stdin.fileno()
482 self._old_termios = None
483
484 def handshake_ok(self):
485 pass
486
487 def opened(self):
488 nonlocals = {'size': (80, 40)}
489
490 def _ResizeWindow():
491 size = GetTerminalSize()
492 if size != nonlocals['size']: # Size not changed, ignore
493 control = {'command': 'resize', 'params': list(size)}
494 payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END)
495 nonlocals['size'] = size
496 try:
497 self.send(payload, binary=True)
498 except Exception:
499 pass
500
501 def _FeedInput():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800502 self._old_termios = termios.tcgetattr(self._stdin_fd)
503 tty.setraw(self._stdin_fd)
504
505 READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3)
506
507 try:
508 state = READY
509 while True:
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800510 # Check if terminal is resized
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800511 _ResizeWindow()
512
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800513 ch = sys.stdin.read(1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800514
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800515 # Scan for escape sequence
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800516 if self._escape:
517 if state == READY:
518 state = ENTER_PRESSED if ch == chr(0x0d) else READY
519 elif state == ENTER_PRESSED:
520 state = ESCAPE_PRESSED if ch == self._escape else READY
521 elif state == ESCAPE_PRESSED:
522 if ch == '.':
523 self.close()
524 break
525 else:
526 state = READY
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800527
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800528 self.send(ch)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800529 except (KeyboardInterrupt, RuntimeError):
530 pass
531
532 t = threading.Thread(target=_FeedInput)
533 t.daemon = True
534 t.start()
535
536 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800537 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800538 termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios)
539 print('Connection to %s closed.' % self._mid)
540
Yilin Yangf64670b2020-01-06 11:22:18 +0800541 def received_message(self, message):
542 if message.is_binary:
Stimim Chena30447c2020-10-06 10:04:00 +0800543 sys.stdout.write(message.data.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800544 sys.stdout.flush()
545
546
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800547class ShellWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800548 def __init__(self, state, output, *args, **kwargs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800549 """Constructor.
550
551 Args:
552 output: output file object.
553 """
554 self.output = output
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800555 super(ShellWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800556
557 def handshake_ok(self):
558 pass
559
560 def opened(self):
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800561 def _FeedInput():
562 try:
563 while True:
Stimim Chena30447c2020-10-06 10:04:00 +0800564 data = sys.stdin.buffer.read(1)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800565
Peter Shihf84a8972017-06-19 15:18:24 +0800566 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800567 self.send(_STDIN_CLOSED * 2)
568 break
569 self.send(data, binary=True)
570 except (KeyboardInterrupt, RuntimeError):
571 pass
572
573 t = threading.Thread(target=_FeedInput)
574 t.daemon = True
575 t.start()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800576
577 def closed(self, code, reason=None):
578 pass
579
Yilin Yangf64670b2020-01-06 11:22:18 +0800580 def received_message(self, message):
581 if message.is_binary:
Stimim Chena30447c2020-10-06 10:04:00 +0800582 self.output.write(message.data.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800583 self.output.flush()
584
585
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800586class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800587 def __init__(self, state, sock, *args, **kwargs):
588 super(ForwarderWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800589 self._sock = sock
590 self._stop = threading.Event()
591
592 def handshake_ok(self):
593 pass
594
595 def opened(self):
596 def _FeedInput():
597 try:
598 self._sock.setblocking(False)
599 while True:
600 rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5)
601 if self._stop.is_set():
602 break
603 if self._sock in rd:
604 data = self._sock.recv(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +0800605 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800606 self.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800607 break
608 self.send(data, binary=True)
609 except Exception:
610 pass
611 finally:
612 self._sock.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800613
614 t = threading.Thread(target=_FeedInput)
615 t.daemon = True
616 t.start()
617
618 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800619 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800620 self._stop.set()
621 sys.exit(0)
622
Yilin Yangf64670b2020-01-06 11:22:18 +0800623 def received_message(self, message):
624 if message.is_binary:
625 self._sock.send(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800626
627
628def Arg(*args, **kwargs):
629 return (args, kwargs)
630
631
632def Command(command, help_msg=None, args=None):
633 """Decorator for adding argparse parameter for a method."""
634 if args is None:
635 args = []
636 def WrapFunc(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800637 @functools.wraps(func)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800638 def Wrapped(*args, **kwargs):
639 return func(*args, **kwargs)
Peter Shih99b73ec2017-06-16 17:54:15 +0800640 # pylint: disable=protected-access
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800641 Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args}
642 return Wrapped
643 return WrapFunc
644
645
646def ParseMethodSubCommands(cls):
647 """Decorator for a class using the @Command decorator.
648
649 This decorator retrieve command info from each method and append it in to the
650 SUBCOMMANDS class variable, which is later used to construct parser.
651 """
Yilin Yang879fbda2020-05-14 13:52:30 +0800652 for unused_key, method in cls.__dict__.items():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800653 if hasattr(method, '__arg_attr'):
Peter Shih99b73ec2017-06-16 17:54:15 +0800654 # pylint: disable=protected-access
655 cls.SUBCOMMANDS.append(method.__arg_attr)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800656 return cls
657
658
659@ParseMethodSubCommands
Fei Shaobd07c9a2020-06-15 19:04:50 +0800660class OverlordCLIClient:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800661 """Overlord command line interface client."""
662
663 SUBCOMMANDS = []
664
665 def __init__(self):
666 self._parser = self._BuildParser()
667 self._selected_mid = None
668 self._server = None
669 self._state = None
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800670 self._escape = None
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800671
672 def _BuildParser(self):
673 root_parser = argparse.ArgumentParser(prog='ovl')
Cheng Yueh1d686a72021-03-10 11:09:42 +0800674 subparsers = root_parser.add_subparsers(title='subcommands',
675 dest='subcommand')
676 subparsers.required = True
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800677
678 root_parser.add_argument('-s', dest='selected_mid', action='store',
679 default=None,
680 help='select target to execute command on')
681 root_parser.add_argument('-S', dest='select_mid_before_action',
682 action='store_true', default=False,
683 help='select target before executing command')
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800684 root_parser.add_argument('-e', dest='escape', metavar='ESCAPE_CHAR',
685 action='store', default=_ESCAPE, type=str,
686 help='set shell escape character, \'none\' to '
687 'disable escape completely')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800688
689 for attr in self.SUBCOMMANDS:
690 parser = subparsers.add_parser(attr['command'], help=attr['help'])
691 parser.set_defaults(which=attr['command'])
692 for arg in attr['args']:
693 parser.add_argument(*arg[0], **arg[1])
694
695 return root_parser
696
697 def Main(self):
698 # We want to pass the rest of arguments after shell command directly to the
699 # function without parsing it.
700 try:
701 index = sys.argv.index('shell')
702 except ValueError:
703 args = self._parser.parse_args()
704 else:
705 args = self._parser.parse_args(sys.argv[1:index + 1])
706
707 command = args.which
708 self._selected_mid = args.selected_mid
709
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800710 if args.escape and args.escape != 'none':
711 self._escape = args.escape[0]
712
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800713 if command == 'start-server':
714 self.StartServer()
715 return
Fei Shao12ecf382020-06-23 18:32:26 +0800716 if command == 'kill-server':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800717 self.KillServer()
718 return
719
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800720 self.CheckDaemon()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800721 if command == 'status':
722 self.Status()
723 return
Fei Shao12ecf382020-06-23 18:32:26 +0800724 if command == 'connect':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800725 self.Connect(args)
726 return
727
728 # The following command requires connection to the server
729 self.CheckConnection()
730
731 if args.select_mid_before_action:
732 self.SelectClient(store=False)
733
734 if command == 'select':
735 self.SelectClient(args)
736 elif command == 'ls':
Peter Shih99b73ec2017-06-16 17:54:15 +0800737 self.ListClients(args)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800738 elif command == 'shell':
739 command = sys.argv[sys.argv.index('shell') + 1:]
740 self.Shell(command)
741 elif command == 'push':
742 self.Push(args)
743 elif command == 'pull':
744 self.Pull(args)
745 elif command == 'forward':
746 self.Forward(args)
747
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800748 def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None):
749 """Perform HTTP POST and upload file to Overlord.
750
751 To minimize the external dependencies, we construct the HTTP post request
752 by ourselves.
753 """
754 url = MakeRequestUrl(self._state, url)
755 size = os.stat(filename).st_size
756 boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC
757 CRLF = '\r\n'
Yilin Yangf54fb912020-01-08 11:42:38 +0800758 parse = urllib.parse.urlparse(url)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800759
760 part_headers = [
761 '--' + boundary,
762 'Content-Disposition: form-data; name="file"; '
763 'filename="%s"' % os.path.basename(filename),
764 'Content-Type: application/octet-stream',
765 '', ''
766 ]
767 part_header = CRLF.join(part_headers)
768 end_part = CRLF + '--' + boundary + '--' + CRLF
769
770 content_length = len(part_header) + size + len(end_part)
771 if parse.scheme == 'http':
Stimim Chena30447c2020-10-06 10:04:00 +0800772 h = http.client.HTTPConnection(parse.netloc)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800773 else:
Stimim Chena30447c2020-10-06 10:04:00 +0800774 h = http.client.HTTPSConnection(parse.netloc,
775 context=self._state.ssl_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800776
777 post_path = url[url.index(parse.netloc) + len(parse.netloc):]
778 h.putrequest('POST', post_path)
779 h.putheader('Content-Length', content_length)
780 h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
781
782 if user and passwd:
783 h.putheader(*BasicAuthHeader(user, passwd))
784 h.endheaders()
Stimim Chena30447c2020-10-06 10:04:00 +0800785 h.send(part_header.encode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800786
787 count = 0
Stimim Chena30447c2020-10-06 10:04:00 +0800788 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800789 while True:
790 data = f.read(_BUFSIZ)
791 if not data:
792 break
793 count += len(data)
794 if progress:
Yilin Yang14d02a22019-11-01 11:32:03 +0800795 progress(count * 100 // size, count)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800796 h.send(data)
797
Stimim Chena30447c2020-10-06 10:04:00 +0800798 h.send(end_part.encode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800799 progress(100)
800
801 if count != size:
802 logging.warning('file changed during upload, upload may be truncated.')
803
Stimim Chena30447c2020-10-06 10:04:00 +0800804 resp = h.getresponse()
805 return resp.status == 200
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800806
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800807 def CheckDaemon(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800808 self._server = OverlordClientDaemon.GetRPCServer()
809 if self._server is None:
810 print('* daemon not running, starting it now on port %d ... *' %
811 _OVERLORD_CLIENT_DAEMON_PORT)
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800812 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800813
814 self._state = self._server.State()
815 sha1sum = GetVersionDigest()
816
817 if sha1sum != self._state.version_sha1sum:
818 print('ovl server is out of date. killing...')
819 KillGraceful(self._server.GetPid())
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800820 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800821
822 def GetSSHControlFile(self, host):
823 return _SSH_CONTROL_SOCKET_PREFIX + host
824
825 def SSHTunnel(self, user, host, port):
826 """SSH forward the remote overlord server.
827
828 Overlord server may not have port 9000 open to the public network, in such
829 case we can SSH forward the port to localhost.
830 """
831
832 control_file = self.GetSSHControlFile(host)
833 try:
834 os.unlink(control_file)
835 except Exception:
836 pass
837
838 subprocess.Popen([
Yilin Yang4e33eef2021-03-03 14:39:57 +0800839 'ssh', '-Nf', '-M', '-S', control_file, '-L', '9000:localhost:9000',
840 '-p',
841 str(port),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800842 '%s%s' % (user + '@' if user else '', host)
843 ]).wait()
844
Yilin Yang83c8f442020-05-05 13:46:51 +0800845 p = process_utils.Spawn([
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800846 'ssh',
847 '-S', control_file,
848 '-O', 'check', host,
Yilin Yang83c8f442020-05-05 13:46:51 +0800849 ], read_stderr=True, ignore_stdout=True)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800850
Yilin Yang83c8f442020-05-05 13:46:51 +0800851 s = re.search(r'pid=(\d+)', p.stderr_data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800852 if s:
853 return int(s.group(1))
854
855 raise RuntimeError('can not establish ssh connection')
856
857 def CheckConnection(self):
858 if self._state.host is None:
859 raise RuntimeError('not connected to any server, abort')
860
861 try:
862 self._server.Clients()
863 except Exception:
864 raise RuntimeError('remote server disconnected, abort')
865
866 if self._state.ssh_pid is not None:
867 ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)],
868 stdout=subprocess.PIPE,
869 stderr=subprocess.PIPE).wait()
870 if ret != 0:
871 raise RuntimeError('ssh tunnel disconnected, please re-connect')
872
873 def CheckClient(self):
874 if self._selected_mid is None:
875 if self._state.selected_mid is None:
876 raise RuntimeError('No client is selected')
877 self._selected_mid = self._state.selected_mid
878
Peter Shihcf0f3b22017-06-19 15:59:22 +0800879 if not any(client['mid'] == self._selected_mid
880 for client in self._server.Clients()):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800881 raise RuntimeError('client %s disappeared' % self._selected_mid)
882
883 def CheckOutput(self, command):
884 headers = []
885 if self._state.username is not None and self._state.password is not None:
886 headers.append(BasicAuthHeader(self._state.username,
887 self._state.password))
888
889 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Yilin Yang8d4f9d02019-11-28 17:12:11 +0800890 sio = StringIO()
Peter Shihe03450b2017-06-14 14:02:08 +0800891 ws = ShellWebSocketClient(
892 self._state, sio, scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
893 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +0800894 urllib.parse.quote(self._selected_mid),
895 urllib.parse.quote(command)),
Peter Shihe03450b2017-06-14 14:02:08 +0800896 headers=headers)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800897 ws.connect()
898 ws.run()
899 return sio.getvalue()
900
901 @Command('status', 'show Overlord connection status')
902 def Status(self):
903 if self._state.host is None:
904 print('Not connected to any host.')
905 else:
906 if self._state.ssh_pid is not None:
907 print('Connected to %s with SSH tunneling.' % self._state.orig_host)
908 else:
909 print('Connected to %s:%d.' % (self._state.host, self._state.port))
910
911 if self._selected_mid is None:
912 self._selected_mid = self._state.selected_mid
913
914 if self._selected_mid is None:
915 print('No client is selected.')
916 else:
917 print('Client %s selected.' % self._selected_mid)
918
919 @Command('connect', 'connect to Overlord server', [
920 Arg('host', metavar='HOST', type=str, default='localhost',
921 help='Overlord hostname/IP'),
Yilin Yang3db92cc2020-10-26 15:31:47 +0800922 Arg('port', metavar='PORT', type=int, default=_OVERLORD_HTTP_PORT,
923 help='Overlord port'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800924 Arg('-f', '--forward', dest='ssh_forward', default=False,
Yilin Yang3db92cc2020-10-26 15:31:47 +0800925 action='store_true', help='connect with SSH forwarding to the host'),
926 Arg('-p', '--ssh-port', dest='ssh_port', default=22, type=int,
927 help='SSH server port for SSH forwarding'),
928 Arg('-l', '--ssh-login', dest='ssh_login', default='', type=str,
929 help='SSH server login name for SSH forwarding'),
930 Arg('-u', '--user', dest='user', default=None, type=str,
931 help='Overlord HTTP auth username'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800932 Arg('-w', '--passwd', dest='passwd', default=None, type=str,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800933 help='Overlord HTTP auth password'),
Yilin Yang3db92cc2020-10-26 15:31:47 +0800934 Arg('-c', '--root-CA', dest='cert', default=None, type=str,
935 help='Path to root CA certificate, only assign at the first time'),
936 Arg('-i', '--no-check-hostname', dest='check_hostname', default=True,
Yilin Yang77668412021-02-02 10:53:36 +0800937 action='store_false', help='Ignore SSL cert hostname check'),
938 Arg('-b', '--certificate-dir', dest='certificate_dir', default=None,
939 type=str, help='Path to overlord certificate directory')
Yilin Yang3db92cc2020-10-26 15:31:47 +0800940 ])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800941 def Connect(self, args):
942 ssh_pid = None
943 host = args.host
944 orig_host = args.host
945
Yilin Yang77668412021-02-02 10:53:36 +0800946 if args.certificate_dir:
947 args.cert = os.path.join(args.certificate_dir, 'rootCA.pem')
948
949 ovl_password_file = os.path.join(args.certificate_dir, 'ovl_password')
950 with open(ovl_password_file, 'r') as f:
951 args.passwd = f.read().strip()
952 args.user = 'ovl'
953
Yilin Yang3db92cc2020-10-26 15:31:47 +0800954 if args.cert and os.path.exists(args.cert):
955 os.makedirs(_CERT_DIR, exist_ok=True)
956 shutil.copy(args.cert, os.path.join(_CERT_DIR, '%s.cert' % host))
957
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800958 if args.ssh_forward:
959 # Kill previous SSH tunnel
960 self.KillSSHTunnel()
961
962 ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port)
963 host = 'localhost'
964
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800965 username_provided = args.user is not None
966 password_provided = args.passwd is not None
967 prompt = False
968
Peter Shih533566a2018-09-05 17:48:03 +0800969 for unused_i in range(3): # pylint: disable=too-many-nested-blocks
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800970 try:
971 if prompt:
972 if not username_provided:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800973 args.user = input('Username: ')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800974 if not password_provided:
975 args.passwd = getpass.getpass('Password: ')
976
977 ret = self._server.Connect(host, args.port, ssh_pid, args.user,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800978 args.passwd, orig_host,
979 args.check_hostname)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800980 if isinstance(ret, list):
Fei Shao12ecf382020-06-23 18:32:26 +0800981 if ret[0] == 'HTTPError':
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800982 code, except_str, body = ret[1:]
983 if code == 401:
984 print('connect: %s' % body)
985 prompt = True
986 if not username_provided or not password_provided:
987 continue
Fei Shaof91ab8f2020-06-23 17:54:03 +0800988 break
Fei Shao0e4e2c62020-06-23 18:22:26 +0800989 logging.error('%s; %s', except_str, body)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800990
Yilin Yang3db92cc2020-10-26 15:31:47 +0800991 if ret in ('SSLCertificateNotExisted', 'SSLVerifyFailed'):
992 print(_TLS_CERT_FAILED_WARNING % ret)
993 return
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800994 if ret is not True:
995 print('can not connect to %s: %s' % (host, ret))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800996 else:
997 print('connection to %s:%d established.' % (host, args.port))
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800998 except Exception as e:
Yilin Yang3db92cc2020-10-26 15:31:47 +0800999 logging.exception(e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001000 else:
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001001 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001002
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001003 @Command('start-server', 'start overlord CLI client server')
1004 def StartServer(self):
1005 self._server = OverlordClientDaemon.GetRPCServer()
1006 if self._server is None:
1007 OverlordClientDaemon().Start()
1008 time.sleep(1)
1009 self._server = OverlordClientDaemon.GetRPCServer()
1010 if self._server is not None:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001011 print('* daemon started successfully *\n')
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001012
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001013 @Command('kill-server', 'kill overlord CLI client server')
1014 def KillServer(self):
1015 self._server = OverlordClientDaemon.GetRPCServer()
1016 if self._server is None:
1017 return
1018
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001019 self._state = self._server.State()
1020
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001021 # Kill SSH Tunnel
1022 self.KillSSHTunnel()
1023
1024 # Kill server daemon
1025 KillGraceful(self._server.GetPid())
1026
1027 def KillSSHTunnel(self):
1028 if self._state.ssh_pid is not None:
1029 KillGraceful(self._state.ssh_pid)
1030
Peter Shihcf0f3b22017-06-19 15:59:22 +08001031 def _FilterClients(self, clients, prop_filters, mid=None):
1032 def _ClientPropertiesMatch(client, key, regex):
1033 try:
1034 return bool(re.search(regex, client['properties'][key]))
1035 except KeyError:
1036 return False
1037
1038 for prop_filter in prop_filters:
1039 key, sep, regex = prop_filter.partition('=')
1040 if not sep:
1041 # The filter doesn't contains =.
1042 raise ValueError('Invalid filter condition %r' % filter)
1043 clients = [c for c in clients if _ClientPropertiesMatch(c, key, regex)]
1044
1045 if mid is not None:
1046 client = next((c for c in clients if c['mid'] == mid), None)
1047 if client:
1048 return [client]
1049 clients = [c for c in clients if c['mid'].startswith(mid)]
1050 return clients
1051
1052 @Command('ls', 'list clients', [
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001053 Arg(
1054 '-f', '--filter', default=[], dest='filters', action='append',
Peter Shihcf0f3b22017-06-19 15:59:22 +08001055 help=('Conditions to filter clients by properties. '
1056 'Should be in form "key=regex", where regex is the regular '
1057 'expression that should be found in the value. '
1058 'Multiple --filter arguments would be ANDed.')),
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001059 Arg('-m', '--mid-only', default=False, action='store_true',
1060 help='Print mid only.'),
Peter Shih99b73ec2017-06-16 17:54:15 +08001061 Arg('-v', '--verbose', default=False, action='store_true',
1062 help='Print properties of each client.')
1063 ])
1064 def ListClients(self, args):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001065 clients = self._FilterClients(self._server.Clients(), args.filters)
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001066
1067 if args.verbose:
1068 for client in clients:
Peter Shih99b73ec2017-06-16 17:54:15 +08001069 print(yaml.safe_dump(client, default_flow_style=False))
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001070 return
1071
1072 # Used in station_setup to ckeck if there is duplicate mid.
1073 if args.mid_only:
1074 for client in clients:
Peter Shihcf0f3b22017-06-19 15:59:22 +08001075 print(client['mid'])
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001076 return
1077
1078 def FormatPrint(length, string):
1079 print('%*s' % (length + 2, string), end='|')
1080
Yilin Yangdc347592021-02-03 11:33:04 +08001081 columns = [
1082 'mid', 'serial', 'status', 'pytest', 'model', 'ip', 'track_connection'
1083 ]
Yilin Yangf35f5cd2020-11-11 12:49:36 +08001084 columns_max_len = {column: len(column)
1085 for column in columns}
1086
1087 for client in clients:
1088 for column in columns:
1089 columns_max_len[column] = max(columns_max_len[column],
1090 len(str(client[column])))
1091
1092 for column in columns:
1093 FormatPrint(columns_max_len[column], column)
1094 print()
1095
1096 for client in clients:
1097 for column in columns:
1098 FormatPrint(columns_max_len[column], str(client[column]))
1099 print()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001100
1101 @Command('select', 'select default client', [
Peter Shihcf0f3b22017-06-19 15:59:22 +08001102 Arg('-f', '--filter', default=[], dest='filters', action='append',
1103 help=('Conditions to filter clients by properties. '
1104 'Should be in form "key=regex", where regex is the regular '
1105 'expression that should be found in the value. '
1106 'Multiple --filter arguments would be ANDed.')),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001107 Arg('mid', metavar='mid', nargs='?', default=None)])
1108 def SelectClient(self, args=None, store=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001109 mid = args.mid if args is not None else None
Wei-Ning Huang6796a132017-06-22 14:46:32 +08001110 filters = args.filters if args is not None else []
1111 clients = self._FilterClients(self._server.Clients(), filters, mid=mid)
Peter Shihcf0f3b22017-06-19 15:59:22 +08001112
1113 if not clients:
1114 raise RuntimeError('select: client not found')
Fei Shao0e4e2c62020-06-23 18:22:26 +08001115 if len(clients) == 1:
Peter Shihcf0f3b22017-06-19 15:59:22 +08001116 mid = clients[0]['mid']
1117 else:
1118 # This case would not happen when args.mid is specified.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001119 print('Select from the following clients:')
1120 for i, client in enumerate(clients):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001121 print(' %d. %s' % (i + 1, client['mid']))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001122
1123 print('\nSelection: ', end='')
1124 try:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001125 choice = int(input()) - 1
Peter Shihcf0f3b22017-06-19 15:59:22 +08001126 mid = clients[choice]['mid']
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001127 except ValueError:
1128 raise RuntimeError('select: invalid selection')
1129 except IndexError:
1130 raise RuntimeError('select: selection out of range')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001131
1132 self._selected_mid = mid
1133 if store:
1134 self._server.SelectClient(mid)
1135 print('Client %s selected' % mid)
1136
1137 @Command('shell', 'open a shell or execute a shell command', [
1138 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1139 def Shell(self, command=None):
1140 if command is None:
1141 command = []
1142 self.CheckClient()
1143
1144 headers = []
1145 if self._state.username is not None and self._state.password is not None:
1146 headers.append(BasicAuthHeader(self._state.username,
1147 self._state.password))
1148
1149 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Peter Shihe03450b2017-06-14 14:02:08 +08001150 if command:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001151 cmd = ' '.join(command)
Peter Shihe03450b2017-06-14 14:02:08 +08001152 ws = ShellWebSocketClient(
1153 self._state, sys.stdout,
1154 scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
1155 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001156 urllib.parse.quote(self._selected_mid), urllib.parse.quote(cmd)),
Peter Shihe03450b2017-06-14 14:02:08 +08001157 headers=headers)
1158 else:
1159 ws = TerminalWebSocketClient(
1160 self._state, self._selected_mid, self._escape,
1161 scheme + '%s:%d/api/agent/tty/%s' % (
1162 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001163 urllib.parse.quote(self._selected_mid)),
Peter Shihe03450b2017-06-14 14:02:08 +08001164 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001165 try:
1166 ws.connect()
1167 ws.run()
1168 except socket.error as e:
1169 if e.errno == 32: # Broken pipe
1170 pass
1171 else:
1172 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001173
1174 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001175 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001176 Arg('dst', metavar='DESTINATION')])
1177 def Push(self, args):
1178 self.CheckClient()
1179
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001180 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001181 def _push(src, dst):
1182 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001183
1184 # Local file is a link
1185 if os.path.islink(src):
1186 pbar = ProgressBar(src_base)
1187 link_path = os.readlink(src)
1188 self.CheckOutput('mkdir -p %(dirname)s; '
1189 'if [ -d "%(dst)s" ]; then '
1190 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1191 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1192 dict(dirname=os.path.dirname(dst),
1193 link_path=link_path, dst=dst,
1194 link_name=src_base))
1195 pbar.End()
1196 return
1197
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001198 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1199 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001200 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001201 urllib.parse.quote(self._selected_mid), dst, mode))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001202 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001203 UrlOpen(self._state, url + '&filename=%s' % src_base)
Yilin Yangf54fb912020-01-08 11:42:38 +08001204 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001205 msg = json.loads(e.read()).get('error', None)
1206 raise RuntimeError('push: %s' % msg)
1207
1208 pbar = ProgressBar(src_base)
1209 self._HTTPPostFile(url, src, pbar.SetProgress,
1210 self._state.username, self._state.password)
1211 pbar.End()
1212
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001213 def _push_single_target(src, dst):
1214 if os.path.isdir(src):
1215 dst_exists = ast.literal_eval(self.CheckOutput(
1216 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1217 for root, unused_x, files in os.walk(src):
1218 # If destination directory does not exist, we should strip the first
1219 # layer of directory. For example: src_dir contains a single file 'A'
1220 #
1221 # push src_dir dest_dir
1222 #
1223 # If dest_dir exists, the resulting directory structure should be:
1224 # dest_dir/src_dir/A
1225 # If dest_dir does not exist, the resulting directory structure should
1226 # be:
1227 # dest_dir/A
1228 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1229 for name in files:
1230 _push(os.path.join(root, name),
1231 os.path.join(dst, dst_root, name))
1232 else:
1233 _push(src, dst)
1234
1235 if len(args.srcs) > 1:
1236 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1237 '2>/dev/null' % args.dst).strip()
1238 if not dst_type:
1239 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1240 if dst_type != 'directory':
1241 raise RuntimeError('push: %s: Not a directory' % args.dst)
1242
1243 for src in args.srcs:
1244 if not os.path.exists(src):
1245 raise RuntimeError('push: can not stat "%s": no such file or directory'
1246 % src)
1247 if not os.access(src, os.R_OK):
1248 raise RuntimeError('push: can not open "%s" for reading' % src)
1249
1250 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001251
1252 @Command('pull', 'pull a file or directory from remote', [
1253 Arg('src', metavar='SOURCE'),
1254 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1255 def Pull(self, args):
1256 self.CheckClient()
1257
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001258 @AutoRetry('pull', _RETRY_TIMES)
Peter Shihe6afab32018-09-11 17:16:48 +08001259 def _pull(src, dst, ftype, perm=0o644, link=None):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001260 try:
1261 os.makedirs(os.path.dirname(dst))
1262 except Exception:
1263 pass
1264
1265 src_base = os.path.basename(src)
1266
1267 # Remote file is a link
1268 if ftype == 'l':
1269 pbar = ProgressBar(src_base)
1270 if os.path.exists(dst):
1271 os.remove(dst)
1272 os.symlink(link, dst)
1273 pbar.End()
1274 return
1275
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001276 url = ('%s:%d/api/agent/download/%s?filename=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001277 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001278 urllib.parse.quote(self._selected_mid), urllib.parse.quote(src)))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001279 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001280 h = UrlOpen(self._state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +08001281 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001282 msg = json.loads(e.read()).get('error', 'unkown error')
1283 raise RuntimeError('pull: %s' % msg)
1284 except KeyboardInterrupt:
1285 return
1286
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001287 pbar = ProgressBar(src_base)
Stimim Chena30447c2020-10-06 10:04:00 +08001288 with open(dst, 'wb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001289 os.fchmod(f.fileno(), perm)
1290 total_size = int(h.headers.get('Content-Length'))
1291 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001292
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001293 while True:
1294 data = h.read(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +08001295 if not data:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001296 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001297 downloaded_size += len(data)
Yilin Yang14d02a22019-11-01 11:32:03 +08001298 pbar.SetProgress(downloaded_size * 100 / total_size,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001299 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001300 f.write(data)
1301 pbar.End()
1302
1303 # Use find to get a listing of all files under a root directory. The 'stat'
1304 # command is used to retrieve the filename and it's filemode.
1305 output = self.CheckOutput(
1306 'cd $HOME; '
1307 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001308 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1309 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001310 % {'src': args.src})
1311
1312 # We got error from the stat command
1313 if output.startswith('stat: '):
1314 sys.stderr.write(output)
1315 return
1316
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001317 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001318 common_prefix = os.path.dirname(args.src)
1319
1320 if len(entries) == 1:
1321 entry = entries[0]
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001322 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001323 if os.path.isdir(args.dst):
1324 dst = os.path.join(args.dst, os.path.basename(src_path))
1325 else:
1326 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001327 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001328 else:
1329 if not os.path.exists(args.dst):
1330 common_prefix = args.src
1331
1332 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001333 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001334 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001335 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1336 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001337
1338 @Command('forward', 'forward remote port to local port', [
1339 Arg('--list', dest='list_all', action='store_true', default=False,
1340 help='list all port forwarding sessions'),
1341 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1342 default=None,
1343 help='remove port forwarding for local port LOCAL_PORT'),
1344 Arg('--remove-all', dest='remove_all', action='store_true',
1345 default=False, help='remove all port forwarding'),
1346 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1347 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1348 def Forward(self, args):
1349 if args.list_all:
1350 max_len = 10
Peter Shihf84a8972017-06-19 15:18:24 +08001351 if self._state.forwards:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001352 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1353
1354 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1355 for local in sorted(self._state.forwards.keys()):
1356 value = self._state.forwards[local]
1357 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1358 return
1359
1360 if args.remove_all:
1361 self._server.RemoveAllForward()
1362 return
1363
1364 if args.remove:
1365 self._server.RemoveForward(args.remove)
1366 return
1367
1368 self.CheckClient()
1369
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001370 if args.remote is None:
1371 raise RuntimeError('remote port not specified')
1372
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001373 if args.local is None:
Yilin Yang77668412021-02-02 10:53:36 +08001374 args.local = net_utils.FindUnusedPort()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001375 remote = int(args.remote)
1376 local = int(args.local)
1377
1378 def HandleConnection(conn):
1379 headers = []
1380 if self._state.username is not None and self._state.password is not None:
1381 headers.append(BasicAuthHeader(self._state.username,
1382 self._state.password))
1383
1384 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1385 ws = ForwarderWebSocketClient(
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001386 self._state, conn,
Peter Shihe03450b2017-06-14 14:02:08 +08001387 scheme + '%s:%d/api/agent/forward/%s?port=%d' % (
1388 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001389 urllib.parse.quote(self._selected_mid), remote),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001390 headers=headers)
1391 try:
1392 ws.connect()
1393 ws.run()
1394 except Exception as e:
1395 print('error: %s' % e)
1396 finally:
1397 ws.close()
1398
1399 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1400 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1401 server.bind(('0.0.0.0', local))
1402 server.listen(5)
1403
1404 pid = os.fork()
1405 if pid == 0:
1406 while True:
1407 conn, unused_addr = server.accept()
1408 t = threading.Thread(target=HandleConnection, args=(conn,))
1409 t.daemon = True
1410 t.start()
1411 else:
Yilin Yang77668412021-02-02 10:53:36 +08001412 print('ovl_forward_port: http://localhost:%d' % local)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001413 self._server.AddForward(self._selected_mid, remote, local, pid)
1414
1415
1416def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001417 # Setup logging format
1418 logger = logging.getLogger()
Stimim Chena30447c2020-10-06 10:04:00 +08001419 logger.setLevel(logging.DEBUG)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001420 handler = logging.StreamHandler()
1421 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1422 handler.setFormatter(formatter)
1423 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001424
1425 # Add DaemonState to JSONRPC lib classes
Stimim Chena30447c2020-10-06 10:04:00 +08001426 config.DEFAULT.classes.add(DaemonState)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001427
1428 ovl = OverlordCLIClient()
1429 try:
1430 ovl.Main()
1431 except KeyboardInterrupt:
1432 print('Ctrl-C received, abort')
1433 except Exception as e:
Stimim Chena30447c2020-10-06 10:04:00 +08001434 logging.exception('exit with error [%s]', e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001435
1436
1437if __name__ == '__main__':
1438 main()