From ce03b84898eb7d228253e73d948067479ec9ce52 Mon Sep 17 00:00:00 2001 From: Robert Schmidt Date: Thu, 15 Dec 2022 07:54:09 +0100 Subject: [PATCH] Add new SSH/local cmd classes --- ci-scripts/cls_cmd.py | 217 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 ci-scripts/cls_cmd.py diff --git a/ci-scripts/cls_cmd.py b/ci-scripts/cls_cmd.py new file mode 100644 index 0000000000..12eb75e7da --- /dev/null +++ b/ci-scripts/cls_cmd.py @@ -0,0 +1,217 @@ +#/* +# * Licensed to the OpenAirInterface (OAI) Software Alliance under one or more +# * contributor license agreements. See the NOTICE file distributed with +# * this work for additional information regarding copyright ownership. +# * The OpenAirInterface Software Alliance licenses this file to You under +# * the OAI Public License, Version 1.1 (the "License"); you may not use this file +# * except in compliance with the License. +# * You may obtain a copy of the License at +# * +# * http://www.openairinterface.org/?page_id=698 +# * +# * Unless required by applicable law or agreed to in writing, software +# * distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. +# *------------------------------------------------------------------------------- +# * For more information about the OpenAirInterface (OAI) Software Alliance: +# * contact@openairinterface.org +# */ +#--------------------------------------------------------------------- +# +# Required Python Version +# Python 3.x +# +#--------------------------------------------------------------------- + +import abc +import logging +import subprocess as sp +import os +import paramiko +import uuid + +# provides a partial interface for the legacy SSHconnection class (getBefore(), command()) +class Cmd(metaclass=abc.ABCMeta): + def cd(self, d, silent=False): + if d == None or d == '' or d == []: + self.cwd = None + elif d[0] == '/': + self.cwd = d + else: + if not self.cwd: + # no cwd set: get current working directory + self.cwd = self.run('pwd').stdout.strip() + self.cwd += f"/{d}" + if not silent: + logging.debug(f'cd {self.cwd}') + + @abc.abstractmethod + def run(self, line, timeout=300, silent=False): + return + + @abc.abstractmethod + def command(self, commandline, expectedline, timeout, silent=False, resync=False): + return + + @abc.abstractmethod + def close(self): + return + + @abc.abstractmethod + def getBefore(self): + return + + @abc.abstractmethod + def copyin(self, scpIp, scpUser, scpPw, src, tgt): + return + + @abc.abstractmethod + def copyout(self, scpIp, scpUser, scpPw, src, tgt): + return + +class LocalCmd(Cmd): + def __init__(self, d = None): + self.cwd = d + self.cp = sp.CompletedProcess(args='', returncode=0, stdout='') + + def run(self, line, timeout=300, silent=False, reportNonZero=True): + if type(line) is str: + line = [s for s in line.split(' ') if len(s) > 0] + if not silent: + logging.debug(' '.join(line)) + try: + ret = sp.run(line, cwd=self.cwd, stdout=sp.PIPE, stderr=sp.STDOUT, timeout=timeout) + except Exception as e: + ret = sp.CompletedProcess(args=line, returncode=255, stdout=f'Exception: {str(e)}'.encode('utf-8')) + if ret.stdout is None: + ret.stdout = b'' + ret.stdout = ret.stdout.decode('utf-8').strip() + if reportNonZero and ret.returncode != 0: + cmd = ' '.join(ret.args) + logging.warning(f'command "{cmd}" returned non-zero returncode {ret.returncode}: output:\n{ret.stdout}') + self.cp = ret + return ret + + def command(self, commandline, expectedline=None, timeout=300, silent=False, resync=False): + line = [s for s in commandline.split(' ') if len(s) > 0] + if line[0] == 'cd': + self.cd(line[1], silent) + else: + self.run(line, timeout, silent) + return 0 + + def close(self): + pass + + def getBefore(self): + return self.cp.stdout + + def copyin(self, scpIp, scpUser, scpPw, src, tgt): + logging.warning("LocalCmd emulating sshconnection.copyin() function") + self.run(f'cp -r {src} {tgt}') + + def copyout(self, scpIp, scpUser, scpPw, src, tgt): + logging.warning("LocalCmd emulating sshconnection.copyout() function") + self.run(f'cp -r {src} {tgt}') + +class RemoteCmd(Cmd): + def __init__(self, hostname, d=None): + logging.getLogger('paramiko').setLevel(logging.INFO) # prevent spamming through Paramiko + self.client = paramiko.SSHClient() + self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + cfg = RemoteCmd._lookup_ssh_config(hostname) + self.client.connect(**cfg) + self.cwd = d + self.cp = sp.CompletedProcess(args='', returncode=0, stdout='') + + def _lookup_ssh_config(hostname): + ssh_config = paramiko.SSHConfig() + user_config_file = os.path.expanduser("~/.ssh/config") + if os.path.exists(user_config_file): + with open(user_config_file) as f: + ssh_config.parse(f) + else: + raise FileNotFoundError('class needs SSH config at ~/.ssh/config') + ucfg = ssh_config.lookup(hostname) + if 'identityfile' not in ucfg or 'user' not in ucfg: + raise KeyError(f'no identityfile or user in SSH config for host {hostname}') + cfg = {'hostname':hostname, 'username':ucfg['user'], 'key_filename':ucfg['identityfile']} + if 'hostname' in ucfg: + cfg['hostname'] = ucfg['hostname'] # override user-given hostname with what is in config + if 'port' in ucfg: + cfg['port'] = int(ucfg['port']) + if 'proxycommand' in ucfg: + cfg['sock'] = paramiko.ProxyCommand(ucfg['proxycommand']) + return cfg + + def run(self, line, timeout=300, silent=False, reportNonZero=True): + if type(line) is list: + line = ' '.join(line) + if not silent: + logging.debug(line) + if self.cwd: + line = f"cd {self.cwd} && {line}" + args = line.split(' ') + try: + stdin, stdout, stderr = self.client.exec_command(line, timeout=timeout) + ret = sp.CompletedProcess(args=args, returncode=stdout.channel.recv_exit_status(), stdout=stdout.read(size=None) + stderr.read(size=None)) + except Exception as e: + ret = sp.CompletedProcess(args=args, returncode=255, stdout=f'Exception: {str(e)}'.encode('utf-8')) + ret.stdout = ret.stdout.decode('utf-8').strip() + if reportNonZero and ret.returncode != 0: + cmd = ' '.join(ret.args) + logging.warning(f'command "{cmd}" returned non-zero returncode {ret.returncode}: output:\n{ret.stdout}') + self.cp = ret + return ret + + def command(self, commandline, expectedline=None, timeout=300, silent=False, resync=False): + line = [s for s in commandline.split(' ') if len(s) > 0] + if line[0] == 'cd': + self.cd(line[1], silent) + else: + self.run(line, timeout, silent) + return 0 + + def close(self): + self.client.close() + + def getBefore(self): + return self.cp.stdout + + def copyout(self, src, tgt, recursive=False): + logging.warning("RemoteCmd emulating sshconnection.copyout() function, ignoring scpIp") + logging.debug(f"copyout: local:{src} -> remote:{tgt}") + if recursive: + tmpfile = f"{uuid.uuid4()}.tar" + abstmpfile = f"/tmp/{tmpfile}" + cmd = LocalCmd() + cmd.run(f"tar -cf {abstmpfile} {src}") + sftp = self.client.open_sftp() + sftp.put(abstmpfile, abstmpfile) + sftp.close() + cmd.run(f"rm {abstmpfile}") + self.run(f"mv {abstmpfile} {tgt}; cd {tgt} && tar -xf {tmpfile} && rm {tmpfile}") + else: + sftp = self.client.open_sftp() + sftp.put(src, tgt) + sftp.close() + + def copyin(self, src, tgt, recursive=False): + logging.warning("RemoteCmd emulating sshconnection.copyout() function") + logging.debug(f"copyin: remote:{src} -> local:{tgt}") + if recursive: + tmpfile = f"{uuid.uuid4()}.tar" + abstmpfile = f"/tmp/{tmpfile}" + self.run(f"tar -cf {abstmpfile} {src}") + sftp = self.client.open_sftp() + sftp.get(abstmpfile, abstmpfile) + sftp.close() + self.run(f"rm {abstmpfile}") + cmd = LocalCmd() + cmd.run(f"mv {abstmpfile} {tgt}; cd {tgt} && tar -xf {tmpfile} && rm {tmpfile}") + else: + sftp = self.client.open_sftp() + sftp.get(src, tgt) + sftp.close()