blob: 0bef64569e98c3e18d157fa8895bbd6e26d25986 [file] [log] [blame]
Yilin Yang19da6932019-12-10 13:39:28 +08001#!/usr/bin/env python3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08002# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Yilin Yang14d02a22019-11-01 11:32:03 +08006from __future__ import division
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08007from __future__ import print_function
8
9import argparse
10import ast
11import base64
12import fcntl
Peter Shih13e78c52018-01-23 12:57:07 +080013import functools
Wei-Ning Huangba768ab2016-02-07 14:38:06 +080014import getpass
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080015import hashlib
Yilin Yang752db712019-09-27 15:42:38 +080016import http.client
Yilin Yang8d4f9d02019-11-28 17:12:11 +080017from io import StringIO
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080018import json
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080019import logging
20import os
21import re
22import select
23import signal
24import socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080025import ssl
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080026import struct
27import subprocess
28import sys
29import tempfile
30import termios
31import threading
32import time
33import tty
Peter Shih99b73ec2017-06-16 17:54:15 +080034import unicodedata # required by pyinstaller, pylint: disable=unused-import
Yilin Yangf54fb912020-01-08 11:42:38 +080035import urllib.error
36import urllib.parse
37import urllib.request
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080038
Peter Shih99b73ec2017-06-16 17:54:15 +080039import jsonrpclib
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080040from jsonrpclib.config import Config
Peter Shih99b73ec2017-06-16 17:54:15 +080041from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
Yilin Yangea784662019-09-26 13:51:03 +080042from six import iteritems
Yilin Yang8cc5dfb2019-10-22 15:58:53 +080043from six.moves import input
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080044from ws4py.client import WebSocketBaseClient
Peter Shih99b73ec2017-06-16 17:54:15 +080045import yaml
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080046
Yilin Yang83c8f442020-05-05 13:46:51 +080047from cros.factory.utils import process_utils
48
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080049
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080050_CERT_DIR = os.path.expanduser('~/.config/ovl')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080051
52_ESCAPE = '~'
53_BUFSIZ = 8192
54_OVERLORD_PORT = 4455
55_OVERLORD_HTTP_PORT = 9000
56_OVERLORD_CLIENT_DAEMON_PORT = 4488
57_OVERLORD_CLIENT_DAEMON_RPC_ADDR = ('127.0.0.1', _OVERLORD_CLIENT_DAEMON_PORT)
58
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080059_CONNECT_TIMEOUT = 3
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080060_DEFAULT_HTTP_TIMEOUT = 30
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080061_LIST_CACHE_TIMEOUT = 2
62_DEFAULT_TERMINAL_WIDTH = 80
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080063_RETRY_TIMES = 3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080064
65# echo -n overlord | md5sum
66_HTTP_BOUNDARY_MAGIC = '9246f080c855a69012707ab53489b921'
67
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080068# Terminal resize control
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080069_CONTROL_START = 128
70_CONTROL_END = 129
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080071
72# Stream control
73_STDIN_CLOSED = '##STDIN_CLOSED##'
74
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080075_SSH_CONTROL_SOCKET_PREFIX = os.path.join(tempfile.gettempdir(),
76 'ovl-ssh-control-')
77
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080078_TLS_CERT_FAILED_WARNING = """
79@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
80@ WARNING: REMOTE HOST VERIFICATION HAS FAILED! @
81@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
82IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
83Someone could be eavesdropping on you right now (man-in-the-middle attack)!
84It is also possible that the server is using a self-signed certificate.
85The fingerprint for the TLS host certificate sent by the remote host is
86
87%s
88
89Do you want to trust this certificate and proceed? [Y/n] """
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080090
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080091_TLS_CERT_CHANGED_WARNING = """
92@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
93@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
94@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
95IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
96Someone could be eavesdropping on you right now (man-in-the-middle attack)!
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080097It is also possible that the TLS host certificate has just been changed.
98The fingerprint for the TLS host certificate sent by the remote host is
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080099
100%s
101
102Remove '%s' if you still want to proceed.
103SSL Certificate verification failed."""
104
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800105
106def GetVersionDigest():
107 """Return the sha1sum of the current executing script."""
Wei-Ning Huang31609662016-08-11 00:22:25 +0800108 # Check python script by default
109 filename = __file__
110
111 # If we are running from a frozen binary, we should calculate the checksum
112 # against that binary instead of the python script.
113 # See: https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
114 if getattr(sys, 'frozen', False):
115 filename = sys.executable
116
Yilin Yang0412c272019-12-05 16:57:40 +0800117 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800118 return hashlib.sha1(f.read()).hexdigest()
119
120
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800121def GetTLSCertPath(host):
122 return os.path.join(_CERT_DIR, '%s.cert' % host)
123
124
125def UrlOpen(state, url):
Yilin Yangf54fb912020-01-08 11:42:38 +0800126 """Wrapper for urllib.request.urlopen.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800127
128 It selects correct HTTP scheme according to self._state.ssl, add HTTP
129 basic auth headers, and add specify correct SSL context.
130 """
131 url = MakeRequestUrl(state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +0800132 request = urllib.request.Request(url)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800133 if state.username is not None and state.password is not None:
134 request.add_header(*BasicAuthHeader(state.username, state.password))
Yilin Yangf54fb912020-01-08 11:42:38 +0800135 return urllib.request.urlopen(request, timeout=_DEFAULT_HTTP_TIMEOUT,
136 context=state.ssl_context)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800137
138
139def GetTLSCertificateSHA1Fingerprint(cert_pem):
140 beg = cert_pem.index('\n')
141 end = cert_pem.rindex('\n', 0, len(cert_pem) - 2)
142 cert_pem = cert_pem[beg:end] # Remove BEGIN/END CERTIFICATE boundary
143 cert_der = base64.b64decode(cert_pem)
144 return hashlib.sha1(cert_der).hexdigest()
145
146
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800147def KillGraceful(pid, wait_secs=1):
148 """Kill a process gracefully by first sending SIGTERM, wait for some time,
149 then send SIGKILL to make sure it's killed."""
150 try:
151 os.kill(pid, signal.SIGTERM)
152 time.sleep(wait_secs)
153 os.kill(pid, signal.SIGKILL)
154 except OSError:
155 pass
156
157
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800158def AutoRetry(action_name, retries):
159 """Decorator for retry function call."""
160 def Wrap(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800161 @functools.wraps(func)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800162 def Loop(*args, **kwargs):
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800163 for unused_i in range(retries):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800164 try:
165 func(*args, **kwargs)
166 except Exception as e:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800167 print('error: %s: %s: retrying ...' % (args[0], e))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800168 else:
169 break
170 else:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800171 print('error: failed to %s %s' % (action_name, args[0]))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800172 return Loop
173 return Wrap
174
175
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800176def BasicAuthHeader(user, password):
177 """Return HTTP basic auth header."""
178 credential = base64.b64encode('%s:%s' % (user, password))
179 return ('Authorization', 'Basic %s' % credential)
180
181
182def GetTerminalSize():
183 """Retrieve terminal window size."""
184 ws = struct.pack('HHHH', 0, 0, 0, 0)
185 ws = fcntl.ioctl(0, termios.TIOCGWINSZ, ws)
186 lines, columns, unused_x, unused_y = struct.unpack('HHHH', ws)
187 return lines, columns
188
189
190def MakeRequestUrl(state, url):
191 return 'http%s://%s' % ('s' if state.ssl else '', url)
192
193
194class ProgressBar(object):
195 SIZE_WIDTH = 11
196 SPEED_WIDTH = 10
197 DURATION_WIDTH = 6
198 PERCENTAGE_WIDTH = 8
199
200 def __init__(self, name):
201 self._start_time = time.time()
202 self._name = name
203 self._size = 0
204 self._width = 0
205 self._name_width = 0
206 self._name_max = 0
207 self._stat_width = 0
208 self._max = 0
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800209 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800210 self.SetProgress(0)
211
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800212 def _CalculateSize(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800213 self._width = GetTerminalSize()[1] or _DEFAULT_TERMINAL_WIDTH
214 self._name_width = int(self._width * 0.3)
215 self._name_max = self._name_width
216 self._stat_width = self.SIZE_WIDTH + self.SPEED_WIDTH + self.DURATION_WIDTH
217 self._max = (self._width - self._name_width - self._stat_width -
218 self.PERCENTAGE_WIDTH)
219
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800220 def _SizeToHuman(self, size_in_bytes):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800221 if size_in_bytes < 1024:
222 unit = 'B'
223 value = size_in_bytes
224 elif size_in_bytes < 1024 ** 2:
225 unit = 'KiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800226 value = size_in_bytes / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800227 elif size_in_bytes < 1024 ** 3:
228 unit = 'MiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800229 value = size_in_bytes / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800230 elif size_in_bytes < 1024 ** 4:
231 unit = 'GiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800232 value = size_in_bytes / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800233 return ' %6.1f %3s' % (value, unit)
234
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800235 def _SpeedToHuman(self, speed_in_bs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800236 if speed_in_bs < 1024:
237 unit = 'B'
238 value = speed_in_bs
239 elif speed_in_bs < 1024 ** 2:
240 unit = 'K'
Yilin Yang14d02a22019-11-01 11:32:03 +0800241 value = speed_in_bs / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800242 elif speed_in_bs < 1024 ** 3:
243 unit = 'M'
Yilin Yang14d02a22019-11-01 11:32:03 +0800244 value = speed_in_bs / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800245 elif speed_in_bs < 1024 ** 4:
246 unit = 'G'
Yilin Yang14d02a22019-11-01 11:32:03 +0800247 value = speed_in_bs / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800248 return ' %6.1f%s/s' % (value, unit)
249
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800250 def _DurationToClock(self, duration):
Yilin Yang14d02a22019-11-01 11:32:03 +0800251 return ' %02d:%02d' % (duration // 60, duration % 60)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800252
253 def SetProgress(self, percentage, size=None):
254 current_width = GetTerminalSize()[1]
255 if self._width != current_width:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800256 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800257
258 if size is not None:
259 self._size = size
260
261 elapse_time = time.time() - self._start_time
Yilin Yang14d02a22019-11-01 11:32:03 +0800262 speed = self._size / elapse_time
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800263
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800264 size_str = self._SizeToHuman(self._size)
265 speed_str = self._SpeedToHuman(speed)
266 elapse_str = self._DurationToClock(elapse_time)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800267
268 width = int(self._max * percentage / 100.0)
269 sys.stdout.write(
270 '%*s' % (- self._name_max,
271 self._name if len(self._name) <= self._name_max else
272 self._name[:self._name_max - 4] + ' ...') +
273 size_str + speed_str + elapse_str +
274 ((' [' + '#' * width + ' ' * (self._max - width) + ']' +
275 '%4d%%' % int(percentage)) if self._max > 2 else '') + '\r')
276 sys.stdout.flush()
277
278 def End(self):
279 self.SetProgress(100.0)
280 sys.stdout.write('\n')
281 sys.stdout.flush()
282
283
284class DaemonState(object):
285 """DaemonState is used for storing Overlord state info."""
286 def __init__(self):
287 self.version_sha1sum = GetVersionDigest()
288 self.host = None
289 self.port = None
290 self.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800291 self.ssl_self_signed = False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800292 self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800293 self.ssh = False
294 self.orig_host = None
295 self.ssh_pid = None
296 self.username = None
297 self.password = None
298 self.selected_mid = None
299 self.forwards = {}
300 self.listing = []
301 self.last_list = 0
302
303
304class OverlordClientDaemon(object):
305 """Overlord Client Daemon."""
306 def __init__(self):
307 self._state = DaemonState()
308 self._server = None
309
310 def Start(self):
311 self.StartRPCServer()
312
313 def StartRPCServer(self):
314 self._server = SimpleJSONRPCServer(_OVERLORD_CLIENT_DAEMON_RPC_ADDR,
315 logRequests=False)
316 exports = [
317 (self.State, 'State'),
318 (self.Ping, 'Ping'),
319 (self.GetPid, 'GetPid'),
320 (self.Connect, 'Connect'),
321 (self.Clients, 'Clients'),
322 (self.SelectClient, 'SelectClient'),
323 (self.AddForward, 'AddForward'),
324 (self.RemoveForward, 'RemoveForward'),
325 (self.RemoveAllForward, 'RemoveAllForward'),
326 ]
327 for func, name in exports:
328 self._server.register_function(func, name)
329
330 pid = os.fork()
331 if pid == 0:
Peter Shih4d55ded2017-07-03 17:19:01 +0800332 for fd in range(3):
333 os.close(fd)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800334 self._server.serve_forever()
335
336 @staticmethod
337 def GetRPCServer():
338 """Returns the Overlord client daemon RPC server."""
339 server = jsonrpclib.Server('http://%s:%d' %
340 _OVERLORD_CLIENT_DAEMON_RPC_ADDR)
341 try:
342 server.Ping()
343 except Exception:
344 return None
345 return server
346
347 def State(self):
348 return self._state
349
350 def Ping(self):
351 return True
352
353 def GetPid(self):
354 return os.getpid()
355
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800356 def _GetJSON(self, path):
357 url = '%s:%d%s' % (self._state.host, self._state.port, path)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800358 return json.loads(UrlOpen(self._state, url).read())
359
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800360 def _TLSEnabled(self):
361 """Determine if TLS is enabled on given server address."""
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800362 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
363 try:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800364 # Allow any certificate since we only want to check if server talks TLS.
365 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
366 context.verify_mode = ssl.CERT_NONE
367
368 sock = context.wrap_socket(sock, server_hostname=self._state.host)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800369 sock.settimeout(_CONNECT_TIMEOUT)
370 sock.connect((self._state.host, self._state.port))
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800371 return True
Wei-Ning Huangecc80b82016-07-01 16:55:10 +0800372 except ssl.SSLError:
373 return False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800374 except socket.error: # Connect refused or timeout
375 raise
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800376 except Exception:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800377 return False # For whatever reason above failed, assume False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800378
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800379 def _CheckTLSCertificate(self, check_hostname=True):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800380 """Check TLS certificate.
381
382 Returns:
383 A tupple (check_result, if_certificate_is_loaded)
384 """
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800385 def _DoConnect(context):
386 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
387 try:
388 sock.settimeout(_CONNECT_TIMEOUT)
389 sock = context.wrap_socket(sock, server_hostname=self._state.host)
390 sock.connect((self._state.host, self._state.port))
391 except ssl.SSLError:
392 return False
393 finally:
394 sock.close()
395
396 # Save SSLContext for future use.
397 self._state.ssl_context = context
398 return True
399
400 # First try connect with built-in certificates
401 tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
402 if _DoConnect(tls_context):
403 return True
404
405 # Try with already saved certificate, if any.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800406 tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
407 tls_context.verify_mode = ssl.CERT_REQUIRED
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800408 tls_context.check_hostname = check_hostname
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800409
410 tls_cert_path = GetTLSCertPath(self._state.host)
411 if os.path.exists(tls_cert_path):
412 tls_context.load_verify_locations(tls_cert_path)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800413 self._state.ssl_self_signed = True
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800414
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800415 return _DoConnect(tls_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800416
417 def Connect(self, host, port=_OVERLORD_HTTP_PORT, ssh_pid=None,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800418 username=None, password=None, orig_host=None,
419 check_hostname=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800420 self._state.username = username
421 self._state.password = password
422 self._state.host = host
423 self._state.port = port
424 self._state.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800425 self._state.ssl_self_signed = False
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800426 self._state.orig_host = orig_host
427 self._state.ssh_pid = ssh_pid
428 self._state.selected_mid = None
429
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800430 tls_enabled = self._TLSEnabled()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800431 if tls_enabled:
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800432 result = self._CheckTLSCertificate(check_hostname)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800433 if not result:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800434 if self._state.ssl_self_signed:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800435 return ('SSLCertificateChanged', ssl.get_server_certificate(
436 (self._state.host, self._state.port)))
Yilin Yang15a3f8f2020-01-03 17:49:00 +0800437 return ('SSLVerifyFailed', ssl.get_server_certificate(
438 (self._state.host, self._state.port)))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800439
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800440 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800441 self._state.ssl = tls_enabled
442 UrlOpen(self._state, '%s:%d' % (host, port))
Yilin Yangf54fb912020-01-08 11:42:38 +0800443 except urllib.error.HTTPError as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800444 return ('HTTPError', e.getcode(), str(e), e.read().strip())
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800445 except Exception as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800446 return str(e)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800447 else:
448 return True
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800449
450 def Clients(self):
451 if time.time() - self._state.last_list <= _LIST_CACHE_TIMEOUT:
452 return self._state.listing
453
Peter Shihcf0f3b22017-06-19 15:59:22 +0800454 self._state.listing = self._GetJSON('/api/agents/list')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800455 self._state.last_list = time.time()
456 return self._state.listing
457
458 def SelectClient(self, mid):
459 self._state.selected_mid = mid
460
461 def AddForward(self, mid, remote, local, pid):
462 self._state.forwards[local] = (mid, remote, pid)
463
464 def RemoveForward(self, local_port):
465 try:
466 unused_mid, unused_remote, pid = self._state.forwards[local_port]
467 KillGraceful(pid)
468 del self._state.forwards[local_port]
469 except (KeyError, OSError):
470 pass
471
472 def RemoveAllForward(self):
473 for unused_mid, unused_remote, pid in self._state.forwards.values():
474 try:
475 KillGraceful(pid)
476 except OSError:
477 pass
478 self._state.forwards = {}
479
480
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800481class SSLEnabledWebSocketBaseClient(WebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800482 def __init__(self, state, *args, **kwargs):
483 cafile = ssl.get_default_verify_paths().openssl_cafile
484 # For some system / distribution, python can not detect system cafile path.
485 # In such case we fallback to the default path.
486 if not os.path.exists(cafile):
487 cafile = '/etc/ssl/certs/ca-certificates.crt'
488
489 if state.ssl_self_signed:
490 cafile = GetTLSCertPath(state.host)
491
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800492 ssl_options = {
493 'cert_reqs': ssl.CERT_REQUIRED,
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800494 'ca_certs': cafile
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800495 }
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800496 # ws4py does not allow you to specify SSLContext, but rather passing in the
497 # argument of ssl.wrap_socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800498 super(SSLEnabledWebSocketBaseClient, self).__init__(
499 ssl_options=ssl_options, *args, **kwargs)
500
501
502class TerminalWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800503 def __init__(self, state, mid, escape, *args, **kwargs):
504 super(TerminalWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800505 self._mid = mid
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800506 self._escape = escape
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800507 self._stdin_fd = sys.stdin.fileno()
508 self._old_termios = None
509
510 def handshake_ok(self):
511 pass
512
513 def opened(self):
514 nonlocals = {'size': (80, 40)}
515
516 def _ResizeWindow():
517 size = GetTerminalSize()
518 if size != nonlocals['size']: # Size not changed, ignore
519 control = {'command': 'resize', 'params': list(size)}
520 payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END)
521 nonlocals['size'] = size
522 try:
523 self.send(payload, binary=True)
524 except Exception:
525 pass
526
527 def _FeedInput():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800528 self._old_termios = termios.tcgetattr(self._stdin_fd)
529 tty.setraw(self._stdin_fd)
530
531 READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3)
532
533 try:
534 state = READY
535 while True:
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800536 # Check if terminal is resized
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800537 _ResizeWindow()
538
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800539 ch = sys.stdin.read(1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800540
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800541 # Scan for escape sequence
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800542 if self._escape:
543 if state == READY:
544 state = ENTER_PRESSED if ch == chr(0x0d) else READY
545 elif state == ENTER_PRESSED:
546 state = ESCAPE_PRESSED if ch == self._escape else READY
547 elif state == ESCAPE_PRESSED:
548 if ch == '.':
549 self.close()
550 break
551 else:
552 state = READY
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800553
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800554 self.send(ch)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800555 except (KeyboardInterrupt, RuntimeError):
556 pass
557
558 t = threading.Thread(target=_FeedInput)
559 t.daemon = True
560 t.start()
561
562 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800563 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800564 termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios)
565 print('Connection to %s closed.' % self._mid)
566
Yilin Yangf64670b2020-01-06 11:22:18 +0800567 def received_message(self, message):
568 if message.is_binary:
569 sys.stdout.write(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800570 sys.stdout.flush()
571
572
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800573class ShellWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800574 def __init__(self, state, output, *args, **kwargs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800575 """Constructor.
576
577 Args:
578 output: output file object.
579 """
580 self.output = output
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800581 super(ShellWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800582
583 def handshake_ok(self):
584 pass
585
586 def opened(self):
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800587 def _FeedInput():
588 try:
589 while True:
590 data = sys.stdin.read(1)
591
Peter Shihf84a8972017-06-19 15:18:24 +0800592 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800593 self.send(_STDIN_CLOSED * 2)
594 break
595 self.send(data, binary=True)
596 except (KeyboardInterrupt, RuntimeError):
597 pass
598
599 t = threading.Thread(target=_FeedInput)
600 t.daemon = True
601 t.start()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800602
603 def closed(self, code, reason=None):
604 pass
605
Yilin Yangf64670b2020-01-06 11:22:18 +0800606 def received_message(self, message):
607 if message.is_binary:
608 self.output.write(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800609 self.output.flush()
610
611
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800612class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800613 def __init__(self, state, sock, *args, **kwargs):
614 super(ForwarderWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800615 self._sock = sock
616 self._stop = threading.Event()
617
618 def handshake_ok(self):
619 pass
620
621 def opened(self):
622 def _FeedInput():
623 try:
624 self._sock.setblocking(False)
625 while True:
626 rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5)
627 if self._stop.is_set():
628 break
629 if self._sock in rd:
630 data = self._sock.recv(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +0800631 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800632 self.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800633 break
634 self.send(data, binary=True)
635 except Exception:
636 pass
637 finally:
638 self._sock.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800639
640 t = threading.Thread(target=_FeedInput)
641 t.daemon = True
642 t.start()
643
644 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800645 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800646 self._stop.set()
647 sys.exit(0)
648
Yilin Yangf64670b2020-01-06 11:22:18 +0800649 def received_message(self, message):
650 if message.is_binary:
651 self._sock.send(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800652
653
654def Arg(*args, **kwargs):
655 return (args, kwargs)
656
657
658def Command(command, help_msg=None, args=None):
659 """Decorator for adding argparse parameter for a method."""
660 if args is None:
661 args = []
662 def WrapFunc(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800663 @functools.wraps(func)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800664 def Wrapped(*args, **kwargs):
665 return func(*args, **kwargs)
Peter Shih99b73ec2017-06-16 17:54:15 +0800666 # pylint: disable=protected-access
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800667 Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args}
668 return Wrapped
669 return WrapFunc
670
671
672def ParseMethodSubCommands(cls):
673 """Decorator for a class using the @Command decorator.
674
675 This decorator retrieve command info from each method and append it in to the
676 SUBCOMMANDS class variable, which is later used to construct parser.
677 """
Yilin Yangea784662019-09-26 13:51:03 +0800678 for unused_key, method in iteritems(cls.__dict__):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800679 if hasattr(method, '__arg_attr'):
Peter Shih99b73ec2017-06-16 17:54:15 +0800680 # pylint: disable=protected-access
681 cls.SUBCOMMANDS.append(method.__arg_attr)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800682 return cls
683
684
685@ParseMethodSubCommands
686class OverlordCLIClient(object):
687 """Overlord command line interface client."""
688
689 SUBCOMMANDS = []
690
691 def __init__(self):
692 self._parser = self._BuildParser()
693 self._selected_mid = None
694 self._server = None
695 self._state = None
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800696 self._escape = None
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800697
698 def _BuildParser(self):
699 root_parser = argparse.ArgumentParser(prog='ovl')
700 subparsers = root_parser.add_subparsers(help='sub-command')
701
702 root_parser.add_argument('-s', dest='selected_mid', action='store',
703 default=None,
704 help='select target to execute command on')
705 root_parser.add_argument('-S', dest='select_mid_before_action',
706 action='store_true', default=False,
707 help='select target before executing command')
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800708 root_parser.add_argument('-e', dest='escape', metavar='ESCAPE_CHAR',
709 action='store', default=_ESCAPE, type=str,
710 help='set shell escape character, \'none\' to '
711 'disable escape completely')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800712
713 for attr in self.SUBCOMMANDS:
714 parser = subparsers.add_parser(attr['command'], help=attr['help'])
715 parser.set_defaults(which=attr['command'])
716 for arg in attr['args']:
717 parser.add_argument(*arg[0], **arg[1])
718
719 return root_parser
720
721 def Main(self):
722 # We want to pass the rest of arguments after shell command directly to the
723 # function without parsing it.
724 try:
725 index = sys.argv.index('shell')
726 except ValueError:
727 args = self._parser.parse_args()
728 else:
729 args = self._parser.parse_args(sys.argv[1:index + 1])
730
731 command = args.which
732 self._selected_mid = args.selected_mid
733
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800734 if args.escape and args.escape != 'none':
735 self._escape = args.escape[0]
736
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800737 if command == 'start-server':
738 self.StartServer()
739 return
740 elif command == 'kill-server':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800741 self.KillServer()
742 return
743
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800744 self.CheckDaemon()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800745 if command == 'status':
746 self.Status()
747 return
748 elif command == 'connect':
749 self.Connect(args)
750 return
751
752 # The following command requires connection to the server
753 self.CheckConnection()
754
755 if args.select_mid_before_action:
756 self.SelectClient(store=False)
757
758 if command == 'select':
759 self.SelectClient(args)
760 elif command == 'ls':
Peter Shih99b73ec2017-06-16 17:54:15 +0800761 self.ListClients(args)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800762 elif command == 'shell':
763 command = sys.argv[sys.argv.index('shell') + 1:]
764 self.Shell(command)
765 elif command == 'push':
766 self.Push(args)
767 elif command == 'pull':
768 self.Pull(args)
769 elif command == 'forward':
770 self.Forward(args)
771
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800772 def _SaveTLSCertificate(self, host, cert_pem):
773 try:
774 os.makedirs(_CERT_DIR)
775 except Exception:
776 pass
777 with open(GetTLSCertPath(host), 'w') as f:
778 f.write(cert_pem)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800779
780 def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None):
781 """Perform HTTP POST and upload file to Overlord.
782
783 To minimize the external dependencies, we construct the HTTP post request
784 by ourselves.
785 """
786 url = MakeRequestUrl(self._state, url)
787 size = os.stat(filename).st_size
788 boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC
789 CRLF = '\r\n'
Yilin Yangf54fb912020-01-08 11:42:38 +0800790 parse = urllib.parse.urlparse(url)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800791
792 part_headers = [
793 '--' + boundary,
794 'Content-Disposition: form-data; name="file"; '
795 'filename="%s"' % os.path.basename(filename),
796 'Content-Type: application/octet-stream',
797 '', ''
798 ]
799 part_header = CRLF.join(part_headers)
800 end_part = CRLF + '--' + boundary + '--' + CRLF
801
802 content_length = len(part_header) + size + len(end_part)
803 if parse.scheme == 'http':
Yilin Yang752db712019-09-27 15:42:38 +0800804 h = http.client.HTTP(parse.netloc)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800805 else:
Yilin Yang752db712019-09-27 15:42:38 +0800806 h = http.client.HTTPS(parse.netloc, context=self._state.ssl_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800807
808 post_path = url[url.index(parse.netloc) + len(parse.netloc):]
809 h.putrequest('POST', post_path)
810 h.putheader('Content-Length', content_length)
811 h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
812
813 if user and passwd:
814 h.putheader(*BasicAuthHeader(user, passwd))
815 h.endheaders()
816 h.send(part_header)
817
818 count = 0
819 with open(filename, 'r') as f:
820 while True:
821 data = f.read(_BUFSIZ)
822 if not data:
823 break
824 count += len(data)
825 if progress:
Yilin Yang14d02a22019-11-01 11:32:03 +0800826 progress(count * 100 // size, count)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800827 h.send(data)
828
829 h.send(end_part)
830 progress(100)
831
832 if count != size:
833 logging.warning('file changed during upload, upload may be truncated.')
834
835 errcode, unused_x, unused_y = h.getreply()
836 return errcode == 200
837
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800838 def CheckDaemon(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800839 self._server = OverlordClientDaemon.GetRPCServer()
840 if self._server is None:
841 print('* daemon not running, starting it now on port %d ... *' %
842 _OVERLORD_CLIENT_DAEMON_PORT)
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800843 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800844
845 self._state = self._server.State()
846 sha1sum = GetVersionDigest()
847
848 if sha1sum != self._state.version_sha1sum:
849 print('ovl server is out of date. killing...')
850 KillGraceful(self._server.GetPid())
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800851 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800852
853 def GetSSHControlFile(self, host):
854 return _SSH_CONTROL_SOCKET_PREFIX + host
855
856 def SSHTunnel(self, user, host, port):
857 """SSH forward the remote overlord server.
858
859 Overlord server may not have port 9000 open to the public network, in such
860 case we can SSH forward the port to localhost.
861 """
862
863 control_file = self.GetSSHControlFile(host)
864 try:
865 os.unlink(control_file)
866 except Exception:
867 pass
868
869 subprocess.Popen([
870 'ssh', '-Nf',
871 '-M', # Enable master mode
872 '-S', control_file,
873 '-L', '9000:localhost:9000',
874 '-p', str(port),
875 '%s%s' % (user + '@' if user else '', host)
876 ]).wait()
877
Yilin Yang83c8f442020-05-05 13:46:51 +0800878 p = process_utils.Spawn([
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800879 'ssh',
880 '-S', control_file,
881 '-O', 'check', host,
Yilin Yang83c8f442020-05-05 13:46:51 +0800882 ], read_stderr=True, ignore_stdout=True)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800883
Yilin Yang83c8f442020-05-05 13:46:51 +0800884 s = re.search(r'pid=(\d+)', p.stderr_data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800885 if s:
886 return int(s.group(1))
887
888 raise RuntimeError('can not establish ssh connection')
889
890 def CheckConnection(self):
891 if self._state.host is None:
892 raise RuntimeError('not connected to any server, abort')
893
894 try:
895 self._server.Clients()
896 except Exception:
897 raise RuntimeError('remote server disconnected, abort')
898
899 if self._state.ssh_pid is not None:
900 ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)],
901 stdout=subprocess.PIPE,
902 stderr=subprocess.PIPE).wait()
903 if ret != 0:
904 raise RuntimeError('ssh tunnel disconnected, please re-connect')
905
906 def CheckClient(self):
907 if self._selected_mid is None:
908 if self._state.selected_mid is None:
909 raise RuntimeError('No client is selected')
910 self._selected_mid = self._state.selected_mid
911
Peter Shihcf0f3b22017-06-19 15:59:22 +0800912 if not any(client['mid'] == self._selected_mid
913 for client in self._server.Clients()):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800914 raise RuntimeError('client %s disappeared' % self._selected_mid)
915
916 def CheckOutput(self, command):
917 headers = []
918 if self._state.username is not None and self._state.password is not None:
919 headers.append(BasicAuthHeader(self._state.username,
920 self._state.password))
921
922 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Yilin Yang8d4f9d02019-11-28 17:12:11 +0800923 sio = StringIO()
Peter Shihe03450b2017-06-14 14:02:08 +0800924 ws = ShellWebSocketClient(
925 self._state, sio, scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
926 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +0800927 urllib.parse.quote(self._selected_mid),
928 urllib.parse.quote(command)),
Peter Shihe03450b2017-06-14 14:02:08 +0800929 headers=headers)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800930 ws.connect()
931 ws.run()
932 return sio.getvalue()
933
934 @Command('status', 'show Overlord connection status')
935 def Status(self):
936 if self._state.host is None:
937 print('Not connected to any host.')
938 else:
939 if self._state.ssh_pid is not None:
940 print('Connected to %s with SSH tunneling.' % self._state.orig_host)
941 else:
942 print('Connected to %s:%d.' % (self._state.host, self._state.port))
943
944 if self._selected_mid is None:
945 self._selected_mid = self._state.selected_mid
946
947 if self._selected_mid is None:
948 print('No client is selected.')
949 else:
950 print('Client %s selected.' % self._selected_mid)
951
952 @Command('connect', 'connect to Overlord server', [
953 Arg('host', metavar='HOST', type=str, default='localhost',
954 help='Overlord hostname/IP'),
955 Arg('port', metavar='PORT', type=int,
956 default=_OVERLORD_HTTP_PORT, help='Overlord port'),
957 Arg('-f', '--forward', dest='ssh_forward', default=False,
958 action='store_true',
959 help='connect with SSH forwarding to the host'),
960 Arg('-p', '--ssh-port', dest='ssh_port', default=22,
961 type=int, help='SSH server port for SSH forwarding'),
962 Arg('-l', '--ssh-login', dest='ssh_login', default='',
963 type=str, help='SSH server login name for SSH forwarding'),
964 Arg('-u', '--user', dest='user', default=None,
965 type=str, help='Overlord HTTP auth username'),
966 Arg('-w', '--passwd', dest='passwd', default=None, type=str,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800967 help='Overlord HTTP auth password'),
968 Arg('-i', '--no-check-hostname', dest='check_hostname',
969 default=True, action='store_false',
970 help='Ignore SSL cert hostname check')])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800971 def Connect(self, args):
972 ssh_pid = None
973 host = args.host
974 orig_host = args.host
975
976 if args.ssh_forward:
977 # Kill previous SSH tunnel
978 self.KillSSHTunnel()
979
980 ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port)
981 host = 'localhost'
982
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800983 username_provided = args.user is not None
984 password_provided = args.passwd is not None
985 prompt = False
986
Peter Shih533566a2018-09-05 17:48:03 +0800987 for unused_i in range(3): # pylint: disable=too-many-nested-blocks
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800988 try:
989 if prompt:
990 if not username_provided:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800991 args.user = input('Username: ')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800992 if not password_provided:
993 args.passwd = getpass.getpass('Password: ')
994
995 ret = self._server.Connect(host, args.port, ssh_pid, args.user,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800996 args.passwd, orig_host,
997 args.check_hostname)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800998 if isinstance(ret, list):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800999 if ret[0].startswith('SSL'):
1000 cert_pem = ret[1]
1001 fp = GetTLSCertificateSHA1Fingerprint(cert_pem)
1002 fp_text = ':'.join([fp[i:i+2] for i in range(0, len(fp), 2)])
1003
1004 if ret[0] == 'SSLCertificateChanged':
1005 print(_TLS_CERT_CHANGED_WARNING % (fp_text, GetTLSCertPath(host)))
1006 return
1007 elif ret[0] == 'SSLVerifyFailed':
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001008 print(_TLS_CERT_FAILED_WARNING % (fp_text), end='')
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001009 response = input()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001010 if response.lower() in ['y', 'ye', 'yes']:
1011 self._SaveTLSCertificate(host, cert_pem)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001012 print('TLS host Certificate trusted, you will not be prompted '
1013 'next time.\n')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001014 continue
1015 else:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001016 print('connection aborted.')
1017 return
1018 elif ret[0] == 'HTTPError':
1019 code, except_str, body = ret[1:]
1020 if code == 401:
1021 print('connect: %s' % body)
1022 prompt = True
1023 if not username_provided or not password_provided:
1024 continue
1025 else:
1026 break
1027 else:
1028 logging.error('%s; %s', except_str, body)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001029
1030 if ret is not True:
1031 print('can not connect to %s: %s' % (host, ret))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001032 else:
1033 print('connection to %s:%d established.' % (host, args.port))
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001034 except Exception as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001035 logging.error(e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001036 else:
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001037 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001038
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001039 @Command('start-server', 'start overlord CLI client server')
1040 def StartServer(self):
1041 self._server = OverlordClientDaemon.GetRPCServer()
1042 if self._server is None:
1043 OverlordClientDaemon().Start()
1044 time.sleep(1)
1045 self._server = OverlordClientDaemon.GetRPCServer()
1046 if self._server is not None:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001047 print('* daemon started successfully *\n')
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001048
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001049 @Command('kill-server', 'kill overlord CLI client server')
1050 def KillServer(self):
1051 self._server = OverlordClientDaemon.GetRPCServer()
1052 if self._server is None:
1053 return
1054
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001055 self._state = self._server.State()
1056
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001057 # Kill SSH Tunnel
1058 self.KillSSHTunnel()
1059
1060 # Kill server daemon
1061 KillGraceful(self._server.GetPid())
1062
1063 def KillSSHTunnel(self):
1064 if self._state.ssh_pid is not None:
1065 KillGraceful(self._state.ssh_pid)
1066
Peter Shihcf0f3b22017-06-19 15:59:22 +08001067 def _FilterClients(self, clients, prop_filters, mid=None):
1068 def _ClientPropertiesMatch(client, key, regex):
1069 try:
1070 return bool(re.search(regex, client['properties'][key]))
1071 except KeyError:
1072 return False
1073
1074 for prop_filter in prop_filters:
1075 key, sep, regex = prop_filter.partition('=')
1076 if not sep:
1077 # The filter doesn't contains =.
1078 raise ValueError('Invalid filter condition %r' % filter)
1079 clients = [c for c in clients if _ClientPropertiesMatch(c, key, regex)]
1080
1081 if mid is not None:
1082 client = next((c for c in clients if c['mid'] == mid), None)
1083 if client:
1084 return [client]
1085 clients = [c for c in clients if c['mid'].startswith(mid)]
1086 return clients
1087
1088 @Command('ls', 'list clients', [
1089 Arg('-f', '--filter', default=[], dest='filters', action='append',
1090 help=('Conditions to filter clients by properties. '
1091 'Should be in form "key=regex", where regex is the regular '
1092 'expression that should be found in the value. '
1093 'Multiple --filter arguments would be ANDed.')),
Peter Shih99b73ec2017-06-16 17:54:15 +08001094 Arg('-v', '--verbose', default=False, action='store_true',
1095 help='Print properties of each client.')
1096 ])
1097 def ListClients(self, args):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001098 clients = self._FilterClients(self._server.Clients(), args.filters)
1099 for client in clients:
1100 if args.verbose:
Peter Shih99b73ec2017-06-16 17:54:15 +08001101 print(yaml.safe_dump(client, default_flow_style=False))
Peter Shihcf0f3b22017-06-19 15:59:22 +08001102 else:
1103 print(client['mid'])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001104
1105 @Command('select', 'select default client', [
Peter Shihcf0f3b22017-06-19 15:59:22 +08001106 Arg('-f', '--filter', default=[], dest='filters', action='append',
1107 help=('Conditions to filter clients by properties. '
1108 'Should be in form "key=regex", where regex is the regular '
1109 'expression that should be found in the value. '
1110 'Multiple --filter arguments would be ANDed.')),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001111 Arg('mid', metavar='mid', nargs='?', default=None)])
1112 def SelectClient(self, args=None, store=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001113 mid = args.mid if args is not None else None
Wei-Ning Huang6796a132017-06-22 14:46:32 +08001114 filters = args.filters if args is not None else []
1115 clients = self._FilterClients(self._server.Clients(), filters, mid=mid)
Peter Shihcf0f3b22017-06-19 15:59:22 +08001116
1117 if not clients:
1118 raise RuntimeError('select: client not found')
1119 elif len(clients) == 1:
1120 mid = clients[0]['mid']
1121 else:
1122 # This case would not happen when args.mid is specified.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001123 print('Select from the following clients:')
1124 for i, client in enumerate(clients):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001125 print(' %d. %s' % (i + 1, client['mid']))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001126
1127 print('\nSelection: ', end='')
1128 try:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001129 choice = int(input()) - 1
Peter Shihcf0f3b22017-06-19 15:59:22 +08001130 mid = clients[choice]['mid']
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001131 except ValueError:
1132 raise RuntimeError('select: invalid selection')
1133 except IndexError:
1134 raise RuntimeError('select: selection out of range')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001135
1136 self._selected_mid = mid
1137 if store:
1138 self._server.SelectClient(mid)
1139 print('Client %s selected' % mid)
1140
1141 @Command('shell', 'open a shell or execute a shell command', [
1142 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1143 def Shell(self, command=None):
1144 if command is None:
1145 command = []
1146 self.CheckClient()
1147
1148 headers = []
1149 if self._state.username is not None and self._state.password is not None:
1150 headers.append(BasicAuthHeader(self._state.username,
1151 self._state.password))
1152
1153 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Peter Shihe03450b2017-06-14 14:02:08 +08001154 if command:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001155 cmd = ' '.join(command)
Peter Shihe03450b2017-06-14 14:02:08 +08001156 ws = ShellWebSocketClient(
1157 self._state, sys.stdout,
1158 scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
1159 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001160 urllib.parse.quote(self._selected_mid), urllib.parse.quote(cmd)),
Peter Shihe03450b2017-06-14 14:02:08 +08001161 headers=headers)
1162 else:
1163 ws = TerminalWebSocketClient(
1164 self._state, self._selected_mid, self._escape,
1165 scheme + '%s:%d/api/agent/tty/%s' % (
1166 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001167 urllib.parse.quote(self._selected_mid)),
Peter Shihe03450b2017-06-14 14:02:08 +08001168 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001169 try:
1170 ws.connect()
1171 ws.run()
1172 except socket.error as e:
1173 if e.errno == 32: # Broken pipe
1174 pass
1175 else:
1176 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001177
1178 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001179 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001180 Arg('dst', metavar='DESTINATION')])
1181 def Push(self, args):
1182 self.CheckClient()
1183
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001184 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001185 def _push(src, dst):
1186 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001187
1188 # Local file is a link
1189 if os.path.islink(src):
1190 pbar = ProgressBar(src_base)
1191 link_path = os.readlink(src)
1192 self.CheckOutput('mkdir -p %(dirname)s; '
1193 'if [ -d "%(dst)s" ]; then '
1194 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1195 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1196 dict(dirname=os.path.dirname(dst),
1197 link_path=link_path, dst=dst,
1198 link_name=src_base))
1199 pbar.End()
1200 return
1201
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001202 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1203 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001204 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001205 urllib.parse.quote(self._selected_mid), dst, mode))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001206 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001207 UrlOpen(self._state, url + '&filename=%s' % src_base)
Yilin Yangf54fb912020-01-08 11:42:38 +08001208 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001209 msg = json.loads(e.read()).get('error', None)
1210 raise RuntimeError('push: %s' % msg)
1211
1212 pbar = ProgressBar(src_base)
1213 self._HTTPPostFile(url, src, pbar.SetProgress,
1214 self._state.username, self._state.password)
1215 pbar.End()
1216
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001217 def _push_single_target(src, dst):
1218 if os.path.isdir(src):
1219 dst_exists = ast.literal_eval(self.CheckOutput(
1220 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1221 for root, unused_x, files in os.walk(src):
1222 # If destination directory does not exist, we should strip the first
1223 # layer of directory. For example: src_dir contains a single file 'A'
1224 #
1225 # push src_dir dest_dir
1226 #
1227 # If dest_dir exists, the resulting directory structure should be:
1228 # dest_dir/src_dir/A
1229 # If dest_dir does not exist, the resulting directory structure should
1230 # be:
1231 # dest_dir/A
1232 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1233 for name in files:
1234 _push(os.path.join(root, name),
1235 os.path.join(dst, dst_root, name))
1236 else:
1237 _push(src, dst)
1238
1239 if len(args.srcs) > 1:
1240 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1241 '2>/dev/null' % args.dst).strip()
1242 if not dst_type:
1243 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1244 if dst_type != 'directory':
1245 raise RuntimeError('push: %s: Not a directory' % args.dst)
1246
1247 for src in args.srcs:
1248 if not os.path.exists(src):
1249 raise RuntimeError('push: can not stat "%s": no such file or directory'
1250 % src)
1251 if not os.access(src, os.R_OK):
1252 raise RuntimeError('push: can not open "%s" for reading' % src)
1253
1254 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001255
1256 @Command('pull', 'pull a file or directory from remote', [
1257 Arg('src', metavar='SOURCE'),
1258 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1259 def Pull(self, args):
1260 self.CheckClient()
1261
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001262 @AutoRetry('pull', _RETRY_TIMES)
Peter Shihe6afab32018-09-11 17:16:48 +08001263 def _pull(src, dst, ftype, perm=0o644, link=None):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001264 try:
1265 os.makedirs(os.path.dirname(dst))
1266 except Exception:
1267 pass
1268
1269 src_base = os.path.basename(src)
1270
1271 # Remote file is a link
1272 if ftype == 'l':
1273 pbar = ProgressBar(src_base)
1274 if os.path.exists(dst):
1275 os.remove(dst)
1276 os.symlink(link, dst)
1277 pbar.End()
1278 return
1279
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001280 url = ('%s:%d/api/agent/download/%s?filename=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001281 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001282 urllib.parse.quote(self._selected_mid), urllib.parse.quote(src)))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001283 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001284 h = UrlOpen(self._state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +08001285 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001286 msg = json.loads(e.read()).get('error', 'unkown error')
1287 raise RuntimeError('pull: %s' % msg)
1288 except KeyboardInterrupt:
1289 return
1290
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001291 pbar = ProgressBar(src_base)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001292 with open(dst, 'w') as f:
1293 os.fchmod(f.fileno(), perm)
1294 total_size = int(h.headers.get('Content-Length'))
1295 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001296
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001297 while True:
1298 data = h.read(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +08001299 if not data:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001300 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001301 downloaded_size += len(data)
Yilin Yang14d02a22019-11-01 11:32:03 +08001302 pbar.SetProgress(downloaded_size * 100 / total_size,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001303 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001304 f.write(data)
1305 pbar.End()
1306
1307 # Use find to get a listing of all files under a root directory. The 'stat'
1308 # command is used to retrieve the filename and it's filemode.
1309 output = self.CheckOutput(
1310 'cd $HOME; '
1311 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001312 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1313 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001314 % {'src': args.src})
1315
1316 # We got error from the stat command
1317 if output.startswith('stat: '):
1318 sys.stderr.write(output)
1319 return
1320
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001321 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001322 common_prefix = os.path.dirname(args.src)
1323
1324 if len(entries) == 1:
1325 entry = entries[0]
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001326 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001327 if os.path.isdir(args.dst):
1328 dst = os.path.join(args.dst, os.path.basename(src_path))
1329 else:
1330 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001331 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001332 else:
1333 if not os.path.exists(args.dst):
1334 common_prefix = args.src
1335
1336 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001337 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001338 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001339 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1340 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001341
1342 @Command('forward', 'forward remote port to local port', [
1343 Arg('--list', dest='list_all', action='store_true', default=False,
1344 help='list all port forwarding sessions'),
1345 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1346 default=None,
1347 help='remove port forwarding for local port LOCAL_PORT'),
1348 Arg('--remove-all', dest='remove_all', action='store_true',
1349 default=False, help='remove all port forwarding'),
1350 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1351 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1352 def Forward(self, args):
1353 if args.list_all:
1354 max_len = 10
Peter Shihf84a8972017-06-19 15:18:24 +08001355 if self._state.forwards:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001356 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1357
1358 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1359 for local in sorted(self._state.forwards.keys()):
1360 value = self._state.forwards[local]
1361 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1362 return
1363
1364 if args.remove_all:
1365 self._server.RemoveAllForward()
1366 return
1367
1368 if args.remove:
1369 self._server.RemoveForward(args.remove)
1370 return
1371
1372 self.CheckClient()
1373
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001374 if args.remote is None:
1375 raise RuntimeError('remote port not specified')
1376
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001377 if args.local is None:
1378 args.local = args.remote
1379 remote = int(args.remote)
1380 local = int(args.local)
1381
1382 def HandleConnection(conn):
1383 headers = []
1384 if self._state.username is not None and self._state.password is not None:
1385 headers.append(BasicAuthHeader(self._state.username,
1386 self._state.password))
1387
1388 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1389 ws = ForwarderWebSocketClient(
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001390 self._state, conn,
Peter Shihe03450b2017-06-14 14:02:08 +08001391 scheme + '%s:%d/api/agent/forward/%s?port=%d' % (
1392 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001393 urllib.parse.quote(self._selected_mid), remote),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001394 headers=headers)
1395 try:
1396 ws.connect()
1397 ws.run()
1398 except Exception as e:
1399 print('error: %s' % e)
1400 finally:
1401 ws.close()
1402
1403 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1404 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1405 server.bind(('0.0.0.0', local))
1406 server.listen(5)
1407
1408 pid = os.fork()
1409 if pid == 0:
1410 while True:
1411 conn, unused_addr = server.accept()
1412 t = threading.Thread(target=HandleConnection, args=(conn,))
1413 t.daemon = True
1414 t.start()
1415 else:
1416 self._server.AddForward(self._selected_mid, remote, local, pid)
1417
1418
1419def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001420 # Setup logging format
1421 logger = logging.getLogger()
1422 logger.setLevel(logging.INFO)
1423 handler = logging.StreamHandler()
1424 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1425 handler.setFormatter(formatter)
1426 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001427
1428 # Add DaemonState to JSONRPC lib classes
1429 Config.instance().classes.add(DaemonState)
1430
1431 ovl = OverlordCLIClient()
1432 try:
1433 ovl.Main()
1434 except KeyboardInterrupt:
1435 print('Ctrl-C received, abort')
1436 except Exception as e:
1437 print('error: %s' % e)
1438
1439
1440if __name__ == '__main__':
1441 main()