A profile with fleet.count: N spawns N independent simulated CPEs in one process. Each gets its own parameter tree, transport (cookie jar / Digest auth cache), event tracker, session, generator runner, and stamped per-instance values. Bootstrap Informs fire in parallel.
Minimal fleet¶
fleet:
count: 100
serialPattern: "TEST-{i:04}"
Each CPE's Device.DeviceInfo.SerialNumber (the leaf named in deviceIdPaths.serialNumber) gets stamped with the pattern: TEST-0001, TEST-0002, ..., TEST-0100.
{i} is the 1-based instance index. {i:N} is the same with zero-padding to N digits. {base} substitutes the literal value: from the SerialNumber leaf.
Per-CPE differentiation¶
Real fleets need more than just unique serials. WAN IPs, MAC addresses, hostnames, SSIDs all differ per device. Two mechanisms:
Inline placeholders (one-off)¶
Recognized in any leaf value when fleet.count > 1:
| Placeholder | Substitutes to | Example (instance 7) |
|---|---|---|
{cpe} |
1-based instance index | 7 |
{cpe:N} |
Zero-padded decimal to N digits | 007 for {cpe:3} |
{cpe:hex:N} |
Zero-padded lowercase hex to N digits | 0007 for {cpe:hex:4} |
{cpe:HEX:N} |
Zero-padded uppercase hex | 0007 for {cpe:HEX:4} |
{cpe:mac:N} |
N bytes of MAC NIC portion (1..3) | 00:00:07 for {cpe:mac:3} |
{cpe:MAC:N} |
Same as mac but uppercase hex |
00:00:07 for {cpe:MAC:3} |
{cpe:ipv4:CIDR} |
Nth host in the IPv4 CIDR | 203.0.113.7 for {cpe:ipv4:203.0.113.0/24} |
{cpe:ipv6:CIDR} |
Nth host in the IPv6 CIDR | 2001:db8::7 for {cpe:ipv6:2001:db8::/64} |
{cpe:ipv6prefix:SUPER,SUBLEN} |
Nth /SUBLEN prefix from SUPER | 2001:db8:cafe:700::/56 for {cpe:ipv6prefix:2001:db8:cafe::/48,56} |
{cpe_id} |
Assigned CPE ID | cpe-7 |
Named pools (referenced from many leaves)¶
Declare CIDRs once in fleet.pools, reference them by name:
fleet:
count: 1000
serialPattern: "TEST-{i:04}"
pools:
wan_ipv4:
type: ipv4
cidr: "203.0.113.0/24" # RFC 5737 docs prefix; capacity 254
wan_ipv6:
type: ipv6
cidr: "2001:db8:1::/64" # RFC 3849 docs prefix
delegated_prefix:
type: ipv6prefix
super: "2001:db8:cafe::/48" # operator-side super-prefix
sublen: 56 # /56 per CPE; capacity 256
lan_subnet:
type: ipv4
cidr: "10.0.0.0/16" # capacity 65535
Then any leaf references the pool by name:
parameters:
- path: Device.IP.Interface.2.IPv4Address.1.IPAddress
value: "{wan_ipv4}" # 203.0.113.1, .2, ... per CPE
- path: Device.IP.Interface.2.IPv6Address.1.IPAddress
value: "{wan_ipv6}" # 2001:db8:1::1, ::2, ... per CPE
- path: Device.IP.Interface.2.IPv6Prefix.1.Prefix
value: "{delegated_prefix}" # 2001:db8:cafe:100::/56, :200::/56, ...
Pool types:
ipv4: Nth host in the CIDR. Capacity =2^(32-prefixLen) - 1(skips network base).ipv6: Nth host in the IPv6 CIDR. Same capacity formula on 128 bits.ipv6prefix: Nth/sublenprefix fromsuper. Capacity =2^(sublen - super.prefixLen). Models DHCPv6-PD-style ISP delegation.
Capacity is checked at LoadProfile¶
If fleet.count: 1001 references a pool that holds 1000, profile load rejects with:
fleet.count=1001 but pool "wan_ipv4" can't satisfy that many CPEs:
instance 1001 exceeds capacity 254 for cidr 203.0.113.0/24
Fail-fast at startup beats per-CPE failure mid-bootstrap.
Worked example: one container, 100 CPEs¶
deviceIdPaths:
manufacturer: Device.DeviceInfo.Manufacturer
oui: Device.DeviceInfo.ManufacturerOUI
productClass: Device.DeviceInfo.ProductClass
serialNumber: Device.DeviceInfo.SerialNumber
fleet:
count: 100
serialPattern: "TEST-{i:04}"
pools:
wan_ipv4:
type: ipv4
cidr: "203.0.113.0/24"
parameters:
- path: Device.DeviceInfo.Manufacturer
value: "ACME"
- path: Device.DeviceInfo.ManufacturerOUI
value: "001122"
- path: Device.DeviceInfo.ProductClass
value: "HomeGateway"
- path: Device.DeviceInfo.SerialNumber
value: "TEST"
- path: Device.IP.Interface.1.IPv4Address
value: "{wan_ipv4}" # 203.0.113.1 .. 203.0.113.100
writable: true
- path: Device.Ethernet.Interface.1.MACAddress
value: "AA:BB:CC:{cpe:mac:3}" # AA:BB:CC:00:00:01 .. AA:BB:CC:00:00:64
writable: true
- path: Device.Hosts.Host.1.HostName
value: "host-{cpe_id}" # host-cpe-1 .. host-cpe-100
writable: true
bin/cpe-sim --profile=this.yaml --acs-url=http://acs/cwmp
100 CPEs bootstrap in parallel. Each shows up at the ACS with a unique serial / MAC / IP / hostname. Logs carry cpe_id per line so grepping one CPE out of the noise is trivial.
CR routing per CPE¶
When fleet.count > 1 and --cr-bind-addr is set, the connection-request listener routes incoming requests by URL path: <cr-path>/<cpe-id> (e.g. /cr/cpe-3). Each CPE writes its full URL into the leaf named by --cr-publish-path so the next Inform reports the right ConnectionRequestURL.
When fleet.count == 1, the path is --cr-path verbatim. Single-CPE deployments unchanged.
Determinism¶
Per-CPE jitter and generator state derive from a per-CPE *rand.Rand seeded by FNV-64a hash of (rootSeed, cpeID). The root seed comes from --seed (or CPE_SIM_SEED); when 0, it's derived from time.Now().UnixNano() and logged at startup as root_seed=<N>. Pass that value back via --seed=<N> next run and every CPE's stream replays byte-for-byte.
Reserved pool names¶
Pool names must match [A-Za-z_][A-Za-z0-9_]* and cannot collide with built-in placeholders (cpe, cpe_id, i, base). Use snake_case for clarity.