verify VMs and delete old templates

master
Max Erenberg 2 months ago
parent 1f2428f804
commit 77d9fd3865
  1. 1
      main.go
  2. 69
      pkg/cloudbuilder/cloudbuilder.go
  3. 22
      pkg/cloudbuilder/email-message.txt
  4. 24
      pkg/cloudbuilder/template_deletion.go
  5. 161
      pkg/cloudbuilder/vm_verifier.go
  6. 106
      pkg/cloudstack/cloudstack.go
  7. 15
      pkg/config/config.go
  8. 6
      pkg/distros/almalinux.go
  9. 4
      pkg/distros/debian.go
  10. 6
      pkg/distros/fedora.go
  11. 187
      pkg/distros/template_manager.go
  12. 6
      pkg/distros/ubuntu.go

@ -19,7 +19,6 @@ func main() {
setupLogging()
cfg := config.New()
builder := cloudbuilder.New(cfg)
if err := builder.Start(); err != nil {
panic(err)
}

@ -99,7 +99,7 @@ func (c *CloudBuilder) CreateVM(
if err != nil {
return
}
const pollInterval = 10 * time.Second
const pollInterval = 20 * time.Second
// Due to clock drift, we may end up waiting a bit longer than this
const maxTimeToWait = 30 * time.Minute
var timeWaited time.Duration = 0
@ -132,9 +132,14 @@ type DistroInfo struct {
user string
}
func (c *CloudBuilder) getExistingTemplateVersions() map[string]string {
type TemplateVersionInfo struct {
TemplateId string
Version string
}
func (c *CloudBuilder) getExistingTemplateVersions() map[string]TemplateVersionInfo {
templatePattern := regexp.MustCompile("^CSC (?P<distro>[A-Za-z][A-Za-z ]+[A-Za-z]) (?P<version>\\d+(\\.\\d+)?)$")
mostRecentVersions := make(map[string]string)
mostRecentVersions := make(map[string]TemplateVersionInfo)
templates := c.client.ListTemplates()
for _, template := range templates {
submatches := templatePattern.FindStringSubmatch(template.Name)
@ -146,23 +151,22 @@ func (c *CloudBuilder) getExistingTemplateVersions() map[string]string {
version := submatches[2]
log.Debug().Str("distro", distro).Str("version", version).Msg("Found template")
otherVersion, ok := mostRecentVersions[distro]
if !ok || version > otherVersion {
mostRecentVersions[distro] = version
if !ok || version > otherVersion.Version {
mostRecentVersions[distro] = TemplateVersionInfo{
TemplateId: template.Id,
Version: version,
}
}
}
return mostRecentVersions
}
func (c *CloudBuilder) sendEmailNotification(
templateName string, vm *cloudstack.VirtualMachine, vmUser string,
) (err error) {
func (c *CloudBuilder) sendEmailNotification(templateName string) (err error) {
tmpl := template.Must(template.New("email-message").Parse(emailMessageTemplate))
data := map[string]interface{}{
"cfg": c.cfg,
"date": time.Now().Format(time.RFC1123Z),
"templateName": templateName,
"vm": vm,
"vmUser": vmUser,
}
var buf bytes.Buffer
if err = tmpl.Execute(&buf, data); err != nil {
@ -200,6 +204,15 @@ func (c *CloudBuilder) createNewTemplate(
return
}
func (c *CloudBuilder) destroyVirtualMachine(vmID string) error {
log.Info().Str("id", vmID).Msg("Deleting VM")
deletionJobID, err := c.client.DestroyVirtualMachine(vmID)
if err != nil {
return err
}
return c.client.WaitForJobToComplete(deletionJobID)
}
func (c *CloudBuilder) versionStringCompare(version1, version2 string) int {
f1, err := strconv.ParseFloat(version1, 32)
if err != nil {
@ -247,7 +260,8 @@ func (c *CloudBuilder) Start() (err error) {
user: "almalinux",
},
}
existingVersions := c.getExistingTemplateVersions()
existingTemplates := c.getExistingTemplateVersions()
newVersions := make(map[string]string)
for _, distroLower := range c.cfg.DistrosToCheck {
distroInfo, ok := distrosInfo[distroLower]
if !ok {
@ -262,7 +276,8 @@ func (c *CloudBuilder) Start() (err error) {
}
log.Debug().Str("newVersion", newVersion).Str("codename", codename).Msg(distro)
curVersion, ok := existingVersions[distro]
curTemplate, ok := existingTemplates[distro]
curVersion := curTemplate.Version
if ok && c.versionStringCompare(newVersion, curVersion) <= 0 {
log.Debug().
Str("distro", distro).
@ -270,9 +285,11 @@ func (c *CloudBuilder) Start() (err error) {
Msg("Existing version is up to date, skipping")
continue
}
newVersions[distro] = newVersion
log.Info().
Str("distro", distroLower).
Msg("Existing template is out of date, creating a new one")
Msg("Template is nonexistent or out of date, creating a new one")
var template *cloudstack.Template
template, err = c.createNewTemplate(newVersion, codename, &distroInfo)
@ -280,6 +297,7 @@ func (c *CloudBuilder) Start() (err error) {
return
}
// Create a VM using the new template
vmName := strings.Join([]string{
strings.ReplaceAll(distroLower, " ", "-"),
strings.ReplaceAll(newVersion, ".", "-"),
@ -291,7 +309,30 @@ func (c *CloudBuilder) Start() (err error) {
return
}
if err = c.sendEmailNotification(template.Name, vm, distroInfo.user); err != nil {
// Make sure that everything is working properly in the VM
verifier := NewVMVerifier(c.cfg, distroInfo.user, vm.Nic[0].IpAddress, vmName, distroInfo.manager)
if err = verifier.Verify(); err != nil {
return
}
// Since we don't need the VM anymore, delete it
if err = c.destroyVirtualMachine(vm.Id); err != nil {
return
}
vm = nil
// When we originally created the template, it wasn't public,
// so make it public now
if err = c.client.MakeTemplatePublicAndFeatured(template.Id); err != nil {
return
}
if err = c.sendEmailNotification(template.Name); err != nil {
return
}
}
if c.cfg.DeleteOldTemplates {
if err = c.DeleteOldTemplates(existingTemplates, newVersions); err != nil {
return
}
}

@ -10,24 +10,12 @@ This is an automated message from cloudbuild, the CSC VM template
builder.
A new VM template, {{ .templateName }}, has been uploaded to CloudStack.
It is not public. A new VM, {{ .vm.Name }}, has been created from this
template. You can SSH into this VM from biloba or chamomile by running
the following:
It is public and featured. If you have a chance, please create a new VM
from this template and test it out to make sure everything is working
properly.
ssh -i /var/lib/cloudstack/management/.ssh/id_rsa {{ .vmUser }}@{{ (index .vm.Nic 0).IpAddress }}
Please login to the VM and verify that everything is working correctly.
Once you have done this, please login to CloudStack with the admin account
and perform the following:
1. Delete the VM (enable the "Expunge" option too).
2. Make the template public:
From the web page for the template, click the "Update Template Sharing"
circular button in the top right corner, and toggle the "Public"
and "Featured" sliders.
3. Delete the old template (if it is not being used by any VMs):
From the web page for the old template, click the "Zones" tab, then
press the red circular "Delete" button beside "{{ .cfg.CloudstackZoneName }}".
If you have any issues with cloudbuild, please report them here:
https://git.csclub.uwaterloo.ca/cloud/cloudbuild/issues
Sincerely,
cloudbuild

@ -0,0 +1,24 @@
package cloudbuilder
import "github.com/rs/zerolog/log"
func (c *CloudBuilder) DeleteOldTemplates(oldTemplates map[string]TemplateVersionInfo, newVersions map[string]string) error {
for distro, _ := range newVersions {
oldTemplate, ok := oldTemplates[distro]
if !ok {
continue
}
log.Info().
Str("oldVersion", oldTemplate.Version).
Str("distro", distro).
Msg("Deleting template")
jobID, err := c.client.DeleteTemplate(oldTemplate.TemplateId)
if err != nil {
return err
}
if err = c.client.WaitForJobToComplete(jobID); err != nil {
return err
}
}
return nil
}

@ -0,0 +1,161 @@
package cloudbuilder
import (
"bytes"
"encoding/json"
"fmt"
"net"
"os/exec"
"strings"
"time"
"github.com/rs/zerolog/log"
"git.csclub.uwaterloo.ca/cloud/cloudbuild/pkg/config"
"git.csclub.uwaterloo.ca/cloud/cloudbuild/pkg/distros"
)
type VMVerifier struct {
cfg *config.Config
user string
ipAddress string
hostname string
templateManager distros.ITemplateManager
}
func NewVMVerifier(
cfg *config.Config,
user string,
ipAddress string,
hostname string,
templateManager distros.ITemplateManager,
) *VMVerifier {
return &VMVerifier{
cfg: cfg,
user: user,
ipAddress: ipAddress,
hostname: hostname,
templateManager: templateManager,
}
}
func waitUntilPort22IsOpen(ipAddress string) error {
const maxTimeToWait = 5 * time.Minute
const retryInterval = 5 * time.Second
const maxTries = int(maxTimeToWait / retryInterval)
address := ipAddress + ":22"
connected := false
for i := 0; i < maxTries; i++ {
_, err := net.DialTimeout("tcp", address, retryInterval)
if err == nil {
connected = true
break
}
if err.(*net.OpError).Timeout() {
log.Debug().Str("address", address).Msg("TCP connection timed out")
} else {
log.Debug().Str("address", address).Msg(err.Error())
time.Sleep(retryInterval)
}
}
if !connected {
return fmt.Errorf("Could not connect to %s", address)
}
return nil
}
func (v *VMVerifier) prepareSSHCommand(args ...string) *exec.Cmd {
log.Debug().
Str("user", v.user).
Str("address", v.ipAddress).
Msg("Running `" + strings.Join(args, " ") + "`")
args = append(
[]string{
"-i", v.cfg.SSHKeyPath,
"-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=no",
"-o", "CheckHostIP=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR",
v.user + "@" + v.ipAddress,
},
args...,
)
return exec.Command("ssh", args...)
}
func (v *VMVerifier) runSSHCommand(args ...string) error {
return v.prepareSSHCommand(args...).Run()
}
func (v *VMVerifier) verifyThatVMCanResolveItsOwnHostname() error {
return v.runSSHCommand("ping", "-c", "1", "-w", "1", v.hostname)
}
func (v *VMVerifier) verifyThatSLAACandRAareDisabled() error {
output, err := v.prepareSSHCommand("ip", "-6", "addr", "show").Output()
if err != nil {
return err
}
if bytes.Contains(output, []byte("dynamic")) {
log.Debug().
Str("address", v.ipAddress).
Msg("IPv6 SLAAC address detected:\n" + string(output))
return fmt.Errorf("IPv6 SLAAC address detected")
}
output, err = v.prepareSSHCommand("ip", "-6", "route", "show").Output()
if err != nil {
return err
}
if bytes.Contains(output, []byte("proto ra")) {
log.Debug().
Str("address", v.ipAddress).
Msg("IPv6 RA route detected:\n" + string(output))
return fmt.Errorf("IPv6 RA route detected")
}
return nil
}
func (v *VMVerifier) verifyThatPackageCacheCanBeUpdated() error {
return v.runSSHCommand(v.templateManager.CommandToUpdatePackageCache()...)
}
func (v *VMVerifier) verifyThatCloudInitServicesDidNotFail() error {
output, err := v.prepareSSHCommand("systemctl", "list-units", "--state=failed", "-o", "json").Output()
if err != nil {
return err
}
var data []struct {
Unit string `json:"unit"`
Active string `json:"active"`
}
if err = json.Unmarshal(output, &data); err != nil {
return err
}
for _, unit := range data {
if strings.HasPrefix(unit.Unit, "cloud-") && unit.Active == "failed" {
return fmt.Errorf("unit %s failed", unit.Unit)
}
}
return nil
}
func (v *VMVerifier) Verify() (err error) {
log.Debug().Str("user", v.user).Str("address", v.ipAddress).Msg("Verifying VM")
if err = waitUntilPort22IsOpen(v.ipAddress); err != nil {
return
}
if err = v.verifyThatVMCanResolveItsOwnHostname(); err != nil {
return
}
if err = v.verifyThatSLAACandRAareDisabled(); err != nil {
return
}
if err = v.verifyThatPackageCacheCanBeUpdated(); err != nil {
return
}
if err = v.verifyThatCloudInitServicesDidNotFail(); err != nil {
return
}
return
}

@ -19,6 +19,9 @@ import (
"net/url"
"sort"
"strings"
"time"
"github.com/rs/zerolog/log"
"git.csclub.uwaterloo.ca/cloud/cloudbuild/pkg/config"
)
@ -330,6 +333,49 @@ func (client *CloudstackClient) RegisterTemplate(name string, downloadUrl string
return &data.Template[0], nil
}
type UpdateTemplatePermissionsResponse struct {
ErrorInfo
Success bool `json:"success"`
}
func (client *CloudstackClient) MakeTemplatePublicAndFeatured(templateID string) error {
url := client.createURL(map[string]string{
"command": "updateTemplatePermissions",
"id": templateID,
"ispublic": "true",
"isfeatured": "true",
})
responseWrapper := struct {
Response UpdateTemplatePermissionsResponse `json:"updatetemplatepermissionsresponse"`
}{}
getDeserializedResponse(url, &responseWrapper)
data := &responseWrapper.Response
checkErrorInfo(&data.ErrorInfo)
if !data.Success {
return fmt.Errorf("updateTemplatePermissions failed")
}
return nil
}
type DeleteTemplateResponse struct {
ErrorInfo
JobId string `json:"jobid"`
}
func (client *CloudstackClient) DeleteTemplate(templateID string) (jobID string, err error) {
url := client.createURL(map[string]string{
"command": "deleteTemplate",
"id": templateID,
})
responseWrapper := struct {
Response DeleteTemplateResponse `json:"deletetemplateresponse"`
}{}
getDeserializedResponse(url, &responseWrapper)
data := &responseWrapper.Response
checkErrorInfo(&data.ErrorInfo)
return data.JobId, nil
}
type VirtualMachine struct {
Id string `json:"id"`
DisplayName string `json:"displayname"`
@ -377,6 +423,66 @@ func (client *CloudstackClient) DeployVirtualMachine(
return data.Id, nil
}
type DestroyVirtualMachineResponse struct {
ErrorInfo
JobId string `json:"jobid"`
}
// DestroyVirtualMachine returns the job ID of the job which deletes the
// VM asynchronously.
func (client *CloudstackClient) DestroyVirtualMachine(vmID string) (jobID string, err error) {
url := client.createURL(map[string]string{
"command": "destroyVirtualMachine",
"id": vmID,
"expunge": "true",
})
responseWrapper := struct {
Response DestroyVirtualMachineResponse `json:"destroyvirtualmachineresponse"`
}{}
getDeserializedResponse(url, &responseWrapper)
data := &responseWrapper.Response
checkErrorInfo(&data.ErrorInfo)
return data.JobId, nil
}
type QueryAsyncJobResultResponse struct {
ErrorInfo
Created string `json:"created"`
Completed string `json:"completed"`
JobStatus int `json:"jobstatus"`
}
func (client *CloudstackClient) WaitForJobToComplete(jobID string) error {
url := client.createURL(map[string]string{
"command": "queryAsyncJobResult",
"jobid": jobID,
})
const maxTimeToWait = 30 * time.Minute
const retryInterval = 5 * time.Second
const maxTries = int(maxTimeToWait / retryInterval)
for i := 0; i < maxTries; i++ {
responseWrapper := struct {
Response QueryAsyncJobResultResponse `json:"queryasyncjobresultresponse"`
}{}
getDeserializedResponse(url, &responseWrapper)
data := &responseWrapper.Response
checkErrorInfo(&data.ErrorInfo)
if data.JobStatus == 0 {
log.Debug().Str("id", jobID).Msg("job is still pending")
time.Sleep(retryInterval)
continue
} else if data.JobStatus == 1 {
log.Debug().Str("id", jobID).Msg("job completed successfully")
return nil
} else {
// either an error, or an unknown status code
// see https://github.com/apache/cloudstack-cloudmonkey/blob/main/cmd/network.go
return fmt.Errorf("Job %s has status code %d", jobID, data.JobStatus)
}
}
return fmt.Errorf("Job %s took too long", jobID)
}
type ListVirtualMachinesResponse struct {
ErrorInfo
Count int `json:"count"`

@ -7,6 +7,11 @@ import (
"strings"
)
func isFalsy(s string) bool {
s = strings.ToLower(s)
return s == "false" || s == "no" || s == "0"
}
// A Config holds all of the configuration values needed for the program.
type Config struct {
CloudstackApiKey string
@ -15,6 +20,8 @@ type Config struct {
CloudstackZoneName string
CloudstackServiceOfferingName string
CloudstackKeypairName string
SSHKeyPath string
DeleteOldTemplates bool
// These must be keys of the distrosInfo map in cloudbuilder.go
DistrosToCheck []string
UploadDirectory string
@ -33,9 +40,14 @@ func New() *Config {
cfg := &Config{
CloudstackApiKey: os.Getenv("CLOUDSTACK_API_KEY"),
CloudstackSecretKey: os.Getenv("CLOUDSTACK_SECRET_KEY"),
SSHKeyPath: os.Getenv("SSH_KEY_PATH"),
UploadDirectory: os.Getenv("UPLOAD_DIRECTORY"),
UploadBaseUrl: os.Getenv("UPLOAD_BASE_URL"),
EmailRecipient: os.Getenv("EMAIL_RECIPIENT"),
DeleteOldTemplates: true,
}
if isFalsy(os.Getenv("DELETE_OLD_TEMPLATES")) {
cfg.DeleteOldTemplates = false
}
if cfg.CloudstackApiKey == "" {
panic("CLOUDSTACK_API_KEY is empty or not set")
@ -43,6 +55,9 @@ func New() *Config {
if cfg.CloudstackSecretKey == "" {
panic("CLOUDSTACK_SECRET_KEY is empty or not set")
}
if cfg.SSHKeyPath == "" {
panic("SSH_KEY_PATH is empty or not set")
}
if val, ok := os.LookupEnv("DISTROS_TO_CHECK"); ok {
cfg.DistrosToCheck = strings.Split(val, ",")
} else {

@ -82,6 +82,10 @@ func (mgr *AlmaLinuxTemplateManager) HasSELinuxEnabled() bool {
return true
}
func (mgr *AlmaLinuxTemplateManager) CommandToUpdatePackageCache() []string {
return fedoraCommandToUpdatePackageCache()
}
var almaLinuxYumRepoBaseUrlPattern *regexp.Regexp = regexp.MustCompile(
"^(?P<scheme>https?://)[A-Za-z0-9./-]+(?P<path>/almalinux/\\$releasever/[A-Za-z0-9./$-]+)$",
)
@ -105,7 +109,7 @@ func (mgr *AlmaLinuxTemplateManager) PerformDistroSpecificModifications(handle *
if err = mgr.addCloudInitSnippet(handle); err != nil {
return
}
if err = mgr.replaceYumRepoMirrorUrls(handle, mgr.transformAlmaLinuxYumRepoBaseUrl); err != nil {
if err = mgr.replaceYumMirrorUrls(handle, mgr.transformAlmaLinuxYumRepoBaseUrl); err != nil {
return
}
return

@ -80,6 +80,10 @@ func (mgr *DebianTemplateManager) UsesNetworkManager() bool {
return false
}
func (mgr *DebianTemplateManager) CommandToUpdatePackageCache() []string {
return debianCommandToUpdatePackageCache()
}
func (mgr *DebianTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
if err = mgr.setChronyOptions(handle, "/etc/chrony/chrony.conf"); err != nil {
return

@ -127,6 +127,10 @@ func (mgr *FedoraTemplateManager) HasSELinuxEnabled() bool {
return true
}
func (mgr *FedoraTemplateManager) CommandToUpdatePackageCache() []string {
return fedoraCommandToUpdatePackageCache()
}
var fedoraYumRepoBaseUrlPattern *regexp.Regexp = regexp.MustCompile(
"^(?P<scheme>https?://)[A-Za-z0-9./-]+(?P<path>/fedora/linux/[A-Za-z0-9./$-]+)$",
)
@ -150,7 +154,7 @@ func (mgr *FedoraTemplateManager) PerformDistroSpecificModifications(handle *gue
if err = mgr.addCloudInitSnippet(handle); err != nil {
return
}
if err = mgr.replaceYumRepoMirrorUrls(handle, mgr.transformFedoraYumRepoBaseUrl); err != nil {
if err = mgr.replaceYumMirrorUrls(handle, mgr.transformFedoraYumRepoBaseUrl); err != nil {
return
}
return

@ -45,6 +45,7 @@ type TemplateManager struct {
}
type ITemplateManager interface {
IDistroSpecificTemplateManager
// 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)
@ -66,6 +67,10 @@ type IDistroSpecificTemplateManager interface {
PerformDistroSpecificModifications(handle *guestfs.Guestfs) error
// HasSELinuxEnabled returns whether SELinux is enabled for this distro
HasSELinuxEnabled() bool
// CommandToUpdatePackageCache returns the command which updates
// (but does not upgrade) the packages for this distro, e.g.
// "sudo apt update".
CommandToUpdatePackageCache() []string
}
func (mgr *TemplateManager) DownloadTemplateGeneric(filename, url string) (path string, err error) {
@ -86,6 +91,14 @@ func (mgr *TemplateManager) DownloadTemplateGeneric(filename, url string) (path
return
}
func debianCommandToUpdatePackageCache() []string {
return []string{"sudo", "apt", "update"}
}
func fedoraCommandToUpdatePackageCache() []string {
return []string{"sudo", "dnf", "makecache"}
}
func (mgr *TemplateManager) performDistroAgnosticModifications(handle *guestfs.Guestfs) (err error) {
if err = mgr.setupIpv6Scripts(handle); err != nil {
return
@ -347,7 +360,24 @@ func getNamedRegexGroup(re *regexp.Regexp, submatches []string, groupName string
return value
}
func (mgr *TemplateManager) addAugeasComment(
func getNumAugeasComments(handle *guestfs.Guestfs, parentNode string) (numComments int, err error) {
keyPaths, err := handle.Aug_ls(parentNode)
if err != nil {
return 0, fmt.Errorf("aug_ls(%s) failed: %w", parentNode, err)
}
for _, keyPath := range keyPaths {
key, err := handle.Aug_label(keyPath)
if err != nil {
return 0, fmt.Errorf("aug_label(%s) failed: %w", keyPath, err)
}
if strings.HasPrefix(key, "#comment") {
numComments += 1
}
}
return
}
func addAugeasComment(
handle *guestfs.Guestfs, numExistingComments int, parentNode string, comment string,
) error {
return handle.Aug_set(
@ -356,17 +386,35 @@ func (mgr *TemplateManager) addAugeasComment(
)
}
// replaceYumRepoMirrorUrls comments out the metalink URLs and uncomments and
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 a commented line.
// 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) replaceYumRepoMirrorUrls(
func (mgr *TemplateManager) replaceYumMirrorUrls(
handle *guestfs.Guestfs, transformBaseurl func(string) string,
) (err error) {
log := mgr.logger
) error {
repoPaths, err := handle.Aug_ls("/files/etc/yum.repos.d")
if err != nil {
return fmt.Errorf("Could not enumerate yum repos: %w", err)
@ -379,67 +427,78 @@ func (mgr *TemplateManager) replaceYumRepoMirrorUrls(
}
// A subrepoPath looks like e.g. /files/etc/yum.repos.d/almalinux-appstream.repo/appstream
for _, subrepoPath := range subrepoPaths {
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
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 == "baseurl" {
uncommentedBaseurl = value
}
if err = mgr.replaceYumMirrorUrlsForSingleSubrepo(handle, transformBaseurl, subrepoPath); err != nil {
return fmt.Errorf("Could not replace mirror URLs for %s: %w", subrepoPath, err)
}
var baseurl string
if uncommentedBaseurl != "" {
baseurl = transformBaseurl(uncommentedBaseurl)
if baseurl == uncommentedBaseurl {
baseurl = ""
}
} else if commentedBaseurl != "" {
baseurl = transformBaseurl(commentedBaseurl)
if baseurl == commentedBaseurl {
baseurl = ""
}
}
if baseurl != "" {
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 != "" {
// comment out the mirrorlist line
log.Debug().Msg("Commenting out " + subrepoPath + "/mirrorlist")
if _, err = handle.Aug_rm(subrepoPath + "/mirrorlist"); err != nil {
return fmt.Errorf("Could not remove %s: %w", subrepoPath+"/mirrorlist", err)
}
if err = mgr.addAugeasComment(handle, numComments, subrepoPath, "mirrorlist="+mirrorlist); err != nil {
return fmt.Errorf("Could not insert comment in %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
}

@ -92,7 +92,7 @@ func (mgr *UbuntuTemplateManager) disableNoisyMotdMessages(handle *guestfs.Guest
"00-header",
"10-help-text",
"50-landscape-sysinfo",
"50-motd-news",
//"50-motd-news",
"88-esm-announce",
}
for _, filename := range filesToDisable {
@ -113,6 +113,10 @@ func (mgr *UbuntuTemplateManager) UsesNetworkManager() bool {
return false
}
func (mgr *UbuntuTemplateManager) CommandToUpdatePackageCache() []string {
return debianCommandToUpdatePackageCache()
}
func (mgr *UbuntuTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
if err = mgr.setTimesyncdConf(handle); err != nil {
return

Loading…
Cancel
Save