install systemd-networkd as a fallback
This commit is contained in:
parent
8e21099f1b
commit
c0ca2cd1c5
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue