Skip to content

Commit

Permalink
Merge pull request rancher-sandbox#2 from davidcassany/azure_pr
Browse files Browse the repository at this point in the history
Port of upstream Azure PR 3585
  • Loading branch information
davidcassany committed Jul 14, 2021
2 parents 6a287f8 + 9fd9697 commit 8b187a9
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 2 deletions.
16 changes: 15 additions & 1 deletion pkg/metadata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,19 @@ func main() {
log.SetLevel(log.DebugLevel)
}

providers := []string{"aws", "gcp", "hetzner", "openstack", "scaleway", "vultr", "digitalocean", "packet", "metaldata", "cdrom"}
providers := []string{
"aws",
"azure",
"gcp",
"hetzner",
"openstack",
"scaleway",
"vultr",
"digitalocean",
"packet",
"metaldata",
"cdrom",
}
args := flag.Args()
if len(args) > 0 {
providers = args
Expand All @@ -60,6 +72,8 @@ func main() {
switch {
case p == "aws":
netProviders = append(netProviders, prv.NewAWS())
case p == "azure":
netProviders = append(netProviders, prv.NewAzure())
case p == "gcp":
netProviders = append(netProviders, prv.NewGCP())
case p == "hetzner":
Expand Down
2 changes: 1 addition & 1 deletion pkg/metadata/providers/provider_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"time"
)

const(
const (
// Standard AWS-compatible Metadata URLs
userDataURL = "http://169.254.169.254/latest/user-data"
metaDataURL = "http://169.254.169.254/latest/meta-data/"
Expand Down
169 changes: 169 additions & 0 deletions pkg/metadata/providers/provider_azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package providers

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

log "github.com/sirupsen/logrus"
)

// ProviderAzure reads from Azure's Instance Metadata Service (IMDS) API.
type ProviderAzure struct {
client *http.Client
}

// NewAzure factory
func NewAzure() *ProviderAzure {
client := &http.Client{
Timeout: time.Second * 2,
}
return &ProviderAzure{
client: client,
}
}

func (p *ProviderAzure) String() string {
return "Azure"
}

// Probe checks if Azure IMDS API is available
func (p *ProviderAzure) Probe() bool {
// "Poll" VM Unique ID
// See: https://azure.microsoft.com/en-us/blog/accessing-and-using-azure-vm-unique-id/
_, err := p.imdsGet("compute/vmId")
return (err == nil)
}

// Extract user data via Azure IMDS.
func (p *ProviderAzure) Extract() ([]byte, error) {
if err := p.saveHostname(); err != nil {
return nil, fmt.Errorf("%s: %s", p.String(), err)
}

if err := p.saveSSHKeys(); err != nil {
log.Warnf("%s: Saving SSH keys failed: %s", p.String(), 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")

userData, err := p.getUserData()
if err != nil {
return nil, fmt.Errorf("%s: %s", p.String(), err)
}
return userData, nil
}

func (p *ProviderAzure) saveHostname() 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("%s: Failed to write hostname: %s", p.String(), err)
}
log.Debugf("%s: Saved hostname: %s", p.String(), string(hostname))
return nil
}

func (p *ProviderAzure) saveSSHKeys() 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("%s: Saved authorized_keys", p.String())
return nil
}

// Get resource value from IMDS and write to file in ConfigPath
func (p *ProviderAzure) 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.Warnf("%s: Failed to write file %s:%s %s", p.String(), fileName, value, err)
}
log.Debugf("%s: Saved resource %s: %s", p.String(), resourceName, string(value))
} else {
log.Warnf("%s: Failed to get resource %s: %s", p.String(), resourceName, err)
}
}

// Get IMDS resource value
func (p *ProviderAzure) 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"
apiVersion = "2021-01-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
}

func (p *ProviderAzure) getUserData() ([]byte, error) {
userDataBase64, err := p.imdsGet("compute/userData")
if err != nil {
log.Errorf("Failed to get user data: %s", err)
return nil, err
}

userData := make([]byte, base64.StdEncoding.DecodedLen(len(userDataBase64)))
msgLen, err := base64.StdEncoding.Decode(userData, userDataBase64)
if err != nil {
log.Errorf("Failed to base64-decode user data: %s", err)
return nil, err
}
userData = userData[:msgLen]

defer ReportReady(p.client)

return userData, nil
}
124 changes: 124 additions & 0 deletions pkg/metadata/providers/provider_azure_wire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package providers

import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"

log "github.com/sirupsen/logrus"
)

const (
wireServerURL string = "http://168.63.129.16/machine"
)

// WireServerClient used to report ready to Azure
// 1. GET Goal State from WireServer.
// 2. Build XML repsonse, by extracing ContainerId and InstanceId from
// goal state.
// 3. POST XML response to WireServer indicating successful provisioning.
// See also:
// https://docs.microsoft.com/en-us/azure/virtual-machines/linux/no-agent#generic-steps-without-using-python
type WireServerClient struct{}

// GoalState XML model (request)
type GoalState struct {
ContainerID string `xml:"Container>ContainerId"`
InstanceID string `xml:"Container>RoleInstanceList>RoleInstance>InstanceId"`
}

// Health XML model (response)
type Health struct {
GoalStateIncarnation string `xml:"GoalStateIncarnation"`
ContainerID string `xml:"Container>ContainerId"`
InstanceID string `xml:"Container>RoleInstanceList>Role>InstanceId"`
State string `xml:"Container>RoleInstanceList>Role>Health>State"`
}

// ReportReady to Azure's WireServer, indicating successful provisioning
func ReportReady(client *http.Client) error {
goalState, err := getGoalState(client)
if err != nil {
return fmt.Errorf("Report ready: GET goal state: %s", err)
}
reportReadyXML, err := buildXML(goalState.ContainerID, goalState.InstanceID)
if err != nil {
return fmt.Errorf("Report ready: Build XML: %s", err)
}
err = postReportReady(client, reportReadyXML)
if err != nil {
return fmt.Errorf("Report ready: POST XML: %s", err)
}
log.Debugf(
"Report ready: ContainerId=%s InstanceId=%s succeeded",
goalState.ContainerID,
goalState.InstanceID,
)
return nil
}

// Get goal state from WireServer
func getGoalState(client *http.Client) (*GoalState, error) {
req, err := http.NewRequest("GET", wireServerURL+"?comp=goalstate", nil)
if err != nil {
return nil, fmt.Errorf("http.NewRequest failed: %s", err)
}
req.Header.Set("x-ms-version", "2012-11-30")

resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("WireServer unavailable: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("WireServer 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)
}

var goalState GoalState
if err = xml.Unmarshal(body, &goalState); err != nil {
return nil, fmt.Errorf("Unmarshalling XML failed: %s", err)
}
return &goalState, nil
}

// Build report ready XML from container and instance ID
func buildXML(containerID, instanceID string) ([]byte, error) {
xmlBytes, err := xml.Marshal(
Health{
GoalStateIncarnation: "1",
ContainerID: containerID,
InstanceID: instanceID,
State: "Ready"})
if err != nil {
return nil, fmt.Errorf("Marshalling XML failed: %s", err)
}
return xmlBytes, nil
}

// Post report ready XML to WireServer
func postReportReady(client *http.Client, body []byte) error {
req, err := http.NewRequest("POST", wireServerURL+"?comp=health", bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("http.NewRequest failed: %s", err)
}
req.Header.Set("x-ms-version", "2012-11-30")
req.Header.Set("x-ms-agent-name", "WALinuxAgent")
req.Header.Set("Content-Type", "text/xml;charset=utf-8")

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

0 comments on commit 8b187a9

Please sign in to comment.