diff --git a/mgradm/cmd/install/podman/utils.go b/mgradm/cmd/install/podman/utils.go index fe365f4a0..ee883a8ae 100644 --- a/mgradm/cmd/install/podman/utils.go +++ b/mgradm/cmd/install/podman/utils.go @@ -72,10 +72,11 @@ func installForPodman( return errors.New(L("install podman before running this command")) } - inspectedHostValues, err := utils.InspectHost(false) + authFile, cleaner, err := shared_podman.PodmanLogin() if err != nil { - return utils.Errorf(err, L("cannot inspect host values")) + return utils.Errorf(err, L("failed to login to registry.suse.com")) } + defer cleaner() fqdn, err := getFqdn(args) if err != nil { @@ -87,14 +88,8 @@ func installForPodman( if err != nil { return utils.Errorf(err, L("failed to compute image URL")) } - pullArgs := []string{} - _, scc_user_exist := inspectedHostValues["host_scc_username"] - _, scc_user_password := inspectedHostValues["host_scc_password"] - if scc_user_exist && scc_user_password { - pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"]) - } - preparedImage, err := shared_podman.PrepareImage(image, flags.Image.PullPolicy, pullArgs...) + preparedImage, err := shared_podman.PrepareImage(authFile, image, flags.Image.PullPolicy) if err != nil { return err } diff --git a/mgradm/cmd/install/shared/flags.go b/mgradm/cmd/install/shared/flags.go index d7868683e..2fe185e2d 100644 --- a/mgradm/cmd/install/shared/flags.go +++ b/mgradm/cmd/install/shared/flags.go @@ -214,13 +214,7 @@ func AddInstallFlags(cmd *cobra.Command) { cmd.Flags().Bool("debug-java", false, L("Enable tomcat and taskomatic remote debugging")) cmd_utils.AddImageFlag(cmd) - cmd_utils.AddContainerImageFlags(cmd, "coco", L("confidential computing attestation")) - cmd.Flags().Int("coco-replicas", 0, L("How many replicas of the confidential computing container should be started. (only 0 or 1 supported for now)")) - - _ = utils.AddFlagHelpGroup(cmd, &utils.Group{ID: "coco-container", Title: L("Confidential Computing Flags")}) - _ = utils.AddFlagToHelpGroupID(cmd, "coco-replicas", "coco-container") - _ = utils.AddFlagToHelpGroupID(cmd, "coco-image", "coco-container") - _ = utils.AddFlagToHelpGroupID(cmd, "coco-tag", "coco-container") + cmd_utils.AddCocoFlag(cmd) cmd.Flags().Int("hubxmlrpc-replicas", 0, L("How many replicas of the Hub XML-RPC API service container should be started. (only 0 or 1 supported for now)")) hubXmlrpcImage := path.Join(utils.DefaultNamespace, "server-hub-xmlrpc-api") diff --git a/mgradm/cmd/migrate/kubernetes/utils.go b/mgradm/cmd/migrate/kubernetes/utils.go index f328711b8..2f85d42f0 100644 --- a/mgradm/cmd/migrate/kubernetes/utils.go +++ b/mgradm/cmd/migrate/kubernetes/utils.go @@ -90,7 +90,7 @@ func migrateToKubernetes( return utils.Errorf(err, L("cannot run migration")) } - tz, oldPgVersion, newPgVersion, err := adm_utils.ReadContainerData(scriptDir) + extractedData, err := utils.ReadInspectData[utils.InspectResult](path.Join(scriptDir, "data")) if err != nil { return utils.Errorf(err, L("cannot read data from container")) } @@ -115,7 +115,7 @@ func migrateToKubernetes( helmArgs := []string{ "--reset-values", - "--set", "timezone=" + tz, + "--set", "timezone=" + extractedData.Timezone, } if flags.Mirror != "" { log.Warn().Msgf(L("The mirror data will not be migrated, ensure it is available at %s"), flags.Mirror) @@ -139,6 +139,9 @@ func migrateToKubernetes( return utils.Errorf(err, L("cannot set replicas to 0")) } + oldPgVersion := extractedData.CurrentPgVersion + newPgVersion := extractedData.ImagePgVersion + if oldPgVersion != newPgVersion { if err := kubernetes.RunPgsqlVersionUpgrade(flags.Image, flags.DbUpgradeImage, nodeName, oldPgVersion, newPgVersion); err != nil { return utils.Errorf(err, L("cannot run PostgreSQL version upgrade script")) @@ -146,8 +149,8 @@ func migrateToKubernetes( } schemaUpdateRequired := oldPgVersion != newPgVersion - if err := kubernetes.RunPgsqlFinalizeScript(serverImage, flags.Image.PullPolicy, nodeName, schemaUpdateRequired); err != nil { - return utils.Errorf(err, L("cannot run PostgreSQL version upgrade script")) + if err := kubernetes.RunPgsqlFinalizeScript(serverImage, flags.Image.PullPolicy, nodeName, schemaUpdateRequired, true); err != nil { + return utils.Errorf(err, L("cannot run PostgreSQL finalisation script")) } if err := kubernetes.RunPostUpgradeScript(serverImage, flags.Image.PullPolicy, nodeName); err != nil { diff --git a/mgradm/cmd/migrate/podman/utils.go b/mgradm/cmd/migrate/podman/utils.go index bd24970ea..b7f6ff5cb 100644 --- a/mgradm/cmd/migrate/podman/utils.go +++ b/mgradm/cmd/migrate/podman/utils.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" migration_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared" + "github.com/uyuni-project/uyuni-tools/mgradm/shared/coco" "github.com/uyuni-project/uyuni-tools/mgradm/shared/podman" "github.com/uyuni-project/uyuni-tools/shared" podman_utils "github.com/uyuni-project/uyuni-tools/shared/podman" @@ -31,20 +32,13 @@ func migrateToPodman(globalFlags *types.GlobalFlags, flags *podmanMigrateFlags, return utils.Errorf(err, L("cannot compute image")) } - // FIXME all this code should be centralized. Now it being called in several different places. - inspectedHostValues, err := utils.InspectHost(false) + authFile, cleaner, err := podman_utils.PodmanLogin() if err != nil { - return utils.Errorf(err, L("cannot inspect host values")) + return utils.Errorf(err, L("failed to login to registry.suse.com")) } + defer cleaner() - pullArgs := []string{} - _, scc_user_exist := inspectedHostValues["host_scc_username"] - _, scc_user_password := inspectedHostValues["host_scc_password"] - if scc_user_exist && scc_user_password { - pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"]) - } - - preparedImage, err := podman_utils.PrepareImage(serverImage, flags.Image.PullPolicy, pullArgs...) + preparedImage, err := podman_utils.PrepareImage(authFile, serverImage, flags.Image.PullPolicy) if err != nil { return err } @@ -53,19 +47,24 @@ func migrateToPodman(globalFlags *types.GlobalFlags, flags *podmanMigrateFlags, sshAuthSocket := migration_shared.GetSshAuthSocket() sshConfigPath, sshKnownhostsPath := migration_shared.GetSshPaths() - tz, oldPgVersion, newPgVersion, err := podman.RunMigration(preparedImage, sshAuthSocket, sshConfigPath, sshKnownhostsPath, sourceFqdn, flags.User) + extractedData, err := podman.RunMigration(preparedImage, sshAuthSocket, sshConfigPath, sshKnownhostsPath, sourceFqdn, flags.User) if err != nil { return utils.Errorf(err, L("cannot run migration script")) } + oldPgVersion := extractedData.CurrentPgVersion + newPgVersion := extractedData.ImagePgVersion + if oldPgVersion != newPgVersion { - if err := podman.RunPgsqlVersionUpgrade(flags.Image, flags.DbUpgradeImage, oldPgVersion, newPgVersion); err != nil { + if err := podman.RunPgsqlVersionUpgrade( + authFile, flags.Image, flags.DbUpgradeImage, oldPgVersion, newPgVersion, + ); err != nil { return utils.Errorf(err, L("cannot run PostgreSQL version upgrade script")) } } schemaUpdateRequired := oldPgVersion != newPgVersion - if err := podman.RunPgsqlFinalizeScript(preparedImage, schemaUpdateRequired); err != nil { + if err := podman.RunPgsqlFinalizeScript(preparedImage, schemaUpdateRequired, true); err != nil { return utils.Errorf(err, L("cannot run PostgreSQL finalize script")) } @@ -73,7 +72,9 @@ func migrateToPodman(globalFlags *types.GlobalFlags, flags *podmanMigrateFlags, return utils.Errorf(err, L("cannot run post upgrade script")) } - if err := podman.GenerateSystemdService(tz, preparedImage, false, flags.Mirror, viper.GetStringSlice("podman.arg")); err != nil { + if err := podman.GenerateSystemdService( + extractedData.Timezone, preparedImage, false, flags.Mirror, viper.GetStringSlice("podman.arg"), + ); err != nil { return utils.Errorf(err, L("cannot generate systemd service file")) } @@ -82,6 +83,21 @@ func migrateToPodman(globalFlags *types.GlobalFlags, flags *podmanMigrateFlags, return err } + // Prepare confidential computing containers + if err = coco.Upgrade( + flags.Coco.Image, flags.Image, extractedData.DbPort, extractedData.DbName, + extractedData.DbUser, extractedData.DbPassword, + ); err != nil { + return utils.Errorf(err, L("cannot setup confidential computing attestation service")) + } + + if flags.Coco.Replicas > 0 { + err := podman_utils.ScaleService(flags.Coco.Replicas, podman_utils.ServerAttestationService) + if err != nil { + return err + } + } + log.Info().Msg(L("Server migrated")) if err := podman_utils.EnablePodmanSocket(); err != nil { diff --git a/mgradm/cmd/migrate/shared/flags.go b/mgradm/cmd/migrate/shared/flags.go index 2fc0c7d67..399aa274c 100644 --- a/mgradm/cmd/migrate/shared/flags.go +++ b/mgradm/cmd/migrate/shared/flags.go @@ -6,6 +6,7 @@ package shared import ( "github.com/spf13/cobra" + "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" . "github.com/uyuni-project/uyuni-tools/shared/l10n" "github.com/uyuni-project/uyuni-tools/shared/types" @@ -15,6 +16,7 @@ import ( type MigrateFlags struct { Image types.ImageFlags `mapstructure:",squash"` DbUpgradeImage types.ImageFlags `mapstructure:"dbupgrade"` + Coco shared.CocoFlags User string Mirror string } @@ -24,5 +26,6 @@ func AddMigrateFlags(cmd *cobra.Command) { utils.AddMirrorFlag(cmd) utils.AddImageFlag(cmd) utils.AddDbUpgradeImageFlag(cmd) + utils.AddCocoFlag(cmd) cmd.Flags().String("user", "root", L("User on the source server. Non-root user must have passwordless sudo privileges (NOPASSWD tag in /etc/sudoers).")) } diff --git a/mgradm/cmd/status/podman.go b/mgradm/cmd/status/podman.go index baa018a15..f9bfc5776 100644 --- a/mgradm/cmd/status/podman.go +++ b/mgradm/cmd/status/podman.go @@ -11,7 +11,6 @@ import ( "github.com/spf13/cobra" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" - . "github.com/uyuni-project/uyuni-tools/shared/l10n" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" @@ -29,9 +28,7 @@ func podmanStatus( } else { // Run spacewalk-service status in the container cnx := shared.NewConnection("podman", podman.ServerContainerName, "") - if err := adm_utils.ExecCommand(zerolog.InfoLevel, cnx, "spacewalk-service", "status"); err != nil { - return utils.Errorf(err, L("failed to run spacewalk-service status")) - } + _ = adm_utils.ExecCommand(zerolog.InfoLevel, cnx, "spacewalk-service", "status") } for i := 0; i < podman.CurrentReplicaCount(podman.ServerAttestationService); i++ { diff --git a/mgradm/cmd/support/ptf/podman/utils.go b/mgradm/cmd/support/ptf/podman/utils.go index 00ed9a946..9eddea4f1 100644 --- a/mgradm/cmd/support/ptf/podman/utils.go +++ b/mgradm/cmd/support/ptf/podman/utils.go @@ -30,7 +30,14 @@ func ptfForPodman( if err := flags.checkParameters(); err != nil { return err } - return podman.Upgrade(flags.Image, dummyMigration, dummyCoco, args) + + authFile, cleaner, err := podman_shared.PodmanLogin() + if err != nil { + return utils.Errorf(err, L("failed to login to registry.suse.com")) + } + defer cleaner() + + return podman.Upgrade(authFile, flags.Image, dummyMigration, dummyCoco, args) } func (flags *podmanPTFFlags) checkParameters() error { diff --git a/mgradm/cmd/upgrade/podman/utils.go b/mgradm/cmd/upgrade/podman/utils.go index fb4bb6171..690d1ca72 100644 --- a/mgradm/cmd/upgrade/podman/utils.go +++ b/mgradm/cmd/upgrade/podman/utils.go @@ -7,9 +7,18 @@ package podman import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/shared/podman" + . "github.com/uyuni-project/uyuni-tools/shared/l10n" + shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" + "github.com/uyuni-project/uyuni-tools/shared/utils" ) func upgradePodman(globalFlags *types.GlobalFlags, flags *podmanUpgradeFlags, cmd *cobra.Command, args []string) error { - return podman.Upgrade(flags.Image, flags.DbUpgradeImage, flags.Coco.Image, args) + authFile, cleaner, err := shared_podman.PodmanLogin() + if err != nil { + return utils.Errorf(err, L("failed to login to registry.suse.com")) + } + defer cleaner() + + return podman.Upgrade(authFile, flags.Image, flags.DbUpgradeImage, flags.Coco.Image, args) } diff --git a/mgradm/shared/kubernetes/install.go b/mgradm/shared/kubernetes/install.go index bc08bdef4..33aeb33cf 100644 --- a/mgradm/shared/kubernetes/install.go +++ b/mgradm/shared/kubernetes/install.go @@ -147,8 +147,8 @@ func Upgrade( return err } - fqdn, exist := inspectedValues["fqdn"] - if !exist { + fqdn := inspectedValues.Fqdn + if fqdn == "" { return fmt.Errorf(L("inspect function did non return fqdn value")) } @@ -182,25 +182,25 @@ func Upgrade( err = kubernetes.ReplicasTo(kubernetes.ServerApp, 1) } }() - if inspectedValues["image_pg_version"] > inspectedValues["current_pg_version"] { + if inspectedValues.ImagePgVersion > inspectedValues.CurrentPgVersion { log.Info().Msgf(L("Previous PostgreSQL is %[1]s, new one is %[2]s. Performing a DB version upgrade…"), - inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]) + inspectedValues.CurrentPgVersion, inspectedValues.ImagePgVersion) if err := RunPgsqlVersionUpgrade(*image, *upgradeImage, nodeName, - inspectedValues["current_pg_version"], inspectedValues["image_pg_version"], + inspectedValues.CurrentPgVersion, inspectedValues.ImagePgVersion, ); err != nil { return utils.Errorf(err, L("cannot run PostgreSQL version upgrade script")) } - } else if inspectedValues["image_pg_version"] == inspectedValues["current_pg_version"] { - log.Info().Msgf(L("Upgrading to %s without changing PostgreSQL version"), inspectedValues["uyuni_release"]) + } else if inspectedValues.ImagePgVersion == inspectedValues.CurrentPgVersion { + log.Info().Msgf(L("Upgrading to %s without changing PostgreSQL version"), inspectedValues.UyuniRelease) } else { return fmt.Errorf(L("trying to downgrade PostgreSQL from %[1]s to %[2]s"), - inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]) + inspectedValues.CurrentPgVersion, inspectedValues.ImagePgVersion) } - schemaUpdateRequired := inspectedValues["current_pg_version"] != inspectedValues["image_pg_version"] - if err := RunPgsqlFinalizeScript(serverImage, image.PullPolicy, nodeName, schemaUpdateRequired); err != nil { - return utils.Errorf(err, L("cannot run PostgreSQL version upgrade script")) + schemaUpdateRequired := inspectedValues.CurrentPgVersion != inspectedValues.ImagePgVersion + if err := RunPgsqlFinalizeScript(serverImage, image.PullPolicy, nodeName, schemaUpdateRequired, false); err != nil { + return utils.Errorf(err, L("cannot run PostgreSQL finalize script")) } if err := RunPostUpgradeScript(serverImage, image.PullPolicy, nodeName); err != nil { diff --git a/mgradm/shared/kubernetes/k3s.go b/mgradm/shared/kubernetes/k3s.go index 7f0431843..b5706b056 100644 --- a/mgradm/shared/kubernetes/k3s.go +++ b/mgradm/shared/kubernetes/k3s.go @@ -98,14 +98,16 @@ func RunPgsqlVersionUpgrade(image types.ImageFlags, upgradeImage types.ImageFlag } // RunPgsqlFinalizeScript run the script with all the action required to a db after upgrade. -func RunPgsqlFinalizeScript(serverImage string, pullPolicy string, nodeName string, schemaUpdateRequired bool) error { +func RunPgsqlFinalizeScript( + serverImage string, pullPolicy string, nodeName string, schemaUpdateRequired bool, migration bool, +) error { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf(L("failed to create temporary directory: %s")) } pgsqlFinalizeContainer := "uyuni-finalize-pgsql" - pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript(scriptDir, true, schemaUpdateRequired, true, true, true) + pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript(scriptDir, true, schemaUpdateRequired, true, migration, true) if err != nil { return utils.Errorf(err, L("cannot generate PostgreSQL finalization script")) } diff --git a/mgradm/shared/podman/podman.go b/mgradm/shared/podman/podman.go index fb25bfdb7..2fe88fa74 100644 --- a/mgradm/shared/podman/podman.go +++ b/mgradm/shared/podman/podman.go @@ -11,7 +11,6 @@ import ( "os/exec" "path" "path/filepath" - "strconv" "strings" "github.com/rs/zerolog" @@ -177,10 +176,10 @@ func UpdateSslCertificate(cnx *shared.Connection, chain *ssl.CaChain, serverPair } // RunMigration migrate an existing remote server to a container. -func RunMigration(preparedImage string, sshAuthSocket string, sshConfigPath string, sshKnownhostsPath string, sourceFqdn string, user string) (string, string, string, error) { +func RunMigration(preparedImage string, sshAuthSocket string, sshConfigPath string, sshKnownhostsPath string, sourceFqdn string, user string) (*utils.InspectResult, error) { scriptDir, err := adm_utils.GenerateMigrationScript(sourceFqdn, user, false) if err != nil { - return "", "", "", utils.Errorf(err, L("cannot generate migration script")) + return nil, utils.Errorf(err, L("cannot generate migration script")) } defer os.RemoveAll(scriptDir) @@ -202,19 +201,25 @@ func RunMigration(preparedImage string, sshAuthSocket string, sshConfigPath stri log.Info().Msg(L("Migrating server")) if err := podman.RunContainer("uyuni-migration", preparedImage, utils.ServerVolumeMounts, extraArgs, []string{"/var/lib/uyuni-tools/migrate.sh"}); err != nil { - return "", "", "", utils.Errorf(err, L("cannot run uyuni migration container")) + return nil, utils.Errorf(err, L("cannot run uyuni migration container")) } - tz, oldPgVersion, newPgVersion, err := adm_utils.ReadContainerData(scriptDir) + extractedData, err := utils.ReadInspectData[utils.InspectResult](path.Join(scriptDir, "data")) if err != nil { - return "", "", "", utils.Errorf(err, L("cannot read extracted data")) + return nil, utils.Errorf(err, L("cannot read extracted data")) } - return tz, oldPgVersion, newPgVersion, nil + return extractedData, nil } // RunPgsqlVersionUpgrade perform a PostgreSQL major upgrade. -func RunPgsqlVersionUpgrade(image types.ImageFlags, upgradeImage types.ImageFlags, oldPgsql string, newPgsql string) error { +func RunPgsqlVersionUpgrade( + authFile string, + image types.ImageFlags, + upgradeImage types.ImageFlags, + oldPgsql string, + newPgsql string, +) error { log.Info().Msgf(L("Previous PostgreSQL is %[1]s, new one is %[2]s. Performing a DB version upgrade…"), oldPgsql, newPgsql) scriptDir, err := os.MkdirTemp("", "mgradm-*") @@ -243,19 +248,7 @@ func RunPgsqlVersionUpgrade(image types.ImageFlags, upgradeImage types.ImageFlag } } - inspectedHostValues, err := utils.InspectHost(false) - if err != nil { - return utils.Errorf(err, L("cannot inspect host values")) - } - - pullArgs := []string{} - _, scc_user_exist := inspectedHostValues["host_scc_username"] - _, scc_user_password := inspectedHostValues["host_scc_password"] - if scc_user_exist && scc_user_password && strings.Contains(upgradeImageUrl, "registry.suse.com") { - pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"]) - } - - preparedImage, err := podman.PrepareImage(upgradeImageUrl, image.PullPolicy, pullArgs...) + preparedImage, err := podman.PrepareImage(authFile, upgradeImageUrl, image.PullPolicy) if err != nil { return err } @@ -277,7 +270,7 @@ func RunPgsqlVersionUpgrade(image types.ImageFlags, upgradeImage types.ImageFlag } // RunPgsqlFinalizeScript run the script with all the action required to a db after upgrade. -func RunPgsqlFinalizeScript(serverImage string, schemaUpdateRequired bool) error { +func RunPgsqlFinalizeScript(serverImage string, schemaUpdateRequired bool, migration bool) error { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { @@ -289,7 +282,9 @@ func RunPgsqlFinalizeScript(serverImage string, schemaUpdateRequired bool) error "--security-opt", "label=disable", } pgsqlFinalizeContainer := "uyuni-finalize-pgsql" - pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript(scriptDir, true, schemaUpdateRequired, true, true, false) + pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript( + scriptDir, true, schemaUpdateRequired, true, migration, false, + ) if err != nil { return utils.Errorf(err, L("cannot generate PostgreSQL finalization script")) } @@ -326,7 +321,13 @@ func RunPostUpgradeScript(serverImage string) error { } // Upgrade will upgrade server to the image given as attribute. -func Upgrade(image types.ImageFlags, upgradeImage types.ImageFlags, cocoImage types.ImageFlags, args []string) error { +func Upgrade( + authFile string, + image types.ImageFlags, + upgradeImage types.ImageFlags, + cocoImage types.ImageFlags, + args []string, +) error { if err := CallCloudGuestRegistryAuth(); err != nil { return err } @@ -336,19 +337,7 @@ func Upgrade(image types.ImageFlags, upgradeImage types.ImageFlags, cocoImage ty return fmt.Errorf(L("failed to compute image URL")) } - inspectedHostValues, err := utils.InspectHost(false) - if err != nil { - return utils.Errorf(err, L("cannot inspect host values")) - } - - pullArgs := []string{} - _, scc_user_exist := inspectedHostValues["host_scc_username"] - _, scc_user_password := inspectedHostValues["host_scc_password"] - if scc_user_exist && scc_user_password && strings.Contains(serverImage, "registry.suse.com") { - pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"]) - } - - preparedImage, err := podman.PrepareImage(serverImage, image.PullPolicy, pullArgs...) + preparedImage, err := podman.PrepareImage(authFile, serverImage, image.PullPolicy) if err != nil { return err } @@ -371,20 +360,22 @@ func Upgrade(image types.ImageFlags, upgradeImage types.ImageFlags, cocoImage ty defer func() { err = podman.StartService(podman.ServerService) }() - if inspectedValues["image_pg_version"] > inspectedValues["current_pg_version"] { - log.Info().Msgf(L("Previous postgresql is %[1]s, instead new one is %[2]s. Performing a DB version upgrade…"), inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]) - if err := RunPgsqlVersionUpgrade(image, upgradeImage, inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]); err != nil { + if inspectedValues.ImagePgVersion > inspectedValues.CurrentPgVersion { + log.Info().Msgf(L("Previous postgresql is %[1]s, instead new one is %[2]s. Performing a DB version upgrade…"), inspectedValues.CurrentPgVersion, inspectedValues.ImagePgVersion) + if err := RunPgsqlVersionUpgrade( + authFile, image, upgradeImage, inspectedValues.CurrentPgVersion, inspectedValues.ImagePgVersion, + ); err != nil { return utils.Errorf(err, L("cannot run PostgreSQL version upgrade script")) } - } else if inspectedValues["image_pg_version"] == inspectedValues["current_pg_version"] { - log.Info().Msgf(L("Upgrading to %s without changing PostgreSQL version"), inspectedValues["uyuni_release"]) + } else if inspectedValues.ImagePgVersion == inspectedValues.CurrentPgVersion { + log.Info().Msgf(L("Upgrading to %s without changing PostgreSQL version"), inspectedValues.UyuniRelease) } else { - return fmt.Errorf(L("trying to downgrade PostgreSQL from %[1]s to %[2]s"), inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]) + return fmt.Errorf(L("trying to downgrade PostgreSQL from %[1]s to %[2]s"), inspectedValues.CurrentPgVersion, inspectedValues.ImagePgVersion) } - schemaUpdateRequired := inspectedValues["current_pg_version"] != inspectedValues["image_pg_version"] - if err := RunPgsqlFinalizeScript(preparedImage, schemaUpdateRequired); err != nil { - return utils.Errorf(err, L("cannot run PostgreSQL version upgrade script")) + schemaUpdateRequired := inspectedValues.CurrentPgVersion != inspectedValues.ImagePgVersion + if err := RunPgsqlFinalizeScript(preparedImage, schemaUpdateRequired, false); err != nil { + return utils.Errorf(err, L("cannot run PostgreSQL finalize script")) } if err := RunPostUpgradeScript(preparedImage); err != nil { @@ -396,13 +387,8 @@ func Upgrade(image types.ImageFlags, upgradeImage types.ImageFlags, cocoImage ty } log.Info().Msg(L("Waiting for the server to start…")) - dbPort, err := strconv.Atoi(inspectedValues["db_port"]) - if err != nil { - return utils.Errorf(err, L("error %s is not a valid port number."), inspectedValues["db_port"]) - } - err = coco.Upgrade(cocoImage, image, - dbPort, inspectedValues["db_name"], inspectedValues["db_user"], inspectedValues["db_password"]) + inspectedValues.DbPort, inspectedValues.DbName, inspectedValues.DbUser, inspectedValues.DbPassword) if err != nil { return utils.Errorf(err, L("error upgrading confidential computing service.")) } @@ -411,31 +397,32 @@ func Upgrade(image types.ImageFlags, upgradeImage types.ImageFlags, cocoImage ty } // Inspect check values on a given image and deploy. -func Inspect(preparedImage string) (map[string]string, error) { +func Inspect(preparedImage string) (*utils.ServerInspectData, error) { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { - return map[string]string{}, utils.Errorf(err, L("failed to create temporary directory")) + return nil, utils.Errorf(err, L("failed to create temporary directory")) } - if err := utils.GenerateInspectContainerScript(scriptDir); err != nil { - return map[string]string{}, err + inspector := utils.NewServerInspector(scriptDir) + if err := inspector.GenerateScript(); err != nil { + return nil, err } podmanArgs := []string{ - "-v", scriptDir + ":" + utils.InspectOutputFile.Directory, + "-v", scriptDir + ":" + utils.InspectContainerDirectory, "--security-opt", "label=disable", } err = podman.RunContainer("uyuni-inspect", preparedImage, utils.ServerVolumeMounts, podmanArgs, - []string{utils.InspectOutputFile.Directory + "/" + utils.InspectScriptFilename}) + []string{utils.InspectContainerDirectory + "/" + utils.InspectScriptFilename}) if err != nil { - return map[string]string{}, err + return nil, err } - inspectResult, err := utils.ReadInspectData(scriptDir) + inspectResult, err := inspector.ReadInspectData() if err != nil { - return map[string]string{}, utils.Errorf(err, L("cannot inspect data")) + return nil, utils.Errorf(err, L("cannot inspect data")) } return inspectResult, err diff --git a/mgradm/shared/templates/migrateScriptTemplate.go b/mgradm/shared/templates/migrateScriptTemplate.go index 95fc844eb..3f33317ab 100644 --- a/mgradm/shared/templates/migrateScriptTemplate.go +++ b/mgradm/shared/templates/migrateScriptTemplate.go @@ -80,8 +80,13 @@ echo "Extracting time zone..." $SSH {{ .SourceFqdn }} timedatectl show -p Timezone >/var/lib/uyuni-tools/data echo "Extracting postgresql versions..." -echo "new_pg_version=$(rpm -qa --qf '%{VERSION}\n' 'name=postgresql[0-8][0-9]-server' | cut -d. -f1 | sort -n | tail -1)" >> /var/lib/uyuni-tools/data -echo "old_pg_version=$(cat /var/lib/pgsql/data/PG_VERSION)" >> /var/lib/uyuni-tools/data +echo "image_pg_version=$(rpm -qa --qf '%{VERSION}\n' 'name=postgresql[0-8][0-9]-server' | cut -d. -f1 | sort -n | tail -1)" >> /var/lib/uyuni-tools/data +echo "current_pg_version=$(cat /var/lib/pgsql/data/PG_VERSION)" >> /var/lib/uyuni-tools/data + +grep '^db_user' /etc/rhn/rhn.conf | sed 's/[ \t]//g' >>/var/lib/uyuni-tools/data +grep '^db_password' /etc/rhn/rhn.conf | sed 's/[ \t]//g' >>/var/lib/uyuni-tools/data +grep '^db_name' /etc/rhn/rhn.conf | sed 's/[ \t]//g' >>/var/lib/uyuni-tools/data +grep '^db_port' /etc/rhn/rhn.conf | sed 's/[ \t]//g' >>/var/lib/uyuni-tools/data echo "Altering configuration for domain resolution..." sed 's/report_db_host = {{ .SourceFqdn }}/report_db_host = localhost/' -i /etc/rhn/rhn.conf; diff --git a/mgradm/shared/templates/pgsqlFinalizeScriptTemplate.go b/mgradm/shared/templates/pgsqlFinalizeScriptTemplate.go index 3584ccb2f..72bb628f2 100644 --- a/mgradm/shared/templates/pgsqlFinalizeScriptTemplate.go +++ b/mgradm/shared/templates/pgsqlFinalizeScriptTemplate.go @@ -12,6 +12,14 @@ import ( const postgresFinalizeScriptTemplate = `#!/bin/bash set -e +{{ if .Migration }} +echo "Adding database access for other containers..." +db_user=$(sed -n '/^db_user/{s/^.*=[ \t]\+\(.*\)$/\1/ ; p}' /etc/rhn/rhn.conf) +db_name=$(sed -n '/^db_name/{s/^.*=[ \t]\+\(.*\)$/\1/ ; p}' /etc/rhn/rhn.conf) +ip=$(ip -o -4 addr show up scope global | head -1 | awk '{print $4}' || true) +echo "host $db_name $db_user $ip scram-sha-256" >> /var/lib/pgsql/data/pg_hba.conf +{{ end }} + {{ if .RunAutotune }} echo "Running smdba system-check autotuning..." smdba system-check autotuning @@ -29,7 +37,7 @@ echo "Schema update..." /usr/sbin/spacewalk-startup-helper check-database {{ end }} -{{ if .RunDistroMigration }} +{{ if .Migration }} echo "Updating auto-installable distributions..." spacewalk-sql --select-mode - <&1 /dev/null | grep username | cut -d= -f2 || true"), + types.NewInspectData( + "scc_password", + "cat /etc/zypp/credentials.d/SCCcredentials 2>&1 /dev/null | grep password | cut -d= -f2 || true"), + }, + ScriptDir: scriptDir, + } + return hostInspector{ + BaseInspector: base, + } +} + +// hostInspectData are the data returned by the host inspector. +type hostInspectData struct { + SccUsername string `mapstructure:"scc_username"` + SccPassword string `mapstructure:"scc_password"` +} + +// ReadInspectData parses the data generated by the host inspector. +func (i *hostInspector) ReadInspectData() (*hostInspectData, error) { + return utils.ReadInspectData[hostInspectData](i.GetDataPath()) +} + +func inspectHost() (*hostInspectData, error) { + scriptDir, err := os.MkdirTemp("", "mgradm-*") + defer os.RemoveAll(scriptDir) + if err != nil { + return nil, utils.Errorf(err, L("failed to create temporary directory")) + } + + inspector := newHostInspector(scriptDir) + if err := inspector.GenerateScript(); err != nil { + return nil, err + } + + if err := utils.RunCmdStdMapping(zerolog.DebugLevel, inspector.GetScriptPath()); err != nil { + return nil, utils.Errorf(err, L("failed to run inspect script in host system")) + } + + inspectResult, err := inspector.ReadInspectData() + if err != nil { + return nil, utils.Errorf(err, L("cannot inspect host data")) + } + + return inspectResult, err +} diff --git a/shared/podman/hostinspector_test.go b/shared/podman/hostinspector_test.go new file mode 100644 index 000000000..df3a6a59f --- /dev/null +++ b/shared/podman/hostinspector_test.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package podman + +import ( + "path" + "testing" + + "github.com/uyuni-project/uyuni-tools/shared/test_utils" + "github.com/uyuni-project/uyuni-tools/shared/utils" +) + +func TestHostInspectorGenerate(t *testing.T) { + testDir, cleaner := test_utils.CreateTmpFolder(t) + defer cleaner() + + inspector := newHostInspector(testDir) + if err := inspector.GenerateScript(); err != nil { + t.Errorf("Unexpected error %s", err) + } + + dataPath := inspector.GetDataPath() + + expected := `#!/bin/bash +# inspect.sh, generated by mgradm +echo "scc_username=$(cat /etc/zypp/credentials.d/SCCcredentials 2>&1 /dev/null | grep username | cut -d= -f2 || true)" >> ` + dataPath + ` +echo "scc_password=$(cat /etc/zypp/credentials.d/SCCcredentials 2>&1 /dev/null | grep password | cut -d= -f2 || true)" >> ` + dataPath + ` +exit 0 +` + + actual := test_utils.ReadFile(t, path.Join(testDir, utils.InspectScriptFilename)) + test_utils.AssertEquals(t, "Wrongly generated script", expected, actual) +} + +func TestHostInspectorParse(t *testing.T) { + testDir, cleaner := test_utils.CreateTmpFolder(t) + defer cleaner() + + inspector := newHostInspector(testDir) + + content := ` +scc_username=myuser +scc_password=mysecret +` + test_utils.WriteFile(t, inspector.GetDataPath(), content) + + actual, err := inspector.ReadInspectData() + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + test_utils.AssertEquals(t, "Invalid SCC username", "myuser", actual.SccUsername) + test_utils.AssertEquals(t, "Invalid SCC password", "mysecret", actual.SccPassword) +} diff --git a/shared/podman/images.go b/shared/podman/images.go index 310cc8606..ff5edf9e8 100644 --- a/shared/podman/images.go +++ b/shared/podman/images.go @@ -27,7 +27,7 @@ const rpmImageDir = "/usr/share/suse-docker-images/native/" // Ensure the container image is pulled or pull it if the pull policy allows it. // // Returns the image name to use. Note that it may be changed if the image has been loaded from a local RPM package. -func PrepareImage(image string, pullPolicy string, args ...string) (string, error) { +func PrepareImage(authFile string, image string, pullPolicy string) (string, error) { if strings.ToLower(pullPolicy) != "always" { log.Info().Msgf(L("Ensure image %s is available"), image) @@ -62,7 +62,7 @@ func PrepareImage(image string, pullPolicy string, args ...string) (string, erro if strings.ToLower(pullPolicy) != "never" { log.Debug().Msgf("Pulling image %s because it is missing and pull policy is not 'never'", image) - return image, pullImage(image, args...) + return image, pullImage(authFile, image) } return image, fmt.Errorf(L("image %s is missing and cannot be fetched"), image) @@ -203,21 +203,18 @@ func GetPulledImageName(image string) (string, error) { return string(bytes.TrimSpace(out)), nil } -func pullImage(image string, args ...string) error { +func pullImage(authFile string, image string) error { if utils.ContainsUpperCase(image) { return fmt.Errorf(L("%s should contains just lower case character, otherwise podman pull would fails"), image) } log.Info().Msgf(L("Running podman pull %s"), image) - podmanImageArgs := []string{"pull", image} - podmanArgs := append(podmanImageArgs, args...) + podmanArgs := []string{"pull", image} - loglevel := zerolog.DebugLevel - if len(args) > 0 { - loglevel = zerolog.Disabled - log.Debug().Msg("Additional arguments for pull command will not be shown.") + if authFile != "" { + podmanArgs = append(podmanArgs, "--authfile", authFile) } - return utils.RunCmdStdMapping(loglevel, "podman", podmanArgs...) + return utils.RunCmdStdMapping(zerolog.DebugLevel, "podman", podmanArgs...) } // ShowAvailableTag returns the list of available tag for a given image. diff --git a/shared/podman/login.go b/shared/podman/login.go new file mode 100644 index 000000000..253fd4ff2 --- /dev/null +++ b/shared/podman/login.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package podman + +import ( + "encoding/base64" + "fmt" + "os" + + . "github.com/uyuni-project/uyuni-tools/shared/l10n" + "github.com/uyuni-project/uyuni-tools/shared/utils" +) + +// PodmanLogin logs in the registry.suse.com registry if needed and returns an authentication file, a cleanup function and an error. +func PodmanLogin() (string, func(), error) { + creds, err := inspectHost() + if err != nil { + return "", nil, err + } + + if creds.SccPassword != "" && creds.SccUsername != "" { + // We have SCC credentials, so we are pretty likely to need registry.suse.com + token := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", creds.SccUsername, creds.SccPassword))) + authFileContent := fmt.Sprintf(`{ + "auths": { + "registry.suse.com" : { + "auth": "%s" + } + } +}`, token) + authFile, err := os.CreateTemp("", "mgradm-") + if err != nil { + return "", nil, err + } + authFilePath := authFile.Name() + + if _, err := authFile.Write([]byte(authFileContent)); err != nil { + os.Remove(authFilePath) + return "", nil, err + } + + if err := authFile.Close(); err != nil { + os.Remove(authFilePath) + return "", nil, utils.Errorf(err, L("failed to close the temporary auth file")) + } + + return authFilePath, func() { + os.Remove(authFilePath) + }, nil + } + + return "", func() {}, nil +} diff --git a/shared/podman/systemd.go b/shared/podman/systemd.go index c49b1524f..63c9b99a6 100644 --- a/shared/podman/systemd.go +++ b/shared/podman/systemd.go @@ -277,7 +277,7 @@ func GenerateSystemdConfFile(serviceName string, section string, body string) er systemdConfFilePath := path.Join(systemdConfFolder, section+".conf") content := []byte("[" + section + "]" + "\n" + body + "\n") - if err := os.WriteFile(systemdConfFilePath, content, 0644); err != nil { + if err := os.WriteFile(systemdConfFilePath, content, 0640); err != nil { return utils.Errorf(err, L("cannot write %s file"), systemdConfFilePath) } diff --git a/shared/podman/utils.go b/shared/podman/utils.go index f7cae3c2b..29807d224 100644 --- a/shared/podman/utils.go +++ b/shared/podman/utils.go @@ -173,48 +173,43 @@ func isVolumePresent(volume string) bool { } // Inspect check values on a given image and deploy. -func Inspect(serverImage string, pullPolicy string, proxyHost bool) (map[string]string, error) { +func Inspect(serverImage string, pullPolicy string, proxyHost bool) (*utils.ServerInspectData, error) { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { - return map[string]string{}, utils.Errorf(err, L("failed to create temporary directory")) + return nil, utils.Errorf(err, L("failed to create temporary directory")) } - inspectedHostValues, err := utils.InspectHost(proxyHost) + authFile, cleaner, err := PodmanLogin() if err != nil { - return map[string]string{}, utils.Errorf(err, L("cannot inspect host values")) + return nil, utils.Errorf(err, L("failed to login to registry.suse.com")) } + defer cleaner() - pullArgs := []string{} - _, scc_user_exist := inspectedHostValues["host_scc_username"] - _, scc_user_password := inspectedHostValues["host_scc_password"] - if scc_user_exist && scc_user_password { - pullArgs = append(pullArgs, "--creds", inspectedHostValues["host_scc_username"]+":"+inspectedHostValues["host_scc_password"]) - } - - preparedImage, err := PrepareImage(serverImage, pullPolicy, pullArgs...) + preparedImage, err := PrepareImage(authFile, serverImage, pullPolicy) if err != nil { - return map[string]string{}, err + return nil, err } - if err := utils.GenerateInspectContainerScript(scriptDir); err != nil { - return map[string]string{}, err + inspector := utils.NewServerInspector(scriptDir) + if err := inspector.GenerateScript(); err != nil { + return nil, err } podmanArgs := []string{ - "-v", scriptDir + ":" + utils.InspectOutputFile.Directory, + "-v", scriptDir + ":" + utils.InspectContainerDirectory, "--security-opt", "label=disable", } err = RunContainer("uyuni-inspect", preparedImage, utils.ServerVolumeMounts, podmanArgs, - []string{utils.InspectOutputFile.Directory + "/" + utils.InspectScriptFilename}) + []string{utils.InspectContainerDirectory + "/" + utils.InspectScriptFilename}) if err != nil { - return map[string]string{}, err + return nil, err } - inspectResult, err := utils.ReadInspectData(scriptDir) + inspectResult, err := inspector.ReadInspectData() if err != nil { - return map[string]string{}, utils.Errorf(err, L("cannot inspect data")) + return nil, utils.Errorf(err, L("cannot inspect data")) } return inspectResult, err diff --git a/shared/test_utils/asserts.go b/shared/test_utils/asserts.go new file mode 100644 index 000000000..459694896 --- /dev/null +++ b/shared/test_utils/asserts.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package test_utils + +import "testing" + +// AssertEquals ensures two values are equals and raises and error if not. +func AssertEquals[T comparable](t *testing.T, message string, expected T, actual T) { + if actual != expected { + t.Errorf(message+": got '%v' expected '%v'", actual, expected) + } +} diff --git a/shared/test_utils/files.go b/shared/test_utils/files.go new file mode 100644 index 000000000..82c9f19b1 --- /dev/null +++ b/shared/test_utils/files.go @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package test_utils + +import ( + "os" + "testing" +) + +// CreateTmpFolder creates a temporary folder for testing purposes and returns its path and a cleanup function. +func CreateTmpFolder(t *testing.T) (string, func()) { + testDir, err := os.MkdirTemp("", "uyuni-tools-test-*") + if err != nil { + t.Fatalf("failed to create temporary directory: %s", err) + } + + return testDir, func() { + defer os.RemoveAll(testDir) + } +} + +// WriteFile writes the content in a file at the given path and fails if anything wrong happens. +func WriteFile(t *testing.T, path string, content string) { + if err := os.WriteFile(path, []byte(content), 0755); err != nil { + t.Fatalf("failed to write test file %s: %s", path, err) + } +} + +// ReadFile returns the content of a file as a string and fails is anything wrong happens. +func ReadFile(t *testing.T, path string) string { + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read file %s: %s", path, err) + } + return string(content) +} diff --git a/shared/types/inspect.go b/shared/types/inspect.go index 7b23e3281..77e8539ee 100644 --- a/shared/types/inspect.go +++ b/shared/types/inspect.go @@ -13,20 +13,10 @@ type InspectData struct { Proxy bool } -/* InspectFile represent where the inspect file should be stored -* and the command to run in the container. - */ -type InspectFile struct { - Directory string - Basename string - Commands []InspectData -} - // NewInspectData creates an InspectData instance. -func NewInspectData(variable string, cli string, proxy bool) InspectData { +func NewInspectData(variable string, cli string) InspectData { return InspectData{ Variable: variable, CLI: cli, - Proxy: proxy, } } diff --git a/shared/utils/inspector.go b/shared/utils/inspector.go new file mode 100644 index 000000000..d6c31c287 --- /dev/null +++ b/shared/utils/inspector.go @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "bytes" + "os" + "path" + + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + . "github.com/uyuni-project/uyuni-tools/shared/l10n" + "github.com/uyuni-project/uyuni-tools/shared/templates" + "github.com/uyuni-project/uyuni-tools/shared/types" +) + +// InspectScriptFilename is the inspect script basename. +const InspectScriptFilename = "inspect.sh" + +// InspectOutputFile represents the directory and the basename where the inspect values are stored. +const InspectContainerDirectory = "/var/lib/uyuni-tools" +const inspectDataFile = "data" + +// Inspector implementations can generate a inspection script and parse its results. +// The returned type T depends on the data that are written to the file. +type Inspector[T any] interface { + // GenerateScript writes the inspection script in ScriptDir. + GenerateScript() error + + // Read the data generated by the inspection script and unmarshal them an object. + ReadInspectData() (*T, error) + + // Return the path to the data file. + GetDataPath() string + + // Return the path to the script file. + GetScriptPath() string +} + +// BaseInspector is offering the basic implementation for the Inspector interface. +type BaseInspector struct { + ScriptDir string + Values []types.InspectData +} + +// GenerateScript is a common implementation for all inspectors. +func (i *BaseInspector) GenerateScript() error { + log.Debug().Msgf("Generating inspect script in %s", i.GetScriptPath()) + data := templates.InspectTemplateData{ + Param: i.Values, + OutputFile: i.GetDataPath(), + } + + if err := WriteTemplateToFile(data, i.GetScriptPath(), 0555, true); err != nil { + return Errorf(err, L("failed to generate inspect script")) + } + return nil +} + +// Return the path to the data file. +func (i *BaseInspector) GetDataPath() string { + return path.Join(i.ScriptDir, inspectDataFile) +} + +// Return the path to the script file. +func (i *BaseInspector) GetScriptPath() string { + return path.Join(i.ScriptDir, InspectScriptFilename) +} + +// ReadInspectData returns an unmarshalled object of type T from the data file. +// +// This function is most likely to be used for the implementation of the inspectors, but can also be used directly. +func ReadInspectData[T any](dataFile string) (*T, error) { + log.Debug().Msgf("Trying to read %s", dataFile) + data, err := os.ReadFile(dataFile) + if err != nil { + return nil, Errorf(err, L("cannot read file %s"), dataFile) + } + + viper.SetConfigType("env") + if err := viper.MergeConfig(bytes.NewBuffer(data)); err != nil { + return nil, Errorf(err, L("cannot read config")) + } + + var inspectResult T + if err := viper.Unmarshal(&inspectResult); err != nil { + return nil, Errorf(err, L("failed to unmarshal the inspected data")) + } + return &inspectResult, nil +} diff --git a/shared/utils/inspector_test.go b/shared/utils/inspector_test.go new file mode 100644 index 000000000..8ba60fdb6 --- /dev/null +++ b/shared/utils/inspector_test.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "path" + "testing" + + "github.com/uyuni-project/uyuni-tools/shared/test_utils" +) + +func TestReadInspectData(t *testing.T) { + content := `Timezone=Europe/Berlin +image_pg_version=16 +current_pg_version=14 +db_user=myuser +db_password=mysecret +db_name=mydb +db_port=1234 +` + + testDir, cleaner := test_utils.CreateTmpFolder(t) + defer cleaner() + + dataPath := path.Join(testDir, "data") + test_utils.WriteFile(t, dataPath, content) + + actual, err := ReadInspectData[InspectResult](dataPath) + if err != nil { + t.Fatalf("Unexpected failure: %s", err) + } + + test_utils.AssertEquals(t, "Invalid timezone", "Europe/Berlin", actual.Timezone) + test_utils.AssertEquals(t, "Invalid current postgresql version", "14", actual.CurrentPgVersion) + test_utils.AssertEquals(t, "Invalid image postgresql version", "16", actual.ImagePgVersion) + test_utils.AssertEquals(t, "Invalid DB user", "myuser", actual.DbUser) + test_utils.AssertEquals(t, "Invalid DB password", "mysecret", actual.DbPassword) + test_utils.AssertEquals(t, "Invalid DB name", "mydb", actual.DbName) + test_utils.AssertEquals(t, "Invalid DB port", 1234, actual.DbPort) +} diff --git a/shared/utils/serverinspector.go b/shared/utils/serverinspector.go new file mode 100644 index 000000000..802becf0d --- /dev/null +++ b/shared/utils/serverinspector.go @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "github.com/uyuni-project/uyuni-tools/shared/types" +) + +// ServerInspector inspects a running server container or its image. +type ServerInspector struct { + BaseInspector +} + +// NewServerInspector creates a new ServerInspector generating the inspection script and data in scriptDir. +func NewServerInspector(scriptDir string) ServerInspector { + base := BaseInspector{ + Values: []types.InspectData{ + types.NewInspectData( + "uyuni_release", + "cat /etc/*release | grep 'Uyuni release' | cut -d ' ' -f3 || true"), + types.NewInspectData( + "suse_manager_release", + "cat /etc/*release | grep 'SUSE Manager release' | cut -d ' ' -f4 || true"), + types.NewInspectData( + "fqdn", + "cat /etc/rhn/rhn.conf 2>/dev/null | grep 'java.hostname' | cut -d' ' -f3 || true"), + types.NewInspectData( + "image_pg_version", + "rpm -qa --qf '%{VERSION}\\n' 'name=postgresql[0-8][0-9]-server' | cut -d. -f1 | sort -n | tail -1 || true"), + types.NewInspectData("current_pg_version", + "(test -e /var/lib/pgsql/data/PG_VERSION && cat /var/lib/pgsql/data/PG_VERSION) || true"), + types.NewInspectData("db_user", + "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_user' | cut -d' ' -f3 || true"), + types.NewInspectData("db_password", + "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_password' | cut -d' ' -f3 || true"), + types.NewInspectData("db_name", + "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_name' | cut -d' ' -f3 || true"), + types.NewInspectData("db_port", + "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_port' | cut -d' ' -f3 || true"), + }, + ScriptDir: scriptDir, + } + return ServerInspector{ + BaseInspector: base, + } +} + +// CommonInspectData are data common between the migration source inspect and server inspector results. +type CommonInspectData struct { + CurrentPgVersion string `mapstructure:"current_pg_version"` + ImagePgVersion string `mapstructure:"image_pg_version"` + DbUser string `mapstructure:"db_user"` + DbPassword string `mapstructure:"db_password"` + DbName string `mapstructure:"db_name"` + DbPort int `mapstructure:"db_port"` +} + +// ServerInspectData are the data extracted by a server inspector. +type ServerInspectData struct { + CommonInspectData `mapstructure:",squash"` + UyuniRelease string `mapstructure:"uyuni_release"` + SuseManagerRelease string `mapstructure:"suse_manager_release"` + Fqdn string +} + +// ReadInspectData parses the data generated by the server inspector. +func (i *ServerInspector) ReadInspectData() (*ServerInspectData, error) { + return ReadInspectData[ServerInspectData](i.GetDataPath()) +} diff --git a/shared/utils/serverinspector_test.go b/shared/utils/serverinspector_test.go new file mode 100644 index 000000000..fe1a48a54 --- /dev/null +++ b/shared/utils/serverinspector_test.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 SUSE LLC +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "path" + "testing" + + "github.com/uyuni-project/uyuni-tools/shared/test_utils" +) + +func TestServerInspectorGenerate(t *testing.T) { + testDir, cleaner := test_utils.CreateTmpFolder(t) + defer cleaner() + + inspector := NewServerInspector(testDir) + if err := inspector.GenerateScript(); err != nil { + t.Errorf("Unexpected error %s", err) + } + + dataPath := inspector.GetDataPath() + + expected := `#!/bin/bash +# inspect.sh, generated by mgradm +echo "uyuni_release=$(cat /etc/*release | grep 'Uyuni release' | cut -d ' ' -f3 || true)" >> ` + dataPath + ` +echo "suse_manager_release=$(cat /etc/*release | grep 'SUSE Manager release' | cut -d ' ' -f4 || true)" >> ` + dataPath + ` +echo "fqdn=$(cat /etc/rhn/rhn.conf 2>/dev/null | grep 'java.hostname' | cut -d' ' -f3 || true)" >> ` + dataPath + ` +echo "image_pg_version=$(rpm -qa --qf '%{VERSION}\n' 'name=postgresql[0-8][0-9]-server' | cut -d. -f1 | sort -n | tail -1 || true)" >> ` + dataPath + ` +echo "current_pg_version=$((test -e /var/lib/pgsql/data/PG_VERSION && cat /var/lib/pgsql/data/PG_VERSION) || true)" >> ` + dataPath + ` +echo "db_user=$(cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_user' | cut -d' ' -f3 || true)" >> ` + dataPath + ` +echo "db_password=$(cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_password' | cut -d' ' -f3 || true)" >> ` + dataPath + ` +echo "db_name=$(cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_name' | cut -d' ' -f3 || true)" >> ` + dataPath + ` +echo "db_port=$(cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_port' | cut -d' ' -f3 || true)" >> ` + dataPath + ` +exit 0 +` + + actual := test_utils.ReadFile(t, path.Join(testDir, InspectScriptFilename)) + test_utils.AssertEquals(t, "Wrongly generated script", expected, actual) +} + +func TestServerInspectorParse(t *testing.T) { + testDir, cleaner := test_utils.CreateTmpFolder(t) + defer cleaner() + + inspector := NewServerInspector(testDir) + + content := ` +uyuni_release=2024.5 +suse_manager_release=5.0 +fqdn=my.server.name +image_pg_version=16 +current_pg_version=14 +db_user=myuser +db_password=mysecret +db_name=mydb +db_port=1234 +` + test_utils.WriteFile(t, inspector.GetDataPath(), content) + + actual, err := inspector.ReadInspectData() + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + test_utils.AssertEquals(t, "Invalid uyuni release", "2024.5", actual.UyuniRelease) + test_utils.AssertEquals(t, "Invalid SUSE Manager release", "5.0", actual.SuseManagerRelease) + test_utils.AssertEquals(t, "Invalid FQDN", "my.server.name", actual.Fqdn) + test_utils.AssertEquals(t, "Invalid current postgresql version", "14", actual.CurrentPgVersion) + test_utils.AssertEquals(t, "Invalid image postgresql version", "16", actual.ImagePgVersion) + test_utils.AssertEquals(t, "Invalid DB user", "myuser", actual.DbUser) + test_utils.AssertEquals(t, "Invalid DB password", "mysecret", actual.DbPassword) + test_utils.AssertEquals(t, "Invalid DB name", "mydb", actual.DbName) + test_utils.AssertEquals(t, "Invalid DB port", 1234, actual.DbPort) +} diff --git a/shared/utils/utils.go b/shared/utils/utils.go index 018d8d764..e21daa101 100644 --- a/shared/utils/utils.go +++ b/shared/utils/utils.go @@ -15,7 +15,6 @@ import ( "net/http" "os" "path" - "path/filepath" "regexp" "strconv" "strings" @@ -24,9 +23,7 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/spf13/viper" . "github.com/uyuni-project/uyuni-tools/shared/l10n" - "github.com/uyuni-project/uyuni-tools/shared/templates" "github.com/uyuni-project/uyuni-tools/shared/types" "golang.org/x/term" ) @@ -36,29 +33,10 @@ const prompt_end = ": " var prodVersionArchRegex = regexp.MustCompile(`suse\/manager\/.*:`) var imageValid = regexp.MustCompile("^((?:[^:/]+(?::[0-9]+)?/)?[^:]+)(?::([^:]+))?$") -// InspectScriptFilename is the inspect script basename. -var InspectScriptFilename = "inspect.sh" - -var inspectValues = []types.InspectData{ - types.NewInspectData("uyuni_release", "cat /etc/*release | grep 'Uyuni release' | cut -d ' ' -f3 || true", false), - types.NewInspectData("suse_manager_release", "cat /etc/*release | grep 'SUSE Manager release' | cut -d ' ' -f4 || true", false), - types.NewInspectData("architecture", "lscpu | grep Architecture | awk '{print $2}' || true", false), - types.NewInspectData("fqdn", "cat /etc/rhn/rhn.conf 2>/dev/null | grep 'java.hostname' | cut -d' ' -f3 || true", false), - types.NewInspectData("image_pg_version", "rpm -qa --qf '%{VERSION}\\n' 'name=postgresql[0-8][0-9]-server' | cut -d. -f1 | sort -n | tail -1 || true", false), - types.NewInspectData("current_pg_version", "(test -e /var/lib/pgsql/data/PG_VERSION && cat /var/lib/pgsql/data/PG_VERSION) || true", false), - types.NewInspectData("registration_info", "env LC_ALL=C LC_MESSAGES=C LANG=C transactional-update --quiet register --status 2>/dev/null || true", false), - types.NewInspectData("scc_username", "cat /etc/zypp/credentials.d/SCCcredentials 2>&1 /dev/null | grep username | cut -d= -f2 || true", true), - types.NewInspectData("scc_password", "cat /etc/zypp/credentials.d/SCCcredentials 2>&1 /dev/null | grep password | cut -d= -f2 || true", true), - types.NewInspectData("db_user", "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_user' | cut -d' ' -f3 || true", false), - types.NewInspectData("db_password", "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_password' | cut -d' ' -f3 || true", false), - types.NewInspectData("db_name", "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_name' | cut -d' ' -f3 || true", false), - types.NewInspectData("db_port", "cat /etc/rhn/rhn.conf 2>/dev/null | grep '^db_port' | cut -d' ' -f3 || true", false), -} - -// InspectOutputFile represents the directory and the basename where the inspect values are stored. -var InspectOutputFile = types.InspectFile{ - Directory: "/var/lib/uyuni-tools", - Basename: "data", +// InspectResult holds the results of the inspection scripts. +type InspectResult struct { + CommonInspectData `mapstructure:",squash"` + Timezone string } func checkValueSize(value string, min int, max int) bool { @@ -358,90 +336,6 @@ func DownloadFile(filepath string, URL string) (err error) { return nil } -// ReadInspectData returns a map with the values inspected by an image and deploy. -func ReadInspectData(scriptDir string, prefix ...string) (map[string]string, error) { - path := filepath.Join(scriptDir, "data") - log.Debug().Msgf("Trying to read %s", path) - data, err := os.ReadFile(path) - if err != nil { - return map[string]string{}, Errorf(err, L("cannot parse file %s"), path) - } - - inspectResult := make(map[string]string) - - viper.SetConfigType("env") - if err := viper.MergeConfig(bytes.NewBuffer(data)); err != nil { - return map[string]string{}, Errorf(err, L("cannot read config")) - } - - for _, v := range inspectValues { - if len(viper.GetString(v.Variable)) > 0 { - index := v.Variable - /* Just the first value of prefix is used. - * This slice is just to allow an empty argument - */ - if len(prefix) >= 1 { - index = prefix[0] + v.Variable - } - inspectResult[index] = viper.GetString(v.Variable) - } - } - return inspectResult, nil -} - -// InspectHost check values on a host machine. -func InspectHost(serverHost bool) (map[string]string, error) { - scriptDir, err := os.MkdirTemp("", "mgradm-*") - defer os.RemoveAll(scriptDir) - if err != nil { - return map[string]string{}, Errorf(err, L("failed to create temporary directory")) - } - - if err := GenerateInspectHostScript(scriptDir, serverHost); err != nil { - return map[string]string{}, err - } - - if err := RunCmdStdMapping(zerolog.DebugLevel, scriptDir+"/inspect.sh"); err != nil { - return map[string]string{}, Errorf(err, L("failed to run inspect script in host system")) - } - - inspectResult, err := ReadInspectData(scriptDir, "host_") - if err != nil { - return map[string]string{}, Errorf(err, L("cannot inspect host data")) - } - - return inspectResult, err -} - -// GenerateInspectContainerScript create the host inspect script. -func GenerateInspectHostScript(scriptDir string, proxyHost bool) error { - data := templates.InspectTemplateData{ - Param: inspectValues, - OutputFile: scriptDir + "/" + InspectOutputFile.Basename, - ProxyHost: proxyHost, - } - - scriptPath := filepath.Join(scriptDir, InspectScriptFilename) - if err := WriteTemplateToFile(data, scriptPath, 0555, true); err != nil { - return Errorf(err, L("failed to generate inspect script")) - } - return nil -} - -// GenerateInspectContainerScript create the container inspect script. -func GenerateInspectContainerScript(scriptDir string) error { - data := templates.InspectTemplateData{ - Param: inspectValues, - OutputFile: InspectOutputFile.Directory + "/" + InspectOutputFile.Basename, - } - - scriptPath := filepath.Join(scriptDir, InspectScriptFilename) - if err := WriteTemplateToFile(data, scriptPath, 0555, true); err != nil { - return Errorf(err, L("failed to generate inspect script")) - } - return nil -} - // CompareVersion compare the server image version and the server deployed version. func CompareVersion(imageVersion string, deployedVersion string) int { re := regexp.MustCompile(`\((.*?)\)`) diff --git a/uyuni-tools.changes.cbosdo.coco-migrate b/uyuni-tools.changes.cbosdo.coco-migrate new file mode 100644 index 000000000..9b74ac3cb --- /dev/null +++ b/uyuni-tools.changes.cbosdo.coco-migrate @@ -0,0 +1,2 @@ +- Setup confidential computing container during migration + (bsc#1227588) diff --git a/uyuni-tools.changes.cbosdo.inspect-refactoring b/uyuni-tools.changes.cbosdo.inspect-refactoring new file mode 100644 index 000000000..101373f96 --- /dev/null +++ b/uyuni-tools.changes.cbosdo.inspect-refactoring @@ -0,0 +1 @@ +- Clean the inspection code to make it faster