diff --git a/cmd/openshift-install/testdata/agent/unconfigured-ignition/configurations/interactive.txt b/cmd/openshift-install/testdata/agent/unconfigured-ignition/configurations/interactive.txt index f4990fad8cf..f29555b52c6 100644 --- a/cmd/openshift-install/testdata/agent/unconfigured-ignition/configurations/interactive.txt +++ b/cmd/openshift-install/testdata/agent/unconfigured-ignition/configurations/interactive.txt @@ -90,11 +90,12 @@ status: debugInfo: eventsURL: "" -- expected/rendezvous-host.env -- +#{{ $$isIPv6 := false }}{{ $$host := .RendezvousIP }}{{ range ( len .RendezvousIP ) }}{{if eq ( index ( slice $$host . ) 0 ) ':'}}{{ $$isIPv6 = true }}{{ end }}{{ end }} NODE_ZERO_IP={{.RendezvousIP}} -SERVICE_BASE_URL=http://{{.RendezvousIP}}:8090/ -IMAGE_SERVICE_BASE_URL=http://{{.RendezvousIP}}:8888/ +SERVICE_BASE_URL=http://{{ if $$isIPv6 }}{{ printf "[%s]" .RendezvousIP }}{{ else }}{{ .RendezvousIP }}{{ end }}:8090/ +IMAGE_SERVICE_BASE_URL=http://{{ if $$isIPv6 }}{{ printf "[%s]" .RendezvousIP }}{{ else }}{{ .RendezvousIP }}{{ end }}:8888/ PULL_SECRET_TOKEN= USER_AUTH_TOKEN= WORKFLOW_TYPE=install-interactive-disconnected -AIUI_APP_API_URL=http://{{.RendezvousIP}}:8090/ -AIUI_URL=http://{{.RendezvousIP}}:3001/ \ No newline at end of file +AIUI_APP_API_URL=http://{{ if $$isIPv6 }}{{ printf "[%s]" .RendezvousIP }}{{ else }}{{ .RendezvousIP }}{{ end }}:8090/ +AIUI_URL=http://{{ if $$isIPv6 }}{{ printf "[%s]" .RendezvousIP }}{{ else }}{{ .RendezvousIP }}{{ end }}:3001/ diff --git a/data/data/agent/files/usr/local/bin/install-status.sh b/data/data/agent/files/usr/local/bin/install-status.sh index 01a7855bc68..12c2892b793 100644 --- a/data/data/agent/files/usr/local/bin/install-status.sh +++ b/data/data/agent/files/usr/local/bin/install-status.sh @@ -9,7 +9,7 @@ inactive_services() { if [ -f "/etc/assisted/interactive-ui" ]; then # interactive workflow - services+=" agent-start-ui.service" + services+=" agent-ui.service" elif [ -f "/etc/assisted/add-nodes.env" ]; then # add nodes workflow services+=" agent-import-cluster.service agent-register-infraenv.service apply-host-config.service agent-add-node.service" @@ -53,7 +53,7 @@ check_host_config() { check_ui() { local ui_issue="90_ui-availability" - if systemctl is-active --quiet "agent-start-ui"; then + if systemctl is-active --quiet "agent-ui"; then printf '\\e{green}Please go to \\e{lightgreen}%s\\e{reset}\\e{green} in your browser to continue the installation\\e{reset}' "${AIUI_URL}" | set_issue "${ui_issue}" else clear_issue "${ui_issue}" diff --git a/data/data/agent/systemd/units/agent-check-config-image.service b/data/data/agent/systemd/units/agent-check-config-image.service index d480f8b7058..39e7759d531 100644 --- a/data/data/agent/systemd/units/agent-check-config-image.service +++ b/data/data/agent/systemd/units/agent-check-config-image.service @@ -2,6 +2,7 @@ Description=Displays message to console if config image has not been inserted or mounted in unconfigured-ignition environment Wants=network-online.target After=network-online.target +ConditionPathExists=!/etc/assisted/no-config-image [Service] Type=oneshot diff --git a/data/data/agent/systemd/units/agent-extract-tui.service b/data/data/agent/systemd/units/agent-extract-tui.service index e20f02046ea..1cc7d7ca3fc 100644 --- a/data/data/agent/systemd/units/agent-extract-tui.service +++ b/data/data/agent/systemd/units/agent-extract-tui.service @@ -1,9 +1,9 @@ [Unit] Description=Extract agent-tui at boot After=selinux.service start-local-registry.service -Before=agent-interactive-console.service -ConditionPathExists=/etc/assisted/rendezvous-host.env +ConditionPathExists=/etc/assisted/rendezvous-host.env.template ConditionPathExists=/etc/assisted/interactive-ui +ConditionPathExists=!/usr/local/bin/agent-tui [Service] Type=oneshot @@ -11,4 +11,3 @@ ExecStart=/usr/local/bin/agent-extract-tui.sh TimeoutStartSec=300s [Install] -WantedBy=getty@tty1.service diff --git a/data/data/agent/systemd/units/agent-start-ui.service.template b/data/data/agent/systemd/units/agent-ui.service.template similarity index 63% rename from data/data/agent/systemd/units/agent-start-ui.service.template rename to data/data/agent/systemd/units/agent-ui.service.template index c74390744a5..1b33ed7f027 100644 --- a/data/data/agent/systemd/units/agent-start-ui.service.template +++ b/data/data/agent/systemd/units/agent-ui.service.template @@ -1,9 +1,6 @@ -### This is currently a draft placeholder for running the UI, to be completed - [Unit] -Description=Service that runs the Agent Installer UI +Description=Agent Installer Web UI Wants=network-online.target assisted-service.service -Conflicts=agent-register-cluster.service agent-import-cluster.service After=network-online.target assisted-service.service PartOf=assisted-service-pod.service ConditionPathExists=/etc/assisted/node0 @@ -13,15 +10,15 @@ ConditionPathExists=/etc/assisted/interactive-ui Environment=PODMAN_SYSTEMD_UNIT=%n EnvironmentFile=/usr/local/share/assisted-service/agent-images.env EnvironmentFile=/etc/assisted/rendezvous-host.env +Restart=on-failure ExecStartPre=/bin/rm -f %t/%n.ctr-id ExecStartPre=/usr/local/bin/wait-for-assisted-service.sh -ExecStart=/usr/bin/podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --restart=on-failure:10 --pod-id-file=%t/assisted-service-pod.pod-id --replace -d --name=agent-installer-ui --env AIUI_APP_API_URL $INSTALLER_UI_IMAGE +ExecStart=/usr/bin/podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --rm --pod-id-file=%t/assisted-service-pod.pod-id --replace -d --name=agent-installer-ui --env AIUI_APP_API_URL $INSTALLER_UI_IMAGE ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id KillMode=mixed -Type=oneshot -RemainAfterExit=true +Type=notify [Install] WantedBy=multi-user.target diff --git a/docs/user/agent/agent-services.md b/docs/user/agent/agent-services.md index 60b59b7eef3..62a0822301c 100644 --- a/docs/user/agent/agent-services.md +++ b/docs/user/agent/agent-services.md @@ -61,5 +61,5 @@ The interactive workflow allows the user to install a cluster by using the assis the agent-tui is also used interactively to configure which node will be the rendezvous host, and to configure accordingly the other nodes. * agent-extract-tui - extracts agent-tui and nmstate libraries from the agent-installer-utils image during boot -* agent-start-ui - runs the assisted UI on the rendezvous node +* agent-ui - runs the assisted UI on the rendezvous node diff --git a/internal/tshelpers/custom_commands.go b/internal/tshelpers/custom_commands.go index 738cf1157db..f2e1b3d4ea8 100644 --- a/internal/tshelpers/custom_commands.go +++ b/internal/tshelpers/custom_commands.go @@ -397,6 +397,9 @@ func archiveFileNames(isoPath string) (string, string, error) { func expand(ts *testscript.TestScript, s []byte) string { return os.Expand(string(s), func(key string) string { + if key == "$" { + return "$" + } return ts.Getenv(key) }) } @@ -431,7 +434,7 @@ func byteCompareInternal(ts *testscript.TestScript, neg bool, aData, eData []byt return } - ts.Logf(aText) + ts.Logf("%s", aText) var sb strings.Builder if err := diff.Text(eFilePath, aFilePath, eText, aText, &sb); err != nil { diff --git a/pkg/asset/agent/image/ignition.go b/pkg/asset/agent/image/ignition.go index 09f7cd977a0..a093aba9ac0 100644 --- a/pkg/asset/agent/image/ignition.go +++ b/pkg/asset/agent/image/ignition.go @@ -1,12 +1,12 @@ package image import ( + "bytes" "context" "encoding/json" "fmt" "html/template" "net" - "net/url" "path" "path/filepath" "strings" @@ -306,7 +306,7 @@ func (a *Ignition) Generate(ctx context.Context, dependencies asset.Parents) err rendezvousHostFile := ignition.FileFromString(rendezvousHostEnvPath, "root", 0644, - getRendezvousHostEnv(agentTemplateData.ServiceProtocol, a.RendezvousIP, authConfig.AgentAuthToken, authConfig.UserAuthToken, agentWorkflow.Workflow)) + getRendezvousHostEnv(agentTemplateData, a.RendezvousIP, agentWorkflow.Workflow)) config.Storage.Files = append(config.Storage.Files, rendezvousHostFile) err = addBootstrapScripts(&config, agentManifests.ClusterImageSet.Spec.ReleaseImage) @@ -370,11 +370,13 @@ func (a *Ignition) Generate(ctx context.Context, dependencies asset.Parents) err func getDefaultEnabledServices() []string { return []string{ + "agent-extract-tui.service", "agent-interactive-console.service", "agent-interactive-console-serial@.service", "agent-register-cluster.service", "agent-import-cluster.service", "agent-register-infraenv.service", + "agent-ui.service", "agent.service", "assisted-service-db.service", "assisted-service-pod.service", @@ -446,17 +448,12 @@ func getTemplateData(name, pullSecret, releaseImageList, releaseImage, releaseIm } } -func getRendezvousHostEnv(serviceProtocol, nodeZeroIP, agentAuthtoken, userAuthToken string, workflowType workflow.AgentWorkflowType) string { - serviceBaseURL := url.URL{ - Scheme: serviceProtocol, - Host: net.JoinHostPort(nodeZeroIP, "8090"), - Path: "/", - } - imageServiceBaseURL := url.URL{ - Scheme: serviceProtocol, - Host: net.JoinHostPort(nodeZeroIP, "8888"), - Path: "/", - } +func getRendezvousHostEnvTemplate(data *agentTemplateData, workflowType workflow.AgentWorkflowType) string { + host := "{{ if $isIPv6 }}{{ printf \"[%s]\" .RendezvousIP }}{{ else }}{{ .RendezvousIP }}{{ end }}" + serviceBaseURL := fmt.Sprintf("%s://%s:8090/", data.ServiceProtocol, host) + imageServiceBaseURL := fmt.Sprintf("%s://%s:8888/", data.ServiceProtocol, host) + uiBaseURL := fmt.Sprintf("%s://%s:3001/", data.ServiceProtocol, host) + // USER_AUTH_TOKEN is required to authenticate API requests against agent-installer-local auth type // and for the endpoints marked with userAuth security definition in assisted-service swagger.yaml. // PULL_SECRET_TOKEN contains the AGENT_AUTH_TOKEN and is required for the endpoints marked with agentAuth security definition in assisted-service swagger.yaml. @@ -469,27 +466,40 @@ func getRendezvousHostEnv(serviceProtocol, nodeZeroIP, agentAuthtoken, userAuthT // and ensure successful authentication. // In the absence of PULL_SECRET_TOKEN, the cluster installation will wait forever. - rendezvousHostEnv := fmt.Sprintf(`NODE_ZERO_IP=%s + rendezvousHostEnvTemplate := fmt.Sprintf(`#{{ $isIPv6 := false }}{{ $host := .RendezvousIP }}{{ range ( len .RendezvousIP ) }}{{if eq ( index ( slice $host . ) 0 ) ':'}}{{ $isIPv6 = true }}{{ end }}{{ end }} +NODE_ZERO_IP={{.RendezvousIP}} SERVICE_BASE_URL=%s IMAGE_SERVICE_BASE_URL=%s PULL_SECRET_TOKEN=%s USER_AUTH_TOKEN=%s WORKFLOW_TYPE=%s -`, nodeZeroIP, serviceBaseURL.String(), imageServiceBaseURL.String(), agentAuthtoken, userAuthToken, workflowType) - - if workflowType == workflow.AgentWorkflowTypeInstallInteractiveDisconnected { - uiBaseURL := url.URL{ - Scheme: serviceProtocol, - Host: net.JoinHostPort(nodeZeroIP, "3001"), - Path: "/", - } - uiEnv := fmt.Sprintf(`AIUI_APP_API_URL=%s +AIUI_APP_API_URL=%s AIUI_URL=%s -`, serviceBaseURL.String(), uiBaseURL.String()) - rendezvousHostEnv = fmt.Sprintf("%s%s", rendezvousHostEnv, uiEnv) +`, serviceBaseURL, imageServiceBaseURL, data.AgentAuthToken, data.UserAuthToken, workflowType, serviceBaseURL, uiBaseURL) + + return rendezvousHostEnvTemplate +} + +func getRendezvousHostEnvFromTemplate(hostEnvTemplate, nodeZeroIP string) (string, error) { + tmpl, err := template.New("rendezvous-host.env").Parse(hostEnvTemplate) + if err != nil { + return "", err + } + buf := &bytes.Buffer{} + if err := tmpl.Execute(buf, struct{ RendezvousIP string }{nodeZeroIP}); err != nil { + return "", err } + return buf.String(), nil +} - return rendezvousHostEnv +func getRendezvousHostEnv(data *agentTemplateData, nodeZeroIP string, workflowType workflow.AgentWorkflowType) string { + env, err := getRendezvousHostEnvFromTemplate( + getRendezvousHostEnvTemplate(data, workflowType), + nodeZeroIP) + if err != nil { + panic(err) + } + return env } func getAddNodesEnv(clusterInfo joiner.ClusterInfo, authTokenExpiry string) string { diff --git a/pkg/asset/agent/image/ignition_test.go b/pkg/asset/agent/image/ignition_test.go index 7b6bcaf5ee9..50e5d3ce040 100644 --- a/pkg/asset/agent/image/ignition_test.go +++ b/pkg/asset/agent/image/ignition_test.go @@ -124,9 +124,13 @@ func TestIgnition_getRendezvousHostEnv(t *testing.T) { nodeZeroIP := "2001:db8::dead:beef" agentAuthtoken := "agentAuthtoken" userAuthToken := "userAuthToken" - rendezvousHostEnv := getRendezvousHostEnv("http", nodeZeroIP, agentAuthtoken, userAuthToken, workflow.AgentWorkflowTypeInstall) + rendezvousHostEnv := getRendezvousHostEnv(&agentTemplateData{ + ServiceProtocol: "http", + AgentAuthToken: agentAuthtoken, + UserAuthToken: userAuthToken, + }, nodeZeroIP, workflow.AgentWorkflowTypeInstall) assert.Equal(t, - "NODE_ZERO_IP="+nodeZeroIP+"\nSERVICE_BASE_URL=http://["+nodeZeroIP+"]:8090/\nIMAGE_SERVICE_BASE_URL=http://["+nodeZeroIP+"]:8888/\nPULL_SECRET_TOKEN="+agentAuthtoken+"\nUSER_AUTH_TOKEN="+userAuthToken+"\nWORKFLOW_TYPE=install\n", + "#\nNODE_ZERO_IP="+nodeZeroIP+"\nSERVICE_BASE_URL=http://["+nodeZeroIP+"]:8090/\nIMAGE_SERVICE_BASE_URL=http://["+nodeZeroIP+"]:8888/\nPULL_SECRET_TOKEN="+agentAuthtoken+"\nUSER_AUTH_TOKEN="+userAuthToken+"\nWORKFLOW_TYPE=install\nAIUI_APP_API_URL=http://["+nodeZeroIP+"]:8090/\nAIUI_URL=http://["+nodeZeroIP+"]:3001/\n", rendezvousHostEnv) } diff --git a/pkg/asset/agent/image/unconfigured_ignition.go b/pkg/asset/agent/image/unconfigured_ignition.go index 9c8365e9b7c..3f66f0cdad1 100644 --- a/pkg/asset/agent/image/unconfigured_ignition.go +++ b/pkg/asset/agent/image/unconfigured_ignition.go @@ -2,7 +2,7 @@ package image import ( "context" - "net/url" + "fmt" "os" "path" "path/filepath" @@ -151,6 +151,7 @@ func (a *UnconfiguredIgnition) Generate(_ context.Context, dependencies asset.Pa a.CPUArch = *osImage.CPUArchitecture agentTemplateData := &agentTemplateData{ + ServiceProtocol: "http", PullSecret: pullSecretAsset.GetPullSecretData(), ReleaseImages: releaseImageList, ReleaseImage: clusterImageSet.Spec.ReleaseImage, @@ -160,46 +161,45 @@ func (a *UnconfiguredIgnition) Generate(_ context.Context, dependencies asset.Pa InfraEnvID: infraEnvID, OSImage: osImage, Proxy: infraEnv.Spec.Proxy, + AuthType: "none", } enabledServices := getDefaultEnabledServices() + rendezvousHostTemplateData := getRendezvousHostEnvTemplate(agentTemplateData, agentWorkflow.Workflow) + rendezvousHostTemplateFile := ignition.FileFromString(fmt.Sprintf("%s.template", rendezvousHostEnvPath), "root", 0644, rendezvousHostTemplateData) + config.Storage.Files = append(config.Storage.Files, rendezvousHostTemplateFile) + + rendezvousIP, err := RetrieveRendezvousIP(agentConfig.Config, nil, nmStateConfigs.Config) + if err == nil { + rendezvousHostData, err := getRendezvousHostEnvFromTemplate(rendezvousHostTemplateData, rendezvousIP) + if err != nil { + return err + } + rendezvousHostFile := ignition.FileFromString(rendezvousHostEnvPath, "root", 0644, rendezvousHostData) + config.Storage.Files = append(config.Storage.Files, rendezvousHostFile) + } + switch agentWorkflow.Workflow { case workflow.AgentWorkflowTypeInstall: agentTemplateData.ConfigImageFiles = strings.Join(GetConfigImageFiles(), ",") - // Enable the agent-check-config-image.service for the current workflow. - enabledServices = append(enabledServices, "agent-check-config-image.service") - case workflow.AgentWorkflowTypeInstallInteractiveDisconnected: // Add the rendezvous host file. Agent TUI will interact with that file in case // the rendezvous IP wasn't previously configured, by managing it as a template file. - rendezvousIP := "{{.RendezvousIP}}" - if agentConfig.Config != nil { - rendezvousIP = agentConfig.Config.RendezvousIP - } - // Avoids escaping in case the template parameter was used. - rendezvousHostData, err := url.QueryUnescape(getRendezvousHostEnv("http", rendezvousIP, "", "", agentWorkflow.Workflow)) - if err != nil { - return err + if rendezvousIP == "" { + rendezvousHostFile := ignition.FileFromString(rendezvousHostEnvPath, "root", 0644, rendezvousHostTemplateData) + config.Storage.Files = append(config.Storage.Files, rendezvousHostFile) } - rendezvousHostFile := ignition.FileFromString(rendezvousHostEnvPath, "root", 0644, rendezvousHostData) - config.Storage.Files = append(config.Storage.Files, rendezvousHostFile) // Explicitly disable the load-config-iso service, not required in the current flow // (even though disabled by default, the udev rule may require it). config.Storage.Files = append(config.Storage.Files, ignition.FileFromString("/etc/assisted/no-config-image", "root", 0644, "")) // Enable the UI service. - enabledServices = append(enabledServices, "agent-start-ui.service") interactiveUIFile := ignition.FileFromString("/etc/assisted/interactive-ui", "root", 0644, "") config.Storage.Files = append(config.Storage.Files, interactiveUIFile) - // Enable the agent-extract-tui service - enabledServices = append(enabledServices, "agent-extract-tui.service") - - // Let's disable the assisted-service authentication. - agentTemplateData.AuthType = "none" } // Required by assisted-service. @@ -242,6 +242,8 @@ func (a *UnconfiguredIgnition) Generate(_ context.Context, dependencies asset.Pa config.Storage.Files = append(config.Storage.Files, manifestFile) } + // the agent-check-config-image.service added only to the unconfigured ignition + enabledServices = append(enabledServices, "agent-check-config-image.service") err = bootstrap.AddSystemdUnits(&config, "agent/systemd/units", agentTemplateData, enabledServices) if err != nil { return err diff --git a/pkg/asset/agent/image/unconfigured_ignition_test.go b/pkg/asset/agent/image/unconfigured_ignition_test.go index 71e2a0aa04f..d83477cbc93 100644 --- a/pkg/asset/agent/image/unconfigured_ignition_test.go +++ b/pkg/asset/agent/image/unconfigured_ignition_test.go @@ -40,7 +40,7 @@ func TestUnconfiguredIgnition_Generate(t *testing.T) { "pre-network-manager-config.service": false, "oci-eval-user-data.service": true, "agent-check-config-image.service": true, - "agent-extract-tui.service": false}, + "agent-extract-tui.service": true}, }, { name: "interactive-disconnected-workflow-should-have-agent-extract-tui-service-enabled", @@ -218,6 +218,7 @@ func generatedFilesUnconfiguredIgnition(otherFiles ...string) []string { "/etc/assisted/manifests/pull-secret.yaml", "/etc/assisted/manifests/cluster-image-set.yaml", "/etc/assisted/manifests/infraenv.yaml", + "/etc/assisted/rendezvous-host.env.template", } unconfiguredIgnitionFiles = append(unconfiguredIgnitionFiles, otherFiles...) return append(unconfiguredIgnitionFiles, commonFiles()...)