https://github.com/ExpelliarmusSuperComp/Expelliarmus
Raw File
Tip revision: 83a8b7d8fc3d3b7dd4ac7ef5df4c02bd648bc526 authored by ExpelliarmusSuperComp on 07 August 2023, 14:18:01 UTC
LICENSE
Tip revision: 83a8b7d
VMIManipulation.py
import os
import re
import sys
import itertools
import threading
import time
import guestfs
import tarfile
from abc import ABCMeta, abstractmethod
import subprocess

import shutil

from StaticInfo import StaticInfo


class VMIManipulator:
    __metaclass__ = ABCMeta
    @abstractmethod
    def __init__(self, pathToVMI, vmiName, distribution, arch, guest):
        self.guest = guest
        self.local_relPathToVMI = pathToVMI
        self.vmiName = vmiName
        self.distribution = distribution
        self.vmi_arch = arch
        self.local_currentDir = os.path.dirname(os.path.realpath(__file__))
        self.local_absPathToVMI = self.local_currentDir + '/' + pathToVMI
        self.vmi_repackagingFolder = "/var/exportpackages"
        self.vmi_repoFolder = "/var/tempRepository"
        self.localUserBackupPath = StaticInfo.relPathLocalRepositoryUserFolders + "/userfolder_" + self.vmiName + ".tar"
        self.loading = False

    @staticmethod
    def getVMIManipulator(pathToVMI, vmiName, guest, root, pkgManager=None, distro=None, arch=None):
        #print ('Creating VMIManipulator for disk: \"' + pathToVMI + '\"')
        if pkgManager is None:
            pkgManager  = guest.inspect_get_package_management(root)
        if distro is None:
            distro      = guest.inspect_get_distro(root)
        if arch is None:
            arch        = guest.inspect_get_arch(root)
        '''print "VMIManipulatorAPT"
        print "Distribution:\t\t" + distro
        print "Package Management:\t" + pkgManager'''
        if pkgManager == "apt":
            return VMIManipulatorAPT(pathToVMI, vmiName, distro, arch, guest)
        elif pkgManager == "dnf":
            return VMIManipulatorDNF(pathToVMI, vmiName, distro, arch, guest)
        else:
            raise (Exception("VMI's Package Management \"" + pkgManager + "\" is not supported."))

    def checkSELinux(self):
        try:
            self.guest.sh("sestatus")
            return True
        except RuntimeError as e:
            pass
        return False

    @abstractmethod
    def exportPackages(self, packageDict):pass

    @abstractmethod
    def importPackages(self, mainServices, filenames):pass

    @abstractmethod
    def removePackages(self, packageList):pass

    @abstractmethod
    def exportHomeDir(self): pass

    @abstractmethod
    def importHomeDir(self):pass

    @abstractmethod
    def removeHomeDir(self):pass

    @staticmethod
    def compare(a, b):
        return len(a) - len(b)

    @staticmethod
    def relabelSELinux(pathToVMI):
        print ('Relabeling VMI ' + pathToVMI + ' (required by SELinux): ')

        absPathToVMI = os.path.dirname(os.path.realpath(__file__)) + '/' + pathToVMI
        subprocess.call(
            ['/home/csat2890/Downloads/libguestfs-1.36.7/run', 'virt-customize',
             '--add', absPathToVMI,
             '--selinux-relabel'],
            stdout=subprocess.PIPE)

    # TODO: check blkid-tab
    @staticmethod
    def resetImage(pathToVMI):
        """
        Resets certain properties of the image. Intended to use after cloning
        Reset properties:
            abrt-data:          crash data generated by ABRT
            backup-files:       editor backup files
            bash-history:       bash-history
            blkid-tab:          cached information from blkid? #TODO
            crash-data:         automatically generated kdump kernel crash data
            dhcp-client-state:  DHCP client leases
            dhcp-server-state:  DHCP server leases
            dovecot-data:       Dovecot (mail server) data
            logfiles:           see http://libguestfs.org/virt-sysprep.1.html -> logfiles
            lvm-uuids:          LVM2 PV and VG UUIDs
            machine-id:         local machine ID
            mail-spool:         email from the local mail spool directory
            net-hostname:       HOSTNAME and DHCP_HOSTNAME in network interface configuration
            net-hwaddr:         HWADDR (hard-coded MAC address) configuration
            pacct-log:          process accounting log files
            pam-data:           pam data, see http://manpages.ubuntu.com/manpages/trusty/man5/pam.d.5.html
            passwd-backups:     /etc/passwd- and similar backup files
            puppet-data-log:    data and log files of puppet
            rh-subscription-manager: RH subscription manager files
            rhn-systemid:       Red Hat Network system ID
            rpm-db:             host-specific RPM database files and locks
            samba-db-log:       database and log files of Samba
            smolt-uuid:         Smolt hardware UUID
            !ssh-hostkeys:      SSH host keys
                                If, after cloning, the guest gets the same IP address, ssh will give you a stark warning about the host key changing:
            ssh-userdir:        ".ssh" directories in the guest
            sssd-db-log:        database and log files of sssd
            tmp-files:          temporary files under /tmp and /var/tmp
            udev-persistent-net:udev persistent net rules
            utmp:               utmp file
            yum-uuid:           yum UUID
            customize           to generate new random seed

            REMOVED because of unidenfiable problems with virt-sysprep
            package-manager-cache: package manager cache
            cron-spool:         user's at-jobs and cron-jobs (scheduled jobs)
        :param pathToVMI:
        :return:
        """
        print ('Resetting VMI (e.g. Log files, crashreports, editor backups ...): ')

        absPathToVMI = os.path.dirname(os.path.realpath(__file__)) + '/' + pathToVMI
        subprocess.call(
            [StaticInfo.absPathLibguestfsRun, 'virt-sysprep',
             '--add', absPathToVMI,
             '--enable','customize'],
            stdout=subprocess.PIPE)
        subprocess.call(
            [StaticInfo.absPathLibguestfsRun, 'virt-sysprep',
                '--add', absPathToVMI,
                '--operations',
                'customize,abrt-data,backup-files,bash-history,blkid-tab,crash-data,dhcp-client-state,dhcp-server-state,dovecot-data,logfiles,lvm-uuids,machine-id,mail-spool,net-hostname,net-hwaddr,pacct-log,pam-data,passwd-backups,puppet-data-log,rh-subscription-manager,rhn-systemid,rpm-db,samba-db-log,smolt-uuid,ssh-hostkeys,ssh-userdir,sssd-db-log,tmp-files,udev-persistent-net,utmp,yum-uuid'],
            stdout=subprocess.PIPE)


    def load(self):
        for c in itertools.cycle(['.  ', '.. ', '...', ' ..', '  .','   ']):
            if not self.loading:
                break
            sys.stdout.write('\r' + c)
            sys.stdout.flush()
            time.sleep(0.5)

    def startLoadingAnimation(self):
        self.loading = True;
        t = threading.Thread(target=self.load)
        t.start()

    def stopLoadingAnimation(self):
        self.loading = False
        sys.stdout.write('\r   ')
        sys.stdout.flush()

