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
43 changes: 43 additions & 0 deletions docs/datagen.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,49 @@ user := env.Users[r.Intn(len(env.Users))]
system := env.Systems[r.Intn(len(env.Systems))]
```

## OS Types (`OSType`)

A `SystemIdentity` carries an `OSType`: the operating system of a real OS host,
as opposed to the embedded `ApplianceOS` a purpose-built appliance runs (see
below).

### Real vs appliance boundary

The test is **installability, not openness**. A real OS installs on supported
hardware you choose; it belongs in `OSType` even when it is closed (Windows,
macOS, and ESXi all qualify). An appliance OS ships preinstalled on dedicated
hardware and is not separately installable, so it lives in `ApplianceOS`
(NimbleOS, PAN-OS, NX-OS, and the rest). ESXi is the boundary case that makes the
rule concrete: closed like an appliance, but you install it on hardware you pick,
so it is a real OS.

### The `OSType` set

`ParseOSType` gates the user-facing `os:` knob against this set; each value has a
coherent release pool, an OS-appropriate `host.id`, and a bespoke server-role
service roster.

| `OSType` | `os.type` (semconv) | Notes |
|----------|---------------------|-------|
| `linux` | `linux` | general-purpose Linux distros |
| `windows` | `windows` | |
| `macos` | `darwin` | |
| `esxi` | `esxi` | VMware ESXi, a real OS (closed, but installable) |
| `xen-dom0` | `linux` | Xen dom0 (XCP-ng / Citrix Hypervisor) |
| `nutanix-ahv` | `linux` | Nutanix AHV host |
| `openstack-kvm` | `linux` | OpenStack/KVM compute host |
| `aix` | `aix` | |
| `solaris` | `solaris` | Oracle Solaris |
| `freebsd` | `freebsd` | |
| `openbsd` | `openbsd` | |

The three hypervisor-host Linux flavors are distinct simulate-as selectors that
carry their real distro in `os.name`/`os.version` while reporting `os.type=linux`
(they are Linux under the hood). The Unix families are already valid semconv
`os.type` values, so they pass through unchanged. Random environment population
draws Linux and Windows hosts; the expanded OSes are selected by explicit
configuration.

## Appliance Identities

Alongside the general-purpose `SystemIdentity`, datagen models purpose-built
Expand Down
18 changes: 17 additions & 1 deletion internal/config/generator_hostmetrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,28 @@ func TestHostMetricsGeneratorConfig_Validate(t *testing.T) {
},
},
{
name: "invalid OS",
name: "valid OS esxi",
config: HostMetricsGeneratorConfig{
Workers: 1,
Rate: time.Second,
OS: "esxi",
},
},
{
name: "valid OS solaris",
config: HostMetricsGeneratorConfig{
Workers: 1,
Rate: time.Second,
OS: "solaris",
},
},
{
name: "invalid OS",
config: HostMetricsGeneratorConfig{
Workers: 1,
Rate: time.Second,
OS: "plan9",
},
wantErr: true,
errMsg: "unsupported OS",
},
Expand Down
81 changes: 78 additions & 3 deletions internal/datagen/osrelease.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,67 @@ var (
}
)

// osRelease is one authentic release for a PIPE-1260 OS: os.name / os.version /
// os.build_id / os.description, each an internally-consistent real value.
type osRelease struct{ name, version, buildID, description string }

// extendedReleases holds the authentic release pools for the OSType values added
// in PIPE-1260. The hypervisor-host Linux flavors carry their real distro in
// os.name and emit os.type=linux via SemconvOSType. Values are real product
// versions and build strings.
var extendedReleases = map[OSType][]osRelease{
OSESXi: {
{"VMware ESXi", "8.0.2", "22380479", "VMware ESXi 8.0 Update 2 (build 22380479)"},
{"VMware ESXi", "8.0.1", "21495797", "VMware ESXi 8.0 Update 1 (build 21495797)"},
{"VMware ESXi", "8.0.0", "20513097", "VMware ESXi 8.0 GA (build 20513097)"},
{"VMware ESXi", "7.0.3", "19193900", "VMware ESXi 7.0 Update 3c (build 19193900)"},
},
OSAIX: {
{"AIX", "7.3", "7300-02-02-2420", "AIX 7.3 TL2 SP2"},
{"AIX", "7.3", "7300-01-03-2346", "AIX 7.3 TL1 SP3"},
{"AIX", "7.2", "7200-05-07-2420", "AIX 7.2 TL5 SP7"},
},
OSSolaris: {
{"Oracle Solaris", "11.4", "11.4.54.0.1.135", "Oracle Solaris 11.4 SRU54"},
{"Oracle Solaris", "11.4", "11.4.42.0.1.111", "Oracle Solaris 11.4 SRU42"},
{"Oracle Solaris", "11.4", "11.4.60.0.1.146", "Oracle Solaris 11.4 SRU60"},
},
OSFreeBSD: {
{"FreeBSD", "14.1-RELEASE", "14.1-RELEASE-p6", "FreeBSD 14.1-RELEASE-p6"},
{"FreeBSD", "14.0-RELEASE", "14.0-RELEASE-p8", "FreeBSD 14.0-RELEASE-p8"},
{"FreeBSD", "13.3-RELEASE", "13.3-RELEASE-p5", "FreeBSD 13.3-RELEASE-p5"},
},
OSOpenBSD: {
{"OpenBSD", "7.5", "GENERIC.MP#82", "OpenBSD 7.5"},
{"OpenBSD", "7.4", "GENERIC.MP#4", "OpenBSD 7.4"},
{"OpenBSD", "7.3", "GENERIC.MP#1", "OpenBSD 7.3"},
},
OSXenDom0: {
{"XCP-ng", "8.3", "4.19.0+1", "XCP-ng 8.3"},
{"XCP-ng", "8.2.1", "4.19.0+1", "XCP-ng 8.2.1 LTS"},
},
OSNutanixAHV: {
{"Nutanix AHV", "20230302.103003", "5.10.149-2.nutanix.el8", "Nutanix AHV 20230302.103003"},
{"Nutanix AHV", "20220304.480", "5.10.149-1.nutanix.el7", "Nutanix AHV 20220304.480"},
},
OSOpenStackKVM: {
{"Red Hat Enterprise Linux", "9.4", "5.14.0-427.el9_4.x86_64", "Red Hat Enterprise Linux 9.4 (Plow)"},
{"Ubuntu", "22.04.4", "5.15.0-105-generic", "Ubuntu 22.04.4 LTS"},
},
}

// buildExtendedOSInfo projects an osRelease into an OSInfo carrying os as the
// os.type source. SemconvOSType handles the linux mapping for hypervisor hosts.
func buildExtendedOSInfo(os OSType, rel osRelease) OSInfo {
return OSInfo{
Type: os,
Name: rel.name,
Version: rel.version,
BuildID: rel.buildID,
Description: rel.description,
}
}

// GenerateOSInfo returns a coherent OSInfo for the given OS type, drawn from the
// authentic release pools. Windows picks one real UBR per selection.
// Deterministic for a given (r, os).
Expand All @@ -92,6 +153,9 @@ func GenerateOSInfo(r *rand.Rand, os OSType) OSInfo {
case OSMacOS:
return buildMacOSInfo(macReleases[r.Intn(len(macReleases))]) // #nosec G404
default:
if pool, ok := extendedReleases[os]; ok {
return buildExtendedOSInfo(os, pool[r.Intn(len(pool))]) // #nosec G404
}
return buildLinuxOSInfo(linuxReleases[r.Intn(len(linuxReleases))]) // #nosec G404
}
}
Expand All @@ -108,6 +172,9 @@ func osInfoForTier(r *rand.Rand, os OSType, older bool) OSInfo {
case OSMacOS:
return buildMacOSInfo(macReleases[pickHalfIndex(r, len(macReleases), older)])
default:
if pool, ok := extendedReleases[os]; ok {
return buildExtendedOSInfo(os, pool[pickHalfIndex(r, len(pool), older)])
}
return buildLinuxOSInfo(linuxReleases[r.Intn(len(linuxReleases))]) // #nosec G404
}
}
Expand Down Expand Up @@ -157,15 +224,23 @@ func buildLinuxOSInfo(rel linuxRelease) OSInfo {
}

// GenerateHostID returns an OS-appropriate host.id: a /etc/machine-id-style
// 32-char lowercase hex string on Linux, a registry MachineGuid-style GUID on
// Windows, and an uppercase IOPlatformUUID on macOS.
// 32-char lowercase hex string on Linux (and the hypervisor-host Linux flavors),
// a registry MachineGuid-style GUID on Windows, an uppercase IOPlatformUUID on
// macOS, a lowercase SMBIOS/system UUID on ESXi and the BSDs, an AIX
// uname-m-style machine id, and an 8-hex-digit hostid on Solaris.
func GenerateHostID(r *rand.Rand, os OSType) string {
h := randomHex(r, 16) // 32 lowercase hex chars
switch os {
case OSWindows:
case OSWindows, OSESXi, OSFreeBSD, OSOpenBSD:
return formatUUID(h)
case OSMacOS:
return strings.ToUpper(formatUUID(h))
case OSAIX:
// uname -m form: "00" + 6-hex serial + "4C00" model suffix.
return "00" + strings.ToUpper(h[:6]) + "4C00"
case OSSolaris:
// hostid: 8 lowercase hex digits.
return h[:8]
default:
return h
}
Expand Down
51 changes: 37 additions & 14 deletions internal/datagen/ostype.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,36 @@ import (
// internally and to the user, but a real OpenTelemetry pipeline stamps
// os.type=darwin, so that is the value emitted on records.

// simulatableOSes is the set of OSType values the fake-identity path can render
// a coherent host for. ParseOSType gates the user-facing `os:` knob against it.
var simulatableOSes = map[OSType]bool{
OSLinux: true, OSWindows: true, OSMacOS: true,
OSESXi: true, OSXenDom0: true, OSNutanixAHV: true, OSOpenStackKVM: true,
OSAIX: true, OSSolaris: true, OSFreeBSD: true, OSOpenBSD: true,
}

// osAliases maps accepted spellings to their canonical OSType.
var osAliases = map[string]OSType{
"darwin": OSMacOS,
"vmware": OSESXi,
"vmware-esxi": OSESXi,
"xen": OSXenDom0,
"ahv": OSNutanixAHV,
"kvm": OSOpenStackKVM,
}

// ParseOSType maps a user-supplied OS string to an OSType for the fake-identity
// path. It accepts the three simulate-able OSes, treating "darwin" as an alias
// for "macos". Unknown values return an error.
// path. It accepts every simulate-able OS plus a few aliases (e.g. "darwin" for
// macos, "vmware" for esxi). Unknown values return an error.
func ParseOSType(s string) (OSType, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "linux":
return OSLinux, nil
case "windows":
return OSWindows, nil
case "macos", "darwin":
return OSMacOS, nil
default:
return "", fmt.Errorf("datagen: unsupported OS %q (want one of: linux, windows, macos)", s)
key := strings.ToLower(strings.TrimSpace(s))
if alias, ok := osAliases[key]; ok {
return alias, nil
}
if os := OSType(key); simulatableOSes[os] {
return os, nil
}
return "", fmt.Errorf("datagen: unsupported OS %q (want one of: linux, windows, macos, esxi, xen-dom0, nutanix-ahv, openstack-kvm, aix, solaris, freebsd, openbsd)", s)
}

// OSTypeFromGOOS maps a runtime.GOOS value to an OSType for the real-host path.
Expand All @@ -53,10 +69,17 @@ func OSTypeFromGOOS(goos string) OSType {
}

// SemconvOSType returns the OpenTelemetry semantic-convention os.type value for
// o, which differs from the OSType constant only for macOS (macos -> darwin).
// o. It differs from the OSType constant for macOS (macos -> darwin) and for the
// hypervisor-host Linux flavors, which are Linux under the hood (-> linux). The
// Unix families (aix, solaris, freebsd, openbsd) are already valid semconv
// os.type values, so they pass through unchanged.
func (o OSType) SemconvOSType() string {
if o == OSMacOS {
switch {
case o == OSMacOS:
return "darwin"
case hypervisorHostLinux[o]:
return "linux"
default:
return string(o)
}
return string(o)
}
122 changes: 122 additions & 0 deletions internal/datagen/ostype_expansion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package datagen

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

// expandedOSes is the PIPE-1260 additions: ESXi, the hypervisor-host Linux
// flavors, and the Unix families. semconv is the os.type wire value each emits.
var expandedOSes = []struct {
os OSType
semconv string
name string // substring expected in os.name
}{
{OSESXi, "esxi", "ESXi"},
{OSXenDom0, "linux", "XCP-ng"},
{OSNutanixAHV, "linux", "Nutanix"},
{OSOpenStackKVM, "linux", ""}, // a real Linux distro; os.name is the distro
{OSAIX, "aix", "AIX"},
{OSSolaris, "solaris", "Solaris"},
{OSFreeBSD, "freebsd", "FreeBSD"},
{OSOpenBSD, "openbsd", "OpenBSD"},
}

func TestParseOSType_Expanded(t *testing.T) {
for _, e := range expandedOSes {
got, err := ParseOSType(string(e.os))
if err != nil {
t.Errorf("ParseOSType(%q): unexpected error: %v", e.os, err)
continue
}
if got != e.os {
t.Errorf("ParseOSType(%q) = %q, want %q", e.os, got, e.os)
}
}
}

func TestSemconvOSType_Expanded(t *testing.T) {
for _, e := range expandedOSes {
if got := e.os.SemconvOSType(); got != e.semconv {
t.Errorf("%q.SemconvOSType() = %q, want %q", e.os, got, e.semconv)
}
}
}

func TestGenerateOSInfo_Expanded(t *testing.T) {
for _, e := range expandedOSes {
info := GenerateOSInfo(rand.New(rand.NewSource(1)), e.os) // #nosec G404
if info.Type != e.os {
t.Errorf("%s: Type = %q, want %q", e.os, info.Type, e.os)
}
if info.Name == "" || info.Version == "" || info.BuildID == "" || info.Description == "" {
t.Errorf("%s: incomplete OSInfo: %+v", e.os, info)
}
if e.name != "" && !regexp.MustCompile(regexp.QuoteMeta(e.name)).MatchString(info.Name) {
t.Errorf("%s: os.name = %q, want to contain %q", e.os, info.Name, e.name)
}
// Deterministic for a given (seed, os).
again := GenerateOSInfo(rand.New(rand.NewSource(1)), e.os) // #nosec G404
if info != again {
t.Errorf("%s: GenerateOSInfo not deterministic: %+v vs %+v", e.os, info, again)
}
}
}

func TestGenerateHostID_Expanded(t *testing.T) {
uuidLower := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
linux32 := regexp.MustCompile(`^[0-9a-f]{32}$`)
aixRE := regexp.MustCompile(`^00[0-9A-F]{6}4C00$`)
solarisRE := regexp.MustCompile(`^[0-9a-f]{8}$`)

cases := map[OSType]*regexp.Regexp{
OSESXi: uuidLower,
OSFreeBSD: uuidLower,
OSOpenBSD: uuidLower,
OSXenDom0: linux32,
OSNutanixAHV: linux32,
OSOpenStackKVM: linux32,
OSAIX: aixRE,
OSSolaris: solarisRE,
}
for os, re := range cases {
id := GenerateHostID(rand.New(rand.NewSource(1)), os) // #nosec G404
if !re.MatchString(id) {
t.Errorf("%s host.id = %q, want match %s", os, id, re)
}
}
}

func TestGenerateServicesForSystem_Expanded(t *testing.T) {
contains := func(svcs []*ServiceIdentity, name string) bool {
for _, s := range svcs {
if s.Name == name {
return true
}
}
return false
}

// Every expanded OS in a server role gets its bespoke daemon set, which is
// larger than the one-service default fallback.
for _, e := range expandedOSes {
svcs := GenerateServicesForSystem(rand.New(rand.NewSource(1)), e.os, RoleServer, "host1") // #nosec G404
if len(svcs) < 3 {
t.Errorf("%s server services = %d, want a bespoke set (>=3), not the default fallback", e.os, len(svcs))
}
}

// The virtualization hosts must carry their signature virtualization daemon.
// These pools are small enough that the full set is always returned.
xen := GenerateServicesForSystem(rand.New(rand.NewSource(1)), OSXenDom0, RoleServer, "h") // #nosec G404
if !contains(xen, "xapi") {
t.Errorf("Xen dom0 services missing xapi: %+v", xen)
}
for _, os := range []OSType{OSNutanixAHV, OSOpenStackKVM} {
svcs := GenerateServicesForSystem(rand.New(rand.NewSource(1)), os, RoleServer, "h") // #nosec G404
if !contains(svcs, "libvirtd") {
t.Errorf("%s services missing libvirtd: %+v", os, svcs)
}
}
}
2 changes: 1 addition & 1 deletion internal/datagen/ostype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestParseOSType(t *testing.T) {
"macos": {"macos", OSMacOS, false},
"darwin alias": {"darwin", OSMacOS, false},
"upper+space": {" MacOS ", OSMacOS, false},
"unsupported": {"freebsd", "", true},
"unsupported": {"plan9", "", true},
"empty": {"", "", true},
}
for name, c := range cases {
Expand Down
Loading
Loading