1
0
mirror of https://github.com/coreos/ignition.git synced 2026-02-05 15:47:26 +01:00
Files
ignition/internal/providers/openstack/openstack.go
yasminvalim d0c5d54d06 openstack.go: fix fetchConfigFromMetadataService bug removing encoding/json
Remove encoding/json, changed fetchConfigFromMetadataServiceIPv4Only and fetchConfigParallel now returning raw data
2025-12-16 16:01:44 -03:00

329 lines
7.8 KiB
Go

// Copyright 2016 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// The OpenStack provider fetches configurations from the userdata available in
// both the config-drive as well as the network metadata service. Whichever
// responds first is the config that is used.
// NOTE: This provider is still EXPERIMENTAL.
package openstack
import (
"context"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"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"
)
const (
configDriveUserdataPath = "/openstack/latest/user_data"
)
var (
userdataURLs = map[string]url.URL{
resource.IPv4: {
Scheme: "http",
Host: "169.254.169.254",
Path: "openstack/latest/user_data",
},
resource.IPv6: {
Scheme: "http",
Host: "[fe80::a9fe:a9fe%iface]",
Path: "openstack/latest/user_data",
},
}
)
func init() {
platform.Register(platform.Provider{
Name: "openstack",
Fetch: fetchConfig,
})
// the brightbox platform ID just uses the OpenStack provider code
platform.Register(platform.Provider{
Name: "brightbox",
Fetch: fetchConfig,
})
}
func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) {
// The fetch-offline approach doesn't work well here because of the "split
// personality" of this provider. See:
// https://github.com/coreos/ignition/issues/1081
if f.Offline {
return types.Config{}, report.Report{}, resource.ErrNeedNet
}
var data []byte
errChan := make(chan error)
ctx, cancel := context.WithCancel(context.Background())
dispatchCount := 0
dispatch := func(name string, fn func() ([]byte, error)) {
dispatchCount++
go func() {
raw, err := fn()
if err != nil {
switch err {
case context.Canceled:
default:
f.Logger.Err("failed to fetch config from %s: %v", name, err)
}
errChan <- err
return
}
data = raw
cancel()
}()
}
dispatch("config drive (config-2)", func() ([]byte, error) {
return fetchConfigFromDevice(f.Logger, ctx, filepath.Join(distro.DiskByLabelDir(), "config-2"))
})
dispatch("config drive (CONFIG-2)", func() ([]byte, error) {
return fetchConfigFromDevice(f.Logger, ctx, filepath.Join(distro.DiskByLabelDir(), "CONFIG-2"))
})
dispatch("metadata service", func() ([]byte, error) {
return fetchConfigFromMetadataService(f)
})
Loop:
for {
select {
case <-ctx.Done():
break Loop
case <-errChan:
dispatchCount--
if dispatchCount == 0 {
f.Logger.Info("couldn't fetch config")
break Loop
}
}
}
return util.ParseConfig(f.Logger, data)
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return (err == nil)
}
func fetchConfigFromDevice(logger *log.Logger, ctx context.Context, path string) ([]byte, error) {
for !fileExists(path) {
logger.Debug("config drive (%q) not found. Waiting...", path)
select {
case <-time.After(time.Second):
case <-ctx.Done():
return nil, ctx.Err()
}
}
logger.Debug("creating temporary mount point")
mnt, err := os.MkdirTemp("", "ignition-configdrive")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %v", err)
}
defer func() {
if removeErr := os.Remove(mnt); removeErr != nil {
logger.Warning("failed to remove temp directory %q: %v", mnt, removeErr)
}
}()
cmd := exec.Command(distro.MountCmd(), "-o", "ro", "-t", "auto", path, mnt)
if _, err := logger.LogCmd(cmd, "mounting config drive"); err != nil {
return nil, err
}
defer func() {
_ = logger.LogOp(
func() error {
return ut.UmountPath(mnt)
},
"unmounting %q at %q", path, mnt,
)
}()
if !fileExists(filepath.Join(mnt, configDriveUserdataPath)) {
return nil, nil
}
return os.ReadFile(filepath.Join(mnt, configDriveUserdataPath))
}
func fetchConfigFromMetadataService(f *resource.Fetcher) ([]byte, error) {
ipv6Interfaces, err := findInterfacesWithIPv6()
if err != nil {
f.Logger.Info("No active IPv6 network interface found: %v", err)
// Fall back to IPv4 only
return fetchConfigFromMetadataServiceIPv4Only(f)
}
urls := []url.URL{userdataURLs[resource.IPv4]}
for _, ifaceName := range ipv6Interfaces {
ipv6Url := userdataURLs[resource.IPv6]
ipv6Url.Host = strings.Replace(ipv6Url.Host, "iface", ifaceName, 1)
urls = append(urls, ipv6Url)
}
// Use parallel fetching for all interfaces
data, err := fetchConfigParallel(f, urls)
if err != nil {
// the metadata server exists but doesn't contain any actual metadata,
// assume that there is no config specified
if err == resource.ErrNotFound {
return nil, nil
}
return nil, err
}
return data, nil
}
func fetchConfigFromMetadataServiceIPv4Only(f *resource.Fetcher) ([]byte, error) {
ipv4Url := userdataURLs[resource.IPv4]
data, err := f.FetchToBuffer(ipv4Url, resource.FetchOptions{})
if err != nil {
if err == resource.ErrNotFound {
return nil, nil
}
return nil, err
}
return data, nil
}
func fetchConfigParallel(f *resource.Fetcher, urls []url.URL) ([]byte, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var (
err error
nbErrors int
)
cfg := make(map[url.URL][]byte)
success := make(chan url.URL, 1)
errors := make(chan error, len(urls))
// Use waitgroup to wait for all goroutines to complete
var wg sync.WaitGroup
fetch := func(_ context.Context, u url.URL) {
defer wg.Done()
d, e := f.FetchToBuffer(u, resource.FetchOptions{})
if e != nil {
f.Logger.Err("fetching configuration for %s: %v", u.String(), e)
err = e
errors <- e
} else {
cfg[u] = d
select {
case success <- u:
default:
}
}
}
// Start goroutines for all URLs
for _, u := range urls {
wg.Add(1)
go fetch(ctx, u)
}
// Wait for the first success or all failures
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case u := <-success:
f.Logger.Debug("got configuration from: %s", u.String())
return cfg[u], nil
case <-errors:
nbErrors++
if nbErrors == len(urls) {
f.Logger.Debug("all routines have failed to fetch configuration, returning last known error: %v", err)
return nil, err
}
case <-done:
// All goroutines completed, check if we have any success
if len(cfg) > 0 {
// Return the first successful configuration
for u, data := range cfg {
f.Logger.Debug("got configuration from: %s", u.String())
return data, nil
}
}
}
return nil, err
}
func findInterfacesWithIPv6() ([]string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("error fetching network interfaces: %v", err)
}
var ipv6Interfaces []string
for _, iface := range interfaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To16() != nil && ipnet.IP.To4() == nil {
ipv6Interfaces = append(ipv6Interfaces, iface.Name)
break
}
}
}
if len(ipv6Interfaces) == 0 {
return nil, fmt.Errorf("no active IPv6 network interface found")
}
return ipv6Interfaces, nil
}