blob: 433863e0930a43fd8ae0796bf5483e0fa0d9e2f7 [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):
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800449 def __init__(self, host, mid, escape, *args, **kwargs):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800450 super(TerminalWebSocketClient, self).__init__(host, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800451 self._mid = mid
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800452 self._escape = escape
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800453 self._stdin_fd = sys.stdin.fileno()
454 self._old_termios = None
455
456 def handshake_ok(self):
457 pass
458
459 def opened(self):
460 nonlocals = {'size': (80, 40)}
461
462 def _ResizeWindow():
463 size = GetTerminalSize()
464 if size != nonlocals['size']: # Size not changed, ignore
465 control = {'command': 'resize', 'params': list(size)}
466 payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END)
467 nonlocals['size'] = size
468 try:
469 self.send(payload, binary=True)
470 except Exception:
471 pass
472
473 def _FeedInput():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800474 self._old_termios = termios.tcgetattr(self._stdin_fd)
475 tty.setraw(self._stdin_fd)
476
477 READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3)
478
479 try:
480 state = READY
481 while True:
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800482 # Check if terminal is resized
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800483 _ResizeWindow()
484
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800485 ch = sys.stdin.read(1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800486
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800487 # Scan for escape sequence
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800488 if self._escape:
489 if state == READY:
490 state = ENTER_PRESSED if ch == chr(0x0d) else READY
491 elif state == ENTER_PRESSED:
492 state = ESCAPE_PRESSED if ch == self._escape else READY
493 elif state == ESCAPE_PRESSED:
494 if ch == '.':
495 self.close()
496 break
497 else:
498 state = READY
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800499
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800500 self.send(ch)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800501 except (KeyboardInterrupt, RuntimeError):
502 pass
503
504 t = threading.Thread(target=_FeedInput)
505 t.daemon = True
506 t.start()
507
508 def closed(self, code, reason=None):
509 termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios)
510 print('Connection to %s closed.' % self._mid)
511
512 def received_message(self, msg):
513 if msg.is_binary:
514 sys.stdout.write(msg.data)
515 sys.stdout.flush()
516
517
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800518class ShellWebSocketClient(SSLEnabledWebSocketBaseClient):
519 def __init__(self, host, output, *args, **kwargs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800520 """Constructor.
521
522 Args:
523 output: output file object.
524 """
525 self.output = output
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800526 super(ShellWebSocketClient, self).__init__(host, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800527
528 def handshake_ok(self):
529 pass
530
531 def opened(self):
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800532 def _FeedInput():
533 try:
534 while True:
535 data = sys.stdin.read(1)
536
537 if len(data) == 0:
538 self.send(_STDIN_CLOSED * 2)
539 break
540 self.send(data, binary=True)
541 except (KeyboardInterrupt, RuntimeError):
542 pass
543
544 t = threading.Thread(target=_FeedInput)
545 t.daemon = True
546 t.start()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800547
548 def closed(self, code, reason=None):
549 pass
550
551 def received_message(self, msg):
552 if msg.is_binary:
553 self.output.write(msg.data)
554 self.output.flush()
555
556
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800557class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient):
558 def __init__(self, host, sock, *args, **kwargs):
559 super(ForwarderWebSocketClient, self).__init__(host, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800560 self._sock = sock
561 self._stop = threading.Event()
562
563 def handshake_ok(self):
564 pass
565
566 def opened(self):
567 def _FeedInput():
568 try:
569 self._sock.setblocking(False)
570 while True:
571 rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5)
572 if self._stop.is_set():
573 break
574 if self._sock in rd:
575 data = self._sock.recv(_BUFSIZ)
576 if len(data) == 0:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800577 self.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800578 break
579 self.send(data, binary=True)
580 except Exception:
581 pass
582 finally:
583 self._sock.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800584
585 t = threading.Thread(target=_FeedInput)
586 t.daemon = True
587 t.start()
588
589 def closed(self, code, reason=None):
590 self._stop.set()
591 sys.exit(0)
592
593 def received_message(self, msg):
594 if msg.is_binary:
595 self._sock.send(msg.data)
596
597
598def Arg(*args, **kwargs):
599 return (args, kwargs)
600
601
602def Command(command, help_msg=None, args=None):
603 """Decorator for adding argparse parameter for a method."""
604 if args is None:
605 args = []
606 def WrapFunc(func):
607 def Wrapped(*args, **kwargs):
608 return func(*args, **kwargs)
609 # pylint: disable=W0212
610 Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args}
611 return Wrapped
612 return WrapFunc
613
614
615def ParseMethodSubCommands(cls):
616 """Decorator for a class using the @Command decorator.
617
618 This decorator retrieve command info from each method and append it in to the
619 SUBCOMMANDS class variable, which is later used to construct parser.
620 """
621 for unused_key, method in cls.__dict__.iteritems():
622 if hasattr(method, '__arg_attr'):
623 cls.SUBCOMMANDS.append(method.__arg_attr) # pylint: disable=W0212
624 return cls
625
626
627@ParseMethodSubCommands
628class OverlordCLIClient(object):
629 """Overlord command line interface client."""
630
631 SUBCOMMANDS = []
632
633 def __init__(self):
634 self._parser = self._BuildParser()
635 self._selected_mid = None
636 self._server = None
637 self._state = None
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800638 self._escape = None
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800639
640 def _BuildParser(self):
641 root_parser = argparse.ArgumentParser(prog='ovl')
642 subparsers = root_parser.add_subparsers(help='sub-command')
643
644 root_parser.add_argument('-s', dest='selected_mid', action='store',
645 default=None,
646 help='select target to execute command on')
647 root_parser.add_argument('-S', dest='select_mid_before_action',
648 action='store_true', default=False,
649 help='select target before executing command')
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800650 root_parser.add_argument('-e', dest='escape', metavar='ESCAPE_CHAR',
651 action='store', default=_ESCAPE, type=str,
652 help='set shell escape character, \'none\' to '
653 'disable escape completely')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800654
655 for attr in self.SUBCOMMANDS:
656 parser = subparsers.add_parser(attr['command'], help=attr['help'])
657 parser.set_defaults(which=attr['command'])
658 for arg in attr['args']:
659 parser.add_argument(*arg[0], **arg[1])
660
661 return root_parser
662
663 def Main(self):
664 # We want to pass the rest of arguments after shell command directly to the
665 # function without parsing it.
666 try:
667 index = sys.argv.index('shell')
668 except ValueError:
669 args = self._parser.parse_args()
670 else:
671 args = self._parser.parse_args(sys.argv[1:index + 1])
672
673 command = args.which
674 self._selected_mid = args.selected_mid
675
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800676 if args.escape and args.escape != 'none':
677 self._escape = args.escape[0]
678
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800679 if command == 'start-server':
680 self.StartServer()
681 return
682 elif command == 'kill-server':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800683 self.KillServer()
684 return
685
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800686 self.CheckDaemon()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800687 if command == 'status':
688 self.Status()
689 return
690 elif command == 'connect':
691 self.Connect(args)
692 return
693
694 # The following command requires connection to the server
695 self.CheckConnection()
696
697 if args.select_mid_before_action:
698 self.SelectClient(store=False)
699
700 if command == 'select':
701 self.SelectClient(args)
702 elif command == 'ls':
703 self.ListClients()
704 elif command == 'shell':
705 command = sys.argv[sys.argv.index('shell') + 1:]
706 self.Shell(command)
707 elif command == 'push':
708 self.Push(args)
709 elif command == 'pull':
710 self.Pull(args)
711 elif command == 'forward':
712 self.Forward(args)
713
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800714 def _SaveTLSCertificate(self, host, cert_pem):
715 try:
716 os.makedirs(_CERT_DIR)
717 except Exception:
718 pass
719 with open(GetTLSCertPath(host), 'w') as f:
720 f.write(cert_pem)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800721
722 def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None):
723 """Perform HTTP POST and upload file to Overlord.
724
725 To minimize the external dependencies, we construct the HTTP post request
726 by ourselves.
727 """
728 url = MakeRequestUrl(self._state, url)
729 size = os.stat(filename).st_size
730 boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC
731 CRLF = '\r\n'
732 parse = urlparse.urlparse(url)
733
734 part_headers = [
735 '--' + boundary,
736 'Content-Disposition: form-data; name="file"; '
737 'filename="%s"' % os.path.basename(filename),
738 'Content-Type: application/octet-stream',
739 '', ''
740 ]
741 part_header = CRLF.join(part_headers)
742 end_part = CRLF + '--' + boundary + '--' + CRLF
743
744 content_length = len(part_header) + size + len(end_part)
745 if parse.scheme == 'http':
746 h = httplib.HTTP(parse.netloc)
747 else:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800748 h = httplib.HTTPS(parse.netloc, context=self._state.ssl_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800749
750 post_path = url[url.index(parse.netloc) + len(parse.netloc):]
751 h.putrequest('POST', post_path)
752 h.putheader('Content-Length', content_length)
753 h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
754
755 if user and passwd:
756 h.putheader(*BasicAuthHeader(user, passwd))
757 h.endheaders()
758 h.send(part_header)
759
760 count = 0
761 with open(filename, 'r') as f:
762 while True:
763 data = f.read(_BUFSIZ)
764 if not data:
765 break
766 count += len(data)
767 if progress:
768 progress(int(count * 100.0 / size), count)
769 h.send(data)
770
771 h.send(end_part)
772 progress(100)
773
774 if count != size:
775 logging.warning('file changed during upload, upload may be truncated.')
776
777 errcode, unused_x, unused_y = h.getreply()
778 return errcode == 200
779
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800780 def CheckDaemon(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800781 self._server = OverlordClientDaemon.GetRPCServer()
782 if self._server is None:
783 print('* daemon not running, starting it now on port %d ... *' %
784 _OVERLORD_CLIENT_DAEMON_PORT)
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800785 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800786
787 self._state = self._server.State()
788 sha1sum = GetVersionDigest()
789
790 if sha1sum != self._state.version_sha1sum:
791 print('ovl server is out of date. killing...')
792 KillGraceful(self._server.GetPid())
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800793 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800794
795 def GetSSHControlFile(self, host):
796 return _SSH_CONTROL_SOCKET_PREFIX + host
797
798 def SSHTunnel(self, user, host, port):
799 """SSH forward the remote overlord server.
800
801 Overlord server may not have port 9000 open to the public network, in such
802 case we can SSH forward the port to localhost.
803 """
804
805 control_file = self.GetSSHControlFile(host)
806 try:
807 os.unlink(control_file)
808 except Exception:
809 pass
810
811 subprocess.Popen([
812 'ssh', '-Nf',
813 '-M', # Enable master mode
814 '-S', control_file,
815 '-L', '9000:localhost:9000',
816 '-p', str(port),
817 '%s%s' % (user + '@' if user else '', host)
818 ]).wait()
819
820 p = subprocess.Popen([
821 'ssh',
822 '-S', control_file,
823 '-O', 'check', host,
824 ], stderr=subprocess.PIPE)
825 unused_stdout, stderr = p.communicate()
826
827 s = re.search(r'pid=(\d+)', stderr)
828 if s:
829 return int(s.group(1))
830
831 raise RuntimeError('can not establish ssh connection')
832
833 def CheckConnection(self):
834 if self._state.host is None:
835 raise RuntimeError('not connected to any server, abort')
836
837 try:
838 self._server.Clients()
839 except Exception:
840 raise RuntimeError('remote server disconnected, abort')
841
842 if self._state.ssh_pid is not None:
843 ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)],
844 stdout=subprocess.PIPE,
845 stderr=subprocess.PIPE).wait()
846 if ret != 0:
847 raise RuntimeError('ssh tunnel disconnected, please re-connect')
848
849 def CheckClient(self):
850 if self._selected_mid is None:
851 if self._state.selected_mid is None:
852 raise RuntimeError('No client is selected')
853 self._selected_mid = self._state.selected_mid
854
855 if self._selected_mid not in self._server.Clients():
856 raise RuntimeError('client %s disappeared' % self._selected_mid)
857
858 def CheckOutput(self, command):
859 headers = []
860 if self._state.username is not None and self._state.password is not None:
861 headers.append(BasicAuthHeader(self._state.username,
862 self._state.password))
863
864 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
865 sio = StringIO.StringIO()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800866 ws = ShellWebSocketClient(self._state.host, sio,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800867 scheme + '%s:%d/api/agent/shell/%s?command=%s' %
868 (self._state.host, self._state.port,
869 self._selected_mid, urllib2.quote(command)),
870 headers=headers)
871 ws.connect()
872 ws.run()
873 return sio.getvalue()
874
875 @Command('status', 'show Overlord connection status')
876 def Status(self):
877 if self._state.host is None:
878 print('Not connected to any host.')
879 else:
880 if self._state.ssh_pid is not None:
881 print('Connected to %s with SSH tunneling.' % self._state.orig_host)
882 else:
883 print('Connected to %s:%d.' % (self._state.host, self._state.port))
884
885 if self._selected_mid is None:
886 self._selected_mid = self._state.selected_mid
887
888 if self._selected_mid is None:
889 print('No client is selected.')
890 else:
891 print('Client %s selected.' % self._selected_mid)
892
893 @Command('connect', 'connect to Overlord server', [
894 Arg('host', metavar='HOST', type=str, default='localhost',
895 help='Overlord hostname/IP'),
896 Arg('port', metavar='PORT', type=int,
897 default=_OVERLORD_HTTP_PORT, help='Overlord port'),
898 Arg('-f', '--forward', dest='ssh_forward', default=False,
899 action='store_true',
900 help='connect with SSH forwarding to the host'),
901 Arg('-p', '--ssh-port', dest='ssh_port', default=22,
902 type=int, help='SSH server port for SSH forwarding'),
903 Arg('-l', '--ssh-login', dest='ssh_login', default='',
904 type=str, help='SSH server login name for SSH forwarding'),
905 Arg('-u', '--user', dest='user', default=None,
906 type=str, help='Overlord HTTP auth username'),
907 Arg('-w', '--passwd', dest='passwd', default=None, type=str,
908 help='Overlord HTTP auth password')])
909 def Connect(self, args):
910 ssh_pid = None
911 host = args.host
912 orig_host = args.host
913
914 if args.ssh_forward:
915 # Kill previous SSH tunnel
916 self.KillSSHTunnel()
917
918 ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port)
919 host = 'localhost'
920
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800921 username_provided = args.user is not None
922 password_provided = args.passwd is not None
923 prompt = False
924
925 for unused_i in range(3):
926 try:
927 if prompt:
928 if not username_provided:
929 args.user = raw_input('Username: ')
930 if not password_provided:
931 args.passwd = getpass.getpass('Password: ')
932
933 ret = self._server.Connect(host, args.port, ssh_pid, args.user,
934 args.passwd, orig_host)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800935 if isinstance(ret, list):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800936 if ret[0].startswith('SSL'):
937 cert_pem = ret[1]
938 fp = GetTLSCertificateSHA1Fingerprint(cert_pem)
939 fp_text = ':'.join([fp[i:i+2] for i in range(0, len(fp), 2)])
940
941 if ret[0] == 'SSLCertificateChanged':
942 print(_TLS_CERT_CHANGED_WARNING % (fp_text, GetTLSCertPath(host)))
943 return
944 elif ret[0] == 'SSLVerifyFailed':
945 print('Server fingerprint: %s' % fp_text)
946 response = raw_input('Do you want to continue? [Y/n] ')
947 if response.lower() in ['y', 'ye', 'yes']:
948 self._SaveTLSCertificate(host, cert_pem)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800949 continue
950 else:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800951 print('connection aborted.')
952 return
953 elif ret[0] == 'HTTPError':
954 code, except_str, body = ret[1:]
955 if code == 401:
956 print('connect: %s' % body)
957 prompt = True
958 if not username_provided or not password_provided:
959 continue
960 else:
961 break
962 else:
963 logging.error('%s; %s', except_str, body)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800964
965 if ret is not True:
966 print('can not connect to %s: %s' % (host, ret))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800967 else:
968 print('connection to %s:%d established.' % (host, args.port))
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800969 except Exception as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800970 logging.error(e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800971 else:
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800972 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800973
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800974 @Command('start-server', 'start overlord CLI client server')
975 def StartServer(self):
976 self._server = OverlordClientDaemon.GetRPCServer()
977 if self._server is None:
978 OverlordClientDaemon().Start()
979 time.sleep(1)
980 self._server = OverlordClientDaemon.GetRPCServer()
981 if self._server is not None:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800982 print('* daemon started successfully *\n')
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800983
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800984 @Command('kill-server', 'kill overlord CLI client server')
985 def KillServer(self):
986 self._server = OverlordClientDaemon.GetRPCServer()
987 if self._server is None:
988 return
989
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800990 self._state = self._server.State()
991
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800992 # Kill SSH Tunnel
993 self.KillSSHTunnel()
994
995 # Kill server daemon
996 KillGraceful(self._server.GetPid())
997
998 def KillSSHTunnel(self):
999 if self._state.ssh_pid is not None:
1000 KillGraceful(self._state.ssh_pid)
1001
1002 @Command('ls', 'list all clients')
1003 def ListClients(self):
1004 for client in self._server.Clients():
1005 print(client)
1006
1007 @Command('select', 'select default client', [
1008 Arg('mid', metavar='mid', nargs='?', default=None)])
1009 def SelectClient(self, args=None, store=True):
1010 clients = self._server.Clients()
1011
1012 mid = args.mid if args is not None else None
1013 if mid is None:
1014 print('Select from the following clients:')
1015 for i, client in enumerate(clients):
1016 print(' %d. %s' % (i + 1, client))
1017
1018 print('\nSelection: ', end='')
1019 try:
1020 choice = int(raw_input()) - 1
1021 mid = clients[choice]
1022 except ValueError:
1023 raise RuntimeError('select: invalid selection')
1024 except IndexError:
1025 raise RuntimeError('select: selection out of range')
1026 else:
1027 if mid not in clients:
1028 raise RuntimeError('select: client %s does not exist' % mid)
1029
1030 self._selected_mid = mid
1031 if store:
1032 self._server.SelectClient(mid)
1033 print('Client %s selected' % mid)
1034
1035 @Command('shell', 'open a shell or execute a shell command', [
1036 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1037 def Shell(self, command=None):
1038 if command is None:
1039 command = []
1040 self.CheckClient()
1041
1042 headers = []
1043 if self._state.username is not None and self._state.password is not None:
1044 headers.append(BasicAuthHeader(self._state.username,
1045 self._state.password))
1046
1047 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1048 if len(command) == 0:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001049 ws = TerminalWebSocketClient(self._state.host, self._selected_mid,
Wei-Ning Huang0c520e92016-03-19 20:01:10 +08001050 self._escape,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001051 scheme + '%s:%d/api/agent/tty/%s' %
1052 (self._state.host, self._state.port,
1053 self._selected_mid), headers=headers)
1054 else:
1055 cmd = ' '.join(command)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001056 ws = ShellWebSocketClient(self._state.host, sys.stdout,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001057 scheme + '%s:%d/api/agent/shell/%s?command=%s' %
1058 (self._state.host, self._state.port,
1059 self._selected_mid, urllib2.quote(cmd)),
1060 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001061 try:
1062 ws.connect()
1063 ws.run()
1064 except socket.error as e:
1065 if e.errno == 32: # Broken pipe
1066 pass
1067 else:
1068 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001069
1070 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001071 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001072 Arg('dst', metavar='DESTINATION')])
1073 def Push(self, args):
1074 self.CheckClient()
1075
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001076 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001077 def _push(src, dst):
1078 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001079
1080 # Local file is a link
1081 if os.path.islink(src):
1082 pbar = ProgressBar(src_base)
1083 link_path = os.readlink(src)
1084 self.CheckOutput('mkdir -p %(dirname)s; '
1085 'if [ -d "%(dst)s" ]; then '
1086 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1087 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1088 dict(dirname=os.path.dirname(dst),
1089 link_path=link_path, dst=dst,
1090 link_name=src_base))
1091 pbar.End()
1092 return
1093
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001094 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1095 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
1096 (self._state.host, self._state.port, self._selected_mid, dst,
1097 mode))
1098 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001099 UrlOpen(self._state, url + '&filename=%s' % src_base)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001100 except urllib2.HTTPError as e:
1101 msg = json.loads(e.read()).get('error', None)
1102 raise RuntimeError('push: %s' % msg)
1103
1104 pbar = ProgressBar(src_base)
1105 self._HTTPPostFile(url, src, pbar.SetProgress,
1106 self._state.username, self._state.password)
1107 pbar.End()
1108
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001109 def _push_single_target(src, dst):
1110 if os.path.isdir(src):
1111 dst_exists = ast.literal_eval(self.CheckOutput(
1112 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1113 for root, unused_x, files in os.walk(src):
1114 # If destination directory does not exist, we should strip the first
1115 # layer of directory. For example: src_dir contains a single file 'A'
1116 #
1117 # push src_dir dest_dir
1118 #
1119 # If dest_dir exists, the resulting directory structure should be:
1120 # dest_dir/src_dir/A
1121 # If dest_dir does not exist, the resulting directory structure should
1122 # be:
1123 # dest_dir/A
1124 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1125 for name in files:
1126 _push(os.path.join(root, name),
1127 os.path.join(dst, dst_root, name))
1128 else:
1129 _push(src, dst)
1130
1131 if len(args.srcs) > 1:
1132 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1133 '2>/dev/null' % args.dst).strip()
1134 if not dst_type:
1135 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1136 if dst_type != 'directory':
1137 raise RuntimeError('push: %s: Not a directory' % args.dst)
1138
1139 for src in args.srcs:
1140 if not os.path.exists(src):
1141 raise RuntimeError('push: can not stat "%s": no such file or directory'
1142 % src)
1143 if not os.access(src, os.R_OK):
1144 raise RuntimeError('push: can not open "%s" for reading' % src)
1145
1146 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001147
1148 @Command('pull', 'pull a file or directory from remote', [
1149 Arg('src', metavar='SOURCE'),
1150 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1151 def Pull(self, args):
1152 self.CheckClient()
1153
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001154 @AutoRetry('pull', _RETRY_TIMES)
1155 def _pull(src, dst, ftype, perm=0644, link=None):
1156 try:
1157 os.makedirs(os.path.dirname(dst))
1158 except Exception:
1159 pass
1160
1161 src_base = os.path.basename(src)
1162
1163 # Remote file is a link
1164 if ftype == 'l':
1165 pbar = ProgressBar(src_base)
1166 if os.path.exists(dst):
1167 os.remove(dst)
1168 os.symlink(link, dst)
1169 pbar.End()
1170 return
1171
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001172 url = ('%s:%d/api/agent/download/%s?filename=%s' %
1173 (self._state.host, self._state.port, self._selected_mid,
1174 urllib2.quote(src)))
1175 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001176 h = UrlOpen(self._state, url)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001177 except urllib2.HTTPError as e:
1178 msg = json.loads(e.read()).get('error', 'unkown error')
1179 raise RuntimeError('pull: %s' % msg)
1180 except KeyboardInterrupt:
1181 return
1182
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001183 pbar = ProgressBar(src_base)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001184 with open(dst, 'w') as f:
1185 os.fchmod(f.fileno(), perm)
1186 total_size = int(h.headers.get('Content-Length'))
1187 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001188
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001189 while True:
1190 data = h.read(_BUFSIZ)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001191 if len(data) == 0:
1192 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001193 downloaded_size += len(data)
1194 pbar.SetProgress(float(downloaded_size) * 100 / total_size,
1195 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001196 f.write(data)
1197 pbar.End()
1198
1199 # Use find to get a listing of all files under a root directory. The 'stat'
1200 # command is used to retrieve the filename and it's filemode.
1201 output = self.CheckOutput(
1202 'cd $HOME; '
1203 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001204 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1205 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001206 % {'src': args.src})
1207
1208 # We got error from the stat command
1209 if output.startswith('stat: '):
1210 sys.stderr.write(output)
1211 return
1212
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001213 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001214 common_prefix = os.path.dirname(args.src)
1215
1216 if len(entries) == 1:
1217 entry = entries[0]
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 if os.path.isdir(args.dst):
1220 dst = os.path.join(args.dst, os.path.basename(src_path))
1221 else:
1222 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001223 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001224 else:
1225 if not os.path.exists(args.dst):
1226 common_prefix = args.src
1227
1228 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001229 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001230 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001231 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1232 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001233
1234 @Command('forward', 'forward remote port to local port', [
1235 Arg('--list', dest='list_all', action='store_true', default=False,
1236 help='list all port forwarding sessions'),
1237 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1238 default=None,
1239 help='remove port forwarding for local port LOCAL_PORT'),
1240 Arg('--remove-all', dest='remove_all', action='store_true',
1241 default=False, help='remove all port forwarding'),
1242 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1243 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1244 def Forward(self, args):
1245 if args.list_all:
1246 max_len = 10
1247 if len(self._state.forwards):
1248 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1249
1250 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1251 for local in sorted(self._state.forwards.keys()):
1252 value = self._state.forwards[local]
1253 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1254 return
1255
1256 if args.remove_all:
1257 self._server.RemoveAllForward()
1258 return
1259
1260 if args.remove:
1261 self._server.RemoveForward(args.remove)
1262 return
1263
1264 self.CheckClient()
1265
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001266 if args.remote is None:
1267 raise RuntimeError('remote port not specified')
1268
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001269 if args.local is None:
1270 args.local = args.remote
1271 remote = int(args.remote)
1272 local = int(args.local)
1273
1274 def HandleConnection(conn):
1275 headers = []
1276 if self._state.username is not None and self._state.password is not None:
1277 headers.append(BasicAuthHeader(self._state.username,
1278 self._state.password))
1279
1280 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1281 ws = ForwarderWebSocketClient(
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001282 self._state.host, conn,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001283 scheme + '%s:%d/api/agent/forward/%s?port=%d' %
1284 (self._state.host, self._state.port, self._selected_mid, remote),
1285 headers=headers)
1286 try:
1287 ws.connect()
1288 ws.run()
1289 except Exception as e:
1290 print('error: %s' % e)
1291 finally:
1292 ws.close()
1293
1294 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1295 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1296 server.bind(('0.0.0.0', local))
1297 server.listen(5)
1298
1299 pid = os.fork()
1300 if pid == 0:
1301 while True:
1302 conn, unused_addr = server.accept()
1303 t = threading.Thread(target=HandleConnection, args=(conn,))
1304 t.daemon = True
1305 t.start()
1306 else:
1307 self._server.AddForward(self._selected_mid, remote, local, pid)
1308
1309
1310def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001311 # Setup logging format
1312 logger = logging.getLogger()
1313 logger.setLevel(logging.INFO)
1314 handler = logging.StreamHandler()
1315 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1316 handler.setFormatter(formatter)
1317 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001318
1319 # Add DaemonState to JSONRPC lib classes
1320 Config.instance().classes.add(DaemonState)
1321
1322 ovl = OverlordCLIClient()
1323 try:
1324 ovl.Main()
1325 except KeyboardInterrupt:
1326 print('Ctrl-C received, abort')
1327 except Exception as e:
1328 print('error: %s' % e)
1329
1330
1331if __name__ == '__main__':
1332 main()