class VMIManipulatorAPT(VMIManipulator):
    def __init__(self, pathToVMI, vmiName, distribution, arch, guest):
        super(VMIManipulatorAPT, self).__init__(pathToVMI, vmiName, distribution, arch, guest)
        self.local_packageFolder = StaticInfo.relPathLocalRepositoryPackages + "/" + self.distribution
        self.vmi_sourcesConfigFolder = "/etc/apt/sources.list.d"
        self.vmi_tmpSourceConfigPath = self.vmi_sourcesConfigFolder + "/tempRepo.list"
        self.localSourcesFile = StaticInfo.relPathGuestRepoConfigUbuntu
        self.vmi_UserFolder = "/home"
        self.checkFolderExistence()

    def checkFolderExistence(self):
        if not os.path.isdir(self.local_packageFolder):
            os.mkdir(self.local_packageFolder)
        if not os.path.isfile(self.localSourcesFile):
            sys.exit("ERROR: config file for guest repository not found (looking for %s)" % StaticInfo.relPathGuestRepoConfigs)

    def exportPackages(self, packageDict):
        """
            :param dict() packageDict:
            # in the form of {pkg,{name:"pkg", version:"1.1", architecture:"amd64", essential:False}}
            :return list() packageInfoList:
            # in the form of [(name, version, architecture, distribution, filename)]
        """
        patternPkgName = r"([^'`]*)"
        patternFileName = r"./([^'`]*)"
        depMatcher = re.compile(r"^dpkg-deb: building package ['`]"+patternPkgName+"['`] in ['`]"+patternFileName+"['`].$")
        numPackages = len(packageDict)
        packageInfoDict = dict(packageDict) # same as input + filenames

        # check if any packages have to be exported at all
        if (numPackages > 0):
            try:
                # Repackage in guest
                self.guest.mkdir(self.vmi_repackagingFolder)
            except:
                print "\tExisting folder %s in VMI will be replaced" % self.vmi_repackagingFolder
                self.guest.rm_rf(self.vmi_repackagingFolder)
                self.guest.mkdir(self.vmi_repackagingFolder)
            print "\tStarting to repackage and export " + str(numPackages) + " package(s)."
            packageFileNames = self.guest.sh(
                "cd /var/exportpackages && fakeroot -u dpkg-repack " + " ".join(packageDict.keys()))

            # Download and extract packages, delete temp folder in guest
            localpackagesFilePath = self.local_packageFolder + "/" + self.vmiName + "Packages.tar"
            self.guest.tar_out(self.vmi_repackagingFolder, localpackagesFilePath)
            self.guest.rm_rf(self.vmi_repackagingFolder)
            with tarfile.open(localpackagesFilePath) as tar:
                tar.extractall(path=self.local_packageFolder)
            os.remove(localpackagesFilePath)

            # save filename information of packages
            for line in packageFileNames.split("\n"):
                matchResult = depMatcher.match(line)
                if matchResult:
                    pkgName = matchResult.group(1)
                    pkgFileName = matchResult.group(2)
                    pkgNewpath = self.local_packageFolder + "/" + pkgFileName
                    packageInfoDict[pkgName][StaticInfo.dictKeyFilePath] = pkgNewpath

        # make sure every package in packageInfoDict has a path
        for pkg,pkgInfo in packageInfoDict.iteritems():
            if "path" not in pkgInfo:
                del packageInfoDict[pkg]
                print "ATTENTION: package \"%s\" was planned to be exported but failed."

        print "\t" + str(len(packageInfoDict)) + " package(s) exported"
        return packageInfoDict

    def importPackages(self, mainServices, filenames):

        errorString = None

        # check if installation necessary
        if len(mainServices) > 0:
            # prepare tarfile with compressed packages for import
            localpackagesFilePath = self.local_packageFolder + "/" + self.vmiName + "Packages.tar"
            with tarfile.open(localpackagesFilePath, mode='w') as tar:
                for pkgFileName in filenames:
                    tar.add(pkgFileName)

            # Upload packages to temporary repository
            try:
                self.guest.mkdir(self.vmi_repoFolder)
            except:
                print "\"" + self.vmi_repoFolder + "\" already exist in guest. Proceeding anyway."
            self.guest.tar_in(localpackagesFilePath, self.vmi_repoFolder)

            # Rename default .list
            self.guest.rename("/etc/apt/sources.list", "/etc/apt/sources.list2")
            self.guest.rename("/etc/apt/sources.list.d", "/etc/apt/sources.list.d2")

            # Upload temporary sources.list
            self.guest.mkdir(self.vmi_sourcesConfigFolder)
            self.guest.upload(self.localSourcesFile, self.vmi_tmpSourceConfigPath)

            self.guest.sh("cd /var/tempRepository && dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz")

            # Installing package
            try:
                self.guest.sh("apt-get update \
                                && DEBIAN_FRONTEND=noninteractive "
                                     "apt-get --option Dpkg::Options::=--force-confnew -y --allow-unauthenticated "
                                     "install " + " ".join(mainServices) + "")
                #exec >> '/var/builder.log' 2>&1 &&
            except RuntimeError as e:
                if "Can not write log (Is /dev/pts mounted?)" in e.message:
                    errorString = e.message
                else:
                    print e.message
                    sys.exit("ERROR while reassembling: Importing packages to \"%s\" exited with errors." % self.local_relPathToVMI)

            # Remove temporary repository
            self.guest.rm_rf(self.vmi_repoFolder)

            # Remove temporary sources.list
            self.guest.rm_rf(self.vmi_sourcesConfigFolder)

            # Rename default .list
            self.guest.rename("/etc/apt/sources.list2", "/etc/apt/sources.list")
            self.guest.rename("/etc/apt/sources.list.d2", "/etc/apt/sources.list.d")

            # Remove temporary tarfile
            os.remove(localpackagesFilePath)

        return errorString

    def removePackages(self, packageList):
        """
            removes main services and other orphaned packages
        :param packageList: packages to remove (main services)
        :return: list of packages that have been removed
        """
        self.guest.sh("DEBIAN_FRONTEND=noninteractive apt-get purge --auto-remove -y " + " ".join(packageList))
        self.guest.sh("DEBIAN_FRONTEND=noninteractive apt-get clean")

    def exportHomeDir(self):
        if os.path.isfile(self.localUserBackupPath):
            print "\tExisting user folder in " + self.localUserBackupPath + " will be replaced."
            os.remove(self.localUserBackupPath)
        self.guest.tar_out(self.vmi_UserFolder, self.localUserBackupPath)
        return self.localUserBackupPath

    def importHomeDir(self, pathToHomeDir):
        if self.guest.exists(self.vmi_UserFolder):
            print "Existing user folder in " + self.local_relPathToVMI + " will be replaced."
            self.guest.rm_rf(self.vmi_UserFolder)
        self.guest.mkdir(self.vmi_UserFolder)
        self.guest.tar_in(pathToHomeDir, self.vmi_UserFolder)

    def removeHomeDir(self):
        self.guest.rm_rf(self.vmi_UserFolder)

