https://github.com/ExpelliarmusSuperComp/Expelliarmus
Tip revision: 83a8b7d8fc3d3b7dd4ac7ef5df4c02bd648bc526 authored by ExpelliarmusSuperComp on 07 August 2023, 14:18:01 UTC
LICENSE
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)