blob: f5bb5dbfba8c7305114b61c30bbeab9425b446fb [file] [log] [blame]
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001#!/usr/bin/env python
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08002# -*- coding: utf-8 -*-
3#
4# Copyright 2015 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8from __future__ import print_function
9
10import argparse
11import ast
12import base64
13import fcntl
Wei-Ning Huangba768ab2016-02-07 14:38:06 +080014import getpass
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080015import hashlib
16import httplib
17import json
18import jsonrpclib
19import 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 StringIO
27import struct
28import subprocess
29import sys
30import tempfile
31import termios
32import threading
33import time
34import tty
35import urllib2
36import urlparse
37
38from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
39from jsonrpclib.config import Config
40from ws4py.client import WebSocketBaseClient
41
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080042
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080043_CERT_DIR = os.path.expanduser('~/.config/ovl')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080044
45_ESCAPE = '~'
46_BUFSIZ = 8192
47_OVERLORD_PORT = 4455
48_OVERLORD_HTTP_PORT = 9000
49_OVERLORD_CLIENT_DAEMON_PORT = 4488
50_OVERLORD_CLIENT_DAEMON_RPC_ADDR = ('127.0.0.1', _OVERLORD_CLIENT_DAEMON_PORT)
51
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080052_CONNECT_TIMEOUT = 3
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080053_DEFAULT_HTTP_TIMEOUT = 30
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080054_LIST_CACHE_TIMEOUT = 2
55_DEFAULT_TERMINAL_WIDTH = 80
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080056_RETRY_TIMES = 3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080057
58# echo -n overlord | md5sum
59_HTTP_BOUNDARY_MAGIC = '9246f080c855a69012707ab53489b921'
60
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080061# Terminal resize control
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080062_CONTROL_START = 128
63_CONTROL_END = 129
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080064
65# Stream control
66_STDIN_CLOSED = '##STDIN_CLOSED##'
67
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080068_SSH_CONTROL_SOCKET_PREFIX = os.path.join(tempfile.gettempdir(),
69 'ovl-ssh-control-')
70
71# A string that will always be included in the response of
72# GET http://OVERLORD_SERVER:_OVERLORD_HTTP_PORT
Wei-Ning Huangba768ab2016-02-07 14:38:06 +080073_OVERLORD_RESPONSE_KEYWORD = 'HTTP'
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080074
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080075_TLS_CERT_CHANGED_WARNING = """
76@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
77@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
78@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
79IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
80Someone could be eavesdropping on you right now (man-in-the-middle attack)!
81It is also possible that the SSL host certificate has just been changed.
82The fingerprint for the SSL host certificate sent by the remote host is
83
84%s
85
86Remove '%s' if you still want to proceed.
87SSL Certificate verification failed."""
88
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080089
90def GetVersionDigest():
91 """Return the sha1sum of the current executing script."""
92 with open(__file__, 'r') as f:
93 return hashlib.sha1(f.read()).hexdigest()
94
95
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080096def GetTLSCertPath(host):
97 return os.path.join(_CERT_DIR, '%s.cert' % host)
98
99
100def UrlOpen(state, url):
101 """Wrapper for urllib2.urlopen.
102
103 It selects correct HTTP scheme according to self._state.ssl, add HTTP
104 basic auth headers, and add specify correct SSL context.
105 """
106 url = MakeRequestUrl(state, url)
107 request = urllib2.Request(url)
108 if state.username is not None and state.password is not None:
109 request.add_header(*BasicAuthHeader(state.username, state.password))
110 return urllib2.urlopen(request, timeout=_DEFAULT_HTTP_TIMEOUT,
111 context=state.ssl_context)
112
113
114def GetTLSCertificateSHA1Fingerprint(cert_pem):
115 beg = cert_pem.index('\n')
116 end = cert_pem.rindex('\n', 0, len(cert_pem) - 2)
117 cert_pem = cert_pem[beg:end] # Remove BEGIN/END CERTIFICATE boundary
118 cert_der = base64.b64decode(cert_pem)
119 return hashlib.sha1(cert_der).hexdigest()
120
121
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800122def KillGraceful(pid, wait_secs=1):
123 """Kill a process gracefully by first sending SIGTERM, wait for some time,
124 then send SIGKILL to make sure it's killed."""
125 try:
126 os.kill(pid, signal.SIGTERM)
127 time.sleep(wait_secs)
128 os.kill(pid, signal.SIGKILL)
129 except OSError:
130 pass
131
132
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800133def AutoRetry(action_name, retries):
134 """Decorator for retry function call."""
135 def Wrap(func):
136 def Loop(*args, **kwargs):
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800137 for unused_i in range(retries):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800138 try:
139 func(*args, **kwargs)
140 except Exception as e:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800141 print('error: %s: %s: retrying ...' % (args[0], e))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800142 else:
143 break
144 else:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800145 print('error: failed to %s %s' % (action_name, args[0]))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800146 return Loop
147 return Wrap
148
149
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800150def BasicAuthHeader(user, password):
151 """Return HTTP basic auth header."""
152 credential = base64.b64encode('%s:%s' % (user, password))
153 return ('Authorization', 'Basic %s' % credential)
154
155
156def GetTerminalSize():
157 """Retrieve terminal window size."""
158 ws = struct.pack('HHHH', 0, 0, 0, 0)
159 ws = fcntl.ioctl(0, termios.TIOCGWINSZ, ws)
160 lines, columns, unused_x, unused_y = struct.unpack('HHHH', ws)
161 return lines, columns
162
163
164def MakeRequestUrl(state, url):
165 return 'http%s://%s' % ('s' if state.ssl else '', url)
166
167
168class ProgressBar(object):
169 SIZE_WIDTH = 11
170 SPEED_WIDTH = 10
171 DURATION_WIDTH = 6
172 PERCENTAGE_WIDTH = 8
173
174 def __init__(self, name):
175 self._start_time = time.time()
176 self._name = name
177 self._size = 0
178 self._width = 0
179 self._name_width = 0
180 self._name_max = 0
181 self._stat_width = 0
182 self._max = 0
183 self.CalculateSize()
184 self.SetProgress(0)
185
186 def CalculateSize(self):
187 self._width = GetTerminalSize()[1] or _DEFAULT_TERMINAL_WIDTH
188 self._name_width = int(self._width * 0.3)
189 self._name_max = self._name_width
190 self._stat_width = self.SIZE_WIDTH + self.SPEED_WIDTH + self.DURATION_WIDTH
191 self._max = (self._width - self._name_width - self._stat_width -
192 self.PERCENTAGE_WIDTH)
193
194 def SizeToHuman(self, size_in_bytes):
195 if size_in_bytes < 1024:
196 unit = 'B'
197 value = size_in_bytes
198 elif size_in_bytes < 1024 ** 2:
199 unit = 'KiB'
200 value = size_in_bytes / 1024.0
201 elif size_in_bytes < 1024 ** 3:
202 unit = 'MiB'
203 value = size_in_bytes / (1024.0 ** 2)
204 elif size_in_bytes < 1024 ** 4:
205 unit = 'GiB'
206 value = size_in_bytes / (1024.0 ** 3)
207 return ' %6.1f %3s' % (value, unit)
208
209 def SpeedToHuman(self, speed_in_bs):
210 if speed_in_bs < 1024:
211 unit = 'B'
212 value = speed_in_bs
213 elif speed_in_bs < 1024 ** 2:
214 unit = 'K'
215 value = speed_in_bs / 1024.0
216 elif speed_in_bs < 1024 ** 3:
217 unit = 'M'
218 value = speed_in_bs / (1024.0 ** 2)
219 elif speed_in_bs < 1024 ** 4:
220 unit = 'G'
221 value = speed_in_bs / (1024.0 ** 3)
222 return ' %6.1f%s/s' % (value, unit)
223
224 def DurationToClock(self, duration):
225 return ' %02d:%02d' % (duration / 60, duration % 60)
226
227 def SetProgress(self, percentage, size=None):
228 current_width = GetTerminalSize()[1]
229 if self._width != current_width:
230 self.CalculateSize()
231
232 if size is not None:
233 self._size = size
234
235 elapse_time = time.time() - self._start_time
236 speed = self._size / float(elapse_time)
237
238 size_str = self.SizeToHuman(self._size)
239 speed_str = self.SpeedToHuman(speed)
240 elapse_str = self.DurationToClock(elapse_time)
241
242 width = int(self._max * percentage / 100.0)
243 sys.stdout.write(
244 '%*s' % (- self._name_max,
245 self._name if len(self._name) <= self._name_max else
246 self._name[:self._name_max - 4] + ' ...') +
247 size_str + speed_str + elapse_str +
248 ((' [' + '#' * width + ' ' * (self._max - width) + ']' +
249 '%4d%%' % int(percentage)) if self._max > 2 else '') + '\r')
250 sys.stdout.flush()
251
252 def End(self):
253 self.SetProgress(100.0)
254 sys.stdout.write('\n')
255 sys.stdout.flush()
256
257
258class DaemonState(object):
259 """DaemonState is used for storing Overlord state info."""
260 def __init__(self):
261 self.version_sha1sum = GetVersionDigest()
262 self.host = None
263 self.port = None
264 self.ssl = False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800265 self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800266 self.ssh = False
267 self.orig_host = None
268 self.ssh_pid = None
269 self.username = None
270 self.password = None
271 self.selected_mid = None
272 self.forwards = {}
273 self.listing = []
274 self.last_list = 0
275
276
277class OverlordClientDaemon(object):
278 """Overlord Client Daemon."""
279 def __init__(self):
280 self._state = DaemonState()
281 self._server = None
282
283 def Start(self):
284 self.StartRPCServer()
285
286 def StartRPCServer(self):
287 self._server = SimpleJSONRPCServer(_OVERLORD_CLIENT_DAEMON_RPC_ADDR,
288 logRequests=False)
289 exports = [
290 (self.State, 'State'),
291 (self.Ping, 'Ping'),
292 (self.GetPid, 'GetPid'),
293 (self.Connect, 'Connect'),
294 (self.Clients, 'Clients'),
295 (self.SelectClient, 'SelectClient'),
296 (self.AddForward, 'AddForward'),
297 (self.RemoveForward, 'RemoveForward'),
298 (self.RemoveAllForward, 'RemoveAllForward'),
299 ]
300 for func, name in exports:
301 self._server.register_function(func, name)
302
303 pid = os.fork()
304 if pid == 0:
305 self._server.serve_forever()
306
307 @staticmethod
308 def GetRPCServer():
309 """Returns the Overlord client daemon RPC server."""
310 server = jsonrpclib.Server('http://%s:%d' %
311 _OVERLORD_CLIENT_DAEMON_RPC_ADDR)
312 try:
313 server.Ping()
314 except Exception:
315 return None
316 return server
317
318 def State(self):
319 return self._state
320
321 def Ping(self):
322 return True
323
324 def GetPid(self):
325 return os.getpid()
326
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800327 def _GetJSON(self, path):
328 url = '%s:%d%s' % (self._state.host, self._state.port, path)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800329 return json.loads(UrlOpen(self._state, url).read())
330
331 def _OverlordHTTPSEnabled(self):
332 """Determine if SSL is enabled on the Overlord HTTP server."""
333 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
334 try:
335 sock.settimeout(_CONNECT_TIMEOUT)
336 sock.connect((self._state.host, self._state.port))
337 sock.send('GET\r\n')
338
339 data = sock.recv(16)
340 return _OVERLORD_RESPONSE_KEYWORD not in data
341 except Exception:
342 return False # For whatever reason above failed, assume HTTP
343
344 def _CheckTLSCertificate(self):
345 """Check TLS certificate.
346
347 Returns:
348 A tupple (check_result, if_certificate_is_loaded)
349 """
350 tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
351 tls_context.verify_mode = ssl.CERT_REQUIRED
352 tls_context.check_hostname = True
353 cert_loaded = False
354
355 tls_cert_path = GetTLSCertPath(self._state.host)
356 if os.path.exists(tls_cert_path):
357 tls_context.load_verify_locations(tls_cert_path)
358 cert_loaded = True
359
360 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
361 try:
362 sock.settimeout(_CONNECT_TIMEOUT)
363 sock = tls_context.wrap_socket(sock, server_hostname=self._state.host)
364 sock.connect((self._state.host, self._state.port))
365 except ssl.SSLError:
366 return False, cert_loaded
367 finally:
368 sock.close()
369
370 # Save SSLContext for future use.
371 self._state.ssl_context = tls_context
372 return True, None
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800373
374 def Connect(self, host, port=_OVERLORD_HTTP_PORT, ssh_pid=None,
375 username=None, password=None, orig_host=None):
376 self._state.username = username
377 self._state.password = password
378 self._state.host = host
379 self._state.port = port
380 self._state.ssl = False
381 self._state.orig_host = orig_host
382 self._state.ssh_pid = ssh_pid
383 self._state.selected_mid = None
384
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800385 tls_enabled = self._OverlordHTTPSEnabled()
386 if tls_enabled:
387 result, cert_loaded = self._CheckTLSCertificate()
388 if not result:
389 if cert_loaded:
390 return ('SSLCertificateChanged', ssl.get_server_certificate(
391 (self._state.host, self._state.port)))
392 else:
393 return ('SSLVerifyFailed', ssl.get_server_certificate(
394 (self._state.host, self._state.port)))
395
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800396 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800397 self._state.ssl = tls_enabled
398 UrlOpen(self._state, '%s:%d' % (host, port))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800399 except urllib2.HTTPError as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800400 return ('HTTPError', e.getcode(), str(e), e.read().strip())
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800401 except Exception as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800402 return str(e)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800403 else:
404 return True
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800405
406 def Clients(self):
407 if time.time() - self._state.last_list <= _LIST_CACHE_TIMEOUT:
408 return self._state.listing
409
410 mids = [client['mid'] for client in self._GetJSON('/api/agents/list')]
411 self._state.listing = sorted(list(set(mids)))
412 self._state.last_list = time.time()
413 return self._state.listing
414
415 def SelectClient(self, mid):
416 self._state.selected_mid = mid
417
418 def AddForward(self, mid, remote, local, pid):
419 self._state.forwards[local] = (mid, remote, pid)
420
421 def RemoveForward(self, local_port):
422 try:
423 unused_mid, unused_remote, pid = self._state.forwards[local_port]
424 KillGraceful(pid)
425 del self._state.forwards[local_port]
426 except (KeyError, OSError):
427 pass
428
429 def RemoveAllForward(self):
430 for unused_mid, unused_remote, pid in self._state.forwards.values():
431 try:
432 KillGraceful(pid)
433 except OSError:
434 pass
435 self._state.forwards = {}
436
437
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800438class SSLEnabledWebSocketBaseClient(WebSocketBaseClient):
439 def __init__(self, host, *args, **kwargs):
440 ssl_options = {
441 'cert_reqs': ssl.CERT_REQUIRED,
442 'ca_certs': GetTLSCertPath(host)
443 }
444 super(SSLEnabledWebSocketBaseClient, self).__init__(
445 ssl_options=ssl_options, *args, **kwargs)
446
447
448class TerminalWebSocketClient(SSLEnabledWebSocketBaseClient):
449 def __init__(self, host, mid, *args, **kwargs):
450 super(TerminalWebSocketClient, self).__init__(host, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800451 self._mid = mid
452 self._stdin_fd = sys.stdin.fileno()
453 self._old_termios = None
454
455 def handshake_ok(self):
456 pass
457
458 def opened(self):
459 nonlocals = {'size': (80, 40)}
460
461 def _ResizeWindow():
462 size = GetTerminalSize()
463 if size != nonlocals['size']: # Size not changed, ignore
464 control = {'command': 'resize', 'params': list(size)}
465 payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END)
466 nonlocals['size'] = size
467 try:
468 self.send(payload, binary=True)
469 except Exception:
470 pass
471
472 def _FeedInput():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800473 self._old_termios = termios.tcgetattr(self._stdin_fd)
474 tty.setraw(self._stdin_fd)
475
476 READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3)
477
478 try:
479 state = READY
480 while True:
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800481 # Check if terminal is resized
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800482 _ResizeWindow()
483
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800484 ch = sys.stdin.read(1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800485
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800486 # Scan for escape sequence
487 if state == READY:
488 state = ENTER_PRESSED if ch == chr(0x0d) else READY
489 elif state == ENTER_PRESSED:
490 state = ESCAPE_PRESSED if ch == _ESCAPE else READY
491 elif state == ESCAPE_PRESSED:
492 if ch == '.':
493 self.close()
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800494 break
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800495 else:
496 state = READY
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800497
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800498 self.send(ch)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800499 except (KeyboardInterrupt, RuntimeError):
500 pass
501
502 t = threading.Thread(target=_FeedInput)
503 t.daemon = True
504 t.start()
505
506 def closed(self, code, reason=None):
507 termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios)
508 print('Connection to %s closed.' % self._mid)
509
510 def received_message(self, msg):
511 if msg.is_binary:
512 sys.stdout.write(msg.data)
513 sys.stdout.flush()
514
515
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800516class ShellWebSocketClient(SSLEnabledWebSocketBaseClient):
517 def __init__(self, host, output, *args, **kwargs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800518 """Constructor.
519
520 Args:
521 output: output file object.
522 """
523 self.output = output
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800524 super(ShellWebSocketClient, self).__init__(host, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800525
526 def handshake_ok(self):
527 pass
528
529 def opened(self):
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800530 def _FeedInput():
531 try:
532 while True:
533 data = sys.stdin.read(1)
534
535 if len(data) == 0:
536 self.send(_STDIN_CLOSED * 2)
537 break
538 self.send(data, binary=True)
539 except (KeyboardInterrupt, RuntimeError):
540 pass
541
542 t = threading.Thread(target=_FeedInput)
543 t.daemon = True
544 t.start()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800545
546 def closed(self, code, reason=None):
547 pass
548
549 def received_message(self, msg):
550 if msg.is_binary:
551 self.output.write(msg.data)
552 self.output.flush()
553
554
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800555class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient):
556 def __init__(self, host, sock, *args, **kwargs):
557 super(ForwarderWebSocketClient, self).__init__(host, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800558 self._sock = sock
559 self._stop = threading.Event()
560
561 def handshake_ok(self):
562 pass
563
564 def opened(self):
565 def _FeedInput():
566 try:
567 self._sock.setblocking(False)
568 while True:
569 rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5)
570 if self._stop.is_set():
571 break
572 if self._sock in rd:
573 data = self._sock.recv(_BUFSIZ)
574 if len(data) == 0:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800575 self.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800576 break
577 self.send(data, binary=True)
578 except Exception:
579 pass
580 finally:
581 self._sock.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800582
583 t = threading.Thread(target=_FeedInput)
584 t.daemon = True
585 t.start()
586
587 def closed(self, code, reason=None):
588 self._stop.set()
589 sys.exit(0)
590
591 def received_message(self, msg):
592 if msg.is_binary:
593 self._sock.send(msg.data)
594
595
596def Arg(*args, **kwargs):
597 return (args, kwargs)
598
599
600def Command(command, help_msg=None, args=None):
601 """Decorator for adding argparse parameter for a method."""
602 if args is None:
603 args = []
604 def WrapFunc(func):
605 def Wrapped(*args, **kwargs):
606 return func(*args, **kwargs)
607 # pylint: disable=W0212
608 Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args}
609 return Wrapped
610 return WrapFunc
611
612
613def ParseMethodSubCommands(cls):
614 """Decorator for a class using the @Command decorator.
615
616 This decorator retrieve command info from each method and append it in to the
617 SUBCOMMANDS class variable, which is later used to construct parser.
618 """
619 for unused_key, method in cls.__dict__.iteritems():
620 if hasattr(method, '__arg_attr'):
621 cls.SUBCOMMANDS.append(method.__arg_attr) # pylint: disable=W0212
622 return cls
623
624
625@ParseMethodSubCommands
626class OverlordCLIClient(object):
627 """Overlord command line interface client."""
628
629 SUBCOMMANDS = []
630
631 def __init__(self):
632 self._parser = self._BuildParser()
633 self._selected_mid = None
634 self._server = None
635 self._state = None
636
637 def _BuildParser(self):
638 root_parser = argparse.ArgumentParser(prog='ovl')
639 subparsers = root_parser.add_subparsers(help='sub-command')
640
641 root_parser.add_argument('-s', dest='selected_mid', action='store',
642 default=None,
643 help='select target to execute command on')
644 root_parser.add_argument('-S', dest='select_mid_before_action',
645 action='store_true', default=False,
646 help='select target before executing command')
647
648 for attr in self.SUBCOMMANDS:
649 parser = subparsers.add_parser(attr['command'], help=attr['help'])
650 parser.set_defaults(which=attr['command'])
651 for arg in attr['args']:
652 parser.add_argument(*arg[0], **arg[1])
653
654 return root_parser
655
656 def Main(self):
657 # We want to pass the rest of arguments after shell command directly to the
658 # function without parsing it.
659 try:
660 index = sys.argv.index('shell')
661 except ValueError:
662 args = self._parser.parse_args()
663 else:
664 args = self._parser.parse_args(sys.argv[1:index + 1])
665
666 command = args.which
667 self._selected_mid = args.selected_mid
668
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800669 if command == 'start-server':
670 self.StartServer()
671 return
672 elif command == 'kill-server':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800673 self.KillServer()
674 return
675
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800676 self.CheckDaemon()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800677 if command == 'status':
678 self.Status()
679 return
680 elif command == 'connect':
681 self.Connect(args)
682 return
683
684 # The following command requires connection to the server
685 self.CheckConnection()
686
687 if args.select_mid_before_action:
688 self.SelectClient(store=False)
689
690 if command == 'select':
691 self.SelectClient(args)
692 elif command == 'ls':
693 self.ListClients()
694 elif command == 'shell':
695 command = sys.argv[sys.argv.index('shell') + 1:]
696 self.Shell(command)
697 elif command == 'push':
698 self.Push(args)
699 elif command == 'pull':
700 self.Pull(args)
701 elif command == 'forward':
702 self.Forward(args)
703
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800704 def _SaveTLSCertificate(self, host, cert_pem):
705 try:
706 os.makedirs(_CERT_DIR)
707 except Exception:
708 pass
709 with open(GetTLSCertPath(host), 'w') as f:
710 f.write(cert_pem)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800711
712 def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None):
713 """Perform HTTP POST and upload file to Overlord.
714
715 To minimize the external dependencies, we construct the HTTP post request
716 by ourselves.
717 """
718 url = MakeRequestUrl(self._state, url)
719 size = os.stat(filename).st_size
720 boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC
721 CRLF = '\r\n'
722 parse = urlparse.urlparse(url)
723
724 part_headers = [
725 '--' + boundary,
726 'Content-Disposition: form-data; name="file"; '
727 'filename="%s"' % os.path.basename(filename),
728 'Content-Type: application/octet-stream',
729 '', ''
730 ]
731 part_header = CRLF.join(part_headers)
732 end_part = CRLF + '--' + boundary + '--' + CRLF
733
734 content_length = len(part_header) + size + len(end_part)
735 if parse.scheme == 'http':
736 h = httplib.HTTP(parse.netloc)
737 else:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800738 h = httplib.HTTPS(parse.netloc, context=self._state.ssl_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800739
740 post_path = url[url.index(parse.netloc) + len(parse.netloc):]
741 h.putrequest('POST', post_path)
742 h.putheader('Content-Length', content_length)
743 h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
744
745 if user and passwd:
746 h.putheader(*BasicAuthHeader(user, passwd))
747 h.endheaders()
748 h.send(part_header)
749
750 count = 0
751 with open(filename, 'r') as f:
752 while True:
753 data = f.read(_BUFSIZ)
754 if not data:
755 break
756 count += len(data)
757 if progress:
758 progress(int(count * 100.0 / size), count)
759 h.send(data)
760
761 h.send(end_part)
762 progress(100)
763
764 if count != size:
765 logging.warning('file changed during upload, upload may be truncated.')
766
767 errcode, unused_x, unused_y = h.getreply()
768 return errcode == 200
769
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800770 def CheckDaemon(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800771 self._server = OverlordClientDaemon.GetRPCServer()
772 if self._server is None:
773 print('* daemon not running, starting it now on port %d ... *' %
774 _OVERLORD_CLIENT_DAEMON_PORT)
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800775 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800776
777 self._state = self._server.State()
778 sha1sum = GetVersionDigest()
779
780 if sha1sum != self._state.version_sha1sum:
781 print('ovl server is out of date. killing...')
782 KillGraceful(self._server.GetPid())
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800783 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800784
785 def GetSSHControlFile(self, host):
786 return _SSH_CONTROL_SOCKET_PREFIX + host
787
788 def SSHTunnel(self, user, host, port):
789 """SSH forward the remote overlord server.
790
791 Overlord server may not have port 9000 open to the public network, in such
792 case we can SSH forward the port to localhost.
793 """
794
795 control_file = self.GetSSHControlFile(host)
796 try:
797 os.unlink(control_file)
798 except Exception:
799 pass
800
801 subprocess.Popen([
802 'ssh', '-Nf',
803 '-M', # Enable master mode
804 '-S', control_file,
805 '-L', '9000:localhost:9000',
806 '-p', str(port),
807 '%s%s' % (user + '@' if user else '', host)
808 ]).wait()
809
810 p = subprocess.Popen([
811 'ssh',
812 '-S', control_file,
813 '-O', 'check', host,
814 ], stderr=subprocess.PIPE)
815 unused_stdout, stderr = p.communicate()
816
817 s = re.search(r'pid=(\d+)', stderr)
818 if s:
819 return int(s.group(1))
820
821 raise RuntimeError('can not establish ssh connection')
822
823 def CheckConnection(self):
824 if self._state.host is None:
825 raise RuntimeError('not connected to any server, abort')
826
827 try:
828 self._server.Clients()
829 except Exception:
830 raise RuntimeError('remote server disconnected, abort')
831
832 if self._state.ssh_pid is not None:
833 ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)],
834 stdout=subprocess.PIPE,
835 stderr=subprocess.PIPE).wait()
836 if ret != 0:
837 raise RuntimeError('ssh tunnel disconnected, please re-connect')
838
839 def CheckClient(self):
840 if self._selected_mid is None:
841 if self._state.selected_mid is None:
842 raise RuntimeError('No client is selected')
843 self._selected_mid = self._state.selected_mid
844
845 if self._selected_mid not in self._server.Clients():
846 raise RuntimeError('client %s disappeared' % self._selected_mid)
847
848 def CheckOutput(self, command):
849 headers = []
850 if self._state.username is not None and self._state.password is not None:
851 headers.append(BasicAuthHeader(self._state.username,
852 self._state.password))
853
854 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
855 sio = StringIO.StringIO()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800856 ws = ShellWebSocketClient(self._state.host, sio,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800857 scheme + '%s:%d/api/agent/shell/%s?command=%s' %
858 (self._state.host, self._state.port,
859 self._selected_mid, urllib2.quote(command)),
860 headers=headers)
861 ws.connect()
862 ws.run()
863 return sio.getvalue()
864
865 @Command('status', 'show Overlord connection status')
866 def Status(self):
867 if self._state.host is None:
868 print('Not connected to any host.')
869 else:
870 if self._state.ssh_pid is not None:
871 print('Connected to %s with SSH tunneling.' % self._state.orig_host)
872 else:
873 print('Connected to %s:%d.' % (self._state.host, self._state.port))
874
875 if self._selected_mid is None:
876 self._selected_mid = self._state.selected_mid
877
878 if self._selected_mid is None:
879 print('No client is selected.')
880 else:
881 print('Client %s selected.' % self._selected_mid)
882
883 @Command('connect', 'connect to Overlord server', [
884 Arg('host', metavar='HOST', type=str, default='localhost',
885 help='Overlord hostname/IP'),
886 Arg('port', metavar='PORT', type=int,
887 default=_OVERLORD_HTTP_PORT, help='Overlord port'),
888 Arg('-f', '--forward', dest='ssh_forward', default=False,
889 action='store_true',
890 help='connect with SSH forwarding to the host'),
891 Arg('-p', '--ssh-port', dest='ssh_port', default=22,
892 type=int, help='SSH server port for SSH forwarding'),
893 Arg('-l', '--ssh-login', dest='ssh_login', default='',
894 type=str, help='SSH server login name for SSH forwarding'),
895 Arg('-u', '--user', dest='user', default=None,
896 type=str, help='Overlord HTTP auth username'),
897 Arg('-w', '--passwd', dest='passwd', default=None, type=str,
898 help='Overlord HTTP auth password')])
899 def Connect(self, args):
900 ssh_pid = None
901 host = args.host
902 orig_host = args.host
903
904 if args.ssh_forward:
905 # Kill previous SSH tunnel
906 self.KillSSHTunnel()
907
908 ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port)
909 host = 'localhost'
910
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800911 username_provided = args.user is not None
912 password_provided = args.passwd is not None
913 prompt = False
914
915 for unused_i in range(3):
916 try:
917 if prompt:
918 if not username_provided:
919 args.user = raw_input('Username: ')
920 if not password_provided:
921 args.passwd = getpass.getpass('Password: ')
922
923 ret = self._server.Connect(host, args.port, ssh_pid, args.user,
924 args.passwd, orig_host)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800925 if isinstance(ret, list):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800926 if ret[0].startswith('SSL'):
927 cert_pem = ret[1]
928 fp = GetTLSCertificateSHA1Fingerprint(cert_pem)
929 fp_text = ':'.join([fp[i:i+2] for i in range(0, len(fp), 2)])
930
931 if ret[0] == 'SSLCertificateChanged':
932 print(_TLS_CERT_CHANGED_WARNING % (fp_text, GetTLSCertPath(host)))
933 return
934 elif ret[0] == 'SSLVerifyFailed':
935 print('Server fingerprint: %s' % fp_text)
936 response = raw_input('Do you want to continue? [Y/n] ')
937 if response.lower() in ['y', 'ye', 'yes']:
938 self._SaveTLSCertificate(host, cert_pem)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800939 continue
940 else:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800941 print('connection aborted.')
942 return
943 elif ret[0] == 'HTTPError':
944 code, except_str, body = ret[1:]
945 if code == 401:
946 print('connect: %s' % body)
947 prompt = True
948 if not username_provided or not password_provided:
949 continue
950 else:
951 break
952 else:
953 logging.error('%s; %s', except_str, body)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800954
955 if ret is not True:
956 print('can not connect to %s: %s' % (host, ret))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800957 else:
958 print('connection to %s:%d established.' % (host, args.port))
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800959 except Exception as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800960 logging.error(e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800961 else:
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800962 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800963
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800964 @Command('start-server', 'start overlord CLI client server')
965 def StartServer(self):
966 self._server = OverlordClientDaemon.GetRPCServer()
967 if self._server is None:
968 OverlordClientDaemon().Start()
969 time.sleep(1)
970 self._server = OverlordClientDaemon.GetRPCServer()
971 if self._server is not None:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800972 print('* daemon started successfully *\n')
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800973
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800974 @Command('kill-server', 'kill overlord CLI client server')
975 def KillServer(self):
976 self._server = OverlordClientDaemon.GetRPCServer()
977 if self._server is None:
978 return
979
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800980 self._state = self._server.State()
981
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800982 # Kill SSH Tunnel
983 self.KillSSHTunnel()
984
985 # Kill server daemon
986 KillGraceful(self._server.GetPid())
987
988 def KillSSHTunnel(self):
989 if self._state.ssh_pid is not None:
990 KillGraceful(self._state.ssh_pid)
991
992 @Command('ls', 'list all clients')
993 def ListClients(self):
994 for client in self._server.Clients():
995 print(client)
996
997 @Command('select', 'select default client', [
998 Arg('mid', metavar='mid', nargs='?', default=None)])
999 def SelectClient(self, args=None, store=True):
1000 clients = self._server.Clients()
1001
1002 mid = args.mid if args is not None else None
1003 if mid is None:
1004 print('Select from the following clients:')
1005 for i, client in enumerate(clients):
1006 print(' %d. %s' % (i + 1, client))
1007
1008 print('\nSelection: ', end='')
1009 try:
1010 choice = int(raw_input()) - 1
1011 mid = clients[choice]
1012 except ValueError:
1013 raise RuntimeError('select: invalid selection')
1014 except IndexError:
1015 raise RuntimeError('select: selection out of range')
1016 else:
1017 if mid not in clients:
1018 raise RuntimeError('select: client %s does not exist' % mid)
1019
1020 self._selected_mid = mid
1021 if store:
1022 self._server.SelectClient(mid)
1023 print('Client %s selected' % mid)
1024
1025 @Command('shell', 'open a shell or execute a shell command', [
1026 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1027 def Shell(self, command=None):
1028 if command is None:
1029 command = []
1030 self.CheckClient()
1031
1032 headers = []
1033 if self._state.username is not None and self._state.password is not None:
1034 headers.append(BasicAuthHeader(self._state.username,
1035 self._state.password))
1036
1037 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1038 if len(command) == 0:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001039 ws = TerminalWebSocketClient(self._state.host, self._selected_mid,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001040 scheme + '%s:%d/api/agent/tty/%s' %
1041 (self._state.host, self._state.port,
1042 self._selected_mid), headers=headers)
1043 else:
1044 cmd = ' '.join(command)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001045 ws = ShellWebSocketClient(self._state.host, sys.stdout,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001046 scheme + '%s:%d/api/agent/shell/%s?command=%s' %
1047 (self._state.host, self._state.port,
1048 self._selected_mid, urllib2.quote(cmd)),
1049 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001050 try:
1051 ws.connect()
1052 ws.run()
1053 except socket.error as e:
1054 if e.errno == 32: # Broken pipe
1055 pass
1056 else:
1057 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001058
1059 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001060 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001061 Arg('dst', metavar='DESTINATION')])
1062 def Push(self, args):
1063 self.CheckClient()
1064
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001065 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001066 def _push(src, dst):
1067 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001068
1069 # Local file is a link
1070 if os.path.islink(src):
1071 pbar = ProgressBar(src_base)
1072 link_path = os.readlink(src)
1073 self.CheckOutput('mkdir -p %(dirname)s; '
1074 'if [ -d "%(dst)s" ]; then '
1075 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1076 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1077 dict(dirname=os.path.dirname(dst),
1078 link_path=link_path, dst=dst,
1079 link_name=src_base))
1080 pbar.End()
1081 return
1082
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001083 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1084 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
1085 (self._state.host, self._state.port, self._selected_mid, dst,
1086 mode))
1087 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001088 UrlOpen(self._state, url + '&filename=%s' % src_base)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001089 except urllib2.HTTPError as e:
1090 msg = json.loads(e.read()).get('error', None)
1091 raise RuntimeError('push: %s' % msg)
1092
1093 pbar = ProgressBar(src_base)
1094 self._HTTPPostFile(url, src, pbar.SetProgress,
1095 self._state.username, self._state.password)
1096 pbar.End()
1097
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001098 def _push_single_target(src, dst):
1099 if os.path.isdir(src):
1100 dst_exists = ast.literal_eval(self.CheckOutput(
1101 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1102 for root, unused_x, files in os.walk(src):
1103 # If destination directory does not exist, we should strip the first
1104 # layer of directory. For example: src_dir contains a single file 'A'
1105 #
1106 # push src_dir dest_dir
1107 #
1108 # If dest_dir exists, the resulting directory structure should be:
1109 # dest_dir/src_dir/A
1110 # If dest_dir does not exist, the resulting directory structure should
1111 # be:
1112 # dest_dir/A
1113 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1114 for name in files:
1115 _push(os.path.join(root, name),
1116 os.path.join(dst, dst_root, name))
1117 else:
1118 _push(src, dst)
1119
1120 if len(args.srcs) > 1:
1121 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1122 '2>/dev/null' % args.dst).strip()
1123 if not dst_type:
1124 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1125 if dst_type != 'directory':
1126 raise RuntimeError('push: %s: Not a directory' % args.dst)
1127
1128 for src in args.srcs:
1129 if not os.path.exists(src):
1130 raise RuntimeError('push: can not stat "%s": no such file or directory'
1131 % src)
1132 if not os.access(src, os.R_OK):
1133 raise RuntimeError('push: can not open "%s" for reading' % src)
1134
1135 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001136
1137 @Command('pull', 'pull a file or directory from remote', [
1138 Arg('src', metavar='SOURCE'),
1139 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1140 def Pull(self, args):
1141 self.CheckClient()
1142
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001143 @AutoRetry('pull', _RETRY_TIMES)
1144 def _pull(src, dst, ftype, perm=0644, link=None):
1145 try:
1146 os.makedirs(os.path.dirname(dst))
1147 except Exception:
1148 pass
1149
1150 src_base = os.path.basename(src)
1151
1152 # Remote file is a link
1153 if ftype == 'l':
1154 pbar = ProgressBar(src_base)
1155 if os.path.exists(dst):
1156 os.remove(dst)
1157 os.symlink(link, dst)
1158 pbar.End()
1159 return
1160
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001161 url = ('%s:%d/api/agent/download/%s?filename=%s' %
1162 (self._state.host, self._state.port, self._selected_mid,
1163 urllib2.quote(src)))
1164 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001165 h = UrlOpen(self._state, url)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001166 except urllib2.HTTPError as e:
1167 msg = json.loads(e.read()).get('error', 'unkown error')
1168 raise RuntimeError('pull: %s' % msg)
1169 except KeyboardInterrupt:
1170 return
1171
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001172 pbar = ProgressBar(src_base)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001173 with open(dst, 'w') as f:
1174 os.fchmod(f.fileno(), perm)
1175 total_size = int(h.headers.get('Content-Length'))
1176 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001177
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001178 while True:
1179 data = h.read(_BUFSIZ)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001180 if len(data) == 0:
1181 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001182 downloaded_size += len(data)
1183 pbar.SetProgress(float(downloaded_size) * 100 / total_size,
1184 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001185 f.write(data)
1186 pbar.End()
1187
1188 # Use find to get a listing of all files under a root directory. The 'stat'
1189 # command is used to retrieve the filename and it's filemode.
1190 output = self.CheckOutput(
1191 'cd $HOME; '
1192 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001193 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1194 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001195 % {'src': args.src})
1196
1197 # We got error from the stat command
1198 if output.startswith('stat: '):
1199 sys.stderr.write(output)
1200 return
1201
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001202 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001203 common_prefix = os.path.dirname(args.src)
1204
1205 if len(entries) == 1:
1206 entry = entries[0]
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001207 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001208 if os.path.isdir(args.dst):
1209 dst = os.path.join(args.dst, os.path.basename(src_path))
1210 else:
1211 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001212 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001213 else:
1214 if not os.path.exists(args.dst):
1215 common_prefix = args.src
1216
1217 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001218 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001219 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001220 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1221 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001222
1223 @Command('forward', 'forward remote port to local port', [
1224 Arg('--list', dest='list_all', action='store_true', default=False,
1225 help='list all port forwarding sessions'),
1226 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1227 default=None,
1228 help='remove port forwarding for local port LOCAL_PORT'),
1229 Arg('--remove-all', dest='remove_all', action='store_true',
1230 default=False, help='remove all port forwarding'),
1231 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1232 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1233 def Forward(self, args):
1234 if args.list_all:
1235 max_len = 10
1236 if len(self._state.forwards):
1237 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1238
1239 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1240 for local in sorted(self._state.forwards.keys()):
1241 value = self._state.forwards[local]
1242 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1243 return
1244
1245 if args.remove_all:
1246 self._server.RemoveAllForward()
1247 return
1248
1249 if args.remove:
1250 self._server.RemoveForward(args.remove)
1251 return
1252
1253 self.CheckClient()
1254
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001255 if args.remote is None:
1256 raise RuntimeError('remote port not specified')
1257
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001258 if args.local is None:
1259 args.local = args.remote
1260 remote = int(args.remote)
1261 local = int(args.local)
1262
1263 def HandleConnection(conn):
1264 headers = []
1265 if self._state.username is not None and self._state.password is not None:
1266 headers.append(BasicAuthHeader(self._state.username,
1267 self._state.password))
1268
1269 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1270 ws = ForwarderWebSocketClient(
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001271 self._state.host, conn,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001272 scheme + '%s:%d/api/agent/forward/%s?port=%d' %
1273 (self._state.host, self._state.port, self._selected_mid, remote),
1274 headers=headers)
1275 try:
1276 ws.connect()
1277 ws.run()
1278 except Exception as e:
1279 print('error: %s' % e)
1280 finally:
1281 ws.close()
1282
1283 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1284 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1285 server.bind(('0.0.0.0', local))
1286 server.listen(5)
1287
1288 pid = os.fork()
1289 if pid == 0:
1290 while True:
1291 conn, unused_addr = server.accept()
1292 t = threading.Thread(target=HandleConnection, args=(conn,))
1293 t.daemon = True
1294 t.start()
1295 else:
1296 self._server.AddForward(self._selected_mid, remote, local, pid)
1297
1298
1299def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001300 # Setup logging format
1301 logger = logging.getLogger()
1302 logger.setLevel(logging.INFO)
1303 handler = logging.StreamHandler()
1304 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1305 handler.setFormatter(formatter)
1306 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001307
1308 # Add DaemonState to JSONRPC lib classes
1309 Config.instance().classes.add(DaemonState)
1310
1311 ovl = OverlordCLIClient()
1312 try:
1313 ovl.Main()
1314 except KeyboardInterrupt:
1315 print('Ctrl-C received, abort')
1316 except Exception as e:
1317 print('error: %s' % e)
1318
1319
1320if __name__ == '__main__':
1321 main()