cloudbuild/pkg/distros/template_manager.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"))
}