Yilin Yang | 19da693 | 2019-12-10 13:39:28 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 2 | # 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 Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 6 | import argparse |
| 7 | import ast |
| 8 | import base64 |
| 9 | import fcntl |
Peter Shih | 13e78c5 | 2018-01-23 12:57:07 +0800 | [diff] [blame] | 10 | import functools |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 11 | import getpass |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 12 | import hashlib |
Yilin Yang | 752db71 | 2019-09-27 15:42:38 +0800 | [diff] [blame] | 13 | import http.client |
Yilin Yang | 8d4f9d0 | 2019-11-28 17:12:11 +0800 | [diff] [blame] | 14 | from io import StringIO |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 15 | import json |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 16 | import logging |
| 17 | import os |
| 18 | import re |
| 19 | import select |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 20 | import shutil |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 21 | import signal |
| 22 | import socket |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 23 | import ssl |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 24 | import struct |
| 25 | import subprocess |
| 26 | import sys |
| 27 | import tempfile |
| 28 | import termios |
| 29 | import threading |
| 30 | import time |
| 31 | import tty |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 32 | import unicodedata # required by pyinstaller, pylint: disable=unused-import |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 33 | import urllib.error |
| 34 | import urllib.parse |
| 35 | import urllib.request |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 36 | |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 37 | import jsonrpclib |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 38 | from jsonrpclib import config |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 39 | from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 40 | from ws4py.client import WebSocketBaseClient |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 41 | import yaml |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 42 | |
Yilin Yang | 7766841 | 2021-02-02 10:53:36 +0800 | [diff] [blame] | 43 | from cros.factory.utils import net_utils |
Yilin Yang | 83c8f44 | 2020-05-05 13:46:51 +0800 | [diff] [blame] | 44 | from cros.factory.utils import process_utils |
| 45 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 46 | |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 47 | _CERT_DIR = os.path.expanduser('~/.config/ovl') |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 48 | |
| 49 | _ESCAPE = '~' |
| 50 | _BUFSIZ = 8192 |
| 51 | _OVERLORD_PORT = 4455 |
| 52 | _OVERLORD_HTTP_PORT = 9000 |
| 53 | _OVERLORD_CLIENT_DAEMON_PORT = 4488 |
| 54 | _OVERLORD_CLIENT_DAEMON_RPC_ADDR = ('127.0.0.1', _OVERLORD_CLIENT_DAEMON_PORT) |
| 55 | |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 56 | _CONNECT_TIMEOUT = 3 |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 57 | _DEFAULT_HTTP_TIMEOUT = 30 |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 58 | _LIST_CACHE_TIMEOUT = 2 |
| 59 | _DEFAULT_TERMINAL_WIDTH = 80 |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 60 | _RETRY_TIMES = 3 |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 61 | |
| 62 | # echo -n overlord | md5sum |
| 63 | _HTTP_BOUNDARY_MAGIC = '9246f080c855a69012707ab53489b921' |
| 64 | |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 65 | # Terminal resize control |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 66 | _CONTROL_START = 128 |
| 67 | _CONTROL_END = 129 |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 68 | |
| 69 | # Stream control |
| 70 | _STDIN_CLOSED = '##STDIN_CLOSED##' |
| 71 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 72 | _SSH_CONTROL_SOCKET_PREFIX = os.path.join(tempfile.gettempdir(), |
| 73 | 'ovl-ssh-control-') |
| 74 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 75 | _TLS_CERT_FAILED_WARNING = """ |
| 76 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
| 77 | @ WARNING: REMOTE HOST VERIFICATION HAS FAILED! @ |
| 78 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 79 | Failed Reason: %s. |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 80 | |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 81 | Please use -c option to specify path of root CA certificate. |
| 82 | This root CA certificate should be the one that signed the certificate used by |
| 83 | overlord server.""" |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 84 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 85 | |
| 86 | def GetVersionDigest(): |
| 87 | """Return the sha1sum of the current executing script.""" |
Wei-Ning Huang | 3160966 | 2016-08-11 00:22:25 +0800 | [diff] [blame] | 88 | # Check python script by default |
| 89 | filename = __file__ |
| 90 | |
| 91 | # If we are running from a frozen binary, we should calculate the checksum |
| 92 | # against that binary instead of the python script. |
| 93 | # See: https://pyinstaller.readthedocs.io/en/stable/runtime-information.html |
| 94 | if getattr(sys, 'frozen', False): |
| 95 | filename = sys.executable |
| 96 | |
Yilin Yang | 0412c27 | 2019-12-05 16:57:40 +0800 | [diff] [blame] | 97 | with open(filename, 'rb') as f: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 98 | return hashlib.sha1(f.read()).hexdigest() |
| 99 | |
| 100 | |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 101 | def GetTLSCertPath(host): |
| 102 | return os.path.join(_CERT_DIR, '%s.cert' % host) |
| 103 | |
| 104 | |
| 105 | def UrlOpen(state, url): |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 106 | """Wrapper for urllib.request.urlopen. |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 107 | |
| 108 | It selects correct HTTP scheme according to self._state.ssl, add HTTP |
| 109 | basic auth headers, and add specify correct SSL context. |
| 110 | """ |
| 111 | url = MakeRequestUrl(state, url) |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 112 | request = urllib.request.Request(url) |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 113 | if state.username is not None and state.password is not None: |
| 114 | request.add_header(*BasicAuthHeader(state.username, state.password)) |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 115 | return urllib.request.urlopen(request, timeout=_DEFAULT_HTTP_TIMEOUT, |
| 116 | context=state.ssl_context) |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 117 | |
| 118 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 119 | def KillGraceful(pid, wait_secs=1): |
| 120 | """Kill a process gracefully by first sending SIGTERM, wait for some time, |
| 121 | then send SIGKILL to make sure it's killed.""" |
| 122 | try: |
| 123 | os.kill(pid, signal.SIGTERM) |
| 124 | time.sleep(wait_secs) |
| 125 | os.kill(pid, signal.SIGKILL) |
| 126 | except OSError: |
| 127 | pass |
| 128 | |
| 129 | |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 130 | def AutoRetry(action_name, retries): |
| 131 | """Decorator for retry function call.""" |
| 132 | def Wrap(func): |
Peter Shih | 13e78c5 | 2018-01-23 12:57:07 +0800 | [diff] [blame] | 133 | @functools.wraps(func) |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 134 | def Loop(*args, **kwargs): |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 135 | for unused_i in range(retries): |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 136 | try: |
| 137 | func(*args, **kwargs) |
| 138 | except Exception as e: |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 139 | print('error: %s: %s: retrying ...' % (args[0], e)) |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 140 | else: |
| 141 | break |
| 142 | else: |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 143 | print('error: failed to %s %s' % (action_name, args[0])) |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 144 | return Loop |
| 145 | return Wrap |
| 146 | |
| 147 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 148 | def BasicAuthHeader(user, password): |
| 149 | """Return HTTP basic auth header.""" |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 150 | credential = base64.b64encode( |
| 151 | b'%s:%s' % (user.encode('utf-8'), password.encode('utf-8'))) |
| 152 | return ('Authorization', 'Basic %s' % credential.decode('utf-8')) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 153 | |
| 154 | |
| 155 | def GetTerminalSize(): |
| 156 | """Retrieve terminal window size.""" |
| 157 | ws = struct.pack('HHHH', 0, 0, 0, 0) |
| 158 | ws = fcntl.ioctl(0, termios.TIOCGWINSZ, ws) |
| 159 | lines, columns, unused_x, unused_y = struct.unpack('HHHH', ws) |
| 160 | return lines, columns |
| 161 | |
| 162 | |
| 163 | def MakeRequestUrl(state, url): |
| 164 | return 'http%s://%s' % ('s' if state.ssl else '', url) |
| 165 | |
| 166 | |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 167 | class ProgressBar: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 168 | SIZE_WIDTH = 11 |
| 169 | SPEED_WIDTH = 10 |
| 170 | DURATION_WIDTH = 6 |
| 171 | PERCENTAGE_WIDTH = 8 |
| 172 | |
| 173 | def __init__(self, name): |
| 174 | self._start_time = time.time() |
| 175 | self._name = name |
| 176 | self._size = 0 |
| 177 | self._width = 0 |
| 178 | self._name_width = 0 |
| 179 | self._name_max = 0 |
| 180 | self._stat_width = 0 |
| 181 | self._max = 0 |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 182 | self._CalculateSize() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 183 | self.SetProgress(0) |
| 184 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 185 | def _CalculateSize(self): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 186 | self._width = GetTerminalSize()[1] or _DEFAULT_TERMINAL_WIDTH |
| 187 | self._name_width = int(self._width * 0.3) |
| 188 | self._name_max = self._name_width |
| 189 | self._stat_width = self.SIZE_WIDTH + self.SPEED_WIDTH + self.DURATION_WIDTH |
| 190 | self._max = (self._width - self._name_width - self._stat_width - |
| 191 | self.PERCENTAGE_WIDTH) |
| 192 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 193 | def _SizeToHuman(self, size_in_bytes): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 194 | if size_in_bytes < 1024: |
| 195 | unit = 'B' |
| 196 | value = size_in_bytes |
| 197 | elif size_in_bytes < 1024 ** 2: |
| 198 | unit = 'KiB' |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 199 | value = size_in_bytes / 1024 |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 200 | elif size_in_bytes < 1024 ** 3: |
| 201 | unit = 'MiB' |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 202 | value = size_in_bytes / (1024 ** 2) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 203 | elif size_in_bytes < 1024 ** 4: |
| 204 | unit = 'GiB' |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 205 | value = size_in_bytes / (1024 ** 3) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 206 | return ' %6.1f %3s' % (value, unit) |
| 207 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 208 | def _SpeedToHuman(self, speed_in_bs): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 209 | if speed_in_bs < 1024: |
| 210 | unit = 'B' |
| 211 | value = speed_in_bs |
| 212 | elif speed_in_bs < 1024 ** 2: |
| 213 | unit = 'K' |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 214 | value = speed_in_bs / 1024 |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 215 | elif speed_in_bs < 1024 ** 3: |
| 216 | unit = 'M' |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 217 | value = speed_in_bs / (1024 ** 2) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 218 | elif speed_in_bs < 1024 ** 4: |
| 219 | unit = 'G' |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 220 | value = speed_in_bs / (1024 ** 3) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 221 | return ' %6.1f%s/s' % (value, unit) |
| 222 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 223 | def _DurationToClock(self, duration): |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 224 | return ' %02d:%02d' % (duration // 60, duration % 60) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 225 | |
| 226 | def SetProgress(self, percentage, size=None): |
| 227 | current_width = GetTerminalSize()[1] |
| 228 | if self._width != current_width: |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 229 | self._CalculateSize() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 230 | |
| 231 | if size is not None: |
| 232 | self._size = size |
| 233 | |
| 234 | elapse_time = time.time() - self._start_time |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 235 | speed = self._size / elapse_time |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 236 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 237 | size_str = self._SizeToHuman(self._size) |
| 238 | speed_str = self._SpeedToHuman(speed) |
| 239 | elapse_str = self._DurationToClock(elapse_time) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 240 | |
| 241 | width = int(self._max * percentage / 100.0) |
| 242 | sys.stdout.write( |
| 243 | '%*s' % (- self._name_max, |
| 244 | self._name if len(self._name) <= self._name_max else |
| 245 | self._name[:self._name_max - 4] + ' ...') + |
| 246 | size_str + speed_str + elapse_str + |
| 247 | ((' [' + '#' * width + ' ' * (self._max - width) + ']' + |
| 248 | '%4d%%' % int(percentage)) if self._max > 2 else '') + '\r') |
| 249 | sys.stdout.flush() |
| 250 | |
| 251 | def End(self): |
| 252 | self.SetProgress(100.0) |
| 253 | sys.stdout.write('\n') |
| 254 | sys.stdout.flush() |
| 255 | |
| 256 | |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 257 | class DaemonState: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 258 | """DaemonState is used for storing Overlord state info.""" |
| 259 | def __init__(self): |
| 260 | self.version_sha1sum = GetVersionDigest() |
| 261 | self.host = None |
| 262 | self.port = None |
| 263 | self.ssl = False |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 264 | self.ssl_self_signed = False |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 265 | self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 266 | self.ssh = False |
| 267 | self.orig_host = None |
| 268 | self.ssh_pid = None |
| 269 | self.username = None |
| 270 | self.password = None |
| 271 | self.selected_mid = None |
| 272 | self.forwards = {} |
| 273 | self.listing = [] |
| 274 | self.last_list = 0 |
| 275 | |
| 276 | |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 277 | class OverlordClientDaemon: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 278 | """Overlord Client Daemon.""" |
| 279 | def __init__(self): |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 280 | # Use full module path for jsonrpclib to resolve. |
| 281 | import cros.factory.tools.ovl |
| 282 | self._state = cros.factory.tools.ovl.DaemonState() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 283 | self._server = None |
| 284 | |
| 285 | def Start(self): |
| 286 | self.StartRPCServer() |
| 287 | |
| 288 | def StartRPCServer(self): |
| 289 | self._server = SimpleJSONRPCServer(_OVERLORD_CLIENT_DAEMON_RPC_ADDR, |
| 290 | logRequests=False) |
| 291 | exports = [ |
| 292 | (self.State, 'State'), |
| 293 | (self.Ping, 'Ping'), |
| 294 | (self.GetPid, 'GetPid'), |
| 295 | (self.Connect, 'Connect'), |
| 296 | (self.Clients, 'Clients'), |
| 297 | (self.SelectClient, 'SelectClient'), |
| 298 | (self.AddForward, 'AddForward'), |
| 299 | (self.RemoveForward, 'RemoveForward'), |
| 300 | (self.RemoveAllForward, 'RemoveAllForward'), |
| 301 | ] |
| 302 | for func, name in exports: |
| 303 | self._server.register_function(func, name) |
| 304 | |
| 305 | pid = os.fork() |
| 306 | if pid == 0: |
Peter Shih | 4d55ded | 2017-07-03 17:19:01 +0800 | [diff] [blame] | 307 | for fd in range(3): |
| 308 | os.close(fd) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 309 | self._server.serve_forever() |
| 310 | |
| 311 | @staticmethod |
| 312 | def GetRPCServer(): |
| 313 | """Returns the Overlord client daemon RPC server.""" |
| 314 | server = jsonrpclib.Server('http://%s:%d' % |
| 315 | _OVERLORD_CLIENT_DAEMON_RPC_ADDR) |
| 316 | try: |
| 317 | server.Ping() |
| 318 | except Exception: |
| 319 | return None |
| 320 | return server |
| 321 | |
| 322 | def State(self): |
| 323 | return self._state |
| 324 | |
| 325 | def Ping(self): |
| 326 | return True |
| 327 | |
| 328 | def GetPid(self): |
| 329 | return os.getpid() |
| 330 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 331 | def _GetJSON(self, path): |
| 332 | url = '%s:%d%s' % (self._state.host, self._state.port, path) |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 333 | return json.loads(UrlOpen(self._state, url).read()) |
| 334 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 335 | def _TLSEnabled(self): |
| 336 | """Determine if TLS is enabled on given server address.""" |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 337 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 338 | try: |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 339 | # Allow any certificate since we only want to check if server talks TLS. |
| 340 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) |
| 341 | context.verify_mode = ssl.CERT_NONE |
| 342 | |
| 343 | sock = context.wrap_socket(sock, server_hostname=self._state.host) |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 344 | sock.settimeout(_CONNECT_TIMEOUT) |
| 345 | sock.connect((self._state.host, self._state.port)) |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 346 | return True |
Wei-Ning Huang | ecc80b8 | 2016-07-01 16:55:10 +0800 | [diff] [blame] | 347 | except ssl.SSLError: |
| 348 | return False |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 349 | except socket.error: # Connect refused or timeout |
| 350 | raise |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 351 | except Exception: |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 352 | return False # For whatever reason above failed, assume False |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 353 | |
Wei-Ning Huang | 4f8f609 | 2017-06-09 19:35:35 +0800 | [diff] [blame] | 354 | def _CheckTLSCertificate(self, check_hostname=True): |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 355 | """Check TLS certificate. |
| 356 | |
| 357 | Returns: |
| 358 | A tupple (check_result, if_certificate_is_loaded) |
| 359 | """ |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 360 | def _DoConnect(context): |
| 361 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 362 | try: |
| 363 | sock.settimeout(_CONNECT_TIMEOUT) |
| 364 | sock = context.wrap_socket(sock, server_hostname=self._state.host) |
| 365 | sock.connect((self._state.host, self._state.port)) |
| 366 | except ssl.SSLError: |
| 367 | return False |
| 368 | finally: |
| 369 | sock.close() |
| 370 | |
| 371 | # Save SSLContext for future use. |
| 372 | self._state.ssl_context = context |
| 373 | return True |
| 374 | |
| 375 | # First try connect with built-in certificates |
| 376 | tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) |
| 377 | if _DoConnect(tls_context): |
| 378 | return True |
| 379 | |
| 380 | # Try with already saved certificate, if any. |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 381 | tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) |
| 382 | tls_context.verify_mode = ssl.CERT_REQUIRED |
Wei-Ning Huang | 4f8f609 | 2017-06-09 19:35:35 +0800 | [diff] [blame] | 383 | tls_context.check_hostname = check_hostname |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 384 | |
| 385 | tls_cert_path = GetTLSCertPath(self._state.host) |
| 386 | if os.path.exists(tls_cert_path): |
| 387 | tls_context.load_verify_locations(tls_cert_path) |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 388 | self._state.ssl_self_signed = True |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 389 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 390 | return _DoConnect(tls_context) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 391 | |
| 392 | def Connect(self, host, port=_OVERLORD_HTTP_PORT, ssh_pid=None, |
Wei-Ning Huang | 4f8f609 | 2017-06-09 19:35:35 +0800 | [diff] [blame] | 393 | username=None, password=None, orig_host=None, |
| 394 | check_hostname=True): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 395 | self._state.username = username |
| 396 | self._state.password = password |
| 397 | self._state.host = host |
| 398 | self._state.port = port |
| 399 | self._state.ssl = False |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 400 | self._state.ssl_self_signed = False |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 401 | self._state.orig_host = orig_host |
| 402 | self._state.ssh_pid = ssh_pid |
| 403 | self._state.selected_mid = None |
| 404 | |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 405 | tls_enabled = self._TLSEnabled() |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 406 | if tls_enabled: |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 407 | if not os.path.exists(os.path.join(_CERT_DIR, '%s.cert' % host)): |
| 408 | return 'SSLCertificateNotExisted' |
| 409 | |
| 410 | if not self._CheckTLSCertificate(check_hostname): |
| 411 | return 'SSLVerifyFailed' |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 412 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 413 | try: |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 414 | self._state.ssl = tls_enabled |
| 415 | UrlOpen(self._state, '%s:%d' % (host, port)) |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 416 | except urllib.error.HTTPError as e: |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 417 | return ('HTTPError', e.getcode(), str(e), |
| 418 | e.read().strip().decode('utf-8')) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 419 | except Exception as e: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 420 | return str(e) |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 421 | else: |
| 422 | return True |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 423 | |
| 424 | def Clients(self): |
| 425 | if time.time() - self._state.last_list <= _LIST_CACHE_TIMEOUT: |
| 426 | return self._state.listing |
| 427 | |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 428 | self._state.listing = self._GetJSON('/api/agents/list') |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 429 | self._state.last_list = time.time() |
| 430 | return self._state.listing |
| 431 | |
| 432 | def SelectClient(self, mid): |
| 433 | self._state.selected_mid = mid |
| 434 | |
| 435 | def AddForward(self, mid, remote, local, pid): |
| 436 | self._state.forwards[local] = (mid, remote, pid) |
| 437 | |
| 438 | def RemoveForward(self, local_port): |
| 439 | try: |
| 440 | unused_mid, unused_remote, pid = self._state.forwards[local_port] |
| 441 | KillGraceful(pid) |
| 442 | del self._state.forwards[local_port] |
| 443 | except (KeyError, OSError): |
| 444 | pass |
| 445 | |
| 446 | def RemoveAllForward(self): |
| 447 | for unused_mid, unused_remote, pid in self._state.forwards.values(): |
| 448 | try: |
| 449 | KillGraceful(pid) |
| 450 | except OSError: |
| 451 | pass |
| 452 | self._state.forwards = {} |
| 453 | |
| 454 | |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 455 | class SSLEnabledWebSocketBaseClient(WebSocketBaseClient): |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 456 | def __init__(self, state, *args, **kwargs): |
| 457 | cafile = ssl.get_default_verify_paths().openssl_cafile |
| 458 | # For some system / distribution, python can not detect system cafile path. |
| 459 | # In such case we fallback to the default path. |
| 460 | if not os.path.exists(cafile): |
| 461 | cafile = '/etc/ssl/certs/ca-certificates.crt' |
| 462 | |
| 463 | if state.ssl_self_signed: |
| 464 | cafile = GetTLSCertPath(state.host) |
| 465 | |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 466 | ssl_options = { |
| 467 | 'cert_reqs': ssl.CERT_REQUIRED, |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 468 | 'ca_certs': cafile |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 469 | } |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 470 | # ws4py does not allow you to specify SSLContext, but rather passing in the |
| 471 | # argument of ssl.wrap_socket |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 472 | super(SSLEnabledWebSocketBaseClient, self).__init__( |
| 473 | ssl_options=ssl_options, *args, **kwargs) |
| 474 | |
| 475 | |
| 476 | class TerminalWebSocketClient(SSLEnabledWebSocketBaseClient): |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 477 | def __init__(self, state, mid, escape, *args, **kwargs): |
| 478 | super(TerminalWebSocketClient, self).__init__(state, *args, **kwargs) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 479 | self._mid = mid |
Wei-Ning Huang | 0c520e9 | 2016-03-19 20:01:10 +0800 | [diff] [blame] | 480 | self._escape = escape |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 481 | self._stdin_fd = sys.stdin.fileno() |
| 482 | self._old_termios = None |
| 483 | |
| 484 | def handshake_ok(self): |
| 485 | pass |
| 486 | |
| 487 | def opened(self): |
| 488 | nonlocals = {'size': (80, 40)} |
| 489 | |
| 490 | def _ResizeWindow(): |
| 491 | size = GetTerminalSize() |
| 492 | if size != nonlocals['size']: # Size not changed, ignore |
| 493 | control = {'command': 'resize', 'params': list(size)} |
| 494 | payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END) |
| 495 | nonlocals['size'] = size |
| 496 | try: |
| 497 | self.send(payload, binary=True) |
| 498 | except Exception: |
| 499 | pass |
| 500 | |
| 501 | def _FeedInput(): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 502 | self._old_termios = termios.tcgetattr(self._stdin_fd) |
| 503 | tty.setraw(self._stdin_fd) |
| 504 | |
| 505 | READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3) |
| 506 | |
| 507 | try: |
| 508 | state = READY |
| 509 | while True: |
Wei-Ning Huang | 85e763d | 2016-01-21 15:53:18 +0800 | [diff] [blame] | 510 | # Check if terminal is resized |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 511 | _ResizeWindow() |
| 512 | |
Wei-Ning Huang | 85e763d | 2016-01-21 15:53:18 +0800 | [diff] [blame] | 513 | ch = sys.stdin.read(1) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 514 | |
Wei-Ning Huang | 85e763d | 2016-01-21 15:53:18 +0800 | [diff] [blame] | 515 | # Scan for escape sequence |
Wei-Ning Huang | 0c520e9 | 2016-03-19 20:01:10 +0800 | [diff] [blame] | 516 | if self._escape: |
| 517 | if state == READY: |
| 518 | state = ENTER_PRESSED if ch == chr(0x0d) else READY |
| 519 | elif state == ENTER_PRESSED: |
| 520 | state = ESCAPE_PRESSED if ch == self._escape else READY |
| 521 | elif state == ESCAPE_PRESSED: |
| 522 | if ch == '.': |
| 523 | self.close() |
| 524 | break |
| 525 | else: |
| 526 | state = READY |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 527 | |
Wei-Ning Huang | 85e763d | 2016-01-21 15:53:18 +0800 | [diff] [blame] | 528 | self.send(ch) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 529 | except (KeyboardInterrupt, RuntimeError): |
| 530 | pass |
| 531 | |
| 532 | t = threading.Thread(target=_FeedInput) |
| 533 | t.daemon = True |
| 534 | t.start() |
| 535 | |
| 536 | def closed(self, code, reason=None): |
Peter Shih | f84a897 | 2017-06-19 15:18:24 +0800 | [diff] [blame] | 537 | del code, reason # Unused. |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 538 | termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios) |
| 539 | print('Connection to %s closed.' % self._mid) |
| 540 | |
Yilin Yang | f64670b | 2020-01-06 11:22:18 +0800 | [diff] [blame] | 541 | def received_message(self, message): |
| 542 | if message.is_binary: |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 543 | sys.stdout.write(message.data.decode('utf-8')) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 544 | sys.stdout.flush() |
| 545 | |
| 546 | |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 547 | class ShellWebSocketClient(SSLEnabledWebSocketBaseClient): |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 548 | def __init__(self, state, output, *args, **kwargs): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 549 | """Constructor. |
| 550 | |
| 551 | Args: |
| 552 | output: output file object. |
| 553 | """ |
| 554 | self.output = output |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 555 | super(ShellWebSocketClient, self).__init__(state, *args, **kwargs) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 556 | |
| 557 | def handshake_ok(self): |
| 558 | pass |
| 559 | |
| 560 | def opened(self): |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 561 | def _FeedInput(): |
| 562 | try: |
| 563 | while True: |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 564 | data = sys.stdin.buffer.read(1) |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 565 | |
Peter Shih | f84a897 | 2017-06-19 15:18:24 +0800 | [diff] [blame] | 566 | if not data: |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 567 | self.send(_STDIN_CLOSED * 2) |
| 568 | break |
| 569 | self.send(data, binary=True) |
| 570 | except (KeyboardInterrupt, RuntimeError): |
| 571 | pass |
| 572 | |
| 573 | t = threading.Thread(target=_FeedInput) |
| 574 | t.daemon = True |
| 575 | t.start() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 576 | |
| 577 | def closed(self, code, reason=None): |
| 578 | pass |
| 579 | |
Yilin Yang | f64670b | 2020-01-06 11:22:18 +0800 | [diff] [blame] | 580 | def received_message(self, message): |
| 581 | if message.is_binary: |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 582 | self.output.write(message.data.decode('utf-8')) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 583 | self.output.flush() |
| 584 | |
| 585 | |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 586 | class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient): |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 587 | def __init__(self, state, sock, *args, **kwargs): |
| 588 | super(ForwarderWebSocketClient, self).__init__(state, *args, **kwargs) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 589 | self._sock = sock |
| 590 | self._stop = threading.Event() |
| 591 | |
| 592 | def handshake_ok(self): |
| 593 | pass |
| 594 | |
| 595 | def opened(self): |
| 596 | def _FeedInput(): |
| 597 | try: |
| 598 | self._sock.setblocking(False) |
| 599 | while True: |
| 600 | rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5) |
| 601 | if self._stop.is_set(): |
| 602 | break |
| 603 | if self._sock in rd: |
| 604 | data = self._sock.recv(_BUFSIZ) |
Peter Shih | f84a897 | 2017-06-19 15:18:24 +0800 | [diff] [blame] | 605 | if not data: |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 606 | self.close() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 607 | break |
| 608 | self.send(data, binary=True) |
| 609 | except Exception: |
| 610 | pass |
| 611 | finally: |
| 612 | self._sock.close() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 613 | |
| 614 | t = threading.Thread(target=_FeedInput) |
| 615 | t.daemon = True |
| 616 | t.start() |
| 617 | |
| 618 | def closed(self, code, reason=None): |
Peter Shih | f84a897 | 2017-06-19 15:18:24 +0800 | [diff] [blame] | 619 | del code, reason # Unused. |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 620 | self._stop.set() |
| 621 | sys.exit(0) |
| 622 | |
Yilin Yang | f64670b | 2020-01-06 11:22:18 +0800 | [diff] [blame] | 623 | def received_message(self, message): |
| 624 | if message.is_binary: |
| 625 | self._sock.send(message.data) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 626 | |
| 627 | |
| 628 | def Arg(*args, **kwargs): |
| 629 | return (args, kwargs) |
| 630 | |
| 631 | |
| 632 | def Command(command, help_msg=None, args=None): |
| 633 | """Decorator for adding argparse parameter for a method.""" |
| 634 | if args is None: |
| 635 | args = [] |
| 636 | def WrapFunc(func): |
Peter Shih | 13e78c5 | 2018-01-23 12:57:07 +0800 | [diff] [blame] | 637 | @functools.wraps(func) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 638 | def Wrapped(*args, **kwargs): |
| 639 | return func(*args, **kwargs) |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 640 | # pylint: disable=protected-access |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 641 | Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args} |
| 642 | return Wrapped |
| 643 | return WrapFunc |
| 644 | |
| 645 | |
| 646 | def ParseMethodSubCommands(cls): |
| 647 | """Decorator for a class using the @Command decorator. |
| 648 | |
| 649 | This decorator retrieve command info from each method and append it in to the |
| 650 | SUBCOMMANDS class variable, which is later used to construct parser. |
| 651 | """ |
Yilin Yang | 879fbda | 2020-05-14 13:52:30 +0800 | [diff] [blame] | 652 | for unused_key, method in cls.__dict__.items(): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 653 | if hasattr(method, '__arg_attr'): |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 654 | # pylint: disable=protected-access |
| 655 | cls.SUBCOMMANDS.append(method.__arg_attr) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 656 | return cls |
| 657 | |
| 658 | |
| 659 | @ParseMethodSubCommands |
Fei Shao | bd07c9a | 2020-06-15 19:04:50 +0800 | [diff] [blame] | 660 | class OverlordCLIClient: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 661 | """Overlord command line interface client.""" |
| 662 | |
| 663 | SUBCOMMANDS = [] |
| 664 | |
| 665 | def __init__(self): |
| 666 | self._parser = self._BuildParser() |
| 667 | self._selected_mid = None |
| 668 | self._server = None |
| 669 | self._state = None |
Wei-Ning Huang | 0c520e9 | 2016-03-19 20:01:10 +0800 | [diff] [blame] | 670 | self._escape = None |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 671 | |
| 672 | def _BuildParser(self): |
| 673 | root_parser = argparse.ArgumentParser(prog='ovl') |
| 674 | subparsers = root_parser.add_subparsers(help='sub-command') |
| 675 | |
| 676 | root_parser.add_argument('-s', dest='selected_mid', action='store', |
| 677 | default=None, |
| 678 | help='select target to execute command on') |
| 679 | root_parser.add_argument('-S', dest='select_mid_before_action', |
| 680 | action='store_true', default=False, |
| 681 | help='select target before executing command') |
Wei-Ning Huang | 0c520e9 | 2016-03-19 20:01:10 +0800 | [diff] [blame] | 682 | root_parser.add_argument('-e', dest='escape', metavar='ESCAPE_CHAR', |
| 683 | action='store', default=_ESCAPE, type=str, |
| 684 | help='set shell escape character, \'none\' to ' |
| 685 | 'disable escape completely') |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 686 | |
| 687 | for attr in self.SUBCOMMANDS: |
| 688 | parser = subparsers.add_parser(attr['command'], help=attr['help']) |
| 689 | parser.set_defaults(which=attr['command']) |
| 690 | for arg in attr['args']: |
| 691 | parser.add_argument(*arg[0], **arg[1]) |
| 692 | |
| 693 | return root_parser |
| 694 | |
| 695 | def Main(self): |
| 696 | # We want to pass the rest of arguments after shell command directly to the |
| 697 | # function without parsing it. |
| 698 | try: |
| 699 | index = sys.argv.index('shell') |
| 700 | except ValueError: |
| 701 | args = self._parser.parse_args() |
| 702 | else: |
| 703 | args = self._parser.parse_args(sys.argv[1:index + 1]) |
| 704 | |
| 705 | command = args.which |
| 706 | self._selected_mid = args.selected_mid |
| 707 | |
Wei-Ning Huang | 0c520e9 | 2016-03-19 20:01:10 +0800 | [diff] [blame] | 708 | if args.escape and args.escape != 'none': |
| 709 | self._escape = args.escape[0] |
| 710 | |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 711 | if command == 'start-server': |
| 712 | self.StartServer() |
| 713 | return |
Fei Shao | 12ecf38 | 2020-06-23 18:32:26 +0800 | [diff] [blame] | 714 | if command == 'kill-server': |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 715 | self.KillServer() |
| 716 | return |
| 717 | |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 718 | self.CheckDaemon() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 719 | if command == 'status': |
| 720 | self.Status() |
| 721 | return |
Fei Shao | 12ecf38 | 2020-06-23 18:32:26 +0800 | [diff] [blame] | 722 | if command == 'connect': |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 723 | self.Connect(args) |
| 724 | return |
| 725 | |
| 726 | # The following command requires connection to the server |
| 727 | self.CheckConnection() |
| 728 | |
| 729 | if args.select_mid_before_action: |
| 730 | self.SelectClient(store=False) |
| 731 | |
| 732 | if command == 'select': |
| 733 | self.SelectClient(args) |
| 734 | elif command == 'ls': |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 735 | self.ListClients(args) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 736 | elif command == 'shell': |
| 737 | command = sys.argv[sys.argv.index('shell') + 1:] |
| 738 | self.Shell(command) |
| 739 | elif command == 'push': |
| 740 | self.Push(args) |
| 741 | elif command == 'pull': |
| 742 | self.Pull(args) |
| 743 | elif command == 'forward': |
| 744 | self.Forward(args) |
| 745 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 746 | def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None): |
| 747 | """Perform HTTP POST and upload file to Overlord. |
| 748 | |
| 749 | To minimize the external dependencies, we construct the HTTP post request |
| 750 | by ourselves. |
| 751 | """ |
| 752 | url = MakeRequestUrl(self._state, url) |
| 753 | size = os.stat(filename).st_size |
| 754 | boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC |
| 755 | CRLF = '\r\n' |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 756 | parse = urllib.parse.urlparse(url) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 757 | |
| 758 | part_headers = [ |
| 759 | '--' + boundary, |
| 760 | 'Content-Disposition: form-data; name="file"; ' |
| 761 | 'filename="%s"' % os.path.basename(filename), |
| 762 | 'Content-Type: application/octet-stream', |
| 763 | '', '' |
| 764 | ] |
| 765 | part_header = CRLF.join(part_headers) |
| 766 | end_part = CRLF + '--' + boundary + '--' + CRLF |
| 767 | |
| 768 | content_length = len(part_header) + size + len(end_part) |
| 769 | if parse.scheme == 'http': |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 770 | h = http.client.HTTPConnection(parse.netloc) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 771 | else: |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 772 | h = http.client.HTTPSConnection(parse.netloc, |
| 773 | context=self._state.ssl_context) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 774 | |
| 775 | post_path = url[url.index(parse.netloc) + len(parse.netloc):] |
| 776 | h.putrequest('POST', post_path) |
| 777 | h.putheader('Content-Length', content_length) |
| 778 | h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary) |
| 779 | |
| 780 | if user and passwd: |
| 781 | h.putheader(*BasicAuthHeader(user, passwd)) |
| 782 | h.endheaders() |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 783 | h.send(part_header.encode('utf-8')) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 784 | |
| 785 | count = 0 |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 786 | with open(filename, 'rb') as f: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 787 | while True: |
| 788 | data = f.read(_BUFSIZ) |
| 789 | if not data: |
| 790 | break |
| 791 | count += len(data) |
| 792 | if progress: |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 793 | progress(count * 100 // size, count) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 794 | h.send(data) |
| 795 | |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 796 | h.send(end_part.encode('utf-8')) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 797 | progress(100) |
| 798 | |
| 799 | if count != size: |
| 800 | logging.warning('file changed during upload, upload may be truncated.') |
| 801 | |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 802 | resp = h.getresponse() |
| 803 | return resp.status == 200 |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 804 | |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 805 | def CheckDaemon(self): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 806 | self._server = OverlordClientDaemon.GetRPCServer() |
| 807 | if self._server is None: |
| 808 | print('* daemon not running, starting it now on port %d ... *' % |
| 809 | _OVERLORD_CLIENT_DAEMON_PORT) |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 810 | self.StartServer() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 811 | |
| 812 | self._state = self._server.State() |
| 813 | sha1sum = GetVersionDigest() |
| 814 | |
| 815 | if sha1sum != self._state.version_sha1sum: |
| 816 | print('ovl server is out of date. killing...') |
| 817 | KillGraceful(self._server.GetPid()) |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 818 | self.StartServer() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 819 | |
| 820 | def GetSSHControlFile(self, host): |
| 821 | return _SSH_CONTROL_SOCKET_PREFIX + host |
| 822 | |
| 823 | def SSHTunnel(self, user, host, port): |
| 824 | """SSH forward the remote overlord server. |
| 825 | |
| 826 | Overlord server may not have port 9000 open to the public network, in such |
| 827 | case we can SSH forward the port to localhost. |
| 828 | """ |
| 829 | |
| 830 | control_file = self.GetSSHControlFile(host) |
| 831 | try: |
| 832 | os.unlink(control_file) |
| 833 | except Exception: |
| 834 | pass |
| 835 | |
| 836 | subprocess.Popen([ |
| 837 | 'ssh', '-Nf', |
| 838 | '-M', # Enable master mode |
| 839 | '-S', control_file, |
| 840 | '-L', '9000:localhost:9000', |
| 841 | '-p', str(port), |
| 842 | '%s%s' % (user + '@' if user else '', host) |
| 843 | ]).wait() |
| 844 | |
Yilin Yang | 83c8f44 | 2020-05-05 13:46:51 +0800 | [diff] [blame] | 845 | p = process_utils.Spawn([ |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 846 | 'ssh', |
| 847 | '-S', control_file, |
| 848 | '-O', 'check', host, |
Yilin Yang | 83c8f44 | 2020-05-05 13:46:51 +0800 | [diff] [blame] | 849 | ], read_stderr=True, ignore_stdout=True) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 850 | |
Yilin Yang | 83c8f44 | 2020-05-05 13:46:51 +0800 | [diff] [blame] | 851 | s = re.search(r'pid=(\d+)', p.stderr_data) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 852 | if s: |
| 853 | return int(s.group(1)) |
| 854 | |
| 855 | raise RuntimeError('can not establish ssh connection') |
| 856 | |
| 857 | def CheckConnection(self): |
| 858 | if self._state.host is None: |
| 859 | raise RuntimeError('not connected to any server, abort') |
| 860 | |
| 861 | try: |
| 862 | self._server.Clients() |
| 863 | except Exception: |
| 864 | raise RuntimeError('remote server disconnected, abort') |
| 865 | |
| 866 | if self._state.ssh_pid is not None: |
| 867 | ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)], |
| 868 | stdout=subprocess.PIPE, |
| 869 | stderr=subprocess.PIPE).wait() |
| 870 | if ret != 0: |
| 871 | raise RuntimeError('ssh tunnel disconnected, please re-connect') |
| 872 | |
| 873 | def CheckClient(self): |
| 874 | if self._selected_mid is None: |
| 875 | if self._state.selected_mid is None: |
| 876 | raise RuntimeError('No client is selected') |
| 877 | self._selected_mid = self._state.selected_mid |
| 878 | |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 879 | if not any(client['mid'] == self._selected_mid |
| 880 | for client in self._server.Clients()): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 881 | raise RuntimeError('client %s disappeared' % self._selected_mid) |
| 882 | |
| 883 | def CheckOutput(self, command): |
| 884 | headers = [] |
| 885 | if self._state.username is not None and self._state.password is not None: |
| 886 | headers.append(BasicAuthHeader(self._state.username, |
| 887 | self._state.password)) |
| 888 | |
| 889 | scheme = 'ws%s://' % ('s' if self._state.ssl else '') |
Yilin Yang | 8d4f9d0 | 2019-11-28 17:12:11 +0800 | [diff] [blame] | 890 | sio = StringIO() |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 891 | ws = ShellWebSocketClient( |
| 892 | self._state, sio, scheme + '%s:%d/api/agent/shell/%s?command=%s' % ( |
| 893 | self._state.host, self._state.port, |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 894 | urllib.parse.quote(self._selected_mid), |
| 895 | urllib.parse.quote(command)), |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 896 | headers=headers) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 897 | ws.connect() |
| 898 | ws.run() |
| 899 | return sio.getvalue() |
| 900 | |
| 901 | @Command('status', 'show Overlord connection status') |
| 902 | def Status(self): |
| 903 | if self._state.host is None: |
| 904 | print('Not connected to any host.') |
| 905 | else: |
| 906 | if self._state.ssh_pid is not None: |
| 907 | print('Connected to %s with SSH tunneling.' % self._state.orig_host) |
| 908 | else: |
| 909 | print('Connected to %s:%d.' % (self._state.host, self._state.port)) |
| 910 | |
| 911 | if self._selected_mid is None: |
| 912 | self._selected_mid = self._state.selected_mid |
| 913 | |
| 914 | if self._selected_mid is None: |
| 915 | print('No client is selected.') |
| 916 | else: |
| 917 | print('Client %s selected.' % self._selected_mid) |
| 918 | |
| 919 | @Command('connect', 'connect to Overlord server', [ |
| 920 | Arg('host', metavar='HOST', type=str, default='localhost', |
| 921 | help='Overlord hostname/IP'), |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 922 | Arg('port', metavar='PORT', type=int, default=_OVERLORD_HTTP_PORT, |
| 923 | help='Overlord port'), |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 924 | Arg('-f', '--forward', dest='ssh_forward', default=False, |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 925 | action='store_true', help='connect with SSH forwarding to the host'), |
| 926 | Arg('-p', '--ssh-port', dest='ssh_port', default=22, type=int, |
| 927 | help='SSH server port for SSH forwarding'), |
| 928 | Arg('-l', '--ssh-login', dest='ssh_login', default='', type=str, |
| 929 | help='SSH server login name for SSH forwarding'), |
| 930 | Arg('-u', '--user', dest='user', default=None, type=str, |
| 931 | help='Overlord HTTP auth username'), |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 932 | Arg('-w', '--passwd', dest='passwd', default=None, type=str, |
Wei-Ning Huang | 4f8f609 | 2017-06-09 19:35:35 +0800 | [diff] [blame] | 933 | help='Overlord HTTP auth password'), |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 934 | Arg('-c', '--root-CA', dest='cert', default=None, type=str, |
| 935 | help='Path to root CA certificate, only assign at the first time'), |
| 936 | Arg('-i', '--no-check-hostname', dest='check_hostname', default=True, |
Yilin Yang | 7766841 | 2021-02-02 10:53:36 +0800 | [diff] [blame] | 937 | action='store_false', help='Ignore SSL cert hostname check'), |
| 938 | Arg('-b', '--certificate-dir', dest='certificate_dir', default=None, |
| 939 | type=str, help='Path to overlord certificate directory') |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 940 | ]) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 941 | def Connect(self, args): |
| 942 | ssh_pid = None |
| 943 | host = args.host |
| 944 | orig_host = args.host |
| 945 | |
Yilin Yang | 7766841 | 2021-02-02 10:53:36 +0800 | [diff] [blame] | 946 | if args.certificate_dir: |
| 947 | args.cert = os.path.join(args.certificate_dir, 'rootCA.pem') |
| 948 | |
| 949 | ovl_password_file = os.path.join(args.certificate_dir, 'ovl_password') |
| 950 | with open(ovl_password_file, 'r') as f: |
| 951 | args.passwd = f.read().strip() |
| 952 | args.user = 'ovl' |
| 953 | |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 954 | if args.cert and os.path.exists(args.cert): |
| 955 | os.makedirs(_CERT_DIR, exist_ok=True) |
| 956 | shutil.copy(args.cert, os.path.join(_CERT_DIR, '%s.cert' % host)) |
| 957 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 958 | if args.ssh_forward: |
| 959 | # Kill previous SSH tunnel |
| 960 | self.KillSSHTunnel() |
| 961 | |
| 962 | ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port) |
| 963 | host = 'localhost' |
| 964 | |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 965 | username_provided = args.user is not None |
| 966 | password_provided = args.passwd is not None |
| 967 | prompt = False |
| 968 | |
Peter Shih | 533566a | 2018-09-05 17:48:03 +0800 | [diff] [blame] | 969 | for unused_i in range(3): # pylint: disable=too-many-nested-blocks |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 970 | try: |
| 971 | if prompt: |
| 972 | if not username_provided: |
Yilin Yang | 8cc5dfb | 2019-10-22 15:58:53 +0800 | [diff] [blame] | 973 | args.user = input('Username: ') |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 974 | if not password_provided: |
| 975 | args.passwd = getpass.getpass('Password: ') |
| 976 | |
| 977 | ret = self._server.Connect(host, args.port, ssh_pid, args.user, |
Wei-Ning Huang | 4f8f609 | 2017-06-09 19:35:35 +0800 | [diff] [blame] | 978 | args.passwd, orig_host, |
| 979 | args.check_hostname) |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 980 | if isinstance(ret, list): |
Fei Shao | 12ecf38 | 2020-06-23 18:32:26 +0800 | [diff] [blame] | 981 | if ret[0] == 'HTTPError': |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 982 | code, except_str, body = ret[1:] |
| 983 | if code == 401: |
| 984 | print('connect: %s' % body) |
| 985 | prompt = True |
| 986 | if not username_provided or not password_provided: |
| 987 | continue |
Fei Shao | f91ab8f | 2020-06-23 17:54:03 +0800 | [diff] [blame] | 988 | break |
Fei Shao | 0e4e2c6 | 2020-06-23 18:22:26 +0800 | [diff] [blame] | 989 | logging.error('%s; %s', except_str, body) |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 990 | |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 991 | if ret in ('SSLCertificateNotExisted', 'SSLVerifyFailed'): |
| 992 | print(_TLS_CERT_FAILED_WARNING % ret) |
| 993 | return |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 994 | if ret is not True: |
| 995 | print('can not connect to %s: %s' % (host, ret)) |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 996 | else: |
| 997 | print('connection to %s:%d established.' % (host, args.port)) |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 998 | except Exception as e: |
Yilin Yang | 3db92cc | 2020-10-26 15:31:47 +0800 | [diff] [blame] | 999 | logging.exception(e) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1000 | else: |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 1001 | break |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1002 | |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 1003 | @Command('start-server', 'start overlord CLI client server') |
| 1004 | def StartServer(self): |
| 1005 | self._server = OverlordClientDaemon.GetRPCServer() |
| 1006 | if self._server is None: |
| 1007 | OverlordClientDaemon().Start() |
| 1008 | time.sleep(1) |
| 1009 | self._server = OverlordClientDaemon.GetRPCServer() |
| 1010 | if self._server is not None: |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 1011 | print('* daemon started successfully *\n') |
Wei-Ning Huang | 5564eea | 2016-01-19 14:36:45 +0800 | [diff] [blame] | 1012 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1013 | @Command('kill-server', 'kill overlord CLI client server') |
| 1014 | def KillServer(self): |
| 1015 | self._server = OverlordClientDaemon.GetRPCServer() |
| 1016 | if self._server is None: |
| 1017 | return |
| 1018 | |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1019 | self._state = self._server.State() |
| 1020 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1021 | # Kill SSH Tunnel |
| 1022 | self.KillSSHTunnel() |
| 1023 | |
| 1024 | # Kill server daemon |
| 1025 | KillGraceful(self._server.GetPid()) |
| 1026 | |
| 1027 | def KillSSHTunnel(self): |
| 1028 | if self._state.ssh_pid is not None: |
| 1029 | KillGraceful(self._state.ssh_pid) |
| 1030 | |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1031 | def _FilterClients(self, clients, prop_filters, mid=None): |
| 1032 | def _ClientPropertiesMatch(client, key, regex): |
| 1033 | try: |
| 1034 | return bool(re.search(regex, client['properties'][key])) |
| 1035 | except KeyError: |
| 1036 | return False |
| 1037 | |
| 1038 | for prop_filter in prop_filters: |
| 1039 | key, sep, regex = prop_filter.partition('=') |
| 1040 | if not sep: |
| 1041 | # The filter doesn't contains =. |
| 1042 | raise ValueError('Invalid filter condition %r' % filter) |
| 1043 | clients = [c for c in clients if _ClientPropertiesMatch(c, key, regex)] |
| 1044 | |
| 1045 | if mid is not None: |
| 1046 | client = next((c for c in clients if c['mid'] == mid), None) |
| 1047 | if client: |
| 1048 | return [client] |
| 1049 | clients = [c for c in clients if c['mid'].startswith(mid)] |
| 1050 | return clients |
| 1051 | |
| 1052 | @Command('ls', 'list clients', [ |
Yilin Yang | f35f5cd | 2020-11-11 12:49:36 +0800 | [diff] [blame] | 1053 | Arg( |
| 1054 | '-f', '--filter', default=[], dest='filters', action='append', |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1055 | help=('Conditions to filter clients by properties. ' |
| 1056 | 'Should be in form "key=regex", where regex is the regular ' |
| 1057 | 'expression that should be found in the value. ' |
| 1058 | 'Multiple --filter arguments would be ANDed.')), |
Yilin Yang | f35f5cd | 2020-11-11 12:49:36 +0800 | [diff] [blame] | 1059 | Arg('-m', '--mid-only', default=False, action='store_true', |
| 1060 | help='Print mid only.'), |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 1061 | Arg('-v', '--verbose', default=False, action='store_true', |
| 1062 | help='Print properties of each client.') |
| 1063 | ]) |
| 1064 | def ListClients(self, args): |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1065 | clients = self._FilterClients(self._server.Clients(), args.filters) |
Yilin Yang | f35f5cd | 2020-11-11 12:49:36 +0800 | [diff] [blame] | 1066 | |
| 1067 | if args.verbose: |
| 1068 | for client in clients: |
Peter Shih | 99b73ec | 2017-06-16 17:54:15 +0800 | [diff] [blame] | 1069 | print(yaml.safe_dump(client, default_flow_style=False)) |
Yilin Yang | f35f5cd | 2020-11-11 12:49:36 +0800 | [diff] [blame] | 1070 | return |
| 1071 | |
| 1072 | # Used in station_setup to ckeck if there is duplicate mid. |
| 1073 | if args.mid_only: |
| 1074 | for client in clients: |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1075 | print(client['mid']) |
Yilin Yang | f35f5cd | 2020-11-11 12:49:36 +0800 | [diff] [blame] | 1076 | return |
| 1077 | |
| 1078 | def FormatPrint(length, string): |
| 1079 | print('%*s' % (length + 2, string), end='|') |
| 1080 | |
Yilin Yang | dc34759 | 2021-02-03 11:33:04 +0800 | [diff] [blame] | 1081 | columns = [ |
| 1082 | 'mid', 'serial', 'status', 'pytest', 'model', 'ip', 'track_connection' |
| 1083 | ] |
Yilin Yang | f35f5cd | 2020-11-11 12:49:36 +0800 | [diff] [blame] | 1084 | columns_max_len = {column: len(column) |
| 1085 | for column in columns} |
| 1086 | |
| 1087 | for client in clients: |
| 1088 | for column in columns: |
| 1089 | columns_max_len[column] = max(columns_max_len[column], |
| 1090 | len(str(client[column]))) |
| 1091 | |
| 1092 | for column in columns: |
| 1093 | FormatPrint(columns_max_len[column], column) |
| 1094 | print() |
| 1095 | |
| 1096 | for client in clients: |
| 1097 | for column in columns: |
| 1098 | FormatPrint(columns_max_len[column], str(client[column])) |
| 1099 | print() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1100 | |
| 1101 | @Command('select', 'select default client', [ |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1102 | Arg('-f', '--filter', default=[], dest='filters', action='append', |
| 1103 | help=('Conditions to filter clients by properties. ' |
| 1104 | 'Should be in form "key=regex", where regex is the regular ' |
| 1105 | 'expression that should be found in the value. ' |
| 1106 | 'Multiple --filter arguments would be ANDed.')), |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1107 | Arg('mid', metavar='mid', nargs='?', default=None)]) |
| 1108 | def SelectClient(self, args=None, store=True): |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1109 | mid = args.mid if args is not None else None |
Wei-Ning Huang | 6796a13 | 2017-06-22 14:46:32 +0800 | [diff] [blame] | 1110 | filters = args.filters if args is not None else [] |
| 1111 | clients = self._FilterClients(self._server.Clients(), filters, mid=mid) |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1112 | |
| 1113 | if not clients: |
| 1114 | raise RuntimeError('select: client not found') |
Fei Shao | 0e4e2c6 | 2020-06-23 18:22:26 +0800 | [diff] [blame] | 1115 | if len(clients) == 1: |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1116 | mid = clients[0]['mid'] |
| 1117 | else: |
| 1118 | # This case would not happen when args.mid is specified. |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1119 | print('Select from the following clients:') |
| 1120 | for i, client in enumerate(clients): |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1121 | print(' %d. %s' % (i + 1, client['mid'])) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1122 | |
| 1123 | print('\nSelection: ', end='') |
| 1124 | try: |
Yilin Yang | 8cc5dfb | 2019-10-22 15:58:53 +0800 | [diff] [blame] | 1125 | choice = int(input()) - 1 |
Peter Shih | cf0f3b2 | 2017-06-19 15:59:22 +0800 | [diff] [blame] | 1126 | mid = clients[choice]['mid'] |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1127 | except ValueError: |
| 1128 | raise RuntimeError('select: invalid selection') |
| 1129 | except IndexError: |
| 1130 | raise RuntimeError('select: selection out of range') |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1131 | |
| 1132 | self._selected_mid = mid |
| 1133 | if store: |
| 1134 | self._server.SelectClient(mid) |
| 1135 | print('Client %s selected' % mid) |
| 1136 | |
| 1137 | @Command('shell', 'open a shell or execute a shell command', [ |
| 1138 | Arg('command', metavar='CMD', nargs='?', help='command to execute')]) |
| 1139 | def Shell(self, command=None): |
| 1140 | if command is None: |
| 1141 | command = [] |
| 1142 | self.CheckClient() |
| 1143 | |
| 1144 | headers = [] |
| 1145 | if self._state.username is not None and self._state.password is not None: |
| 1146 | headers.append(BasicAuthHeader(self._state.username, |
| 1147 | self._state.password)) |
| 1148 | |
| 1149 | scheme = 'ws%s://' % ('s' if self._state.ssl else '') |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 1150 | if command: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1151 | cmd = ' '.join(command) |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 1152 | ws = ShellWebSocketClient( |
| 1153 | self._state, sys.stdout, |
| 1154 | scheme + '%s:%d/api/agent/shell/%s?command=%s' % ( |
| 1155 | self._state.host, self._state.port, |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 1156 | urllib.parse.quote(self._selected_mid), urllib.parse.quote(cmd)), |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 1157 | headers=headers) |
| 1158 | else: |
| 1159 | ws = TerminalWebSocketClient( |
| 1160 | self._state, self._selected_mid, self._escape, |
| 1161 | scheme + '%s:%d/api/agent/tty/%s' % ( |
| 1162 | self._state.host, self._state.port, |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 1163 | urllib.parse.quote(self._selected_mid)), |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 1164 | headers=headers) |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 1165 | try: |
| 1166 | ws.connect() |
| 1167 | ws.run() |
| 1168 | except socket.error as e: |
| 1169 | if e.errno == 32: # Broken pipe |
| 1170 | pass |
| 1171 | else: |
| 1172 | raise |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1173 | |
| 1174 | @Command('push', 'push a file or directory to remote', [ |
Wei-Ning Huang | 37e6110 | 2016-02-07 13:41:07 +0800 | [diff] [blame] | 1175 | Arg('srcs', nargs='+', metavar='SOURCE'), |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1176 | Arg('dst', metavar='DESTINATION')]) |
| 1177 | def Push(self, args): |
| 1178 | self.CheckClient() |
| 1179 | |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1180 | @AutoRetry('push', _RETRY_TIMES) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1181 | def _push(src, dst): |
| 1182 | src_base = os.path.basename(src) |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1183 | |
| 1184 | # Local file is a link |
| 1185 | if os.path.islink(src): |
| 1186 | pbar = ProgressBar(src_base) |
| 1187 | link_path = os.readlink(src) |
| 1188 | self.CheckOutput('mkdir -p %(dirname)s; ' |
| 1189 | 'if [ -d "%(dst)s" ]; then ' |
| 1190 | 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; ' |
| 1191 | 'else ln -sf "%(link_path)s" "%(dst)s"; fi' % |
| 1192 | dict(dirname=os.path.dirname(dst), |
| 1193 | link_path=link_path, dst=dst, |
| 1194 | link_name=src_base)) |
| 1195 | pbar.End() |
| 1196 | return |
| 1197 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1198 | mode = '0%o' % (0x1FF & os.stat(src).st_mode) |
| 1199 | url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' % |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 1200 | (self._state.host, self._state.port, |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 1201 | urllib.parse.quote(self._selected_mid), dst, mode)) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1202 | try: |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 1203 | UrlOpen(self._state, url + '&filename=%s' % src_base) |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 1204 | except urllib.error.HTTPError as e: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1205 | msg = json.loads(e.read()).get('error', None) |
| 1206 | raise RuntimeError('push: %s' % msg) |
| 1207 | |
| 1208 | pbar = ProgressBar(src_base) |
| 1209 | self._HTTPPostFile(url, src, pbar.SetProgress, |
| 1210 | self._state.username, self._state.password) |
| 1211 | pbar.End() |
| 1212 | |
Wei-Ning Huang | 37e6110 | 2016-02-07 13:41:07 +0800 | [diff] [blame] | 1213 | def _push_single_target(src, dst): |
| 1214 | if os.path.isdir(src): |
| 1215 | dst_exists = ast.literal_eval(self.CheckOutput( |
| 1216 | 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst)) |
| 1217 | for root, unused_x, files in os.walk(src): |
| 1218 | # If destination directory does not exist, we should strip the first |
| 1219 | # layer of directory. For example: src_dir contains a single file 'A' |
| 1220 | # |
| 1221 | # push src_dir dest_dir |
| 1222 | # |
| 1223 | # If dest_dir exists, the resulting directory structure should be: |
| 1224 | # dest_dir/src_dir/A |
| 1225 | # If dest_dir does not exist, the resulting directory structure should |
| 1226 | # be: |
| 1227 | # dest_dir/A |
| 1228 | dst_root = root if dst_exists else root[len(src):].lstrip('/') |
| 1229 | for name in files: |
| 1230 | _push(os.path.join(root, name), |
| 1231 | os.path.join(dst, dst_root, name)) |
| 1232 | else: |
| 1233 | _push(src, dst) |
| 1234 | |
| 1235 | if len(args.srcs) > 1: |
| 1236 | dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' ' |
| 1237 | '2>/dev/null' % args.dst).strip() |
| 1238 | if not dst_type: |
| 1239 | raise RuntimeError('push: %s: No such file or directory' % args.dst) |
| 1240 | if dst_type != 'directory': |
| 1241 | raise RuntimeError('push: %s: Not a directory' % args.dst) |
| 1242 | |
| 1243 | for src in args.srcs: |
| 1244 | if not os.path.exists(src): |
| 1245 | raise RuntimeError('push: can not stat "%s": no such file or directory' |
| 1246 | % src) |
| 1247 | if not os.access(src, os.R_OK): |
| 1248 | raise RuntimeError('push: can not open "%s" for reading' % src) |
| 1249 | |
| 1250 | _push_single_target(src, args.dst) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1251 | |
| 1252 | @Command('pull', 'pull a file or directory from remote', [ |
| 1253 | Arg('src', metavar='SOURCE'), |
| 1254 | Arg('dst', metavar='DESTINATION', default='.', nargs='?')]) |
| 1255 | def Pull(self, args): |
| 1256 | self.CheckClient() |
| 1257 | |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1258 | @AutoRetry('pull', _RETRY_TIMES) |
Peter Shih | e6afab3 | 2018-09-11 17:16:48 +0800 | [diff] [blame] | 1259 | def _pull(src, dst, ftype, perm=0o644, link=None): |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1260 | try: |
| 1261 | os.makedirs(os.path.dirname(dst)) |
| 1262 | except Exception: |
| 1263 | pass |
| 1264 | |
| 1265 | src_base = os.path.basename(src) |
| 1266 | |
| 1267 | # Remote file is a link |
| 1268 | if ftype == 'l': |
| 1269 | pbar = ProgressBar(src_base) |
| 1270 | if os.path.exists(dst): |
| 1271 | os.remove(dst) |
| 1272 | os.symlink(link, dst) |
| 1273 | pbar.End() |
| 1274 | return |
| 1275 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1276 | url = ('%s:%d/api/agent/download/%s?filename=%s' % |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 1277 | (self._state.host, self._state.port, |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 1278 | urllib.parse.quote(self._selected_mid), urllib.parse.quote(src))) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1279 | try: |
Wei-Ning Huang | d3a57dc | 2016-02-09 16:31:04 +0800 | [diff] [blame] | 1280 | h = UrlOpen(self._state, url) |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 1281 | except urllib.error.HTTPError as e: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1282 | msg = json.loads(e.read()).get('error', 'unkown error') |
| 1283 | raise RuntimeError('pull: %s' % msg) |
| 1284 | except KeyboardInterrupt: |
| 1285 | return |
| 1286 | |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1287 | pbar = ProgressBar(src_base) |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 1288 | with open(dst, 'wb') as f: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1289 | os.fchmod(f.fileno(), perm) |
| 1290 | total_size = int(h.headers.get('Content-Length')) |
| 1291 | downloaded_size = 0 |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1292 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1293 | while True: |
| 1294 | data = h.read(_BUFSIZ) |
Peter Shih | f84a897 | 2017-06-19 15:18:24 +0800 | [diff] [blame] | 1295 | if not data: |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1296 | break |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1297 | downloaded_size += len(data) |
Yilin Yang | 14d02a2 | 2019-11-01 11:32:03 +0800 | [diff] [blame] | 1298 | pbar.SetProgress(downloaded_size * 100 / total_size, |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1299 | downloaded_size) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1300 | f.write(data) |
| 1301 | pbar.End() |
| 1302 | |
| 1303 | # Use find to get a listing of all files under a root directory. The 'stat' |
| 1304 | # command is used to retrieve the filename and it's filemode. |
| 1305 | output = self.CheckOutput( |
| 1306 | 'cd $HOME; ' |
| 1307 | 'stat "%(src)s" >/dev/null && ' |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1308 | 'find "%(src)s" \'(\' -type f -o -type l \')\' ' |
| 1309 | '-printf \'%%m\t%%p\t%%y\t%%l\n\'' |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1310 | % {'src': args.src}) |
| 1311 | |
| 1312 | # We got error from the stat command |
| 1313 | if output.startswith('stat: '): |
| 1314 | sys.stderr.write(output) |
| 1315 | return |
| 1316 | |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1317 | entries = output.strip('\n').split('\n') |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1318 | common_prefix = os.path.dirname(args.src) |
| 1319 | |
| 1320 | if len(entries) == 1: |
| 1321 | entry = entries[0] |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1322 | perm, src_path, ftype, link = entry.split('\t', -1) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1323 | if os.path.isdir(args.dst): |
| 1324 | dst = os.path.join(args.dst, os.path.basename(src_path)) |
| 1325 | else: |
| 1326 | dst = args.dst |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1327 | _pull(src_path, dst, ftype, int(perm, base=8), link) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1328 | else: |
| 1329 | if not os.path.exists(args.dst): |
| 1330 | common_prefix = args.src |
| 1331 | |
| 1332 | for entry in entries: |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1333 | perm, src_path, ftype, link = entry.split('\t', -1) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1334 | rel_dst = src_path[len(common_prefix):].lstrip('/') |
Wei-Ning Huang | ee7ca8d | 2015-12-12 05:48:02 +0800 | [diff] [blame] | 1335 | _pull(src_path, os.path.join(args.dst, rel_dst), ftype, |
| 1336 | int(perm, base=8), link) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1337 | |
| 1338 | @Command('forward', 'forward remote port to local port', [ |
| 1339 | Arg('--list', dest='list_all', action='store_true', default=False, |
| 1340 | help='list all port forwarding sessions'), |
| 1341 | Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int, |
| 1342 | default=None, |
| 1343 | help='remove port forwarding for local port LOCAL_PORT'), |
| 1344 | Arg('--remove-all', dest='remove_all', action='store_true', |
| 1345 | default=False, help='remove all port forwarding'), |
| 1346 | Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'), |
| 1347 | Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')]) |
| 1348 | def Forward(self, args): |
| 1349 | if args.list_all: |
| 1350 | max_len = 10 |
Peter Shih | f84a897 | 2017-06-19 15:18:24 +0800 | [diff] [blame] | 1351 | if self._state.forwards: |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1352 | max_len = max([len(v[0]) for v in self._state.forwards.values()]) |
| 1353 | |
| 1354 | print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local')) |
| 1355 | for local in sorted(self._state.forwards.keys()): |
| 1356 | value = self._state.forwards[local] |
| 1357 | print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local)) |
| 1358 | return |
| 1359 | |
| 1360 | if args.remove_all: |
| 1361 | self._server.RemoveAllForward() |
| 1362 | return |
| 1363 | |
| 1364 | if args.remove: |
| 1365 | self._server.RemoveForward(args.remove) |
| 1366 | return |
| 1367 | |
| 1368 | self.CheckClient() |
| 1369 | |
Wei-Ning Huang | 9083b7c | 2016-01-26 16:44:11 +0800 | [diff] [blame] | 1370 | if args.remote is None: |
| 1371 | raise RuntimeError('remote port not specified') |
| 1372 | |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1373 | if args.local is None: |
Yilin Yang | 7766841 | 2021-02-02 10:53:36 +0800 | [diff] [blame] | 1374 | args.local = net_utils.FindUnusedPort() |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1375 | remote = int(args.remote) |
| 1376 | local = int(args.local) |
| 1377 | |
| 1378 | def HandleConnection(conn): |
| 1379 | headers = [] |
| 1380 | if self._state.username is not None and self._state.password is not None: |
| 1381 | headers.append(BasicAuthHeader(self._state.username, |
| 1382 | self._state.password)) |
| 1383 | |
| 1384 | scheme = 'ws%s://' % ('s' if self._state.ssl else '') |
| 1385 | ws = ForwarderWebSocketClient( |
Wei-Ning Huang | 47c79b8 | 2016-05-24 01:24:46 +0800 | [diff] [blame] | 1386 | self._state, conn, |
Peter Shih | e03450b | 2017-06-14 14:02:08 +0800 | [diff] [blame] | 1387 | scheme + '%s:%d/api/agent/forward/%s?port=%d' % ( |
| 1388 | self._state.host, self._state.port, |
Yilin Yang | f54fb91 | 2020-01-08 11:42:38 +0800 | [diff] [blame] | 1389 | urllib.parse.quote(self._selected_mid), remote), |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1390 | headers=headers) |
| 1391 | try: |
| 1392 | ws.connect() |
| 1393 | ws.run() |
| 1394 | except Exception as e: |
| 1395 | print('error: %s' % e) |
| 1396 | finally: |
| 1397 | ws.close() |
| 1398 | |
| 1399 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 1400 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 1401 | server.bind(('0.0.0.0', local)) |
| 1402 | server.listen(5) |
| 1403 | |
| 1404 | pid = os.fork() |
| 1405 | if pid == 0: |
| 1406 | while True: |
| 1407 | conn, unused_addr = server.accept() |
| 1408 | t = threading.Thread(target=HandleConnection, args=(conn,)) |
| 1409 | t.daemon = True |
| 1410 | t.start() |
| 1411 | else: |
Yilin Yang | 7766841 | 2021-02-02 10:53:36 +0800 | [diff] [blame] | 1412 | print('ovl_forward_port: http://localhost:%d' % local) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1413 | self._server.AddForward(self._selected_mid, remote, local, pid) |
| 1414 | |
| 1415 | |
| 1416 | def main(): |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 1417 | # Setup logging format |
| 1418 | logger = logging.getLogger() |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 1419 | logger.setLevel(logging.DEBUG) |
Wei-Ning Huang | ba768ab | 2016-02-07 14:38:06 +0800 | [diff] [blame] | 1420 | handler = logging.StreamHandler() |
| 1421 | formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S') |
| 1422 | handler.setFormatter(formatter) |
| 1423 | logger.addHandler(handler) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1424 | |
| 1425 | # Add DaemonState to JSONRPC lib classes |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 1426 | config.DEFAULT.classes.add(DaemonState) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1427 | |
| 1428 | ovl = OverlordCLIClient() |
| 1429 | try: |
| 1430 | ovl.Main() |
| 1431 | except KeyboardInterrupt: |
| 1432 | print('Ctrl-C received, abort') |
| 1433 | except Exception as e: |
Stimim Chen | a30447c | 2020-10-06 10:04:00 +0800 | [diff] [blame] | 1434 | logging.exception('exit with error [%s]', e) |
Wei-Ning Huang | 91aaeed | 2015-09-24 14:51:56 +0800 | [diff] [blame] | 1435 | |
| 1436 | |
| 1437 | if __name__ == '__main__': |
| 1438 | main() |