788 lines
25 KiB
Go
788 lines
25 KiB
Go
package distros
|
|
|
|
import (
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog"
|
|
"libguestfs.org/guestfs"
|
|
|
|
"git.csclub.uwaterloo.ca/cloud/cloudbuild/pkg/config"
|
|
)
|
|
|
|
//go:embed resources
|
|
var res embed.FS
|
|
|
|
func getResource(filename string) []byte {
|
|
data, err := res.ReadFile("resources/" + filename)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// A TemplateManager downloads and modifies VM templates for a distro.
|
|
type TemplateManager struct {
|
|
cfg *config.Config
|
|
logger *zerolog.Logger
|
|
impl IDistroSpecificTemplateManager
|
|
}
|
|
|
|
type ITemplateManager interface {
|
|
IDistroSpecificTemplateManager
|
|
// ModifyTemplate makes custom modifications to the downloaded VM template
|
|
ModifyTemplate(filename string) error
|
|
}
|
|
|
|
// An IDistroSpecificTemplateManager performs the distro-specific tasks
|
|
// when modifying a VM template. It is used by the generic TemplateManager.
|
|
type IDistroSpecificTemplateManager interface {
|
|
// GetLatestVersion returns the version number and codename of the
|
|
// latest version of particular OS (e.g. version="22.04", codename="jammy")
|
|
GetLatestVersion() (version string, codename string, err error)
|
|
// DownloadTemplate downloads a VM template for a given OS version and
|
|
// codename, and returns the path to where it was downloaded
|
|
DownloadTemplate(version, codename string) (path string, err error)
|
|
// PerformDistroSpecificModifications is called after
|
|
// performDistroAgnosticModifications to modify a template in a
|
|
// distro-specific way.
|
|
PerformDistroSpecificModifications(handle *guestfs.Guestfs) error
|
|
// CommandToUpdatePackageCache returns the command which updates
|
|
// (but does not upgrade) the packages for this distro, e.g.
|
|
// "sudo apt update".
|
|
CommandToUpdatePackageCache() []string
|
|
// InstallSystemdResolved installs the OS-specific package for
|
|
// systemd-resolved.
|
|
InstallSystemdResolved(handle *guestfs.Guestfs) error
|
|
}
|
|
|
|
func (mgr *TemplateManager) InstallSystemdResolved(handle *guestfs.Guestfs) error {
|
|
// Individual distros should override this method if necessary
|
|
return errors.New("InstallSystemdResolved: not implemented for this distro")
|
|
}
|
|
|
|
func (mgr *TemplateManager) DownloadTemplateGeneric(filename, url string) (path string, err error) {
|
|
if mgr.cfg.SkipDownload {
|
|
mgr.logger.Debug().Str("url", url).Msg("SKIP_DOWNLOAD was set, skipping download")
|
|
return filename, nil
|
|
}
|
|
mgr.logger.Debug().Str("url", url).Msg("Downloading template")
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
// TODO: download to somewhere in /tmp
|
|
path = filename
|
|
out, err := os.Create(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer out.Close()
|
|
_, err = io.Copy(out, resp.Body)
|
|
return
|
|
}
|
|
|
|
func fedoraCommandToUpdatePackageCache() []string {
|
|
return []string{"sudo", "dnf", "makecache"}
|
|
}
|
|
|
|
func (mgr *TemplateManager) performDistroAgnosticModifications(handle *guestfs.Guestfs) (err error) {
|
|
if err = mgr.setupCommonNetworkConfigs(handle); err != nil {
|
|
return
|
|
}
|
|
if err = mgr.setupIpv6Scripts(handle); err != nil {
|
|
return
|
|
}
|
|
if err = mgr.setResolvConf(handle); err != nil {
|
|
return
|
|
}
|
|
if err = mgr.setupCommonNTPConfigs(handle); err != nil {
|
|
return
|
|
}
|
|
if err = mgr.setMotd(handle); err != nil {
|
|
return
|
|
}
|
|
if err = mgr.updateSshdConfig(handle); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (mgr *TemplateManager) ModifyTemplate(filename string) (err error) {
|
|
handle, err := mgr.getGuestfsMountedHandle(filename)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer mgr.unmountAndCloseGuestfsHandle(handle)
|
|
hasSELinuxEnabled, err := hasSELinuxEnabled(handle)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if hasSELinuxEnabled {
|
|
availErr := handle.Available([]string{"selinuxrelabel"})
|
|
if availErr != nil {
|
|
return fmt.Errorf("This appliance does not support selinuxrelabel: %w", availErr)
|
|
}
|
|
}
|
|
if err = mgr.createAugeasHandle(handle); err != nil {
|
|
return
|
|
}
|
|
defer mgr.closeAugeasHandle(handle)
|
|
if err = mgr.performDistroAgnosticModifications(handle); err != nil {
|
|
return
|
|
}
|
|
if err = mgr.impl.PerformDistroSpecificModifications(handle); err != nil {
|
|
return
|
|
}
|
|
if hasSELinuxEnabled {
|
|
if err = mgr.selinuxRelabelDirectories(handle); err != nil {
|
|
return
|
|
}
|
|
}
|
|
return mgr.saveAugeasValues(handle)
|
|
}
|
|
|
|
func (mgr *TemplateManager) getGuestfsMountedHandle(filename string) (handle *guestfs.Guestfs, err error) {
|
|
log := mgr.logger
|
|
handle, err = guestfs.Create()
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Needed to install packages in VM
|
|
log.Debug().Msg("Enabling network access")
|
|
err = handle.Set_network(true)
|
|
if err != nil {
|
|
return
|
|
}
|
|
log.Debug().Msg("Adding drive " + filename)
|
|
if err = handle.Add_drive(filename, nil); err != nil {
|
|
return
|
|
}
|
|
log.Debug().Msg("Launching VM")
|
|
if err = handle.Launch(); err != nil {
|
|
return
|
|
}
|
|
log.Debug().Msg("Inspecting OS")
|
|
partitions, err := handle.Inspect_os()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(partitions) != 1 {
|
|
return nil, fmt.Errorf("Expected 1 root partition, found %d", len(partitions))
|
|
}
|
|
rootPartition := partitions[0]
|
|
log.Debug().Msg(fmt.Sprintf("Mounting root filesystem %s on /", rootPartition))
|
|
if err = handle.Mount(rootPartition, "/"); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (mgr *TemplateManager) unmountGuestfsDrive(handle *guestfs.Guestfs) {
|
|
if err := handle.Umount("/", nil); err != nil {
|
|
mgr.logger.Error().Err(err).Msg("")
|
|
}
|
|
}
|
|
|
|
func (mgr *TemplateManager) unmountAndCloseGuestfsHandle(handle *guestfs.Guestfs) {
|
|
mgr.unmountGuestfsDrive(handle)
|
|
handle.Close()
|
|
}
|
|
|
|
func hasSELinuxEnabled(handle *guestfs.Guestfs) (bool, error) {
|
|
return handle.Is_file("/usr/bin/sestatus", nil)
|
|
}
|
|
|
|
func getSelinuxType(handle *guestfs.Guestfs) (selinuxType string, err error) {
|
|
lines, err := handle.Grep("^SELINUXTYPE=", "/etc/selinux/config", nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(lines) != 1 {
|
|
err = fmt.Errorf("Expected 1 line containing SELINUXTYPE, found %d", len(lines))
|
|
return
|
|
}
|
|
selinuxType = strings.Split(lines[0], "=")[1]
|
|
return
|
|
}
|
|
|
|
func getSelinuxDefaultSpecfile(handle *guestfs.Guestfs) (specfile string, err error) {
|
|
selinuxType, err := getSelinuxType(handle)
|
|
if err != nil {
|
|
return
|
|
}
|
|
specfile = fmt.Sprintf("/etc/selinux/%s/contexts/files/file_contexts", selinuxType)
|
|
return
|
|
}
|
|
|
|
func (mgr *TemplateManager) selinuxRelabel(handle *guestfs.Guestfs, specfile, dir string) (err error) {
|
|
mgr.logger.Debug().
|
|
Str("specfile", specfile).
|
|
Str("path", dir).
|
|
Msg("Relabeling SELinux security context")
|
|
return handle.Selinux_relabel(specfile, dir, nil)
|
|
}
|
|
|
|
func (mgr *TemplateManager) selinuxRelabelDirectories(handle *guestfs.Guestfs) (err error) {
|
|
specfile, err := getSelinuxDefaultSpecfile(handle)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, dir := range []string{"/etc", "/var/lib/cloud"} {
|
|
if err = mgr.selinuxRelabel(handle, specfile, dir); err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (mgr *TemplateManager) logAndRunCommand(handle *guestfs.Guestfs, args []string) (string, error) {
|
|
mgr.logger.Debug().Msg("Running command `" + strings.Join(args, " ") + "`")
|
|
return handle.Command(args)
|
|
}
|
|
|
|
func (mgr *TemplateManager) enableSystemdUnit(handle *guestfs.Guestfs, unit string) error {
|
|
_, err := mgr.logAndRunCommand(handle, []string{"systemctl", "enable", unit})
|
|
return err
|
|
}
|
|
|
|
func (mgr *TemplateManager) disableSystemdUnit(handle *guestfs.Guestfs, unit string) error {
|
|
_, err := mgr.logAndRunCommand(handle, []string{"systemctl", "disable", unit})
|
|
return err
|
|
}
|
|
|
|
func (mgr *TemplateManager) systemdServiceIsEnabled(handle *guestfs.Guestfs, unit string) (bool, error) {
|
|
// Use '|| true' because the exit code will be non-zero if the unit
|
|
// is not enabled or does not exist
|
|
command := "systemctl is-enabled " + unit + " || true"
|
|
mgr.logger.Debug().Msg(`Running sh -c "` + command + `"`)
|
|
output, err := handle.Sh(command)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// Strip newline, if there is one
|
|
if len(output) > 0 && output[len(output)-1] == '\n' {
|
|
output = output[:len(output)-1]
|
|
}
|
|
if output == "enabled" {
|
|
return true, nil
|
|
} else if output == "disabled" || output == "not-found" {
|
|
return false, nil
|
|
}
|
|
return false, errors.New("Unexpected output '" + output + "'")
|
|
}
|
|
|
|
// setChronyOptions sets custom NTP server URLs in a chrony config file.
|
|
func (mgr *TemplateManager) setChronyOptions(handle *guestfs.Guestfs) (err error) {
|
|
possiblePaths := []string{"/etc/chrony.conf", "/etc/chrony/chrony.conf"}
|
|
var exists bool
|
|
var path string
|
|
var newLines []string
|
|
for _, path = range possiblePaths {
|
|
exists, err = handle.Is_file(path, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !exists {
|
|
continue
|
|
}
|
|
// comment out any lines beginning with "pool"
|
|
var oldLines []string
|
|
oldLines, err = handle.Read_lines(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
changed := false
|
|
newLines = make([]string, 0, len(oldLines))
|
|
for _, line := range oldLines {
|
|
if strings.HasPrefix(line, "pool ") {
|
|
newLines = append(newLines, "#"+line)
|
|
changed = true
|
|
} else {
|
|
newLines = append(newLines, line)
|
|
}
|
|
}
|
|
if changed {
|
|
newContent := strings.Join(newLines, "\n")
|
|
mgr.logger.Debug().Msg("Writing new content to " + path)
|
|
err = handle.Write(path, []byte(newContent))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
snippet := getResource("chrony-snippet")
|
|
|
|
// e.g. Debian
|
|
exists, err = handle.Is_dir("/etc/chrony/sources.d", nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if exists {
|
|
path := "/etc/chrony/sources.d/csclub.sources"
|
|
mgr.logger.Debug().Msg("Writing to " + path)
|
|
return handle.Write(path, snippet)
|
|
}
|
|
|
|
// e.g. OpenSUSE Tumbleweed
|
|
exists, err = handle.Is_dir("/etc/chrony.d", nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if exists {
|
|
mgr.logger.Debug().Msg("Removing /etc/chrony.d/pool.conf")
|
|
err = handle.Rm_f("/etc/chrony.d/pool.conf")
|
|
if err != nil {
|
|
return
|
|
}
|
|
path := "/etc/chrony.d/csclub.conf"
|
|
mgr.logger.Debug().Msg("Writing to " + path)
|
|
return handle.Write(path, snippet)
|
|
}
|
|
|
|
// Otherwise, assume we need to modify chrony.conf directly
|
|
// e.g. Fedora
|
|
if newLines == nil {
|
|
return errors.New("could not find chrony.conf")
|
|
}
|
|
serverDirectiveExists := false
|
|
for _, line := range newLines {
|
|
if strings.HasPrefix(line, "server ") {
|
|
serverDirectiveExists = true
|
|
break
|
|
}
|
|
}
|
|
if serverDirectiveExists {
|
|
// Assume that this was inserted by us during a previous run
|
|
return
|
|
}
|
|
newContent := string(snippet) + "\n" + strings.Join(newLines, "\n")
|
|
mgr.logger.Debug().Msg("Writing new content to " + path)
|
|
return handle.Write(path, []byte(newContent))
|
|
}
|
|
|
|
func (mgr *TemplateManager) setTimesyncdConf(handle *guestfs.Guestfs) error {
|
|
if err := handle.Mkdir_p("/etc/systemd/timesyncd.conf.d"); err != nil {
|
|
return fmt.Errorf("could not create /etc/systemd/timesyncd.conf.d: %w", err)
|
|
}
|
|
mgr.logger.Debug().Msg("Writing to /etc/systemd/timesyncd.conf.d/csclub.conf")
|
|
return handle.Write("/etc/systemd/timesyncd.conf.d/csclub.conf", getResource("timesyncd.conf"))
|
|
}
|
|
|
|
func (mgr *TemplateManager) usesChrony(handle *guestfs.Guestfs) (bool, error) {
|
|
// The systemd service for chrony is called on chrony.service on e.g. Debian,
|
|
// but is called chronyd.service on e.g. Fedora
|
|
chronyIsEnabled, err := mgr.systemdServiceIsEnabled(handle, "chrony.service")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if chronyIsEnabled {
|
|
return true, nil
|
|
}
|
|
return mgr.systemdServiceIsEnabled(handle, "chronyd.service")
|
|
}
|
|
|
|
func (mgr *TemplateManager) setupCommonNTPConfigs(handle *guestfs.Guestfs) error {
|
|
usesChrony, err := mgr.usesChrony(handle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if usesChrony {
|
|
return mgr.setChronyOptions(handle)
|
|
}
|
|
usesSystemdTimesyncd, err := mgr.systemdServiceIsEnabled(handle, "systemd-timesyncd.service")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if usesSystemdTimesyncd {
|
|
return mgr.setTimesyncdConf(handle)
|
|
}
|
|
return errors.New("could not determine NTP backend")
|
|
}
|
|
|
|
func (mgr *TemplateManager) setNetworkManagerOptions(handle *guestfs.Guestfs) error {
|
|
// We will force NetworkManager to use dhclient instead of its built-in DHCP
|
|
// client. This is necessary because cloud-init is unable to read the DHCP
|
|
// server identifier from NetworkManager's state files, and will always try
|
|
// to read dhclient lease files even if NetworkManager is being used
|
|
// https://github.com/canonical/cloud-init/blob/23.3.x/cloudinit/sources/DataSourceCloudStack.py#L233
|
|
dhclientIsPresent, err := handle.Is_file("/sbin/dhclient", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not determine if /sbin/dhclient is a file: %w", err)
|
|
}
|
|
if !dhclientIsPresent {
|
|
return errors.New("dhclient is not present")
|
|
}
|
|
if err = handle.Mkdir_p("/etc/NetworkManager/conf.d"); err != nil {
|
|
return err
|
|
}
|
|
snippetPath := "/etc/NetworkManager/conf.d/99_csclub.conf"
|
|
mgr.logger.Debug().Msg("Writing to " + snippetPath)
|
|
if err = handle.Write(snippetPath, getResource("network-manager-snippet")); err != nil {
|
|
return err
|
|
}
|
|
// Workaround for bug in cloud-init which prevents the NetworkManager
|
|
// backend from being selected if NetworkManager.conf does not exist
|
|
// https://github.com/canonical/cloud-init/blob/23.3.x/cloudinit/net/network_manager.py#L413
|
|
const cfgFilePath = "/etc/NetworkManager/NetworkManager.conf"
|
|
nmCfgIsPresent, err := handle.Is_file(cfgFilePath, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not determine if %s is a file: %w", cfgFilePath, err)
|
|
}
|
|
if !nmCfgIsPresent {
|
|
mgr.logger.Debug().Msg("Creating " + cfgFilePath)
|
|
if err = handle.Touch(cfgFilePath); err != nil {
|
|
return fmt.Errorf("Could not create %s: %w", cfgFilePath, err)
|
|
}
|
|
}
|
|
// A bug was introduced in cloud-init commit 5942f40 which causes the DHCP
|
|
// server identifier in the dhclient lease file to be ignored. It is fixed by
|
|
// https://github.com/canonical/cloud-init/commit/cb36bf38b823f811a3e938ccffc03d7d13190095,
|
|
// but as of this writing (2024-01-13), this fix is not present in any official
|
|
// distro packages yet.
|
|
// As a workaround, we will install systemd-resolved, whose NSS module
|
|
// nss-resolve has a higher priority than "dns" for the "hosts" database in
|
|
// nsswitch.conf. This allows cloud-init to resolve the "data-server" host name
|
|
// (by querying the CloudStack DNS server instead of the ones which we wrote in
|
|
// /etc/resolv.conf), so it will not need to read any DHCP lease files.
|
|
// We can remove this workaround once the aforementioned fix gets shipped to
|
|
// all of the distros which we support.
|
|
resolvedIsPresent, err := handle.Is_file("/lib/systemd/systemd-resolved", nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not determine if /lib/systemd/systemd-resolved is a file: %w", err)
|
|
}
|
|
if !resolvedIsPresent {
|
|
if err = mgr.impl.InstallSystemdResolved(handle); err != nil {
|
|
return err
|
|
}
|
|
if err = mgr.enableSystemdUnit(handle, "systemd-resolved"); err != nil {
|
|
return err
|
|
}
|
|
// Make sure that systemd-networkd isn't enabled or else it'll interfere
|
|
// with NetworkManager
|
|
if err = mgr.disableSystemdUnit(handle, "systemd-networkd"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (mgr *TemplateManager) usesNetworkManager(handle *guestfs.Guestfs) (bool, error) {
|
|
return mgr.systemdServiceIsEnabled(handle, "NetworkManager.service")
|
|
}
|
|
|
|
func (mgr *TemplateManager) usesEtcNetworkInterfaces(handle *guestfs.Guestfs) (bool, error) {
|
|
configPath := "/etc/network/interfaces"
|
|
configExists, err := handle.Is_file(configPath, nil)
|
|
if err != nil {
|
|
return false, fmt.Errorf("Could not determine if "+configPath+" is a file: %w", err)
|
|
}
|
|
if !configExists {
|
|
return false, nil
|
|
}
|
|
return mgr.systemdServiceIsEnabled(handle, "networking.service")
|
|
}
|
|
|
|
func (mgr *TemplateManager) addSystemdNetworkdCloudInitSnippet(handle *guestfs.Guestfs) error {
|
|
// Use 98 so that each distro can use 99 for extra customization
|
|
path := "/etc/cloud/cloud.cfg.d/98_csclub_network.cfg"
|
|
mgr.logger.Debug().Msg("Writing to " + path)
|
|
return handle.Write(path, getResource("systemd-networkd-cloud-init"))
|
|
}
|
|
|
|
func (mgr *TemplateManager) setupCommonNetworkConfigs(handle *guestfs.Guestfs) error {
|
|
usesNetworkManager, err := mgr.usesNetworkManager(handle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if usesNetworkManager {
|
|
return mgr.setNetworkManagerOptions(handle)
|
|
}
|
|
usesSystemdNetworkd, err := mgr.systemdServiceIsEnabled(handle, "systemd-networkd.service")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if usesSystemdNetworkd {
|
|
return mgr.addSystemdNetworkdCloudInitSnippet(handle)
|
|
}
|
|
usesEtcNetworkInterfaces, err := mgr.usesEtcNetworkInterfaces(handle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if usesEtcNetworkInterfaces {
|
|
return mgr.setDhclientOptions(handle)
|
|
}
|
|
return errors.New("Could not determine network backend")
|
|
}
|
|
|
|
func (mgr *TemplateManager) setupIpv6Scripts(handle *guestfs.Guestfs) (err error) {
|
|
log := mgr.logger
|
|
scripts := []string{"99_csclub_ipv6_addr.sh"}
|
|
usesNetworkManager, err := mgr.usesNetworkManager(handle)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if usesNetworkManager {
|
|
scripts = append(scripts, "98_csclub_disable_nm_ipv6.sh")
|
|
} else {
|
|
scripts = append(scripts, "98_csclub_disable_ipv6_ra.sh")
|
|
}
|
|
scriptDir := "/var/lib/cloud/scripts/per-boot"
|
|
if err = handle.Mkdir_p(scriptDir); err != nil {
|
|
return
|
|
}
|
|
for _, filename := range scripts {
|
|
path := scriptDir + "/" + filename
|
|
log.Debug().Msg("Writing to " + path)
|
|
if err = handle.Write(path, getResource(filename)); err != nil {
|
|
return
|
|
}
|
|
if err = handle.Chmod(0755, path); err != nil {
|
|
return
|
|
}
|
|
}
|
|
sysctlPath := "/etc/sysctl.d/csclub.conf"
|
|
log.Debug().Msg("Writing to " + sysctlPath)
|
|
return handle.Write(sysctlPath, getResource("sysctl.conf"))
|
|
}
|
|
|
|
func (mgr *TemplateManager) setResolvConf(handle *guestfs.Guestfs) (err error) {
|
|
mgr.logger.Debug().Msg("Writing to /etc/resolv.conf")
|
|
if err = handle.Rm_f("/etc/resolv.conf"); err != nil {
|
|
return
|
|
}
|
|
return handle.Write("/etc/resolv.conf", getResource("resolv.conf"))
|
|
}
|
|
|
|
func (mgr *TemplateManager) createAugeasHandle(handle *guestfs.Guestfs) (err error) {
|
|
mgr.logger.Debug().Msg("Creating a new Augeas handle")
|
|
return handle.Aug_init("/", 0)
|
|
}
|
|
|
|
func (mgr *TemplateManager) closeAugeasHandle(handle *guestfs.Guestfs) {
|
|
if err := handle.Aug_close(); err != nil {
|
|
mgr.logger.Error().Err(err).Msg("")
|
|
}
|
|
}
|
|
|
|
func (mgr *TemplateManager) saveAugeasValues(handle *guestfs.Guestfs) error {
|
|
mgr.logger.Debug().Msg("Saving Augeas values")
|
|
return handle.Aug_save()
|
|
}
|
|
|
|
// requires an Augeas handle to be open
|
|
func (mgr *TemplateManager) setDhclientOptions(handle *guestfs.Guestfs) (err error) {
|
|
mgr.logger.Debug().Msg("Retrieving dhclient request options")
|
|
dhclientRequestOptionNodes, err := handle.Aug_ls("/files/etc/dhcp/dhclient.conf/request")
|
|
if err != nil {
|
|
return
|
|
}
|
|
optionsToRemove := map[string]bool{
|
|
"domain-name": true,
|
|
"domain-name-servers": true,
|
|
"domain-search": true,
|
|
"dhcp6.name-servers": true,
|
|
"dhcp6.domain-search": true,
|
|
"dhcp6.fqdn": true,
|
|
"dhcp6.sntp-servers": true,
|
|
"ntp-servers": true,
|
|
"netbios-name-servers": true,
|
|
}
|
|
for _, optionPath := range dhclientRequestOptionNodes {
|
|
// optionPath will look something like /files/etc/dhcp/dhclient.conf/request/5
|
|
var optionValue string
|
|
optionValue, err = handle.Aug_get(optionPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, ok := optionsToRemove[optionValue]; !ok {
|
|
continue
|
|
}
|
|
mgr.logger.Debug().Msg("Removing dhclient request option " + optionValue)
|
|
if _, err = handle.Aug_rm(optionPath); err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func addAugeasComment(
|
|
handle *guestfs.Guestfs, numExistingComments int, parentNode string, comment string,
|
|
) error {
|
|
return handle.Aug_set(
|
|
parentNode+fmt.Sprintf("/#comment[%d]", numExistingComments),
|
|
comment,
|
|
)
|
|
}
|
|
|
|
func (mgr *TemplateManager) commentOutAugeasPath(
|
|
handle *guestfs.Guestfs, numExistingComments int, augeasPath string, commentedValue string,
|
|
) error {
|
|
lastSlashIndex := strings.LastIndex(augeasPath, "/")
|
|
if lastSlashIndex == -1 {
|
|
// sanity check
|
|
panic("augeasPath must have a slash")
|
|
}
|
|
parentNode := augeasPath[:lastSlashIndex]
|
|
mgr.logger.Debug().Msg("Commenting out " + augeasPath)
|
|
if _, err := handle.Aug_rm(augeasPath); err != nil {
|
|
return fmt.Errorf("Could not remove %s: %w", augeasPath, err)
|
|
}
|
|
if err := addAugeasComment(handle, numExistingComments, parentNode, commentedValue); err != nil {
|
|
return fmt.Errorf("Could not insert comment in %s: %w", parentNode, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// replaceYumRepoMirrorUrls comments out the metalink and mirrorlist URLs and
|
|
// replaces the baseurl URLs for each repo in /etc/yum.repos.d.
|
|
// It assumes that the baseurl will be present or commented.
|
|
//
|
|
// transformBaseUrl accepts a repo base URL and replaces the host (and
|
|
// optionally parts of the path) as appropriate. It should return the input
|
|
// if the URL should not be modified.
|
|
func (mgr *TemplateManager) replaceYumMirrorUrls(
|
|
handle *guestfs.Guestfs, transformBaseurl func(string) string,
|
|
) error {
|
|
repoPaths, err := handle.Aug_ls("/files/etc/yum.repos.d")
|
|
if err != nil {
|
|
return fmt.Errorf("Could not enumerate yum repos: %w", err)
|
|
}
|
|
// A repoPath looks like e.g. /files/etc/yum.repos.d/almalinux-appstream.repo
|
|
for _, repoPath := range repoPaths {
|
|
subrepoPaths, err := handle.Aug_ls(repoPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not enumerate subrepos for %s: %w", repoPath, err)
|
|
}
|
|
// A subrepoPath looks like e.g. /files/etc/yum.repos.d/almalinux-appstream.repo/appstream
|
|
for _, subrepoPath := range subrepoPaths {
|
|
if err = mgr.replaceYumMirrorUrlsForSingleSubrepo(handle, transformBaseurl, subrepoPath); err != nil {
|
|
return fmt.Errorf("Could not replace mirror URLs for %s: %w", subrepoPath, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (mgr *TemplateManager) replaceYumMirrorUrlsForSingleSubrepo(
|
|
handle *guestfs.Guestfs, transformBaseurl func(string) string, subrepoPath string,
|
|
) (err error) {
|
|
log := mgr.logger
|
|
// subrepoPath looks like e.g. /files/etc/yum.repos.d/almalinux-appstream.repo/appstream
|
|
keyPaths, err := handle.Aug_ls(subrepoPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not enumerate keys for %s: %w", subrepoPath, err)
|
|
}
|
|
var (
|
|
numComments int
|
|
commentedBaseurl string
|
|
mirrorlist string
|
|
metalink string
|
|
uncommentedBaseurl string
|
|
)
|
|
// A keyPath looks like e.g. /files/etc/yum.repos.d/almalinux-appstream.repo/appstream/mirrorlist
|
|
for _, keyPath := range keyPaths {
|
|
// extract the last part of the node path
|
|
key, err := handle.Aug_label(keyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not get label of %s: %w", keyPath, err)
|
|
}
|
|
value, err := handle.Aug_get(keyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not get %s: %w", keyPath, err)
|
|
}
|
|
if strings.HasPrefix(key, "#comment") {
|
|
numComments += 1
|
|
if strings.HasPrefix(value, "baseurl=") {
|
|
commentedBaseurl = strings.Split(value, "=")[1]
|
|
}
|
|
} else if key == "mirrorlist" {
|
|
mirrorlist = value
|
|
} else if key == "metalink" {
|
|
metalink = value
|
|
} else if key == "baseurl" {
|
|
uncommentedBaseurl = value
|
|
}
|
|
}
|
|
var baseurl string
|
|
if uncommentedBaseurl != "" {
|
|
baseurl = transformBaseurl(uncommentedBaseurl)
|
|
} else if commentedBaseurl != "" {
|
|
baseurl = transformBaseurl(commentedBaseurl)
|
|
}
|
|
if baseurl == "" {
|
|
return
|
|
}
|
|
log.Debug().Msg(fmt.Sprintf("Setting %s to %s", subrepoPath+"/baseurl", baseurl))
|
|
if err = handle.Aug_set(subrepoPath+"/baseurl", baseurl); err != nil {
|
|
return fmt.Errorf("Could not set baseurl for %s: %w", subrepoPath, err)
|
|
}
|
|
if mirrorlist != "" {
|
|
if err = mgr.commentOutAugeasPath(handle, numComments, subrepoPath+"/mirrorlist", "mirrorlist="+mirrorlist); err != nil {
|
|
return
|
|
}
|
|
numComments += 1
|
|
}
|
|
if metalink != "" {
|
|
if err = mgr.commentOutAugeasPath(handle, numComments, subrepoPath+"/metalink", "metalink="+metalink); err != nil {
|
|
return
|
|
}
|
|
numComments += 1
|
|
}
|
|
return
|
|
}
|
|
|
|
func (mgr *TemplateManager) dnfRemoveUnnecessaryPackages(handle *guestfs.Guestfs) (err error) {
|
|
// SSSD is unnecessary in single-user environments and consumes a lot of resources.
|
|
// auditd spams the system log and uses lots of disk IO.
|
|
// bluez is unnecessary on servers.
|
|
args := []string{"dnf", "-C", "remove", "-y", "sssd-common", "audit", "bluez"}
|
|
_, err = mgr.logAndRunCommand(handle, args)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Now that we removed SSSD, we also have to make sure that it's not being used in PAM.
|
|
// The way to do this on Fedora (and likely other RHEL-based distros) is with authselect.
|
|
args = []string{"authselect", "select", "minimal"}
|
|
_, err = mgr.logAndRunCommand(handle, args)
|
|
return err
|
|
}
|
|
|
|
// requires an Augeas handle to be open
|
|
func (mgr *TemplateManager) updateSshdConfig(handle *guestfs.Guestfs) error {
|
|
// WARNING: do NOT create /etc/ssh/sshd_config if it does not exist!
|
|
// On some distros, like OpenSUSE Tumbleweed, sshd_config does not exist,
|
|
// and creating a new file will actually prevent cloud-init from copying
|
|
// the user's SSH key to the default user's authorized_keys.
|
|
const dropinDir = "/etc/ssh/sshd_config.d"
|
|
mgr.logger.Debug().Msg("Creating " + dropinDir)
|
|
err := handle.Mkdir_p(dropinDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filepath := dropinDir + "/csclub.conf"
|
|
mgr.logger.Debug().Msg("Writing to " + filepath)
|
|
return handle.Write(filepath, []byte("PrintLastLog no\n"))
|
|
}
|
|
|
|
func (mgr *TemplateManager) setJournaldConf(handle *guestfs.Guestfs) (err error) {
|
|
mgr.logger.Debug().Msg("Writing custom journald.conf")
|
|
if err = handle.Mkdir_p("/etc/systemd/journald.conf.d"); err != nil {
|
|
return
|
|
}
|
|
return handle.Write("/etc/systemd/journald.conf.d/csclub.conf", getResource("journald.conf"))
|
|
}
|
|
|
|
func (mgr *TemplateManager) setMotd(handle *guestfs.Guestfs) error {
|
|
mgr.logger.Debug().Msg("Writing to /etc/motd")
|
|
return handle.Write("/etc/motd", getResource("motd"))
|
|
}
|