install systemd-networkd as a fallback

This commit is contained in:
Max Erenberg 2023-11-14 10:10:08 -05:00
parent 8e21099f1b
commit c0ca2cd1c5
2 changed files with 297 additions and 75 deletions

View File

@ -1,7 +1,14 @@
package distros
import (
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/rs/zerolog/log"
@ -39,45 +46,148 @@ func (mgr *OpensuseTumbleweedTemplateManager) DownloadTemplate(version, codename
)
}
func (mgr *OpensuseTumbleweedTemplateManager) setSysconfigNetworkParams(handle *guestfs.Guestfs) error {
path := "/etc/sysconfig/network/config"
// Make sure you don't override NETCONFIG_DNS_POLICY; the DNS server IP address
// obtained from DHCP is needed for cloud-init to work.
params := map[string]string{
"NETCONFIG_NTP_POLICY": "",
"AUTO6_WAIT_AT_BOOT": "0",
"AUTO6_UPDATE": "none",
const ossPackagesBaseURL = "https://download.opensuse.org/tumbleweed/repo/oss"
func (mgr *OpensuseTumbleweedTemplateManager) getSystemdNetworkPackageURL() (string, error) {
const indexURL = ossPackagesBaseURL + "/INDEX.gz"
mgr.logger.Debug().Msg("Downloading " + indexURL)
resp, err := http.Get(indexURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return "", err
}
defer gzipReader.Close()
body, err := io.ReadAll(gzipReader)
if err != nil {
return "", err
}
pattern := regexp.MustCompile(`(?m)^\./x86_64/systemd-network-(?:[\d.-]+)\.x86_64\.rpm$`)
match := pattern.Find(body)
if match == nil {
return "", errors.New("Could not find systemd-network package in OpenSUSE Tumbleweed index")
}
packageURL := ossPackagesBaseURL + "/" + string(match[2:])
return packageURL, nil
}
func (mgr *OpensuseTumbleweedTemplateManager) downloadSystemdNetworkPackage() (string, error) {
packageURL, err := mgr.getSystemdNetworkPackageURL()
if err != nil {
return "", err
}
mgr.logger.Debug().Msg("Downloading " + packageURL)
resp, err := http.Get(packageURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
// TODO: download to somewhere in /tmp
const path = "./systemd-network.rpm"
out, err := os.Create(path)
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return "", err
}
return path, nil
}
func (mgr *OpensuseTumbleweedTemplateManager) logAndRunCommand(handle *guestfs.Guestfs, args []string) error {
mgr.logger.Debug().Msg("Running command `" + strings.Join(args, " ") + "`")
_, err := handle.Command(args)
return err
}
func (mgr *OpensuseTumbleweedTemplateManager) addSystemUserAndGroup(handle *guestfs.Guestfs, name, uid, gid string) error {
matches, err := handle.Grep("^"+name, "/etc/group", nil)
if err != nil {
return fmt.Errorf("Could not grep /etc/group: %w", err)
}
if len(matches) == 0 {
err = mgr.logAndRunCommand(handle, []string{
"groupadd",
"--system",
"--gid", gid,
name,
})
if err != nil {
return err
}
}
// Unfortunately this causes Aug_save to fail later. I'm not sure why.
// The error message just says "aug_save: No error".
//for key, value := range params {
// augPath := "/files" + path + "/ + key
// if err := handle.Aug_set(augPath, value); err != nil {
// return err
// }
//}
matches, err = handle.Grep("^"+name, "/etc/passwd", nil)
if err != nil {
return fmt.Errorf("Could not grep /etc/passwd: %w", err)
}
if len(matches) == 0 {
err = mgr.logAndRunCommand(handle, []string{
"useradd",
"--system",
"--no-user-group",
"--uid", uid,
"--gid", gid,
"--home-dir", "/run/systemd",
"--shell", "/usr/sbin/nologin",
name,
})
if err != nil {
return err
}
}
return nil
}
content, err := handle.Cat(path)
// Workaround for bug in VM image: systemd-network user and group are not
// present, and are not created when the systemd-network package is installed
// Also see: https://github.com/lima-vm/lima/issues/1496
//
// https://bugzilla.opensuse.org/show_bug.cgi?id=1203393
func (mgr *OpensuseTumbleweedTemplateManager) addSystemdNetworkUsersAndGroups(handle *guestfs.Guestfs) error {
// UIDs and GIDs were determined by installing systemd-network in
// a VM booted from a live ISO image
if err := mgr.addSystemUserAndGroup(handle, "systemd-network", "470", "470"); err != nil {
return err
}
if err := mgr.addSystemUserAndGroup(handle, "systemd-resolve", "469", "469"); err != nil {
return err
}
return nil
}
func (mgr *OpensuseTumbleweedTemplateManager) InstallSystemdNetworkd(handle *guestfs.Guestfs) error {
localPath, err := mgr.downloadSystemdNetworkPackage()
if err != nil {
return err
}
lines := strings.Split(content, "\n")
replacedLines := make([]string, len(lines))
for i, line := range lines {
for key, value := range params {
if strings.HasPrefix(line, key+"=") {
mgr.logger.Debug().
Str("path", path).
Msg("Setting " + key + "=" + value)
replacedLines[i] = key + "=" + value
} else {
replacedLines[i] = line
}
}
mgr.logger.Debug().Msg("Copying " + localPath + " to /tmp in VM")
err = handle.Copy_in(localPath, "/tmp")
if err != nil {
return err
}
replacedContent := strings.Join(replacedLines, "\n")
return handle.Write(path, []byte(replacedContent))
mgr.logger.Debug().Msg("Deleting " + localPath)
err = os.Remove(localPath)
if err != nil {
return err
}
vmPath := "/tmp/" + filepath.Base(localPath)
args := []string{"zypper", "--no-remote", "--non-interactive", "install", vmPath}
err = mgr.logAndRunCommand(handle, args)
if err != nil {
return err
}
mgr.logger.Debug().Msg("Deleting " + vmPath + " in VM")
err = handle.Rm(vmPath)
if err != nil {
return err
}
return mgr.addSystemdNetworkUsersAndGroups(handle)
}
func (mgr *OpensuseTumbleweedTemplateManager) NeedsDynamicResolvConf() bool {
@ -97,14 +207,14 @@ func (mgr *OpensuseTumbleweedTemplateManager) CommandToUpdatePackageCache() []st
}
func (mgr *OpensuseTumbleweedTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
if err = mgr.maskSystemdUnit(handle, "wickedd-dhcp6.service"); err != nil {
unitPath, err := getSystemdUnitPath(handle, "wickedd.service")
if err != nil {
return
}
if err = mgr.setSysconfigNetworkParams(handle); err != nil {
return
if unitPath != "" {
// OpenSUSE Tumbleweed images should be using NetworkManager
// or systemd-networkd (after we install it)
return errors.New("wickedd.service was unexpectedly found")
}
if err = mgr.removeWelcomeMessage(handle); err != nil {
return
}
return
return mgr.removeWelcomeMessage(handle)
}

View File

@ -70,6 +70,10 @@ type IDistroSpecificTemplateManager interface {
// (but does not upgrade) the packages for this distro, e.g.
// "sudo apt update".
CommandToUpdatePackageCache() []string
// InstallSystemdNetworkd installs the OS-specific package for
// systemd-networkd. Since the VM does not have network access, this
// will require downloading the package and copying it into the VM.
InstallSystemdNetworkd(handle *guestfs.Guestfs) error
}
func (mgr *TemplateManager) NeedsDynamicResolvConf() bool {
@ -78,6 +82,11 @@ func (mgr *TemplateManager) NeedsDynamicResolvConf() bool {
return false
}
func (mgr *TemplateManager) InstallSystemdNetworkd(handle *guestfs.Guestfs) error {
// Individual distros should override this method if necessary
return errors.New("InstallSystemdNetworkd: 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")
@ -268,60 +277,143 @@ func isSameFile(handle *guestfs.Guestfs, path1, path2 string) (bool, error) {
return stat1.Ino == stat2.Ino, nil
}
func systemdServiceIsEnabled(handle *guestfs.Guestfs, unit string) (bool, error) {
dirPrefixes := []string{"/etc", "/lib"}
unitPath := ""
for _, dirPrefix := range dirPrefixes {
var systemdDirPrefixes = []string{"/etc", "/lib"}
func getSystemdUnitPath(handle *guestfs.Guestfs, unit string) (string, error) {
for _, dirPrefix := range systemdDirPrefixes {
possibleUnitPath := dirPrefix + "/systemd/system/" + unit
exists, err := handle.Is_file(possibleUnitPath, nil)
if err != nil {
return false, fmt.Errorf("could not determine if %s is a file: %w", possibleUnitPath, err)
return "", fmt.Errorf("could not determine if %s is a file: %w", possibleUnitPath, err)
}
if exists {
unitPath = possibleUnitPath
break
return possibleUnitPath, nil
}
}
if unitPath == "" {
return false, nil
}
return "", nil
}
func getSystemdServiceWantedBy(handle *guestfs.Guestfs, unitPath string) (systemdUnitInstallInfo, error) {
installInfo := systemdUnitInstallInfo{}
content, err := handle.Cat(unitPath)
if err != nil {
return false, fmt.Errorf("could not read %s: %w", unitPath, err)
return installInfo, fmt.Errorf("could not read %s: %w", unitPath, err)
}
lines := strings.Split(content, "\n")
wantedBy := ""
for _, line := range lines {
if strings.HasPrefix(line, "WantedBy=") {
wantedBy = line[len("WantedBy="):]
break
if installInfo.WantedBy != "" {
return installInfo, fmt.Errorf("multiple WantedBy directives found in %s", unitPath)
}
installInfo.WantedBy = line[len("WantedBy="):]
} else if strings.HasPrefix(line, "Also=") {
installInfo.Also = append(installInfo.Also, line[len("Also="):])
}
}
if wantedBy == "" {
return false, fmt.Errorf("could not find WantedBy property for %s", unitPath)
return installInfo, nil
}
type systemdUnitInstallInfo struct {
WantedBy string
Also []string
}
type systemdUnitInfo struct {
UnitPath string
Enabled bool
systemdUnitInstallInfo
}
// Returns (nil, nil) if the unit does not exist
func getSystemdUnitInfo(handle *guestfs.Guestfs, unit string) (*systemdUnitInfo, error) {
unitPath, err := getSystemdUnitPath(handle, unit)
if err != nil || unitPath == "" {
return nil, err
}
installInfo, err := getSystemdServiceWantedBy(handle, unitPath)
if err != nil {
return nil, err
}
if installInfo.WantedBy == "" {
return nil, fmt.Errorf("could not find WantedBy property for %s", unitPath)
}
linkName := ""
for _, dirPrefix := range dirPrefixes {
possibleLinkName := dirPrefix + "/systemd/system/" + wantedBy + ".wants/" + unit
for _, dirPrefix := range systemdDirPrefixes {
possibleLinkName := dirPrefix + "/systemd/system/" + installInfo.WantedBy + ".wants/" + unit
isSymlink, err := handle.Is_symlink(possibleLinkName)
if err != nil {
return false, fmt.Errorf("could not determine if %s is a symlink: %w", possibleLinkName, err)
return nil, fmt.Errorf("could not determine if %s is a symlink: %w", possibleLinkName, err)
}
if isSymlink {
linkName = possibleLinkName
break
}
}
if linkName == "" {
return false, nil
unitInfo := &systemdUnitInfo{
UnitPath: unitPath,
systemdUnitInstallInfo: installInfo,
}
target, err := handle.Readlink(linkName)
if linkName != "" {
target, err := handle.Readlink(linkName)
if err != nil {
return nil, fmt.Errorf("could not read link at %s: %w", linkName, err)
}
// The target might be in /usr/lib, which will likely be a symlink
// to /lib. So we can't compare the paths directly.
targetIsUnitFile, err := isSameFile(handle, target, unitPath)
if err != nil {
return nil, err
}
if !targetIsUnitFile {
return nil, fmt.Errorf("%s is not the same as %s", target, unitPath)
}
unitInfo.Enabled = true
}
return unitInfo, nil
}
func (mgr *TemplateManager) enableSystemdUnit(handle *guestfs.Guestfs, unit string) error {
unitInfo, err := getSystemdUnitInfo(handle, unit)
if err != nil {
return false, fmt.Errorf("could not read link at %s: %w", linkName, err)
return err
}
// The target might be in /usr/lib, which will likely be a symlink
// to /lib. So we can't compare the paths directly.
return isSameFile(handle, target, unitPath)
if unitInfo == nil {
return fmt.Errorf("Unit %s does not exist", unit)
}
if unitInfo.Enabled {
return nil
}
if unitInfo.WantedBy == "" {
return fmt.Errorf("%s does not have WantedBy directive", unit)
}
wantsDir := "/etc/systemd/system/" + unitInfo.WantedBy + ".wants"
mgr.logger.Debug().Msg("mkdir -p " + wantsDir)
err = handle.Mkdir_p(wantsDir)
if err != nil {
return err
}
linkPath := wantsDir + "/" + unit
mgr.logger.Debug().Msg("ln -sf " + unitInfo.UnitPath + " " + linkPath)
err = handle.Ln_sf(unitInfo.UnitPath, linkPath)
if err != nil {
return err
}
unitInfo.Enabled = true
for _, also := range unitInfo.Also {
err = mgr.enableSystemdUnit(handle, also)
if err != nil {
return err
}
}
return nil
}
func systemdServiceIsEnabled(handle *guestfs.Guestfs, unit string) (bool, error) {
unitInfo, err := getSystemdUnitInfo(handle, unit)
if err != nil || unitInfo == nil {
return false, err
}
return unitInfo.Enabled, nil
}
// setChronyOptions sets custom NTP server URLs in a chrony config file.
@ -495,6 +587,20 @@ func (mgr *TemplateManager) addSystemdNetworkdCloudInitSnippet(handle *guestfs.G
return handle.Write(path, getResource("systemd-networkd-cloud-init"))
}
func (mgr *TemplateManager) installAndEnableSystemdNetworkd(handle *guestfs.Guestfs) error {
isInstalled, err := handle.Is_file("/lib/systemd/system/systemd-networkd.service", nil)
if err != nil {
return err
}
if !isInstalled {
err = mgr.impl.InstallSystemdNetworkd(handle)
if err != nil {
return err
}
}
return mgr.enableSystemdUnit(handle, "systemd-networkd.service")
}
func (mgr *TemplateManager) setupCommonNetworkConfigs(handle *guestfs.Guestfs) error {
usesNetworkManager, err := usesNetworkManager(handle)
if err != nil {
@ -525,7 +631,12 @@ func (mgr *TemplateManager) setupCommonNetworkConfigs(handle *guestfs.Guestfs) e
// Handled separately in opensuse_tumbleweed.go
return nil
}
return fmt.Errorf("Could not determine network backend")
mgr.logger.Info().Msg("Could not determine network backend, resorting to installing systemd-networkd")
err = mgr.installAndEnableSystemdNetworkd(handle)
if err != nil {
return err
}
return mgr.addSystemdNetworkdCloudInitSnippet(handle)
}
func (mgr *TemplateManager) setupIpv6Scripts(handle *guestfs.Guestfs) (err error) {
@ -810,18 +921,19 @@ func (mgr *TemplateManager) dnfRemoveUnnecessaryPackages(handle *guestfs.Guestfs
// requires an Augeas handle to be open
func (mgr *TemplateManager) updateSshdConfig(handle *guestfs.Guestfs) error {
sshdConfigExists, err := handle.Is_file("/etc/ssh/sshd_config", nil)
// 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
}
if !sshdConfigExists {
// 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.
return nil
}
mgr.logger.Debug().Msg("Setting PrintLastLog=no in sshd_config")
return handle.Aug_set("/files/etc/ssh/sshd_config/PrintLastLog", "no")
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) {