Repository: akvorado/akvorado

Issue:
Mixing metadata type static and snmp
Hi,

I've read the docs and looked through the issues and discussions to see if this has come up in the past, but I don't think it did.

I have a couple of exporters in my network that work great with akvorado. I now have an additional exporter that is outside of my network and control. This exporter is a commercial Anti-DDOS solution which sends us flowdata when traffic is routed through them. However, I'm not allowed to do snmp to their exporter ip. 

What I'm looking for is a possibility to have metadata type snmp by default and metadata type static for their exporter ip address. Because as I read, flows are dropped when interface information can't be determined.

I don't think it is possible with the current design.

Hints from issue comments:
@vincentbernat: Yes, currently not possible, but shouldn't it too difficult to implement it.@TvL2386: That would be awesome! :-D@vincentbernat: Well, that's not that easy. I don't have an immediate solution that would fix this.

Solution patch:
diff --git a/cmd/inlet.go b/cmd/inlet.go
index c42fcdf7..fa289a2a 100644
--- a/cmd/inlet.go
+++ b/cmd/inlet.go
@@ -52,7 +52,7 @@ func (c *InletConfiguration) Reset() {
 		Core:      core.DefaultConfiguration(),
 		Schema:    schema.DefaultConfiguration(),
 	}
-	c.Metadata.Provider.Config = snmp.DefaultConfiguration()
+	c.Metadata.Providers = []metadata.ProviderConfiguration{{Config: snmp.DefaultConfiguration()}}
 	c.Routing.Provider.Config = bmp.DefaultConfiguration()
 }
 
diff --git a/console/data/docs/02-configuration.md b/console/data/docs/02-configuration.md
index 2fcab092..bbba21ee 100644
--- a/console/data/docs/02-configuration.md
+++ b/console/data/docs/02-configuration.md
@@ -379,14 +379,17 @@ providers available to poll metadata. The following keys are accepted:
   read them back on startup
 - `workers` tell how many workers to spawn to fetch metadata.
 - `max-batch-requests` define how many requests can be batched together
-- `provider` defines the provider configuration
+- `providers` defines the provider configurations
 
 As flows missing interface information are discarded, persisting the
 cache is useful to quickly be able to handle incoming flows. By
 default, no persistent cache is configured.
 
-The `provider` key contains the configuration of the provider. The provider type
-is defined by the `type` key.
+The `providers` key contains the configuration of the providers. For each, the
+provider type is defined by the `type` key. When using several providers, they
+will be queried in order and the process stops on the first to accept to handle
+a query. Currently, only the `static` provider can skip a query. Therefore, you
+should put it first.
 
 #### SNMP provider
 
@@ -419,7 +422,7 @@ For example:
 ```yaml
 metadata:
   workers: 10
-  provider:
+  providers:
     type: snmp
     communities:
       ::/0:
@@ -460,7 +463,7 @@ For example:
 
 ```yaml
 metadata:
- provider:
+ providers:
   type: gnmi
   authentication-parameters:
    ::/0:
@@ -507,28 +510,32 @@ an exporter configuration. An exporter configuration is map:
 
 An interface is a `name`, a `description` and a `speed`.
 
-For example:
+For example, to add an exception for `2001:db8:1::1`, then use SNMP for
+other exporters:
 
 ```yaml
 metadata:
-  provider:
-    type: static
-    exporters:
-      2001:db8:1::1:
-        name: exporter1
-        default:
-          name: unknown
-          description: Unknown interface
-          speed: 100
-        ifindexes:
-          10:
-            name: Gi0/0/10
-            description: PNI Netflix
-            speed: 1000
-          11:
-            name: Gi0/0/15
-            description: PNI Google
-            speed: 1000
+  providers:
+    - type: static
+      exporters:
+        2001:db8:1::1:
+          name: exporter1
+          default:
+            name: unknown
+            description: Unknown interface
+            speed: 100
+          ifindexes:
+            10:
+              name: Gi0/0/10
+              description: PNI Netflix
+              speed: 1000
+            11:
+              name: Gi0/0/15
+              description: PNI Google
+              speed: 1000
+    - type: snmp
+      communities:
+        ::/0: private
 ```
 
 The `static` provider also accepts a key `exporter-sources`, which will fetch a
