feat: add support for variable substitution in talos.config kernel parameter

The URL to fetch the configuration for a talos node is given by the
talos.config kernel parameter. We add support for 4 variables ${uuid},
${serial}, ${mac} and ${hostname} which substitute the device UUID,
DMI-sourced serial number, MAC address of the first network interface to
be up and the hostname respectively.

Fixes #3272

Signed-off-by: Philipp Sauter <philipp.sauter@siderolabs.com>
This commit is contained in:
Philipp Sauter 2022-06-13 23:25:02 +02:00
parent 103c942256
commit 2deff6b6e1
No known key found for this signature in database
GPG Key ID: D3F8AF32D62A348D
35 changed files with 538 additions and 65 deletions

2
go.mod
View File

@ -40,7 +40,7 @@ require (
github.com/containernetworking/plugins v1.1.1
github.com/coreos/go-iptables v0.6.0
github.com/coreos/go-semver v0.3.0
github.com/cosi-project/runtime v0.0.0-20220609185841-c0aa3e101537
github.com/cosi-project/runtime v0.0.0-20220622192602-9483ac9a4832
github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.17+incompatible
github.com/docker/go-connections v0.4.0

4
go.sum
View File

@ -339,8 +339,8 @@ github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cosi-project/runtime v0.0.0-20220609185841-c0aa3e101537 h1:0ejVbCuG1P97SQS5A4l/gHuQ76wG3aWYATf3EMN+Grs=
github.com/cosi-project/runtime v0.0.0-20220609185841-c0aa3e101537/go.mod h1:4WQcuQDWr7cIT4mUyqf95PkoBwgAgaa2yhMmGwndYCs=
github.com/cosi-project/runtime v0.0.0-20220622192602-9483ac9a4832 h1:ixPX9angaYh/nmaG83TPwine+QXfYqACDvnYhjqB9Ew=
github.com/cosi-project/runtime v0.0.0-20220622192602-9483ac9a4832/go.mod h1:4WQcuQDWr7cIT4mUyqf95PkoBwgAgaa2yhMmGwndYCs=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=

View File

@ -6,11 +6,13 @@ github.com/containernetworking/cni v1.1.0/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8d
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/talos-systems/siderolink v0.1.1/go.mod h1:1PLRyKRx+MAkz1vWJXIP19p5wChF0TejbIbX/CQMWuw=
github.com/vishvananda/netlink v1.2.0-beta h1:CTNzkunO9iTkRaupF540+w47mexyQgNkA/ibnuKc39w=
github.com/vishvananda/netlink v1.2.0-beta/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo=
go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -42,6 +42,15 @@ See [documentation](https://www.talos.dev/v1.1/reference/configuration/#bridge)
* Linux: 5.15.49
"""
[notes.talos-config-kernel-param-variable-substitution]
title = "Variable substitution for URL query parameter in the talos.config kernel parameter"
description="""\
The kernel parameter talos.config can now substitute system information into placeholders inside its URL query values. This example shows all supported variables:
```http://example.com/metadata?h=${hostname}&m=${mac}&s=${serial}&u=${uuid}```
"""
[make_deps]
[make_deps.tools]

View File

@ -0,0 +1,41 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package hardware
import (
"github.com/talos-systems/go-smbios/smbios"
"github.com/talos-systems/talos/pkg/machinery/resources/hardware"
)
// SystemInformation adapter provider conversion from smbios.SMBIOS.
//
//nolint:revive,golint
func SystemInformation(p *hardware.SystemInformation) systemInformation {
return systemInformation{
SystemInformation: p,
}
}
type systemInformation struct {
*hardware.SystemInformation
}
// Update current systemInformation info.
func (p systemInformation) Update(systemInformation *smbios.SystemInformation) {
translateSystemInformationInfo := func(in *smbios.SystemInformation) hardware.SystemInformationSpec {
return hardware.SystemInformationSpec{
Manufacturer: in.Manufacturer,
ProductName: in.ProductName,
Version: in.Version,
SerialNumber: in.SerialNumber,
UUID: in.UUID,
WakeUpType: in.WakeUpType.String(),
SKUNumber: in.SKUNumber,
}
}
*p.SystemInformation.TypedSpec() = translateSystemInformationInfo(systemInformation)
}

View File

@ -15,6 +15,7 @@ import (
"go.uber.org/zap"
hwadapter "github.com/talos-systems/talos/internal/app/machined/pkg/adapters/hardware"
pkgSMBIOS "github.com/talos-systems/talos/internal/pkg/smbios"
"github.com/talos-systems/talos/pkg/machinery/resources/hardware"
)
@ -44,6 +45,10 @@ func (ctrl *SystemInfoController) Outputs() []controller.Output {
Type: hardware.MemoryModuleType,
Kind: controller.OutputExclusive,
},
{
Type: hardware.SystemInformationType,
Kind: controller.OutputExclusive,
},
}
}
@ -59,7 +64,7 @@ func (ctrl *SystemInfoController) Run(ctx context.Context, r controller.Runtime,
// controller runs only once
if ctrl.SMBIOS == nil {
s, err := GetSMBIOSInfo()
s, err := pkgSMBIOS.GetSMBIOSInfo()
if err != nil {
return err
}
@ -67,6 +72,15 @@ func (ctrl *SystemInfoController) Run(ctx context.Context, r controller.Runtime,
ctrl.SMBIOS = s
}
const systemInfoID = "systeminformation"
if err := r.Modify(ctx, hardware.NewSystemInformation(systemInfoID), func(res resource.Resource) error {
hwadapter.SystemInformation(res.(*hardware.SystemInformation)).Update(&ctrl.SMBIOS.SystemInformation)
return nil
}); err != nil {
return fmt.Errorf("error updating objects: %w", err)
}
for _, p := range ctrl.SMBIOS.ProcessorInformation {
// replaces `CPU 0` with `CPU-0`
id := strings.ReplaceAll(p.SocketDesignation, " ", "-")

View File

@ -616,7 +616,7 @@ func (mock *platformMock) Name() string {
return "mock"
}
func (mock *platformMock) Configuration(context.Context) ([]byte, error) {
func (mock *platformMock) Configuration(context.Context, state.State) ([]byte, error) {
return nil, nil
}

View File

@ -17,7 +17,6 @@ import (
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/go-pointer"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/go-smbios/smbios"
pb "github.com/talos-systems/siderolink/api/siderolink"
"go.uber.org/zap"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@ -26,6 +25,7 @@ import (
"google.golang.org/grpc/credentials/insecure"
"inet.af/netaddr"
"github.com/talos-systems/talos/internal/pkg/smbios"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
@ -116,7 +116,7 @@ func (ctrl *ManagerController) Run(ctx context.Context, r controller.Runtime, lo
return nil
}
s, err := smbios.New()
s, err := smbios.GetSMBIOSInfo()
if err != nil {
return fmt.Errorf("error reading node UUID: %w", err)
}

View File

@ -7,6 +7,7 @@ package runtime
import (
"context"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"inet.af/netaddr"
@ -25,7 +26,7 @@ type Platform interface {
//
// On cloud-like platform it is user-data in metadata service.
// For metal platform that is either `talos.config=` URL or mounted ISO image.
Configuration(context.Context) ([]byte, error)
Configuration(context.Context, state.State) ([]byte, error)
// KernelArgs returns additional kernel arguments which should be injected for the kernel boot.
KernelArgs() procfs.Parameters

View File

@ -16,6 +16,7 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"inet.af/netaddr"
@ -51,7 +52,7 @@ func (a *AWS) Name() string {
}
// Configuration implements the runtime.Platform interface.
func (a *AWS) Configuration(ctx context.Context) ([]byte, error) {
func (a *AWS) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from AWS")
userdata, err := a.metadataClient.GetUserDataWithContext(ctx)

View File

@ -18,6 +18,7 @@ import (
"regexp"
"strings"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"golang.org/x/sys/unix"
"inet.af/netaddr"
@ -159,7 +160,7 @@ func (a *Azure) ParseLoadBalancerIP(lbConfig LoadBalancerMetadata, exIP []netadd
}
// Configuration implements the platform.Platform interface.
func (a *Azure) Configuration(ctx context.Context) ([]byte, error) {
func (a *Azure) Configuration(ctx context.Context, r state.State) ([]byte, error) {
defer func() {
if err := linuxAgent(ctx); err != nil {
log.Printf("failed to update instance status, err: %s", err.Error())

View File

@ -12,6 +12,7 @@ import (
"log"
"os"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
@ -28,7 +29,7 @@ func (c *Container) Name() string {
}
// Configuration implements the platform.Platform interface.
func (c *Container) Configuration(context.Context) ([]byte, error) {
func (c *Container) Configuration(context.Context, state.State) ([]byte, error) {
log.Printf("fetching machine config from: USERDATA environment variable")
s := os.Getenv("USERDATA")

View File

@ -9,6 +9,7 @@ import (
stderrors "errors"
"log"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"inet.af/netaddr"
@ -36,7 +37,7 @@ func (d *DigitalOcean) Name() string {
}
// Configuration implements the platform.Platform interface.
func (d *DigitalOcean) Configuration(ctx context.Context) ([]byte, error) {
func (d *DigitalOcean) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from: %q", DigitalOceanUserDataEndpoint)
return download.Download(ctx, DigitalOceanUserDataEndpoint,

View File

@ -14,6 +14,7 @@ import (
"net/http"
"time"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/go-retry/retry"
"inet.af/netaddr"
@ -89,7 +90,7 @@ func (p *EquinixMetal) Name() string {
}
// Configuration implements the platform.Platform interface.
func (p *EquinixMetal) Configuration(ctx context.Context) ([]byte, error) {
func (p *EquinixMetal) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from: %q", EquinixMetalUserDataEndpoint)
return download.Download(ctx, EquinixMetalUserDataEndpoint,

View File

@ -9,6 +9,7 @@ import (
"strings"
"cloud.google.com/go/compute/metadata"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"inet.af/netaddr"
@ -26,7 +27,7 @@ func (g *GCP) Name() string {
}
// Configuration implements the platform.Platform interface.
func (g *GCP) Configuration(ctx context.Context) ([]byte, error) {
func (g *GCP) Configuration(ctx context.Context, r state.State) ([]byte, error) {
userdata, err := metadata.InstanceAttributeValue("user-data")
if err != nil {
if _, ok := err.(metadata.NotDefinedError); ok {

View File

@ -10,6 +10,7 @@ import (
"fmt"
"log"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"gopkg.in/yaml.v3"
"inet.af/netaddr"
@ -158,7 +159,7 @@ func (h *Hcloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, host, e
}
// Configuration implements the runtime.Platform interface.
func (h *Hcloud) Configuration(ctx context.Context) ([]byte, error) {
func (h *Hcloud) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from: %q", HCloudUserDataEndpoint)
return download.Download(ctx, HCloudUserDataEndpoint,

View File

@ -11,18 +11,24 @@ import (
"log"
"net/url"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-blockdevice/blockdevice/filesystem"
"github.com/talos-systems/go-blockdevice/blockdevice/probe"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/go-smbios/smbios"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors"
"github.com/talos-systems/talos/pkg/download"
"github.com/talos-systems/talos/pkg/machinery/constants"
hardwareResource "github.com/talos-systems/talos/pkg/machinery/resources/hardware"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
)
const (
@ -38,7 +44,7 @@ func (m *Metal) Name() string {
}
// Configuration implements the platform.Platform interface.
func (m *Metal) Configuration(ctx context.Context) ([]byte, error) {
func (m *Metal) Configuration(ctx context.Context, r state.State) ([]byte, error) {
var option *string
if option = procfs.ProcCmdline().Get(constants.KernelParamConfig).First(); option == nil {
return nil, errors.ErrNoConfigSource
@ -48,13 +54,15 @@ func (m *Metal) Configuration(ctx context.Context) ([]byte, error) {
return nil, errors.ErrNoConfigSource
}
log.Printf("fetching machine config from: %q", *option)
downloadURL, err := PopulateURLParameters(*option, getSystemUUID)
downloadURL, err := PopulateURLParameters(ctx, *option, r)
if err != nil {
log.Fatalf("failed to populate talos.config fetch URL: %q ; %s", *option, err.Error())
return nil, err
}
log.Printf("fetching machine config from: %q", downloadURL)
switch downloadURL {
case constants.MetalConfigISOLabel:
return readConfigFromISO()
@ -63,30 +71,129 @@ func (m *Metal) Configuration(ctx context.Context) ([]byte, error) {
}
}
func keyToVar(key string) string {
return `${` + key + `}`
}
type matcher struct {
Key string
Regexp *regexp.Regexp
}
func newMatcher(key string) *matcher {
return &matcher{
Key: keyToVar(key),
Regexp: regexp.MustCompile(`(?i)` + regexp.QuoteMeta(keyToVar(key))),
}
}
type replacer struct {
original string
Regexp *regexp.Regexp
Matches [][]int
}
func (m *matcher) process(original string) *replacer {
var r replacer
r.Regexp = m.Regexp
r.original = original
r.Matches = m.Regexp.FindAllStringIndex(original, -1)
return &r
}
func (r *replacer) ReplaceMatches(replacement string) string {
var res string
if len(r.Matches) < 1 {
return res
}
res += r.original[:r.Matches[0][0]]
res += replacement
for i := 0; i < len(r.Matches)-1; i++ {
res += r.original[r.Matches[i][1]:r.Matches[i+1][0]]
res += replacement
}
res += r.original[r.Matches[len(r.Matches)-1][1]:]
return res
}
// PopulateURLParameters fills in empty parameters in the download URL.
func PopulateURLParameters(downloadURL string, getSystemUUID func() (string, error)) (string, error) {
u, err := url.Parse(downloadURL)
//nolint:gocyclo
func PopulateURLParameters(ctx context.Context, downloadURL string, r state.State) (string, error) {
populatedURL := downloadURL
genErr := func(varOfKey string, errToWrap error) error {
return fmt.Errorf("error while substituting %s: %w", varOfKey, errToWrap)
}
u, err := url.Parse(populatedURL)
if err != nil {
return "", fmt.Errorf("failed to parse %s: %w", constants.KernelParamConfig, err)
return "", fmt.Errorf("failed to parse %s: %w", populatedURL, err)
}
values := u.Query()
for key, qValues := range values {
switch key {
case "uuid":
// don't touch uuid field if it already has some value
if !(len(qValues) == 1 && len(strings.TrimSpace(qValues[0])) > 0) {
uid, err := getSystemUUID()
substitute := func(varToSubstitute string, getFunc func(ctx context.Context, r state.State) (string, error)) error {
m := newMatcher(varToSubstitute)
for qKey, qValues := range values {
if len(qValues) == 0 {
continue
}
qVal := qValues[0]
// backwards compatible behavior for the uuid key
if qKey == constants.UUIDKey && !(len(qValues) == 1 && len(strings.TrimSpace(qVal)) > 0) {
uid, err := getSystemUUID(ctx, r)
if err != nil {
return "", err
return fmt.Errorf("error while substituting UUID: %w", err)
}
values.Set("uuid", uid)
values.Set(constants.UUIDKey, uid)
continue
}
default:
log.Printf("unsupported query parameter: %q", key)
replacer := m.process(qVal)
if len(replacer.Matches) < 1 {
continue
}
val, err := getFunc(ctx, r)
if err != nil {
return genErr(m.Key, err)
}
qVal = replacer.ReplaceMatches(val)
values.Set(qKey, qVal)
}
return nil
}
if err := substitute(constants.UUIDKey, getSystemUUID); err != nil {
return "", err
}
if err := substitute(constants.SerialNumberKey, getSystemSerialNumber); err != nil {
return "", err
}
if err := substitute(constants.MacKey, getMACAddress); err != nil {
return "", err
}
if err := substitute(constants.HostnameKey, getHostname); err != nil {
return "", err
}
u.RawQuery = values.Encode()
@ -94,13 +201,88 @@ func PopulateURLParameters(downloadURL string, getSystemUUID func() (string, err
return u.String(), nil
}
func getSystemUUID() (string, error) {
s, err := smbios.New()
func getResource[T resource.Resource](ctx context.Context, r state.State, namespace, typ, valName string, isReadyFunc func(T) bool, checkAndGetFunc func(T) string) (string, error) {
metadata := resource.NewMetadata(namespace, typ, "", resource.VersionUndefined)
watchCtx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
events := make(chan safe.WrappedStateEvent[T])
err := safe.StateWatchKind[T](watchCtx, r, metadata, events, state.WithBootstrapContents(true))
if err != nil {
return "", err
return "", fmt.Errorf("failed to watch %s resources: %w", typ, err)
}
return s.SystemInformation.UUID, nil
var watchErr error
for {
select {
case <-watchCtx.Done():
err := fmt.Errorf("failed to determine %s of %s: %w", valName, typ, watchCtx.Err())
err = fmt.Errorf("%s; %w", err.Error(), watchErr)
return "", err
case event := <-events:
eventResource, err := event.Resource()
if err != nil {
watchErr = fmt.Errorf("%s; invalid resource in wrapped event: %w", watchErr.Error(), err)
}
if !isReadyFunc(eventResource) {
continue
}
val := checkAndGetFunc(eventResource)
if val == "" {
return "", fmt.Errorf("%s property of resource %s is empty", valName, typ)
}
return val, nil
}
}
}
func getUUIDProperty(r *hardwareResource.SystemInformation) string {
return r.TypedSpec().UUID
}
func getSerialNumberProperty(r *hardwareResource.SystemInformation) string {
return r.TypedSpec().SerialNumber
}
func getSystemUUID(ctx context.Context, r state.State) (string, error) {
return getResource(ctx, r, hardwareResource.NamespaceName, hardwareResource.SystemInformationType, "UUID", func(*hardwareResource.SystemInformation) bool { return true }, getUUIDProperty)
}
func getSystemSerialNumber(ctx context.Context, r state.State) (string, error) {
return getResource(ctx,
r,
hardwareResource.NamespaceName,
hardwareResource.SystemInformationType,
"Serial Number",
func(*hardwareResource.SystemInformation) bool { return true },
getSerialNumberProperty)
}
func getMACAddressProperty(r *network.LinkStatus) string {
return r.TypedSpec().HardwareAddr.String()
}
func checkLinkUp(r *network.LinkStatus) bool {
return r.TypedSpec().LinkState
}
func getMACAddress(ctx context.Context, r state.State) (string, error) {
return getResource(ctx, r, network.NamespaceName, network.LinkStatusType, "MAC Address", checkLinkUp, getMACAddressProperty)
}
func getHostnameProperty(r *network.HostnameSpec) string {
return r.TypedSpec().Hostname
}
func getHostname(ctx context.Context, r state.State) (string, error) {
return getResource(ctx, r, network.NamespaceName, network.HostnameSpecType, "Hostname", func(*network.HostnameSpec) bool { return true }, getHostnameProperty)
}
// Mode implements the platform.Platform interface.
@ -108,10 +290,8 @@ func (m *Metal) Mode() runtime.Mode {
return runtime.ModeMetal
}
func readConfigFromISO() (b []byte, err error) {
var dev *probe.ProbedBlockDevice
dev, err = probe.GetDevWithFileSystemLabel(constants.MetalConfigISOLabel)
func readConfigFromISO() ([]byte, error) {
dev, err := probe.GetDevWithFileSystemLabel(constants.MetalConfigISOLabel)
if err != nil {
return nil, fmt.Errorf("failed to find %s iso: %w", constants.MetalConfigISOLabel, err)
}
@ -125,14 +305,14 @@ func readConfigFromISO() (b []byte, err error) {
}
if sb == nil {
return nil, fmt.Errorf("failed to get filesystem type")
return nil, fmt.Errorf("error while substituting filesystem type")
}
if err = unix.Mount(dev.Device().Name(), mnt, sb.Type(), unix.MS_RDONLY, ""); err != nil {
return nil, fmt.Errorf("failed to mount iso: %w", err)
}
b, err = ioutil.ReadFile(filepath.Join(mnt, filepath.Base(constants.ConfigPath)))
b, err := ioutil.ReadFile(filepath.Join(mnt, filepath.Base(constants.ConfigPath)))
if err != nil {
return nil, fmt.Errorf("read config: %s", err.Error())
}

View File

@ -5,17 +5,31 @@
package metal_test
import (
"context"
"fmt"
"net"
"net/url"
"testing"
"github.com/google/uuid"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal"
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
"github.com/talos-systems/talos/pkg/machinery/resources/hardware"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
)
func TestPopulateURLParameters(t *testing.T) {
mockUUID := uuid.New().String()
mockUUID := "40dcbd19-3b10-444e-bfff-aaee44a51fda"
mockMAC := "52:2f:fd:df:fc:c0"
mockSerialNumber := "0OCZJ19N65"
mockHostname := "myTestHostname"
for _, tt := range []struct {
name string
@ -38,6 +52,16 @@ func TestPopulateURLParameters(t *testing.T) {
url: "http://example.com/metadata?uuid=xyz",
expectedURL: "http://example.com/metadata?uuid=xyz",
},
{
name: "multiple uuids in one query parameter",
url: "http://example.com/metadata?u=this-${uuid}-equals-${uuid}-exactly",
expectedURL: fmt.Sprintf("http://example.com/metadata?u=this-%s-equals-%s-exactly", mockUUID, mockUUID),
},
{
name: "uuid and mac in one query parameter",
url: "http://example.com/metadata?u=this-${uuid}-and-${mac}-together",
expectedURL: fmt.Sprintf("http://example.com/metadata?u=this-%s-and-%s-together", mockUUID, mockMAC),
},
{
name: "other parameters",
url: "http://example.com/metadata?foo=a",
@ -48,18 +72,86 @@ func TestPopulateURLParameters(t *testing.T) {
url: "http://example.com/metadata?uuid=xyz&uuid=foo",
expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID),
},
{
name: "single serial number",
url: "http://example.com/metadata?serial=${serial}",
expectedURL: fmt.Sprintf("http://example.com/metadata?serial=%s", mockSerialNumber),
},
{
name: "single MAC",
url: "http://example.com/metadata?mac=${mac}",
expectedURL: fmt.Sprintf("http://example.com/metadata?mac=%s", mockMAC),
},
{
name: "single hostname",
url: "http://example.com/metadata?host=${hostname}",
expectedURL: fmt.Sprintf("http://example.com/metadata?host=%s", mockHostname),
},
{
name: "serial number, MAC and hostname",
url: "http://example.com/metadata?h=${hostname}&m=${mac}&s=${serial}",
expectedURL: fmt.Sprintf("http://example.com/metadata?h=%s&m=%s&s=%s", mockHostname, mockMAC, mockSerialNumber),
},
{
name: "uuid, serial number, MAC and hostname; case-insensitive",
url: "http://example.com/metadata?h=${HOSTname}&m=${mAC}&s=${SERIAL}&u=${uUid}",
expectedURL: fmt.Sprintf("http://example.com/metadata?h=%s&m=%s&s=%s&u=%s", mockHostname, mockMAC, mockSerialNumber, mockUUID),
},
{
name: "MAC and UUID without variable",
url: "http://example.com/metadata?macaddr=${mac}&uuid=",
expectedURL: fmt.Sprintf("http://example.com/metadata?macaddr=%s&uuid=%s", mockMAC, mockUUID),
},
{
name: "serial number and UUID without variable, order is not preserved",
url: "http://example.com/metadata?uuid=&ser=${serial}",
expectedURL: fmt.Sprintf("http://example.com/metadata?ser=%s&uuid=%s", mockSerialNumber, mockUUID),
},
{
name: "UUID variable",
url: "http://example.com/metadata?uuid=${uuid}",
expectedURL: fmt.Sprintf("http://example.com/metadata?uuid=%s", mockUUID),
},
{
name: "serial number and UUID with variable, order is not preserved",
url: "http://example.com/metadata?uuid=${uuid}&ser=${serial}",
expectedURL: fmt.Sprintf("http://example.com/metadata?ser=%s&uuid=%s", mockSerialNumber, mockUUID),
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
output, err := metal.PopulateURLParameters(tt.url, func() (string, error) {
return mockUUID, nil
})
ctx := context.Background()
st := state.WrapCore(namespaced.NewState(inmem.Build))
testID := "testID"
sysInfo := hardware.NewSystemInformation(testID)
sysInfo.TypedSpec().UUID = mockUUID
sysInfo.TypedSpec().SerialNumber = mockSerialNumber
assert.NoError(t, st.Create(ctx, sysInfo))
hostnameSpec := network.NewHostnameSpec(network.NamespaceName, testID)
hostnameSpec.TypedSpec().Hostname = mockHostname
assert.NoError(t, st.Create(ctx, hostnameSpec))
linkStatusSpec := network.NewLinkStatus(network.NamespaceName, testID)
parsedMockMAC, err := net.ParseMAC(mockMAC)
assert.NoError(t, err)
linkStatusSpec.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(parsedMockMAC)
linkStatusSpec.TypedSpec().LinkState = true
assert.NoError(t, st.Create(ctx, linkStatusSpec))
output, err := metal.PopulateURLParameters(ctx, tt.url, st)
if tt.expectedError != "" {
assert.EqualError(t, err, tt.expectedError)
} else {
assert.Equal(t, output, tt.expectedURL)
u, err := url.Parse(tt.expectedURL)
assert.NoError(t, err)
u.RawQuery = u.Query().Encode()
assert.Equal(t, u.String(), output)
}
})
}

View File

@ -16,12 +16,12 @@ import (
"github.com/talos-systems/go-blockdevice/blockdevice/filesystem"
"github.com/talos-systems/go-blockdevice/blockdevice/probe"
"github.com/talos-systems/go-smbios/smbios"
"golang.org/x/sys/unix"
"inet.af/netaddr"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors"
"github.com/talos-systems/talos/internal/pkg/smbios"
"github.com/talos-systems/talos/pkg/download"
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
@ -179,7 +179,7 @@ func (n *Nocloud) configFromCD() (metaConfig []byte, networkConfig []byte, machi
//nolint:gocyclo
func (n *Nocloud) acquireConfig(ctx context.Context) (metadataConfigDl, metadataNetworkConfigDl, machineConfigDl []byte, hostname string, err error) {
s, err := smbios.New()
s, err := smbios.GetSMBIOSInfo()
if err != nil {
return nil, nil, nil, "", err
}

View File

@ -10,6 +10,7 @@ import (
stderrors "errors"
"fmt"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
yaml "gopkg.in/yaml.v3"
@ -59,7 +60,7 @@ func (n *Nocloud) ParseMetadata(unmarshalledNetworkConfig *NetworkConfig, hostna
}
// Configuration implements the runtime.Platform interface.
func (n *Nocloud) Configuration(ctx context.Context) ([]byte, error) {
func (n *Nocloud) Configuration(ctx context.Context, r state.State) ([]byte, error) {
_, _, machineConfigDl, _, err := n.acquireConfig(ctx) //nolint:dogsled
if err != nil {
return nil, err

View File

@ -12,6 +12,7 @@ import (
"fmt"
"log"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"inet.af/netaddr"
@ -209,7 +210,7 @@ func (o *Openstack) ParseMetadata(unmarshalledMetadataConfig *MetadataConfig, un
}
// Configuration implements the runtime.Platform interface.
func (o *Openstack) Configuration(ctx context.Context) (machineConfig []byte, err error) {
func (o *Openstack) Configuration(ctx context.Context, r state.State) (machineConfig []byte, err error) {
_, _, machineConfig, err = o.configFromCD()
if err != nil {
_, _, machineConfig, err = o.configFromNetwork(ctx)

View File

@ -11,6 +11,7 @@ import (
"fmt"
"log"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
@ -83,7 +84,7 @@ func (o *Oracle) ParseMetadata(interfaceAddresses []NetworkConfig, hostname stri
}
// Configuration implements the platform.Platform interface.
func (o *Oracle) Configuration(ctx context.Context) ([]byte, error) {
func (o *Oracle) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from: %q", OracleUserDataEndpoint)
machineConfigDl, err := download.Download(ctx, OracleUserDataEndpoint,

View File

@ -10,6 +10,7 @@ import (
"log"
"strconv"
"github.com/cosi-project/runtime/pkg/state"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/talos-systems/go-procfs/procfs"
"inet.af/netaddr"
@ -141,7 +142,7 @@ func (s *Scaleway) ParseMetadata(metadataConfig *instance.Metadata) (*runtime.Pl
}
// Configuration implements the runtime.Platform interface.
func (s *Scaleway) Configuration(ctx context.Context) ([]byte, error) {
func (s *Scaleway) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from %q", ScalewayUserDataEndpoint)
machineConfigDl, err := download.Download(ctx, ScalewayUserDataEndpoint,

View File

@ -10,6 +10,7 @@ import (
"fmt"
"log"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"inet.af/netaddr"
@ -194,7 +195,7 @@ func (u *UpCloud) ParseMetadata(meta *MetaData) (*runtime.PlatformNetworkConfig,
}
// Configuration implements the runtime.Platform interface.
func (u *UpCloud) Configuration(ctx context.Context) ([]byte, error) {
func (u *UpCloud) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from: %q", UpCloudUserDataEndpoint)
return download.Download(ctx, UpCloudUserDataEndpoint,

View File

@ -15,6 +15,7 @@ import (
"fmt"
"log"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"github.com/vmware/govmomi/ovf"
"github.com/vmware/vmw-guestinfo/rpcvmx"
@ -111,7 +112,7 @@ func readConfigFromOvf(extraConfig *rpcvmx.Config, key string) ([]byte, error) {
// Configuration implements the platform.Platform interface.
//nolint:gocyclo
func (v *VMware) Configuration(context.Context) ([]byte, error) {
func (v *VMware) Configuration(context.Context, state.State) ([]byte, error) {
var option *string
if option = procfs.ProcCmdline().Get(constants.KernelParamConfig).First(); option == nil {
return nil, fmt.Errorf("%s not found", constants.KernelParamConfig)

View File

@ -11,6 +11,7 @@ import (
"context"
"fmt"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
@ -25,7 +26,7 @@ func (v *VMware) Name() string {
}
// Configuration implements the platform.Platform interface.
func (v *VMware) Configuration(context.Context) ([]byte, error) {
func (v *VMware) Configuration(context.Context, state.State) ([]byte, error) {
return nil, fmt.Errorf("arch not supported")
}

View File

@ -12,6 +12,7 @@ import (
"log"
"net"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/go-procfs/procfs"
"github.com/vultr/metadata"
"inet.af/netaddr"
@ -121,7 +122,7 @@ func (v *Vultr) ParseMetadata(meta *metadata.MetaData, extIP []byte) (*runtime.P
}
// Configuration implements the runtime.Platform interface.
func (v *Vultr) Configuration(ctx context.Context) ([]byte, error) {
func (v *Vultr) Configuration(ctx context.Context, r state.State) ([]byte, error) {
log.Printf("fetching machine config from: %q", VultrUserDataEndpoint)
return download.Download(ctx, VultrUserDataEndpoint,

View File

@ -583,7 +583,7 @@ func SaveConfig(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFu
func fetchConfig(ctx context.Context, r runtime.Runtime) (out []byte, err error) {
var b []byte
if b, err = r.State().Platform().Configuration(ctx); err != nil {
if b, err = r.State().Platform().Configuration(ctx, r.State().V1Alpha2().Resources()); err != nil {
return nil, err
}

View File

@ -91,6 +91,7 @@ func NewState() (*State, error) {
&files.EtcFileStatus{},
&hardware.Processor{},
&hardware.MemoryModule{},
&hardware.SystemInformation{},
&k8s.AdmissionControlConfig{},
&k8s.APIServerConfig{},
&k8s.ConfigStatus{},

View File

@ -7,7 +7,7 @@ package keys
import (
"fmt"
"github.com/talos-systems/go-smbios/smbios"
"github.com/talos-systems/talos/internal/pkg/smbios"
)
// NodeIDKeyHandler generates the key based on current node information
@ -26,7 +26,7 @@ func (h *NodeIDKeyHandler) GetKey(options ...KeyOption) ([]byte, error) {
return nil, err
}
s, err := smbios.New()
s, err := smbios.GetSMBIOSInfo()
if err != nil {
return nil, err
}

View File

@ -2,7 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package hardware
package smbios
import (
"sync"

View File

@ -679,3 +679,11 @@ const (
//nolint:golint
SYSLOG_ACTION_READ_ALL = 3
)
// names of variable that can be substituted in the talos.config kernel parameter.
const (
UUIDKey = "uuid"
SerialNumberKey = "serial"
HostnameKey = "hostname"
MacKey = "mac"
)

View File

@ -2,7 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Code generated by "deep-copy -type ProcessorSpec -type MemorySpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
// Code generated by "deep-copy -type ProcessorSpec -type MemoryModuleSpec -type SystemInformationSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
package hardware
@ -12,8 +12,14 @@ func (o ProcessorSpec) DeepCopy() ProcessorSpec {
return cp
}
// DeepCopy generates a deep copy of MemorySpec.
// DeepCopy generates a deep copy of MemoryModuleSpec.
func (o MemoryModuleSpec) DeepCopy() MemoryModuleSpec {
var cp MemoryModuleSpec = o
return cp
}
// DeepCopy generates a deep copy of SystemInformationSpec.
func (o SystemInformationSpec) DeepCopy() SystemInformationSpec {
var cp SystemInformationSpec = o
return cp
}

View File

@ -0,0 +1,85 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package hardware
import (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
"github.com/cosi-project/runtime/pkg/resource/typed"
)
// SystemInformationType is type of SystemInformation resource.
const SystemInformationType = resource.Type("SystemInformations.hardware.talos.dev")
// SystemInformation resource holds node SystemInformation information.
type SystemInformation = typed.Resource[SystemInformationSpec, SystemInformationRD]
// SystemInformationSpec represents the system information obtained from smbios.
type SystemInformationSpec struct {
Manufacturer string `yaml:"manufacturer,omitempty"`
ProductName string `yaml:"productName,omitempty"`
Version string `yaml:"version,omitempty"`
SerialNumber string `yaml:"serialnumber,omitempty"`
UUID string `yaml:"uuid,omitempty"`
WakeUpType string `yaml:"wakeUpType,omitempty"`
SKUNumber string `yaml:"skuNumber,omitempty"`
}
// NewSystemInformation initializes a SystemInformationInfo resource.
func NewSystemInformation(id string) *SystemInformation {
return typed.NewResource[SystemInformationSpec, SystemInformationRD](
resource.NewMetadata(NamespaceName, SystemInformationType, id, resource.VersionUndefined),
SystemInformationSpec{},
)
}
// SystemInformationRD provides auxiliary methods for SystemInformation.
type SystemInformationRD struct{}
// ResourceDefinition implements typed.ResourceDefinition interface.
func (c SystemInformationRD) ResourceDefinition(resource.Metadata, SystemInformationSpec) meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: SystemInformationType,
Aliases: []resource.Type{
"systeminformation",
},
DefaultNamespace: NamespaceName,
PrintColumns: []meta.PrintColumn{
{
Name: "Manufacturer",
JSONPath: `{.manufacturer}`,
},
{
Name: "ProductName",
JSONPath: `{.productName}`,
},
{
Name: "Version",
JSONPath: `{.version}`,
},
{
Name: "SerialNumber",
JSONPath: `{.serialnumber}`,
},
{
Name: "UUID",
JSONPath: `{.uuid}`,
},
{
Name: "WakeUpType",
JSONPath: `{.wakeUpType}`,
},
{
Name: "SKUNumber",
JSONPath: `{.skuNumber}`,
},
},
}
}

View File

@ -83,6 +83,25 @@ Several of these are enforced by the Kernel Self Protection Project [KSPP](https
#### `talos.config`
The URL at which the machine configuration data may be found.
This parameter supports variable substitution inside URL query values for the following case-insensitive placeholders:
* `${uuid}` the SMBIOS UUID
* `${serial}` the SMBIOS Serial Number
* `${mac}` the MAC address of the first network interface attaining link state `up`
* `${hostname}` the hostname of the machine
The following example
`http://example.com/metadata?h=${hostname}&m=${mac}&s=${serial}&u=${uuid}`
may translate to
`http://example.com/metadata?h=myTestHostname&m=52%3A2f%3Afd%3Adf%3Afc%3Ac0&s=0OCZJ19N65&u=40dcbd19-3b10-444e-bfff-aaee44a51fda`
For backwards compatibility we insert the system UUID into the query parameter `uuid` if its value is empty. As in
`http://example.com/metadata?uuid=` => `http://example.com/metadata?uuid=40dcbd19-3b10-444e-bfff-aaee44a51fda`
#### `talos.platform`