class VMIManipulatorDNF(VMIManipulator):
    def __init__(self, pathToVMI, vmiName, distribution, arch, guest):
        super(VMIManipulatorDNF, self).__init__(pathToVMI, vmiName, distribution, arch, guest)
        self.local_packageFolder    = StaticInfo.relPathLocalRepositoryPackages + "/" + self.distribution
        self.localSourcesFile       = StaticInfo.relPathGuestRepoConfigFedora
        self.vmi_sourcesFolderPath  = "/etc/yum.repos.d/"
        self.vmi_tmpSourceConfigPath= "/etc/yum.repos.d/tempRepo.repo"
        self.vmi_UserFolder = "/home"
        self.checkFolderExistence()

    def checkFolderExistence(self):
        if not os.path.isdir(self.local_packageFolder):
            os.mkdir(self.local_packageFolder)
        if not os.path.isfile(self.localSourcesFile):
            sys.exit("ERROR: config file for guest repository not found (looking for %s)" % StaticInfo.relPathGuestRepoConfigs)

    def exportPackages(self, packageDict):
        """
            :param dict() packageDict:
            # in the form of {pkg,{name:"pkg", version:"1.1", architecture:"amd64", essential:False}}
            :return list() packageInfoDict:
            # in the form of {pkg,{name:"pkg", version:"1.1", architecture:"amd64", essential:False, path:localRepo/pkg.rpm}}
        """
        numPackages = len(packageDict)
        packageInfoDict = dict(packageDict)  # same as input + filenames

        # check if any packages have to be exported at all
        if (numPackages > 0):
            # Check if Export folder in guest exists
            try:
                self.guest.mkdir(self.vmi_repackagingFolder)
            except:
                print "\tExisting folder %s in VMI will be replaced" % self.vmi_repackagingFolder
                self.guest.rm_rf(self.vmi_repackagingFolder)
                self.guest.mkdir(self.vmi_repackagingFolder)
            print "\tStarting to repackage and export " + str(numPackages) + " package(s)."


            startTime = time.time()

            pkgFileNames = dict() # in form {pkgName:rebuildOutput}
            i = 1
            for pkgName in packageDict.keys():
                #sys.stdout.write ("\r\t{:26s}| {:20s}".format(pkgName,"%i/%i (%.0f%%)" % (i,numPackages, (float(i)/numPackages*100))))
                sys.stdout.write ("\r\t{:26s}| {:30s}".format("Progress: %i/%i (%.0f%%)" % (i,numPackages, (float(i)/numPackages*100)),pkgName))
                #sys.stdout.write ("\r\tProgress: %i/%i (%.0f%%)" % (i,numPackages, (float(i)/numPackages*100)))
                sys.stdout.flush()
                i = i + 1
                output = self.guest.sh("rpmrebuild --batch --comment-missing=yes --directory " + self.vmi_repackagingFolder + " " + pkgName)
                for line in output.split("\n"):
                    if line.startswith("result: "):
                        packageInfoDict[pkgName][StaticInfo.dictKeyFilePath] = self.local_packageFolder +\
                                                                               "/" + line.rsplit("/",1)[1]
                        break
                # check if all packages were exported and received a filename
                if StaticInfo.dictKeyFilePath not in packageInfoDict[pkgName]:
                    print "\t\tcould not find repackaged \"%s\"" % pkgName
                    print "\t\tOutput of repackaging:"
                    print "\t\t" + str(output)
            print ""
            # Download and extract packages, delete temp folder in guest
            localTempFileFolder = self.local_packageFolder + "/" + self.vmiName
            os.mkdir(localTempFileFolder)
            localTempPackagesTar = localTempFileFolder + "/Packages.tar"

            self.guest.tar_out(self.vmi_repackagingFolder, localTempPackagesTar)
            self.guest.rm_rf(self.vmi_repackagingFolder)
            with tarfile.open(localTempPackagesTar) as tar:
                tar.extractall(path=localTempFileFolder)
            os.remove(localTempPackagesTar)

            # move files in temp folder to actual repository folder (flattens directory structure)
            for root, _dirs, files in os.walk(localTempFileFolder):
                for filename in files:
                    shutil.move(root + "/" + filename, self.local_packageFolder + "/" + filename)
            shutil.rmtree(localTempFileFolder)

        # make sure every package in packageInfoDict has a path
        for pkg, pkgInfo in packageInfoDict.iteritems():
            if "path" not in pkgInfo:
                del packageInfoDict[pkg]
                print "ATTENTION: package \"%s\" was planned to be exported but failed."
        return packageInfoDict

    def importPackages(self, mainServices, filenames):
        if len(mainServices) > 0:

            # prepare tarfile with compressed packages for import
            localPkgsTarPath = self.local_packageFolder + "/" + self.vmiName + "Packages.tar"
            with tarfile.open(localPkgsTarPath, mode='w') as tar:
                for pkgFileName in filenames:
                    tar.add(pkgFileName)

            # Upload packages to temporary repository
            try:
                self.guest.mkdir(self.vmi_repoFolder)
            except:
                print "\"" + self.vmi_repoFolder + "\" already exist in guest. Proceeding anyway."

            self.guest.tar_in(localPkgsTarPath, self.vmi_repoFolder)

            # Backup VMI repo configs locally and remove in vmi
            localVmiRepoConfigBackup = StaticInfo.relPathLocalRepository + "/" + self.vmiName + "_repoConfigs.tar"
            try:
                os.remove(localVmiRepoConfigBackup)
            except OSError as e:
                pass
            self.guest.tar_out(self.vmi_sourcesFolderPath, localVmiRepoConfigBackup)
            self.guest.rm_rf(self.vmi_sourcesFolderPath)
            self.guest.mkdir(self.vmi_sourcesFolderPath)

            # Create temporary local repository config in vmi
            self.guest.upload(self.localSourcesFile, self.vmi_tmpSourceConfigPath)
            self.guest.sh("createrepo " + self.vmi_repoFolder)

            # Installing package
            self.guest.sh("dnf --nogpgcheck -y install " + " ".join(mainServices))

            # Remove temporary repository
            self.guest.rm_rf(self.vmi_repoFolder)

            # Remove temporary repo config
            self.guest.rm(self.vmi_tmpSourceConfigPath)

            # Restore original repositories
            self.guest.tar_in(localVmiRepoConfigBackup, self.vmi_sourcesFolderPath)

            # Remove original repo config backup
            os.remove(localVmiRepoConfigBackup)

            # Remove temporary tarfile
            os.remove(localPkgsTarPath)

            # Cleanup repository
            self.guest.sh("dnf clean all")

    def removePackages(self, packageList):
        try:
            self.guest.sh("dnf -y remove " + " ".join(packageList))
        except RuntimeError as e:
            if "Problem: The operation would result in removing the following protected packages:" in e.message:
                sys.exit("Cannot remove main services \"%s\". Error:\n%s" % (",".join(packageList), e.message))
            else: raise RuntimeError(e.message)
        self.guest.sh("dnf autoremove")
        self.guest.sh("dnf clean all")

    def exportHomeDir(self):
        if os.path.isfile(self.localUserBackupPath):
            print "Existing user folder in " + self.localUserBackupPath + " will be replaced."
            os.remove(self.localUserBackupPath)
        self.guest.tar_out(self.vmi_UserFolder, self.localUserBackupPath)
        return self.localUserBackupPath

    def importHomeDir(self, pathToHomeDir):
        if self.guest.exists(self.vmi_UserFolder):
            print "Existing user folder in " + self.local_relPathToVMI + " will be replaced."
            self.guest.rm_rf(self.vmi_UserFolder)
        self.guest.mkdir(self.vmi_UserFolder)
        self.guest.tar_in(pathToHomeDir, self.vmi_UserFolder)

    def removeHomeDir(self):
        self.guest.rm_rf(self.vmi_UserFolder)
back to top