feat(init): Add azure as a supported platform

Update initramfs to interact with azure endpoints for userdata.

Signed-off-by: Brad Beam <brad.beam@talos-systems.com>
This commit is contained in:
Brad Beam 2019-07-03 12:41:50 -05:00 committed by Andrew Rynhard
parent e9482a4041
commit 7adef1ea62
5 changed files with 335 additions and 6 deletions

View File

@ -300,3 +300,22 @@ push: gitmeta
.PHONY: clean
clean:
@-rm -rf build images vendor
.PHONY: talos-azure
talos-azure:
@docker run --rm -v /dev:/dev -v $(PWD)/build:/out \
--privileged $(DOCKER_ARGS) \
autonomy/installer:$(TAG) \
install \
-n disk \
-r \
-p azure \
-u none \
-e rootdelay=300
@docker run --rm -v $(PWD)/build:/out $(DOCKER_ARGS) \
--entrypoint qemu-img \
autonomy/installer:$(TAG) \
convert \
-f raw \
-o subformat=fixed,force_size \
-O vpc /out/disk.raw /out/talos-azure.vhd

View File

@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package azure
import (
"github.com/talos-systems/talos/pkg/userdata"
)
const (
// AzureUserDataEndpoint is the local endpoint for the user data.
// By specifying format=text and drilling down to the actual key we care about
// we get a base64 encoded userdata response
AzureUserDataEndpoint = "http://169.254.169.254/metadata/instance/compute/customData?api-version=2019-06-01&format=text"
// AzureHostnameEndpoint is the local endpoint for the hostname.
AzureHostnameEndpoint = "http://169.254.169.254/metadata/instance/compute/name?api-version=2019-06-01&format=text"
// AzureInternalEndpoint is the Azure Internal Channel IP
// https://blogs.msdn.microsoft.com/mast/2015/05/18/what-is-the-ip-address-168-63-129-16/
AzureInternalEndpoint = "http://168.63.129.16"
)
// Azure is the concrete type that implements the platform.Platform interface.
type Azure struct{}
// Name implements the platform.Platform interface.
func (a *Azure) Name() string {
return "Azure"
}
// UserData implements the platform.Platform interface.
func (a *Azure) UserData() (*userdata.UserData, error) {
if err := linuxAgent(); err != nil {
return nil, err
}
return userdata.Download(AzureUserDataEndpoint, userdata.WithHeaders(map[string]string{"Metadata": "true"}), userdata.WithFormat("base64"))
}
// Prepare implements the platform.Platform interface and handles initial host preparation.
func (a *Azure) Prepare(data *userdata.UserData) (err error) {
return nil
}
func hostname() (err error) {
// TODO get this sorted; assuming we need to set appropriate headers
return err
/*
resp, err := http.Get(AzureHostnameEndpoint)
if err != nil {
return
}
// nolint: errcheck
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download user data: %d", resp.StatusCode)
}
dataBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if err = unix.Sethostname(dataBytes); err != nil {
return
}
return nil
*/
}
// Install implements the platform.Platform interface and handles additional system setup.
func (a *Azure) Install(data *userdata.UserData) (err error) {
return hostname()
}

View File

@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package azure_test
import "testing"
func TestEmpty(t *testing.T) {
// added for accurate coverage estimation
//
// please remove it once any unit-test is added
// for this package
}

View File

