|
3 | 3 | from pathlib import Path |
4 | 4 |
|
5 | 5 | import asyncssh |
| 6 | +import structlog |
6 | 7 |
|
7 | 8 | from sshsync.config import Config |
| 9 | +from sshsync.logging import setup_logging |
8 | 10 | from sshsync.schemas import FileTransferAction, Host, SSHResult |
9 | 11 |
|
| 12 | +setup_logging() |
| 13 | + |
10 | 14 |
|
11 | 15 | class SSHClient: |
12 | 16 | def __init__(self) -> None: |
13 | 17 | """Initialize the SSHClient with configuration data from the config file.""" |
14 | 18 | self.config = Config() |
| 19 | + self.logger = structlog.get_logger() |
15 | 20 |
|
16 | 21 | def _is_key_encrypted(self, key_path: str) -> bool: |
17 | 22 | """Check if the given ssh key is protected by a passphrase |
@@ -67,42 +72,54 @@ async def _execute_command(self, host: Host, cmd: str) -> SSHResult: |
67 | 72 | conn_kwargs["client_keys"] = [host.identity_file] |
68 | 73 | async with asyncssh.connect(**conn_kwargs) as conn: |
69 | 74 | result = await conn.run(cmd, check=True, timeout=self.timeout) |
70 | | - return SSHResult( |
71 | | - host=host.address, |
72 | | - exit_status=result.exit_status, |
73 | | - success=result.exit_status == EX_OK, |
74 | | - output=( |
| 75 | + data = { |
| 76 | + "host": host.address, |
| 77 | + "exit_status": result.exit_status, |
| 78 | + "success": result.exit_status == EX_OK, |
| 79 | + "output": ( |
75 | 80 | result.stdout if result.exit_status == EX_OK else result.stderr |
76 | 81 | ), |
77 | | - ) |
| 82 | + } |
| 83 | + self.logger.info("SSH Execution completed", **data) |
| 84 | + return SSHResult(**data) |
78 | 85 | except asyncssh.KeyEncryptionError as e: |
79 | | - return SSHResult( |
80 | | - host=host.address, |
81 | | - exit_status=None, |
82 | | - success=False, |
83 | | - output=f"Encrypted private key, passphrase required: {e}", |
84 | | - ) |
| 86 | + data = { |
| 87 | + "host": host.address, |
| 88 | + "exit_status": None, |
| 89 | + "success": False, |
| 90 | + "output": f"Encrypted private key, passphrase required: {e}", |
| 91 | + } |
| 92 | + self.logger.error("SSH error: Encrypted private key", **data) |
| 93 | + return SSHResult(**data) |
85 | 94 | except asyncssh.PermissionDenied as e: |
86 | | - return SSHResult( |
87 | | - host=host.address, |
88 | | - exit_status=None, |
89 | | - success=False, |
90 | | - output=f"Permission denied: {e.reason}", |
91 | | - ) |
| 95 | + data = { |
| 96 | + "host": host.address, |
| 97 | + "exit_status": None, |
| 98 | + "success": False, |
| 99 | + "output": f"Permission denied: {e.reason}", |
| 100 | + } |
| 101 | + self.logger.error("SSH error: Permission denied", **data) |
| 102 | + return SSHResult(**data) |
| 103 | + |
92 | 104 | except asyncssh.ProcessError as e: |
93 | | - return SSHResult( |
94 | | - host=host.address, |
95 | | - exit_status=e.exit_status, |
96 | | - success=False, |
97 | | - output=f"Command failed: {e.stderr}", |
98 | | - ) |
| 105 | + data = { |
| 106 | + "host": host.address, |
| 107 | + "exit_status": e.exit_status, |
| 108 | + "success": False, |
| 109 | + "output": f"Command failed: {e.stderr}", |
| 110 | + } |
| 111 | + self.logger.error("SSH error: Command failed", **data) |
| 112 | + return SSHResult(**data) |
| 113 | + |
99 | 114 | except Exception as e: |
100 | | - return SSHResult( |
101 | | - host=host.address, |
102 | | - exit_status=None, |
103 | | - success=False, |
104 | | - output=f"Unexpected error: {e}", |
105 | | - ) |
| 115 | + data = { |
| 116 | + "host": host.address, |
| 117 | + "exit_status": None, |
| 118 | + "success": False, |
| 119 | + "output": f"Unexpected error: {e}", |
| 120 | + } |
| 121 | + self.logger.error("SSH error: Unexpected error", **data) |
| 122 | + return SSHResult(**data) |
106 | 123 |
|
107 | 124 | async def _transfer_file_across_hosts( |
108 | 125 | self, |
@@ -158,40 +175,53 @@ async def _push(self, local_path: str, remote_path: str, host: Host) -> SSHResul |
158 | 175 | await asyncssh.scp( |
159 | 176 | local_path, (conn, remote_path), recurse=self.recurse |
160 | 177 | ) |
161 | | - return SSHResult( |
162 | | - host=host.address, |
163 | | - exit_status=EX_OK, |
164 | | - success=True, |
165 | | - output=f"Successfully sent to {host.address}:{remote_path}", |
166 | | - ) |
| 178 | + data = { |
| 179 | + "host": host.address, |
| 180 | + "exit_status": EX_OK, |
| 181 | + "success": True, |
| 182 | + "output": f"Successfully sent to {host.address}:{remote_path}", |
| 183 | + } |
| 184 | + self.logger.info("Upload successful", **data) |
| 185 | + return SSHResult(**data) |
167 | 186 | except asyncssh.PermissionDenied as e: |
168 | | - return SSHResult( |
169 | | - host=host.address, |
170 | | - exit_status=None, |
171 | | - success=False, |
172 | | - output=f"Permission denied: {e.reason}", |
173 | | - ) |
| 187 | + data = { |
| 188 | + "host": host.address, |
| 189 | + "exit_status": None, |
| 190 | + "success": False, |
| 191 | + "output": f"Permission denied: {e.reason}", |
| 192 | + } |
| 193 | + self.logger.error("SSH error: Permission denied", **data) |
| 194 | + return SSHResult(**data) |
| 195 | + |
174 | 196 | except asyncssh.SFTPError as e: |
175 | | - return SSHResult( |
176 | | - host=host.address, |
177 | | - exit_status=None, |
178 | | - success=False, |
179 | | - output=f"SFTP error: {e.reason}", |
180 | | - ) |
| 197 | + data = { |
| 198 | + "host": host.address, |
| 199 | + "exit_status": None, |
| 200 | + "success": False, |
| 201 | + "output": f"SFTP error: {e.reason}", |
| 202 | + } |
| 203 | + self.logger.error("SSH error: SFTP error", **data) |
| 204 | + return SSHResult(**data) |
| 205 | + |
181 | 206 | except asyncssh.ChannelOpenError as e: |
182 | | - return SSHResult( |
183 | | - host=host.address, |
184 | | - exit_status=None, |
185 | | - success=False, |
186 | | - output=f"Channel open error: {e.reason}", |
187 | | - ) |
| 207 | + data = { |
| 208 | + "host": host.address, |
| 209 | + "exit_status": None, |
| 210 | + "success": False, |
| 211 | + "output": f"Channel open error: {e.reason}", |
| 212 | + } |
| 213 | + self.logger.error("SSH error: Channel open error", **data) |
| 214 | + return SSHResult(**data) |
| 215 | + |
188 | 216 | except Exception as e: |
189 | | - return SSHResult( |
190 | | - host=host.address, |
191 | | - exit_status=None, |
192 | | - success=False, |
193 | | - output=f"Unexpected error: {e}", |
194 | | - ) |
| 217 | + data = { |
| 218 | + "host": host.address, |
| 219 | + "exit_status": None, |
| 220 | + "success": False, |
| 221 | + "output": f"Unexpected error: {e}", |
| 222 | + } |
| 223 | + self.logger.error("SSH error: Unexpected error", **data) |
| 224 | + return SSHResult(**data) |
195 | 225 |
|
196 | 226 | async def _pull(self, local_path: str, remote_path: str, host: Host) -> SSHResult: |
197 | 227 | """Pull a file or directory from a remote host to the local machine over SSH. |
@@ -223,41 +253,53 @@ async def _pull(self, local_path: str, remote_path: str, host: Host) -> SSHResul |
223 | 253 | await asyncssh.scp( |
224 | 254 | (conn, remote_path), unique_path, recurse=self.recurse |
225 | 255 | ) |
226 | | - |
227 | | - return SSHResult( |
228 | | - host=host.address, |
229 | | - exit_status=EX_OK, |
230 | | - success=True, |
231 | | - output=f"Downloaded successfully from {host.address}:{remote_path}", |
232 | | - ) |
| 256 | + data = { |
| 257 | + "host": host.address, |
| 258 | + "exit_status": EX_OK, |
| 259 | + "success": True, |
| 260 | + "output": f"Downloaded successfully from {host.address}:{remote_path}", |
| 261 | + } |
| 262 | + self.logger.info("Download successful", **data) |
| 263 | + return SSHResult(**data) |
233 | 264 | except asyncssh.PermissionDenied as e: |
234 | | - return SSHResult( |
235 | | - host=host.address, |
236 | | - exit_status=None, |
237 | | - success=False, |
238 | | - output=f"Permission denied: {e.reason}", |
239 | | - ) |
| 265 | + data = { |
| 266 | + "host": host.address, |
| 267 | + "exit_status": None, |
| 268 | + "success": False, |
| 269 | + "output": f"Permission denied: {e.reason}", |
| 270 | + } |
| 271 | + self.logger.error("SSH error: Permission denied", **data) |
| 272 | + return SSHResult(**data) |
| 273 | + |
240 | 274 | except asyncssh.SFTPError as e: |
241 | | - return SSHResult( |
242 | | - host=host.address, |
243 | | - exit_status=None, |
244 | | - success=False, |
245 | | - output=f"SFTP error: {e.reason}", |
246 | | - ) |
| 275 | + data = { |
| 276 | + "host": host.address, |
| 277 | + "exit_status": None, |
| 278 | + "success": False, |
| 279 | + "output": f"SFTP error: {e.reason}", |
| 280 | + } |
| 281 | + self.logger.error("SSH error: SFTP error", **data) |
| 282 | + return SSHResult(**data) |
| 283 | + |
247 | 284 | except asyncssh.ChannelOpenError as e: |
248 | | - return SSHResult( |
249 | | - host=host.address, |
250 | | - exit_status=None, |
251 | | - success=False, |
252 | | - output=f"Channel open error: {e.reason}", |
253 | | - ) |
| 285 | + data = { |
| 286 | + "host": host.address, |
| 287 | + "exit_status": None, |
| 288 | + "success": False, |
| 289 | + "output": f"Channel open error: {e.reason}", |
| 290 | + } |
| 291 | + self.logger.error("SSH error: Channel open error", **data) |
| 292 | + return SSHResult(**data) |
| 293 | + |
254 | 294 | except Exception as e: |
255 | | - return SSHResult( |
256 | | - host=host.address, |
257 | | - exit_status=None, |
258 | | - success=False, |
259 | | - output=f"Unexpected error: {e}", |
260 | | - ) |
| 295 | + data = { |
| 296 | + "host": host.address, |
| 297 | + "exit_status": None, |
| 298 | + "success": False, |
| 299 | + "output": f"Unexpected error: {e}", |
| 300 | + } |
| 301 | + self.logger.error("SSH error: Unexpected error", **data) |
| 302 | + return SSHResult(**data) |
261 | 303 |
|
262 | 304 | def begin( |
263 | 305 | self, cmd: str, hosts: list[Host], timeout: int | None = 10 |
|
0 commit comments