diff --git a/docs/release-notes.md b/docs/release-notes.md index 5de191ab9..8fd6beb42 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ Starting with this release, ignition-validate binaries are signed with the ### Features - Add Azure blob support for fetching ignition configs +- Support IONOS Cloud ### Changes diff --git a/docs/supported-platforms.md b/docs/supported-platforms.md index eef319b22..22cfb5903 100644 --- a/docs/supported-platforms.md +++ b/docs/supported-platforms.md @@ -20,6 +20,7 @@ Ignition is currently supported for the following platforms: * [Hetzner Cloud] (`hetzner`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys are handled separately. * [Microsoft Hyper-V] (`hyperv`) - Ignition will read its configuration from the `ignition.config` key in pool 0 of the Hyper-V Data Exchange Service (KVP). Values are limited to approximately 1 KiB of text, so Ignition can also read and concatenate multiple keys named `ignition.config.0`, `ignition.config.1`, and so on. * [IBM Cloud] (`ibmcloud`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys are handled separately. +* [IONOS Cloud] (`ionoscloud`) - Ignition will read its configuration from an injected user-data file. Per default the user-data is injected in a disk or partition with the label `OEM` and location `/config/user-data`. If this is different for your image build process or configuration you can customize the location for fetching the data using the kernel flags `deviceLabelKernelFlag` and `userDataKernelFlag`. * [KubeVirt] (`kubevirt`) - Ignition will read its configuration from the instance userdata via config drive. Cloud SSH keys are handled separately. * Bare Metal (`metal`) - Use the `ignition.config.url` kernel parameter to provide a URL to the configuration. The URL can use the `http://`, `https://`, `tftp://`, `s3://`, `arn:`, or `gs://` schemes to specify a remote config. * [Nutanix] (`nutanix`) - Ignition will read its configuration from the instance userdata via config drive. Cloud SSH keys are handled separately. @@ -52,6 +53,7 @@ For most cloud providers, cloud SSH keys and custom network configuration are ha [Hetzner Cloud]: https://www.hetzner.com/cloud [Microsoft Hyper-V]: https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/ [IBM Cloud]: https://www.ibm.com/cloud/vpc +[IONOS Cloud]: https://cloud.ionos.com/ [KubeVirt]: https://kubevirt.io [Nutanix]: https://www.nutanix.com/products/ahv [OpenStack]: https://www.openstack.org/ diff --git a/internal/providers/ionoscloud/ionoscloud.go b/internal/providers/ionoscloud/ionoscloud.go new file mode 100644 index 000000000..830fd5d8b --- /dev/null +++ b/internal/providers/ionoscloud/ionoscloud.go @@ -0,0 +1,208 @@ +// Copyright 2024 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// NOTE: This provider is still EXPERIMENTAL. +// +// The IONOS Cloud provider fetches the ignition config from a user-data file. +// This file is created by the IONOS Cloud VM handler before the first boot +// and gets injected into a device at /config/user-data by default. +// +// The kernel parameters deviceLabelKernelFlag and userDataKernelFlag can be +// used during the build process of images and for the VM initialization to +// specify on which disk or partition the user-data is going to be injected. +// +// User data files with the directive #cloud-config and #!/bin/ will be ignored +// See for more: https://docs.ionos.com/cloud/compute-services/compute-engine/how-tos/boot-cloud-init + +package ionoscloud + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/coreos/ignition/v2/config/v3_6_experimental/types" + "github.com/coreos/ignition/v2/internal/distro" + "github.com/coreos/ignition/v2/internal/log" + "github.com/coreos/ignition/v2/internal/platform" + "github.com/coreos/ignition/v2/internal/providers/util" + "github.com/coreos/ignition/v2/internal/resource" + ut "github.com/coreos/ignition/v2/internal/util" + + "github.com/coreos/vcontext/report" +) + +const ( + deviceLabelKernelFlag = "ignition.config.device" + defaultDeviceLabel = "OEM" + userDataKernelFlag = "ignition.config.path" + defaultUserDataPath = "config/user-data" +) + +func init() { + platform.Register(platform.Provider{ + Name: "ionoscloud", + Fetch: fetchConfig, + }) +} + +func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) { + var data []byte + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + + dispatch := func(name string, fn func() ([]byte, error)) { + raw, err := fn() + if err != nil { + switch err { + case context.Canceled: + case context.DeadlineExceeded: + f.Logger.Err("timed out while fetching config from %s", name) + default: + f.Logger.Err("failed to fetch config from %s: %v", name, err) + } + return + } + + data = raw + cancel() + } + + deviceLabel, userDataPath, err := readFromKernelParams(f.Logger) + + if err != nil { + f.Logger.Err("couldn't read kernel parameters: %v", err) + return types.Config{}, report.Report{}, err + } + + if deviceLabel == "" { + deviceLabel = defaultDeviceLabel + } + + if userDataPath == "" { + userDataPath = defaultUserDataPath + } + + go dispatch( + "load config from disk", func() ([]byte, error) { + return fetchConfigFromDevice(f.Logger, ctx, deviceLabel, userDataPath) + }, + ) + + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + f.Logger.Info("disk was not available in time. Continuing without a config...") + } + + return util.ParseConfig(f.Logger, data) +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return (err == nil) +} + +func fetchConfigFromDevice(logger *log.Logger, + ctx context.Context, + deviceLabel string, + dataPath string, +) ([]byte, error) { + device := filepath.Join(distro.DiskByLabelDir(), deviceLabel) + for !fileExists(device) { + logger.Debug("disk (%q) not found. Waiting...", device) + select { + case <-time.After(time.Second): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + + logger.Debug("creating temporary mount point") + mnt, err := os.MkdirTemp("", "ignition-config") + if err != nil { + return nil, fmt.Errorf("failed to create temp directory: %v", err) + } + defer os.Remove(mnt) + + cmd := exec.Command(distro.MountCmd(), "-o", "ro", "-t", "auto", device, mnt) + if _, err := logger.LogCmd(cmd, "mounting disk"); err != nil { + return nil, err + } + defer func() { + _ = logger.LogOp( + func() error { + return ut.UmountPath(mnt) + }, + "unmounting %q at %q", device, mnt, + ) + }() + + if !fileExists(filepath.Join(mnt, dataPath)) { + return nil, nil + } + + contents, err := os.ReadFile(filepath.Join(mnt, dataPath)) + if err != nil { + return nil, err + } + + if util.IsCloudConfig(contents) { + logger.Debug("disk (%q) contains a cloud-config configuration, ignoring", device) + return nil, nil + } + + if util.IsShellScript(contents) { + logger.Debug("disk (%q) contains a shell script, ignoring", device) + return nil, nil + } + + return contents, nil +} + +func readFromKernelParams(logger *log.Logger) (string, string, error) { + args, err := os.ReadFile(distro.KernelCmdlinePath()) + if err != nil { + return "", "", err + } + + deviceLabel, userDataPath := parseParams(args) + logger.Debug("parsed device label from parameters: %s", deviceLabel) + logger.Debug("parsed user-data path from parameters: %s", userDataPath) + return deviceLabel, userDataPath, nil +} + +func parseParams(args []byte) (deviceLabel, userDataPath string) { + for _, arg := range strings.Split(string(args), " ") { + parts := strings.SplitN(strings.TrimSpace(arg), "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + value := parts[1] + + if key == deviceLabelKernelFlag { + deviceLabel = value + } + + if key == userDataKernelFlag { + userDataPath = value + } + } + + return +} diff --git a/internal/providers/proxmoxve/proxmoxve.go b/internal/providers/proxmoxve/proxmoxve.go index 490bfe30f..b0dbb481b 100644 --- a/internal/providers/proxmoxve/proxmoxve.go +++ b/internal/providers/proxmoxve/proxmoxve.go @@ -20,7 +20,6 @@ package proxmoxve import ( - "bytes" "context" "fmt" "os" @@ -132,8 +131,7 @@ func fetchConfigFromDevice(logger *log.Logger, ctx context.Context, path string) return nil, err } - header := []byte("#cloud-config\n") - if bytes.HasPrefix(contents, header) { + if util.IsCloudConfig(contents) { logger.Debug("config drive (%q) contains a cloud-config configuration, ignoring", path) return nil, nil } diff --git a/internal/providers/util/cloudconfig.go b/internal/providers/util/cloudconfig.go new file mode 100644 index 000000000..b57d30eb9 --- /dev/null +++ b/internal/providers/util/cloudconfig.go @@ -0,0 +1,29 @@ +// Copyright 2024 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" +) + +func IsCloudConfig(contents []byte) bool { + header := []byte("#cloud-config\n") + return bytes.HasPrefix(contents, header) +} + +func IsShellScript(contents []byte) bool { + header := []byte("#!/bin/\n") + return bytes.HasPrefix(contents, header) +} diff --git a/internal/register/providers.go b/internal/register/providers.go index bda4b7cfe..63249c7df 100644 --- a/internal/register/providers.go +++ b/internal/register/providers.go @@ -29,6 +29,7 @@ import ( _ "github.com/coreos/ignition/v2/internal/providers/hetzner" _ "github.com/coreos/ignition/v2/internal/providers/hyperv" _ "github.com/coreos/ignition/v2/internal/providers/ibmcloud" + _ "github.com/coreos/ignition/v2/internal/providers/ionoscloud" _ "github.com/coreos/ignition/v2/internal/providers/kubevirt" _ "github.com/coreos/ignition/v2/internal/providers/metal" _ "github.com/coreos/ignition/v2/internal/providers/nutanix"