Merge pull request #213 from crawford/digitalocean
digitalocean: Add support for DigitalOcean
This commit is contained in:
		| @@ -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=<url>', using the cloud-config served by an HTTP GET to <url>", 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) | ||||
| 	} | ||||
|  | ||||
| @@ -209,6 +208,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 | ||||
| } | ||||
|  | ||||
| @@ -234,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()) | ||||
| 	} | ||||
|   | ||||
| @@ -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"}`, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} { | ||||
|   | ||||
							
								
								
									
										107
									
								
								datasource/metadata/digitalocean/metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								datasource/metadata/digitalocean/metadata.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
| } | ||||
							
								
								
									
										99
									
								
								datasource/metadata/digitalocean/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								datasource/metadata/digitalocean/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 "" | ||||
| } | ||||
| @@ -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()) | ||||
| 		} | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										142
									
								
								network/digitalocean.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								network/digitalocean.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										367
									
								
								network/digitalocean_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								network/digitalocean_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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()) | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user