Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions docs/supported-platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/
Expand Down
208 changes: 208 additions & 0 deletions internal/providers/ionoscloud/ionoscloud.go
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can get rid of this check, I believe.

Copy link

@tuunit tuunit Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no we cannot, we want to ignore the file and do nothing if the config contains the #cloud-config directive

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
}
Comment on lines +163 to +171
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on why we need this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backwards compatibility for the Flatcar CoreOS CloudInit fork. But to be honest we don't really need it and can get rid of it.


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
}
4 changes: 1 addition & 3 deletions internal/providers/proxmoxve/proxmoxve.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
package proxmoxve

import (
"bytes"
"context"
"fmt"
"os"
Expand Down Expand Up @@ -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
}
Expand Down
29 changes: 29 additions & 0 deletions internal/providers/util/cloudconfig.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions internal/register/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading