Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d57d7c6

Browse files
committed
Metadata package: add Azure providers (IMDS/OVF)
Signed-off-by: Michael Schnerring <[email protected]>
1 parent 9410a7d commit d57d7c6

File tree

4 files changed

+463
-2
lines changed

4 files changed

+463
-2
lines changed

pkg/metadata/main.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"flag"
66
"fmt"
77
"io/ioutil"
8+
"net/http"
89
"os"
910
"path"
1011
"strconv"
@@ -80,7 +81,19 @@ func main() {
8081
log.SetLevel(log.DebugLevel)
8182
}
8283

83-
providers := []string{"aws", "gcp", "hetzner", "openstack", "scaleway", "vultr", "digitalocean", "packet", "cdrom"}
84+
providers := []string{
85+
"aws",
86+
"gcp",
87+
"hetzner",
88+
"openstack",
89+
"scaleway",
90+
"vultr",
91+
"digitalocean",
92+
"packet",
93+
"cdrom",
94+
"azure-imds",
95+
"azure-ovf",
96+
}
8497
args := flag.Args()
8598
if len(args) > 0 {
8699
providers = args
@@ -103,6 +116,14 @@ func main() {
103116
netProviders = append(netProviders, NewVultr())
104117
case p == "digitalocean":
105118
netProviders = append(netProviders, NewDigitalOcean())
119+
case p == "azure-imds":
120+
netProviders = append(netProviders, NewAzureIMDS())
121+
case p == "azure-ovf":
122+
// TODO not every provider should create a separate http client
123+
client := &http.Client{
124+
Timeout: time.Second * 2,
125+
}
126+
netProviders = append(netProviders, NewAzureOVF(client))
106127
case p == "cdrom":
107128
cdromProviders = ListCDROMs()
108129
case strings.HasPrefix(p, "file="):
@@ -289,7 +310,7 @@ func retry(attempts int, sleep time.Duration, f func() error) (err error) {
289310

290311
time.Sleep(sleep)
291312

292-
log.Printf("retrying after error:", err)
313+
log.Printf("retrying after error: %s", err)
293314
}
294315
return fmt.Errorf("after %d attempts, last error: %s", attempts, err)
295316
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"net/http"
7+
"os"
8+
"path"
9+
"strings"
10+
"time"
11+
12+
log "github.com/sirupsen/logrus"
13+
)
14+
15+
// ProviderAzureIMDS reads from Azure's Instance Metadata Service (IMDS) API.
16+
type ProviderAzureIMDS struct {
17+
client *http.Client
18+
wireServer *WireServerClient
19+
providerOVF *ProviderAzureOVF
20+
}
21+
22+
// NewAzureIMDS factory
23+
func NewAzureIMDS() *ProviderAzureIMDS {
24+
client := &http.Client{
25+
Timeout: time.Second * 2,
26+
}
27+
return &ProviderAzureIMDS{
28+
client: client,
29+
wireServer: NewWireServerClient(client),
30+
providerOVF: NewAzureOVF(client),
31+
}
32+
}
33+
34+
func (p *ProviderAzureIMDS) String() string {
35+
return "Azure-IMDS"
36+
}
37+
38+
// Probe checks if Azure IMDS API is available
39+
func (p *ProviderAzureIMDS) Probe() bool {
40+
// "Poll" VM Unique ID
41+
// see: https://azure.microsoft.com/en-us/blog/accessing-and-using-azure-vm-unique-id/
42+
pollVMID := func() error {
43+
_, err := p.imdsGet("compute/vmId")
44+
return err
45+
}
46+
return retry(6, 5*time.Second, pollVMID) == nil && p.providerOVF.Probe()
47+
}
48+
49+
// Extract user data via Azure IMDS.
50+
func (p *ProviderAzureIMDS) Extract() ([]byte, error) {
51+
if err := p.imdsSaveHostname(); err != nil {
52+
return nil, err
53+
}
54+
55+
p.imdsSave("network/interface/0/ipv4/ipAddress/0/publicIpAddress")
56+
p.imdsSave("network/interface/0/ipv4/ipAddress/0/privateIpAddress")
57+
p.imdsSave("compute/zone")
58+
p.imdsSave("compute/vmId")
59+
60+
if err := p.imdsSaveSSHKeys(); err != nil {
61+
log.Printf("Azure-IMDS: SSH key retrieval failed: %s", err)
62+
}
63+
64+
userData, err := p.imdsGet("compute/customData")
65+
if err != nil {
66+
log.Errorf("Azure-IMDS: failed to get user data: %s", err)
67+
return nil, err
68+
}
69+
70+
// defer ReportReady(p.client)
71+
72+
if len(userData) > 0 { // always false
73+
log.Warnf("Azure-IMDS: user data received: \n%s", string(userData))
74+
// TODO
75+
// Getting user data via IMDS is disabled. See upstream issue:
76+
// * https://github.com/MicrosoftDocs/azure-docs/issues/64154
77+
// * https://github.com/MicrosoftDocs/azure-docs/issues/30370 (OP)
78+
// return userData, nil
79+
}
80+
// As a fallback, extract user data via Azure-OVF provider
81+
log.Warnf(
82+
"Azure-IMDS: user data not supported by provider " + p.String() +
83+
", falling back to " + p.providerOVF.String())
84+
return p.providerOVF.Extract()
85+
}
86+
87+
// Get resource value from IMDS and write to file in ConfigPath
88+
func (p *ProviderAzureIMDS) imdsSave(resourceName string) {
89+
if value, err := p.imdsGet(resourceName); err == nil {
90+
fileName := strings.Replace(resourceName, "/", "_", -1)
91+
err = ioutil.WriteFile(path.Join(ConfigPath, fileName), value, 0644)
92+
if err != nil {
93+
log.Printf("Azure-IMDS: failed to write file %s:%s %s", fileName, value, err)
94+
}
95+
log.Debugf("Azure-IMDS: saved resource %s: %s", resourceName, string(value))
96+
} else {
97+
log.Warnf("Azure-IMDS: failed to get resource %s: %s", resourceName, err)
98+
}
99+
}
100+
101+
func (p *ProviderAzureIMDS) imdsSaveHostname() error {
102+
hostname, err := p.imdsGet("compute/name")
103+
if err != nil {
104+
return err
105+
}
106+
err = ioutil.WriteFile(path.Join(ConfigPath, Hostname), hostname, 0644)
107+
if err != nil {
108+
return fmt.Errorf("Azure-IMDS: failed to write hostname: %s", err)
109+
}
110+
log.Debugf("Azure-IMDS: saved hostname: %s", string(hostname))
111+
return nil
112+
}
113+
114+
func (p *ProviderAzureIMDS) imdsSaveSSHKeys() error {
115+
// TODO support multiple keys
116+
sshKey, err := p.imdsGet("compute/publicKeys/0/keyData")
117+
if err != nil {
118+
return fmt.Errorf("getting SSH key failed: %s", err)
119+
}
120+
if err := os.Mkdir(path.Join(ConfigPath, SSH), 0755); err != nil {
121+
return fmt.Errorf("creating directory %s failed: %s", SSH, err)
122+
}
123+
err = ioutil.WriteFile(path.Join(ConfigPath, SSH, "authorized_keys"), sshKey, 0600)
124+
if err != nil {
125+
return fmt.Errorf("writing SSH key failed: %s", err)
126+
}
127+
log.Debugf("Azure-IMDS: saved authorized_keys: \n%s", string(sshKey))
128+
return nil
129+
}
130+
131+
// Request and extract requested resource
132+
func (p *ProviderAzureIMDS) imdsGet(resourceName string) ([]byte, error) {
133+
req, err := http.NewRequest("GET", imdsURL(resourceName), nil)
134+
if err != nil {
135+
return nil, fmt.Errorf("http.NewRequest failed: %s", err)
136+
}
137+
req.Header.Set("Metadata", "true")
138+
139+
resp, err := p.client.Do(req)
140+
if err != nil {
141+
return nil, fmt.Errorf("IMDS unavailable: %s", err)
142+
}
143+
defer resp.Body.Close()
144+
if resp.StatusCode != 200 {
145+
return nil, fmt.Errorf("IMDS returned status code: %d", resp.StatusCode)
146+
}
147+
148+
body, err := ioutil.ReadAll(resp.Body)
149+
if err != nil {
150+
return nil, fmt.Errorf("reading HTTP response failed: %s", err)
151+
}
152+
153+
return body, nil
154+
}
155+
156+
// Build Azure Instance Metadata Service (IMDS) URL
157+
// For available nodes, see: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
158+
func imdsURL(node string) string {
159+
const (
160+
baseURL = "http://169.254.169.254/metadata/instance"
161+
// TODO Version 2020-10-01 might not yet be available in every region
162+
//apiVersion = "2020-10-01"
163+
apiVersion = "2020-09-01"
164+
// For leaf nodes in /metadata/instance, the format=json doesn't work.
165+
// For these queries, format=text needs to be explicitly specified
166+
// because the default format is JSON.
167+
params = "?api-version=" + apiVersion + "&format=text"
168+
)
169+
if len(node) > 0 {
170+
return baseURL + "/" + node + params
171+
}
172+
return baseURL + params
173+
}

pkg/metadata/provider_azure_ovf.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package main
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/xml"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"path"
10+
"path/filepath"
11+
"syscall"
12+
"time"
13+
14+
log "github.com/sirupsen/logrus"
15+
)
16+
17+
// ProviderAzureOVF extracts user data from ovf-env.xml. The file is on an Azure
18+
// attached DVD containing the user data, encoded as base64.
19+
// Inspired by:
20+
// - WALinuxAgent:
21+
// - https://github.com/Azure/WALinuxAgent
22+
// - cloud-init Azure Datasource docs:
23+
// https://cloudinit.readthedocs.io/en/latest/topics/datasources/azure.html
24+
type ProviderAzureOVF struct {
25+
client *http.Client
26+
mountPoint string
27+
}
28+
29+
// OVF XML model
30+
type OVF struct {
31+
UserDataBase64 string `xml:"ProvisioningSection>LinuxProvisioningConfigurationSet>CustomData"`
32+
}
33+
34+
// NewAzureOVF factory
35+
func NewAzureOVF(client *http.Client) *ProviderAzureOVF {
36+
mountPoint, err := ioutil.TempDir("", "cdrom")
37+
if err != nil {
38+
panic(fmt.Errorf("creating temp mount dir failed: %s", err))
39+
}
40+
return &ProviderAzureOVF{
41+
mountPoint: mountPoint,
42+
client: client,
43+
}
44+
}
45+
46+
func (p *ProviderAzureOVF) String() string {
47+
return "Azure-OVF"
48+
}
49+
50+
// Probe returns true if DVD is successfully mounted, false otherwise
51+
func (p *ProviderAzureOVF) Probe() bool {
52+
// TODO log last error
53+
return retry(6, 5*time.Second, p.mount) == nil
54+
}
55+
56+
// Extract user data from ovf-env.xml file located on Azure attached DVD
57+
func (p *ProviderAzureOVF) Extract() ([]byte, error) {
58+
ovf, err := p.copyOVF()
59+
if err != nil {
60+
return nil, fmt.Errorf("Azure-OVF: copying OVF failed: %s", err)
61+
}
62+
defer ReportReady(p.client)
63+
if ovf == nil || ovf.UserDataBase64 == "" {
64+
log.Debugf("Azure-OVF: user data is empty")
65+
return nil, nil
66+
}
67+
log.Debugf("Azure-OVF: base64 user data: %s", ovf.UserDataBase64)
68+
userData, err := base64.StdEncoding.DecodeString(ovf.UserDataBase64)
69+
if err != nil {
70+
return nil, fmt.Errorf("Azure-OVF: decoding user data failed: %s", err)
71+
}
72+
log.Debugf("Azure-OVF: raw user data: \n%s", string(userData))
73+
return userData, nil
74+
}
75+
76+
// Mount DVD attached by Azure
77+
func (p *ProviderAzureOVF) mount() error {
78+
dev, err := getDvdDevice()
79+
if err != nil {
80+
return err
81+
}
82+
// Read-only mount UDF file system
83+
// https://github.com/Azure/WALinuxAgent/blob/v2.2.52/azurelinuxagent/common/osutil/default.py#L602-L605
84+
return syscall.Mount(dev, p.mountPoint, "udf", syscall.MS_RDONLY, "")
85+
}
86+
87+
// WALinuxAgent implements various methods of finding the DVD device, depending
88+
// on the OS:
89+
// https://github.com/Azure/WALinuxAgent/blob/develop/azurelinuxagent/common/osutil
90+
func getDvdDevice() (string, error) {
91+
var (
92+
// "default" implementation, see:
93+
// https://github.com/Azure/WALinuxAgent/blob/v2.2.52/azurelinuxagent/common/osutil/default.py#L569
94+
dvdPatterns = []string{
95+
"/dev/sr[0-9]",
96+
"/dev/hd[c-z]",
97+
"/dev/cdrom[0-9]",
98+
}
99+
)
100+
for _, pattern := range dvdPatterns {
101+
devs, err := filepath.Glob(pattern)
102+
if err != nil {
103+
panic(fmt.Sprintf("invalid glob pattern: %s", pattern))
104+
}
105+
if len(devs) > 0 {
106+
log.Debugf("found DVD device: %s", devs[0])
107+
return devs[0], nil
108+
}
109+
}
110+
return "", fmt.Errorf("no DVD device found")
111+
}
112+
113+
func (p *ProviderAzureOVF) copyOVF() (*OVF, error) {
114+
xmlContent, err := ioutil.ReadFile(path.Join(p.mountPoint, "ovf-env.xml"))
115+
if err != nil {
116+
return nil, err
117+
}
118+
err = ioutil.WriteFile(path.Join(ConfigPath, "ovf-env.xml"), xmlContent, 0600)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
defer p.unmount()
124+
125+
var ovf OVF
126+
err = xml.Unmarshal(xmlContent, &ovf)
127+
if err != nil {
128+
// An error means no user data was provided
129+
// TODO test this
130+
return nil, nil
131+
}
132+
return &ovf, nil
133+
}
134+
135+
// Unmount DVD
136+
func (p *ProviderAzureOVF) unmount() {
137+
_ = syscall.Unmount(p.mountPoint, 0)
138+
}

0 commit comments

Comments
 (0)