Utils/distroutils.py (418 lines of code) (raw):

import os import pwd import random import crypt import string import platform import re import Utils.logger as logger import Utils.extensionutils as ext_utils import Utils.constants as constants def get_my_distro(config, os_name=None): if 'FreeBSD' in platform.system(): return FreeBSDDistro(config) if os_name is None: if os.path.isfile(constants.os_release): os_name = ext_utils.get_line_starting_with("NAME", constants.os_release) elif os.path.isfile(constants.system_release): os_name = ext_utils.get_file_contents(constants.system_release) if os_name is not None: if re.search("fedora", os_name, re.IGNORECASE): # Fedora return FedoraDistro(config) if re.search("red\s?hat", os_name, re.IGNORECASE): # Red Hat return RedhatDistro(config) if re.search("centos", os_name, re.IGNORECASE): # CentOS return CentOSDistro(config) if re.search("coreos", os_name, re.IGNORECASE): # CoreOs return CoreOSDistro(config) if re.search("freebsd", os_name, re.IGNORECASE): # FreeBSD return FreeBSDDistro(config) if re.search("sles", os_name, re.IGNORECASE): # SuSE return SuSEDistro(config) if re.search("ubuntu", os_name, re.IGNORECASE): return UbuntuDistro(config) if re.search("mariner", os_name, re.IGNORECASE): return MarinerDistro(config) return GenericDistro(config) # noinspection PyMethodMayBeStatic class GenericDistro(object): """ GenericiDstro defines a skeleton necessary for a concrete Distro class. Generic methods and attributes are kept here, distribution specific attributes and behavior are to be placed in the concrete child named distroDistro, where distro is the string returned by calling python platform.linux_distribution()[0]. So for CentOS the derived class is called 'centosDistro'. """ def __init__(self, config): """ Generic Attributes go here. These are based on 'majority rules'. This __init__() may be called or overriden by the child. """ self.selinux = None self.service_cmd = '/usr/sbin/service' self.ssh_service_restart_option = 'restart' self.ssh_service_name = 'ssh' self.distro_name = 'default' self.config = config def is_se_linux_system(self): """ Checks and sets self.selinux = True if SELinux is available on system. """ if self.selinux is None: if ext_utils.run(['which', 'getenforce'], chk_err=False): self.selinux = False else: self.selinux = True return self.selinux def get_home(self): """ Attempt to guess the $HOME location. Return the path string. """ home = None try: home = ext_utils.get_line_starting_with("HOME", "/etc/default/useradd").split('=')[1].strip() except (ValueError, KeyError, AttributeError, EnvironmentError): pass if (home is None) or (not home.startswith("/")): home = "/home" return home def set_se_linux_context(self, path, cn): """ Calls shell 'chcon' with 'path' and 'cn' context. Returns exit result. """ if self.is_se_linux_system(): return ext_utils.run(['chcon', cn, path]) def restart_ssh_service(self): """ Service call to re(start) the SSH service """ ssh_restart_cmd = [self.service_cmd, self.ssh_service_name, self.ssh_service_restart_option] ret_code = ext_utils.run(ssh_restart_cmd) if ret_code != 0: logger.error("Failed to restart SSH service with return code:" + str(ret_code)) return ret_code def ssh_deploy_public_key(self, fprint, path): """ Generic sshDeployPublicKey - over-ridden in some concrete Distro classes due to minor differences in openssl packages deployed """ keygen_retcode = ext_utils.run_command_and_write_stdout_to_file( ['ssh-keygen', '-i', '-m', 'PKCS8', '-f', fprint], path) if keygen_retcode: return 1 else: return 0 def change_password(self, user, password): logger.log("Change user password") crypt_id = self.config.get("Provisioning.PasswordCryptId") if crypt_id is None: crypt_id = "6" salt_len = self.config.get("Provisioning.PasswordCryptSaltLength") try: salt_len = int(salt_len) if salt_len < 0 or salt_len > 10: salt_len = 10 except (ValueError, TypeError): salt_len = 10 return self.chpasswd(user, password, crypt_id=crypt_id, salt_len=salt_len) def chpasswd(self, username, password, crypt_id=6, salt_len=10): passwd_hash = self.gen_password_hash(password, crypt_id, salt_len) cmd = ['usermod', '-p', passwd_hash, username] ret, output = ext_utils.run_command_get_output(cmd, log_cmd=False) if ret != 0: return "Failed to set password for {0}: {1}".format(username, output) def gen_password_hash(self, password, crypt_id, salt_len): collection = string.ascii_letters + string.digits salt = ''.join(random.choice(collection) for _ in range(salt_len)) salt = "${0}${1}".format(crypt_id, salt) return crypt.crypt(password, salt) def create_account(self, user, password, expiration, thumbprint, enable_nopasswd): """ Create a user account, with 'user', 'password', 'expiration', ssh keys and sudo permissions. Returns None if successful, error string on failure. """ user_entry = None try: user_entry = pwd.getpwnam(user) except (KeyError, EnvironmentError): pass uid_min = None try: uid_min = int(ext_utils.get_line_starting_with("UID_MIN", "/etc/login.defs").split()[1]) except (ValueError, KeyError, AttributeError, EnvironmentError): pass if uid_min is None: uid_min = 100 if user_entry is not None and user_entry[2] < uid_min: logger.error( "CreateAccount: " + user + " is a system user. Will not set password.") return "Failed to set password for system user: " + user + " (0x06)." if user_entry is None: command = ['useradd', '-m', user] if expiration is not None: command += ['-e', expiration.split('.')[0]] if ext_utils.run(command): logger.error("Failed to create user account: " + user) return "Failed to create user account: " + user + " (0x07)." else: logger.log("CreateAccount: " + user + " already exists. Will update password.") if password is not None: self.change_password(user, password) try: # for older distros create sudoers.d if not os.path.isdir('/etc/sudoers.d/'): # create the /etc/sudoers.d/ directory os.mkdir('/etc/sudoers.d/') # add the include of sudoers.d to the /etc/sudoers ext_utils.set_file_contents( '/etc/sudoers', ext_utils.get_file_contents('/etc/sudoers') + '\n#includedir /etc/sudoers.d\n') if password is None or enable_nopasswd: ext_utils.set_file_contents("/etc/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") else: ext_utils.set_file_contents("/etc/sudoers.d/waagent", user + " ALL = (ALL) ALL\n") os.chmod("/etc/sudoers.d/waagent", 0o440) except EnvironmentError: logger.error("CreateAccount: Failed to configure sudo access for user.") return "Failed to configure sudo privileges (0x08)." home = self.get_home() if thumbprint is not None: ssh_dir = home + "/" + user + "/.ssh" ext_utils.create_dir(ssh_dir, user, 0o700) pub = ssh_dir + "/id_rsa.pub" prv = ssh_dir + "/id_rsa" ext_utils.run_command_and_write_stdout_to_file(['ssh-keygen', '-y', '-f', thumbprint + '.prv'], pub) for f in [pub, prv]: os.chmod(f, 0o600) ext_utils.change_owner(f, user) ext_utils.set_file_contents(ssh_dir + "/authorized_keys", ext_utils.get_file_contents(pub)) ext_utils.change_owner(ssh_dir + "/authorized_keys", user) logger.log("Created user account: " + user) return None def delete_account(self, user): """ Delete the 'user'. Clear utmp first, to avoid error. Removes the /etc/sudoers.d/waagent file. """ user_entry = None try: user_entry = pwd.getpwnam(user) except (KeyError, EnvironmentError): pass if user_entry is None: logger.error("DeleteAccount: " + user + " not found.") return uid_min = None try: uid_min = int(ext_utils.get_line_starting_with("UID_MIN", "/etc/login.defs").split()[1]) except (ValueError, KeyError, AttributeError, EnvironmentError): pass if uid_min is None: uid_min = 100 if user_entry[2] < uid_min: logger.error( "DeleteAccount: " + user + " is a system user. Will not delete account.") return ext_utils.run(['rm', '-f', '/var/run/utmp']) # Delete utmp to prevent error if we are the 'user' deleted ext_utils.run(['userdel', '-f', '-r', user]) try: os.remove("/etc/sudoers.d/waagent") except EnvironmentError: pass return class UbuntuDistro(GenericDistro): def __init__(self, config): """ Generic Attributes go here. These are based on 'majority rules'. This __init__() may be called or overriden by the child. """ super(UbuntuDistro, self).__init__(config) self.selinux = False self.ssh_service_name = 'sshd' self.sudoers_dir_base = '/usr/local/etc' self.distro_name = 'Ubuntu' def restart_ssh_service(self): """ Service call to re(start) the SSH service starting with Ubuntu 22.10, the service name is ssh not sshd, adding fallback incase sshd fails """ ssh_restart_cmd = [self.service_cmd, self.ssh_service_name, self.ssh_service_restart_option] ret_code = ext_utils.run(ssh_restart_cmd) if ret_code != 0: self.ssh_service_name = 'ssh' ssh_restart_cmd = [self.service_cmd, self.ssh_service_name, self.ssh_service_restart_option] ret_code = ext_utils.run(ssh_restart_cmd) if ret_code != 0: logger.error("Failed to restart SSH service with return code:" + str(ret_code)) return ret_code class FreeBSDDistro(GenericDistro): """ """ def __init__(self, config): """ Generic Attributes go here. These are based on 'majority rules'. This __init__() may be called or overriden by the child. """ super(FreeBSDDistro, self).__init__(config) self.selinux = False self.ssh_service_name = 'sshd' self.sudoers_dir_base = '/usr/local/etc' self.distro_name = 'FreeBSD' # noinspection PyMethodOverriding def chpasswd(self, user, password): return ext_utils.run_send_stdin(['pw', 'usermod', 'user', '-h', '0'], password, log_cmd=False) def create_account(self, user, password, expiration, thumbprint, enable_nopasswd): """ Create a user account, with 'user', 'password', 'expiration', ssh keys and sudo permissions. Returns None if successful, error string on failure. """ userentry = None try: userentry = pwd.getpwnam(user) except (EnvironmentError, KeyError): pass uidmin = None try: if os.path.isfile("/etc/login.defs"): uidmin = int(ext_utils.get_line_starting_with("UID_MIN", "/etc/login.defs").split()[1]) except (ValueError, KeyError, AttributeError, EnvironmentError): pass pass if uidmin is None: uidmin = 100 if userentry is not None and userentry[2] < uidmin: logger.error( "CreateAccount: " + user + " is a system user. Will not set password.") return "Failed to set password for system user: " + user + " (0x06)." if userentry is None: command = ['pw', 'useradd', user, '-m'] if expiration is not None: command += ['-e', expiration.split('.')[0]] if ext_utils.run(command): logger.error("Failed to create user account: " + user) return "Failed to create user account: " + user + " (0x07)." else: logger.log( "CreateAccount: " + user + " already exists. Will update password.") if password is not None: self.change_password(user, password) try: # for older distros create sudoers.d if not os.path.isdir(self.sudoers_dir_base + '/sudoers.d/'): # create the /etc/sudoers.d/ directory os.mkdir(self.sudoers_dir_base + '/sudoers.d') # add the include of sudoers.d to the /etc/sudoers ext_utils.set_file_contents( self.sudoers_dir_base + '/sudoers', ext_utils.get_file_contents( self.sudoers_dir_base + '/sudoers') + '\n#includedir ' + self.sudoers_dir_base + '/sudoers.d\n') if password is None or enable_nopasswd: ext_utils.set_file_contents( self.sudoers_dir_base + "/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") else: ext_utils.set_file_contents(self.sudoers_dir_base + "/sudoers.d/waagent", user + " ALL = (ALL) ALL\n") os.chmod(self.sudoers_dir_base + "/sudoers.d/waagent", 0o440) except (ValueError, KeyError, AttributeError, EnvironmentError): logger.error("CreateAccount: Failed to configure sudo access for user.") return "Failed to configure sudo privileges (0x08)." home = self.get_home() if thumbprint is not None: ssh_dir = home + "/" + user + "/.ssh" ext_utils.create_dir(ssh_dir, user, 0o700) pub = ssh_dir + "/id_rsa.pub" prv = ssh_dir + "/id_rsa" ext_utils.run_command_and_write_stdout_to_file(['sh-keygen', '-y', '-f', thumbprint + '.prv'], pub) ext_utils.set_file_contents( prv, ext_utils.get_file_contents(thumbprint + ".prv")) for f in [pub, prv]: os.chmod(f, 0o600) ext_utils.change_owner(f, user) ext_utils.set_file_contents( ssh_dir + "/authorized_keys", ext_utils.get_file_contents(pub)) ext_utils.change_owner(ssh_dir + "/authorized_keys", user) logger.log("Created user account: " + user) return None def delete_account(self, user): """ Delete the 'user'. Clear utmp first, to avoid error. Removes the /etc/sudoers.d/waagent file. """ userentry = None try: userentry = pwd.getpwnam(user) except (EnvironmentError, KeyError): pass if userentry is None: logger.error("DeleteAccount: " + user + " not found.") return uidmin = None try: if os.path.isfile("/etc/login.defs"): uidmin = int( ext_utils.get_line_starting_with("UID_MIN", "/etc/login.defs").split()[1]) except (ValueError, KeyError, AttributeError, EnvironmentError): pass if uidmin is None: uidmin = 100 if userentry[2] < uidmin: logger.error( "DeleteAccount: " + user + " is a system user. Will not delete account.") return # empty contents of utmp to prevent error if we are the 'user' deleted ext_utils.run_command_and_write_stdout_to_file(['echo'], '/var/run/utmp') ext_utils.run(['rmuser', '-y', user], chk_err=False) try: os.remove(self.sudoers_dir_base + "/sudoers.d/waagent") except EnvironmentError: pass return def get_home(self): return '/home' class CoreOSDistro(GenericDistro): """ CoreOS Distro concrete class Put CoreOS specific behavior here... """ CORE_UID = 500 def __init__(self, config): super(CoreOSDistro, self).__init__(config) self.waagent_path = '/usr/share/oem/bin' self.python_path = '/usr/share/oem/python/bin' self.distro_name = 'CoreOS' if 'PATH' in os.environ: os.environ['PATH'] = "{0}:{1}".format(os.environ['PATH'], self.python_path) else: os.environ['PATH'] = self.python_path if 'PYTHONPATH' in os.environ: os.environ['PYTHONPATH'] = "{0}:{1}".format(os.environ['PYTHONPATH'], self.waagent_path) else: os.environ['PYTHONPATH'] = self.waagent_path def restart_ssh_service(self): """ SSH is socket activated on CoreOS. No need to restart it. """ return 0 def create_account(self, user, password, expiration, thumbprint, enable_nopasswd): """ Create a user account, with 'user', 'password', 'expiration', ssh keys and sudo permissions. Returns None if successful, error string on failure. """ userentry = None try: userentry = pwd.getpwnam(user) except (EnvironmentError, KeyError): pass uidmin = None try: uidmin = int(ext_utils.get_line_starting_with("UID_MIN", "/etc/login.defs").split()[1]) except (ValueError, KeyError, AttributeError, EnvironmentError): pass if uidmin is None: uidmin = 100 if userentry is not None and userentry[2] < uidmin and userentry[2] != self.CORE_UID: logger.error( "CreateAccount: " + user + " is a system user. Will not set password.") return "Failed to set password for system user: " + user + " (0x06)." if userentry is None: command = ['useradd', '--create-home', '--password', '*', user] if expiration is not None: command += ['--expiredate', expiration.split('.')[0]] if ext_utils.run(command): logger.error("Failed to create user account: " + user) return "Failed to create user account: " + user + " (0x07)." else: logger.log("CreateAccount: " + user + " already exists. Will update password.") if password is not None: self.change_password(user, password) try: if password is None or enable_nopasswd: ext_utils.set_file_contents("/etc/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") else: ext_utils.set_file_contents("/etc/sudoers.d/waagent", user + " ALL = (ALL) ALL\n") os.chmod("/etc/sudoers.d/waagent", 0o440) except EnvironmentError: logger.error("CreateAccount: Failed to configure sudo access for user.") return "Failed to configure sudo privileges (0x08)." home = self.get_home() if thumbprint is not None: ssh_dir = home + "/" + user + "/.ssh" ext_utils.create_dir(ssh_dir, user, 0o700) pub = ssh_dir + "/id_rsa.pub" prv = ssh_dir + "/id_rsa" ext_utils.run_command_and_write_stdout_to_file(['ssh-keygen', '-y', '-f', thumbprint + '.prv'], pub) ext_utils.set_file_contents(prv, ext_utils.get_file_contents(thumbprint + ".prv")) for f in [pub, prv]: os.chmod(f, 0o600) ext_utils.change_owner(f, user) ext_utils.set_file_contents(ssh_dir + "/authorized_keys", ext_utils.get_file_contents(pub)) ext_utils.change_owner(ssh_dir + "/authorized_keys", user) logger.log("Created user account: " + user) return None class RedhatDistro(GenericDistro): """ Redhat Distro concrete class Put Redhat specific behavior here... """ def __init__(self, config): super(RedhatDistro, self).__init__(config) self.service_cmd = '/sbin/service' self.ssh_service_restart_option = 'condrestart' self.ssh_service_name = 'sshd' self.distro_name = 'Red Hat' class CentOSDistro(RedhatDistro): def __init__(self, config): super(CentOSDistro, self).__init__(config) self.distro_name = "CentOS" class FedoraDistro(RedhatDistro): """ FedoraDistro concrete class Put Fedora specific behavior here... """ def __init__(self, config): super(FedoraDistro, self).__init__(config) self.service_cmd = '/usr/bin/systemctl' self.hostname_file_path = '/etc/hostname' self.distro_name = 'Fedora' def restart_ssh_service(self): """ Service call to re(start) the SSH service """ ssh_restart_cmd = [self.service_cmd, self.ssh_service_restart_option, self.ssh_service_name] retcode = ext_utils.run(ssh_restart_cmd) if retcode > 0: logger.error("Failed to restart SSH service with return code:" + str(retcode)) return retcode def create_account(self, user, password, expiration, thumbprint, enable_nopasswd): ext_utils.run(['/sbin/usermod', user, '-G', 'wheel']) def delete_account(self, user): ext_utils.run(['/sbin/usermod', user, '-G', '']) class SuSEDistro(GenericDistro): def __init__(self, config): super(SuSEDistro, self).__init__(config) self.ssh_service_name = 'sshd' self.distro_name = "SuSE" class MarinerDistro(GenericDistro): def __init__(self, config): super(MarinerDistro, self).__init__(config) self.ssh_service_name = 'sshd' self.service_cmd = '/usr/bin/systemctl' self.distro_name = 'Mariner' def restart_ssh_service(self): """ Service call to re(start) the SSH service """ ssh_restart_cmd = [self.service_cmd, self.ssh_service_restart_option, self.ssh_service_name] retcode = ext_utils.run(ssh_restart_cmd) if retcode > 0: logger.error("Failed to restart SSH service with return code:" + str(retcode)) return retcode