From 7adef1ea62d6f1e99d8bf76d13f2032b3dbcf315 Mon Sep 17 00:00:00 2001 From: Brad Beam Date: Wed, 3 Jul 2019 12:41:50 -0500 Subject: [PATCH] feat(init): Add azure as a supported platform Update initramfs to interact with azure endpoints for userdata. Signed-off-by: Brad Beam --- Makefile | 19 ++ .../internal/platform/cloud/azure/azure.go | 78 +++++++ .../platform/cloud/azure/azure_test.go | 14 ++ .../internal/platform/cloud/azure/register.go | 215 ++++++++++++++++++ .../app/init/internal/platform/platform.go | 15 +- 5 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 internal/app/init/internal/platform/cloud/azure/azure.go create mode 100644 internal/app/init/internal/platform/cloud/azure/azure_test.go create mode 100644 internal/app/init/internal/platform/cloud/azure/register.go diff --git a/Makefile b/Makefile index d41c243ac..eccf7c5b3 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/internal/app/init/internal/platform/cloud/azure/azure.go b/internal/app/init/internal/platform/cloud/azure/azure.go new file mode 100644 index 000000000..216fe7c72 --- /dev/null +++ b/internal/app/init/internal/platform/cloud/azure/azure.go @@ -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() +} diff --git a/internal/app/init/internal/platform/cloud/azure/azure_test.go b/internal/app/init/internal/platform/cloud/azure/azure_test.go new file mode 100644 index 000000000..2c4b217bc --- /dev/null +++ b/internal/app/init/internal/platform/cloud/azure/azure_test.go @@ -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 +} diff --git a/internal/app/init/internal/platform/cloud/azure/register.go b/internal/app/init/internal/platform/cloud/azure/register.go new file mode 100644 index 000000000..fa027db0c --- /dev/null +++ b/internal/app/init/internal/platform/cloud/azure/register.go @@ -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"` +} diff --git a/internal/app/init/internal/platform/platform.go b/internal/app/init/internal/platform/platform.go index 86969b076..71f0befc6 100644 --- a/internal/app/init/internal/platform/platform.go +++ b/internal/app/init/internal/platform/platform.go @@ -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) }