Skip to content

Commit

Permalink
Add config drive as a source for OpenStack instance metadata
Browse files Browse the repository at this point in the history
This adds the config drive as an additional source for instance metadata
when using OpenStack.
  • Loading branch information
ederst committed Jun 22, 2022
1 parent ae03c6e commit 4056da0
Show file tree
Hide file tree
Showing 6 changed files with 680 additions and 12 deletions.
164 changes: 152 additions & 12 deletions protokube/pkg/protokube/openstack_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,42 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"path"
"strings"

"k8s.io/klog/v2"
"k8s.io/kops/protokube/pkg/gossip"
gossipos "k8s.io/kops/protokube/pkg/gossip/openstack"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
"k8s.io/mount-utils"
utilexec "k8s.io/utils/exec"
)

const MetadataLatest string = "http://169.254.169.254/openstack/latest/meta_data.json"
const (
// MetadataLatestPath is the path to the metadata on the config drive
MetadataLatestPath string = "openstack/latest/meta_data.json"

// MetadataID is the identifier for the metadata service
MetadataID string = "metadataService"

// MetadataLastestServiceURL points to the latest metadata of the metadata service
MetadataLatestServiceURL string = "http://169.254.169.254/" + MetadataLatestPath

// ConfigDriveID is the identifier for the config drive containing metadata
ConfigDriveID string = "configDrive"

// ConfigDriveLabel identifies the config drive by label on the OS
ConfigDriveLabel string = "config-2"

// DefaultMetadataSearchOrder defines the default order in which the metadata services are queried
DefaultMetadataSearchOrder string = ConfigDriveID + ", " + MetadataID

DiskByLabelPath string = "/dev/disk/by-label/"
)

type Metadata struct {
// Matches openstack.TagClusterName
Expand Down Expand Up @@ -59,29 +84,144 @@ type OpenStackCloudProvider struct {
storageZone string
}

type MetadataService struct {
serviceURL string
configDrivePath string
mounter *mount.SafeFormatAndMount
mountTarget string
searchOrder string
}

var _ CloudProvider = &OpenStackCloudProvider{}

func getLocalMetadata() (*InstanceMetadata, error) {
var meta InstanceMetadata
// getFromConfigDrive tries to get metadata by mounting a config drive and returns it as InstanceMetadata
// It will return an error if there is no disk labelled as ConfigDriveLabel or other errors while mounting the disk, or reading the file occur.
func (mds MetadataService) getFromConfigDrive() (*InstanceMetadata, error) {
dev := path.Join(DiskByLabelPath, ConfigDriveLabel)
if _, err := os.Stat(dev); os.IsNotExist(err) {
out, err := mds.mounter.Exec.Command(
"blkid", "-l",
"-t", fmt.Sprintf("LABEL=%s", ConfigDriveLabel),
"-o", "device",
).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("unable to run blkid: %v", err)
}
dev = strings.TrimSpace(string(out))
}

err := mds.mounter.Mount(dev, mds.mountTarget, "iso9660", []string{"ro"})
if err != nil {
err = mds.mounter.Mount(dev, mds.mountTarget, "vfat", []string{"ro"})
}
if err != nil {
return nil, fmt.Errorf("error mounting configdrive '%s': %v", dev, err)
}
defer mds.mounter.Unmount(mds.mountTarget)

f, err := os.Open(
path.Join(mds.mountTarget, mds.configDrivePath))
if err != nil {
return nil, fmt.Errorf("error reading '%s' on config drive: %v", mds.configDrivePath, err)
}
defer f.Close()

return mds.parseMetadata(f)
}

// getFromMetadataService tries to get metadata from a metadata service endpoint and returns it as InstanceMetadata.
// If the service endpoint cannot be contacted or reports a different status than StatusOK it will return an error.
func (mds MetadataService) getFromMetadataService() (*InstanceMetadata, error) {
var client http.Client
resp, err := client.Get(MetadataLatest)

resp, err := client.Get(mds.serviceURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
return mds.parseMetadata(resp.Body)
}

err = fmt.Errorf("fetching metadata from '%s' returned status code '%d'", mds.serviceURL, resp.StatusCode)
return nil, err
}

// parseMetadata reads JSON data from a Reader and returns it as InstanceMetadata.
func (mds MetadataService) parseMetadata(r io.Reader) (*InstanceMetadata, error) {
var meta InstanceMetadata

data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &meta)
if err != nil {
return nil, err
}

return &meta, nil
}

// getMetadata tries to get metadata for the instance by mounting the config drive and/or querying the metadata service endpoint.
// Depending on the searchOrder it will return data from the first source which successfully returns.
// If all the sources in searchOrder are erroneous it will propagate the last error to its caller.
func (mds MetadataService) getMetadata() (*InstanceMetadata, error) {
// Note(ederst): I used and modified code for getting the config drive metadata to work from here:
// * https://github.com/kubernetes/cloud-provider-openstack/blob/27b6fc483451b6df2112a6a4a40a34ffc9093635/pkg/util/metadata/metadata.go

var meta *InstanceMetadata
var err error

ids := strings.Split(mds.searchOrder, ",")
for _, id := range ids {
id = strings.TrimSpace(id)
switch id {
case ConfigDriveID:
meta, err = mds.getFromConfigDrive()
case MetadataID:
meta, err = mds.getFromMetadataService()
default:
err = fmt.Errorf("%s is not a valid metadata search order option. Supported options are %s and %s", id, ConfigDriveID, MetadataID)
}
err = json.Unmarshal(bodyBytes, &meta)
if err != nil {
return nil, err

if err == nil {
break
}
return &meta, nil
}
return nil, err

return meta, err
}

func newMetadataService(serviceURL string, configDrivePath string, mounter *mount.SafeFormatAndMount, mountTarget string, searchOrder string) *MetadataService {
return &MetadataService{
serviceURL: serviceURL,
configDrivePath: configDrivePath,
mounter: mounter,
mountTarget: mountTarget,
searchOrder: searchOrder,
}
}

// getDefaultMounter returns a mount and executor interface to use for getting metadata from a config drive
func getDefaultMounter() *mount.SafeFormatAndMount {
mounter := mount.New("")
exec := utilexec.New()
return &mount.SafeFormatAndMount{
Interface: mounter,
Exec: exec,
}
}

func getLocalMetadata() (*InstanceMetadata, error) {
mountTarget, err := ioutil.TempDir("", "configdrive")
if err != nil {
return nil, err
}
defer os.Remove(mountTarget)

return newMetadataService(MetadataLatestServiceURL, MetadataLatestPath, getDefaultMounter(), mountTarget, DefaultMetadataSearchOrder).getMetadata()
}

// NewOpenStackCloudProvider builds a OpenStackCloudProvider
Expand Down
Loading

0 comments on commit 4056da0

Please sign in to comment.