forked from rancher-sandbox/linuxkit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rancher-sandbox#2 from davidcassany/azure_pr
Port of upstream Azure PR 3585
- Loading branch information
Showing
4 changed files
with
309 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
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,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 | ||
} |
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,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 | ||
} |