diff --git a/internal/providers/cmdline/cmdline.go b/internal/providers/cmdline/cmdline.go index 745f06878..c13c0ce3e 100644 --- a/internal/providers/cmdline/cmdline.go +++ b/internal/providers/cmdline/cmdline.go @@ -18,9 +18,14 @@ package cmdline import ( + "context" + "fmt" "net/url" "os" + "os/exec" + "path/filepath" "strings" + "time" "github.com/coreos/ignition/v2/config/v3_6_experimental/types" "github.com/coreos/ignition/v2/internal/distro" @@ -28,14 +33,25 @@ import ( "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 @@ -46,59 +62,157 @@ var ( ) func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) { - url, err := readCmdline(f.Logger) + opts, err := parseCmdline(f.Logger) 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) + return types.Config{}, report.Report{}, platform.ErrNoProvider } -func readCmdline(logger *log.Logger) (*url.URL, error) { - args, err := os.ReadFile(distro.KernelCmdlinePath()) +func parseCmdline(logger *log.Logger) (*cmdlineOpts, error) { + cmdline, err := os.ReadFile(distro.KernelCmdlinePath()) 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{} - url, err := url.Parse(rawUrl) - if err != nil { - logger.Err("failed to parse url: %v", err) - return nil, err + for _, arg := range strings.Split(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 + } + + url, err := url.Parse(value) + if err != nil { + logger.Err("failed to parse url: %v", err) + continue + } + opts.Url = url + 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.DeviceLabel = value + } } - return url, err + return opts, nil } -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 fetchConfigFromDevice(logger *log.Logger, opts *cmdlineOpts) (types.Config, report.Report, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + var data []byte + + dispatch := func(name string, fn func() ([]byte, error)) { + raw, err := fn() + if err != nil { + switch err { + case context.Canceled: + case context.DeadlineExceeded: + logger.Err("timed out while fetching config from %s", name) + default: + logger.Err("failed to fetch config from %s: %v", name, err) + } + return } - if len(parts) == 2 { - url = parts[1] + data = raw + cancel() + } + + go dispatch( + "load config from disk", func() ([]byte, error) { + return tryMounting(logger, ctx, opts) + }, + ) + + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + logger.Info("disk was not available in time. Continuing without a config...") + } + + return util.ParseConfig(logger, data) +} + +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() } } - return + 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, opts.UserDataPath)) { + return nil, nil + } + + contents, err := os.ReadFile(filepath.Join(mnt, opts.UserDataPath)) + if err != nil { + return nil, err + } + + return contents, nil +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return (err == nil) }