-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Metadata package: add Azure providers (IMDS/OVF)
Signed-off-by: Michael Schnerring <3743342+schnerring@users.noreply.github.com>
- Loading branch information
1 parent
9410a7d
commit d57d7c6
Showing
4 changed files
with
463 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.