Show More
@@ -0,0 +1,183 b'' | |||||
|
1 | #!/usr/bin/env python | |||
|
2 | ||||
|
3 | # | |||
|
4 | # This file is adapted from a paramiko demo, and thus LGPL 2.1. | |||
|
5 | # Original Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> | |||
|
6 | # Edits Copyright (C) 2010 The IPython Team | |||
|
7 | # | |||
|
8 | # Paramiko is free software; you can redistribute it and/or modify it under the | |||
|
9 | # terms of the GNU Lesser General Public License as published by the Free | |||
|
10 | # Software Foundation; either version 2.1 of the License, or (at your option) | |||
|
11 | # any later version. | |||
|
12 | # | |||
|
13 | # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY | |||
|
14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR | |||
|
15 | # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more | |||
|
16 | # details. | |||
|
17 | # | |||
|
18 | # You should have received a copy of the GNU Lesser General Public License | |||
|
19 | # along with Paramiko; if not, write to the Free Software Foundation, Inc., | |||
|
20 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. | |||
|
21 | ||||
|
22 | """ | |||
|
23 | Sample script showing how to do local port forwarding over paramiko. | |||
|
24 | ||||
|
25 | This script connects to the requested SSH server and sets up local port | |||
|
26 | forwarding (the openssh -L option) from a local port through a tunneled | |||
|
27 | connection to a destination reachable from the SSH server machine. | |||
|
28 | """ | |||
|
29 | ||||
|
30 | from __future__ import print_function | |||
|
31 | ||||
|
32 | import getpass | |||
|
33 | import os | |||
|
34 | import socket | |||
|
35 | import select | |||
|
36 | import SocketServer | |||
|
37 | import sys | |||
|
38 | from optparse import OptionParser | |||
|
39 | ||||
|
40 | import paramiko | |||
|
41 | ||||
|
42 | SSH_PORT = 22 | |||
|
43 | DEFAULT_PORT = 4000 | |||
|
44 | ||||
|
45 | g_verbose = False | |||
|
46 | ||||
|
47 | ||||
|
48 | class ForwardServer (SocketServer.ThreadingTCPServer): | |||
|
49 | daemon_threads = True | |||
|
50 | allow_reuse_address = True | |||
|
51 | ||||
|
52 | ||||
|
53 | class Handler (SocketServer.BaseRequestHandler): | |||
|
54 | ||||
|
55 | def handle(self): | |||
|
56 | try: | |||
|
57 | chan = self.ssh_transport.open_channel('direct-tcpip', | |||
|
58 | (self.chain_host, self.chain_port), | |||
|
59 | self.request.getpeername()) | |||
|
60 | except Exception, e: | |||
|
61 | verbose('Incoming request to %s:%d failed: %s' % (self.chain_host, | |||
|
62 | self.chain_port, | |||
|
63 | repr(e))) | |||
|
64 | return | |||
|
65 | if chan is None: | |||
|
66 | verbose('Incoming request to %s:%d was rejected by the SSH server.' % | |||
|
67 | (self.chain_host, self.chain_port)) | |||
|
68 | return | |||
|
69 | ||||
|
70 | verbose('Connected! Tunnel open %r -> %r -> %r' % (self.request.getpeername(), | |||
|
71 | chan.getpeername(), (self.chain_host, self.chain_port))) | |||
|
72 | while True: | |||
|
73 | r, w, x = select.select([self.request, chan], [], []) | |||
|
74 | if self.request in r: | |||
|
75 | data = self.request.recv(1024) | |||
|
76 | if len(data) == 0: | |||
|
77 | break | |||
|
78 | chan.send(data) | |||
|
79 | if chan in r: | |||
|
80 | data = chan.recv(1024) | |||
|
81 | if len(data) == 0: | |||
|
82 | break | |||
|
83 | self.request.send(data) | |||
|
84 | chan.close() | |||
|
85 | self.request.close() | |||
|
86 | verbose('Tunnel closed from %r' % (self.request.getpeername(),)) | |||
|
87 | ||||
|
88 | ||||
|
89 | def forward_tunnel(local_port, remote_host, remote_port, transport): | |||
|
90 | # this is a little convoluted, but lets me configure things for the Handler | |||
|
91 | # object. (SocketServer doesn't give Handlers any way to access the outer | |||
|
92 | # server normally.) | |||
|
93 | class SubHander (Handler): | |||
|
94 | chain_host = remote_host | |||
|
95 | chain_port = remote_port | |||
|
96 | ssh_transport = transport | |||
|
97 | ForwardServer(('', local_port), SubHander).serve_forever() | |||
|
98 | ||||
|
99 | ||||
|
100 | def verbose(s): | |||
|
101 | if g_verbose: | |||
|
102 | print (s) | |||
|
103 | ||||
|
104 | ||||
|
105 | HELP = """\ | |||
|
106 | Set up a forward tunnel across an SSH server, using paramiko. A local port | |||
|
107 | (given with -p) is forwarded across an SSH session to an address:port from | |||
|
108 | the SSH server. This is similar to the openssh -L option. | |||
|
109 | """ | |||
|
110 | ||||
|
111 | ||||
|
112 | def get_host_port(spec, default_port): | |||
|
113 | "parse 'hostname:22' into a host and port, with the port optional" | |||
|
114 | args = (spec.split(':', 1) + [default_port])[:2] | |||
|
115 | args[1] = int(args[1]) | |||
|
116 | return args[0], args[1] | |||
|
117 | ||||
|
118 | ||||
|
119 | def parse_options(): | |||
|
120 | global g_verbose | |||
|
121 | ||||
|
122 | parser = OptionParser(usage='usage: %prog [options] <ssh-server>[:<server-port>]', | |||
|
123 | version='%prog 1.0', description=HELP) | |||
|
124 | parser.add_option('-q', '--quiet', action='store_false', dest='verbose', default=True, | |||
|
125 | help='squelch all informational output') | |||
|
126 | parser.add_option('-p', '--local-port', action='store', type='int', dest='port', | |||
|
127 | default=DEFAULT_PORT, | |||
|
128 | help='local port to forward (default: %d)' % DEFAULT_PORT) | |||
|
129 | parser.add_option('-u', '--user', action='store', type='string', dest='user', | |||
|
130 | default=getpass.getuser(), | |||
|
131 | help='username for SSH authentication (default: %s)' % getpass.getuser()) | |||
|
132 | parser.add_option('-K', '--key', action='store', type='string', dest='keyfile', | |||
|
133 | default=None, | |||
|
134 | help='private key file to use for SSH authentication') | |||
|
135 | parser.add_option('', '--no-key', action='store_false', dest='look_for_keys', default=True, | |||
|
136 | help='don\'t look for or use a private key file') | |||
|
137 | parser.add_option('-P', '--password', action='store_true', dest='readpass', default=False, | |||
|
138 | help='read password (for key or password auth) from stdin') | |||
|
139 | parser.add_option('-r', '--remote', action='store', type='string', dest='remote', default=None, metavar='host:port', | |||
|
140 | help='remote host and port to forward to') | |||
|
141 | options, args = parser.parse_args() | |||
|
142 | ||||
|
143 | if len(args) != 1: | |||
|
144 | parser.error('Incorrect number of arguments.') | |||
|
145 | if options.remote is None: | |||
|
146 | parser.error('Remote address required (-r).') | |||
|
147 | ||||
|
148 | g_verbose = options.verbose | |||
|
149 | server_host, server_port = get_host_port(args[0], SSH_PORT) | |||
|
150 | remote_host, remote_port = get_host_port(options.remote, SSH_PORT) | |||
|
151 | return options, (server_host, server_port), (remote_host, remote_port) | |||
|
152 | ||||
|
153 | ||||
|
154 | def main(): | |||
|
155 | options, server, remote = parse_options() | |||
|
156 | ||||
|
157 | password = None | |||
|
158 | if options.readpass: | |||
|
159 | password = getpass.getpass('Enter SSH password: ') | |||
|
160 | ||||
|
161 | client = paramiko.SSHClient() | |||
|
162 | client.load_system_host_keys() | |||
|
163 | client.set_missing_host_key_policy(paramiko.WarningPolicy()) | |||
|
164 | ||||
|
165 | verbose('Connecting to ssh host %s:%d ...' % (server[0], server[1])) | |||
|
166 | try: | |||
|
167 | client.connect(server[0], server[1], username=options.user, key_filename=options.keyfile, | |||
|
168 | look_for_keys=options.look_for_keys, password=password) | |||
|
169 | except Exception as e: | |||
|
170 | print ('*** Failed to connect to %s:%d: %r' % (server[0], server[1], e)) | |||
|
171 | sys.exit(1) | |||
|
172 | ||||
|
173 | verbose('Now forwarding port %d to %s:%d ...' % (options.port, remote[0], remote[1])) | |||
|
174 | ||||
|
175 | try: | |||
|
176 | forward_tunnel(options.port, remote[0], remote[1], client.get_transport()) | |||
|
177 | except KeyboardInterrupt: | |||
|
178 | print ('C-c: Port forwarding stopped.') | |||
|
179 | sys.exit(0) | |||
|
180 | ||||
|
181 | ||||
|
182 | if __name__ == '__main__': | |||
|
183 | main() |
@@ -0,0 +1,217 b'' | |||||
|
1 | """KernelStarter class that intercepts Control Queue messages, and handles process management.""" | |||
|
2 | ||||
|
3 | from zmq.eventloop import ioloop | |||
|
4 | from streamsession import StreamSession | |||
|
5 | ||||
|
6 | class KernelStarter(object): | |||
|
7 | """Object for resetting/killing the Kernel.""" | |||
|
8 | ||||
|
9 | ||||
|
10 | def __init__(self, session, upstream, downstream, *kernel_args, **kernel_kwargs): | |||
|
11 | self.session = session | |||
|
12 | self.upstream = upstream | |||
|
13 | self.downstream = downstream | |||
|
14 | self.kernel_args = kernel_args | |||
|
15 | self.kernel_kwargs = kernel_kwargs | |||
|
16 | self.handlers = {} | |||
|
17 | for method in 'shutdown_request shutdown_reply'.split(): | |||
|
18 | self.handlers[method] = getattr(self, method) | |||
|
19 | ||||
|
20 | def start(self): | |||
|
21 | self.upstream.on_recv(self.dispatch_request) | |||
|
22 | self.downstream.on_recv(self.dispatch_reply) | |||
|
23 | ||||
|
24 | #-------------------------------------------------------------------------- | |||
|
25 | # Dispatch methods | |||
|
26 | #-------------------------------------------------------------------------- | |||
|
27 | ||||
|
28 | def dispatch_request(self, raw_msg): | |||
|
29 | idents, msg = self.session.feed_identities() | |||
|
30 | try: | |||
|
31 | msg = self.session.unpack_message(msg, content=False) | |||
|
32 | except: | |||
|
33 | print ("bad msg: %s"%msg) | |||
|
34 | ||||
|
35 | msgtype = msg['msg_type'] | |||
|
36 | handler = self.handlers.get(msgtype, None) | |||
|
37 | if handler is None: | |||
|
38 | self.downstream.send_multipart(raw_msg) | |||
|
39 | else: | |||
|
40 | handler(msg) | |||
|
41 | ||||
|
42 | def dispatch_reply(self, raw_msg): | |||
|
43 | idents, msg = self.session.feed_identities() | |||
|
44 | try: | |||
|
45 | msg = self.session.unpack_message(msg, content=False) | |||
|
46 | except: | |||
|
47 | print ("bad msg: %s"%msg) | |||
|
48 | ||||
|
49 | msgtype = msg['msg_type'] | |||
|
50 | handler = self.handlers.get(msgtype, None) | |||
|
51 | if handler is None: | |||
|
52 | self.upstream.send_multipart(raw_msg) | |||
|
53 | else: | |||
|
54 | handler(msg) | |||
|
55 | ||||
|
56 | #-------------------------------------------------------------------------- | |||
|
57 | # Handlers | |||
|
58 | #-------------------------------------------------------------------------- | |||
|
59 | ||||
|
60 | def shutdown_request(self, msg): | |||
|
61 | ||||
|
62 | ||||
|
63 | #-------------------------------------------------------------------------- | |||
|
64 | # Kernel process management methods, from KernelManager: | |||
|
65 | #-------------------------------------------------------------------------- | |||
|
66 | ||||
|
67 | def _check_local(addr): | |||
|
68 | if isinstance(addr, tuple): | |||
|
69 | addr = addr[0] | |||
|
70 | return addr in LOCAL_IPS | |||
|
71 | ||||
|
72 | def start_kernel(self, **kw): | |||
|
73 | """Starts a kernel process and configures the manager to use it. | |||
|
74 | ||||
|
75 | If random ports (port=0) are being used, this method must be called | |||
|
76 | before the channels are created. | |||
|
77 | ||||
|
78 | Parameters: | |||
|
79 | ----------- | |||
|
80 | ipython : bool, optional (default True) | |||
|
81 | Whether to use an IPython kernel instead of a plain Python kernel. | |||
|
82 | """ | |||
|
83 | self.kernel = Process(target=make_kernel, args=self.kernel_args, | |||
|
84 | kwargs=self.kernel_kwargs) | |||
|
85 | ||||
|
86 | def shutdown_kernel(self, restart=False): | |||
|
87 | """ Attempts to the stop the kernel process cleanly. If the kernel | |||
|
88 | cannot be stopped, it is killed, if possible. | |||
|
89 | """ | |||
|
90 | # FIXME: Shutdown does not work on Windows due to ZMQ errors! | |||
|
91 | if sys.platform == 'win32': | |||
|
92 | self.kill_kernel() | |||
|
93 | return | |||
|
94 | ||||
|
95 | # Don't send any additional kernel kill messages immediately, to give | |||
|
96 | # the kernel a chance to properly execute shutdown actions. Wait for at | |||
|
97 | # most 1s, checking every 0.1s. | |||
|
98 | self.xreq_channel.shutdown(restart=restart) | |||
|
99 | for i in range(10): | |||
|
100 | if self.is_alive: | |||
|
101 | time.sleep(0.1) | |||
|
102 | else: | |||
|
103 | break | |||
|
104 | else: | |||
|
105 | # OK, we've waited long enough. | |||
|
106 | if self.has_kernel: | |||
|
107 | self.kill_kernel() | |||
|
108 | ||||
|
109 | def restart_kernel(self, now=False): | |||
|
110 | """Restarts a kernel with the same arguments that were used to launch | |||
|
111 | it. If the old kernel was launched with random ports, the same ports | |||
|
112 | will be used for the new kernel. | |||
|
113 | ||||
|
114 | Parameters | |||
|
115 | ---------- | |||
|
116 | now : bool, optional | |||
|
117 | If True, the kernel is forcefully restarted *immediately*, without | |||
|
118 | having a chance to do any cleanup action. Otherwise the kernel is | |||
|
119 | given 1s to clean up before a forceful restart is issued. | |||
|
120 | ||||
|
121 | In all cases the kernel is restarted, the only difference is whether | |||
|
122 | it is given a chance to perform a clean shutdown or not. | |||
|
123 | """ | |||
|
124 | if self._launch_args is None: | |||
|
125 | raise RuntimeError("Cannot restart the kernel. " | |||
|
126 | "No previous call to 'start_kernel'.") | |||
|
127 | else: | |||
|
128 | if self.has_kernel: | |||
|
129 | if now: | |||
|
130 | self.kill_kernel() | |||
|
131 | else: | |||
|
132 | self.shutdown_kernel(restart=True) | |||
|
133 | self.start_kernel(**self._launch_args) | |||
|
134 | ||||
|
135 | # FIXME: Messages get dropped in Windows due to probable ZMQ bug | |||
|
136 | # unless there is some delay here. | |||
|
137 | if sys.platform == 'win32': | |||
|
138 | time.sleep(0.2) | |||
|
139 | ||||
|
140 | @property | |||
|
141 | def has_kernel(self): | |||
|
142 | """Returns whether a kernel process has been specified for the kernel | |||
|
143 | manager. | |||
|
144 | """ | |||
|
145 | return self.kernel is not None | |||
|
146 | ||||
|
147 | def kill_kernel(self): | |||
|
148 | """ Kill the running kernel. """ | |||
|
149 | if self.has_kernel: | |||
|
150 | # Pause the heart beat channel if it exists. | |||
|
151 | if self._hb_channel is not None: | |||
|
152 | self._hb_channel.pause() | |||
|
153 | ||||
|
154 | # Attempt to kill the kernel. | |||
|
155 | try: | |||
|
156 | self.kernel.kill() | |||
|
157 | except OSError, e: | |||
|
158 | # In Windows, we will get an Access Denied error if the process | |||
|
159 | # has already terminated. Ignore it. | |||
|
160 | if not (sys.platform == 'win32' and e.winerror == 5): | |||
|
161 | raise | |||
|
162 | self.kernel = None | |||
|
163 | else: | |||
|
164 | raise RuntimeError("Cannot kill kernel. No kernel is running!") | |||
|
165 | ||||
|
166 | def interrupt_kernel(self): | |||
|
167 | """ Interrupts the kernel. Unlike ``signal_kernel``, this operation is | |||
|
168 | well supported on all platforms. | |||
|
169 | """ | |||
|
170 | if self.has_kernel: | |||
|
171 | if sys.platform == 'win32': | |||
|
172 | from parentpoller import ParentPollerWindows as Poller | |||
|
173 | Poller.send_interrupt(self.kernel.win32_interrupt_event) | |||
|
174 | else: | |||
|
175 | self.kernel.send_signal(signal.SIGINT) | |||
|
176 | else: | |||
|
177 | raise RuntimeError("Cannot interrupt kernel. No kernel is running!") | |||
|
178 | ||||
|
179 | def signal_kernel(self, signum): | |||
|
180 | """ Sends a signal to the kernel. Note that since only SIGTERM is | |||
|
181 | supported on Windows, this function is only useful on Unix systems. | |||
|
182 | """ | |||
|
183 | if self.has_kernel: | |||
|
184 | self.kernel.send_signal(signum) | |||
|
185 | else: | |||
|
186 | raise RuntimeError("Cannot signal kernel. No kernel is running!") | |||
|
187 | ||||
|
188 | @property | |||
|
189 | def is_alive(self): | |||
|
190 | """Is the kernel process still running?""" | |||
|
191 | # FIXME: not using a heartbeat means this method is broken for any | |||
|
192 | # remote kernel, it's only capable of handling local kernels. | |||
|
193 | if self.has_kernel: | |||
|
194 | if self.kernel.poll() is None: | |||
|
195 | return True | |||
|
196 | else: | |||
|
197 | return False | |||
|
198 | else: | |||
|
199 | # We didn't start the kernel with this KernelManager so we don't | |||
|
200 | # know if it is running. We should use a heartbeat for this case. | |||
|
201 | return True | |||
|
202 | ||||
|
203 | ||||
|
204 | def make_starter(up_addr, down_addr, *args, **kwargs): | |||
|
205 | """entry point script for launching a kernelstarter in a subprocess""" | |||
|
206 | loop = ioloop.IOLoop.instance() | |||
|
207 | ctx = zmq.Context() | |||
|
208 | session = StreamSession() | |||
|
209 | upstream = zmqstream.ZMQStream(ctx.socket(zmq.XREQ),loop) | |||
|
210 | upstream.connect(up_addr) | |||
|
211 | downstream = zmqstream.ZMQStream(ctx.socket(zmq.XREQ),loop) | |||
|
212 | downstream.connect(down_addr) | |||
|
213 | ||||
|
214 | starter = KernelStarter(session, upstream, downstream, *args, **kwargs) | |||
|
215 | starter.start() | |||
|
216 | loop.start() | |||
|
217 | No newline at end of file |
@@ -0,0 +1,123 b'' | |||||
|
1 | ||||
|
2 | ||||
|
3 | #----------------------------------------- | |||
|
4 | # Imports | |||
|
5 | #----------------------------------------- | |||
|
6 | ||||
|
7 | from __future__ import print_function | |||
|
8 | ||||
|
9 | import os,sys | |||
|
10 | from multiprocessing import Process | |||
|
11 | from getpass import getpass, getuser | |||
|
12 | ||||
|
13 | try: | |||
|
14 | import paramiko | |||
|
15 | except ImportError: | |||
|
16 | paramiko = None | |||
|
17 | else: | |||
|
18 | from forward import forward_tunnel | |||
|
19 | ||||
|
20 | from IPython.external import pexpect | |||
|
21 | ||||
|
22 | ||||
|
23 | def launch_ssh_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None, timeout=15): | |||
|
24 | """Create an ssh tunnel using command-line ssh that connects port lport | |||
|
25 | on this machine to localhost:rport on server. The tunnel | |||
|
26 | will automatically close when not in use, remaining open | |||
|
27 | for a minimum of timeout seconds for an initial connection. | |||
|
28 | """ | |||
|
29 | ssh="ssh " | |||
|
30 | if keyfile: | |||
|
31 | ssh += "-i " + keyfile | |||
|
32 | cmd = ssh + " -f -L %i:127.0.0.1:%i %s sleep %i"%(lport, rport, server, timeout) | |||
|
33 | tunnel = pexpect.spawn(cmd) | |||
|
34 | failed = False | |||
|
35 | while True: | |||
|
36 | try: | |||
|
37 | tunnel.expect('[Pp]assword:', timeout=.1) | |||
|
38 | except pexpect.TIMEOUT: | |||
|
39 | continue | |||
|
40 | except pexpect.EOF: | |||
|
41 | if tunnel.exitstatus: | |||
|
42 | print (tunnel.exitstatus) | |||
|
43 | print (tunnel.before) | |||
|
44 | print (tunnel.after) | |||
|
45 | raise RuntimeError("tunnel '%s' failed to start"%(cmd)) | |||
|
46 | else: | |||
|
47 | return tunnel.pid | |||
|
48 | else: | |||
|
49 | if failed: | |||
|
50 | print("Password rejected, try again") | |||
|
51 | tunnel.sendline(getpass()) | |||
|
52 | failed = True | |||
|
53 | ||||
|
54 | def _split_server(server): | |||
|
55 | if '@' in server: | |||
|
56 | username,server = server.split('@', 1) | |||
|
57 | else: | |||
|
58 | username = getuser() | |||
|
59 | if ':' in server: | |||
|
60 | server, port = server.split(':') | |||
|
61 | port = int(port) | |||
|
62 | else: | |||
|
63 | port = 22 | |||
|
64 | return username, server, port | |||
|
65 | ||||
|
66 | def launch_paramiko_tunnel(lport, rport, server, remoteip='127.0.0.1', keyfile=None): | |||
|
67 | """launch a tunner with paramiko in a subprocess""" | |||
|
68 | if paramiko is None: | |||
|
69 | raise ImportError("Paramiko not available") | |||
|
70 | server = _split_server(server) | |||
|
71 | if keyfile is None: | |||
|
72 | passwd = getpass("%s@%s's password: "%(server[0], server[1])) | |||
|
73 | else: | |||
|
74 | passwd = None | |||
|
75 | p = Process(target=_paramiko_tunnel, | |||
|
76 | args=(lport, rport, server, remoteip), | |||
|
77 | kwargs=dict(keyfile=keyfile, password=passwd)) | |||
|
78 | p.daemon=False | |||
|
79 | p.start() | |||
|
80 | return p | |||
|
81 | ||||
|
82 | ||||
|
83 | def _paramiko_tunnel(lport, rport, server, remoteip, keyfile=None, password=None): | |||
|
84 | """function for actually starting a paramiko tunnel, to be passed | |||
|
85 | to multiprocessing.Process(target=this). | |||
|
86 | """ | |||
|
87 | username, server, port = server | |||
|
88 | client = paramiko.SSHClient() | |||
|
89 | client.load_system_host_keys() | |||
|
90 | client.set_missing_host_key_policy(paramiko.WarningPolicy()) | |||
|
91 | ||||
|
92 | try: | |||
|
93 | client.connect(server, port, username=username, key_filename=keyfile, | |||
|
94 | look_for_keys=True, password=password) | |||
|
95 | except Exception as e: | |||
|
96 | print ('*** Failed to connect to %s:%d: %r' % (server, port, e)) | |||
|
97 | sys.exit(1) | |||
|
98 | ||||
|
99 | print ('Now forwarding port %d to %s:%d ...' % (lport, server, rport)) | |||
|
100 | ||||
|
101 | try: | |||
|
102 | forward_tunnel(lport, remoteip, rport, client.get_transport()) | |||
|
103 | except KeyboardInterrupt: | |||
|
104 | print ('C-c: Port forwarding stopped.') | |||
|
105 | sys.exit(0) | |||
|
106 | ||||
|
107 | ||||
|
108 | __all__ = ['launch_ssh_tunnel', 'launch_paramiko_tunnel'] | |||
|
109 | ||||
|
110 | ||||
|
111 | ||||
|
112 | ||||
|
113 | ||||
|
114 | ||||
|
115 | ||||
|
116 | ||||
|
117 | ||||
|
118 | ||||
|
119 | ||||
|
120 | ||||
|
121 | ||||
|
122 | ||||
|
123 |
@@ -11,15 +11,7 b' ANYWHERE = 1 << 3' | |||||
11 | class UnmetDependency(Exception): |
|
11 | class UnmetDependency(Exception): | |
12 | pass |
|
12 | pass | |
13 |
|
13 | |||
14 | class depend2(object): |
|
14 | ||
15 | """dependency decorator""" |
|
|||
16 | def __init__(self, f, *args, **kwargs): |
|
|||
17 | self.dependency = (f,args,kwargs) |
|
|||
18 |
|
||||
19 | def __call__(self, f, *args, **kwargs): |
|
|||
20 | f._dependency = self.dependency |
|
|||
21 | return decorator(_depend_wrapper, f) |
|
|||
22 |
|
||||
23 | class depend(object): |
|
15 | class depend(object): | |
24 | """Dependency decorator, for use with tasks.""" |
|
16 | """Dependency decorator, for use with tasks.""" | |
25 | def __init__(self, f, *args, **kwargs): |
|
17 | def __init__(self, f, *args, **kwargs): |
General Comments 0
You need to be logged in to leave comments.
Login now