Skip to content

Commit 8d6db10

Browse files
author
Olivier Roques
committed
Add files
1 parent 811bbf6 commit 8d6db10

File tree

3 files changed

+249
-0
lines changed

3 files changed

+249
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.swp
2+
*.pyc
3+
__pycache__/

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# python-sshcontroller

sshcontroller.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#!/usr/bin/python3
2+
import errno
3+
import logging
4+
import paramiko
5+
import socket
6+
from os import path
7+
from stat import S_ISDIR, S_ISREG
8+
9+
_KEY_TYPES = {
10+
"dsa": paramiko.DSSKey,
11+
"rsa": paramiko.RSAKey,
12+
"ecdsa": paramiko.ECDSAKey,
13+
"ed25519": paramiko.Ed25519Key,
14+
}
15+
16+
17+
class SFTPController(paramiko.SFTPClient):
18+
def __init__(self, sock):
19+
super().__init__(sock)
20+
21+
def exists(self, path):
22+
try:
23+
self.stat(path)
24+
except IOError as e:
25+
return e.errno != errno.ENOENT
26+
return True
27+
28+
def list_dirs(self, path):
29+
return [
30+
d.filename for d in self.listdir_attr(path) if S_ISDIR(d.st_mode)
31+
]
32+
33+
def list_files(self, path):
34+
return [
35+
f.filename for f in self.listdir_attr(path) if S_ISREG(f.st_mode)
36+
]
37+
38+
@classmethod
39+
def from_transport(cls, t):
40+
chan = t.open_session()
41+
chan.invoke_subsystem("sftp")
42+
return cls(chan)
43+
44+
45+
class SSHController:
46+
def __init__(
47+
self,
48+
host,
49+
user,
50+
key_path=None,
51+
key_password=None,
52+
key_type="rsa",
53+
ssh_password=None,
54+
port=22,
55+
):
56+
self.host = host
57+
self.user = user
58+
self.ssh_password = ssh_password if key_path is None else None
59+
self.port = port
60+
self.nb_bytes = 1024
61+
self.keys, self.transport = [], None
62+
key_type = key_type.lower()
63+
64+
if key_path:
65+
self.keys.append(
66+
_KEY_TYPES[key_type].from_private_key(
67+
open(path.expanduser(key_path), 'r'),
68+
key_password,
69+
)
70+
)
71+
elif ssh_password is None:
72+
try:
73+
self.keys.append(
74+
_KEY_TYPES[key_type].from_private_key(
75+
open(path.expanduser(f"~/.ssh/id_{key_type}"), 'r'),
76+
key_password
77+
)
78+
)
79+
except Exception as e:
80+
agent_keys = paramiko.Agent().get_keys()
81+
if not agent_keys:
82+
raise e
83+
self.keys = agent_keys
84+
85+
def connect(self):
86+
try:
87+
ssh_socket = socket.create_connection((self.host, self.port))
88+
except OSError as e:
89+
logging.error(f"Connection failed: {e.strerror}")
90+
return 1
91+
92+
self.transport = paramiko.Transport(ssh_socket)
93+
94+
if self.ssh_password is not None:
95+
try:
96+
self.transport.connect(
97+
username=self.user,
98+
password=self.ssh_password,
99+
)
100+
except paramiko.SSHException:
101+
pass
102+
else:
103+
for key in self.keys:
104+
try:
105+
self.transport.connect(username=self.user, pkey=key)
106+
except paramiko.SSHException:
107+
continue
108+
break
109+
110+
if not self.transport.is_authenticated():
111+
logging.error("SSH negotiation failed")
112+
return 1
113+
114+
logging.info(f"Successfully connected to {self.user}@{self.host}")
115+
return 0
116+
117+
def __run_until_event(
118+
self,
119+
command,
120+
stop_event,
121+
display=True,
122+
combine_stderr=False,
123+
capture_output=False,
124+
):
125+
channel = self.transport.open_session()
126+
output = []
127+
timeout = 2
128+
129+
channel.settimeout(timeout)
130+
channel.set_combine_stderr(combine_stderr)
131+
channel.get_pty()
132+
channel.exec_command(command)
133+
134+
if not display and not capture_output:
135+
stop_event.wait()
136+
else:
137+
while True:
138+
try:
139+
raw_data = channel.recv(self.nb_bytes)
140+
except socket.timeout:
141+
if stop_event.is_set():
142+
break
143+
continue
144+
145+
if not len(raw_data):
146+
break
147+
148+
data = raw_data.decode("utf-8").splitlines()
149+
150+
if display:
151+
print('\n'.join(data))
152+
153+
if capture_output:
154+
output += data
155+
156+
if stop_event.is_set():
157+
break
158+
159+
channel.close()
160+
return (channel.exit_status_ready(), output)
161+
162+
def __run_until_exit(
163+
self,
164+
command,
165+
timeout,
166+
display=True,
167+
combine_stderr=False,
168+
capture_output=False,
169+
):
170+
channel = self.transport.open_session()
171+
output = []
172+
173+
channel.settimeout(timeout)
174+
channel.set_combine_stderr(combine_stderr)
175+
channel.get_pty()
176+
channel.exec_command(command)
177+
178+
try:
179+
if not display and not capture_output:
180+
return (channel.recv_exit_status(), output)
181+
else:
182+
while True:
183+
raw_data = channel.recv(self.nb_bytes)
184+
185+
if not len(raw_data):
186+
break
187+
188+
data = raw_data.decode("utf-8").splitlines()
189+
190+
if display:
191+
print('\n'.join(data))
192+
193+
if capture_output:
194+
output += data
195+
except socket.timeout:
196+
logging.warning(f"Timeout after {timeout}s")
197+
return (1, output)
198+
except KeyboardInterrupt:
199+
logging.info("KeyboardInterrupt")
200+
return (0, output)
201+
finally:
202+
channel.close()
203+
204+
return (channel.recv_exit_status(), output)
205+
206+
def run(
207+
self,
208+
command,
209+
display=False,
210+
combine_stderr=False,
211+
capture_output=False,
212+
stop_event=None,
213+
timeout=600,
214+
):
215+
if stop_event:
216+
return self.__run_until_event(
217+
command,
218+
stop_event,
219+
display=display,
220+
combine_stderr=combine_stderr,
221+
capture_output=capture_output,
222+
)
223+
else:
224+
return self.__run_until_exit(
225+
command,
226+
timeout,
227+
display=display,
228+
combine_stderr=combine_stderr,
229+
capture_output=capture_output,
230+
)
231+
232+
def disconnect(self):
233+
if self.transport:
234+
self.transport.close()
235+
236+
def __getattr__(self, target):
237+
def wrapper(*args, **kwargs):
238+
if not self.transport.is_authenticated():
239+
logging.error("SSH session is not ready")
240+
return 1
241+
sftp_channel = SFTPController.from_transport(self.transport)
242+
r = getattr(sftp_channel, target)(*args, **kwargs)
243+
sftp_channel.close()
244+
return r
245+
return wrapper

0 commit comments

Comments
 (0)