diff --git a/plugins/linux/ifplugin/descriptor/interface.go b/plugins/linux/ifplugin/descriptor/interface.go index ab4d271014..cf1ffcba83 100644 --- a/plugins/linux/ifplugin/descriptor/interface.go +++ b/plugins/linux/ifplugin/descriptor/interface.go @@ -15,6 +15,7 @@ package descriptor import ( + "fmt" "io/ioutil" "net" "path/filepath" @@ -107,6 +108,10 @@ var ( // EXISTING interface. ErrExistingWithNamespace = errors.New("EXISTING interface defined with namespace") + // ErrExistingIpWithNetalloc is returned when netalloc and EXISTING-IP features are combined, + // which is currently not supported. + ErrExistingIpWithNetalloc = errors.New("it is not supported to reference EXISTING-IP via netalloc") + // ErrInvalidIPWithMask is returned when address is invalid or mask is missing ErrInvalidIPWithMask = errors.New("IP with mask is not valid") @@ -281,6 +286,16 @@ func (d *InterfaceDescriptor) Validate(key string, linuxIf *interfaces.Interface if linuxIf.GetNamespace() != nil { return kvs.NewInvalidValueError(ErrExistingWithNamespace, "namespace") } + // Currently it is not supported to combine netalloc with existing IP. + if linuxIf.GetLinkOnly() { + for i, ipAddr := range linuxIf.GetIpAddresses() { + _, hasAllocDep := d.addrAlloc.GetAddressAllocDep(ipAddr, linuxIf.Name, "") + if hasAllocDep { + return kvs.NewInvalidValueError(ErrExistingIpWithNetalloc, + "type", "link_only", fmt.Sprintf("ip_addresses[%d]", i)) + } + } + } case interfaces.Interface_LOOPBACK: if linuxIf.GetLink() != nil { return kvs.NewInvalidValueError(ErrInterfaceReferenceMismatch, "link") @@ -357,6 +372,8 @@ func (d *InterfaceDescriptor) Create(key string, linuxIf *interfaces.Interface) metadata, err = getMetadata(linuxIf) case interfaces.Interface_VRF_DEVICE: metadata, err = d.createVRF(nsCtx, linuxIf) + case interfaces.Interface_DUMMY: + metadata, err = d.createDummyIf(nsCtx, linuxIf) default: return nil, ErrUnsupportedLinuxInterfaceType } @@ -455,6 +472,8 @@ func (d *InterfaceDescriptor) Delete(key string, linuxIf *interfaces.Interface, return nil case interfaces.Interface_VRF_DEVICE: return d.deleteVRF(linuxIf) + case interfaces.Interface_DUMMY: + return d.deleteDummyIf(linuxIf) } err = ErrUnsupportedLinuxInterfaceType @@ -635,12 +654,20 @@ func (d *InterfaceDescriptor) DerivedValues(key string, linuxIf *interfaces.Inte Value: &prototypes.Empty{}, }) } - if !linuxIf.GetLinkOnly() { + if !linuxIf.GetLinkOnly() || linuxIf.GetType() == interfaces.Interface_EXISTING { + var ipSource netalloc_api.IPAddressSource + var hostName string + if linuxIf.GetLinkOnly() { // interface type = EXISTING + ipSource = netalloc_api.IPAddressSource_EXISTING + hostName = getHostIfName(linuxIf) + } else { + ipSource = netalloc_api.IPAddressSource_STATIC + } // IP addresses for _, ipAddr := range linuxIf.IpAddresses { derValues = append(derValues, kvs.KeyValuePair{ Key: interfaces.InterfaceAddressKey( - linuxIf.Name, ipAddr, linuxIf.VrfMasterInterface, netalloc_api.IPAddressSource_STATIC), + linuxIf.Name, ipAddr, linuxIf.VrfMasterInterface, hostName, ipSource), Value: &prototypes.Empty{}, }) } diff --git a/plugins/linux/ifplugin/descriptor/interface_address.go b/plugins/linux/ifplugin/descriptor/interface_address.go index f929da6cf6..45e8d4eb14 100644 --- a/plugins/linux/ifplugin/descriptor/interface_address.go +++ b/plugins/linux/ifplugin/descriptor/interface_address.go @@ -41,7 +41,8 @@ const ( DisableIPv6SysctlTemplate = "net.ipv6.conf.%s.disable_ipv6" // dependency labels - interfaceVrfDep = "interface-assigned-to-vrf" + interfaceVrfDep = "interface-assigned-to-vrf" + interfaceAddrDep = "address-assigned-to-interface" ) // InterfaceAddressDescriptor (un)assigns IP address to/from Linux interface. @@ -81,17 +82,19 @@ func (d *InterfaceAddressDescriptor) SetInterfaceIndex(intfIndex ifaceidx.LinuxI } // IsInterfaceAddressKey returns true if the key represents assignment of an IP address -// to a Linux interface (that needs to be applied). KVs representing addresses -// already allocated from netalloc plugin are excluded. +// to a Linux interface (that needs to be applied or is expected to exist). +// KVs representing addresses already allocated from netalloc plugin are excluded. func (d *InterfaceAddressDescriptor) IsInterfaceAddressKey(key string) bool { - _, _, _, source, _, isAddrKey := interfaces.ParseInterfaceAddressKey(key) + _, _, _, _, source, _, isAddrKey := interfaces.ParseInterfaceAddressKey(key) return isAddrKey && - (source == netalloc_api.IPAddressSource_STATIC || source == netalloc_api.IPAddressSource_ALLOC_REF) + (source == netalloc_api.IPAddressSource_STATIC || + source == netalloc_api.IPAddressSource_ALLOC_REF || + source == netalloc_api.IPAddressSource_EXISTING) } // Validate validates IP address to be assigned to an interface. func (d *InterfaceAddressDescriptor) Validate(key string, emptyVal proto.Message) (err error) { - iface, addr, _, _, invalidKey, _ := interfaces.ParseInterfaceAddressKey(key) + iface, addr, _, _, _, invalidKey, _ := interfaces.ParseInterfaceAddressKey(key) if invalidKey { return errors.New("invalid key") } @@ -101,7 +104,11 @@ func (d *InterfaceAddressDescriptor) Validate(key string, emptyVal proto.Message // Create assigns IP address to an interface. func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) (metadata kvs.Metadata, err error) { - iface, addr, _, _, _, _ := interfaces.ParseInterfaceAddressKey(key) + iface, addr, _, _, source, _, _ := interfaces.ParseInterfaceAddressKey(key) + if source == netalloc_api.IPAddressSource_EXISTING { + // already exists, nothing to do + return nil, nil + } ifMeta, found := d.intfIndex.LookupByName(iface) if !found { @@ -160,7 +167,11 @@ func (d *InterfaceAddressDescriptor) Create(key string, emptyVal proto.Message) // Delete unassigns IP address from an interface. func (d *InterfaceAddressDescriptor) Delete(key string, emptyVal proto.Message, metadata kvs.Metadata) (err error) { - iface, addr, _, _, _, _ := interfaces.ParseInterfaceAddressKey(key) + iface, addr, _, _, source, _, _ := interfaces.ParseInterfaceAddressKey(key) + if source == netalloc_api.IPAddressSource_EXISTING { + // already existed before Create, nothing to do + return nil + } ifMeta, found := d.intfIndex.LookupByName(iface) if !found { @@ -197,13 +208,19 @@ func (d *InterfaceAddressDescriptor) Delete(key string, emptyVal proto.Message, // Dependencies mentions (non-default) VRF and a potential allocation of the IP address as dependencies. func (d *InterfaceAddressDescriptor) Dependencies(key string, emptyVal proto.Message) (deps []kvs.Dependency) { - iface, addr, vrf, _, _, _ := interfaces.ParseInterfaceAddressKey(key) + iface, addr, vrf, hostName, source, _, _ := interfaces.ParseInterfaceAddressKey(key) if vrf != "" { deps = append(deps, kvs.Dependency{ Label: interfaceVrfDep, Key: interfaces.InterfaceVrfKey(iface, vrf), }) } + if source == netalloc_api.IPAddressSource_EXISTING { + deps = append(deps, kvs.Dependency{ + Label: interfaceAddrDep, + Key: interfaces.InterfaceHostNameWithAddrKey(hostName, addr), + }) + } allocDep, hasAllocDep := d.addrAlloc.GetAddressAllocDep(addr, iface, "") if hasAllocDep { deps = append(deps, allocDep) diff --git a/plugins/linux/ifplugin/descriptor/interface_dummy.go b/plugins/linux/ifplugin/descriptor/interface_dummy.go new file mode 100644 index 0000000000..c62311c86b --- /dev/null +++ b/plugins/linux/ifplugin/descriptor/interface_dummy.go @@ -0,0 +1,77 @@ +// Copyright (c) 2020 Pantheon.tech +// +// 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 descriptor + +import ( + "github.com/pkg/errors" + "go.ligato.io/vpp-agent/v3/plugins/linux/ifplugin/linuxcalls" + + "go.ligato.io/vpp-agent/v3/plugins/linux/ifplugin/ifaceidx" + nslinuxcalls "go.ligato.io/vpp-agent/v3/plugins/linux/nsplugin/linuxcalls" + interfaces "go.ligato.io/vpp-agent/v3/proto/ligato/linux/interfaces" +) + +// createDummyIf creates dummy interface. +func (d *InterfaceDescriptor) createDummyIf( + nsCtx nslinuxcalls.NamespaceMgmtCtx, linuxIf *interfaces.Interface, +) (md *ifaceidx.LinuxIfMetadata, err error) { + hostName := getHostIfName(linuxIf) + agentPrefix := d.serviceLabel.GetAgentPrefix() + + // move to the namespace with the interface + revert, err := d.nsPlugin.SwitchToNamespace(nsCtx, linuxIf.Namespace) + if err != nil { + d.log.Error("switch to namespace failed:", err) + return nil, err + } + defer revert() + + // create a new Dummy interface + err = d.ifHandler.AddDummyInterface(hostName) + if err != nil { + return nil, errors.WithMessagef(err, + "failed to create dummy interface %s", hostName) + } + + // add alias + err = d.ifHandler.SetInterfaceAlias(hostName, agentPrefix+linuxcalls.GetDummyIfAlias(linuxIf)) + if err != nil { + return nil, errors.WithMessagef(err, + "error setting alias for Dummy interface %s", hostName) + } + + // build metadata + link, err := d.ifHandler.GetLinkByName(hostName) + if err != nil { + return nil, errors.WithMessagef(err, "error getting link %s", hostName) + } + + return &ifaceidx.LinuxIfMetadata{ + Namespace: linuxIf.Namespace, + LinuxIfIndex: link.Attrs().Index, + HostIfName: hostName, + }, nil +} + +// deleteDummyIf removes dummy interface. +func (d *InterfaceDescriptor) deleteDummyIf(linuxIf *interfaces.Interface) error { + hostName := getHostIfName(linuxIf) + err := d.ifHandler.DeleteInterface(hostName) + if err != nil { + d.log.Error(err) + return err + } + return nil +} \ No newline at end of file diff --git a/plugins/linux/ifplugin/descriptor/watcher.go b/plugins/linux/ifplugin/descriptor/watcher.go index f9c253dc8f..39e16f3991 100644 --- a/plugins/linux/ifplugin/descriptor/watcher.go +++ b/plugins/linux/ifplugin/descriptor/watcher.go @@ -52,7 +52,7 @@ type InterfaceWatcher struct { // a set of interfaces present in the default namespace ifacesMu sync.Mutex - ifaces map[string]struct{} + ifaces map[string]hostInterface // conditional variable to check if the list of interfaces is in-sync with // Linux network stack @@ -60,9 +60,16 @@ type InterfaceWatcher struct { intfsInSyncCond *sync.Cond // Linux notifications - notifCh chan netlink.LinkUpdate - doneCh chan struct{} - notify func(notification *ifmodel.InterfaceNotification) + linkNotifCh chan netlink.LinkUpdate + addrNotifCh chan netlink.AddrUpdate + doneCh chan struct{} + notify func(notification *ifmodel.InterfaceNotification) +} + +type hostInterface struct { + name string + enabled bool + ipAddrs []string } // NewInterfaceWatcher creates a new instance of the Interface Watcher. @@ -72,8 +79,9 @@ func NewInterfaceWatcher(kvscheduler kvs.KVScheduler, ifHandler linuxcalls.Netli kvscheduler: kvscheduler, ifHandler: ifHandler, notify: notifyInterface, - ifaces: make(map[string]struct{}), - notifCh: make(chan netlink.LinkUpdate), + ifaces: make(map[string]hostInterface), + linkNotifCh: make(chan netlink.LinkUpdate), + addrNotifCh: make(chan netlink.AddrUpdate), doneCh: make(chan struct{}), } descriptor.intfsInSyncCond = sync.NewCond(&descriptor.ifacesMu) @@ -107,12 +115,22 @@ func (w *InterfaceWatcher) Retrieve(correlate []kvs.KVWithMetadata) (values []kv } defer w.ifacesMu.Unlock() - for ifName := range w.ifaces { + for _, hostIface := range w.ifaces { + if !hostIface.enabled { + continue + } values = append(values, kvs.KVWithMetadata{ - Key: ifmodel.InterfaceHostNameKey(ifName), + Key: ifmodel.InterfaceHostNameKey(hostIface.name), Value: &prototypes.Empty{}, Origin: kvs.FromSB, }) + for _, ipAddr := range hostIface.ipAddrs { + values = append(values, kvs.KVWithMetadata{ + Key: ifmodel.InterfaceHostNameWithAddrKey(hostIface.name, ipAddr), + Value: &prototypes.Empty{}, + Origin: kvs.FromSB, + }) + } } return values, nil @@ -121,9 +139,15 @@ func (w *InterfaceWatcher) Retrieve(correlate []kvs.KVWithMetadata) (values []kv // StartWatching starts interface watching. func (w *InterfaceWatcher) StartWatching() error { // watch default namespace to be aware of interfaces not created by this plugin - err := w.ifHandler.LinkSubscribe(w.notifCh, w.doneCh) + err := w.ifHandler.LinkSubscribe(w.linkNotifCh, w.doneCh) + if err != nil { + err = errors.Errorf("failed to subscribe for link notifications: %v", err) + w.log.Error(err) + return err + } + err = w.ifHandler.AddrSubscribe(w.addrNotifCh, w.doneCh) if err != nil { - err = errors.Errorf("failed to subscribe link: %v", err) + err = errors.Errorf("failed to subscribe for address notifications: %v", err) w.log.Error(err) return err } @@ -138,7 +162,7 @@ func (w *InterfaceWatcher) StopWatching() { w.wg.Wait() } -// watchDefaultNamespace watches for notification about added/removed interfaces +// watchDefaultNamespace watches for notification about added/removed interfaces/addresses // to/from the default namespace. func (w *InterfaceWatcher) watchDefaultNamespace() { defer w.wg.Done() @@ -147,9 +171,24 @@ func (w *InterfaceWatcher) watchDefaultNamespace() { links, err := w.ifHandler.GetLinkList() if err == nil { for _, link := range links { - if enabled, err := w.ifHandler.IsInterfaceUp(link.Attrs().Name); enabled && err == nil { - w.ifaces[link.Attrs().Name] = struct{}{} + iface := hostInterface{name: link.Attrs().Name} + enabled, err := w.ifHandler.IsInterfaceUp(iface.name) + if err != nil { + w.log.Warnf("IsInterfaceUp failed for interface %s: %v", + iface.name, err) + continue + } + iface.enabled = enabled + addrs, err := w.ifHandler.GetAddressList(iface.name) + if err != nil { + w.log.Warnf("GetAddressList failed for interface %s: %v", + iface.name, err) + continue + } + for _, addr := range addrs { + iface.ipAddrs = append(iface.ipAddrs, addr.IPNet.String()) } + w.ifaces[iface.name] = iface } } else { w.log.Warnf("failed to list interfaces in the default namespace: %v", err) @@ -163,9 +202,10 @@ func (w *InterfaceWatcher) watchDefaultNamespace() { for { select { - case linkNotif := <-w.notifCh: + case linkNotif := <-w.linkNotifCh: w.processLinkNotification(linkNotif) - + case addrNotif := <-w.addrNotifCh: + w.processAddrNotification(addrNotif) case <-w.ctx.Done(): close(w.doneCh) return @@ -178,18 +218,69 @@ func (w *InterfaceWatcher) processLinkNotification(linkUpdate netlink.LinkUpdate w.ifacesMu.Lock() defer w.ifacesMu.Unlock() + // send notification to any interface state watcher (e.g. Configurator) + w.sendStateNotification(linkUpdate) + + // push update to the KV Scheduler ifName := linkUpdate.Attrs().Name - isUp := isLinkUp(linkUpdate) + prevState := w.ifaces[ifName] + newState := prevState + newState.name = ifName + newState.enabled = isLinkUp(linkUpdate) + if prevState.enabled != newState.enabled { + w.updateLinkKV(ifName, newState.enabled) + // do not advertise IPs if interface is disabled + for _, ipAddr := range newState.ipAddrs { + w.updateAddrKV(ifName, ipAddr, !newState.enabled) + } + } + w.ifaces[ifName] = newState +} - w.sendNotification(linkUpdate) +// processAddrNotification processes address notification received from Linux. +func (w *InterfaceWatcher) processAddrNotification(addrUpdate netlink.AddrUpdate) { + w.ifacesMu.Lock() + defer w.ifacesMu.Unlock() - if !w.needsUpdate(ifName, isUp) { - // ignore notification if the interface admin status remained the same + link, err := w.ifHandler.GetLinkByIndex(addrUpdate.LinkIndex) + if err != nil { + w.log.Warn(err) return } - w.notifyScheduler(ifName, isUp) + // push update to the KV Scheduler + ifName := link.Attrs().Name + addr := addrUpdate.LinkAddress.String() + removed := !addrUpdate.NewAddr + prevState := w.ifaces[ifName] + addrIdx := -1 + for i, ipAddr := range prevState.ipAddrs { + if ipAddr == addr { + addrIdx = i + break + } + } + knownAddr := addrIdx != -1 + if knownAddr != removed { + // removed unknown IP or added already known IP + return + } + if prevState.enabled { + w.updateAddrKV(ifName, addr, removed) + } + // update the internal cache + newState := prevState + newState.name = ifName + if removed { + lastIdx := len(newState.ipAddrs)-1 + newState.ipAddrs[addrIdx] = newState.ipAddrs[lastIdx] + newState.ipAddrs[lastIdx] = "" + newState.ipAddrs = newState.ipAddrs[:lastIdx] + } else { + newState.ipAddrs = append(newState.ipAddrs, addr) + } + w.ifaces[ifName] = newState } func linkToInterfaceType(link netlink.Link) ifmodel.Interface_Type { @@ -200,6 +291,8 @@ func linkToInterfaceType(link netlink.Link) ifmodel.Interface_Type { return ifmodel.Interface_TAP_TO_VPP case "vrf": return ifmodel.Interface_VRF_DEVICE + case "dummy": + return ifmodel.Interface_DUMMY default: if link.Attrs().Name == linuxcalls.DefaultLoopbackName { return ifmodel.Interface_LOOPBACK @@ -208,17 +301,13 @@ func linkToInterfaceType(link netlink.Link) ifmodel.Interface_Type { } } -// notifyScheduler notifies scheduler about interface change. -func (w *InterfaceWatcher) notifyScheduler(ifName string, enabled bool) { +// updateLinkKV updates key-value pair representing the interface latest link status. +func (w *InterfaceWatcher) updateLinkKV(ifName string, enabled bool) { var value proto.Message - if enabled { - w.ifaces[ifName] = struct{}{} + // empty == enabled, nil == disabled value = &prototypes.Empty{} - } else { - delete(w.ifaces, ifName) } - if err := w.kvscheduler.PushSBNotification(kvs.KVWithMetadata{ Key: ifmodel.InterfaceHostNameKey(ifName), Value: value, @@ -228,12 +317,23 @@ func (w *InterfaceWatcher) notifyScheduler(ifName string, enabled bool) { } } -func (w *InterfaceWatcher) needsUpdate(ifName string, isEnabled bool) bool { - _, wasEnabled := w.ifaces[ifName] - return isEnabled != wasEnabled +// updateAddrKV updates key-value pair representing IP address assigned to a host interface. +func (w *InterfaceWatcher) updateAddrKV(ifName string, address string, removed bool) { + var value proto.Message + if !removed { + // empty == assigned, nil == not assigned + value = &prototypes.Empty{} + } + if err := w.kvscheduler.PushSBNotification(kvs.KVWithMetadata{ + Key: ifmodel.InterfaceHostNameWithAddrKey(ifName, address), + Value: value, + Metadata: nil, + }); err != nil { + w.log.Warnf("pushing SB notification failed: %v", err) + } } -func (w *InterfaceWatcher) sendNotification(linkUpdate netlink.LinkUpdate) { +func (w *InterfaceWatcher) sendStateNotification(linkUpdate netlink.LinkUpdate) { if w.notify != nil { attrs := linkUpdate.Attrs() adminStatus := ifmodel.InterfaceState_DOWN diff --git a/plugins/linux/ifplugin/linuxcalls/dump_interface_linuxcalls.go b/plugins/linux/ifplugin/linuxcalls/dump_interface_linuxcalls.go index a04054d540..d3b0bb2f40 100644 --- a/plugins/linux/ifplugin/linuxcalls/dump_interface_linuxcalls.go +++ b/plugins/linux/ifplugin/linuxcalls/dump_interface_linuxcalls.go @@ -198,6 +198,17 @@ func ParseTapAlias(alias string) (linuxTapName, vppTapName, origHostIfName strin return } +// GetDummyIfAlias returns alias for Linux Dummy interface managed by the agent. +func GetDummyIfAlias(linuxIf *interfaces.Interface) string { + return linuxIf.Name +} + +// ParseDummyIfAlias parses out logical name of a Dummy interface from the alias. +// Currently there are no other logical information stored in the alias so it is very straightforward. +func ParseDummyIfAlias(alias string) (ifName string) { + return alias +} + // GetVRFAlias returns alias for Linux VRF devices managed by the agent. func GetVRFAlias(linuxIf *interfaces.Interface) string { return linuxIf.Name @@ -262,6 +273,9 @@ func (h *NetLinkHandler) retrieveInterfaces(nsList []*namespaces.NetNamespace, g PeerIfName: vethPeerIfName, }, } + } else if link.Type() == "dummy" { + iface.Type = interfaces.Interface_DUMMY + iface.Name = ParseDummyIfAlias(alias) } else if link.Type() == "tuntap" || link.Type() == "tun" /* not defined in vishvananda */ { iface.Type = interfaces.Interface_TAP_TO_VPP var vppTapIfName string diff --git a/plugins/linux/ifplugin/linuxcalls/link_linuxcalls.go b/plugins/linux/ifplugin/linuxcalls/link_linuxcalls.go index e8786ae194..2bbaef55b0 100644 --- a/plugins/linux/ifplugin/linuxcalls/link_linuxcalls.go +++ b/plugins/linux/ifplugin/linuxcalls/link_linuxcalls.go @@ -33,6 +33,15 @@ func (h *NetLinkHandler) GetLinkByName(ifName string) (netlink.Link, error) { return link, nil } +// GetLinkByIndex calls netlink API to get Link type from interface index +func (h *NetLinkHandler) GetLinkByIndex(ifIdx int) (netlink.Link, error) { + link, err := netlink.LinkByIndex(ifIdx) + if err != nil { + return nil, errors.Wrapf(err, "LinkByIndex %q", ifIdx) + } + return link, nil +} + // GetLinkList calls netlink API to get all Links in namespace func (h *NetLinkHandler) GetLinkList() ([]netlink.Link, error) { return netlink.LinkList() @@ -52,6 +61,12 @@ func (h *NetLinkHandler) LinkSubscribe(ch chan<- netlink.LinkUpdate, done <-chan return netlink.LinkSubscribe(ch, done) } +// AddrSubscribe takes a channel to which notifications will be sent +// when addresses change. Close the 'done' chan to stop subscription. +func (h *NetLinkHandler) AddrSubscribe(ch chan<- netlink.AddrUpdate, done <-chan struct{}) error { + return netlink.AddrSubscribe(ch, done) +} + // GetInterfaceType returns the type (string representation) of a given interface. func (h *NetLinkHandler) GetInterfaceType(ifName string) (string, error) { link, err := h.GetLinkByName(ifName) @@ -233,6 +248,19 @@ func (h *NetLinkHandler) AddVethInterfacePair(ifName, peerIfName string) error { return nil } +// AddDummyInterface configures dummy interface (effectively additional loopback). +func (h *NetLinkHandler) AddDummyInterface(ifName string) error { + attrs := netlink.NewLinkAttrs() + attrs.Name = ifName + link := &netlink.Dummy{ + LinkAttrs: attrs, + } + if err := netlink.LinkAdd(link); err != nil { + return errors.Wrapf(err, "LinkAdd (dummy-ifName=%s)", ifName) + } + return nil +} + // AddVRFDevice configures new VRF network device. func (h *NetLinkHandler) AddVRFDevice(vrfDevName string, routingTable uint32) error { attrs := netlink.NewLinkAttrs() diff --git a/plugins/linux/ifplugin/linuxcalls/netlink_api.go b/plugins/linux/ifplugin/linuxcalls/netlink_api.go index a430ecc3d1..ea37c0adfc 100644 --- a/plugins/linux/ifplugin/linuxcalls/netlink_api.go +++ b/plugins/linux/ifplugin/linuxcalls/netlink_api.go @@ -72,6 +72,8 @@ type NetlinkAPI interface { // AddVethInterfacePair configures two connected VETH interfaces AddVethInterfacePair(ifName, peerIfName string) error + // AddDummyInterface configures dummy interface (effectively additional loopback). + AddDummyInterface(ifName string) error // AddVRFDevice configures new VRF network device. AddVRFDevice(vrfDevName string, routingTable uint32) error // PutInterfaceIntoVRF assigns Linux interface into a given VRF. @@ -107,13 +109,18 @@ type NetlinkAPI interface { // NetlinkAPIRead interface covers read methods inside linux calls package // needed to manage linux interfaces. type NetlinkAPIRead interface { - // GetLinkByName returns netlink interface type + // GetLinkByName calls netlink API to get Link type from interface name GetLinkByName(ifName string) (netlink.Link, error) + // GetLinkByIndex calls netlink API to get Link type from interface index + GetLinkByIndex(ifIdx int) (netlink.Link, error) // GetLinkList return all links from namespace GetLinkList() ([]netlink.Link, error) // LinkSubscribe takes a channel to which notifications will be sent // when links change. Close the 'done' chan to stop subscription. LinkSubscribe(ch chan<- netlink.LinkUpdate, done <-chan struct{}) error + // AddrSubscribe takes a channel to which notifications will be sent + // when addresses change. Close the 'done' chan to stop subscription. + AddrSubscribe(ch chan<- netlink.AddrUpdate, done <-chan struct{}) error // GetAddressList reads all IP addresses GetAddressList(ifName string) ([]netlink.Addr, error) // InterfaceExists verifies interface existence diff --git a/plugins/linux/l3plugin/descriptor/route.go b/plugins/linux/l3plugin/descriptor/route.go index a73c8eadce..f517f4bd80 100644 --- a/plugins/linux/l3plugin/descriptor/route.go +++ b/plugins/linux/l3plugin/descriptor/route.go @@ -290,11 +290,11 @@ func (d *RouteDescriptor) Dependencies(key string, route *linux_l3.Route) []kvs. dependencies = append(dependencies, kvs.Dependency{ Label: allocatedAddrAttached, AnyOf: kvs.AnyOfDependency{ - // match IP address assignment regardless of VRF (hence key prefix omitting suffix with vrf from the key) + // match IP address assignment regardless of VRF (hence key prefix omitting vrf from the key) KeyPrefixes: []string{ ifmodel.InterfaceAddressKey( route.OutgoingInterface, d.addrAlloc.CreateAddressAllocRef(network, "", false), - "", netalloc_api.IPAddressSource_ALLOC_REF), + "", "", netalloc_api.IPAddressSource_ALLOC_REF), }, }, }) @@ -316,7 +316,7 @@ func (d *RouteDescriptor) Dependencies(key string, route *linux_l3.Route) []kvs. } return false } - ifName, address, _, source, _, isAddrKey := ifmodel.ParseInterfaceAddressKey(key) + ifName, address, _, _, source, _, isAddrKey := ifmodel.ParseInterfaceAddressKey(key) if isAddrKey && source != netalloc_api.IPAddressSource_ALLOC_REF { if _, network, err := net.ParseCIDR(address); err == nil && network.Contains(gwAddr) { // GW address is inside the local network of the outgoing interface diff --git a/proto/ligato/linux/interfaces/interface.pb.go b/proto/ligato/linux/interfaces/interface.pb.go index 8f351485a0..ed9357fb94 100644 --- a/proto/ligato/linux/interfaces/interface.pb.go +++ b/proto/ligato/linux/interfaces/interface.pb.go @@ -32,12 +32,36 @@ const ( Interface_UNDEFINED Interface_Type = 0 Interface_VETH Interface_Type = 1 Interface_TAP_TO_VPP Interface_Type = 2 // TAP created by VPP to have the Linux-side further configured - Interface_LOOPBACK Interface_Type = 3 - Interface_EXISTING Interface_Type = 4 + // LOOPBACK is used to attach configuration to an existing "lo" interface, + // but unlike EXISTING type it is not limited to the default network namespace + // (i.e. loopbacks in other containers can be referenced also). + // To create an additional interface which effectively acts as a loopback, + // use DUMMY interface (see below). + Interface_LOOPBACK Interface_Type = 3 + // Wait for and potentially attach additional network configuration to an interface + // created externally (i.e. not by this agent) in the default network namespace + // (i.e. same as used by the agent). + // Behaviour of the EXISTING interface depends on the values of ip_addresses and + // link_only attributes as follows: + // 1. link_only=false and ip_addresses are empty: agent waits for interface to be created + // externally and then configures it in the L2-only mode (resync will remove any IP addresses + // configured from outside of the agent) + // 2. link_only=false and ip_addresses are non-empty: agent waits for interface to be created + // externally and then attaches the selected IP addresses to it (resync removes any other + // IPs added externally) + // 3. link_only=true and ip_addresses are empty: agent only waits for the interface + // to exists (it doesn't wait for or change any IP addresses attached to it) + // 4. link_only=true and ip_addresses are non empty: agent waits for the interface + // to exists and the selected IP addresses to be assigned (i.e. there will be derived + // value for each expected IP address in the PENDING state until the address is assigned + // to the interface externally) + Interface_EXISTING Interface_Type = 4 // In Linux, VRF is implemented as yet another type of netdevice (i.e. listed with `ip link show`). // Network interfaces are then assigned to VRF simply by enslaving them to the VRF device. // For more information, visit: https://www.kernel.org/doc/Documentation/networking/vrf.txt Interface_VRF_DEVICE Interface_Type = 5 + // Create a dummy Linux interface which effectively behaves just like the loopback. + Interface_DUMMY Interface_Type = 6 ) // Enum value maps for Interface_Type. @@ -49,6 +73,7 @@ var ( 3: "LOOPBACK", 4: "EXISTING", 5: "VRF_DEVICE", + 6: "DUMMY", } Interface_Type_value = map[string]int32{ "UNDEFINED": 0, @@ -57,6 +82,7 @@ var ( "LOOPBACK": 3, "EXISTING": 4, "VRF_DEVICE": 5, + "DUMMY": 6, } ) @@ -511,7 +537,7 @@ var file_ligato_linux_interfaces_interface_proto_rawDesc = []byte{ 0x6f, 0x2e, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x1a, 0x26, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2f, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x05, 0x0a, 0x09, 0x49, + 0x70, 0x61, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa2, 0x05, 0x0a, 0x09, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x6c, 0x69, 0x67, @@ -547,47 +573,48 @@ var file_ligato_linux_interfaces_interface_proto_rawDesc = []byte{ 0x14, 0x76, 0x72, 0x66, 0x5f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x76, 0x72, 0x66, 0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x22, - 0x5b, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, 0x46, + 0x66, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x56, 0x45, 0x54, 0x48, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x54, 0x41, 0x50, 0x5f, 0x54, 0x4f, 0x5f, 0x56, 0x50, 0x50, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x4c, 0x4f, 0x4f, 0x50, 0x42, 0x41, 0x43, 0x4b, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x58, 0x49, 0x53, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, - 0x56, 0x52, 0x46, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x10, 0x05, 0x42, 0x06, 0x0a, 0x04, - 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0xec, 0x02, 0x0a, 0x08, 0x56, 0x65, 0x74, 0x68, 0x4c, 0x69, 0x6e, - 0x6b, 0x12, 0x20, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x66, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x49, 0x66, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x6a, 0x0a, 0x16, 0x72, 0x78, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, - 0x75, 0x6d, 0x5f, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2e, 0x6c, 0x69, 0x6e, - 0x75, 0x78, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x56, 0x65, - 0x74, 0x68, 0x4c, 0x69, 0x6e, 0x6b, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x4f, - 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x14, 0x72, 0x78, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x4f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x12, - 0x6a, 0x0a, 0x16, 0x74, 0x78, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x5f, 0x6f, - 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x34, 0x2e, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2e, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x56, 0x65, 0x74, 0x68, 0x4c, 0x69, - 0x6e, 0x6b, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x4f, 0x66, 0x66, 0x6c, 0x6f, - 0x61, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x14, 0x74, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, - 0x6d, 0x4f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x66, 0x0a, 0x12, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x4f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, - 0x67, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x48, 0x4b, 0x53, 0x4d, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x4f, - 0x41, 0x44, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, - 0x43, 0x48, 0x4b, 0x53, 0x4d, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x45, 0x4e, - 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x48, 0x4b, 0x53, 0x4d, - 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, - 0x44, 0x10, 0x02, 0x22, 0x30, 0x0a, 0x07, 0x54, 0x61, 0x70, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x25, - 0x0a, 0x0f, 0x76, 0x70, 0x70, 0x5f, 0x74, 0x61, 0x70, 0x5f, 0x69, 0x66, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x76, 0x70, 0x70, 0x54, 0x61, 0x70, 0x49, - 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x31, 0x0a, 0x0a, 0x56, 0x72, 0x66, 0x44, 0x65, 0x76, 0x4c, - 0x69, 0x6e, 0x6b, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x74, - 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x72, 0x6f, 0x75, 0x74, - 0x69, 0x6e, 0x67, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x4a, 0x5a, 0x48, 0x67, 0x6f, 0x2e, 0x6c, - 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2e, 0x69, 0x6f, 0x2f, 0x76, 0x70, 0x70, 0x2d, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x69, 0x67, 0x61, - 0x74, 0x6f, 0x2f, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, - 0x63, 0x65, 0x73, 0x3b, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, - 0x61, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x56, 0x52, 0x46, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, + 0x44, 0x55, 0x4d, 0x4d, 0x59, 0x10, 0x06, 0x42, 0x06, 0x0a, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x22, + 0xec, 0x02, 0x0a, 0x08, 0x56, 0x65, 0x74, 0x68, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x20, 0x0a, 0x0c, + 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x66, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x49, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x6a, + 0x0a, 0x16, 0x72, 0x78, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x5f, 0x6f, 0x66, + 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, + 0x2e, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2e, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x2e, 0x56, 0x65, 0x74, 0x68, 0x4c, 0x69, 0x6e, + 0x6b, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x4f, 0x66, 0x66, 0x6c, 0x6f, 0x61, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x14, 0x72, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, + 0x4f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x6a, 0x0a, 0x16, 0x74, 0x78, + 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x5f, 0x6f, 0x66, 0x66, 0x6c, 0x6f, 0x61, + 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, 0x6c, 0x69, 0x67, + 0x61, 0x74, 0x6f, 0x2e, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, + 0x61, 0x63, 0x65, 0x73, 0x2e, 0x56, 0x65, 0x74, 0x68, 0x4c, 0x69, 0x6e, 0x6b, 0x2e, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x4f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x14, 0x74, 0x78, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x4f, 0x66, 0x66, 0x6c, + 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x22, 0x66, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, + 0x75, 0x6d, 0x4f, 0x66, 0x66, 0x6c, 0x6f, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x19, 0x0a, 0x15, + 0x43, 0x48, 0x4b, 0x53, 0x4d, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x44, 0x45, + 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x48, 0x4b, 0x53, 0x4d, + 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x45, 0x4e, 0x41, 0x42, 0x4c, 0x45, 0x44, + 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x48, 0x4b, 0x53, 0x4d, 0x5f, 0x4f, 0x46, 0x46, 0x4c, + 0x4f, 0x41, 0x44, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x22, 0x30, + 0x0a, 0x07, 0x54, 0x61, 0x70, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x25, 0x0a, 0x0f, 0x76, 0x70, 0x70, + 0x5f, 0x74, 0x61, 0x70, 0x5f, 0x69, 0x66, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x76, 0x70, 0x70, 0x54, 0x61, 0x70, 0x49, 0x66, 0x4e, 0x61, 0x6d, 0x65, + 0x22, 0x31, 0x0a, 0x0a, 0x56, 0x72, 0x66, 0x44, 0x65, 0x76, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x23, + 0x0a, 0x0d, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x54, 0x61, + 0x62, 0x6c, 0x65, 0x42, 0x4a, 0x5a, 0x48, 0x67, 0x6f, 0x2e, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, + 0x2e, 0x69, 0x6f, 0x2f, 0x76, 0x70, 0x70, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x33, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2f, 0x6c, 0x69, + 0x6e, 0x75, 0x78, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x3b, 0x6c, + 0x69, 0x6e, 0x75, 0x78, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x73, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/ligato/linux/interfaces/interface.proto b/proto/ligato/linux/interfaces/interface.proto index 8d346a2e97..d69cfa5116 100644 --- a/proto/ligato/linux/interfaces/interface.proto +++ b/proto/ligato/linux/interfaces/interface.proto @@ -11,13 +11,40 @@ message Interface { UNDEFINED = 0; VETH = 1; TAP_TO_VPP = 2; // TAP created by VPP to have the Linux-side further configured + + // LOOPBACK is used to attach configuration to an existing "lo" interface, + // but unlike EXISTING type it is not limited to the default network namespace + // (i.e. loopbacks in other containers can be referenced also). + // To create an additional interface which effectively acts as a loopback, + // use DUMMY interface (see below). LOOPBACK = 3; + + // Wait for and potentially attach additional network configuration to an interface + // created externally (i.e. not by this agent) in the default network namespace + // (i.e. same as used by the agent). + // Behaviour of the EXISTING interface depends on the values of ip_addresses and + // link_only attributes as follows: + // 1. link_only=false and ip_addresses are empty: agent waits for interface to be created + // externally and then configures it in the L2-only mode (resync will remove any IP addresses + // configured from outside of the agent) + // 2. link_only=false and ip_addresses are non-empty: agent waits for interface to be created + // externally and then attaches the selected IP addresses to it (resync removes any other + // IPs added externally) + // 3. link_only=true and ip_addresses are empty: agent only waits for the interface + // to exists (it doesn't wait for or change any IP addresses attached to it) + // 4. link_only=true and ip_addresses are non empty: agent waits for the interface + // to exists and the selected IP addresses to be assigned (i.e. there will be derived + // value for each expected IP address in the PENDING state until the address is assigned + // to the interface externally) EXISTING = 4; // In Linux, VRF is implemented as yet another type of netdevice (i.e. listed with `ip link show`). // Network interfaces are then assigned to VRF simply by enslaving them to the VRF device. // For more information, visit: https://www.kernel.org/doc/Documentation/networking/vrf.txt VRF_DEVICE = 5; + + // Create a dummy Linux interface which effectively behaves just like the loopback. + DUMMY = 6; }; // Name is mandatory field representing logical name for the interface. diff --git a/proto/ligato/linux/interfaces/models.go b/proto/ligato/linux/interfaces/models.go index 8a24ead51d..c5a9c91223 100644 --- a/proto/ligato/linux/interfaces/models.go +++ b/proto/ligato/linux/interfaces/models.go @@ -48,6 +48,8 @@ const ( // existing Linux interfaces in the default namespace (referenced by host names). InterfaceHostNameKeyPrefix = "linux/interface/host-name/" + interfaceHostNameWithAddrKeyTmpl = InterfaceHostNameKeyPrefix + "{host-name}/address/{address}" + /* Interface State (derived) */ // InterfaceStateKeyPrefix is used as a common prefix for keys derived from @@ -72,9 +74,14 @@ const ( // (incl. mask) assigned to a Linux interface (referenced by the logical name). interfaceAddrKeyTmpl = interfaceAddrKeyPrefix + "{address-source}/{address}" - // interfaceAddrKeyTmplWithVrf is extended template for keys of derived - // interface addresses which also includes the VRF name. - interfaceAddrKeyTmplWithVrf = interfaceAddrKeyTmpl + "/vrf/{vrf}" + // interfaceAddrVrfKeyPart is added to key representing IP address assignment + // if the interface is inside a VRF. + interfaceAddrVrfKeyPart = "/vrf/{vrf}" + + // interfaceAddrHostnameKeyPart is added to key representing IP address assignment + // if the host-name of the interface is needed, which is for example the case for EXISTING + // IP addresses (i.e. added externally and being waited for by the agent). + interfaceAddrHostnameKeyPart = "/host-name/{host-name}" /* Interface VRF (derived) */ @@ -95,6 +102,19 @@ func InterfaceHostNameKey(hostName string) string { return InterfaceHostNameKeyPrefix + hostName } +// InterfaceHostNameWithAddrKey returns key representing assignment of an IP address +// to a Linux interface referenced by its host name. +// If address is empty, the function returns key prefix matching any IP address. +func InterfaceHostNameWithAddrKey(hostName, address string) string { + if hostName == "" { + hostName = InvalidKeyPart + } + tmpl := interfaceHostNameWithAddrKeyTmpl + key := strings.Replace(tmpl, "{host-name}", hostName, 1) + key = strings.Replace(key, "{address}", address, 1) + return key +} + /* Interface State (derived) */ // InterfaceStateKey returns key representing admin state of a Linux interface. @@ -141,7 +161,7 @@ func InterfaceAddressPrefix(iface string) string { // InterfaceAddressKey returns key representing IP address assigned to Linux interface. // With undefined vrf the returned key can be also used as a key prefix, matching derived // interface address key regardless of the VRF to which it belongs. -func InterfaceAddressKey(iface, address, vrf string, source netalloc.IPAddressSource) string { +func InterfaceAddressKey(iface, address, vrf, hostName string, source netalloc.IPAddressSource) string { if iface == "" { iface = InvalidKeyPart } @@ -158,7 +178,10 @@ func InterfaceAddressKey(iface, address, vrf string, source netalloc.IPAddressSo // construct key without validating the IP address tmpl := interfaceAddrKeyTmpl if vrf != "" { - tmpl = interfaceAddrKeyTmplWithVrf + tmpl += interfaceAddrVrfKeyPart + } + if hostName != "" { + tmpl += interfaceAddrHostnameKeyPart } key := strings.Replace(tmpl, "{iface}", iface, 1) key = strings.Replace(key, "{address-source}", src, 1) @@ -166,12 +189,16 @@ func InterfaceAddressKey(iface, address, vrf string, source netalloc.IPAddressSo if vrf != "" { key = strings.Replace(key, "{vrf}", vrf, 1) } + if hostName != "" { + key = strings.Replace(key, "{host-name}", hostName, 1) + } return key } // ParseInterfaceAddressKey parses interface address from key derived // from interface by InterfaceAddressKey(). -func ParseInterfaceAddressKey(key string) (iface, address, vrf string, source netalloc.IPAddressSource, invalidKey, isAddrKey bool) { +func ParseInterfaceAddressKey(key string) (iface, address, vrf, hostName string, source netalloc.IPAddressSource, + invalidKey, isAddrKey bool) { parts := strings.Split(key, "/") if len(parts) < 4 || parts[0] != "linux" || parts[1] != "interface" { return @@ -179,12 +206,15 @@ func ParseInterfaceAddressKey(key string) (iface, address, vrf string, source ne addrIdx := -1 vrfIdx := len(parts) + hostNameIdx := len(parts) for idx, part := range parts { switch part { case "address": addrIdx = idx case "vrf": vrfIdx = idx + case "host-name": + hostNameIdx = idx } } if addrIdx == -1 { @@ -215,7 +245,11 @@ func ParseInterfaceAddressKey(key string) (iface, address, vrf string, source ne source = netalloc.IPAddressSource(srcInt) // return address as is (not parsed - this is done by the netalloc plugin) - address = strings.Join(parts[addrIdx+2:vrfIdx], "/") + addrEnd := vrfIdx + if hostNameIdx < vrfIdx { + addrEnd = hostNameIdx + } + address = strings.Join(parts[addrIdx+2:addrEnd], "/") if address == "" { invalidKey = true } @@ -228,6 +262,15 @@ func ParseInterfaceAddressKey(key string) (iface, address, vrf string, source ne } vrf = parts[vrfIdx+1] } + + // parse host-name + if hostNameIdx < len(parts) { + if hostNameIdx == len(parts)-1 { + invalidKey = true + return + } + hostName = parts[hostNameIdx+1] + } return } diff --git a/proto/ligato/linux/interfaces/models_test.go b/proto/ligato/linux/interfaces/models_test.go index a16b26e225..bc89d9c9a6 100644 --- a/proto/ligato/linux/interfaces/models_test.go +++ b/proto/ligato/linux/interfaces/models_test.go @@ -26,6 +26,7 @@ func TestInterfaceAddressKey(t *testing.T) { iface string address string vrf string + hostName string source netalloc.IPAddressSource expectedKey string }{ @@ -121,14 +122,31 @@ func TestInterfaceAddressKey(t *testing.T) { source: netalloc.IPAddressSource_STATIC, expectedKey: "linux/interface/memif0/address/static/2001:db8::/32/vrf/red", }, + { + name: "existing IPv4 address", + iface: "my-tap0", + address: "192.168.1.12/24", + source: netalloc.IPAddressSource_EXISTING, + hostName: "tap0", + expectedKey: "linux/interface/my-tap0/address/existing/192.168.1.12/24/host-name/tap0", + }, + { + name: "existing IPv4 address inside VRF", + iface: "my-tap0", + address: "192.168.1.12/24", + vrf: "blue", + source: netalloc.IPAddressSource_EXISTING, + hostName: "tap0", + expectedKey: "linux/interface/my-tap0/address/existing/192.168.1.12/24/vrf/blue/host-name/tap0", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - key := InterfaceAddressKey(test.iface, test.address, test.vrf, test.source) + key := InterfaceAddressKey(test.iface, test.address, test.vrf, test.hostName, test.source) if key != test.expectedKey { - t.Errorf("failed for: iface=%s address=%s vrf=%s source=%s\n"+ + t.Errorf("failed for: iface=%s address=%s vrf=%s hostName=%s source=%s\n"+ "expected key:\n\t%q\ngot key:\n\t%q", - test.iface, test.address, test.vrf, string(test.source), test.expectedKey, key) + test.iface, test.address, test.vrf, test.hostName, string(test.source), test.expectedKey, key) } }) } @@ -141,6 +159,7 @@ func TestParseInterfaceAddressKey(t *testing.T) { expectedIface string expectedIfaceAddr string expectedIfaceVrf string + expectedHostName string expectedSource netalloc.IPAddressSource expectedInvalidKey bool expectedIsAddrKey bool @@ -202,6 +221,25 @@ func TestParseInterfaceAddressKey(t *testing.T) { expectedSource: netalloc.IPAddressSource_STATIC, expectedIsAddrKey: true, }, + { + name: "existing IPv4 address", + key: "linux/interface/my-tap0/address/existing/192.168.1.12/24/host-name/tap0", + expectedIface: "my-tap0", + expectedIfaceAddr: "192.168.1.12/24", + expectedHostName: "tap0", + expectedSource: netalloc.IPAddressSource_EXISTING, + expectedIsAddrKey: true, + }, + { + name: "existing IPv4 address inside VRF", + key: "linux/interface/my-tap0/address/existing/192.168.1.12/24/vrf/blue/host-name/tap0", + expectedIface: "my-tap0", + expectedIfaceAddr: "192.168.1.12/24", + expectedIfaceVrf: "blue", + expectedHostName: "tap0", + expectedSource: netalloc.IPAddressSource_EXISTING, + expectedIsAddrKey: true, + }, { name: "IPv6 address", key: "linux/interface/tap1/address/static/2001:db8:85a3::8a2e:370:7334/48/vrf/red", @@ -310,7 +348,7 @@ func TestParseInterfaceAddressKey(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - iface, ipAddr, vrf, source, invalidKey, isAddrKey := ParseInterfaceAddressKey(test.key) + iface, ipAddr, vrf, hostName, source, invalidKey, isAddrKey := ParseInterfaceAddressKey(test.key) if isAddrKey != test.expectedIsAddrKey { t.Errorf("expected isAddrKey: %v\tgot: %v", test.expectedIsAddrKey, isAddrKey) } @@ -329,6 +367,9 @@ func TestParseInterfaceAddressKey(t *testing.T) { if vrf != test.expectedIfaceVrf { t.Errorf("expected vrf: %s\tgot: %s", test.expectedIfaceVrf, vrf) } + if hostName != test.expectedHostName { + t.Errorf("expected hostName: %s\tgot: %s", test.expectedHostName, hostName) + } }) } } diff --git a/proto/ligato/netalloc/netalloc.pb.go b/proto/ligato/netalloc/netalloc.pb.go index 8bb83e67b0..9b07327a8e 100644 --- a/proto/ligato/netalloc/netalloc.pb.go +++ b/proto/ligato/netalloc/netalloc.pb.go @@ -121,6 +121,9 @@ const ( // ALLOC_REF is a reference inside NB configuration to an allocated // IP address. IPAddressSource_ALLOC_REF IPAddressSource = 3 + // EXISTING is set when IP address is assigned to (EXISTING) interface + // externally (i.e. by a different agent or manually by an administrator). + IPAddressSource_EXISTING IPAddressSource = 4 ) // Enum value maps for IPAddressSource. @@ -130,12 +133,14 @@ var ( 1: "STATIC", 2: "FROM_DHCP", 3: "ALLOC_REF", + 4: "EXISTING", } IPAddressSource_value = map[string]int32{ "UNDEFINED_SOURCE": 0, "STATIC": 1, "FROM_DHCP": 2, "ALLOC_REF": 3, + "EXISTING": 4, } ) @@ -344,15 +349,16 @@ var file_ligato_netalloc_netalloc_proto_rawDesc = []byte{ 0x44, 0x52, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x4d, 0x41, 0x53, 0x4b, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x44, 0x44, 0x52, 0x5f, 0x4e, 0x45, 0x54, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x5f, 0x41, 0x44, 0x44, 0x52, 0x5f, 0x4e, 0x45, 0x54, 0x10, - 0x04, 0x2a, 0x51, 0x0a, 0x0f, 0x49, 0x50, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x6f, + 0x04, 0x2a, 0x5f, 0x0a, 0x0f, 0x49, 0x50, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x55, 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x54, 0x41, 0x54, 0x49, 0x43, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x52, 0x4f, 0x4d, 0x5f, 0x44, 0x48, 0x43, 0x50, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x41, 0x4c, 0x4c, 0x4f, 0x43, 0x5f, 0x52, - 0x45, 0x46, 0x10, 0x03, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x6c, 0x69, 0x67, 0x61, 0x74, - 0x6f, 0x2e, 0x69, 0x6f, 0x2f, 0x76, 0x70, 0x70, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x76, - 0x33, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2f, 0x6e, - 0x65, 0x74, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x45, 0x46, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x58, 0x49, 0x53, 0x54, 0x49, 0x4e, 0x47, + 0x10, 0x04, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2e, + 0x69, 0x6f, 0x2f, 0x76, 0x70, 0x70, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x33, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x69, 0x67, 0x61, 0x74, 0x6f, 0x2f, 0x6e, 0x65, 0x74, + 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/ligato/netalloc/netalloc.proto b/proto/ligato/netalloc/netalloc.proto index 066c6ec3d0..798b5748b5 100644 --- a/proto/ligato/netalloc/netalloc.proto +++ b/proto/ligato/netalloc/netalloc.proto @@ -57,6 +57,10 @@ enum IPAddressSource { // ALLOC_REF is a reference inside NB configuration to an allocated // IP address. ALLOC_REF = 3; + + // EXISTING is set when IP address is assigned to (EXISTING) interface + // externally (i.e. by a different agent or manually by an administrator). + EXISTING = 4; } // IPAllocation represents a single allocated IP address. diff --git a/tests/e2e/012_linux_interfaces_test.go b/tests/e2e/012_linux_interfaces_test.go new file mode 100644 index 0000000000..827622bf25 --- /dev/null +++ b/tests/e2e/012_linux_interfaces_test.go @@ -0,0 +1,334 @@ +// Copyright (c) 2020 Pantheon.tech +// +// 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 e2e + +import ( + "context" + "testing" + "time" + + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" + + "go.ligato.io/vpp-agent/v3/plugins/netalloc/utils" + "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" + linux_interfaces "go.ligato.io/vpp-agent/v3/proto/ligato/linux/interfaces" + linux_namespace "go.ligato.io/vpp-agent/v3/proto/ligato/linux/namespace" + netalloc_api "go.ligato.io/vpp-agent/v3/proto/ligato/netalloc" +) + +// Test dummy interfaces (additional loopbacks). +func TestDummyInterface(t *testing.T) { + ctx := Setup(t) + defer ctx.Teardown() + + const ( + dummy1Hostname = "lo1" + dummy2Hostname = "lo2" + ipAddr1 = "192.168.7.7" + ipAddr2 = "10.7.7.7" + ipAddr3 = "10.8.8.8" + netMask = "/24" + msName = "microservice1" + ) + + dummyIf1 := &linux_interfaces.Interface{ + Name: "dummy1", + Type: linux_interfaces.Interface_DUMMY, + Enabled: true, + IpAddresses: []string{ipAddr1 + netMask, ipAddr2 + netMask}, + HostIfName: dummy1Hostname, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + dummyIf2 := &linux_interfaces.Interface{ + Name: "dummy2", + Type: linux_interfaces.Interface_DUMMY, + Enabled: true, + IpAddresses: []string{ipAddr3 + netMask}, + HostIfName: dummy2Hostname, + Namespace: &linux_namespace.NetNamespace{ + Type: linux_namespace.NetNamespace_MICROSERVICE, + Reference: msNamePrefix + msName, + }, + } + + ctx.StartMicroservice(msName) + req := ctx.GenericClient().ChangeRequest() + err := req.Update( + dummyIf1, + dummyIf2, + ).Send(context.Background()) + Expect(err).ToNot(HaveOccurred()) + + Eventually(ctx.GetValueStateClb(dummyIf1)).Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Expect(ctx.GetValueState(dummyIf2)).To(Equal(kvscheduler.ValueState_CONFIGURED)) + Expect(ctx.PingFromMs(msName, ipAddr1)).To(Succeed()) + Expect(ctx.PingFromMs(msName, ipAddr2)).To(Succeed()) + Expect(ctx.PingFromMs(msName, ipAddr3)).To(Succeed()) + Expect(ctx.AgentInSync()).To(BeTrue()) + + // Delete dummy2 + req = ctx.GenericClient().ChangeRequest() + err = req.Delete( + dummyIf2, + ).Send(context.Background()) + Expect(err).ToNot(HaveOccurred()) + + Expect(ctx.GetValueState(dummyIf1)).To(Equal(kvscheduler.ValueState_CONFIGURED)) + Expect(ctx.GetValueState(dummyIf2)).ToNot(Equal(kvscheduler.ValueState_CONFIGURED)) + Expect(ctx.PingFromMs(msName, ipAddr1)).To(Succeed()) + Expect(ctx.PingFromMs(msName, ipAddr2)).To(Succeed()) + Expect(ctx.PingFromMs(msName, ipAddr3)).ToNot(Succeed()) + Expect(ctx.AgentInSync()).To(BeTrue()) + + // restart microservice + ctx.StopMicroservice(msName) + Eventually(ctx.GetValueStateClb(dummyIf1)).Should(Equal(kvscheduler.ValueState_PENDING)) + Expect(ctx.AgentInSync()).To(BeTrue()) + ctx.StartMicroservice(msName) + Eventually(ctx.GetValueStateClb(dummyIf1)).Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Expect(ctx.PingFromMs(msName, ipAddr1)).To(Succeed()) + Expect(ctx.PingFromMs(msName, ipAddr2)).To(Succeed()) + Expect(ctx.AgentInSync()).To(BeTrue()) +} + +// Test interfaces created externally but with IP addresses assigned by the agent. +func TestExistingInterface(t *testing.T) { + ctx := Setup(t) + defer ctx.Teardown() + + const ( + ifaceHostName = "loop1" + ifaceName = "existing-loop1" + ipAddr1 = "192.168.7.7" + ipAddr2 = "10.7.7.7" + ipAddr3 = "172.16.7.7" + netMask = "/24" + ) + + existingIface := &linux_interfaces.Interface{ + Name: ifaceName, + Type: linux_interfaces.Interface_EXISTING, + Enabled: true, + IpAddresses: []string{ipAddr1 + netMask, ipAddr2 + netMask}, + HostIfName: ifaceHostName, + } + + hasIP := func(link netlink.Link, ipAddr string) bool { + addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) + Expect(err).ToNot(HaveOccurred()) + for _, addr := range addrs { + if addr.IP.String() == ipAddr { + return true + } + } + return false + } + + addrKey := func(addr string) string { + return linux_interfaces.InterfaceAddressKey( + ifaceName, addr, "", "", netalloc_api.IPAddressSource_STATIC) + } + + req := ctx.GenericClient().ChangeRequest() + err := req.Update( + existingIface, + ).Send(context.Background()) + Expect(err).ToNot(HaveOccurred()) + + // referenced interface does not exist yet + Expect(ctx.GetValueState(existingIface)).To(Equal(kvscheduler.ValueState_PENDING)) + + // create referenced host interface using netlink + attrs := netlink.NewLinkAttrs() + attrs.Name = ifaceHostName + dummyLink := &netlink.Dummy{ + LinkAttrs: attrs, + } + err = netlink.LinkAdd(dummyLink) + Expect(err).ToNot(HaveOccurred()) + ifaceLink, err := netlink.LinkByName(ifaceHostName) + Expect(err).ToNot(HaveOccurred()) + err = netlink.LinkSetUp(ifaceLink) + Expect(err).ToNot(HaveOccurred()) + + Eventually(ctx.GetValueStateClb(existingIface)).Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Eventually(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr1+netMask))). + Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Eventually(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr2+netMask))). + Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Expect(ctx.AgentInSync()).To(BeTrue()) + + // check that the IP addresses have been configured + Expect(hasIP(ifaceLink, ipAddr1)).To(BeTrue()) + Expect(hasIP(ifaceLink, ipAddr2)).To(BeTrue()) + + // add third IP address externally, it should get removed by resync + ipAddr, _, err := utils.ParseIPAddr(ipAddr3+netMask, nil) + Expect(err).ToNot(HaveOccurred()) + err = netlink.AddrAdd(ifaceLink, &netlink.Addr{IPNet: ipAddr}) + Expect(err).ToNot(HaveOccurred()) + + // resync should remove the address that was added externally + Expect(ctx.AgentInSync()).To(BeFalse()) + Expect(hasIP(ifaceLink, ipAddr1)).To(BeTrue()) + Expect(hasIP(ifaceLink, ipAddr2)).To(BeTrue()) + Expect(hasIP(ifaceLink, ipAddr3)).To(BeFalse()) + + // remove the EXISTING interface (IP addresses should be unassigned) + req = ctx.GenericClient().ChangeRequest() + err = req.Delete( + existingIface, + ).Send(context.Background()) + Expect(err).ToNot(HaveOccurred()) + Expect(ctx.GetValueState(existingIface)).ToNot(Equal(kvscheduler.ValueState_CONFIGURED)) + ifaceLink, err = netlink.LinkByName(ifaceHostName) + Expect(err).ToNot(HaveOccurred()) + Expect(hasIP(ifaceLink, ipAddr1)).To(BeFalse()) + Expect(hasIP(ifaceLink, ipAddr2)).To(BeFalse()) + Expect(hasIP(ifaceLink, ipAddr3)).To(BeFalse()) + + // cleanup + err = netlink.LinkDel(ifaceLink) + Expect(err).ToNot(HaveOccurred()) +} + +// Test interfaces created externally including the IP address assignments. +func TestExistingLinkOnlyInterface(t *testing.T) { + ctx := Setup(t) + defer ctx.Teardown() + + SetDefaultConsistentlyDuration(3*time.Second) + SetDefaultConsistentlyPollingInterval(time.Second) + + const ( + ifaceHostName = "loop1" + ifaceName = "existing-loop1" + ipAddr1 = "192.168.7.7" + ipAddr2 = "10.7.7.7" + ipAddr3 = "172.16.7.7" + netMask = "/24" + ) + + existingIface := &linux_interfaces.Interface{ + Name: ifaceName, + Type: linux_interfaces.Interface_EXISTING, + Enabled: true, + IpAddresses: []string{ipAddr1 + netMask, ipAddr2 + netMask, ipAddr3 + netMask}, + HostIfName: ifaceHostName, + LinkOnly: true, // <- agent does not configure IP addresses (they are also "existing") + } + + hasIP := func(link netlink.Link, ipAddr string) bool { + addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) + Expect(err).ToNot(HaveOccurred()) + for _, addr := range addrs { + if addr.IP.String() == ipAddr { + return true + } + } + return false + } + + addrKey := func(addr string) string { + return linux_interfaces.InterfaceAddressKey( + ifaceName, addr, "", ifaceHostName, netalloc_api.IPAddressSource_EXISTING) + } + + req := ctx.GenericClient().ChangeRequest() + err := req.Update( + existingIface, + ).Send(context.Background()) + Expect(err).ToNot(HaveOccurred()) + + // the referenced interface does not exist yet + Expect(ctx.GetValueState(existingIface)).To(Equal(kvscheduler.ValueState_PENDING)) + + // create referenced host interface using netlink (without IPs for now) + attrs := netlink.NewLinkAttrs() + attrs.Name = ifaceHostName + dummyLink := &netlink.Dummy{ + LinkAttrs: attrs, + } + err = netlink.LinkAdd(dummyLink) + Expect(err).ToNot(HaveOccurred()) + ifaceLink, err := netlink.LinkByName(ifaceHostName) + Expect(err).ToNot(HaveOccurred()) + err = netlink.LinkSetUp(ifaceLink) + Expect(err).ToNot(HaveOccurred()) + + Eventually(ctx.GetValueStateClb(existingIface)).Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Consistently(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr1+netMask))). + Should(Equal(kvscheduler.ValueState_PENDING)) + Consistently(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr2+netMask))). + Should(Equal(kvscheduler.ValueState_PENDING)) + Consistently(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr3+netMask))). + Should(Equal(kvscheduler.ValueState_PENDING)) + Expect(ctx.AgentInSync()).To(BeTrue()) + + // add IP addresses via netlink (except ipAddr3) + Expect(hasIP(ifaceLink, ipAddr1)).To(BeFalse()) + Expect(hasIP(ifaceLink, ipAddr2)).To(BeFalse()) + Expect(hasIP(ifaceLink, ipAddr3)).To(BeFalse()) + ipAddr, _, err := utils.ParseIPAddr(ipAddr1+netMask, nil) + Expect(err).ToNot(HaveOccurred()) + err = netlink.AddrAdd(ifaceLink, &netlink.Addr{IPNet: ipAddr}) + Expect(err).ToNot(HaveOccurred()) + ipAddr, _, err = utils.ParseIPAddr(ipAddr2+netMask, nil) + Expect(err).ToNot(HaveOccurred()) + err = netlink.AddrAdd(ifaceLink, &netlink.Addr{IPNet: ipAddr}) + Expect(err).ToNot(HaveOccurred()) + + // ipAddr1 and ipAddr2 should be eventually marked as configured + Eventually(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr1+netMask))). + Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Eventually(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr2+netMask))). + Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Consistently(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr3+netMask))). + Should(Equal(kvscheduler.ValueState_PENDING)) + Expect(ctx.AgentInSync()).To(BeTrue()) + + // remove one IP address + ipAddr, _, err = utils.ParseIPAddr(ipAddr1+netMask, nil) + Expect(err).ToNot(HaveOccurred()) + err = netlink.AddrDel(ifaceLink, &netlink.Addr{IPNet: ipAddr}) + Expect(err).ToNot(HaveOccurred()) + Eventually(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr1+netMask))). + Should(Equal(kvscheduler.ValueState_PENDING)) + Consistently(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr2+netMask))). + Should(Equal(kvscheduler.ValueState_CONFIGURED)) + Consistently(ctx.GetDerivedValueStateClb(existingIface, addrKey(ipAddr3+netMask))). + Should(Equal(kvscheduler.ValueState_PENDING)) + + // remove the EXISTING interface (the actual interface should be left untouched including IPs) + req = ctx.GenericClient().ChangeRequest() + err = req.Delete( + existingIface, + ).Send(context.Background()) + Expect(err).ToNot(HaveOccurred()) + Expect(ctx.GetValueState(existingIface)).ToNot(Equal(kvscheduler.ValueState_CONFIGURED)) + ifaceLink, err = netlink.LinkByName(ifaceHostName) + Expect(err).ToNot(HaveOccurred()) + Expect(hasIP(ifaceLink, ipAddr1)).To(BeFalse()) + Expect(hasIP(ifaceLink, ipAddr2)).To(BeTrue()) + Expect(hasIP(ifaceLink, ipAddr3)).To(BeFalse()) + + // cleanup + err = netlink.LinkDel(ifaceLink) + Expect(err).ToNot(HaveOccurred()) +} diff --git a/tests/e2e/e2e.go b/tests/e2e/e2e.go index b1198a2b5f..29eb8a871a 100644 --- a/tests/e2e/e2e.go +++ b/tests/e2e/e2e.go @@ -532,10 +532,19 @@ func (ctx *TestCtx) TestConnection(fromMs, toMs, toAddr, listenAddr string, func (ctx *TestCtx) GetValueState(value proto.Message) kvscheduler.ValueState { key := models.Key(value) - return ctx.GetValueStateByKey(key) + return ctx.getValueStateByKey(key, "") } func (ctx *TestCtx) GetValueStateByKey(key string) kvscheduler.ValueState { + return ctx.getValueStateByKey(key, "") +} + +func (ctx *TestCtx) GetDerivedValueState(baseValue proto.Message, derivedKey string) kvscheduler.ValueState { + key := models.Key(baseValue) + return ctx.getValueStateByKey(key, derivedKey) +} + +func (ctx *TestCtx) getValueStateByKey(key, derivedKey string) kvscheduler.ValueState { q := fmt.Sprintf(`/scheduler/status?key=%s`, url.QueryEscape(key)) resp, err := ctx.httpClient.GET(q) if err != nil { @@ -548,6 +557,14 @@ func (ctx *TestCtx) GetValueStateByKey(key string) kvscheduler.ValueState { if st.GetValue().GetKey() != key { ctx.t.Fatalf("Received value status for unexpected key: %v", st) } + if derivedKey != "" { + for _, derVal := range st.DerivedValues { + if derVal.Key == derivedKey { + return derVal.State + } + } + return kvscheduler.ValueState_NONEXISTENT + } return st.GetValue().GetState() } @@ -559,6 +576,14 @@ func (ctx *TestCtx) GetValueStateClb(value proto.Message) func() kvscheduler.Val } } +// GetDerivedValueStateClb can be used to repeatedly check derived value state inside +// the assertions "Eventually" and "Consistently" from Omega. +func (ctx *TestCtx) GetDerivedValueStateClb(baseValue proto.Message, derivedKey string) func() kvscheduler.ValueState { + return func() kvscheduler.ValueState { + return ctx.GetDerivedValueState(baseValue, derivedKey) + } +} + func (ctx *TestCtx) startPacketTrace(nodes ...string) (stopTrace func()) { const tracePacketsMax = 100 for i, node := range nodes {