@ -0,0 +1,215 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package azure
import (
"bytes"
"encoding/xml"
"io/ioutil"
"net/http"
"net/url"
)
// This should provide the bare minimum to trigger a node in ready condition to allow
// azure to be happy with the node and let it on it's lawn.
func linuxAgent() (err error) {
var gs *GoalState
gs, err = goalState()
if err != nil {
return err
}
return reportHealth(gs.Incarnation, gs.Container.ContainerID, gs.Container.RoleInstanceList.RoleInstance.InstanceID)
}
func goalState() (gs *GoalState, err error) {
u, err := url.Parse(AzureInternalEndpoint + "/machine/?comp=goalstate")
if err != nil {
return gs, nil
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return gs, err
}
addHeaders(req)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return gs, err
}
// nolint: errcheck
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return gs, err
}
gs = &GoalState{}
err = xml.Unmarshal(body, gs)
return gs, err
}
func reportHealth(gsIncarnation, gsContainerID, gsInstanceID string) (err error) {
// Construct health response
h := &Health{
Xsi: "http://www.w3.org/2001/XMLSchema-instance",
Xsd: "http://www.w3.org/2001/XMLSchema",
WAAgent: WAAgent{
GoalStateIncarnation: gsIncarnation,
Container: &Container{
ContainerID: gsContainerID,
RoleInstanceList: &RoleInstanceList{
Role: &RoleInstance{
InstanceID: gsInstanceID,
Health: &HealthStatus{
State: "Ready",
},
},
},
},
},
}
// Encode health response as xml
b := new(bytes.Buffer)
b.WriteString(xml.Header)
err = xml.NewEncoder(b).Encode(h)
if err != nil {
return err
}
var u *url.URL
u, err = url.Parse(AzureInternalEndpoint + "/machine/?comp=health")
if err != nil {
return nil
}
var req *http.Request
var resp *http.Response
req, err = http.NewRequest("POST", u.String(), b)
if err != nil {
return err
}
addHeaders(req)
client := &http.Client{}
resp, err = client.Do(req)
if err != nil {
return err
}
// TODO probably should do some better check here ( verify status code )
// nolint: errcheck
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return err
}
func addHeaders(req *http.Request) {
req.Header.Add("x-ms-agent-name", "WALinuxAgent")
req.Header.Add("x-ms-version", "2015-04-05")
req.Header.Add("Content-Type", "text/xml;charset=utf-8")
}
// GoalState is the response from the Azure platform when a machine
// starts up. Ref:
// https://github.com/Azure/WALinuxAgent/blob/b26feb7822f7d4a19507b6762fe1bd280c2ba2de/bin/waagent2.0#L4331
// https://github.com/Azure/WALinuxAgent/blob/3be3e1fbf2330303f76961b87d891672e847ce4e/azurelinuxagent/common/protocol/wire.py#L216
type GoalState struct {
XMLName xml.Name `xml:"GoalState"`
Xsi string `xml:"xsi,attr"`
Xsd string `xml:"xsd,attr"`
WAAgent
}
// Health is the response from the local machine to Azure to denote current
// machine state.
type Health struct {
XMLName xml.Name `xml:"Health"`
Xsi string `xml:"xmlns:xsi,attr"`
Xsd string `xml:"xmlns:xsd,attr"`
WAAgent
}
// WAAgent contains the meat of the data format that is passed between the
// Azure platform and the machine.
// Mostly, we just care about the Incarnation and Container fields here.
type WAAgent struct {
Text string `xml:",chardata"`
Version string `xml:"Version,omitempty"`
Incarnation string `xml:"Incarnation,omitempty"`
GoalStateIncarnation string `xml:"GoalStateIncarnation,omitempty"`
Machine *Machine `xml:"Machine,omitempty"`
Container *Container `xml:"Container,omitempty"`
}
// Container holds the interesting details about a provisioned machine.
type Container struct {
Text string `xml:",chardata"`
ContainerID string `xml:"ContainerId"`
RoleInstanceList *RoleInstanceList `xml:"RoleInstanceList"`
}
// RoleInstanceList is a list but only has a single item which is cool I guess.
type RoleInstanceList struct {
Text string `xml:",chardata"`
RoleInstance *RoleInstance `xml:"RoleInstance,omitempty"`
Role *RoleInstance `xml:"Role,omitempty"`
}
// RoleInstance contains the specifics for the provisioned VM.
type RoleInstance struct {
Text string `xml:",chardata"`
InstanceID string `xml:"InstanceId"`
State string `xml:"State,omitempty"`
Configuration *Configuration `xml:"Configuration,omitempty"`
Health *HealthStatus `xml:"Health,omitempty"`
}
// Configuration seems important but isnt really used right now. We could
// very well not include it because we have no use for it right now, but
// since we want completeness, we're going to include it.
type Configuration struct {
Text string `xml:",chardata"`
HostingEnvironmentConfig string `xml:"HostingEnvironmentConfig"`
SharedConfig string `xml:"SharedConfig"`
ExtensionsConfig string `xml:"ExtensionsConfig"`
FullConfig string `xml:"FullConfig"`
Certificates string `xml:"Certificates"`
ConfigName string `xml:"ConfigName"`
}
// Machine holds no useful information for us.
type Machine struct {
Text string `xml:",chardata"`
ExpectedState string `xml:"ExpectedState"`
StopRolesDeadlineHint string `xml:"StopRolesDeadlineHint"`
LBProbePorts *struct {
Text string `xml:",chardata"`
Port string `xml:"Port"`
} `xml:"LBProbePorts,omitempty"`
ExpectHealthReport string `xml:"ExpectHealthReport"`
}
// HealthStatus provides mechanism to trigger Azure to understand that our
// machine has transitioned to a 'Ready' state and is good to go.
// We can fill out details if we want to be more verbose...
type HealthStatus struct {
Text string `xml:",chardata"`
State string `xml:"State"`
Details *struct {
Text string `xml:",chardata"`
SubStatus string `xml:"SubStatus"`
Description string `xml:"Description"`
} `xml:"Details,omitempty"`
}

View File

@ -8,6 +8,7 @@ import (
"github.com/pkg/errors"
"github.com/talos-systems/talos/internal/app/init/internal/platform/baremetal"
"github.com/talos-systems/talos/internal/app/init/internal/platform/cloud/aws"
"github.com/talos-systems/talos/internal/app/init/internal/platform/cloud/azure"
"github.com/talos-systems/talos/internal/app/init/internal/platform/cloud/googlecloud"
"github.com/talos-systems/talos/internal/app/init/internal/platform/cloud/packet"
"github.com/talos-systems/talos/internal/app/init/internal/platform/cloud/vmware"
@ -35,16 +36,18 @@ func NewPlatform() (p Platform, err error) {
switch *platform {
case "aws":
p = &aws.AWS{}
case "googlecloud":
p = &googlecloud.GoogleCloud{}
case "vmware":
p = &vmware.VMware{}
case "azure":
p = &azure.Azure{}
case "bare-metal":
p = &baremetal.BareMetal{}
case "packet":
p = &packet.Packet{}
case "googlecloud":
p = &googlecloud.GoogleCloud{}
case "iso":
p = &iso.ISO{}
case "packet":
p = &packet.Packet{}
case "vmware":
p = &vmware.VMware{}
default:
return nil, errors.Errorf("platform not supported: %s", *platform)
}