From abe43537dabfc83afab40579c2c5e0a49acc81d0 Mon Sep 17 00:00:00 2001 From: Alex Crawford Date: Fri, 29 Aug 2014 16:18:16 -0400 Subject: [PATCH 1/4] metadata: Merge the network config --- coreos-cloudinit.go | 7 +++++++ coreos-cloudinit_test.go | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/coreos-cloudinit.go b/coreos-cloudinit.go index 0668924..1a7166b 100644 --- a/coreos-cloudinit.go +++ b/coreos-cloudinit.go @@ -209,6 +209,13 @@ func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudCon udcc.NetworkConfigPath = mdcc.NetworkConfigPath } } + if mdcc.NetworkConfig != "" { + if udcc.NetworkConfig != "" { + fmt.Printf("Warning: user-data NetworkConfig %s overrides metadata NetworkConfig %s\n", udcc.NetworkConfig, mdcc.NetworkConfig) + } else { + udcc.NetworkConfig = mdcc.NetworkConfig + } + } return udcc } diff --git a/coreos-cloudinit_test.go b/coreos-cloudinit_test.go index 6c6e7f0..81687da 100644 --- a/coreos-cloudinit_test.go +++ b/coreos-cloudinit_test.go @@ -12,6 +12,7 @@ func TestMergeCloudConfig(t *testing.T) { SSHAuthorizedKeys: []string{"abc", "def"}, Hostname: "foobar", NetworkConfigPath: "/path/somewhere", + NetworkConfig: `{}`, } for i, tt := range []struct { udcc initialize.CloudConfig @@ -36,6 +37,7 @@ func TestMergeCloudConfig(t *testing.T) { initialize.CloudConfig{ Hostname: "meta-hostname", NetworkConfigPath: "/path/meta", + NetworkConfig: `{"hostname":"test"}`, }, simplecc, }, @@ -45,6 +47,7 @@ func TestMergeCloudConfig(t *testing.T) { SSHAuthorizedKeys: []string{"abc", "def"}, Hostname: "user-hostname", NetworkConfigPath: "/path/somewhere", + NetworkConfig: `{"hostname":"test"}`, }, initialize.CloudConfig{ SSHAuthorizedKeys: []string{"woof", "qux"}, @@ -54,6 +57,7 @@ func TestMergeCloudConfig(t *testing.T) { SSHAuthorizedKeys: []string{"abc", "def", "woof", "qux"}, Hostname: "user-hostname", NetworkConfigPath: "/path/somewhere", + NetworkConfig: `{"hostname":"test"}`, }, }, { @@ -64,11 +68,13 @@ func TestMergeCloudConfig(t *testing.T) { initialize.CloudConfig{ SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"}, NetworkConfigPath: "/dev/fun", + NetworkConfig: `{"hostname":"test"}`, }, initialize.CloudConfig{ Hostname: "supercool", SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"}, NetworkConfigPath: "/dev/fun", + NetworkConfig: `{"hostname":"test"}`, }, }, { @@ -80,11 +86,13 @@ func TestMergeCloudConfig(t *testing.T) { initialize.CloudConfig{ Hostname: "youyouyou", NetworkConfigPath: "meta-meta-yo", + NetworkConfig: `{"hostname":"test"}`, }, initialize.CloudConfig{ Hostname: "mememe", ManageEtcHosts: initialize.EtcHosts("lolz"), NetworkConfigPath: "meta-meta-yo", + NetworkConfig: `{"hostname":"test"}`, }, }, { @@ -95,10 +103,12 @@ func TestMergeCloudConfig(t *testing.T) { initialize.CloudConfig{ ManageEtcHosts: initialize.EtcHosts("lolz"), NetworkConfigPath: "meta-meta-yo", + NetworkConfig: `{"hostname":"test"}`, }, initialize.CloudConfig{ Hostname: "mememe", NetworkConfigPath: "meta-meta-yo", + NetworkConfig: `{"hostname":"test"}`, }, }, } { From 2a8e6c95668231a286091e9c4a3e3fad4b25a5b0 Mon Sep 17 00:00:00 2001 From: Alex Crawford Date: Thu, 21 Aug 2014 13:55:54 -0700 Subject: [PATCH 2/4] network: Fall back to MAC address if there is no name --- network/interface.go | 6 +++++- network/interface_test.go | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/network/interface.go b/network/interface.go index 6bad7a2..cc0eec2 100644 --- a/network/interface.go +++ b/network/interface.go @@ -82,7 +82,11 @@ func (i *logicalInterface) Netdev() string { } func (i *logicalInterface) Filename() string { - return fmt.Sprintf("%02x-%s", i.configDepth, i.name) + name := i.name + if name == "" { + name = i.hwaddr.String() + } + return fmt.Sprintf("%02x-%s", i.configDepth, name) } func (i *logicalInterface) Children() []networkInterface { diff --git a/network/interface_test.go b/network/interface_test.go index 51d9458..c2ec3dc 100644 --- a/network/interface_test.go +++ b/network/interface_test.go @@ -344,6 +344,8 @@ func TestFilename(t *testing.T) { {logicalInterface{name: "iface", configDepth: 9}, "09-iface"}, {logicalInterface{name: "iface", configDepth: 10}, "0a-iface"}, {logicalInterface{name: "iface", configDepth: 53}, "35-iface"}, + {logicalInterface{hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), configDepth: 1}, "01-01:23:45:67:89:ab"}, + {logicalInterface{name: "iface", hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), configDepth: 1}, "01-iface"}, } { if tt.i.Filename() != tt.f { t.Fatalf("bad filename (%q): got %q, want %q", tt.i, tt.i.Filename(), tt.f) From 3abd6b2225f0cf0117a5a385b4ac59c19096a17c Mon Sep 17 00:00:00 2001 From: Alex Crawford Date: Fri, 15 Aug 2014 18:14:34 -0700 Subject: [PATCH 3/4] digitalocean: Add DigitalOcean metadata service Move debian-related processing into its own file. --- coreos-cloudinit.go | 32 ++-- datasource/metadata/digitalocean/metadata.go | 107 +++++++++++++ .../metadata/digitalocean/metadata_test.go | 99 ++++++++++++ initialize/config.go | 2 + network/{network.go => debian.go} | 4 + network/{network_test.go => debian_test.go} | 0 network/digitalocean.go | 142 ++++++++++++++++++ test | 1 + 8 files changed, 372 insertions(+), 15 deletions(-) create mode 100644 datasource/metadata/digitalocean/metadata.go create mode 100644 datasource/metadata/digitalocean/metadata_test.go rename network/{network.go => debian.go} (83%) rename network/{network_test.go => debian_test.go} (100%) create mode 100644 network/digitalocean.go diff --git a/coreos-cloudinit.go b/coreos-cloudinit.go index 1a7166b..08b6332 100644 --- a/coreos-cloudinit.go +++ b/coreos-cloudinit.go @@ -11,6 +11,7 @@ import ( "github.com/coreos/coreos-cloudinit/datasource/configdrive" "github.com/coreos/coreos-cloudinit/datasource/file" "github.com/coreos/coreos-cloudinit/datasource/metadata/cloudsigma" + "github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean" "github.com/coreos/coreos-cloudinit/datasource/metadata/ec2" "github.com/coreos/coreos-cloudinit/datasource/proc_cmdline" "github.com/coreos/coreos-cloudinit/datasource/url" @@ -30,13 +31,14 @@ var ( printVersion bool ignoreFailure bool sources struct { - file string - configDrive string - metadataService bool - ec2MetadataService string - cloudSigmaMetadataService bool - url string - procCmdLine bool + file string + configDrive string + metadataService bool + ec2MetadataService string + cloudSigmaMetadataService bool + digitalOceanMetadataService string + url string + procCmdLine bool } convertNetconf string workspace string @@ -49,11 +51,12 @@ func init() { flag.StringVar(&sources.file, "from-file", "", "Read user-data from provided file") flag.StringVar(&sources.configDrive, "from-configdrive", "", "Read data from provided cloud-drive directory") flag.BoolVar(&sources.metadataService, "from-metadata-service", false, "[DEPRECATED - Use -from-ec2-metadata] Download data from metadata service") - flag.StringVar(&sources.ec2MetadataService, "from-ec2-metadata", "", "Download data from the provided metadata service") + flag.StringVar(&sources.ec2MetadataService, "from-ec2-metadata", "", "Download EC2 data from the provided url") flag.BoolVar(&sources.cloudSigmaMetadataService, "from-cloudsigma-metadata", false, "Download data from CloudSigma server context") + flag.StringVar(&sources.digitalOceanMetadataService, "from-digitalocean-metadata", "", "Download DigitalOcean data from the provided url") flag.StringVar(&sources.url, "from-url", "", "Download user-data from provided url") flag.BoolVar(&sources.procCmdLine, "from-proc-cmdline", false, fmt.Sprintf("Parse %s for '%s=', using the cloud-config served by an HTTP GET to ", proc_cmdline.ProcCmdlineLocation, proc_cmdline.ProcCmdlineCloudConfigFlag)) - flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files (requires the -from-configdrive flag)") + flag.StringVar(&convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files") flag.StringVar(&workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data") flag.StringVar(&sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name") } @@ -73,16 +76,12 @@ func main() { os.Exit(0) } - if convertNetconf != "" && sources.configDrive == "" { - fmt.Println("-convert-netconf flag requires -from-configdrive") - os.Exit(1) - } - switch convertNetconf { case "": case "debian": + case "digitalocean": default: - fmt.Printf("Invalid option to -convert-netconf: '%s'. Supported options: 'debian'\n", convertNetconf) + fmt.Printf("Invalid option to -convert-netconf: '%s'. Supported options: 'debian, digitalocean'\n", convertNetconf) os.Exit(1) } @@ -241,6 +240,9 @@ func getDatasources() []datasource.Datasource { if sources.cloudSigmaMetadataService { dss = append(dss, cloudsigma.NewServerContextService()) } + if sources.digitalOceanMetadataService != "" { + dss = append(dss, digitalocean.NewDatasource(sources.digitalOceanMetadataService)) + } if sources.procCmdLine { dss = append(dss, proc_cmdline.NewDatasource()) } diff --git a/datasource/metadata/digitalocean/metadata.go b/datasource/metadata/digitalocean/metadata.go new file mode 100644 index 0000000..77f2988 --- /dev/null +++ b/datasource/metadata/digitalocean/metadata.go @@ -0,0 +1,107 @@ +package digitalocean + +import ( + "encoding/json" + "strconv" + + "github.com/coreos/coreos-cloudinit/datasource/metadata" +) + +const ( + DefaultAddress = "http://169.254.169.254/" + apiVersion = "metadata/v1" + userdataUrl = apiVersion + "/user-data" + metadataPath = apiVersion + ".json" +) + +type Address struct { + IPAddress string `json:"ip_address"` + Netmask string `json:"netmask"` + Cidr int `json:"cidr"` + Gateway string `json:"gateway"` +} + +type Interface struct { + IPv4 *Address `json:"ipv4"` + IPv6 *Address `json:"ipv6"` + MAC string `json:"mac"` + Type string `json:"type"` +} + +type Interfaces struct { + Public []Interface `json:"public"` + Private []Interface `json:"private"` +} + +type DNS struct { + Nameservers []string `json:"nameservers"` +} + +type Metadata struct { + Hostname string `json:"hostname"` + Interfaces Interfaces `json:"interfaces"` + PublicKeys []string `json:"public_keys"` + DNS DNS `json:"dns"` +} + +type metadataService struct { + interfaces Interfaces + dns DNS + metadata.MetadataService +} + +func NewDatasource(root string) *metadataService { + return &metadataService{MetadataService: metadata.NewDatasource(root, apiVersion, userdataUrl, metadataPath)} +} + +func (ms *metadataService) FetchMetadata() ([]byte, error) { + data, err := ms.FetchData(ms.MetadataUrl()) + if err != nil || len(data) == 0 { + return []byte{}, err + } + + var metadata Metadata + if err := json.Unmarshal(data, &metadata); err != nil { + return []byte{}, err + } + + ms.interfaces = metadata.Interfaces + ms.dns = metadata.DNS + + attrs := make(map[string]interface{}) + if len(metadata.Interfaces.Public) > 0 { + if metadata.Interfaces.Public[0].IPv4 != nil { + attrs["public-ipv4"] = metadata.Interfaces.Public[0].IPv4.IPAddress + } + if metadata.Interfaces.Public[0].IPv6 != nil { + attrs["public-ipv6"] = metadata.Interfaces.Public[0].IPv6.IPAddress + } + } + if len(metadata.Interfaces.Private) > 0 { + if metadata.Interfaces.Private[0].IPv4 != nil { + attrs["local-ipv4"] = metadata.Interfaces.Private[0].IPv4.IPAddress + } + if metadata.Interfaces.Private[0].IPv6 != nil { + attrs["local-ipv6"] = metadata.Interfaces.Private[0].IPv6.IPAddress + } + } + attrs["hostname"] = metadata.Hostname + keys := make(map[string]string) + for i, key := range metadata.PublicKeys { + keys[strconv.Itoa(i)] = key + } + attrs["public_keys"] = keys + + return json.Marshal(attrs) +} + +func (ms metadataService) FetchNetworkConfig(filename string) ([]byte, error) { + return json.Marshal(Metadata{ + Interfaces: ms.interfaces, + DNS: ms.dns, + }) +} + +func (ms metadataService) Type() string { + return "digitalocean-metadata-service" +} diff --git a/datasource/metadata/digitalocean/metadata_test.go b/datasource/metadata/digitalocean/metadata_test.go new file mode 100644 index 0000000..0917e8e --- /dev/null +++ b/datasource/metadata/digitalocean/metadata_test.go @@ -0,0 +1,99 @@ +package digitalocean + +import ( + "bytes" + "fmt" + "testing" + + "github.com/coreos/coreos-cloudinit/datasource/metadata" + "github.com/coreos/coreos-cloudinit/datasource/metadata/test" + "github.com/coreos/coreos-cloudinit/pkg" +) + +func TestType(t *testing.T) { + want := "digitalocean-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 []byte + clientErr error + expectErr error + }{ + { + root: "/", + metadataPath: "v1.json", + resources: map[string]string{ + "/v1.json": "bad", + }, + expectErr: fmt.Errorf("invalid character 'b' looking for beginning of value"), + }, + { + root: "/", + metadataPath: "v1.json", + resources: map[string]string{ + "/v1.json": `{ + "droplet_id": 1, + "user_data": "hello", + "vendor_data": "hello", + "public_keys": [ + "publickey1", + "publickey2" + ], + "region": "nyc2", + "interfaces": { + "public": [ + { + "ipv4": { + "ip_address": "192.168.1.2", + "netmask": "255.255.255.0", + "gateway": "192.168.1.1" + }, + "ipv6": { + "ip_address": "fe00::", + "cidr": 126, + "gateway": "fe00::" + }, + "mac": "ab:cd:ef:gh:ij", + "type": "public" + } + ] + } +}`, + }, + expect: []byte(`{"hostname":"","public-ipv4":"192.168.1.2","public-ipv6":"fe00::","public_keys":{"0":"publickey1","1":"publickey2"}}`), + }, + { + clientErr: pkg.ErrTimeout{fmt.Errorf("test error")}, + expectErr: pkg.ErrTimeout{fmt.Errorf("test error")}, + }, + } { + service := &metadataService{ + MetadataService: metadata.MetadataService{ + Root: tt.root, + Client: &test.HttpClient{tt.resources, tt.clientErr}, + MetadataPath: tt.metadataPath, + }, + } + metadata, err := service.FetchMetadata() + if Error(err) != Error(tt.expectErr) { + t.Fatalf("bad error (%q): want %q, got %q", tt.resources, tt.expectErr, err) + } + if !bytes.Equal(metadata, tt.expect) { + t.Fatalf("bad fetch (%q): want %q, got %q", tt.resources, tt.expect, metadata) + } + } +} + +func Error(err error) string { + if err != nil { + return err.Error() + } + return "" +} diff --git a/initialize/config.go b/initialize/config.go index 3d07328..be5d090 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -263,6 +263,8 @@ func Apply(cfg CloudConfig, env *Environment) error { switch env.NetconfType() { case "debian": interfaces, err = network.ProcessDebianNetconf(cfg.NetworkConfig) + case "digitalocean": + interfaces, err = network.ProcessDigitalOceanNetconf(cfg.NetworkConfig) default: return fmt.Errorf("Unsupported network config format %q", env.NetconfType()) } diff --git a/network/network.go b/network/debian.go similarity index 83% rename from network/network.go rename to network/debian.go index 2af5f39..f686c3e 100644 --- a/network/network.go +++ b/network/debian.go @@ -1,10 +1,12 @@ package network import ( + "log" "strings" ) func ProcessDebianNetconf(config string) ([]InterfaceGenerator, error) { + log.Println("Processing Debian network config") lines := formatConfig(config) stanzas, err := parseStanzas(lines) if err != nil { @@ -18,7 +20,9 @@ func ProcessDebianNetconf(config string) ([]InterfaceGenerator, error) { interfaces = append(interfaces, s) } } + log.Printf("Parsed %d network interfaces\n", len(interfaces)) + log.Println("Processed Debian network config") return buildInterfaces(interfaces), nil } diff --git a/network/network_test.go b/network/debian_test.go similarity index 100% rename from network/network_test.go rename to network/debian_test.go diff --git a/network/digitalocean.go b/network/digitalocean.go new file mode 100644 index 0000000..b924e42 --- /dev/null +++ b/network/digitalocean.go @@ -0,0 +1,142 @@ +package network + +import ( + "encoding/json" + "fmt" + "log" + "net" + + "github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean" +) + +func ProcessDigitalOceanNetconf(config string) ([]InterfaceGenerator, error) { + log.Println("Processing DigitalOcean network config") + if config == "" { + return nil, nil + } + + var cfg digitalocean.Metadata + if err := json.Unmarshal([]byte(config), &cfg); err != nil { + return nil, err + } + + log.Println("Parsing nameservers") + nameservers, err := parseNameservers(cfg.DNS) + if err != nil { + return nil, err + } + log.Printf("Parsed %d nameservers\n", len(nameservers)) + + log.Println("Parsing interfaces") + generators, err := parseInterfaces(cfg.Interfaces, nameservers) + if err != nil { + return nil, err + } + log.Printf("Parsed %d network interfaces\n", len(generators)) + + log.Println("Processed DigitalOcean network config") + return generators, nil +} + +func parseNameservers(cfg digitalocean.DNS) ([]net.IP, error) { + nameservers := make([]net.IP, 0, len(cfg.Nameservers)) + for _, ns := range cfg.Nameservers { + if ip := net.ParseIP(ns); ip == nil { + return nil, fmt.Errorf("could not parse %q as nameserver IP address", ns) + } else { + nameservers = append(nameservers, ip) + } + } + return nameservers, nil +} + +func parseInterfaces(cfg digitalocean.Interfaces, nameservers []net.IP) ([]InterfaceGenerator, error) { + generators := make([]InterfaceGenerator, 0, len(cfg.Public)+len(cfg.Private)) + for _, iface := range cfg.Public { + if generator, err := parseInterface(iface, nameservers, true); err == nil { + generators = append(generators, &physicalInterface{*generator}) + } else { + return nil, err + } + } + for _, iface := range cfg.Private { + if generator, err := parseInterface(iface, []net.IP{}, false); err == nil { + generators = append(generators, &physicalInterface{*generator}) + } else { + return nil, err + } + } + return generators, nil +} + +func parseInterface(iface digitalocean.Interface, nameservers []net.IP, useRoute bool) (*logicalInterface, error) { + routes := make([]route, 0) + addresses := make([]net.IPNet, 0) + if iface.IPv4 != nil { + var ip, mask, gateway net.IP + if ip = net.ParseIP(iface.IPv4.IPAddress); ip == nil { + return nil, fmt.Errorf("could not parse %q as IPv4 address", iface.IPv4.IPAddress) + } + if mask = net.ParseIP(iface.IPv4.Netmask); mask == nil { + return nil, fmt.Errorf("could not parse %q as IPv4 mask", iface.IPv4.Netmask) + } + addresses = append(addresses, net.IPNet{ + IP: ip, + Mask: net.IPMask(mask), + }) + + if useRoute { + if gateway = net.ParseIP(iface.IPv4.Gateway); gateway == nil { + return nil, fmt.Errorf("could not parse %q as IPv4 gateway", iface.IPv4.Gateway) + } + routes = append(routes, route{ + destination: net.IPNet{ + IP: net.IPv4zero, + Mask: net.IPMask(net.IPv4zero), + }, + gateway: gateway, + }) + } + } + if iface.IPv6 != nil { + var ip, gateway net.IP + if ip = net.ParseIP(iface.IPv6.IPAddress); ip == nil { + return nil, fmt.Errorf("could not parse %q as IPv6 address", iface.IPv6.IPAddress) + } + addresses = append(addresses, net.IPNet{ + IP: ip, + Mask: net.CIDRMask(iface.IPv6.Cidr, net.IPv6len*8), + }) + + if useRoute { + if gateway = net.ParseIP(iface.IPv6.Gateway); gateway == nil { + return nil, fmt.Errorf("could not parse %q as IPv6 gateway", iface.IPv6.Gateway) + } + routes = append(routes, route{ + destination: net.IPNet{ + IP: net.IPv6zero, + Mask: net.IPMask(net.IPv6zero), + }, + gateway: gateway, + }) + } + } + + hwaddr, err := net.ParseMAC(iface.MAC) + if err != nil { + return nil, err + } + + if nameservers == nil { + nameservers = []net.IP{} + } + + return &logicalInterface{ + hwaddr: hwaddr, + config: configMethodStatic{ + addresses: addresses, + nameservers: nameservers, + routes: routes, + }, + }, nil +} diff --git a/test b/test index 74812c4..8292c2a 100755 --- a/test +++ b/test @@ -20,6 +20,7 @@ declare -a TESTPKGS=(initialize datasource/file datasource/metadata datasource/metadata/cloudsigma + datasource/metadata/digitalocean datasource/metadata/ec2 datasource/proc_cmdline datasource/url From 2134fce7913b792f24157cb123449abf70320129 Mon Sep 17 00:00:00 2001 From: Alex Crawford Date: Wed, 27 Aug 2014 18:33:05 -0700 Subject: [PATCH 4/4] digitalocean: Add tests for network unit generation --- network/digitalocean_test.go | 367 +++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 network/digitalocean_test.go diff --git a/network/digitalocean_test.go b/network/digitalocean_test.go new file mode 100644 index 0000000..23a7c24 --- /dev/null +++ b/network/digitalocean_test.go @@ -0,0 +1,367 @@ +package network + +import ( + "errors" + "net" + "reflect" + "testing" + + "github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean" +) + +func TestParseNameservers(t *testing.T) { + for _, tt := range []struct { + dns digitalocean.DNS + nss []net.IP + err error + }{ + { + dns: digitalocean.DNS{}, + nss: []net.IP{}, + }, + { + dns: digitalocean.DNS{[]string{"1.2.3.4"}}, + nss: []net.IP{net.ParseIP("1.2.3.4")}, + }, + { + dns: digitalocean.DNS{[]string{"bad"}}, + err: errors.New("could not parse \"bad\" as nameserver IP address"), + }, + } { + nss, err := parseNameservers(tt.dns) + if !errorsEqual(tt.err, err) { + t.Fatalf("bad error (%+v): want %q, got %q", tt.dns, tt.err, err) + } + if !reflect.DeepEqual(tt.nss, nss) { + t.Fatalf("bad nameservers (%+v): want %#v, got %#v", tt.dns, tt.nss, nss) + } + } +} + +func TestParseInterface(t *testing.T) { + for _, tt := range []struct { + cfg digitalocean.Interface + nss []net.IP + useRoute bool + iface *logicalInterface + err error + }{ + { + cfg: digitalocean.Interface{ + MAC: "bad", + }, + err: errors.New("invalid MAC address: bad"), + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + }, + nss: []net.IP{}, + iface: &logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{}, + nameservers: []net.IP{}, + routes: []route{}, + }, + }, + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + }, + useRoute: true, + nss: []net.IP{net.ParseIP("1.2.3.4")}, + iface: &logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{}, + nameservers: []net.IP{net.ParseIP("1.2.3.4")}, + routes: []route{}, + }, + }, + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv4: &digitalocean.Address{ + IPAddress: "bad", + Netmask: "255.255.0.0", + }, + }, + nss: []net.IP{}, + err: errors.New("could not parse \"bad\" as IPv4 address"), + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv4: &digitalocean.Address{ + IPAddress: "1.2.3.4", + Netmask: "bad", + }, + }, + nss: []net.IP{}, + err: errors.New("could not parse \"bad\" as IPv4 mask"), + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv4: &digitalocean.Address{ + IPAddress: "1.2.3.4", + Netmask: "255.255.0.0", + Gateway: "ignoreme", + }, + }, + nss: []net.IP{}, + iface: &logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{net.IPNet{net.ParseIP("1.2.3.4"), net.IPMask(net.ParseIP("255.255.0.0"))}}, + nameservers: []net.IP{}, + routes: []route{}, + }, + }, + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv4: &digitalocean.Address{ + IPAddress: "1.2.3.4", + Netmask: "255.255.0.0", + Gateway: "bad", + }, + }, + useRoute: true, + nss: []net.IP{}, + err: errors.New("could not parse \"bad\" as IPv4 gateway"), + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv4: &digitalocean.Address{ + IPAddress: "1.2.3.4", + Netmask: "255.255.0.0", + Gateway: "5.6.7.8", + }, + }, + useRoute: true, + nss: []net.IP{}, + iface: &logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{net.IPNet{net.ParseIP("1.2.3.4"), net.IPMask(net.ParseIP("255.255.0.0"))}}, + nameservers: []net.IP{}, + routes: []route{route{net.IPNet{net.IPv4zero, net.IPMask(net.IPv4zero)}, net.ParseIP("5.6.7.8")}}, + }, + }, + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv6: &digitalocean.Address{ + IPAddress: "bad", + Cidr: 16, + }, + }, + nss: []net.IP{}, + err: errors.New("could not parse \"bad\" as IPv6 address"), + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv6: &digitalocean.Address{ + IPAddress: "fe00::", + Cidr: 16, + Gateway: "ignoreme", + }, + }, + nss: []net.IP{}, + iface: &logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{net.IPNet{net.ParseIP("fe00::"), net.IPMask(net.ParseIP("ffff::"))}}, + nameservers: []net.IP{}, + routes: []route{}, + }, + }, + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv6: &digitalocean.Address{ + IPAddress: "fe00::", + Cidr: 16, + Gateway: "bad", + }, + }, + useRoute: true, + nss: []net.IP{}, + err: errors.New("could not parse \"bad\" as IPv6 gateway"), + }, + { + cfg: digitalocean.Interface{ + MAC: "01:23:45:67:89:AB", + IPv6: &digitalocean.Address{ + IPAddress: "fe00::", + Cidr: 16, + Gateway: "fe00:1234::", + }, + }, + useRoute: true, + nss: []net.IP{}, + iface: &logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{net.IPNet{net.ParseIP("fe00::"), net.IPMask(net.ParseIP("ffff::"))}}, + nameservers: []net.IP{}, + routes: []route{route{net.IPNet{net.IPv6zero, net.IPMask(net.IPv6zero)}, net.ParseIP("fe00:1234::")}}, + }, + }, + }, + } { + iface, err := parseInterface(tt.cfg, tt.nss, tt.useRoute) + if !errorsEqual(tt.err, err) { + t.Fatalf("bad error (%+v): want %q, got %q", tt.cfg, tt.err, err) + } + if !reflect.DeepEqual(tt.iface, iface) { + t.Fatalf("bad interface (%+v): want %#v, got %#v", tt.cfg, tt.iface, iface) + } + } +} + +func TestParseInterfaces(t *testing.T) { + for _, tt := range []struct { + cfg digitalocean.Interfaces + nss []net.IP + ifaces []InterfaceGenerator + err error + }{ + { + ifaces: []InterfaceGenerator{}, + }, + { + cfg: digitalocean.Interfaces{ + Public: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}}, + }, + ifaces: []InterfaceGenerator{ + &physicalInterface{logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{}, + nameservers: []net.IP{}, + routes: []route{}, + }, + }}, + }, + }, + { + cfg: digitalocean.Interfaces{ + Private: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}}, + }, + ifaces: []InterfaceGenerator{ + &physicalInterface{logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{}, + nameservers: []net.IP{}, + routes: []route{}, + }, + }}, + }, + }, + { + cfg: digitalocean.Interfaces{ + Public: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}}, + }, + nss: []net.IP{net.ParseIP("1.2.3.4")}, + ifaces: []InterfaceGenerator{ + &physicalInterface{logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{}, + nameservers: []net.IP{net.ParseIP("1.2.3.4")}, + routes: []route{}, + }, + }}, + }, + }, + { + cfg: digitalocean.Interfaces{ + Private: []digitalocean.Interface{{MAC: "01:23:45:67:89:AB"}}, + }, + nss: []net.IP{net.ParseIP("1.2.3.4")}, + ifaces: []InterfaceGenerator{ + &physicalInterface{logicalInterface{ + hwaddr: net.HardwareAddr([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab}), + config: configMethodStatic{ + addresses: []net.IPNet{}, + nameservers: []net.IP{}, + routes: []route{}, + }, + }}, + }, + }, + { + cfg: digitalocean.Interfaces{ + Public: []digitalocean.Interface{{MAC: "bad"}}, + }, + err: errors.New("invalid MAC address: bad"), + }, + { + cfg: digitalocean.Interfaces{ + Private: []digitalocean.Interface{{MAC: "bad"}}, + }, + err: errors.New("invalid MAC address: bad"), + }, + } { + ifaces, err := parseInterfaces(tt.cfg, tt.nss) + if !errorsEqual(tt.err, err) { + t.Fatalf("bad error (%+v): want %q, got %q", tt.cfg, tt.err, err) + } + if !reflect.DeepEqual(tt.ifaces, ifaces) { + t.Fatalf("bad interfaces (%+v): want %#v, got %#v", tt.cfg, tt.ifaces, ifaces) + } + } +} + +func TestProcessDigitalOceanNetconf(t *testing.T) { + for _, tt := range []struct { + cfg string + ifaces []InterfaceGenerator + err error + }{ + { + cfg: ``, + }, + { + cfg: `{"dns":{"nameservers":["bad"]}}`, + err: errors.New("could not parse \"bad\" as nameserver IP address"), + }, + { + cfg: `{"interfaces":{"public":[{"ipv4":{"ip_address":"bad"}}]}}`, + err: errors.New("could not parse \"bad\" as IPv4 address"), + }, + { + cfg: `{}`, + ifaces: []InterfaceGenerator{}, + }, + } { + ifaces, err := ProcessDigitalOceanNetconf(tt.cfg) + if !errorsEqual(tt.err, err) { + t.Fatalf("bad error (%q): want %q, got %q", tt.cfg, tt.err, err) + } + if !reflect.DeepEqual(tt.ifaces, ifaces) { + t.Fatalf("bad interfaces (%q): want %#v, got %#v", tt.cfg, tt.ifaces, ifaces) + } + } +} + +func errorsEqual(a, b error) bool { + if a == nil && b == nil { + return true + } + if (a != nil && b == nil) || (a == nil && b != nil) { + return false + } + return (a.Error() == b.Error()) +}