diff --git a/examples/localclient_linux/route/main.go b/examples/localclient_linux/route/main.go new file mode 100644 index 0000000000..d2f8add484 --- /dev/null +++ b/examples/localclient_linux/route/main.go @@ -0,0 +1,345 @@ +// Copyright (c) 2021 Cisco and/or its affiliates. +// +// 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 main + +import ( + "context" + linux_l3 "go.ligato.io/vpp-agent/v3/proto/ligato/linux/l3" + "log" + "sync" + "time" + + "github.com/namsral/flag" + "go.ligato.io/cn-infra/v2/agent" + "go.ligato.io/cn-infra/v2/logging" + "go.ligato.io/cn-infra/v2/logging/logrus" + + "go.ligato.io/vpp-agent/v3/clientv2/linux/localclient" + "go.ligato.io/vpp-agent/v3/cmd/vpp-agent/app" + linux_ifplugin "go.ligato.io/vpp-agent/v3/plugins/linux/ifplugin" + linux_nsplugin "go.ligato.io/vpp-agent/v3/plugins/linux/nsplugin" + "go.ligato.io/vpp-agent/v3/plugins/orchestrator" + vpp_ifplugin "go.ligato.io/vpp-agent/v3/plugins/vpp/ifplugin" + linux_intf "go.ligato.io/vpp-agent/v3/proto/ligato/linux/interfaces" + vpp_intf "go.ligato.io/vpp-agent/v3/proto/ligato/vpp/interfaces" +) + +var ( + timeout = flag.Int("timeout", 5, "Timeout between applying of initial and modified configuration in seconds") +) + +/* Vpp-agent Init and Close*/ + +// Start Agent plugins selected for this example. +func main() { + // Set inter-dependency between VPP & Linux plugins + vpp_ifplugin.DefaultPlugin.LinuxIfPlugin = &linux_ifplugin.DefaultPlugin + vpp_ifplugin.DefaultPlugin.NsPlugin = &linux_nsplugin.DefaultPlugin + linux_ifplugin.DefaultPlugin.VppIfPlugin = &vpp_ifplugin.DefaultPlugin + + // Init close channel to stop the example. + exampleFinished := make(chan struct{}) + go closeExample("Route example finished", exampleFinished) + + // Inject dependencies to example plugin + ep := &RouteExamplePlugin{ + Log: logging.DefaultLogger, + VPP: app.DefaultVPP(), + Linux: app.DefaultLinux(), + Orchestrator: &orchestrator.DefaultPlugin, + } + + // Start Agent + a := agent.NewAgent( + agent.AllPlugins(ep), + agent.QuitOnClose(exampleFinished), + ) + if err := a.Run(); err != nil { + log.Fatal(err) + } +} + +// Stop the agent with desired info message. +func closeExample(message string, exampleFinished chan struct{}) { + time.Sleep(time.Duration(*timeout*4) * time.Second) + logrus.DefaultLogger().Info(message) + close(exampleFinished) +} + +/* Route Example */ + +// RouteExamplePlugin uses localclient to transport example route, tap and its linux part +// configuration to linuxplugin or VPP plugins +type RouteExamplePlugin struct { + Log logging.Logger + app.VPP + app.Linux + Orchestrator *orchestrator.Plugin + + wg sync.WaitGroup + cancelResync context.CancelFunc + cancelModified context.CancelFunc +} + +// PluginName represents name of plugin. +const PluginName = "route-example" + +// Init initializes example plugin. +func (p *RouteExamplePlugin) Init() error { + // Logger + p.Log = logrus.DefaultLogger() + p.Log.SetLevel(logging.DebugLevel) + p.Log.Info("Initializing route device example") + + // Flags + flag.Parse() + p.Log.Infof("Timeout between create and modify set to %d", *timeout) + + p.Log.Info("Route example initialization done") + return nil +} + +// AfterInit initializes example plugin. +func (p *RouteExamplePlugin) AfterInit() error { + // apply initial Linux/VPP configuration + p.putInitialData() + + // schedule route resync + var ctx context.Context + ctx, p.cancelResync = context.WithCancel(context.Background()) + p.wg.Add(1) + go p.resync(ctx, *timeout) + + // schedule route reconfiguration + ctx, p.cancelModified = context.WithCancel(context.Background()) + p.wg.Add(1) + go p.putModifiedData(ctx, *timeout*2) + + return nil +} + +// Close cleans up the resources. +func (p *RouteExamplePlugin) Close() error { + p.cancelResync() + p.cancelModified() + p.wg.Wait() + + p.Log.Info("Closed route plugin") + return nil +} + +// String returns plugin name +func (p *RouteExamplePlugin) String() string { + return PluginName +} + +// Configure initial data +func (p *RouteExamplePlugin) putInitialData() { + p.Log.Infof("Applying initial configuration") + err := localclient.DataResyncRequest(PluginName). + VppInterface(tap()). + VppInterface(tapIPv6()). + LinuxInterface(linuxTap()). + LinuxInterface(linuxTapIpv6()). + LinuxRoute(route()). + LinuxRoute(routeIPv6()). + Send().ReceiveReply() + if err != nil { + p.Log.Errorf("Initial configuration failed: %v", err) + } else { + p.Log.Info("Initial configuration successful") + } +} + +// Configure modified data +// This step serves as a route resync check. After the resync, IPv4 route +// is expected to be re-created (metric change) while the IPv6 should be +// updated because of modified scope +func (p *RouteExamplePlugin) resync(ctx context.Context, timeout int) { + select { + case <-time.After(time.Duration(timeout) * time.Second): + p.Log.Infof("Applying resync routes") + // Simulate configuration change after timeout + err := localclient.DataResyncRequest(PluginName). + VppInterface(tap()). + VppInterface(tapIPv6()). + LinuxInterface(linuxTap()). + LinuxInterface(linuxTapIpv6()). + LinuxRoute(routeResync()). // recreate + LinuxRoute(routeResyncIPv6()). // update + Send().ReceiveReply() + if err != nil { + p.Log.Errorf("Resync failed: %v", err) + } else { + p.Log.Info("Resync successful") + } + case <-ctx.Done(): + // Cancel the scheduled re-configuration. + p.Log.Info("Resync of configuration canceled") + } + p.wg.Done() +} + +// Calling modify should cause IPv4 route to update (modified scope) while +// the IPv6 is expected to be recreated (changed metric) +func (p *RouteExamplePlugin) putModifiedData(ctx context.Context, timeout int) { + select { + case <-time.After(time.Duration(timeout) * time.Second): + p.Log.Infof("Applying modified configuration") + // Simulate configuration change after timeout + err := localclient.DataChangeRequest(PluginName). + Put(). + VppInterface(tap()). + VppInterface(tapIPv6()). + LinuxInterface(linuxTap()). + LinuxInterface(linuxTapIpv6()). + LinuxRoute(routeModified()). // update + LinuxRoute(routeModifiedIPv6()). // recreate + Send().ReceiveReply() + if err != nil { + p.Log.Errorf("Modified configuration failed: %v", err) + } else { + p.Log.Info("Modified configuration successful") + } + case <-ctx.Done(): + // Cancel the scheduled re-configuration. + p.Log.Info("Modification of configuration canceled") + } + p.wg.Done() +} + +/* Example Data */ + +func tap() *vpp_intf.Interface { + return &vpp_intf.Interface{ + Name: "tap1", + Type: vpp_intf.Interface_TAP, + Enabled: true, + PhysAddress: "d5:bc:dc:12:e4:0e", + IpAddresses: []string{ + "10.0.0.10/24", + }, + Link: &vpp_intf.Interface_Tap{ + Tap: &vpp_intf.TapLink{ + Version: 2, + }, + }, + } +} + +func tapIPv6() *vpp_intf.Interface { + return &vpp_intf.Interface{ + Name: "tap2", + Type: vpp_intf.Interface_TAP, + Enabled: true, + PhysAddress: "d5:bc:dc:12:e4:0d", + IpAddresses: []string{ + "abc1::10/64", + }, + Link: &vpp_intf.Interface_Tap{ + Tap: &vpp_intf.TapLink{ + Version: 2, + }, + }, + } +} + +func linuxTap() *linux_intf.Interface { + return &linux_intf.Interface{ + Name: "linux-tap1", + Type: linux_intf.Interface_TAP_TO_VPP, + Enabled: true, + PhysAddress: "12:e4:0e:d5:bc:dc", + IpAddresses: []string{ + "11.0.0.20/24", + }, + Link: &linux_intf.Interface_Tap{ + Tap: &linux_intf.TapLink{ + VppTapIfName: "tap1", + }, + }, + } +} + +func linuxTapIpv6() *linux_intf.Interface { + return &linux_intf.Interface{ + Name: "linux-tap2", + Type: linux_intf.Interface_TAP_TO_VPP, + Enabled: true, + PhysAddress: "44:bc:12:e4:0e:aa", + IpAddresses: []string{ + "abc2::20/64", + }, + Link: &linux_intf.Interface_Tap{ + Tap: &linux_intf.TapLink{ + VppTapIfName: "tap2", + }, + }, + } +} + +func route() *linux_l3.Route { + return &linux_l3.Route{ + OutgoingInterface: "linux-tap1", + DstNetwork: "100.10.0.0/24", + Scope: linux_l3.Route_GLOBAL, + Metric: 0, + } +} + +func routeResync() *linux_l3.Route { + return &linux_l3.Route{ + OutgoingInterface: "linux-tap1", + DstNetwork: "100.10.0.0/24", + Scope: linux_l3.Route_GLOBAL, + Metric: 500, + } +} + +func routeModified() *linux_l3.Route { + return &linux_l3.Route{ + OutgoingInterface: "linux-tap1", + DstNetwork: "100.10.0.0/24", + Scope: linux_l3.Route_LINK, + Metric: 500, + } +} + +func routeIPv6() *linux_l3.Route { + return &linux_l3.Route{ + OutgoingInterface: "linux-tap2", + DstNetwork: "aaa1::/64", + Scope: linux_l3.Route_GLOBAL, + Metric: 0, + } +} + +func routeResyncIPv6() *linux_l3.Route { + return &linux_l3.Route{ + OutgoingInterface: "linux-tap2", + DstNetwork: "aaa1::/64", + Scope: linux_l3.Route_LINK, + Metric: 1024, + } +} + +func routeModifiedIPv6() *linux_l3.Route { + return &linux_l3.Route{ + OutgoingInterface: "linux-tap2", + DstNetwork: "aaa1::/64", + Scope: linux_l3.Route_LINK, + Metric: 500, + } +} diff --git a/plugins/linux/l3plugin/descriptor/route.go b/plugins/linux/l3plugin/descriptor/route.go index 8bbec8b046..cb492f2dd3 100644 --- a/plugins/linux/l3plugin/descriptor/route.go +++ b/plugins/linux/l3plugin/descriptor/route.go @@ -51,6 +51,9 @@ const ( routeOutInterfaceIPAddrDep = "outgoing-interface-has-ip-address" routeGwReachabilityDep = "gw-reachable" allocatedAddrAttached = "allocated-addr-attached" + + // default metric of the IPv6 route + ipv6DefaultMetric = 1024 ) // A list of non-retriable errors: @@ -95,19 +98,20 @@ func NewRouteDescriptor( log: log.NewLogger("route-descriptor"), } typedDescr := &adapter.RouteDescriptor{ - Name: RouteDescriptorName, - NBKeyPrefix: linux_l3.ModelRoute.KeyPrefix(), - ValueTypeName: linux_l3.ModelRoute.ProtoName(), - KeySelector: linux_l3.ModelRoute.IsKeyValid, - KeyLabel: linux_l3.ModelRoute.StripKeyPrefix, - ValueComparator: ctx.EquivalentRoutes, - Validate: ctx.Validate, - Create: ctx.Create, - Delete: ctx.Delete, - Update: ctx.Update, - Retrieve: ctx.Retrieve, - DerivedValues: ctx.DerivedValues, - Dependencies: ctx.Dependencies, + Name: RouteDescriptorName, + NBKeyPrefix: linux_l3.ModelRoute.KeyPrefix(), + ValueTypeName: linux_l3.ModelRoute.ProtoName(), + KeySelector: linux_l3.ModelRoute.IsKeyValid, + KeyLabel: linux_l3.ModelRoute.StripKeyPrefix, + ValueComparator: ctx.EquivalentRoutes, + Validate: ctx.Validate, + Create: ctx.Create, + Delete: ctx.Delete, + Update: ctx.Update, + UpdateWithRecreate: ctx.UpdateWithRecreate, + Retrieve: ctx.Retrieve, + DerivedValues: ctx.DerivedValues, + Dependencies: ctx.Dependencies, RetrieveDependencies: []string{ netalloc_descr.IPAllocDescriptorName, ifdescriptor.InterfaceDescriptorName}, @@ -119,8 +123,11 @@ func NewRouteDescriptor( func (d *RouteDescriptor) EquivalentRoutes(key string, oldRoute, newRoute *linux_l3.Route) bool { // attributes compared as usually: if oldRoute.OutgoingInterface != newRoute.OutgoingInterface || - oldRoute.Scope != newRoute.Scope || - oldRoute.Metric != newRoute.Metric { + oldRoute.Scope != newRoute.Scope { + return false + } + // compare metrics + if !isRouteMetricEqual(oldRoute, newRoute) { return false } @@ -159,12 +166,17 @@ func (d *RouteDescriptor) Delete(key string, route *linux_l3.Route, metadata int return d.updateRoute(route, "delete", d.l3Handler.DelRoute) } -// Update is able to change route scope, metric and GW address. +// Update is able to change route scope and GW address. func (d *RouteDescriptor) Update(key string, oldRoute, newRoute *linux_l3.Route, oldMetadata interface{}) (newMetadata interface{}, err error) { err = d.updateRoute(newRoute, "modify", d.l3Handler.ReplaceRoute) return nil, err } +// UpdateWithRecreate in case the metric was changed +func (d *RouteDescriptor) UpdateWithRecreate(_ string, oldRoute, newRoute *linux_l3.Route, _ interface{}) bool { + return !isRouteMetricEqual(oldRoute, newRoute) +} + // updateRoute adds, modifies or deletes a Linux route. func (d *RouteDescriptor) updateRoute(route *linux_l3.Route, actionName string, actionClb func(route *netlink.Route) error) error { var err error @@ -488,3 +500,17 @@ func getGwAddr(route *linux_l3.Route) string { } return route.GwAddr } + +// compares route metrics. For IPv6, Metric 0 & 1024 are considered the same value +func isRouteMetricEqual(oldRoute, newRoute *linux_l3.Route) bool { + if oldRoute.Metric != newRoute.Metric { + dstNetwork := net.ParseIP(strings.Split(newRoute.DstNetwork, "/")[0]) + if dstNetwork != nil && dstNetwork.To4() == nil { + return (oldRoute.Metric == 0 && newRoute.Metric == ipv6DefaultMetric) || + (oldRoute.Metric == ipv6DefaultMetric && newRoute.Metric == 0) + } else { + return false + } + } + return true +}