Skip to content

Commit 7feb4ce

Browse files
authored
Merge pull request #2230 from atd9876/add-metal-config-drive
cmdline: add support for loading config from a local device
2 parents c5f9538 + 55c888a commit 7feb4ce

9 files changed

Lines changed: 691 additions & 32 deletions

File tree

docs/release-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Starting with this release, ignition-validate binaries are signed with the
1515

1616
- Support reading configs from `/run/ignition` and `/etc/ignition/` in addition to `/usr/lib/ignition/`, searched in descending priority order ([#2221](https://github.com/coreos/ignition/pull/2221))
1717
- Add support for `virtiofs`
18+
- Support loading Ignition config from a labeled device via `ignition.config.device` and `ignition.config.path` kernel command-line arguments
1819
- Allow deleting a disk partition while creating another partition with number 0. ([#2234](https://github.com/coreos/ignition/pull/2234))
1920

2021
### Changes

docs/supported-platforms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Ignition is currently supported for the following platforms:
2121
* [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.
2222
* [IBM Cloud] (`ibmcloud`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys are handled separately.
2323
* [KubeVirt] (`kubevirt`) - Ignition will read its configuration from the instance userdata via `cloudInitConfigDrive` or `cloudInitNoCloud`. Cloud SSH keys are handled separately.
24-
* 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.
24+
* 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.
2525
* [Nutanix] (`nutanix`) - Ignition will read its configuration from the instance userdata via config drive. Cloud SSH keys are handled separately.
2626
* [NVIDIA BlueField] (`nvidiabluefield`) - Ignition will read its configuration from the bootfifo sysfs interface from the mlxbf_bootctl platform driver.
2727
* [OpenStack] (`openstack`) - Ignition will read its configuration from the instance userdata via either metadata service or config drive. Cloud SSH keys are handled separately.

internal/distro/distro.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ var (
9090

9191
func DiskByLabelDir() string { return diskByLabelDir }
9292

93-
func KernelCmdlinePath() string { return kernelCmdlinePath }
93+
func KernelCmdlinePath() string { return fromEnv("KERNEL_CMDLINE_PATH", kernelCmdlinePath) }
9494
func BootIDPath() string { return bootIDPath }
9595
func SystemRuntimeConfigDir() string {
9696
return fromEnv("SYSTEM_RUNTIME_CONFIG_DIR", systemRuntimeConfigDir)

internal/providers/cmdline/cmdline.go

Lines changed: 155 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,47 @@
1313
// limitations under the License.
1414

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

1819
package cmdline
1920

2021
import (
22+
"context"
23+
"errors"
24+
"fmt"
2125
"net/url"
2226
"os"
27+
"os/exec"
28+
"path/filepath"
2329
"strings"
30+
"time"
2431

2532
"github.com/coreos/ignition/v2/config/v3_7_experimental/types"
2633
"github.com/coreos/ignition/v2/internal/distro"
2734
"github.com/coreos/ignition/v2/internal/log"
2835
"github.com/coreos/ignition/v2/internal/platform"
2936
"github.com/coreos/ignition/v2/internal/providers/util"
3037
"github.com/coreos/ignition/v2/internal/resource"
38+
ut "github.com/coreos/ignition/v2/internal/util"
3139

3240
"github.com/coreos/vcontext/report"
3341
)
3442

43+
type cmdlineFlag string
44+
3545
const (
36-
cmdlineUrlFlag = "ignition.config.url"
46+
flagUrl cmdlineFlag = "ignition.config.url"
47+
flagDeviceLabel cmdlineFlag = "ignition.config.device"
48+
flagUserDataPath cmdlineFlag = "ignition.config.path"
3749
)
3850

51+
type cmdlineOpts struct {
52+
Url *url.URL
53+
UserDataPath string
54+
DeviceLabel string
55+
}
56+
3957
var (
4058
// we are a special-cased system provider; don't register ourselves
4159
// for lookup by name
@@ -46,59 +64,167 @@ var (
4664
)
4765

4866
func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) {
49-
url, err := readCmdline(f.Logger)
67+
opts, err := parseCmdline(f.Logger, distro.KernelCmdlinePath())
5068
if err != nil {
5169
return types.Config{}, report.Report{}, err
5270
}
5371

54-
if url == nil {
55-
return types.Config{}, report.Report{}, platform.ErrNoProvider
72+
var data []byte
73+
74+
if opts.Url != nil {
75+
if opts.DeviceLabel != "" || opts.UserDataPath != "" {
76+
f.Logger.Warning("%q takes precedence; ignoring %q and %q",
77+
string(flagUrl), string(flagDeviceLabel), string(flagUserDataPath))
78+
}
79+
data, err = f.FetchToBuffer(*opts.Url, resource.FetchOptions{})
80+
if err != nil {
81+
return types.Config{}, report.Report{}, err
82+
}
83+
84+
return util.ParseConfig(f.Logger, data)
85+
}
86+
87+
if opts.UserDataPath != "" && opts.DeviceLabel != "" {
88+
return fetchConfigFromDevice(f.Logger, opts)
5689
}
5790

58-
data, err := f.FetchToBuffer(*url, resource.FetchOptions{})
59-
if err != nil {
60-
return types.Config{}, report.Report{}, err
91+
if opts.UserDataPath != "" || opts.DeviceLabel != "" {
92+
return types.Config{}, report.Report{}, fmt.Errorf("both %q and %q must be provided together",
93+
string(flagDeviceLabel), string(flagUserDataPath))
6194
}
6295

63-
return util.ParseConfig(f.Logger, data)
96+
return types.Config{}, report.Report{}, platform.ErrNoProvider
6497
}
6598

66-
func readCmdline(logger *log.Logger) (*url.URL, error) {
67-
args, err := os.ReadFile(distro.KernelCmdlinePath())
99+
func parseCmdline(logger *log.Logger, path string) (*cmdlineOpts, error) {
100+
cmdline, err := os.ReadFile(path)
68101
if err != nil {
69102
logger.Err("couldn't read cmdline: %v", err)
70103
return nil, err
71104
}
72105

73-
rawUrl := parseCmdline(args)
74-
logger.Debug("parsed url from cmdline: %q", rawUrl)
75-
if rawUrl == "" {
76-
logger.Info("no config URL provided")
77-
return nil, nil
106+
opts := &cmdlineOpts{}
107+
108+
for _, arg := range strings.Fields(string(cmdline)) {
109+
parts := strings.SplitN(strings.TrimSpace(arg), "=", 2)
110+
if len(parts) != 2 {
111+
continue
112+
}
113+
114+
key := cmdlineFlag(parts[0])
115+
value := parts[1]
116+
117+
switch key {
118+
case flagUrl:
119+
if value == "" {
120+
logger.Info("url flag found but no value provided")
121+
continue
122+
}
123+
124+
parsedURL, err := url.Parse(value)
125+
if err != nil {
126+
logger.Err("failed to parse url: %v", err)
127+
continue
128+
}
129+
opts.Url = parsedURL
130+
case flagDeviceLabel:
131+
if value == "" {
132+
logger.Info("device label flag found but no value provided")
133+
continue
134+
}
135+
opts.DeviceLabel = value
136+
case flagUserDataPath:
137+
if value == "" {
138+
logger.Info("user data path flag found but no value provided")
139+
continue
140+
}
141+
opts.UserDataPath = value
142+
}
143+
}
144+
145+
return opts, nil
146+
}
147+
148+
func fetchConfigFromDevice(logger *log.Logger, opts *cmdlineOpts) (types.Config, report.Report, error) {
149+
if err := validateDeviceLabel(opts.DeviceLabel); err != nil {
150+
return types.Config{}, report.Report{}, err
78151
}
79152

80-
url, err := url.Parse(rawUrl)
153+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
154+
defer cancel()
155+
156+
data, err := tryMounting(logger, ctx, opts)
157+
if errors.Is(err, context.DeadlineExceeded) {
158+
return types.Config{}, report.Report{}, fmt.Errorf("device %q did not appear within timeout", opts.DeviceLabel)
159+
}
81160
if err != nil {
82-
logger.Err("failed to parse url: %v", err)
83-
return nil, err
161+
return types.Config{}, report.Report{}, err
162+
}
163+
if data == nil {
164+
return types.Config{}, report.Report{}, fmt.Errorf("config file %q not found on device %q", opts.UserDataPath, opts.DeviceLabel)
84165
}
85166

86-
return url, err
167+
return util.ParseConfig(logger, data)
87168
}
88169

89-
func parseCmdline(cmdline []byte) (url string) {
90-
for _, arg := range strings.Split(string(cmdline), " ") {
91-
parts := strings.SplitN(strings.TrimSpace(arg), "=", 2)
92-
key := parts[0]
170+
func validateDeviceLabel(label string) error {
171+
// Reject labels that are not a single path component to prevent path traversal.
172+
if label != filepath.Base(label) || label == ".." || label == "." {
173+
return fmt.Errorf("invalid device label %q", label)
174+
}
175+
return nil
176+
}
93177

94-
if key != cmdlineUrlFlag {
95-
continue
178+
func tryMounting(logger *log.Logger, ctx context.Context, opts *cmdlineOpts) ([]byte, error) {
179+
device := filepath.Join(distro.DiskByLabelDir(), opts.DeviceLabel)
180+
for !fileExists(device) {
181+
logger.Debug("disk (%q) not found. Waiting...", device)
182+
select {
183+
case <-time.After(time.Second):
184+
case <-ctx.Done():
185+
return nil, ctx.Err()
96186
}
187+
}
97188

98-
if len(parts) == 2 {
99-
url = parts[1]
189+
logger.Debug("creating temporary mount point")
190+
mnt, err := os.MkdirTemp("", "ignition-config")
191+
if err != nil {
192+
return nil, fmt.Errorf("failed to create temp directory: %v", err)
193+
}
194+
defer func() {
195+
if err := os.Remove(mnt); err != nil {
196+
logger.Err("failed to remove temporary mount point %q: %v", mnt, err)
100197
}
198+
}()
199+
200+
cmd := exec.CommandContext(ctx, distro.MountCmd(), "-o", "ro", "-t", "auto", device, mnt)
201+
if _, err := logger.LogCmd(cmd, "mounting disk"); err != nil {
202+
return nil, err
101203
}
204+
defer func() {
205+
_ = logger.LogOp(
206+
func() error {
207+
return ut.UmountPath(mnt)
208+
},
209+
"unmounting %q at %q", device, mnt,
210+
)
211+
}()
212+
213+
configPath := filepath.Join(mnt, filepath.Clean(filepath.Join("/", opts.UserDataPath)))
214+
if !fileExists(configPath) {
215+
logger.Debug("config file %q not found on device %q", opts.UserDataPath, opts.DeviceLabel)
216+
return nil, nil
217+
}
218+
219+
contents, err := os.ReadFile(configPath)
220+
if err != nil {
221+
return nil, err
222+
}
223+
224+
return contents, nil
225+
}
102226

103-
return
227+
func fileExists(path string) bool {
228+
_, err := os.Stat(path)
229+
return (err == nil)
104230
}

0 commit comments

Comments
 (0)