diff --git a/cmd/cloudinitsave/cloudinitsave.go b/cmd/cloudinitsave/cloudinitsave.go index b9e96f0d..d715c988 100755 --- a/cmd/cloudinitsave/cloudinitsave.go +++ b/cmd/cloudinitsave/cloudinitsave.go @@ -34,6 +34,7 @@ import ( "github.com/rancher/os/config/cloudinit/datasource/configdrive" "github.com/rancher/os/config/cloudinit/datasource/file" "github.com/rancher/os/config/cloudinit/datasource/metadata/aliyun" + "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/gce" @@ -232,7 +233,11 @@ func getDatasources(datasources []string) []datasource.Datasource { switch parts[0] { case "*": - dss = append(dss, getDatasources([]string{"configdrive", "vmware", "ec2", "digitalocean", "packet", "gce"})...) + dss = append(dss, getDatasources([]string{"configdrive", "vmware", "ec2", "digitalocean", "packet", "gce", "cloudstack"})...) + case "cloudstack": + for _, source := range cloudstack.NewDatasource(root) { + dss = append(dss, source) + } case "ec2": dss = append(dss, ec2.NewDatasource(root)) case "file": diff --git a/config/cloudinit/datasource/metadata/cloudstack/metadata.go b/config/cloudinit/datasource/metadata/cloudstack/metadata.go new file mode 100755 index 00000000..3cb38188 --- /dev/null +++ b/config/cloudinit/datasource/metadata/cloudstack/metadata.go @@ -0,0 +1,118 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstack + +import ( + "net" + "strconv" + "strings" + + "github.com/rancher/os/netconf" + + "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/log" + "github.com/vishvananda/netlink" +) + +const ( + apiVersion = "latest/" + userdataPath = apiVersion + "user-data" + metadataPath = apiVersion + "meta-data/" + + serverIdentifier = "dhcp_server_identifier" +) + +type MetadataService struct { + metadata.Service +} + +func NewDatasource(root string) []*MetadataService { + roots := make([]string, 0, 5) + + if root == "" { + if links, err := netlink.LinkList(); err == nil { + log.Infof("Checking to see if a cloudstack server-identifier is available") + for _, link := range links { + linkName := link.Attrs().Name + if linkName == "lo" { + continue + } + + log.Infof("searching for cloudstack server %s on %s", serverIdentifier, linkName) + lease := netconf.GetDhcpLease(linkName) + if server, ok := lease[serverIdentifier]; ok { + log.Infof("found cloudstack server '%s'", server) + server = "http://" + server + "/" + roots = append(roots, server) + } + } + } else { + log.Errorf("error getting LinkList: %s", err) + } + } else { + roots = append(roots, root) + } + + sources := make([]*MetadataService, 0, len(roots)) + for _, server := range roots { + datasource := metadata.NewDatasourceWithCheckPath(server, apiVersion, metadataPath, userdataPath, metadataPath, nil) + sources = append(sources, &MetadataService{datasource}) + } + return sources +} + +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 "cloudstack-metadata-service" +} diff --git a/config/cloudinit/datasource/metadata/cloudstack/metadata_test.go b/config/cloudinit/datasource/metadata/cloudstack/metadata_test.go new file mode 100755 index 00000000..3729da98 --- /dev/null +++ b/config/cloudinit/datasource/metadata/cloudstack/metadata_test.go @@ -0,0 +1,102 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudstack + +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 := "cloudstack-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: "latest/meta-data/", + resources: map[string]string{ + "/latest/meta-data/local-hostname": "host", + "/latest/meta-data/local-ipv4": "1.2.3.4", + "/latest/meta-data/public-ipv4": "5.6.7.8", + "/latest/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: "latest/meta-data/", + resources: map[string]string{ + "/latest/meta-data/local-hostname": "host domain another_domain", + "/latest/meta-data/local-ipv4": "21.2.3.4", + "/latest/meta-data/public-ipv4": "25.6.7.8", + "/latest/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/config/cloudinit/datasource/metadata/metadata.go b/config/cloudinit/datasource/metadata/metadata.go index 97df4180..6a5fd152 100755 --- a/config/cloudinit/datasource/metadata/metadata.go +++ b/config/cloudinit/datasource/metadata/metadata.go @@ -26,25 +26,34 @@ import ( ) type Service struct { - Root string - Client pkg.Getter - APIVersion string - UserdataPath string - MetadataPath string - lastError error + Root string + Client pkg.Getter + APIVersion string + IsAvailableCheckPath string + UserdataPath string + MetadataPath string + lastError error } +// NewDatasource creates as HTTP based cloud-data service with the corresponding paths for the user-data and meta-data. +// To check the available in IsAvailable, the apiVersion is used as path. func NewDatasource(root, apiVersion, userdataPath, metadataPath string, header http.Header) Service { + return NewDatasourceWithCheckPath(root, apiVersion, apiVersion, userdataPath, metadataPath, header) +} + +// NewDatasourceWithCheckPath creates as HTTP based cloud-data service with the corresponding paths for the user-data and meta-data. +func NewDatasourceWithCheckPath(root, apiVersion, isAvailableCheckPath, userdataPath, metadataPath string, header http.Header) Service { if !strings.HasSuffix(root, "/") { root += "/" } - return Service{root, pkg.NewHTTPClientHeader(header), apiVersion, userdataPath, metadataPath, nil} + return Service{root, pkg.NewHTTPClientHeader(header), apiVersion, isAvailableCheckPath, userdataPath, metadataPath, nil} } func (ms Service) IsAvailable() bool { - _, ms.lastError = ms.Client.Get(ms.Root + ms.APIVersion) + checkURL := ms.Root + ms.IsAvailableCheckPath + _, ms.lastError = ms.Client.Get(checkURL) if ms.lastError != nil { - log.Errorf("%s: %s (lastError: %s)", "IsAvailable", ms.Root+":"+ms.UserdataPath, ms.lastError) + log.Errorf("%s: %s (lastError: %s)", "IsAvailable", checkURL, ms.lastError) } return (ms.lastError == nil) } @@ -54,7 +63,7 @@ func (ms *Service) Finish() error { } func (ms *Service) String() string { - return fmt.Sprintf("%s: %s (lastError: %s)", "metadata", ms.Root+ms.UserdataPath, ms.lastError) + return fmt.Sprintf("%s: %s (lastError: %s)", "metadata", ms.UserdataURL(), ms.lastError) } func (ms Service) AvailabilityChanges() bool { diff --git a/config/cloudinit/datasource/metadata/metadata_test.go b/config/cloudinit/datasource/metadata/metadata_test.go index 1ec63f70..e4a010fc 100644 --- a/config/cloudinit/datasource/metadata/metadata_test.go +++ b/config/cloudinit/datasource/metadata/metadata_test.go @@ -33,14 +33,14 @@ func TestAvailabilityChanges(t *testing.T) { func TestIsAvailable(t *testing.T) { for _, tt := range []struct { - root string - apiVersion string - resources map[string]string - expect bool + root string + checkPath string + resources map[string]string + expect bool }{ { - root: "/", - apiVersion: "2009-04-04", + root: "/", + checkPath: "2009-04-04", resources: map[string]string{ "/2009-04-04": "", }, @@ -53,9 +53,9 @@ func TestIsAvailable(t *testing.T) { }, } { service := &Service{ - Root: tt.root, - Client: &test.HTTPClient{Resources: tt.resources, Err: nil}, - APIVersion: tt.apiVersion, + Root: tt.root, + Client: &test.HTTPClient{Resources: tt.resources, Err: nil}, + IsAvailableCheckPath: tt.checkPath, } if a := service.IsAvailable(); a != tt.expect { t.Fatalf("bad isAvailable (%q): want %t, got %t", tt.resources, tt.expect, a) diff --git a/netconf/netconf_linux.go b/netconf/netconf_linux.go index 83e341ea..4f240922 100755 --- a/netconf/netconf_linux.go +++ b/netconf/netconf_linux.go @@ -191,7 +191,7 @@ func ApplyNetworkConfigs(netCfg *NetworkConfig, userSetHostname, userSetDNS bool linkName := link.Attrs().Name if linkName != "lo" { log.Infof("dns testing %s", linkName) - lease := getDhcpLease(linkName) + lease := GetDhcpLease(linkName) if _, ok := lease["domain_name_servers"]; ok { log.Infof("dns was dhcp set for %s", linkName) dnsSet = true @@ -238,7 +238,7 @@ func applyOuter(link netlink.Link, netCfg *NetworkConfig, wg *sync.WaitGroup, us }(linkName, match) } -func getDhcpLease(iface string) (lease map[string]string) { +func GetDhcpLease(iface string) (lease map[string]string) { lease = make(map[string]string) out := getDhcpLeaseString(iface)