Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions internal/config/example_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@ agent:
# WarnWeakCrypto: no-pq-kex
# IdentityFile:
# - ~/.ssh/work-ssh-key.pub
# firewall-nodes:
# # Devices whose NetBox name has no domain, such as fw01, become
# # fw01.fw.example.net. Applies only when one suffix is listed.
# domain_suffix:
# - .fw.example.net
# auth:
# mode: password
# credential_provider: op-work
# password_ref: op://Work/network-admin/password
# username: netops
# match:
# manufacturer:
# - Palo Alto
# role:
# - Firewall Cluster Node
# status:
# - active
# hosts:
# legacy-switch.example.net:
# aliases:
Expand Down
53 changes: 43 additions & 10 deletions internal/inventory/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,11 @@ func Reconcile(objects []Object, selectors []config.InventoryGroupSelector, prov
plan.Unmatched = append(plan.Unmatched, *obj)
continue
case 1:
selector := matches[0]
host := objectToProviderHost(obj, selector.Group)
host.AuthMode = groupAuthMode(selector.Group, groups)
host := groupProviderHost(obj, matches[0].Group, groups)
desired[host.ObjectID] = host
default:
if selector, ok := BestGroupSelectorMatch(obj, matches); ok {
host := objectToProviderHost(obj, selector.Group)
host.AuthMode = groupAuthMode(selector.Group, groups)
host := groupProviderHost(obj, selector.Group, groups)
desired[host.ObjectID] = host
continue
}
Expand Down Expand Up @@ -91,13 +88,49 @@ func selectorGroups(selectors []config.InventoryGroupSelector) []string {
return groups
}

func groupProviderHost(obj *Object, group string, groups map[string]config.GroupConfig) *ProviderHost {
groupCfg := groupConfig(group, groups)
qualified := qualifyShortObject(*obj, groupCfg.DomainSuffix)
host := objectToProviderHost(&qualified, group)
host.AuthMode = groupAuthMode(group, groups)
return host
}

func groupConfig(group string, groups map[string]config.GroupConfig) config.GroupConfig {
_, shortGroup, err := config.ParseInventoryGroupID(group)
if err != nil {
shortGroup = group
}
return groups[shortGroup]
}

// qualifyShortObject appends a group's single domain suffix to an object whose
// name has no domain and whose provider supplied no other address, so short
// provider names become resolvable FQDN hosts.
func qualifyShortObject(obj Object, suffixes []string) Object {
if len(suffixes) != 1 {
return obj
}
suffix := strings.TrimSpace(suffixes[0])
name := strings.TrimSpace(obj.Name)
if suffix == "" || name == "" || strings.Contains(name, ".") {
return obj
}
if host := strings.TrimSpace(obj.HostName); host != "" && host != name {
return obj
}
if !strings.HasPrefix(suffix, ".") {
suffix = "." + suffix
}
obj.Name = name + suffix
obj.FQDN = obj.Name
obj.HostName = obj.Name
return obj
}

func groupAuthMode(group string, groups map[string]config.GroupConfig) string {
if groups != nil {
_, shortGroup, err := config.ParseInventoryGroupID(group)
if err != nil {
shortGroup = group
}
auth := groups[shortGroup].Auth
auth := groupConfig(group, groups).Auth
auth.Normalize()
if auth.Mode != "" {
return auth.Mode
Expand Down
43 changes: 43 additions & 0 deletions internal/inventory/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,46 @@ func TestReconcileChoosesMostSpecificWildcardDomainSuffixSelector(t *testing.T)
t.Fatalf("group = %q, want netbox-prod/ldap", plan.Adds[0].Group)
}
}

func TestReconcileQualifiesShortNamesWithGroupDomainSuffix(t *testing.T) {
selectors := []config.InventoryGroupSelector{{
Group: "netbox-prod/vpan",
Provider: "netbox-prod",
Match: config.InventoryMatch{"manufacturer": {"Palo Alto"}},
}}
groups := map[string]config.GroupConfig{"vpan": {DomainSuffix: []string{".custcbb.local"}}}
attrs := map[string][]string{"manufacturer": {"Palo Alto"}}
objects := []Object{
{ObjectID: "1", Name: "ACME-vPAN01", HostName: "ACME-vPAN01", Attributes: attrs},
{ObjectID: "2", Name: "acme-vpan.expedient.com", HostName: "acme-vpan.expedient.com", Attributes: attrs},
{ObjectID: "3", Name: "lab-vpan", HostName: "10.0.0.5", Attributes: attrs},
}

plan := Reconcile(objects, selectors, "netbox-prod", nil, groups)
got := make(map[string]*ProviderHost)
for _, host := range plan.Adds {
got[host.ObjectID] = host
}
if host := got["1"]; host.Host != "ACME-vPAN01.custcbb.local" || host.HostName != "ACME-vPAN01.custcbb.local" ||
!slices.Equal(host.Patterns, []string{"ACME-vPAN01.custcbb.local", "ACME-vPAN01"}) {
t.Fatalf("short host = %+v", host)
}
if host := got["2"]; host.HostName != "acme-vpan.expedient.com" {
t.Fatalf("dotted host changed: %+v", host)
}
if host := got["3"]; host.Host != "lab-vpan" || host.HostName != "10.0.0.5" {
t.Fatalf("addressed host changed: %+v", host)
}
}

func TestReconcileLeavesShortNamesWithoutSingleGroupDomainSuffix(t *testing.T) {
selectors := []config.InventoryGroupSelector{{Group: "netbox-prod/fw", Provider: "netbox-prod"}}
objects := []Object{{ObjectID: "1", Name: "fw01", HostName: "fw01"}}
for _, suffixes := range [][]string{nil, {".a.local", ".b.local"}} {
groups := map[string]config.GroupConfig{"fw": {DomainSuffix: suffixes}}
plan := Reconcile(objects, selectors, "netbox-prod", nil, groups)
if len(plan.Adds) != 1 || plan.Adds[0].HostName != "fw01" {
t.Fatalf("suffixes %v: adds = %+v", suffixes, plan.Adds)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ Current external providers:
`jump_host`.

Provider-owned group selectors assign discovered objects to canonical groups.
A provider group's top-level `domain_suffix` qualifies short object names:
when it lists exactly one suffix, an object assigned to that group whose name
has no domain and no separate address becomes `<name><suffix>`, with the short
name kept as a lookup name. Selection still uses the object's original name, so
match such groups on fields other than `match.domain_suffix`.
When resolved inventory auth mode is `password`, nssh renders OpenSSH options
that force password-style authentication:
`PreferredAuthentications=keyboard-interactive,password` and
Expand Down
Loading