diff --git a/cmd/cloudinitsave/cloudinitsave.go b/cmd/cloudinitsave/cloudinitsave.go index c6ccaf4f..4e3e56c7 100644 --- a/cmd/cloudinitsave/cloudinitsave.go +++ b/cmd/cloudinitsave/cloudinitsave.go @@ -36,6 +36,7 @@ import ( "github.com/rancher/os/config/cloudinit/datasource/metadata/cloudstack" "github.com/rancher/os/config/cloudinit/datasource/metadata/digitalocean" "github.com/rancher/os/config/cloudinit/datasource/metadata/ec2" + "github.com/rancher/os/config/cloudinit/datasource/metadata/exoscale" "github.com/rancher/os/config/cloudinit/datasource/metadata/gce" "github.com/rancher/os/config/cloudinit/datasource/metadata/packet" "github.com/rancher/os/config/cloudinit/datasource/proccmdline" @@ -228,7 +229,9 @@ func getDatasources(datasources []string) []datasource.Datasource { switch parts[0] { case "*": - dss = append(dss, getDatasources([]string{"configdrive", "vmware", "ec2", "digitalocean", "packet", "gce", "cloudstack"})...) + dss = append(dss, getDatasources([]string{"configdrive", "vmware", "ec2", "digitalocean", "packet", "gce", "cloudstack", "exoscale"})...) + case "exoscale": + dss = append(dss, exoscale.NewDatasource(root)) case "cloudstack": for _, source := range cloudstack.NewDatasource(root) { dss = append(dss, source) diff --git a/config/cloudinit/datasource/metadata/exoscale/metadata.go b/config/cloudinit/datasource/metadata/exoscale/metadata.go new file mode 100644 index 00000000..a9889212 --- /dev/null +++ b/config/cloudinit/datasource/metadata/exoscale/metadata.go @@ -0,0 +1,82 @@ +package exoscale + +import ( + "net" + "strconv" + "strings" + + "github.com/rancher/os/config/cloudinit/datasource" + "github.com/rancher/os/config/cloudinit/datasource/metadata" + "github.com/rancher/os/config/cloudinit/pkg" + "github.com/rancher/os/pkg/log" +) + +const ( + defaultAddress = "http://169.254.169.254/" + apiVersion = "1.0/" + userdataPath = apiVersion + "user-data" + metadataPath = apiVersion + "meta-data/" +) + +type MetadataService struct { + metadata.Service +} + +func NewDatasource(root string) *MetadataService { + if root == "" { + root = defaultAddress + } + + return &MetadataService{ + metadata.NewDatasourceWithCheckPath( + root, + apiVersion, + metadataPath, + userdataPath, + metadataPath, + nil, + ), + } +} + +func (ms MetadataService) AvailabilityChanges() bool { + // TODO: if it can't find the network, maybe we can start it? + return false +} + +func (ms MetadataService) FetchMetadata() (datasource.Metadata, error) { + metadata := datasource.Metadata{} + + if sshKeys, err := ms.FetchAttributes("public-keys"); err == nil { + metadata.SSHPublicKeys = map[string]string{} + for i, sshkey := range sshKeys { + log.Printf("Found SSH key %d", i) + metadata.SSHPublicKeys[strconv.Itoa(i)] = sshkey + } + } else if _, ok := err.(pkg.ErrNotFound); !ok { + return metadata, err + } + + if hostname, err := ms.FetchAttribute("local-hostname"); err == nil { + metadata.Hostname = strings.Split(hostname, " ")[0] + } else if _, ok := err.(pkg.ErrNotFound); !ok { + return metadata, err + } + + if localAddr, err := ms.FetchAttribute("local-ipv4"); err == nil { + metadata.PrivateIPv4 = net.ParseIP(localAddr) + } else if _, ok := err.(pkg.ErrNotFound); !ok { + return metadata, err + } + if publicAddr, err := ms.FetchAttribute("public-ipv4"); err == nil { + metadata.PublicIPv4 = net.ParseIP(publicAddr) + } else if _, ok := err.(pkg.ErrNotFound); !ok { + return metadata, err + } + + return metadata, nil +} + +func (ms MetadataService) Type() string { + return "exoscale-metadata-service" +} diff --git a/config/cloudinit/datasource/metadata/exoscale/metadata_test.go b/config/cloudinit/datasource/metadata/exoscale/metadata_test.go new file mode 100644 index 00000000..d6039776 --- /dev/null +++ b/config/cloudinit/datasource/metadata/exoscale/metadata_test.go @@ -0,0 +1,88 @@ +package exoscale + +import ( + "fmt" + "net" + "reflect" + "testing" + + "github.com/rancher/os/config/cloudinit/datasource" + "github.com/rancher/os/config/cloudinit/datasource/metadata" + "github.com/rancher/os/config/cloudinit/datasource/metadata/test" + "github.com/rancher/os/config/cloudinit/pkg" +) + +func TestType(t *testing.T) { + want := "exoscale-metadata-service" + if kind := (MetadataService{}).Type(); kind != want { + t.Fatalf("bad type: want %q, got %q", want, kind) + } +} + +func TestFetchMetadata(t *testing.T) { + for _, tt := range []struct { + root string + metadataPath string + resources map[string]string + expect datasource.Metadata + clientErr error + expectErr error + }{ + { + root: "/", + metadataPath: "1.0/meta-data/", + resources: map[string]string{ + "/1.0/meta-data/local-hostname": "host", + "/1.0/meta-data/local-ipv4": "1.2.3.4", + "/1.0/meta-data/public-ipv4": "5.6.7.8", + "/1.0/meta-data/public-keys": "key\n", + }, + expect: datasource.Metadata{ + Hostname: "host", + PrivateIPv4: net.ParseIP("1.2.3.4"), + PublicIPv4: net.ParseIP("5.6.7.8"), + SSHPublicKeys: map[string]string{"0": "key"}, + }, + }, + { + root: "/", + metadataPath: "1.0/meta-data/", + resources: map[string]string{ + "/1.0/meta-data/local-hostname": "host domain another_domain", + "/1.0/meta-data/local-ipv4": "21.2.3.4", + "/1.0/meta-data/public-ipv4": "25.6.7.8", + "/1.0/meta-data/public-keys": "key\n", + }, + expect: datasource.Metadata{ + Hostname: "host", + PrivateIPv4: net.ParseIP("21.2.3.4"), + PublicIPv4: net.ParseIP("25.6.7.8"), + SSHPublicKeys: map[string]string{"0": "key"}, + }, + }, + { + clientErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")}, + expectErr: pkg.ErrTimeout{Err: fmt.Errorf("test error")}, + }, + } { + service := &MetadataService{metadata.Service{ + Root: tt.root, + Client: &test.HTTPClient{Resources: tt.resources, Err: tt.clientErr}, + MetadataPath: tt.metadataPath, + }} + metadata, err := service.FetchMetadata() + if Error(err) != Error(tt.expectErr) { + t.Fatalf("bad error (%q): \nwant %q, \ngot %q\n", tt.resources, tt.expectErr, err) + } + if !reflect.DeepEqual(tt.expect, metadata) { + t.Fatalf("bad fetch (%q): \nwant %#v, \ngot %#v\n", tt.resources, tt.expect, metadata) + } + } +} + +func Error(err error) string { + if err != nil { + return err.Error() + } + return "" +} diff --git a/pkg/init/cloudinit/cloudinit.go b/pkg/init/cloudinit/cloudinit.go index dbf6cab4..2dfad308 100644 --- a/pkg/init/cloudinit/cloudinit.go +++ b/pkg/init/cloudinit/cloudinit.go @@ -1,6 +1,7 @@ package cloudinit import ( + "io/ioutil" "path/filepath" "strings" @@ -27,6 +28,14 @@ func CloudInit(cfg *config.CloudConfig) (*config.CloudConfig, error) { cfg.Rancher.CloudInit.Datasources = append(cfg.Rancher.CloudInit.Datasources, hypervisor) } + exoscale, err := onlyExoscale() + if err != nil { + log.Error(err) + } + if exoscale { + cfg.Rancher.CloudInit.Datasources = append([]string{"exoscale"}, cfg.Rancher.CloudInit.Datasources...) + } + if len(cfg.Rancher.CloudInit.Datasources) == 0 { log.Info("No specific datasources, ignore cloudinit") return cfg, nil @@ -133,3 +142,12 @@ func onlyDigitalOcean(datasources []string) bool { } return false } + +func onlyExoscale() (bool, error) { + f, err := ioutil.ReadFile("/sys/class/dmi/id/product_name") + if err != nil { + return false, err + } + + return strings.HasPrefix(string(f), "Exoscale"), nil +}