Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extend cmdline / metal provider for loading loading config from device #2018

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
176 changes: 145 additions & 31 deletions internal/providers/cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,40 @@
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"
"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 +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)
}
Loading