Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
171 changes: 140 additions & 31 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,149 @@
)

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

data, err := f.FetchToBuffer(*url, resource.FetchOptions{})
if err != nil {
return types.Config{}, report.Report{}, err
if opts.UserDataPath != "" && opts.DeviceLabel != "" {
return fetchConfigFromDevice(f.Logger, opts)
}

return util.ParseConfig(f.Logger, data)
if opts.UserDataPath != "" || opts.DeviceLabel != "" {
f.Logger.Warning("both %q and %q must be provided together; ignoring",
string(flagDeviceLabel), string(flagUserDataPath))
}

return types.Config{}, report.Report{}, platform.ErrNoProvider
Comment thread
atd9876 marked this conversation as resolved.
}

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) {
logger.Info("disk was not available in time. Continuing without a config...")
Comment thread
atd9876 marked this conversation as resolved.
Outdated
return types.Config{}, report.Report{}, configErrors.ErrEmpty
}
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)
Comment thread
atd9876 marked this conversation as resolved.
Outdated
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 os.Remove(mnt)

Check failure on line 181 in internal/providers/cmdline/cmdline.go

View workflow job for this annotation

GitHub Actions / Test (1.25.x)

Error return value of `os.Remove` is not checked (errcheck)
Comment thread
atd9876 marked this conversation as resolved.
Outdated

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