Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/supported-platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Ignition is currently supported for the following platforms:
* [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.
* [KubeVirt] (`kubevirt`) - Ignition will read its configuration from the instance userdata via `cloudInitConfigDrive` or `cloudInitNoCloud`. 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.
* 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. Alternatively, use `ignition.config.device` (a disk-by-label name, e.g. `CONFIG`) and `ignition.config.path` (the path to the config file on that device, e.g. `/ignition/config.ign`) to load the configuration from a locally attached device. Both parameters must be provided together.
* [Nutanix] (`nutanix`) - Ignition will read its configuration from the instance userdata via config drive. Cloud SSH keys are handled separately.
* [NVIDIA BlueField] (`nvidiabluefield`) - Ignition will read its configuration from the bootfifo sysfs interface from the mlxbf_bootctl platform driver.
* [OpenStack] (`openstack`) - Ignition will read its configuration from the instance userdata via either metadata service or config drive. Cloud SSH keys are handled separately.
Expand Down
2 changes: 1 addition & 1 deletion internal/distro/distro.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ var (

func DiskByLabelDir() string { return diskByLabelDir }

func KernelCmdlinePath() string { return kernelCmdlinePath }
func KernelCmdlinePath() string { return fromEnv("KERNEL_CMDLINE_PATH", kernelCmdlinePath) }
func BootIDPath() string { return bootIDPath }
func SystemConfigDir() string { return fromEnv("SYSTEM_CONFIG_DIR", systemConfigDir) }

Expand Down
172 changes: 142 additions & 30 deletions internal/providers/cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,48 @@
// limitations under the License.

// The cmdline provider fetches a remote configuration from the URL specified
// in the kernel boot option "ignition.config.url".
// in the kernel boot option "ignition.config.url", or from a local device
// specified by "ignition.config.device" and "ignition.config.path".

package cmdline

import (
"context"
"errors"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

configErrors "github.com/coreos/ignition/v2/config/shared/errors"
"github.com/coreos/ignition/v2/config/v3_7_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"
)

type cmdlineFlag string

const (
cmdlineUrlFlag = "ignition.config.url"
flagUrl cmdlineFlag = "ignition.config.url"
flagDeviceLabel cmdlineFlag = "ignition.config.device"
flagUserDataPath cmdlineFlag = "ignition.config.path"
)

type cmdlineOpts struct {
Url *url.URL
UserDataPath string
DeviceLabel string
}

var (
// we are a special-cased system provider; don't register ourselves
// for lookup by name
Expand All @@ -46,59 +65,152 @@ var (
)

func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) {
url, err := readCmdline(f.Logger)
opts, err := parseCmdline(f.Logger, distro.KernelCmdlinePath())
if err != nil {
return types.Config{}, report.Report{}, err
}

if url == nil {
return types.Config{}, report.Report{}, platform.ErrNoProvider
var data []byte

if opts.Url != nil {
data, err = f.FetchToBuffer(*opts.Url, resource.FetchOptions{})
if err != nil {
return types.Config{}, report.Report{}, err
}

return util.ParseConfig(f.Logger, data)
}

if opts.UserDataPath != "" && opts.DeviceLabel != "" {
return fetchConfigFromDevice(f.Logger, opts)
}

data, err := f.FetchToBuffer(*url, resource.FetchOptions{})
if err != nil {
return types.Config{}, report.Report{}, err
if opts.UserDataPath != "" || opts.DeviceLabel != "" {
f.Logger.Warning("both %q and %q must be provided together; ignoring",
string(flagDeviceLabel), string(flagUserDataPath))
}

return util.ParseConfig(f.Logger, data)
return types.Config{}, report.Report{}, platform.ErrNoProvider
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hmm, this feels like it could lead to misconfiguration, if a user sets the wrong file location, we would get an error, but then that would essentially lead to a log, and the continuation of the provisioning no?

}

func readCmdline(logger *log.Logger) (*url.URL, error) {
args, err := os.ReadFile(distro.KernelCmdlinePath())
func parseCmdline(logger *log.Logger, path string) (*cmdlineOpts, error) {
cmdline, err := os.ReadFile(path)
if err != nil {
logger.Err("couldn't read cmdline: %v", err)
return nil, err
}

rawUrl := parseCmdline(args)
logger.Debug("parsed url from cmdline: %q", rawUrl)
if rawUrl == "" {
logger.Info("no config URL provided")
return nil, nil
opts := &cmdlineOpts{}

for _, arg := range strings.Fields(string(cmdline)) {
parts := strings.SplitN(strings.TrimSpace(arg), "=", 2)
if len(parts) != 2 {
continue
}

key := cmdlineFlag(parts[0])
value := parts[1]

switch key {
case flagUrl:
if value == "" {
logger.Info("url flag found but no value provided")
continue
}

parsedURL, err := url.Parse(value)
if err != nil {
logger.Err("failed to parse url: %v", err)
continue
}
opts.Url = parsedURL
case flagDeviceLabel:
if value == "" {
logger.Info("device label flag found but no value provided")
continue
}
opts.DeviceLabel = value
case flagUserDataPath:
if value == "" {
logger.Info("user data path flag found but no value provided")
continue
}
opts.UserDataPath = value
}
}

url, err := url.Parse(rawUrl)
return opts, nil
}

func fetchConfigFromDevice(logger *log.Logger, opts *cmdlineOpts) (types.Config, report.Report, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
Comment thread
prestist marked this conversation as resolved.
defer cancel()

data, err := tryMounting(logger, ctx, opts)
if errors.Is(err, context.DeadlineExceeded) {
return types.Config{}, report.Report{}, fmt.Errorf("device %q did not appear within timeout", opts.DeviceLabel)
}
Comment thread
atd9876 marked this conversation as resolved.
if err != nil {
logger.Err("failed to parse url: %v", err)
return nil, err
return types.Config{}, report.Report{}, err
}
if data == nil {
logger.Info("config file %q not found on device. Continuing without config...", opts.UserDataPath)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Im not sure we would want to continue if we cannot find the user specified config.

return types.Config{}, report.Report{}, configErrors.ErrEmpty
}

return url, err
return util.ParseConfig(logger, data)
}

func parseCmdline(cmdline []byte) (url string) {
for _, arg := range strings.Split(string(cmdline), " ") {
parts := strings.SplitN(strings.TrimSpace(arg), "=", 2)
key := parts[0]

if key != cmdlineUrlFlag {
continue
func tryMounting(logger *log.Logger, ctx context.Context, opts *cmdlineOpts) ([]byte, error) {
device := filepath.Join(distro.DiskByLabelDir(), opts.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()
}
}

if len(parts) == 2 {
url = parts[1]
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 func() {
if err := os.Remove(mnt); err != nil {
logger.Err("failed to remove temporary mount point %q: %v", mnt, err)
}
}()

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,
)
}()

configPath := filepath.Join(mnt, filepath.Clean(filepath.Join("/", opts.UserDataPath)))
if !fileExists(configPath) {
logger.Debug("config file %q not found on device %q", opts.UserDataPath, opts.DeviceLabel)
return nil, nil
}

contents, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}

return
return contents, nil
}

func fileExists(path string) bool {
_, err := os.Stat(path)
return (err == nil)
}
Loading
Loading