diff --git a/ipam/mas.go b/ipam/mas.go index ae12ec5aee..45b6ec4fda 100644 --- a/ipam/mas.go +++ b/ipam/mas.go @@ -5,129 +5,195 @@ package ipam import ( "encoding/json" + "errors" + "io/ioutil" "net" - "net/http" - "time" + "runtime" + "strings" - "github.com/Azure/azure-container-networking/common" "github.com/Azure/azure-container-networking/log" ) const ( - // Host URL to query. - masQueryUrl = "http://169.254.169.254:6642/ListNetwork" - - // Minimum time interval between consecutive queries. - masQueryInterval = 10 * time.Second + defaultLinuxFilePath = "/etc/kubernetes/interfaces.json" + defaultWindowsFilePath = `c:\k\interfaces.json` + windows = "windows" + name = "MAS" ) // Microsoft Azure Stack IPAM configuration source. type masSource struct { - name string - sink addressConfigSink - queryUrl string - queryInterval time.Duration - lastRefresh time.Time + name string + sink addressConfigSink + fileLoaded bool + filePath string } // MAS host agent JSON object format. -type jsonObject struct { - Isolation string - IPs []struct { - IP string - IsolationId string - Mask string - DefaultGateways []string - DnsServers []string - } +type NetworkInterfaces struct { + Interfaces []Interface +} + +type Interface struct { + MacAddress string + IsPrimary bool + IPSubnets []IPSubnet +} + +type IPSubnet struct { + Prefix string + IPAddresses []IPAddress +} + +type IPAddress struct { + Address string + IsPrimary bool } // Creates the MAS source. func newMasSource(options map[string]interface{}) (*masSource, error) { - queryUrl, _ := options[common.OptIpamQueryUrl].(string) - if queryUrl == "" { - queryUrl = masQueryUrl - } - - i, _ := options[common.OptIpamQueryInterval].(int) - queryInterval := time.Duration(i) * time.Second - if queryInterval == 0 { - queryInterval = masQueryInterval + var filePath string + if runtime.GOOS == windows { + filePath = defaultWindowsFilePath + } else { + filePath = defaultLinuxFilePath } return &masSource{ - name: "MAS", - queryUrl: queryUrl, - queryInterval: queryInterval, + name: name, + filePath: filePath, }, nil } // Starts the MAS source. -func (s *masSource) start(sink addressConfigSink) error { - s.sink = sink +func (source *masSource) start(sink addressConfigSink) error { + source.sink = sink return nil } // Stops the MAS source. -func (s *masSource) stop() { - s.sink = nil - return +func (source *masSource) stop() { + source.sink = nil } // Refreshes configuration. -func (s *masSource) refresh() error { +func (source *masSource) refresh() error { + if source == nil { + return errors.New("masSource is nil") + } - // Refresh only if enough time has passed since the last query. - if time.Since(s.lastRefresh) < s.queryInterval { + if source.fileLoaded { return nil } - s.lastRefresh = time.Now() - // Configure the local default address space. - local, err := s.sink.newAddressSpace(LocalDefaultAddressSpaceId, LocalScope) + // Query the list of local interfaces. + localInterfaces, err := net.Interfaces() if err != nil { return err } - // Fetch configuration. - resp, err := http.Get(s.queryUrl) + // Query the list of Azure Network Interfaces + sdnInterfaces, err := getSDNInterfaces(source.filePath) if err != nil { return err } - defer resp.Body.Close() - - // Decode JSON object. - var obj jsonObject - decoder := json.NewDecoder(resp.Body) - err = decoder.Decode(&obj) + // Configure the local default address space. + local, err := source.sink.newAddressSpace(LocalDefaultAddressSpaceId, LocalScope) if err != nil { return err } - // Add the IP addresses to the local address space. - for _, v := range obj.IPs { - address := net.ParseIP(v.IP) - subnet := net.IPNet{ - IP: net.ParseIP(v.IP), - Mask: net.IPMask(net.ParseIP(v.Mask)), + if err = populateAddressSpace(local, sdnInterfaces, localInterfaces); err != nil { + return err + } + + // Set the local address space as active. + if err = source.sink.setAddressSpace(local); err != nil { + return err + } + + log.Printf("[ipam] Address space successfully populated from config file") + source.fileLoaded = true + + return nil +} + +func getSDNInterfaces(fileLocation string) (*NetworkInterfaces, error) { + data, err := ioutil.ReadFile(fileLocation) + if err != nil { + return nil, err + } + + interfaces := &NetworkInterfaces{} + if err = json.Unmarshal(data, interfaces); err != nil { + return nil, err + } + + return interfaces, nil +} + +func populateAddressSpace(localAddressSpace *addressSpace, sdnInterfaces *NetworkInterfaces, localInterfaces []net.Interface) error { + //Find the interface with matching MacAddress or Name + for _, sdnIf := range sdnInterfaces.Interfaces { + ifName := "" + + for _, localIf := range localInterfaces { + if macAddressesEqual(sdnIf.MacAddress, localIf.HardwareAddr.String()) { + ifName = localIf.Name + break + } } - ap, err := local.newAddressPool("eth0", 0, &subnet) - if err != nil { - log.Printf("[ipam] Failed to create pool:%v err:%v.", subnet, err) + // Skip if interface is not found. + if ifName == "" { + log.Printf("[ipam] Failed to find interface with MAC address:%v", sdnIf.MacAddress) continue } - _, err = ap.newAddressRecord(&address) - if err != nil { - log.Printf("[ipam] Failed to create address:%v err:%v.", address, err) - continue + // Prioritize secondary interfaces. + priority := 0 + if !sdnIf.IsPrimary { + priority = 1 } - } - // Set the local address space as active. - s.sink.setAddressSpace(local) + for _, subnet := range sdnIf.IPSubnets { + _, network, err := net.ParseCIDR(subnet.Prefix) + if err != nil { + log.Printf("[ipam] Failed to parse subnet:%v err:%v.", subnet.Prefix, err) + continue + } + + addressPool, err := localAddressSpace.newAddressPool(ifName, priority, network) + if err != nil { + log.Printf("[ipam] Failed to create pool:%v ifName:%v err:%v.", subnet, ifName, err) + continue + } + + // Add the IP addresses to the localAddressSpace address space. + for _, ipAddr := range subnet.IPAddresses { + // Primary addresses are reserved for the host. + if ipAddr.IsPrimary { + continue + } + + address := net.ParseIP(ipAddr.Address) + + _, err = addressPool.newAddressRecord(&address) + if err != nil { + log.Printf("[ipam] Failed to create address:%v err:%v.", address, err) + continue + } + } + } + } return nil } + +func macAddressesEqual(macAddress1 string, macAddress2 string) bool { + macAddress1 = strings.ToLower(strings.Replace(macAddress1, ":", "", -1)) + macAddress2 = strings.ToLower(strings.Replace(macAddress2, ":", "", -1)) + + return macAddress1 == macAddress2 +} diff --git a/ipam/mas_test.go b/ipam/mas_test.go new file mode 100644 index 0000000000..15cfe98051 --- /dev/null +++ b/ipam/mas_test.go @@ -0,0 +1,266 @@ +package ipam + +import ( + "net" + "reflect" + "runtime" + "testing" +) + +func TestNewMasSource(t *testing.T) { + options := make(map[string]interface{}) + mas, _ := newMasSource(options) + + if runtime.GOOS == windows { + if mas.filePath != defaultWindowsFilePath { + t.Fatalf("default file path set incorrectly") + } + } else { + if mas.filePath != defaultLinuxFilePath { + t.Fatalf("default file path set incorrectly") + } + } + if mas.name != "MAS" { + t.Fatalf("mas source Name incorrect") + } +} + +func TestGetSDNInterfaces(t *testing.T) { + const validFileName = "testfiles/masInterfaceConfig.json" + const invalidFileName = "mas_test.go" + const nonexistentFileName = "bad" + + interfaces, err := getSDNInterfaces(validFileName) + if err != nil { + t.Fatalf("failed to get sdn Interfaces from file: %v", err) + } + + correctInterfaces := &NetworkInterfaces{ + Interfaces: []Interface{ + { + MacAddress: "000D3A6E1825", + IsPrimary: true, + IPSubnets: []IPSubnet{ + { + Prefix: "1.0.0.0/12", + IPAddresses: []IPAddress{ + {Address: "1.0.0.4", IsPrimary: true}, + {Address: "1.0.0.5", IsPrimary: false}, + {Address: "1.0.0.6", IsPrimary: false}, + {Address: "1.0.0.7", IsPrimary: false}, + }, + }, + }, + }, + }, + } + + if !reflect.DeepEqual(interfaces, correctInterfaces) { + t.Fatalf("Interface list did not match expected list. expected: %v, actual: %v", interfaces, correctInterfaces) + } + + interfaces, err = getSDNInterfaces(invalidFileName) + if interfaces != nil || err == nil { + t.Fatal("didn't throw error on invalid file") + } + + interfaces, err = getSDNInterfaces(nonexistentFileName) + if interfaces != nil || err == nil { + t.Fatal("didn't throw error on nonexistent file") + } +} + +func TestPopulateAddressSpace(t *testing.T) { + hardwareAddress0, _ := net.ParseMAC("00:00:00:00:00:00") + hardwareAddress1, _ := net.ParseMAC("11:11:11:11:11:11") + hardwareAddress2, _ := net.ParseMAC("00:0d:3a:6e:18:25") + + localInterfaces := []net.Interface{ + {HardwareAddr: hardwareAddress0, Name: "eth0"}, + {HardwareAddr: hardwareAddress1, Name: "eth1"}, + {HardwareAddr: hardwareAddress2, Name: "eth2"}, + } + + local := &addressSpace{ + Id: LocalDefaultAddressSpaceId, + Scope: LocalScope, + Pools: make(map[string]*addressPool), + } + + sdnInterfaces := &NetworkInterfaces{ + Interfaces: []Interface{ + { + MacAddress: "000D3A6E1825", + IsPrimary: true, + IPSubnets: []IPSubnet{ + { + Prefix: "1.0.0.0/12", + IPAddresses: []IPAddress{ + {Address: "1.1.1.5", IsPrimary: true}, + {Address: "1.1.1.6", IsPrimary: false}, + {Address: "1.1.1.6", IsPrimary: false}, + {Address: "1.1.1.7", IsPrimary: false}, + {Address: "invalid", IsPrimary: false}, + }, + }, + }, + }, + }, + } + + err := populateAddressSpace(local, sdnInterfaces, localInterfaces) + if err != nil { + t.Fatalf("Error populating address space: %v", err) + } + + if len(local.Pools) != 1 { + t.Fatalf("Pool list has incorrect length. expected: %d, actual: %d", 1, len(local.Pools)) + } + + pool, ok := local.Pools["1.0.0.0/12"] + if !ok { + t.Fatal("Address pool 1.0.0.0/12 missing") + } + + if pool.IfName != "eth2" { + t.Fatalf("Incorrect interface name. expected: %s, actual %s", "eth2", pool.IfName) + } + + if pool.Priority != 0 { + t.Fatalf("Incorrect interface priority. expected: %d, actual %d", 0, pool.Priority) + } + + if len(pool.Addresses) != 2 { + t.Fatalf("Address list has incorrect length. expected: %d, actual: %d", 2, len(pool.Addresses)) + } + + _, ok = pool.Addresses["1.1.1.6"] + if !ok { + t.Fatal("Address 1.1.1.6 missing") + } + + _, ok = pool.Addresses["1.1.1.7"] + if !ok { + t.Fatal("Address 1.1.1.7 missing") + } +} + +func TestPopulateAddressSpaceMultipleSDNInterfaces(t *testing.T) { + hardwareAddress0, _ := net.ParseMAC("00:00:00:00:00:00") + hardwareAddress1, _ := net.ParseMAC("11:11:11:11:11:11") + localInterfaces := []net.Interface{ + {HardwareAddr: hardwareAddress0, Name: "eth0"}, + {HardwareAddr: hardwareAddress1, Name: "eth1"}, + } + + local := &addressSpace{ + Id: LocalDefaultAddressSpaceId, + Scope: LocalScope, + Pools: make(map[string]*addressPool), + } + + sdnInterfaces := &NetworkInterfaces{ + Interfaces: []Interface{ + { + MacAddress: "000000000000", + IsPrimary: true, + IPSubnets: []IPSubnet{ + { + Prefix: "0.0.0.0/24", + IPAddresses: []IPAddress{}, + }, + { + Prefix: "0.1.0.0/24", + IPAddresses: []IPAddress{}, + }, + { + Prefix: "0.0.0.0/24", + }, + { + Prefix: "invalid", + }, + }, + }, + { + MacAddress: "111111111111", + IsPrimary: false, + IPSubnets: []IPSubnet{ + { + Prefix: "1.0.0.0/24", + IPAddresses: []IPAddress{}, + }, + { + Prefix: "1.1.0.0/24", + IPAddresses: []IPAddress{}, + }, + }, + }, + { + MacAddress: "222222222222", + IsPrimary: false, + IPSubnets: []IPSubnet{}, + }, + }, + } + + err := populateAddressSpace(local, sdnInterfaces, localInterfaces) + if err != nil { + t.Fatalf("Error populating address space: %v", err) + } + + if len(local.Pools) != 4 { + t.Fatalf("Pool list has incorrect length. expected: %d, actual: %d", 4, len(local.Pools)) + } + + pool, ok := local.Pools["0.0.0.0/24"] + if !ok { + t.Fatal("Address pool 0.0.0.0/24 missing") + } + + if pool.IfName != "eth0" { + t.Fatalf("Incorrect interface name. expected: %s, actual %s", "eth0", pool.IfName) + } + + if pool.Priority != 0 { + t.Fatalf("Incorrect interface priority. expected: %d, actual %d", 0, pool.Priority) + } + + pool, ok = local.Pools["0.1.0.0/24"] + if !ok { + t.Fatal("Address pool 0.1.0.0/24 missing") + } + + if pool.IfName != "eth0" { + t.Fatalf("Incorrect interface name. expected: %s, actual %s", "eth0", pool.IfName) + } + + if pool.Priority != 0 { + t.Fatalf("Incorrect interface priority. expected: %d, actual %d", 0, pool.Priority) + } + + pool, ok = local.Pools["1.0.0.0/24"] + if !ok { + t.Fatal("Address pool 1.0.0.0/24 missing") + } + + if pool.IfName != "eth1" { + t.Fatalf("Incorrect interface name. expected: %s, actual %s", "eth1", pool.IfName) + } + + if pool.Priority != 1 { + t.Fatalf("Incorrect interface priority. expected: %d, actual %d", 1, pool.Priority) + } + + pool, ok = local.Pools["1.1.0.0/24"] + if !ok { + t.Fatal("Address pool 1.1.0.0/24 missing") + } + + if pool.IfName != "eth1" { + t.Fatalf("Incorrect interface name. expected: %s, actual %s", "eth1", pool.IfName) + } + + if pool.Priority != 1 { + t.Fatalf("Incorrect interface priority. expected: %d, actual %d", 1, pool.Priority) + } +} \ No newline at end of file diff --git a/ipam/testfiles/masInterfaceConfig.json b/ipam/testfiles/masInterfaceConfig.json new file mode 100644 index 0000000000..04b697658e --- /dev/null +++ b/ipam/testfiles/masInterfaceConfig.json @@ -0,0 +1,31 @@ +{ + "Interfaces": [ + { + "MacAddress": "000D3A6E1825", + "IsPrimary": true, + "IPSubnets": [ + { + "Prefix": "1.0.0.0/12", + "IPAddresses": [ + { + "Address": "1.0.0.4", + "IsPrimary": true + }, + { + "Address": "1.0.0.5", + "IsPrimary": false + }, + { + "Address": "1.0.0.6", + "IsPrimary": false + }, + { + "Address": "1.0.0.7", + "IsPrimary": false + } + ] + } + ] + } + ] +}