blob: 717de43204b0b589d6c8183882ea87379220b0ff [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', [
1042 Arg('-f', '--filter', default=[], dest='filters', action='append',
1043 help=('Conditions to filter clients by properties. '
1044 'Should be in form "key=regex", where regex is the regular '
1045 'expression that should be found in the value. '
1046 'Multiple --filter arguments would be ANDed.')),
Peter Shih99b73ec2017-06-16 17:54:15 +08001047 Arg('-v', '--verbose', default=False, action='store_true',
1048 help='Print properties of each client.')
1049 ])
1050 def ListClients(self, args):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001051 clients = self._FilterClients(self._server.Clients(), args.filters)
1052 for client in clients:
1053 if args.verbose:
Peter Shih99b73ec2017-06-16 17:54:15 +08001054 print(yaml.safe_dump(client, default_flow_style=False))
Peter Shihcf0f3b22017-06-19 15:59:22 +08001055 else:
1056 print(client['mid'])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001057
1058 @Command('select', 'select default client', [
Peter Shihcf0f3b22017-06-19 15:59:22 +08001059 Arg('-f', '--filter', default=[], dest='filters', action='append',
1060 help=('Conditions to filter clients by properties. '
1061 'Should be in form "key=regex", where regex is the regular '
1062 'expression that should be found in the value. '
1063 'Multiple --filter arguments would be ANDed.')),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001064 Arg('mid', metavar='mid', nargs='?', default=None)])
1065 def SelectClient(self, args=None, store=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001066 mid = args.mid if args is not None else None
Wei-Ning Huang6796a132017-06-22 14:46:32 +08001067 filters = args.filters if args is not None else []
1068 clients = self._FilterClients(self._server.Clients(), filters, mid=mid)
Peter Shihcf0f3b22017-06-19 15:59:22 +08001069
1070 if not clients:
1071 raise RuntimeError('select: client not found')
Fei Shao0e4e2c62020-06-23 18:22:26 +08001072 if len(clients) == 1:
Peter Shihcf0f3b22017-06-19 15:59:22 +08001073 mid = clients[0]['mid']
1074 else:
1075 # This case would not happen when args.mid is specified.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001076 print('Select from the following clients:')
1077 for i, client in enumerate(clients):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001078 print(' %d. %s' % (i + 1, client['mid']))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001079
1080 print('\nSelection: ', end='')
1081 try:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001082 choice = int(input()) - 1
Peter Shihcf0f3b22017-06-19 15:59:22 +08001083 mid = clients[choice]['mid']
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001084 except ValueError:
1085 raise RuntimeError('select: invalid selection')
1086 except IndexError:
1087 raise RuntimeError('select: selection out of range')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001088
1089 self._selected_mid = mid
1090 if store:
1091 self._server.SelectClient(mid)
1092 print('Client %s selected' % mid)
1093
1094 @Command('shell', 'open a shell or execute a shell command', [
1095 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1096 def Shell(self, command=None):
1097 if command is None:
1098 command = []
1099 self.CheckClient()
1100
1101 headers = []
1102 if self._state.username is not None and self._state.password is not None:
1103 headers.append(BasicAuthHeader(self._state.username,
1104 self._state.password))
1105
1106 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Peter Shihe03450b2017-06-14 14:02:08 +08001107 if command:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001108 cmd = ' '.join(command)
Peter Shihe03450b2017-06-14 14:02:08 +08001109 ws = ShellWebSocketClient(
1110 self._state, sys.stdout,
1111 scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
1112 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001113 urllib.parse.quote(self._selected_mid), urllib.parse.quote(cmd)),
Peter Shihe03450b2017-06-14 14:02:08 +08001114 headers=headers)
1115 else:
1116 ws = TerminalWebSocketClient(
1117 self._state, self._selected_mid, self._escape,
1118 scheme + '%s:%d/api/agent/tty/%s' % (
1119 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001120 urllib.parse.quote(self._selected_mid)),
Peter Shihe03450b2017-06-14 14:02:08 +08001121 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001122 try:
1123 ws.connect()
1124 ws.run()
1125 except socket.error as e:
1126 if e.errno == 32: # Broken pipe
1127 pass
1128 else:
1129 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001130
1131 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001132 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001133 Arg('dst', metavar='DESTINATION')])
1134 def Push(self, args):
1135 self.CheckClient()
1136
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001137 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001138 def _push(src, dst):
1139 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001140
1141 # Local file is a link
1142 if os.path.islink(src):
1143 pbar = ProgressBar(src_base)
1144 link_path = os.readlink(src)
1145 self.CheckOutput('mkdir -p %(dirname)s; '
1146 'if [ -d "%(dst)s" ]; then '
1147 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1148 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1149 dict(dirname=os.path.dirname(dst),
1150 link_path=link_path, dst=dst,
1151 link_name=src_base))
1152 pbar.End()
1153 return
1154
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001155 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1156 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001157 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001158 urllib.parse.quote(self._selected_mid), dst, mode))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001159 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001160 UrlOpen(self._state, url + '&filename=%s' % src_base)
Yilin Yangf54fb912020-01-08 11:42:38 +08001161 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001162 msg = json.loads(e.read()).get('error', None)
1163 raise RuntimeError('push: %s' % msg)
1164
1165 pbar = ProgressBar(src_base)
1166 self._HTTPPostFile(url, src, pbar.SetProgress,
1167 self._state.username, self._state.password)
1168 pbar.End()
1169
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001170 def _push_single_target(src, dst):
1171 if os.path.isdir(src):
1172 dst_exists = ast.literal_eval(self.CheckOutput(
1173 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1174 for root, unused_x, files in os.walk(src):
1175 # If destination directory does not exist, we should strip the first
1176 # layer of directory. For example: src_dir contains a single file 'A'
1177 #
1178 # push src_dir dest_dir
1179 #
1180 # If dest_dir exists, the resulting directory structure should be:
1181 # dest_dir/src_dir/A
1182 # If dest_dir does not exist, the resulting directory structure should
1183 # be:
1184 # dest_dir/A
1185 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1186 for name in files:
1187 _push(os.path.join(root, name),
1188 os.path.join(dst, dst_root, name))
1189 else:
1190 _push(src, dst)
1191
1192 if len(args.srcs) > 1:
1193 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1194 '2>/dev/null' % args.dst).strip()
1195 if not dst_type:
1196 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1197 if dst_type != 'directory':
1198 raise RuntimeError('push: %s: Not a directory' % args.dst)
1199
1200 for src in args.srcs:
1201 if not os.path.exists(src):
1202 raise RuntimeError('push: can not stat "%s": no such file or directory'
1203 % src)
1204 if not os.access(src, os.R_OK):
1205 raise RuntimeError('push: can not open "%s" for reading' % src)
1206
1207 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001208
1209 @Command('pull', 'pull a file or directory from remote', [
1210 Arg('src', metavar='SOURCE'),
1211 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1212 def Pull(self, args):
1213 self.CheckClient()
1214
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001215 @AutoRetry('pull', _RETRY_TIMES)
Peter Shihe6afab32018-09-11 17:16:48 +08001216 def _pull(src, dst, ftype, perm=0o644, link=None):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001217 try:
1218 os.makedirs(os.path.dirname(dst))
1219 except Exception:
1220 pass
1221
1222 src_base = os.path.basename(src)
1223
1224 # Remote file is a link
1225 if ftype == 'l':
1226 pbar = ProgressBar(src_base)
1227 if os.path.exists(dst):
1228 os.remove(dst)
1229 os.symlink(link, dst)
1230 pbar.End()
1231 return
1232
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001233 url = ('%s:%d/api/agent/download/%s?filename=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001234 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001235 urllib.parse.quote(self._selected_mid), urllib.parse.quote(src)))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001236 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001237 h = UrlOpen(self._state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +08001238 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001239 msg = json.loads(e.read()).get('error', 'unkown error')
1240 raise RuntimeError('pull: %s' % msg)
1241 except KeyboardInterrupt:
1242 return
1243
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001244 pbar = ProgressBar(src_base)
Stimim Chena30447c2020-10-06 10:04:00 +08001245 with open(dst, 'wb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001246 os.fchmod(f.fileno(), perm)
1247 total_size = int(h.headers.get('Content-Length'))
1248 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001249
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001250 while True:
1251 data = h.read(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +08001252 if not data:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001253 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001254 downloaded_size += len(data)
Yilin Yang14d02a22019-11-01 11:32:03 +08001255 pbar.SetProgress(downloaded_size * 100 / total_size,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001256 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001257 f.write(data)
1258 pbar.End()
1259
1260 # Use find to get a listing of all files under a root directory. The 'stat'
1261 # command is used to retrieve the filename and it's filemode.
1262 output = self.CheckOutput(
1263 'cd $HOME; '
1264 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001265 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1266 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001267 % {'src': args.src})
1268
1269 # We got error from the stat command
1270 if output.startswith('stat: '):
1271 sys.stderr.write(output)
1272 return
1273
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001274 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001275 common_prefix = os.path.dirname(args.src)
1276
1277 if len(entries) == 1:
1278 entry = entries[0]
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001279 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001280 if os.path.isdir(args.dst):
1281 dst = os.path.join(args.dst, os.path.basename(src_path))
1282 else:
1283 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001284 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001285 else:
1286 if not os.path.exists(args.dst):
1287 common_prefix = args.src
1288
1289 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001290 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001291 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001292 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1293 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001294
1295 @Command('forward', 'forward remote port to local port', [
1296 Arg('--list', dest='list_all', action='store_true', default=False,
1297 help='list all port forwarding sessions'),
1298 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1299 default=None,
1300 help='remove port forwarding for local port LOCAL_PORT'),
1301 Arg('--remove-all', dest='remove_all', action='store_true',
1302 default=False, help='remove all port forwarding'),
1303 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1304 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1305 def Forward(self, args):
1306 if args.list_all:
1307 max_len = 10
Peter Shihf84a8972017-06-19 15:18:24 +08001308 if self._state.forwards:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001309 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1310
1311 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1312 for local in sorted(self._state.forwards.keys()):
1313 value = self._state.forwards[local]
1314 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1315 return
1316
1317 if args.remove_all:
1318 self._server.RemoveAllForward()
1319 return
1320
1321 if args.remove:
1322 self._server.RemoveForward(args.remove)
1323 return
1324
1325 self.CheckClient()
1326
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001327 if args.remote is None:
1328 raise RuntimeError('remote port not specified')
1329
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001330 if args.local is None:
1331 args.local = args.remote
1332 remote = int(args.remote)
1333 local = int(args.local)
1334
1335 def HandleConnection(conn):
1336 headers = []
1337 if self._state.username is not None and self._state.password is not None:
1338 headers.append(BasicAuthHeader(self._state.username,
1339 self._state.password))
1340
1341 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1342 ws = ForwarderWebSocketClient(
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001343 self._state, conn,
Peter Shihe03450b2017-06-14 14:02:08 +08001344 scheme + '%s:%d/api/agent/forward/%s?port=%d' % (
1345 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001346 urllib.parse.quote(self._selected_mid), remote),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001347 headers=headers)
1348 try:
1349 ws.connect()
1350 ws.run()
1351 except Exception as e:
1352 print('error: %s' % e)
1353 finally:
1354 ws.close()
1355
1356 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1357 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1358 server.bind(('0.0.0.0', local))
1359 server.listen(5)
1360
1361 pid = os.fork()
1362 if pid == 0:
1363 while True:
1364 conn, unused_addr = server.accept()
1365 t = threading.Thread(target=HandleConnection, args=(conn,))
1366 t.daemon = True
1367 t.start()
1368 else:
1369 self._server.AddForward(self._selected_mid, remote, local, pid)
1370
1371
1372def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001373 # Setup logging format
1374 logger = logging.getLogger()
Stimim Chena30447c2020-10-06 10:04:00 +08001375 logger.setLevel(logging.DEBUG)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001376 handler = logging.StreamHandler()
1377 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1378 handler.setFormatter(formatter)
1379 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001380
1381 # Add DaemonState to JSONRPC lib classes
Stimim Chena30447c2020-10-06 10:04:00 +08001382 config.DEFAULT.classes.add(DaemonState)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001383
1384 ovl = OverlordCLIClient()
1385 try:
1386 ovl.Main()
1387 except KeyboardInterrupt:
1388 print('Ctrl-C received, abort')
1389 except Exception as e:
Stimim Chena30447c2020-10-06 10:04:00 +08001390 logging.exception('exit with error [%s]', e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001391
1392
1393if __name__ == '__main__':
1394 main()