@@ -553,7 +560,7 @@ For example:
 
 ```yaml
 metadata:
-  provider:
+  providers:
     type: static
     exporter-sources:
       gostatic:
diff --git a/console/data/docs/99-changelog.md b/console/data/docs/99-changelog.md
index 0b49730e..75be3c43 100644
--- a/console/data/docs/99-changelog.md
+++ b/console/data/docs/99-changelog.md
@@ -16,6 +16,7 @@ identified with a specific icon:
 - ✨ *inlet*: add gNMI metadata provider
 - ✨ *inlet*: static metadata provider can provide exporter and interface metadata
 - ✨ *inlet*: static metadata provider can fetch its configuration from an HTTP endpoint
+- ✨ *inlet*: metadata can be fetched from multiple providers (eg, static, then SNMP)
 - ✨ *inlet*: add support for several SNMPv2 communities
 - 🩹 *cmd*: fix parsing of `inlet`→`metadata`→`provider`→`ports`
 - 🩹 *console*: fix use of `InIfBoundary` and `OutIfBoundary` as dimensions
diff --git a/inlet/metadata/config.go b/inlet/metadata/config.go
index 9f7a9849..4de859dc 100644
--- a/inlet/metadata/config.go
+++ b/inlet/metadata/config.go
@@ -4,6 +4,8 @@
 package metadata
 
 import (
+	"fmt"
+	"reflect"
 	"time"
 
 	"akvorado/common/helpers"
@@ -11,6 +13,8 @@ import (
 	"akvorado/inlet/metadata/provider/gnmi"
 	"akvorado/inlet/metadata/provider/snmp"
 	"akvorado/inlet/metadata/provider/static"
+
+	"github.com/mitchellh/mapstructure"
 )
 
 // Configuration describes the configuration for the metadata client
@@ -24,8 +28,8 @@ type Configuration struct {
 	// CachePersist defines a file to store cache and survive restarts
 	CachePersistFile string
 
-	// Provider defines the configuration of the provider to sue
-	Provider ProviderConfiguration
+	// Provider defines the configuration of the providers to use
+	Providers []ProviderConfiguration
 
 	// Workers define the number of workers used to poll metadata
 	Workers int `validate:"min=1"`
@@ -45,6 +49,40 @@ func DefaultConfiguration() Configuration {
 	}
 }
 
+// ConfigurationUnmarshallerHook renames "provider" to "providers".
+func ConfigurationUnmarshallerHook() mapstructure.DecodeHookFunc {
+	return func(from, to reflect.Value) (interface{}, error) {
+		if from.Kind() != reflect.Map || from.IsNil() || to.Type() != reflect.TypeOf(Configuration{}) {
+			return from.Interface(), nil
+		}
+
+		// provider → providers
+		{
+			var providerKey, providersKey *reflect.Value
+			fromKeys := from.MapKeys()
+			for i, k := range fromKeys {
+				k = helpers.ElemOrIdentity(k)
+				if k.Kind() != reflect.String {
+					return from.Interface(), nil
+				}
+				if helpers.MapStructureMatchName(k.String(), "Provider") {
+					providerKey = &fromKeys[i]
+				} else if helpers.MapStructureMatchName(k.String(), "Providers") {
+					providersKey = &fromKeys[i]
+				}
+			}
+			if providersKey != nil && providerKey != nil {
+				return nil, fmt.Errorf("cannot have both %q and %q", providerKey.String(), providersKey.String())
+			}
+			if providerKey != nil {
+				from.SetMapIndex(reflect.ValueOf("providers"), from.MapIndex(*providerKey))
+				from.SetMapIndex(*providerKey, reflect.Value{})
+			}
+		}
+		return from.Interface(), nil
+	}
+}
+
 // ProviderConfiguration represents the configuration for a metadata provider.
 type ProviderConfiguration struct {
 	// Config is the actual configuration for the provider.
@@ -68,6 +106,7 @@ var providers = map[string](func() provider.Configuration){
 }
 
 func init() {
+	helpers.RegisterMapstructureUnmarshallerHook(ConfigurationUnmarshallerHook())
 	helpers.RegisterMapstructureUnmarshallerHook(
 		helpers.ParametrizedConfigurationUnmarshallerHook(ProviderConfiguration{}, providers))
 }
diff --git a/inlet/metadata/provider/root.go b/inlet/metadata/provider/root.go
index ffe974d7..795ceee9 100644
--- a/inlet/metadata/provider/root.go
+++ b/inlet/metadata/provider/root.go
@@ -6,12 +6,17 @@ package provider
 
 import (
 	"context"
+	"errors"
 	"net/netip"
 
 	"akvorado/common/reporter"
 	"akvorado/common/schema"
 )
 
+// ErrSkipProvider is the error returned on lookup for providers unwilling to
+// handle a request.
+var ErrSkipProvider = errors.New("provider skips query")
+
 // Interface contains the information about an interface.
 type Interface struct {
 	Name         string `validate:"required"`
diff --git a/inlet/metadata/provider/static/root.go b/inlet/metadata/provider/static/root.go
index 909d2758..ed330f3d 100644
--- a/inlet/metadata/provider/static/root.go
+++ b/inlet/metadata/provider/static/root.go
@@ -51,7 +51,7 @@ func (configuration Configuration) New(r *reporter.Reporter, put func(provider.U
 func (p *Provider) Query(_ context.Context, query provider.BatchQuery) error {
 	exporter, ok := p.exporters.Load().Lookup(query.ExporterIP)
 	if !ok {
-		return nil
+		return provider.ErrSkipProvider
 	}
 	for _, ifIndex := range query.IfIndexes {
 		iface, ok := exporter.IfIndexes[ifIndex]
diff --git a/inlet/metadata/root.go b/inlet/metadata/root.go
index 5be8c8e9..25631231 100644
--- a/inlet/metadata/root.go
+++ b/inlet/metadata/root.go
@@ -39,7 +39,7 @@ type Component struct {
 	providerBreakersLock   sync.Mutex
 	providerBreakerLoggers map[netip.Addr]reporter.Logger
 	providerBreakers       map[netip.Addr]*breaker.Breaker
-	provider               provider.Provider
+	providers              []provider.Provider
 
 	metrics struct {
 		cacheRefreshRuns         reporter.Counter
@@ -80,17 +80,20 @@ func New(r *reporter.Reporter, configuration Configuration, dependencies Depende
 		dispatcherBChannel:     make(chan (<-chan bool)),
 		providerBreakers:       make(map[netip.Addr]*breaker.Breaker),
 		providerBreakerLoggers: make(map[netip.Addr]reporter.Logger),
+		providers:              make([]provider.Provider, 0, 1),
 	}
 	c.d.Daemon.Track(&c.t, "inlet/metadata")
 
-	// Initialize the provider
-	selectedProvider, err := c.config.Provider.Config.New(r, func(update provider.Update) {
-		c.sc.Put(c.d.Clock.Now(), update.Query, update.Answer)
-	})
-	if err != nil {
-		return nil, err
+	// Initialize providers
+	for _, p := range c.config.Providers {
+		selectedProvider, err := p.Config.New(r, func(update provider.Update) {
+			c.sc.Put(c.d.Clock.Now(), update.Query, update.Answer)
+		})
+		if err != nil {
+			return nil, err
+		}
+		c.providers = append(c.providers, selectedProvider)
 	}
-	c.provider = selectedProvider
 
 	c.metrics.cacheRefreshRuns = r.Counter(
 		reporter.CounterOpts{
@@ -291,7 +294,18 @@ func (c *Component) providerIncomingRequest(request provider.BatchQuery) {
 	c.providerBreakersLock.Unlock()
 
 	if err := providerBreaker.Run(func() error {
-		return c.provider.Query(c.t.Context(nil), request)
+		ctx := c.t.Context(nil)
+		for _, p := range c.providers {
+			// Query providers in the order they are defined and stop on the
+			// first provider accepting to handle the query.
+			if err := p.Query(ctx, request); err != nil && err != provider.ErrSkipProvider {
+				return err
+			} else if err == provider.ErrSkipProvider {
+				continue
+			}
+			return nil
+		}
+		return nil
 	}); err == breaker.ErrBreakerOpen {
 		c.metrics.providerBreakerOpenCount.WithLabelValues(request.ExporterIP.Unmap().String()).Inc()
 		c.providerBreakersLock.Lock()



Test patch:
diff --git a/cmd/testdata/configurations/gnmi/expected.yaml b/cmd/testdata/configurations/gnmi/expected.yaml
index 3361f2cf..9f7735cf 100644
--- a/cmd/testdata/configurations/gnmi/expected.yaml
+++ b/cmd/testdata/configurations/gnmi/expected.yaml
@@ -1,27 +1,27 @@
 ---
 paths:
-  inlet.0.metadata.provider:
-    type: gnmi
-    timeout: "1s"
-    minimalrefreshinterval: "1m0s"
-    ports:
-      ::/0: 9339
-    targets: {}
-    settarget: {}
-    authenticationparameters: {}
-    models:
-      - name: custom
-        ifindexpaths:
-          - /some/path
-        ifdescriptionpaths:
-          - /some/other/path
-        ifnamekeys: []
-        ifnamepaths:
-          - /something
-        ifspeedpaths:
-          - path: /path1
-            unit: mbps
-          - path: /path2
-            unit: ethernet
-        systemnamepaths:
-          - /another/path
+  inlet.0.metadata.providers:
+    - type: gnmi
+      timeout: "1s"
+      minimalrefreshinterval: "1m0s"
+      ports:
+        ::/0: 9339
+      targets: {}
+      settarget: {}
+      authenticationparameters: {}
+      models:
+        - name: custom
+          ifindexpaths:
+            - /some/path
+          ifdescriptionpaths:
+            - /some/other/path
+          ifnamekeys: []
+          ifnamepaths:
+            - /something
+          ifspeedpaths:
+            - path: /path1
+              unit: mbps
+            - path: /path2
+              unit: ethernet
+          systemnamepaths:
+            - /another/path
diff --git a/cmd/testdata/configurations/gnmi/in.yaml b/cmd/testdata/configurations/gnmi/in.yaml
index 50aeb627..7898e283 100644
--- a/cmd/testdata/configurations/gnmi/in.yaml
+++ b/cmd/testdata/configurations/gnmi/in.yaml
@@ -1,16 +1,16 @@
 ---
 inlet:
   metadata:
-    provider:
-      type: gnmi
-      models:
-        - name: custom
-          if-index-paths: /some/path
-          if-description-paths: /some/other/path
-          if-name-paths: /something
-          if-speed-paths:
-            - path: /path1
-              unit: mbps
-            - path: /path2
-              unit: ethernet
-          system-name-paths: /another/path
+    providers:
+      - type: gnmi
+        models:
+          - name: custom
+            if-index-paths: /some/path
+            if-description-paths: /some/other/path
+            if-name-paths: /something
+            if-speed-paths:
+              - path: /path1
+                unit: mbps
+              - path: /path2
+                unit: ethernet
+            system-name-paths: /another/path
diff --git a/cmd/testdata/configurations/snmp-communities-migration/expected.yaml b/cmd/testdata/configurations/snmp-communities-migration/expected.yaml
index cdd7a511..ddfe40a4 100644
--- a/cmd/testdata/configurations/snmp-communities-migration/expected.yaml
+++ b/cmd/testdata/configurations/snmp-communities-migration/expected.yaml
@@ -1,13 +1,13 @@
 ---
 paths:
-  inlet.0.metadata.provider:
-    type: snmp
-    pollerretries: 1
-    pollertimeout: 1s
-    communities:
-      ::/0: [yopla]
-      203.0.113.0/24: [yopli]
-    securityparameters: {}
-    agents: {}
-    ports:
-      ::/0: 161
+  inlet.0.metadata.providers:
+    - type: snmp
+      pollerretries: 1
+      pollertimeout: 1s
+      communities:
+        ::/0: [yopla]
+        203.0.113.0/24: [yopli]
+      securityparameters: {}
+      agents: {}
+      ports:
+        ::/0: 161
diff --git a/cmd/testdata/configurations/snmp-ports/expected.yaml b/cmd/testdata/configurations/snmp-ports/expected.yaml
index fa9ba457..e6c8b042 100644
--- a/cmd/testdata/configurations/snmp-ports/expected.yaml
+++ b/cmd/testdata/configurations/snmp-ports/expected.yaml
@@ -1,4 +1,4 @@
 ---
 paths:
-  inlet.0.metadata.provider.ports:
+  inlet.0.metadata.providers.0.ports:
     ::/0: 1611
diff --git a/cmd/testdata/configurations/snmp-to-metadata/expected.yaml b/cmd/testdata/configurations/snmp-to-metadata/expected.yaml
index aedc96d0..1e91d834 100644
--- a/cmd/testdata/configurations/snmp-to-metadata/expected.yaml
+++ b/cmd/testdata/configurations/snmp-to-metadata/expected.yaml
@@ -7,14 +7,14 @@ paths:
     cacherefresh: 30m0s
     cachecheckinterval: 2m0s
     cachepersistfile: ""
-    provider:
-      type: snmp
-      pollerretries: 3
-      pollertimeout: 1s
-      agents:
-        192.0.2.10: 192.0.2.11
-      communities:
-        ::/0: [private]
-      ports:
-        ::/0: 161
-      securityparameters: {}
+    providers:
+      - type: snmp
+        pollerretries: 3
+        pollertimeout: 1s
+        agents:
+          192.0.2.10: 192.0.2.11
+        communities:
+          ::/0: [private]
+        ports:
+          ::/0: 161
+        securityparameters: {}
diff --git a/inlet/metadata/root_test.go b/inlet/metadata/root_test.go
index 7f893312..2f691ac5 100644
--- a/inlet/metadata/root_test.go
+++ b/inlet/metadata/root_test.go
@@ -17,6 +17,7 @@ import (
 	"akvorado/common/helpers"
 	"akvorado/common/reporter"
 	"akvorado/inlet/metadata/provider"
+	"akvorado/inlet/metadata/provider/static"
 )
 
 func expectMockLookup(t *testing.T, c *Component, exporter string, ifIndex uint, expected provider.Answer) {
@@ -156,7 +157,7 @@ func TestConfigCheck(t *testing.T) {
 		configuration.CacheDuration = 10 * time.Minute
 		configuration.CacheRefresh = 5 * time.Minute
 		configuration.CacheCheckInterval = time.Minute
-		configuration.Provider.Config = mockProviderConfiguration{}
+		configuration.Providers = []ProviderConfiguration{{Config: mockProviderConfiguration{}}}
 		if _, err := New(reporter.NewMock(t), configuration, Dependencies{Daemon: daemon.NewMock(t)}); err == nil {
 			t.Fatal("New() should trigger an error")
 		}
@@ -166,7 +167,7 @@ func TestConfigCheck(t *testing.T) {
 		configuration.CacheDuration = 10 * time.Minute
 		configuration.CacheRefresh = 15 * time.Minute
 		configuration.CacheCheckInterval = 12 * time.Minute
-		configuration.Provider.Config = mockProviderConfiguration{}
+		configuration.Providers = []ProviderConfiguration{{Config: mockProviderConfiguration{}}}
 		if _, err := New(reporter.NewMock(t), configuration, Dependencies{Daemon: daemon.NewMock(t)}); err == nil {
 			t.Fatal("New() should trigger an error")
 		}
@@ -176,7 +177,7 @@ func TestConfigCheck(t *testing.T) {
 		configuration.CacheDuration = 10 * time.Minute
 		configuration.CacheRefresh = 0
 		configuration.CacheCheckInterval = 2 * time.Minute
-		configuration.Provider.Config = mockProviderConfiguration{}
+		configuration.Providers = []ProviderConfiguration{{Config: mockProviderConfiguration{}}}
 		if _, err := New(reporter.NewMock(t), configuration, Dependencies{Daemon: daemon.NewMock(t)}); err != nil {
 			t.Fatalf("New() error:\n%+v", err)
 		}
@@ -216,7 +217,7 @@ func TestProviderBreaker(t *testing.T) {
 			r := reporter.NewMock(t)
 			configuration := DefaultConfiguration()
 			configuration.MaxBatchRequests = 0
-			configuration.Provider.Config = tc.ProviderConfiguration
+			configuration.Providers = []ProviderConfiguration{{Config: tc.ProviderConfiguration}}
 			c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
 			c.metrics.providerBreakerOpenCount.WithLabelValues("127.0.0.1").Add(0)
 
@@ -263,7 +264,7 @@ func TestBatching(t *testing.T) {
 	r := reporter.NewMock(t)
 	t.Run("run", func(t *testing.T) {
 		configuration := DefaultConfiguration()
-		configuration.Provider.Config = &bcp
+		configuration.Providers = []ProviderConfiguration{{Config: &bcp}}
 		c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
 
 		// Block dispatcher
@@ -284,21 +285,105 @@ func TestBatching(t *testing.T) {
 		c.Lookup(c.d.Clock.Now(), netip.MustParseAddr("::ffff:127.0.0.1"), 769)
 	})
 
-	gotMetrics := r.GetMetrics("akvorado_inlet_metadata_provider_", "batched_requests_total")
-	expectedMetrics := map[string]string{
-		`batched_requests_total`: "4",
+	t.Run("check", func(t *testing.T) {
+		gotMetrics := r.GetMetrics("akvorado_inlet_metadata_provider_", "batched_requests_total")
+		expectedMetrics := map[string]string{
+			`batched_requests_total`: "4",
+		}
+		if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
+			t.Errorf("Metrics (-got, +want):\n%s", diff)
+		}
+
+		expectedAccepted := []provider.BatchQuery{
+			{
+				ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
+				IfIndexes:  []uint{766, 767, 768, 769},
+			},
+		}
+		if diff := helpers.Diff(bcp.received, expectedAccepted); diff != "" {
+			t.Errorf("Accepted requests (-got, +want):\n%s", diff)
+		}
+	})
+}
+
+func TestMultipleProviders(t *testing.T) {
+	r := reporter.NewMock(t)
+	staticConfiguration1 := static.Configuration{
+		Exporters: helpers.MustNewSubnetMap(map[string]static.ExporterConfiguration{
+			"2001:db8:1::/48": {
+				Exporter: provider.Exporter{
+					Name: "static1",
+				},
+				IfIndexes: map[uint]provider.Interface{
+					10: {
+						Name:        "Gi10",
+						Description: "10th interface",
+						Speed:       1000,
+					},
+					11: {
+						Name:        "Gi11",
+						Description: "11th interface",
+						Speed:       1000,
+					},
+				},
+			},
+		}),
 	}
-	if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
-		t.Errorf("Metrics (-got, +want):\n%s", diff)
+	staticConfiguration2 := static.Configuration{
+		Exporters: helpers.MustNewSubnetMap(map[string]static.ExporterConfiguration{
+			"2001:db8:2::/48": {
+				Exporter: provider.Exporter{
+					Name: "static2",
+				},
+				IfIndexes: map[uint]provider.Interface{
+					12: {
+						Name:        "Gi12",
+						Description: "12th interface",
+						Speed:       1000,
+					},
+					13: {
+						Name:        "Gi13",
+						Description: "13th interface",
+						Speed:       1000,
+					},
+				},
+			},
+		}),
 	}
-
-	expectedAccepted := []provider.BatchQuery{
+	configuration := DefaultConfiguration()
+	configuration.Providers = []ProviderConfiguration{
+		{Config: staticConfiguration1},
+		{Config: staticConfiguration2},
+	}
+	c := NewMock(t, r, configuration, Dependencies{Daemon: daemon.NewMock(t)})
+	c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:1::1"), 10)
+	c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:2::2"), 12)
+	time.Sleep(30 * time.Millisecond)
+	got1, _ := c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:1::1"), 10)
+	got2, _ := c.Lookup(time.Now(), netip.MustParseAddr("2001:db8:2::2"), 12)
+	got := []provider.Answer{got1, got2}
+	expected := []provider.Answer{
 		{
-			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
-			IfIndexes:  []uint{766, 767, 768, 769},
+			Exporter: provider.Exporter{
+				Name: "static1",
+			},
+			Interface: provider.Interface{
+				Name:        "Gi10",
+				Description: "10th interface",
+				Speed:       1000,
+			},
+		}, {
+			Exporter: provider.Exporter{
+				Name: "static2",
+			},
+			Interface: provider.Interface{
+				Name:        "Gi12",
+				Description: "12th interface",
+				Speed:       1000,
+			},
 		},
 	}
-	if diff := helpers.Diff(bcp.received, expectedAccepted); diff != "" {
-		t.Errorf("Accepted requests (-got, +want):\n%s", diff)
+	if diff := helpers.Diff(got, expected); diff != "" {
+		t.Fatalf("Lookup() (-got, +want):\n%s", diff)
 	}
 }
diff --git a/inlet/metadata/tests.go b/inlet/metadata/tests.go
index 7aed6207..7d8ad4a3 100644
--- a/inlet/metadata/tests.go
+++ b/inlet/metadata/tests.go
@@ -71,8 +71,8 @@ func (mpc mockProviderConfiguration) New(_ *reporter.Reporter, put func(provider
 // NewMock creates a new metadata component building synthetic values. It is already started.
 func NewMock(t *testing.T, reporter *reporter.Reporter, configuration Configuration, dependencies Dependencies) *Component {
 	t.Helper()
-	if configuration.Provider.Config == nil {
-		configuration.Provider.Config = mockProviderConfiguration{}
+	if configuration.Providers == nil {
+		configuration.Providers = []ProviderConfiguration{{Config: mockProviderConfiguration{}}}
 	}
 	c, err := New(reporter, configuration, dependencies)
 	if err != nil {