def test_bmc_config(args): """Check if BMC configuration are valid. Testing BMC config with ipmitool is possible only when ipmi over lan is configured(out-band setup). It is taken care by test_bmc_is_accessible. So, validation on bmc onfiguration with bmc ip, user and secret value through ssh is fine at this time. """ bmc_ip = Conf.get(GLOBAL_CONF, BMC_IP_KEY) bmc_user = Conf.get(GLOBAL_CONF, BMC_USER_KEY) bmc_secret = Conf.get(GLOBAL_CONF, BMC_SECRET_KEY) bmc_key = Cipher.generate_key(MACHINE_ID, "server_node") bmc_passwd = Cipher.decrypt(bmc_key, bmc_secret.encode("utf-8")).decode("utf-8") # check BMC ip, user, password are valid session = SSHChannel(bmc_ip, bmc_user, bmc_passwd) session.disconnect()
def validate_controller_accessibility(self, ip, username, password): """Check contoller console is accessible to node.""" # Check if ssh connection is successful try: session = SSHChannel(host=ip, username=username, password=password) session.disconnect() except: err = traceback.format_exc() raise VError( errno.EINVAL, "Failed to create ssh connection to %s, Error: %s" % (ip, err)) # ping controller IP cmd = "ping -c 1 -W 1 %s" % ip cmd_proc = SimpleProcess(cmd) _, stderr, rc = cmd_proc.run() if rc != 0: msg = "Ping failed for IP '%s'. Command: '%s', Return Code: '%s'." % ( ip, cmd, rc) msg += stderr.decode("utf-8").replace('\r', '').replace('\n', '') raise VError(errno.EINVAL, msg)
def validate_firmware(self, ip, username, password, mc_expected): """ Check expected contoller bundle version found. mc_expected: string or list of expected version(s). """ try: session = SSHChannel(host=ip, username=username, password=password) except: err = traceback.format_exc() raise VError( errno.EINVAL, "Failed to create ssh connection to %s, Error: %s'" % (ip, err)) # check firmware version command execution on MC rc, output = session.execute(self.version_cmd) if (rc != 0) or (self.success_msg not in output): raise VError( errno.EINVAL, "Controller command failure. Command: %s Output: %s" % (self.version_cmd, output)) # check expected bundle version is found on MC _supported = False if mc_expected: if isinstance(mc_expected, list): _supported = any(ver for ver in mc_expected if ver in output) else: _supported = True if mc_expected in output else False if not _supported: raise VError( errno.EINVAL, "Unsupported firmware version found on %s.\ Expected controller bundle version(s): %s" % (ip, mc_expected)) session.disconnect()
class PkgV: """Pkg related validations.""" def __init__(self): self.passwdless_ssh_enabled = False self.ssh = None def __execute_cmd(self, cmd): """ Execute command using SSHChannel or SimpleProcees and returns result. Uses SimpleProcess to execute the command on passwordless ssh configured host. Otherwise, uses SSHChannel to execute command. """ if self.ssh: retcode, result = self.ssh.execute(cmd) else: handler = SimpleProcess(cmd) stdout, stderr, retcode = handler.run() result = stdout.decode("utf-8") if retcode == 0 else stderr.decode( "utf-8") if retcode != 0: raise VError(errno.EINVAL, "Command failure. cmd: %s stderr: %s" % (cmd, result)) return result def validate(self, v_type: str, args: list, host: str = None): """ Process rpm validations. Usage (arguments to be provided): 1. pkg validate_rpms host (optional) [packagenames] 2. pkg validate_pip3s host (optional) [pip3 packagenames] host should be host url in case passwordless ssh is not configured. host url format - user:passwd@fqdn:port """ host = socket.getfqdn( ) if host == None or host == "localhost" else host host_details = re.search(r"(\w+):(.+)@(.+):(\d+)", host) if host_details: user = host_details.groups()[0] passwd = host_details.groups()[1] host = host_details.groups()[2] port = host_details.groups()[3] self.ssh = SSHChannel(host=host, username=user, password=passwd, port=port) elif host != socket.getfqdn(): # Ensure we can perform passwordless ssh and there are no prompts NetworkV().validate("passwordless", [pwd.getpwuid(os.getuid()).pw_name, host]) self.passwdless_ssh_enabled = True if v_type == "rpms": return self.validate_rpm_pkgs(host, args) elif v_type == "pip3s": return self.validate_pip3_pkgs(host, args) else: raise VError(errno.EINVAL, "Action parameter %s not supported" % v_type) if self.ssh: self.ssh.disconnect() def validate_rpm_pkgs(self, host, pkgs, skip_version_check=True): """Check if rpm pkg is installed.""" cmd = "ssh %s rpm -qa" % host if self.passwdless_ssh_enabled else "rpm -qa" result = self.__execute_cmd(cmd) if not isinstance(pkgs, dict): skip_version_check = True for pkg in pkgs: if result.find("%s" % pkg) == -1: raise VError( errno.EINVAL, "rpm pkg %s not installed on host %s." % (pkg, host)) if not skip_version_check: matched_str = re.search(r"%s-([^-][0-9.]+)-" % pkg, result) installed_version = matched_str.groups()[0] expected_version = pkgs[pkg] if installed_version != expected_version: raise VError( errno.EINVAL, "Mismatched version for rpm package %s on host %s. Installed %s. Expected %s." % (pkg, host, installed_version, expected_version)) def validate_pip3_pkgs(self, host, pkgs, skip_version_check=True): """Check if pip3 pkg is installed.""" cmd = "ssh %s pip3 list" % host if self.passwdless_ssh_enabled else "pip3 list" result = self.__execute_cmd(cmd) if not isinstance(pkgs, dict): skip_version_check = True for pkg in pkgs: if result.find("%s" % pkg) == -1: raise VError( errno.EINVAL, "pip3 pkg %s not installed on host %s." % (pkg, host)) if not skip_version_check: matched_str = re.search(r"%s \((.*)\)" % pkg, result) installed_version = matched_str.groups()[0] expected_version = pkgs[pkg] if installed_version != expected_version: raise VError( errno.EINVAL, "Mismatched version for pip3 package %s on host %s. Installed %s. Expected %s." % (pkg, host, installed_version, expected_version))
class BmcV: """BMC related validations.""" def __init__(self): self.channel_cmd = "channel info" self.session = None def __get_bmc_ip(self, node): """ Get BMC IP along with status of command """ cmd = "ipmitool lan print 1 | grep 'IP Address'" rc, output = self.session.execute(cmd) if rc != 0: msg = f"Failed to get BMC IP of {node}. Command: '{cmd}',\ Return Code: '{rc}'." msg += output raise VError(errno.EINVAL, msg) bmc_ip = output.split()[-1] return bmc_ip def __get_bmc_power_status(self, node): """ Get BMC power status """ cmd = "ipmitool chassis status | grep 'System Power'" rc, output = self.session.execute(cmd) if rc != 0: msg = f"Failed to get BMC power status of {node}. Command: '{cmd}',\ Return Code: '{rc}'." msg += output raise VError(errno.EINVAL, msg) pw_status = output.split()[-1] return pw_status def __ping_bmc(self, node): """ Ping BMC IP """ ip = self.__get_bmc_ip(node) cmd = f"ping -c 1 -W 1 {ip}" rc, output = self.session.execute(cmd) if rc != 0: msg = f"Ping failed for IP '{ip}'. Command: '{cmd}',\ Return Code: '{rc}'." msg += output raise VError(errno.ECONNREFUSED, msg) def validate(self, v_type, args): """ Process BMC validations. Usage (arguments to be provided): 1. bmc accessible <node> <bmc_ip> <bmc_user> <bmc_passwd> 2. bmc stonith <node> <bmc_ip> <bmc_user> <bmc_passwd> """ if not isinstance(args, list): raise VError(errno.EINVAL, f"Invalid parameters '{args}'") if len(args) < 1: raise VError(errno.EINVAL, "No parameters specified") if len(args) < 4: raise VError( errno.EINVAL, f"Insufficient parameters '{args}' for 'bmc validate'. Refer usage." ) elif len(args) > 4: raise VError( errno.EINVAL, f"Too many parameters '{args}' for 'bmc validate'. Refer usage." ) # Root user to execute ipmitool command user = "******" node = args[0] self.session = SSHChannel(node, user, pkey_filename='/root/.ssh/id_rsa_prvsnr') if v_type == "accessible": self.validate_bmc_accessibility(node, args[1], args[2], args[3]) elif v_type == "stonith": self.validate_bmc_stonith_config(node, args[1], args[2], args[3]) else: raise VError(errno.EINVAL, f"Action parameter '{v_type}' not supported") self.session.disconnect() def validate_bmc_accessibility(self, node, bmc_ip, bmc_user, bmc_passwd): """ Validate BMC accessibility """ # Validate bmc accessibility on inband setup self.validate_inband_bmc_channel(node) # BMC IP based validations # Validate bmc accessibility on outband setup self.validate_bmc_channel_over_lan(bmc_ip, bmc_user, bmc_passwd) # Check if we can ping BMC self.__ping_bmc(node) def validate_bmc_stonith_config(self, node, bmc_ip, bmc_user, bmc_passwd): """ Validations for BMC STONITH Configuration """ cmd = f"fence_ipmilan -P -a {bmc_ip} -o status -l {bmc_user} -p {bmc_passwd}" rc, output = self.session.execute(cmd) if rc != 0: msg = f"Failed to check BMC STONITH Config. Command: '{cmd}',\ Return Code: '{rc}'." msg += output raise VError(errno.EINVAL, msg) def validate_inband_bmc_channel(self, node): """ Get BMC channel information (inband) """ cmd = f"ipmitool {self.channel_cmd}" rc, output = self.session.execute(cmd) if rc != 0: msg = f"Failed to get BMC channel info of '{node}''. Command: '{cmd}',\ Return Code: '{rc}'." msg += output raise VError(errno.EINVAL, msg) return True def validate_bmc_channel_over_lan(self, bmc_ip, bmc_user, bmc_passwd): """ Get BMC channel information over lan (out-of-band) """ # check BMC ip is accessible over lan (out of band) cmd = f"ipmitool -H {bmc_ip} -U {bmc_user} -P {bmc_passwd} -I lan {self.channel_cmd}" rc, output = self.session.execute(cmd) if rc != 0: msg = f"Failed to get BMC channel info of '{bmc_ip}' over lan. Command: '{cmd}',\ Return Code: '{rc}'." msg += output raise VError(errno.EINVAL, msg) return True
class TestSSHChannel(unittest.TestCase): """ Check ssh connection related validations """ def setUp(self): """ Create temporary user for doing ssh on localhost """ self.hostname = "localhost" self.__user = "******" self.__passwd = "P@ssw0rd" e_pass = crypt.crypt(self.__passwd, "22") os.system(f"sudo useradd -M -p {e_pass} {self.__user}") # Let ssh connection be alive across all tests self.session = SSHChannel(self.hostname, self.__user, self.__passwd, sftp_enabled=True) def test_connection_ok(self): """ Check if ssh connection is alive """ if not self.session.is_connection_alive(): raise VError(errno.EINVAL, f"Connection is lost with client '{self.session.host}'") def test_command_execution_ok(self): """ Check if command is executed successfully """ cmd = "whoami" rc, output = self.session.execute(cmd) if rc !=0 or self.__user not in output: raise VError( errno.EINVAL, f"Command execution failed on {self.hostname}.\ Command: '{cmd}'\ Return Code: {rc}") def test_reconnect_ok(self): """ Check ssh re-connect after disconnection caller """ # Close ssh connection self.session.disconnect() if self.session.is_connection_alive(): raise VError(errno.EINVAL, f"SSH connection is not closed with client '{self.session.host}'") # Establish ssh to same client without invoking instance # and validate ssh connection is active self.session.connect() if not self.session.is_connection_alive(): raise VError(errno.EINVAL, f"Connection is lost with client '{self.session.host}'") def test_send_file(self): # Create local file local_file = '/tmp/test_file.txt' fObj = open(local_file, 'w') fObj.close() # Send file to remote location remote_dir = '/tmp/tmp_remote/' os.makedirs(remote_dir, exist_ok=True) os.chmod(remote_dir, 0o657) remote_file = os.path.join(remote_dir, 'test_file.txt') self.session.send_file(local_file, remote_file) os.remove(local_file) os.system(f"rm -rf {remote_dir}") def test_recv_file(self): # Create remote file remote_dir = '/tmp/tmp_remote/' os.makedirs(remote_dir, exist_ok=True) remote_file = os.path.join(remote_dir, 'test_file.txt') fObj = open(remote_file, 'w') fObj.close() # Receive file from remote location local_file = '/tmp/test_file.txt' self.session.recv_file(remote_file, local_file) os.remove(remote_file) def tearDown(self): """ Cleanup the setup """ self.session.disconnect() os.system(f"sudo userdel {self.__user}")