Skip to content
Open
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
81 changes: 81 additions & 0 deletions docs/datagen.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,84 @@ For generators that need the full identity hierarchy (e.g., Windows Event Log ge
user := env.Users[r.Intn(len(env.Users))]
system := env.Systems[r.Intn(len(env.Systems))]
```

## Appliance Identities

Alongside the general-purpose `SystemIdentity`, datagen models purpose-built
appliances that run an embedded `ApplianceOS` rather than a general-purpose OS.
The classes modeled so far are storage arrays (`StorageSystemIdentity`) and
network hardware (`NetworkSystemIdentity`); further appliance classes follow the
same pattern (identity struct, vendor pools, `Validate()`, and Environment
composition).

### `ApplianceOS` taxonomy (`appliance.go`)

Appliances ship closed-source embedded OSes (NimbleOS, PAN-OS, NX-OS, …) that
are not "Linux" from any consumer's perspective — different kernel, management
plane, lifecycle, and telemetry. They are tracked separately from the
general-purpose `OSType` enum via an `ApplianceOS{Vendor, Family, Version}`
triple.

The family and version strings, and the OS self-report rendered by
`ApplianceOS.String()`, are the **real forms a device reports**, so generated
records parse the way genuine device output does:

| Family | `String()` self-report |
|--------|------------------------|
| NimbleOS | `NimbleOS 6.1.2.0` |
| HPE 3PAR OS | `HPE 3PAR OS 3.3.1.410` |
| BIG-IP | `BIG-IP 17.1.0.3` |
| Cisco IOS XE | `Cisco IOS XE Software, Version 17.12.3` |
| Cisco NX-OS | `Cisco Nexus Operating System (NX-OS) Software, Version 10.3(4a)` |
| Arista EOS | `Arista EOS 4.31.2F` |
| Junos | `Junos: 23.4R1` |
| PAN-OS | `PAN-OS 11.1.3` |
| FortiOS | `FortiOS v7.4.3` |

Each family maps to exactly one vendor, so vendor/family coherence is
structural: a NimbleOS is always HPE and can never carry another vendor's
family.

### `StorageSystemIdentity` (`storage.go`)

A first-class storage array: vendor/model/serial, an `ApplianceOS`, storage
fabric identifiers (`WWN`, `WWPN[]`, `NAA`, `IQN`), a capacity model
(raw/usable/effective plus data-reduction ratio), and hardware inventory
(controllers, shelves, drives). The first vendor pool covers HPE Nimble, 3PAR,
Alletra, and StoreOnce. `Validate()` checks fabric-ID formats and capacity
sanity.

### `NetworkSystemIdentity` (`network_appliance.go`)

A first-class network device composed from capability facets — any of
`L2SwitchingCapability`, `L3RoutingCapability`, `FirewallCapability`,
`LoadBalancingCapability`, `WirelessCapability` (a nil facet means the device
lacks that capability). Real products compose facets: a Catalyst 9300 is L2 +
L3, a BIG-IP is load-balancing + firewall + L3, a PA-3220 is firewall + L3. The
first vendor pool spans F5, Cisco (IOS-XE and NX-OS), Arista, Juniper, Palo
Alto, and Fortinet. `Validate()` requires vendor/OS coherence and at least one
facet.

### Appliance hostnames

`StyleAppliance` produces hostnames like `nimble-core-east-01`, combining a
vendor short-code with a role (`ApplianceRoles`) and site (`ApplianceSites`).

### Environment composition

`GenerateEnvironment` composes `StorageSystems` and `NetworkSystems` alongside
the general-purpose `Systems`, each driven by its own seed
(`SeedConfig.StorageSystems` / `SeedConfig.NetworkSystems`, both falling back to
`Shared` when negative). Counts come from `EnvironmentOpts.StorageSystemCount`
(default 2) and `NetworkSystemCount` (default 4). Each appliance's management
interface is bound to the environment's management subnet.

```go
env := datagen.GenerateEnvironment(seeds, &datagen.EnvironmentOpts{
StorageSystemCount: 3,
NetworkSystemCount: 5,
})
for _, array := range env.AllStorageSystems() {
fmt.Println(array.Model, array.OS) // e.g. "Nimble AF40 NimbleOS 6.1.2.0"
}
```
152 changes: 152 additions & 0 deletions internal/datagen/appliance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package datagen

import (
"fmt"
"math/rand"
"regexp"
)

// ApplianceVendor identifies the maker of an embedded/appliance operating
// system, spelled as the vendor is named in device telemetry. Appliance
// vendors ship closed-source OSes on storage arrays and network hardware that
// are not "Linux" from any consumer's perspective, so they are tracked
// separately from datagen's general-purpose OSType.
type ApplianceVendor string

const (
VendorHPE ApplianceVendor = "HPE"
VendorF5 ApplianceVendor = "F5"
VendorCisco ApplianceVendor = "Cisco"
VendorArista ApplianceVendor = "Arista"
VendorJuniper ApplianceVendor = "Juniper"
VendorPaloAlto ApplianceVendor = "Palo Alto Networks"
VendorFortinet ApplianceVendor = "Fortinet"
)

// ApplianceOSFamily identifies the OS family running on an appliance, spelled
// as the device reports it (e.g. via "show version" / SNMP sysDescr / vendor
// API). Each family maps to exactly one vendor (see applianceOSVendor).
type ApplianceOSFamily string

const (
FamilyNimbleOS ApplianceOSFamily = "NimbleOS" // HPE Nimble arrays
Family3PAROS ApplianceOSFamily = "HPE 3PAR OS" // HPE 3PAR arrays
FamilyAlletraOS ApplianceOSFamily = "Array OS" // HPE Alletra 6000 (NimbleOS lineage)
FamilyStoreOnceOS ApplianceOSFamily = "HPE StoreOnce" // HPE StoreOnce backup appliances
FamilyBIGIP ApplianceOSFamily = "BIG-IP" // F5 (TMOS is the underlying OS; devices report "BIG-IP")
FamilyIOSXE ApplianceOSFamily = "Cisco IOS XE" // Cisco Catalyst / IOS-XE routers & switches
FamilyNXOS ApplianceOSFamily = "Cisco NX-OS" // Cisco Nexus data-center switches
FamilyEOS ApplianceOSFamily = "Arista EOS" // Arista switches
FamilyJunos ApplianceOSFamily = "Junos" // Juniper routers, switches, SRX firewalls
FamilyPANOS ApplianceOSFamily = "PAN-OS" // Palo Alto Networks NGFWs
FamilyFortiOS ApplianceOSFamily = "FortiOS" // Fortinet FortiGate NGFWs
)

// ApplianceOS is the {Vendor, Family, Version} triple describing the embedded
// OS an appliance runs, e.g. {HPE, NimbleOS, 6.1.2.0} or {F5, BIG-IP,
// 17.1.0.3}. String renders the OS the way the device itself reports it.
type ApplianceOS struct {
Vendor ApplianceVendor
Family ApplianceOSFamily
Version string
}

// applianceVersionRE matches a dotted version with a leading numeric segment
// and at least one more dot-separated segment. It accepts the real formats
// appliance OSes report: pure-numeric (NimbleOS "6.1.2.0", IOS-XE "17.12.3"),
// letter-suffixed (EOS "4.31.2F", Junos "23.4R1"), and parenthesized builds
// (NX-OS "10.3(4a)"), while rejecting non-versions like "latest" and
// single-segment values like "6".
var applianceVersionRE = regexp.MustCompile(`^\d+(\.[0-9A-Za-z()\-]+)+$`)

// applianceOSSelfReportFmt overrides how specific families render their OS
// self-report. Families not listed fall back to "<Family> <Version>", which is
// already correct for NimbleOS, BIG-IP, PAN-OS, Arista EOS, and the HPE storage
// families. The overrides capture the vendor's exact phrasing.
var applianceOSSelfReportFmt = map[ApplianceOSFamily]string{
FamilyIOSXE: "Cisco IOS XE Software, Version %s",
FamilyNXOS: "Cisco Nexus Operating System (NX-OS) Software, Version %s",
FamilyJunos: "Junos: %s",
FamilyFortiOS: "FortiOS v%s",
}

// String renders the OS the way the device reports it, e.g. "NimbleOS 6.1.2.0",
// "Junos: 23.4R1", or "FortiOS v7.4.3".
func (a ApplianceOS) String() string {
if f, ok := applianceOSSelfReportFmt[a.Family]; ok {
return fmt.Sprintf(f, a.Version)
}
return fmt.Sprintf("%s %s", a.Family, a.Version)
}

// Validate reports whether the ApplianceOS is well-formed: non-empty vendor,
// family, and a dotted version in one of the real appliance formats. Returns an
// error rather than panicking, per the datagen error-return convention
// (PIPE-1003).
func (a ApplianceOS) Validate() error {
if a.Vendor == "" {
return fmt.Errorf("appliance OS vendor must not be empty")
}
if a.Family == "" {
return fmt.Errorf("appliance OS family must not be empty")
}
if a.Version == "" {
return fmt.Errorf("appliance OS version must not be empty")
}
if !applianceVersionRE.MatchString(a.Version) {
return fmt.Errorf("appliance OS version %q is not a recognized version format", a.Version)
}
return nil
}

// applianceOSVendor maps each OS family to its one true vendor. This is what
// makes vendor/family coherence structural: a NimbleOS is always HPE.
var applianceOSVendor = map[ApplianceOSFamily]ApplianceVendor{
FamilyNimbleOS: VendorHPE,
Family3PAROS: VendorHPE,
FamilyAlletraOS: VendorHPE,
FamilyStoreOnceOS: VendorHPE,
FamilyBIGIP: VendorF5,
FamilyIOSXE: VendorCisco,
FamilyNXOS: VendorCisco,
FamilyEOS: VendorArista,
FamilyJunos: VendorJuniper,
FamilyPANOS: VendorPaloAlto,
FamilyFortiOS: VendorFortinet,
}

// applianceOSVersions holds real published version strings per family, in the
// format the device reports (note NX-OS's parenthesized build and the
// letter-suffixed network-OS releases). Representative, not exhaustive.
var applianceOSVersions = map[ApplianceOSFamily][]string{
FamilyNimbleOS: {"6.1.2.0", "6.1.1.100", "6.0.0.400", "5.3.1.0"},
Family3PAROS: {"3.3.1.410", "3.3.1.485", "3.3.1.215"},
FamilyAlletraOS: {"6.1.2.502", "6.1.2.400", "6.0.0.900"},
FamilyStoreOnceOS: {"4.3.13", "4.3.9", "4.2.3"},
FamilyBIGIP: {"17.1.0.3", "16.1.4", "15.1.10.2"},
FamilyIOSXE: {"17.12.3", "17.9.4", "17.6.5"},
FamilyNXOS: {"10.3(4a)", "10.2(5)", "9.3(12)"},
FamilyEOS: {"4.31.2F", "4.30.4M", "4.29.6M"},
FamilyJunos: {"23.4R1", "22.4R3", "21.4R3"},
FamilyPANOS: {"11.1.3", "11.0.4", "10.2.9"},
FamilyFortiOS: {"7.4.3", "7.2.8", "7.0.14"},
}

// GenerateApplianceOS returns a valid ApplianceOS for the given family with a
// random real version drawn from that family's version pool. The vendor is
// derived from the family, so a NimbleOS result is always HPE and can never
// carry another vendor's family. An unknown family yields a zero vendor and a
// "0.0" placeholder version, which Validate rejects — callers should pass a
// known family constant.
func GenerateApplianceOS(r *rand.Rand, family ApplianceOSFamily) ApplianceOS {
versions := applianceOSVersions[family]
version := "0.0"
if len(versions) > 0 {
version = versions[r.Intn(len(versions))] // #nosec G404
}
return ApplianceOS{
Vendor: applianceOSVendor[family],
Family: family,
Version: version,
}
}
94 changes: 94 additions & 0 deletions internal/datagen/appliance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package datagen

import (
"math/rand"
"testing"
)

func TestApplianceOS_String(t *testing.T) {
tests := []struct {
os ApplianceOS
want string
}{
{ApplianceOS{VendorHPE, FamilyNimbleOS, "6.1.2.0"}, "NimbleOS 6.1.2.0"},
{ApplianceOS{VendorHPE, FamilyStoreOnceOS, "4.3.13"}, "HPE StoreOnce 4.3.13"},
{ApplianceOS{VendorF5, FamilyBIGIP, "17.1.0.3"}, "BIG-IP 17.1.0.3"},
{ApplianceOS{VendorCisco, FamilyIOSXE, "17.12.3"}, "Cisco IOS XE Software, Version 17.12.3"},
{ApplianceOS{VendorCisco, FamilyNXOS, "10.3(4a)"}, "Cisco Nexus Operating System (NX-OS) Software, Version 10.3(4a)"},
{ApplianceOS{VendorJuniper, FamilyJunos, "23.4R1"}, "Junos: 23.4R1"},
{ApplianceOS{VendorFortinet, FamilyFortiOS, "7.4.3"}, "FortiOS v7.4.3"},
{ApplianceOS{VendorPaloAlto, FamilyPANOS, "11.1.3"}, "PAN-OS 11.1.3"},
}
for _, tt := range tests {
if got := tt.os.String(); got != tt.want {
t.Errorf("String() = %q, want %q", got, tt.want)
}
}
}

func TestApplianceOS_Validate(t *testing.T) {
tests := []struct {
name string
os ApplianceOS
wantErr bool
}{
{"valid", ApplianceOS{VendorHPE, FamilyNimbleOS, "6.1.2.0"}, false},
{"valid three-segment", ApplianceOS{VendorCisco, FamilyIOSXE, "17.12.3"}, false},
{"valid vendor-suffixed", ApplianceOS{VendorJuniper, FamilyJunos, "23.4R1"}, false},
{"missing vendor", ApplianceOS{"", FamilyNimbleOS, "6.1.2.0"}, true},
{"missing family", ApplianceOS{VendorHPE, "", "6.1.2.0"}, true},
{"missing version", ApplianceOS{VendorHPE, FamilyNimbleOS, ""}, true},
{"non-numeric version", ApplianceOS{VendorHPE, FamilyNimbleOS, "latest"}, true},
{"single-segment version", ApplianceOS{VendorHPE, FamilyNimbleOS, "6"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.os.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

// knownApplianceFamilies is every family GenerateApplianceOS must support.
var knownApplianceFamilies = []ApplianceOSFamily{
FamilyNimbleOS, Family3PAROS, FamilyAlletraOS, FamilyStoreOnceOS,
FamilyBIGIP, FamilyIOSXE, FamilyNXOS, FamilyEOS, FamilyJunos, FamilyPANOS, FamilyFortiOS,
}

func TestGenerateApplianceOS_AllFamiliesValid(t *testing.T) {
r := rand.New(rand.NewSource(42))
for _, f := range knownApplianceFamilies {
os := GenerateApplianceOS(r, f)
if err := os.Validate(); err != nil {
t.Errorf("GenerateApplianceOS(%q) produced invalid OS %v: %v", f, os, err)
}
if os.Family != f {
t.Errorf("GenerateApplianceOS(%q) family = %q, want %q", f, os.Family, f)
}
if os.Vendor == "" {
t.Errorf("GenerateApplianceOS(%q) has empty vendor", f)
}
}
}

func TestGenerateApplianceOS_Deterministic(t *testing.T) {
a := GenerateApplianceOS(rand.New(rand.NewSource(7)), FamilyNimbleOS)
b := GenerateApplianceOS(rand.New(rand.NewSource(7)), FamilyNimbleOS)
if a != b {
t.Errorf("same seed produced different results: %v vs %v", a, b)
}
}

func TestGenerateApplianceOS_VendorCoherence(t *testing.T) {
// A family always resolves to its one true vendor; NimbleOS is HPE and can
// never drift to F5's tmos.
os := GenerateApplianceOS(rand.New(rand.NewSource(1)), FamilyNimbleOS)
if os.Vendor != VendorHPE {
t.Errorf("NimbleOS vendor = %q, want %q", os.Vendor, VendorHPE)
}
if os.Family == FamilyBIGIP {
t.Error("NimbleOS family drifted to BIG-IP")
}
}
Loading
Loading