Skip to content

Commit

Permalink
Metadata package: add Azure providers (IMDS/OVF)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Schnerring <3743342+schnerring@users.noreply.github.com>
  • Loading branch information
schnerring committed Dec 28, 2020
1 parent 9410a7d commit 2d489f8
Show file tree
Hide file tree
Showing 4 changed files with 462 additions and 1 deletion.
23 changes: 22 additions & 1 deletion pkg/metadata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
Expand Down Expand Up @@ -80,7 +81,19 @@ func main() {
log.SetLevel(log.DebugLevel)
}

providers := []string{"aws", "gcp", "hetzner", "openstack", "scaleway", "vultr", "digitalocean", "packet", "cdrom"}
providers := []string{
"aws",
"gcp",
"hetzner",
"openstack",
"scaleway",
"vultr",
"digitalocean",
"packet",
"cdrom",
"azure-imds",
"azure-ovf",
}
args := flag.Args()
if len(args) > 0 {
providers = args
Expand All @@ -103,6 +116,14 @@ func main() {
netProviders = append(netProviders, NewVultr())
case p == "digitalocean":
netProviders = append(netProviders, NewDigitalOcean())
case p == "azure-imds":
netProviders = append(netProviders, NewAzureIMDS())
case p == "azure-ovf":
// TODO not every provider should create a separate http client
client := &http.Client{
Timeout: time.Second * 2,
}
netProviders = append(netProviders, NewAzureOVF(client))
case p == "cdrom":
cdromProviders = ListCDROMs()
case strings.HasPrefix(p, "file="):
Expand Down
173 changes: 173 additions & 0 deletions pkg/metadata/provider_azure_imds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package main

import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"time"

log "github.com/sirupsen/logrus"
)

// ProviderAzureIMDS reads from Azure's Instance Metadata Service (IMDS) API.
type ProviderAzureIMDS struct {
client *http.Client
wireServer *WireServerClient
providerOVF *ProviderAzureOVF
}

// NewAzureIMDS factory
func NewAzureIMDS() *ProviderAzureIMDS {
client := &http.Client{
Timeout: time.Second * 2,
}
return &ProviderAzureIMDS{
client: client,
wireServer: NewWireServerClient(client),
providerOVF: NewAzureOVF(client),
}
}

func (p *ProviderAzureIMDS) String() string {
return "Azure-IMDS"
}

// Probe checks if Azure IMDS API is available
func (p *ProviderAzureIMDS) Probe() bool {
// "Poll" VM Unique ID
// see: https://azure.microsoft.com/en-us/blog/accessing-and-using-azure-vm-unique-id/
pollVMID := func() error {
_, err := p.imdsGet("compute/vmId")
return err
}
return retry(6, 5*time.Second, pollVMID) == nil && p.providerOVF.Probe()
}

// Extract user data via Azure IMDS.
func (p *ProviderAzureIMDS) Extract() ([]byte, error) {
if err := p.imdsSaveHostname(); err != nil {
return nil, err
}

p.imdsSave("network/interface/0/ipv4/ipAddress/0/publicIpAddress")
p.imdsSave("network/interface/0/ipv4/ipAddress/0/privateIpAddress")
p.imdsSave("compute/zone")
p.imdsSave("compute/vmId")

if err := p.imdsSaveSSHKeys(); err != nil {
log.Printf("Azure-IMDS: SSH key retrieval failed: %s", err)
}

userData, err := p.imdsGet("compute/customData")
if err != nil {
log.Errorf("Azure-IMDS: failed to get user data: %s", err)
return nil, err
}

// defer ReportReady(p.client)

if len(userData) > 0 { // always false
log.Warnf("Azure-IMDS: user data received: \n%s", string(userData))
// TODO
// Getting user data via IMDS is disabled. See upstream issue:
// * https://github.com/MicrosoftDocs/azure-docs/issues/64154
// * https://github.com/MicrosoftDocs/azure-docs/issues/30370 (OP)
// return userData, nil
}
// As a fallback, extract user data via Azure-OVF provider
log.Warnf(
"Azure-IMDS: user data not supported by provider " + p.String() +
", falling back to " + p.providerOVF.String())
return p.providerOVF.Extract()
}

// Get resource value from IMDS and write to file in ConfigPath
func (p *ProviderAzureIMDS) imdsSave(resourceName string) {
if value, err := p.imdsGet(resourceName); err == nil {
fileName := strings.Replace(resourceName, "/", "_", -1)
err = ioutil.WriteFile(path.Join(ConfigPath, fileName), value, 0644)
if err != nil {
log.Printf("Azure-IMDS: failed to write file %s:%s %s", fileName, value, err)
}
log.Debugf("Azure-IMDS: saved resource %s: %s", resourceName, string(value))
} else {
log.Warnf("Azure-IMDS: failed to get resource %s: %s", resourceName, err)
}
}

func (p *ProviderAzureIMDS) imdsSaveHostname() error {
hostname, err := p.imdsGet("compute/name")
if err != nil {
return err
}
err = ioutil.WriteFile(path.Join(ConfigPath, Hostname), hostname, 0644)
if err != nil {
return fmt.Errorf("Azure-IMDS: failed to write hostname: %s", err)
}
log.Debugf("Azure-IMDS: saved hostname: %s", string(hostname))
return nil
}

func (p *ProviderAzureIMDS) imdsSaveSSHKeys() error {
// TODO support multiple keys
sshKey, err := p.imdsGet("compute/publicKeys/0/keyData")
if err != nil {
return fmt.Errorf("getting SSH key failed: %s", err)
}
if err := os.Mkdir(path.Join(ConfigPath, SSH), 0755); err != nil {
return fmt.Errorf("creating directory %s failed: %s", SSH, err)
}
err = ioutil.WriteFile(path.Join(ConfigPath, SSH, "authorized_keys"), sshKey, 0600)
if err != nil {
return fmt.Errorf("writing SSH key failed: %s", err)
}
log.Debugf("Azure-IMDS: saved authorized_keys: \n%s", string(sshKey))
return nil
}

// Request and extract requested resource
func (p *ProviderAzureIMDS) imdsGet(resourceName string) ([]byte, error) {
req, err := http.NewRequest("GET", imdsURL(resourceName), nil)
if err != nil {
return nil, fmt.Errorf("http.NewRequest failed: %s", err)
}
req.Header.Set("Metadata", "true")

resp, err := p.client.Do(req)
if err != nil {
return nil, fmt.Errorf("IMDS unavailable: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("IMDS returned status code: %d", resp.StatusCode)
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading HTTP response failed: %s", err)
}

return body, nil
}

// Build Azure Instance Metadata Service (IMDS) URL
// For available nodes, see: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
func imdsURL(node string) string {
const (
baseURL = "http://169.254.169.254/metadata/instance"
// TODO Version 2020-10-01 might not yet be available in every region
//apiVersion = "2020-10-01"
apiVersion = "2020-09-01"
// For leaf nodes in /metadata/instance, the format=json doesn't work.
// For these queries, format=text needs to be explicitly specified
// because the default format is JSON.
params = "?api-version=" + apiVersion + "&format=text"
)
if len(node) > 0 {
return baseURL + "/" + node + params
}
return baseURL + params
}
138 changes: 138 additions & 0 deletions pkg/metadata/provider_azure_ovf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package main

import (
"encoding/base64"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"path"
"path/filepath"
"syscall"
"time"

log "github.com/sirupsen/logrus"
)

// ProviderAzureOVF extracts user data from ovf-env.xml. The file is on an Azure
// attached DVD containing the user data, encoded as base64.
// Inspired by:
// - WALinuxAgent:
// - https://github.com/Azure/WALinuxAgent
// - cloud-init Azure Datasource docs:
// https://cloudinit.readthedocs.io/en/latest/topics/datasources/azure.html
type ProviderAzureOVF struct {
client *http.Client
mountPoint string
}

// OVF XML model
type OVF struct {
UserDataBase64 string `xml:"ProvisioningSection>LinuxProvisioningConfigurationSet>CustomData"`
}

// NewAzureOVF factory
func NewAzureOVF(client *http.Client) *ProviderAzureOVF {
mountPoint, err := ioutil.TempDir("", "cdrom")
if err != nil {
panic(fmt.Errorf("creating temp mount dir failed: %s", err))
}
return &ProviderAzureOVF{
mountPoint: mountPoint,
client: client,
}
}

func (p *ProviderAzureOVF) String() string {
return "Azure-OVF"
}

// Probe returns true if DVD is successfully mounted, false otherwise
func (p *ProviderAzureOVF) Probe() bool {
// TODO log last error
return retry(6, 5*time.Second, p.mount) == nil
}

// Extract user data from ovf-env.xml file located on Azure attached DVD
func (p *ProviderAzureOVF) Extract() ([]byte, error) {
ovf, err := p.copyOVF()
if err != nil {
return nil, fmt.Errorf("Azure-OVF: copying OVF failed: %s", err)
}
defer ReportReady(p.client)
if ovf == nil || ovf.UserDataBase64 == "" {
log.Debugf("Azure-OVF: user data is empty")
return nil, nil
}
log.Debugf("Azure-OVF: base64 user data: %s", ovf.UserDataBase64)
userData, err := base64.StdEncoding.DecodeString(ovf.UserDataBase64)
if err != nil {
return nil, fmt.Errorf("Azure-OVF: decoding user data failed: %s", err)
}
log.Debugf("Azure-OVF: raw user data: \n%s", string(userData))
return userData, nil
}

// Mount DVD attached by Azure
func (p *ProviderAzureOVF) mount() error {
dev, err := getDvdDevice()
if err != nil {
return err
}
// Read-only mount UDF file system
// https://github.com/Azure/WALinuxAgent/blob/v2.2.52/azurelinuxagent/common/osutil/default.py#L602-L605
return syscall.Mount(dev, p.mountPoint, "udf", syscall.MS_RDONLY, "")
}

// WALinuxAgent implements various methods of finding the DVD device, depending
// on the OS:
// https://github.com/Azure/WALinuxAgent/blob/develop/azurelinuxagent/common/osutil
func getDvdDevice() (string, error) {
var (
// "default" implementation, see:
// https://github.com/Azure/WALinuxAgent/blob/v2.2.52/azurelinuxagent/common/osutil/default.py#L569
dvdPatterns = []string{
"/dev/sr[0-9]",
"/dev/hd[c-z]",
"/dev/cdrom[0-9]",
}
)
for _, pattern := range dvdPatterns {
devs, err := filepath.Glob(pattern)
if err != nil {
panic(fmt.Sprintf("invalid glob pattern: %s", pattern))
}
if len(devs) > 0 {
log.Debugf("found DVD device: %s", devs[0])
return devs[0], nil
}
}
return "", fmt.Errorf("no DVD device found")
}

func (p *ProviderAzureOVF) copyOVF() (*OVF, error) {
xmlContent, err := ioutil.ReadFile(path.Join(p.mountPoint, "ovf-env.xml"))
if err != nil {
return nil, err
}
err = ioutil.WriteFile(path.Join(ConfigPath, "ovf-env.xml"), xmlContent, 0600)
if err != nil {
return nil, err
}

defer p.unmount()

var ovf OVF
err = xml.Unmarshal(xmlContent, &ovf)
if err != nil {
// An error means no user data was provided
// TODO test this
return nil, nil
}
return &ovf, nil
}

// Unmount DVD
func (p *ProviderAzureOVF) unmount() {
_ = syscall.Unmount(p.mountPoint, 0)
}
Loading

0 comments on commit 2d489f8

Please sign in to comment.