py.lib
2022-08-14
Parent:1668cc57225b
py.lib/ssh_tools.py
+ Возможность обработки параметров конфигурации перед добавлением в класс конфигурации . Переформатирование части кода по PEP
| awgur@10 | 1 # -*- coding: utf-8 -*- |
| awgur@10 | 2 import paramiko |
| awgur@10 | 3 from datetime import datetime |
| awgur@10 | 4 |
| awgur@10 | 5 # --- OPTS --- # |
| awgur@10 | 6 TIMEOUT = 120 |
| awgur@10 | 7 |
| awgur@10 | 8 # --- CONST --- # |
| awgur@10 | 9 # Политика ключей узла |
| awgur@10 | 10 HOSTKEY_autoAdd = 0 |
| awgur@10 | 11 HOSTKEY_warn = 1 |
| awgur@10 | 12 HOSTKEY_reject = 2 |
| awgur@10 | 13 |
| awgur@23 | 14 |
| awgur@23 | 15 def prompt_checker(user, host, buf): |
| awgur@23 | 16 signs = ['[', ']', '#', '<', '>', '$', '!', '%', '(', ')', '@', '+', ':', ';', ' ', '\t'] |
| awgur@23 | 17 is_prompt = False |
| awgur@23 | 18 is_sep = True |
| awgur@23 | 19 end_buf = '' |
| awgur@23 | 20 |
| awgur@23 | 21 ch_pos = 0 |
| awgur@23 | 22 for ch_pos in range(1, len(buf) + 1): |
| awgur@23 | 23 if not is_prompt: |
| awgur@23 | 24 if buf[-ch_pos] == '\n': |
| awgur@23 | 25 end_buf = buf[-ch_pos] + end_buf |
| awgur@23 | 26 continue |
| awgur@23 | 27 is_prompt = True |
| awgur@23 | 28 |
| awgur@23 | 29 if is_sep: |
| awgur@23 | 30 if buf[-ch_pos] in signs: |
| awgur@23 | 31 end_buf = buf[-ch_pos] + end_buf |
| awgur@23 | 32 continue |
| awgur@23 | 33 is_sep = False |
| awgur@23 | 34 |
| awgur@23 | 35 if buf[-ch_pos] == '\n': |
| awgur@23 | 36 break |
| awgur@23 | 37 |
| awgur@23 | 38 _startProc = len(buf) - ch_pos |
| awgur@23 | 39 _endProc = len(buf) - len(end_buf) |
| awgur@23 | 40 buf = buf[_startProc:_endProc] |
| awgur@23 | 41 search_cond = [] |
| awgur@10 | 42 |
| awgur@23 | 43 for ch_pos in [user, host]: |
| awgur@23 | 44 buf_l = '' |
| awgur@23 | 45 buf_r = '' |
| awgur@23 | 46 index = buf.find(ch_pos) |
| awgur@23 | 47 if index != -1: |
| awgur@23 | 48 for ch in buf[:index][::-1]: |
| awgur@23 | 49 if ch in signs: |
| awgur@23 | 50 buf_l = ch + buf_l |
| awgur@23 | 51 else: |
| awgur@23 | 52 break |
| awgur@23 | 53 index = index + len(ch_pos) |
| awgur@23 | 54 for ch in buf[index:]: |
| awgur@23 | 55 if ch in signs: |
| awgur@23 | 56 buf_r += ch |
| awgur@23 | 57 else: |
| awgur@23 | 58 break |
| awgur@23 | 59 search_cond.append(buf_l + ch_pos + buf_r) |
| awgur@23 | 60 |
| awgur@23 | 61 def check(check_buf): |
| awgur@23 | 62 if check_buf.endswith(end_buf): |
| awgur@23 | 63 end_search = len(check_buf) - len(end_buf) |
| awgur@23 | 64 start_search = check_buf.rfind('\n', 0, len(check_buf) - len(end_buf)) |
| awgur@23 | 65 |
| awgur@23 | 66 if start_search == -1: |
| awgur@23 | 67 start_search = 0 |
| awgur@23 | 68 |
| awgur@23 | 69 _buf = check_buf[start_search:end_search] |
| awgur@23 | 70 for i in search_cond: |
| awgur@23 | 71 if _buf.find(i) == -1: |
| awgur@23 | 72 return False |
| awgur@23 | 73 |
| awgur@23 | 74 return True |
| awgur@23 | 75 |
| awgur@23 | 76 else: |
| awgur@23 | 77 return False |
| awgur@23 | 78 |
| awgur@23 | 79 return check |
| awgur@23 | 80 |
| awgur@23 | 81 |
| awgur@23 | 82 class SSHError(Exception): |
| awgur@23 | 83 pass |
| awgur@23 | 84 |
| awgur@10 | 85 |
| awgur@10 | 86 class TOCheck(object): |
| awgur@23 | 87 """ |
| awgur@23 | 88 Взято из: https://bitbucket.org/awgur/pylib/src/default/timetools.py |
| awgur@23 | 89 """ |
| awgur@23 | 90 def __init__(self, timeout): |
| awgur@23 | 91 self.start = datetime.now() |
| awgur@23 | 92 self.timeout = timeout |
| awgur@23 | 93 |
| awgur@23 | 94 def __call__(self, timeout=None): |
| awgur@23 | 95 if not timeout: |
| awgur@23 | 96 timeout = self.timeout |
| awgur@23 | 97 |
| awgur@23 | 98 buf = datetime.now() - self.start |
| awgur@23 | 99 |
| awgur@23 | 100 if buf.seconds > timeout: |
| awgur@23 | 101 return True |
| awgur@23 | 102 |
| awgur@23 | 103 else: |
| awgur@23 | 104 return False |
| awgur@23 | 105 |
| awgur@23 | 106 def __bool__(self): |
| awgur@23 | 107 return not self() |
| awgur@10 | 108 |
| awgur@10 | 109 |
| awgur@10 | 110 class SSHSubsh(object): |
| awgur@23 | 111 """Класс для облегчения работы с контекстами, когда приглашение коммандной оболочки |
| awgur@23 | 112 меняется в результате исполнения комманды. |
| awgur@23 | 113 """ |
| awgur@23 | 114 def __init__(self, ssh, prompt): |
| awgur@23 | 115 self.ssh = ssh |
| awgur@23 | 116 self.prompt = SSHPromptCheck(prompt) |
| awgur@23 | 117 |
| awgur@23 | 118 def __call__(self, cmd): |
| awgur@23 | 119 res = self.ssh(cmd, wait=False) |
| awgur@23 | 120 res += self.waitPrompt(self.prompt) |
| awgur@23 | 121 return res |
| awgur@23 | 122 |
| awgur@10 | 123 |
| awgur@10 | 124 class SSHPromptCheck(object): |
| awgur@23 | 125 """ |
| awgur@23 | 126 Проверяет, появилось ли приглашение коммандной строки в выводе сервера |
| awgur@23 | 127 |
| awgur@23 | 128 Принимает: |
| awgur@23 | 129 - prompt: |
| awgur@23 | 130 Строка, которая должна однозначно идентифицировать приглашение |
| awgur@23 | 131 коммандной строки, либо функция которой передаётся строка, и она |
| awgur@23 | 132 должна вернуть True, если строка завершается приглашением коммандной |
| awgur@23 | 133 строки либо False в обратном случае. Кроме того можно передат сам объект, |
| awgur@23 | 134 созданный заранее. |
| awgur@23 | 135 """ |
| awgur@23 | 136 def __init__(self, prompt): |
| awgur@23 | 137 if isinstance(prompt, self.__class__): |
| awgur@23 | 138 self.prompt = prompt.prompt |
| awgur@23 | 139 self.check = prompt.check |
| awgur@23 | 140 |
| awgur@23 | 141 else: |
| awgur@23 | 142 def is_func(): |
| awgur@23 | 143 pass |
| awgur@23 | 144 |
| awgur@23 | 145 if type(prompt) == type(is_func): |
| awgur@23 | 146 self.check = prompt |
| awgur@23 | 147 self.prompt = None |
| awgur@23 | 148 |
| awgur@23 | 149 else: |
| awgur@23 | 150 self.prompt = prompt |
| awgur@23 | 151 |
| awgur@23 | 152 if self.prompt is None: |
| awgur@23 | 153 raise SSHError('Prompt not set') |
| awgur@23 | 154 |
| awgur@23 | 155 def check(self, buf): |
| awgur@23 | 156 if buf.endswith(self.prompt): |
| awgur@23 | 157 return True |
| awgur@23 | 158 |
| awgur@23 | 159 else: |
| awgur@23 | 160 return False |
| awgur@23 | 161 |
| awgur@23 | 162 def __call__(self, buf): |
| awgur@23 | 163 if isinstance(buf, SSHOutput): |
| awgur@23 | 164 return self.check(buf.out) |
| awgur@23 | 165 |
| awgur@23 | 166 elif buf: |
| awgur@23 | 167 return self.check(buf) |
| awgur@23 | 168 |
| awgur@23 | 169 else: |
| awgur@23 | 170 return False |
| awgur@23 | 171 |
| awgur@23 | 172 |
| awgur@10 | 173 class SSHOutput(object): |
| awgur@23 | 174 """Содержит вывод удалённого сервера и помогает управляться с ним. |
| awgur@23 | 175 """ |
| awgur@23 | 176 |
| awgur@23 | 177 def __init__(self, out='', err=''): |
| awgur@23 | 178 self.out = out |
| awgur@23 | 179 self.err = err |
| awgur@23 | 180 |
| awgur@23 | 181 def error(self, buf): |
| awgur@23 | 182 """Добавить буфер к буферу сообщения об ошибке""" |
| awgur@23 | 183 |
| awgur@23 | 184 self.err += buf |
| awgur@23 | 185 |
| awgur@23 | 186 def __add__(self, buf): |
| awgur@23 | 187 if isinstance(buf, self.__class__): |
| awgur@23 | 188 self.out += buf.out |
| awgur@23 | 189 self.err += buf.err |
| awgur@23 | 190 else: |
| awgur@23 | 191 self.out += buf |
| awgur@23 | 192 return self |
| awgur@23 | 193 |
| awgur@23 | 194 def __str__(self): |
| awgur@23 | 195 return self.out |
| awgur@23 | 196 |
| awgur@23 | 197 def get_error(self): |
| awgur@23 | 198 return self.err |
| awgur@23 | 199 |
| awgur@23 | 200 def __bool__(self): |
| awgur@23 | 201 if self.out or self.err: |
| awgur@23 | 202 return True |
| awgur@23 | 203 else: |
| awgur@23 | 204 return False |
| awgur@23 | 205 |
| awgur@23 | 206 def __repr__(self): |
| awgur@23 | 207 buf = '' |
| awgur@23 | 208 buf += '---- OUTPUT -----------' |
| awgur@23 | 209 buf += self.out |
| awgur@23 | 210 buf += '---- ERRORS -----------' |
| awgur@23 | 211 buf += self.err |
| awgur@23 | 212 return buf |
| awgur@23 | 213 |
| awgur@23 | 214 def __getitem__(self, key): |
| awgur@23 | 215 return self.out.splittines()[key] |
| awgur@23 | 216 |
| awgur@23 | 217 def __iter__(self): |
| awgur@23 | 218 for line in self.out.splitlines(): |
| awgur@23 | 219 yield line |
| awgur@23 | 220 |
| awgur@23 | 221 def is_eol(self): |
| awgur@23 | 222 if self.out[-1] == '\n': |
| awgur@23 | 223 return True |
| awgur@23 | 224 else: |
| awgur@23 | 225 return False |
| awgur@23 | 226 |
| awgur@23 | 227 |
| awgur@10 | 228 class SSHCommunicate(object): |
| awgur@23 | 229 """\ |
| awgur@23 | 230 Взаимодействие с командой ssh. В сосотоянии отправлять ей данные и принимать данные от неё. |
| awgur@23 | 231 |
| awgur@23 | 232 Обеспечивает работу с exec_command в SSH |
| awgur@23 | 233 """ |
| awgur@23 | 234 |
| awgur@23 | 235 def __init__(self, stdin, stdout, stderr): |
| awgur@23 | 236 self.inC = stdin |
| awgur@23 | 237 self.out = stdout |
| awgur@23 | 238 self.err = stderr |
| awgur@23 | 239 |
| awgur@23 | 240 def __call__(self, buf=None): |
| awgur@23 | 241 if buf is not None: |
| awgur@23 | 242 self.inC.write(buf) |
| awgur@23 | 243 return SSHOutput(self.out.read(), self.err.read()) |
| awgur@23 | 244 |
| awgur@23 | 245 def __str__(self): |
| awgur@23 | 246 return str(self()) |
| awgur@23 | 247 |
| awgur@23 | 248 def __repr__(self): |
| awgur@23 | 249 return repr(self()) |
| awgur@23 | 250 |
| awgur@10 | 251 |
| awgur@10 | 252 class _SSH(object): |
| awgur@23 | 253 """\ |
| awgur@23 | 254 Метакласс для SSH соединений |
| awgur@23 | 255 |
| awgur@23 | 256 Принимает: |
| awgur@23 | 257 - host: Имя узла |
| awgur@23 | 258 - user: Имя пользователя |
| awgur@23 | 259 - passwd(не обязательно, если есть key): пароль пользователя |
| awgur@23 | 260 - key(не обязательно если есть passwd): файл ключа |
| awgur@23 | 261 - hostKeyPol[HOSTKEY_autoAdd | HOSTKEY_warn | HOSTKEY_reject]: |
| awgur@23 | 262 HOSTKEY_autoAdd(По умолчанию): |
| awgur@23 | 263 если ключ удалённого узла нам неизвестен, добавить, соединение разрешить. |
| awgur@23 | 264 HOSTKEY_warn: если ключ удалённого узла нам неизвестен, предупредить. |
| awgur@23 | 265 HOSTKEY_reject: если ключ удалённого узла нам неизвестен, разорвать соединение. |
| awgur@23 | 266 - timeout: таймаут операций с сервером. |
| awgur@23 | 267 """ |
| awgur@23 | 268 |
| awgur@23 | 269 def __init__(self, host, user, passwd=None, key=None, |
| awgur@23 | 270 host_key_pol=HOSTKEY_autoAdd, timeout=None |
| awgur@23 | 271 ): |
| awgur@23 | 272 |
| awgur@23 | 273 if passwd is None and key is None: |
| awgur@23 | 274 raise SSHError('Auth type not set') |
| awgur@23 | 275 |
| awgur@23 | 276 if host_key_pol == HOSTKEY_autoAdd: |
| awgur@23 | 277 _hostKeyPol = paramiko.AutoAddPolicy() |
| awgur@23 | 278 elif host_key_pol == HOSTKEY_warn: |
| awgur@23 | 279 _hostKeyPol = paramiko.WarningPolicy() |
| awgur@23 | 280 elif host_key_pol == HOSTKEY_reject: |
| awgur@23 | 281 _hostKeyPol = paramiko.RejectPolicy() |
| awgur@23 | 282 else: |
| awgur@23 | 283 raise SSHError('Unknown policy type') |
| awgur@23 | 284 |
| awgur@23 | 285 self.ssh = paramiko.SSHClient() |
| awgur@23 | 286 self.ssh.set_missing_host_key_policy(_hostKeyPol) |
| awgur@23 | 287 if passwd: |
| awgur@23 | 288 self.ssh.connect(hostname=host, username=user, password=passwd, look_for_keys=False, allow_agent=False) |
| awgur@23 | 289 else: |
| awgur@23 | 290 self.ssh.connect(hostname=host, username=user, key_filename=key, look_for_keys=False, allow_agent=False) |
| awgur@23 | 291 |
| awgur@23 | 292 if timeout is not None: |
| awgur@23 | 293 self.timeout = timeout |
| awgur@23 | 294 |
| awgur@23 | 295 else: |
| awgur@23 | 296 self.timeout = TIMEOUT |
| awgur@23 | 297 |
| awgur@23 | 298 self.user = user |
| awgur@23 | 299 self.host = host |
| awgur@23 | 300 |
| awgur@23 | 301 def __del__(self): |
| awgur@23 | 302 try: |
| awgur@23 | 303 self.ssh.close() |
| awgur@23 | 304 |
| awgur@23 | 305 except: |
| awgur@23 | 306 pass |
| awgur@23 | 307 |
| awgur@23 | 308 del self.ssh |
| awgur@23 | 309 |
| awgur@23 | 310 |
| awgur@10 | 311 class InterSSH(_SSH): |
| awgur@23 | 312 """Класс Взаимодействия с сервером SSH |
| awgur@23 | 313 |
| awgur@23 | 314 Кроме аргументов необходимых для метакласса принимает: |
| awgur@23 | 315 - prompt: |
| awgur@23 | 316 Параметр конструктора SSHPromptCheck. Принимается строго по имени. |
| awgur@23 | 317 """ |
| awgur@23 | 318 def __init__(self, *a, **kwa): |
| awgur@23 | 319 prompt = None |
| awgur@23 | 320 if 'prompt' in kwa: |
| awgur@23 | 321 prompt = kwa['prompt'] |
| awgur@23 | 322 del kwa['prompt'] |
| awgur@23 | 323 |
| awgur@23 | 324 _SSH.__init__(self, *a, **kwa) |
| awgur@23 | 325 |
| awgur@23 | 326 self.shell = self.ssh.invoke_shell() |
| awgur@23 | 327 self.shell.settimeout(self.timeout) |
| awgur@23 | 328 if prompt is None: |
| awgur@23 | 329 prompt = self._get_prompt() |
| awgur@23 | 330 self.ready = True |
| awgur@23 | 331 |
| awgur@23 | 332 else: |
| awgur@23 | 333 self.ready = False |
| awgur@23 | 334 |
| awgur@23 | 335 self.prompt = SSHPromptCheck(prompt) |
| awgur@10 | 336 |
| awgur@23 | 337 def __bool__(self): |
| awgur@23 | 338 """Готово ли соединение принимать новые команды""" |
| awgur@23 | 339 return self.ready |
| awgur@23 | 340 |
| awgur@23 | 341 def _get_prompt(self): |
| awgur@23 | 342 """Возвращает объект проверки на приглашение комммандной строки из текщего |
| awgur@23 | 343 буфера вывода. |
| awgur@23 | 344 """ |
| awgur@23 | 345 buf = '' |
| awgur@23 | 346 while self.shell.recv_ready(): |
| awgur@23 | 347 buf += self.shell.recv(1024) |
| awgur@23 | 348 |
| awgur@23 | 349 func = prompt_checker(self.user, self.host, buf) |
| awgur@23 | 350 return SSHPromptCheck(func) |
| awgur@23 | 351 |
| awgur@23 | 352 def wait_prompt(self, prompt=None): |
| awgur@23 | 353 """\ |
| awgur@23 | 354 Ожидание приглашения командной строки |
| awgur@10 | 355 |
| awgur@23 | 356 Принимает необязательный аргумент: Экземпляр класса SSHPromptCheck, с помощью котороого |
| awgur@23 | 357 проверяется присутствие приглашения командного интерпретатора в выводе. |
| awgur@23 | 358 """ |
| awgur@23 | 359 |
| awgur@23 | 360 if prompt is None: |
| awgur@23 | 361 prompt = self.prompt |
| awgur@23 | 362 |
| awgur@23 | 363 else: |
| awgur@23 | 364 prompt = SSHPromptCheck(prompt) |
| awgur@10 | 365 |
| awgur@23 | 366 res = SSHOutput() |
| awgur@23 | 367 if not self.ready: |
| awgur@23 | 368 time_check = TOCheck(self.timeout) |
| awgur@23 | 369 while True: |
| awgur@23 | 370 while self.shell.recv_stderr_ready(): |
| awgur@23 | 371 res.error(self.shell.recv_stderr(1024)) |
| awgur@23 | 372 |
| awgur@23 | 373 while self.shell.recv_ready(): |
| awgur@23 | 374 res += self.shell.recv(1024) |
| awgur@23 | 375 |
| awgur@23 | 376 if prompt(res): |
| awgur@23 | 377 break |
| awgur@10 | 378 |
| awgur@23 | 379 if time_check(): |
| awgur@23 | 380 raise SSHError('Command call timeout\n' + repr(res)) |
| awgur@23 | 381 |
| awgur@23 | 382 self.ready = True |
| awgur@23 | 383 return res |
| awgur@23 | 384 |
| awgur@23 | 385 def __call__(self, cmd, wait=True): |
| awgur@23 | 386 """Запуск команды |
| awgur@23 | 387 |
| awgur@23 | 388 Принимает: |
| awgur@23 | 389 - cmd: Комманда |
| awgur@23 | 390 - wait(необязательный аргумент): Признак ожидания завершения комманды. |
| awgur@10 | 391 |
| awgur@23 | 392 Возвращает: Экземпляр класса SSHOutput |
| awgur@23 | 393 """ |
| awgur@23 | 394 res = SSHOutput() |
| awgur@23 | 395 if not self.ready: |
| awgur@23 | 396 res += self.wait_prompt() |
| awgur@23 | 397 bytes = self.shell.send('%s\n' % cmd) |
| awgur@23 | 398 if bytes == 0: |
| awgur@23 | 399 raise SSHError('Channel closed') |
| awgur@23 | 400 if wait: |
| awgur@23 | 401 res += self.wait_prompt() |
| awgur@23 | 402 else: |
| awgur@23 | 403 self.ready = False |
| awgur@23 | 404 return res |
| awgur@10 | 405 |
| awgur@23 | 406 def sub_shell(self, cmd, prompt=None): |
| awgur@23 | 407 """Возвращает объект SSHSubsh, через который удобно работать |
| awgur@23 | 408 интерпретаторами, запускаемыми из текущего, либо с ситуацией изменения приглашения |
| awgur@23 | 409 командного интерпретатора. |
| awgur@23 | 410 """ |
| awgur@23 | 411 self(cmd, wait=False) |
| awgur@23 | 412 if prompt is None: |
| awgur@23 | 413 prompt = self._getPropt() |
| awgur@23 | 414 return SSHSubsh(self, prompt) |
| awgur@23 | 415 |
| awgur@23 | 416 def close(self): |
| awgur@23 | 417 res = SSHOutput |
| awgur@23 | 418 if not self: |
| awgur@23 | 419 res += self.wait_prompt() |
| awgur@23 | 420 self.ssh.close() |
| awgur@23 | 421 return res |
| awgur@23 | 422 |
| awgur@10 | 423 |
| awgur@10 | 424 class SSH(_SSH): |
| awgur@23 | 425 """Класс призванный управлять выполнением единичных комманд, без привлечения контекста. |
| awgur@23 | 426 |
| awgur@23 | 427 Принимает аргументы необходимые для метакласса _SSH |
| awgur@23 | 428 """ |
| awgur@23 | 429 def __init__(self, *a, **kwa): |
| awgur@23 | 430 _SSH.__init__(self, *a, **kwa) |
| awgur@23 | 431 |
| awgur@23 | 432 def __call__(self, cmd): |
| awgur@23 | 433 return SSHCommunicate(*self.ssh.exec_command(cmd, timeout=self.timeout)) |