Repository navigation
Expand file tree
/
Copy pathschema.json
More file actions
580 lines (580 loc) · 31.5 KB
/
Copy pathschema.json
File metadata and controls
580 lines (580 loc) · 31.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/bauer-group/XPD-RPIImage/main/config/schema.json",
"title": "BAUER GROUP RPI Image Variant Config",
"description": "Declarative configuration for a CustomPiOS-based Raspberry Pi image. String values may reference environment variables using ${VAR} or ${VAR:-default}.",
"type": "object",
"required": ["variant", "base_image", "targets", "hostname", "users"],
"additionalProperties": false,
"properties": {
"extends": {
"type": "string",
"description": "Relative path to a parent variant JSON. This variant is deep-merged onto the parent before schema validation (objects merge, arrays concatenate)."
},
"variant": {
"type": "object",
"required": ["name"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
"description": { "type": "string" },
"version": { "type": "string" }
}
},
"base_image": {
"type": "object",
"required": ["url", "arch"],
"additionalProperties": false,
"description": "Upstream image the build starts from. Must be a Raspberry Pi OS *Lite* image: this is a headless appliance (no GUI is configured anywhere), and the Desktop edition costs ~3 GiB of rootfs, which pushes the produced image past the ~7.28 GiB usable on a nominally 8 GB CM4 eMMC.",
"properties": {
"url": {
"type": "string",
"pattern": "^https://",
"description": "Direct .img.xz URL. `format: uri` is not enforced (generate.py validates without a format_checker), so the scheme is asserted with a pattern instead."
},
"sha256": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$|^$" },
"arch": { "type": "string", "enum": ["arm64", "armhf"] }
},
"if": {
"properties": { "url": { "pattern": "^https://downloads\\.raspberrypi\\.com/" } },
"required": ["url"]
},
"then": {
"properties": {
"url": {
"pattern": "/raspios_lite_",
"description": "Raspberry Pi OS downloads must come from a raspios_lite_* directory. The Desktop (stage4) and Full (stage5) trees differ from Lite by one path segment, which is exactly how the oversized image shipped through v0.4.4."
}
}
}
},
"targets": {
"type": "array",
"minItems": 1,
"items": { "type": "string", "enum": ["rpi4", "rpi5", "cm4", "cm5"] },
"uniqueItems": true
},
"hostname": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,62}$" },
"locale": {
"type": "object",
"additionalProperties": false,
"properties": {
"timezone": { "type": "string" },
"keyboard": { "type": "string" },
"locale": { "type": "string" }
}
},
"users": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "password"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,31}$" },
"password": { "type": "string" },
"groups": { "type": "array", "items": { "type": "string" } },
"shell": { "type": "string" },
"sudo_nopasswd": { "type": "boolean" },
"ssh_authorized_keys": { "type": "array", "items": { "type": "string" } }
}
}
},
"remove_users": {
"type": "array",
"items": { "type": "string" }
},
"root": {
"type": "object",
"additionalProperties": false,
"properties": {
"su_nopasswd_users": { "type": "array", "items": { "type": "string" } },
"ssh_password_auth": { "type": "boolean" },
"ssh_permit_root_login": { "type": "boolean" }
}
},
"packages": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true
},
"network": {
"type": "object",
"additionalProperties": false,
"properties": {
"ethernet": { "$ref": "#/$defs/interface" },
"wifi": {
"allOf": [
{ "$ref": "#/$defs/interface" },
{
"type": "object",
"properties": {
"country": { "type": "string", "pattern": "^[A-Z]{2}$" },
"networks": {
"type": "array",
"items": {
"type": "object",
"required": ["ssid", "psk"],
"additionalProperties": false,
"properties": {
"ssid": { "type": "string" },
"psk": { "type": "string" },
"priority": { "type": "integer" },
"hidden": { "type": "boolean" }
}
}
}
}
}
]
}
}
},
"boot_config": {
"type": "object",
"additionalProperties": false,
"properties": {
"core_freq_fixed": { "type": "boolean", "description": "Emits core_freq_fixed=1, which disables active scaling of the core clock and pins it to the board's own turbo frequency. Upstream recommends this over per-model core_freq/core_freq_min because it is portable across Pi 4, CM4, Pi 5 and CM5. Needed when driving SPI peripherals with a hard clock ceiling (e.g. MCP2515): spi-bcm2835 computes its divisor once at probe and never revisits it, so a scaling core silently changes the real SCK. Not overclocking." },
"enable_i2c": { "type": "boolean" },
"enable_spi": { "type": "boolean" },
"enable_i2s": { "type": "boolean" },
"enable_uart": { "type": "boolean" },
"disable_wifi": { "type": "boolean" },
"dtoverlays": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"id": { "type": "string", "description": "Disambiguator used ONLY as the merge key when a child variant extends a parent; never rendered. Needed when one overlay is loaded more than once, which is how the MCP251XFD family works: mcp2515 ships one overlay per channel (mcp2515-can0 / mcp2515-can1), but mcp251xfd ships ONE overlay for every channel and picks the chip with a boolean spi<n>-<m> parameter. Two entries both named 'mcp251xfd' merge by name into a single overlay carrying both chip-select selectors, with 'interrupt' resolved last-wins - one CAN interface instead of two, bound to the other channel's INT GPIO, with nothing failing at build time. Omit it and the merge key stays 'name', which is what every single-instance overlay wants." },
"params": {
"type": "object",
"additionalProperties": { "type": ["string", "number", "boolean"] },
"description": "Rendered as comma-separated dtoverlay parameters. A boolean true renders as a BARE token (dtoverlay=mcp251xfd,spi0-0) because the firmware treats overlay booleans as present-is-true/absent-is-false; false omits the key entirely, since 'k=false' would still make the parameter present."
}
}
}
},
"extra_lines": { "type": "array", "items": { "type": "string" } }
}
},
"bluetooth": {
"type": "object",
"additionalProperties": false,
"description": "Onboard Bluetooth radio. Enabled installs bluez and enables bluetooth.service; disabled adds dtoverlay=disable-bt and masks the service. Note that hciuart.service no longer exists on trixie - the UART attach is handled by the device tree and bluez.",
"properties": {
"enabled": { "type": "boolean", "description": "Default true. false emits dtoverlay=disable-bt, which frees the PL011 UART for GPIO serial." }
}
},
"camera": {
"type": "object",
"additionalProperties": false,
"description": "CSI camera configuration. Pi5 always uses libcamera; Pi4 requires legacy=false (libcamera) or legacy=true (deprecated raspivid stack).",
"properties": {
"enabled": { "type": "boolean" },
"autodetect": { "type": "boolean", "description": "camera_auto_detect=1 (Pi4/5 libcamera). Leave true unless pinning an explicit sensor overlay." },
"sensors": {
"type": "array",
"description": "Explicit sensor overlays when autodetect is false (e.g. ov5647, imx219, imx296, imx378, imx477, imx519, imx708).",
"items": { "type": "string" }
},
"legacy": { "type": "boolean", "description": "Pi4 only: start_x=1 + gpu_mem split for the deprecated raspivid stack." }
}
},
"hdmi": {
"type": "object",
"additionalProperties": false,
"description": "Per-output HDMI configuration. port 0 is the left/micro-HDMI on Pi4/5, port 1 is the right one.",
"properties": {
"outputs": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["port"],
"properties": {
"port": { "type": "integer", "enum": [0, 1] },
"force_hotplug": { "type": "boolean", "description": "hdmi_force_hotplug - keep the port active even without a connected display." },
"group": { "type": "integer", "enum": [0, 1, 2], "description": "hdmi_group: 0=auto, 1=CEA (TV), 2=DMT (PC monitor)." },
"mode": { "type": "integer", "minimum": 0, "description": "hdmi_mode within the chosen group (see Raspberry Pi docs for the mode table)." },
"drive": { "type": "string", "enum": ["hdmi", "dvi"], "description": "hdmi_drive: hdmi routes audio, dvi does not." },
"audio": { "type": "boolean", "description": "Force HDMI audio even if EDID reports none (hdmi_drive=2 + hdmi_ignore_edid_audio)." },
"rotate": { "type": "integer", "enum": [0, 90, 180, 270], "description": "display_hdmi_rotate (Pi4) / video=...,rotate= (Pi5 KMS)." },
"boost": { "type": "integer", "minimum": 0, "maximum": 11, "description": "config_hdmi_boost for long/noisy cables." }
}
}
}
}
},
"display": {
"type": "object",
"additionalProperties": false,
"description": "Console / DSI-LCD display settings orthogonal to HDMI.",
"properties": {
"console_rotate": { "type": "integer", "enum": [0, 90, 180, 270], "description": "fbcon rotation for the Linux text console." },
"lcd_rotate": { "type": "integer", "enum": [0, 90, 180, 270], "description": "display_lcd_rotate - applies to the official Pi touch DSI display." }
}
},
"audio": {
"type": "object",
"additionalProperties": false,
"description": "Audio subsystem - which output the ALSA default device lands on and whether onboard audio is active.",
"properties": {
"enabled": { "type": "boolean", "description": "dtparam=audio=on (onboard PWM/headphone jack)." },
"default_output": { "type": "string", "enum": ["auto", "hdmi0", "hdmi1", "headphones", "dac"], "description": "Preferred ALSA default sink." }
}
},
"gpio": {
"type": "object",
"additionalProperties": false,
"description": "GPIO-level hardware toggles. The 1-Wire protocol is exposed as its own block for convenience (DS18B20 temperature sensors etc.).",
"properties": {
"one_wire": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"pin": { "type": "integer", "minimum": 0, "maximum": 27, "description": "BCM pin for w1-gpio, default 4." }
}
}
}
},
"rtc": {
"type": "object",
"additionalProperties": false,
"description": "I2C real-time clock HAT. A hardware RTC keeps time across power cycles on off-grid / field devices without NTP. fake_hwclock is installed as a fallback when the HAT is absent.",
"properties": {
"enabled": { "type": "boolean" },
"model": { "type": "string", "enum": ["ds3231", "pcf8523", "pcf85063"], "description": "dtoverlay name; matches i2c-rtc's accepted values." },
"i2c_bus": { "type": "integer", "enum": [0, 1, 3, 4, 5, 6], "default": 1 },
"fake_hwclock": { "type": "boolean", "description": "Install fake-hwclock to persist time across reboots when no HAT is attached." }
}
},
"fan": {
"type": "object",
"additionalProperties": false,
"description": "Active cooling. mode=gpio drives a simple on/off MOSFET via dtoverlay gpio-fan; mode=pwm uses the pwm-fan overlay with temp-curve; mode=emc2301 targets the Pi5 cooling HAT (built-in on the Active Cooler).",
"properties": {
"enabled": { "type": "boolean" },
"mode": { "type": "string", "enum": ["gpio", "pwm", "emc2301"] },
"gpio": { "type": "integer", "minimum": 0, "maximum": 27, "description": "BCM pin for gpio/pwm fan (typically 14 on PoE HAT, 18 for PWM HAT)." },
"temp_on": { "type": "integer", "minimum": 0, "description": "Turn-on temperature in millidegC." },
"temp_off": { "type": "integer", "minimum": 0, "description": "Turn-off temperature in millidegC (hysteresis)." }
}
},
"leds": {
"type": "object",
"additionalProperties": false,
"description": "Power / activity LED behaviour. Use 'off' for stealth deployments, 'heartbeat' for liveness, 'mmc0' for disk I/O feedback.",
"properties": {
"power": { "type": "string", "enum": ["on", "off", "heartbeat", "mmc0", "default"] },
"activity": { "type": "string", "enum": ["on", "off", "heartbeat", "mmc0", "default"] }
}
},
"overclock": {
"type": "object",
"additionalProperties": false,
"description": "Performance tuning. Requires an explicit warranty-void acknowledgement - otherwise the config fails validation.",
"properties": {
"enabled": { "type": "boolean" },
"accept_warranty_void": { "type": "boolean", "description": "MUST be true when enabled=true. Overclocking sets the permanent OTP warranty bit on Pi4/5." },
"arm_freq": { "type": "integer", "minimum": 600, "maximum": 3000, "description": "ARM clock in MHz." },
"gpu_freq": { "type": "integer", "minimum": 250, "maximum": 1000, "description": "GPU core clock in MHz." },
"over_voltage": { "type": "integer", "minimum": -16, "maximum": 14, "description": "CPU/GPU core voltage offset (0.025 V steps)." },
"sdram_freq": { "type": "integer", "minimum": 400, "maximum": 800, "description": "SDRAM clock in MHz." }
}
},
"memory": {
"type": "object",
"additionalProperties": false,
"description": "Memory split between VideoCore and Linux. Headless images should shrink gpu_mem (default 76 / 128 depending on board) to free RAM.",
"properties": {
"gpu_mem": { "type": "integer", "minimum": 16, "maximum": 944, "description": "Generic gpu_mem in MiB (applies to all boards unless a size-specific override is set)." },
"gpu_mem_256": { "type": "integer", "minimum": 16, "maximum": 192 },
"gpu_mem_512": { "type": "integer", "minimum": 16, "maximum": 384 },
"gpu_mem_1024": { "type": "integer", "minimum": 16, "maximum": 944 },
"cma": { "type": "integer", "minimum": 32, "maximum": 944, "description": "Contiguous memory allocator size (MiB). Used for camera + GL." }
}
},
"pcie": {
"type": "object",
"additionalProperties": false,
"description": "Pi5 / CM4 / CM5 PCIe slot. nvme_boot flips bootloader boot_order so the device boots from an attached NVMe first.",
"properties": {
"enabled": { "type": "boolean" },
"gen": { "type": "integer", "enum": [1, 2, 3], "description": "Max PCIe generation (gen3 requires explicit opt-in - beyond spec but usually works on good boards)." },
"nvme_boot": { "type": "boolean", "description": "Advisory only - documents that the board is meant to boot from NVMe. It emits nothing on its own; the EEPROM change is made by bootloader.boot_order (e.g. 0xf461 = NVMe,USB,SD,repeat)." }
}
},
"usb": {
"type": "object",
"additionalProperties": false,
"description": "USB power / behaviour tuning.",
"properties": {
"max_usb_current": { "type": "boolean", "description": "Pi4 only: raise the USB port current ceiling when using a 3A supply (max_usb_current=1)." }
}
},
"bootloader": {
"type": "object",
"additionalProperties": false,
"description": "Pi5 / CM5 EEPROM bootloader settings. Applied on first boot via rpi-eeprom-config.",
"properties": {
"boot_order": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$", "description": "Raw BOOT_ORDER hex (e.g. 0xf461 = NVMe,USB,SD,repeat)." },
"wake_on_gpio": { "type": "boolean", "description": "WAKE_ON_GPIO - soft-off power button behaviour." },
"power_off_on_halt": { "type": "boolean", "description": "POWER_OFF_ON_HALT - actually cut power on `poweroff` instead of idling the SoC." }
}
},
"watchdog": {
"type": "object",
"additionalProperties": false,
"description": "Hardware watchdog (bcm2835-wdt). systemd kicks it automatically; failure to kick within timeout forces a reboot.",
"properties": {
"enabled": { "type": "boolean" },
"runtime_sec": { "type": "integer", "minimum": 5, "maximum": 15, "default": 15, "description": "systemd RuntimeWatchdogSec: the hardware timeout, kicked by PID 1 once per main-loop iteration and at least every timeout/2. The maximum of 15 is not a clamp boundary, it is a cliff. bcm2835_wdt_start() arms the hardware with (t << 16) & 0xfffff, so only the low four bits of the seconds survive and the effective timeout is t MOD 16: 20 gives 4s, 30 gives 14s, and 16 gives ZERO - a reset loop at every boot. Since the 6.8 fix backported into rpi-6.1.y (f33f5b1fd1be) the ioctl no longer returns EINVAL either, so an out-of-range value is applied silently and the device just reboots forever. Trixie ships exactly those kernels. Note also that PID 1 is the ONLY thing kicking below 16s (the kernel keepalive worker needs a timeout above max_hw_heartbeat_ms), and systemd issue 7932 measured 4.3s of PID 1 blocked in one SIGCHLD dispatch under a fork storm - which a Docker host with container healthchecks produces routinely. Prefer 15: it buys the largest margin the hardware allows." },
"reboot_sec": { "type": "integer", "minimum": 240, "default": 600, "description": "systemd RebootWatchdogSec. NOT the time allowed for an orderly shutdown - that is the natural reading and the wrong one. It arms the watchdog only for the SECOND phase of a reboot, after PID 1 has been replaced by systemd-shutdown; stopping Docker and its containers happens in phase one, governed by runtime_sec and the units' own TimeoutStopSec. Phase two runs sync_with_progress() (effectively unbounded - it resets its own attempt counter while dirty pages keep shrinking), then SIGTERM with a 90s timeout, then SIGKILL with another 90s, and only THEN issues its first watchdog_ping(): 210s worst case, open-ended. The 240 floor exists so no value can be set inside that window, where the watchdog fires during unmount_all() and corrupts the filesystem it was installed to protect. Upstream defaults to 600 and so do we." }
}
},
"can": {
"type": "object",
"additionalProperties": false,
"properties": {
"interfaces": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "bitrate"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "pattern": "^can[0-9]+$" },
"bitrate": { "type": "integer", "minimum": 1000 },
"sample_point": { "type": "number", "minimum": 50, "maximum": 95,
"description": "Sample point in PERCENT (e.g. 87.5) - NOT the fraction 0.875 that 'ip -details link show' prints. Rendered into 40-<name>.network as [CAN] SamplePoint=<v>%. systemd parses this key with parse_permille(), which requires a % suffix and accepts at most one decimal place; the value is rounded to one decimal on render. The 50 lower bound catches the fraction 0.875 at build time: it would otherwise render as SamplePoint=0.9%, which systemd accepts as 9 permille and the kernel then refuses (can_calc_bittiming finds no candidate with sample_point <= 9 and returns -EINVAL), leaving a device that boots with a dead CAN link. The kernel treats this value as a target and rounds DOWN to the nearest achievable point without reporting the difference. Unset lets the kernel pick, which is 87.5% at 500 kbit/s and below." },
"auto_up": { "type": "boolean" },
"txqueuelen": { "type": "integer", "minimum": 1, "maximum": 4294967294,
"description": "Rendered into 70-<name>.link as TransmitQueueLength (a .link key applied by systemd-udevd, NOT a .network key). Unset leaves the CAN core default of 10." },
"restart_ms": { "type": "integer", "minimum": 0, "maximum": 4294967295,
"description": "Milliseconds after a bus-off before the controller rejoins the bus, rendered into 40-<name>.network as [CAN] RestartSec=<n>ms. Unset takes the house default of 100. 0 means no automatic recovery - the kernel and systemd default, which leaves a bus-off link dead until someone cycles it by hand." },
"dbitrate": { "type": "integer", "minimum": 1000,
"description": "CAN FD DATA-phase bitrate in bit/s. Setting it turns the interface into a CAN FD interface: the renderer emits [CAN] FDMode=yes alongside DataBitRate=, and the two are inseparable. can_validate() in drivers/net/can/dev/netlink.c rejects IFLA_CAN_DATA_BITTIMING without CAN_CTRLMODE_FD with -EOPNOTSUPP, and that error kills the WHOLE netlink message - the nominal bitrate, the sample point and the restart time are all lost with it, so a half-configured FD interface is not a degraded link but an unconfigured one. Requires a controller that actually does FD (MCP2518FD yes, MCP2515 no); the arbitration phase keeps using `bitrate`. Omit for classic CAN." },
"data_sample_point": { "type": "number", "minimum": 50, "maximum": 95,
"description": "CAN FD data-phase sample point in PERCENT, rendered as [CAN] DataSamplePoint=<v>%. Same parse_permille() rules and the same fraction-vs-percent trap as sample_point, and likewise ignored by systemd unless dbitrate is set (networkd only copies it into the bittiming struct inside the can_data_bitrate > 0 branch). LEAVE IT UNSET unless every node on the bus is documented to use a specific value - the sample point is a property of the bus that all nodes must agree on, not a per-board tuning knob, and the kernel already picks a sane one per bitrate in can_update_sample_point(): 75% above 800 kbit/s, 80% above 500 kbit/s, 87.5% at or below 500 kbit/s. So a 2 Mbit/s data phase defaults to 75% and a 500 kbit/s arbitration phase to 87.5% with nothing set at all." },
"fd_non_iso": { "type": "boolean",
"description": "Emits [CAN] FDNonISO=yes, selecting the pre-standard Bosch CAN FD frame format (different CRC and stuff-bit counter) instead of ISO 11898-1:2015. Needed ONLY to talk to first-generation FD silicon that predates the ISO standard, and the two formats do not interoperate: a non-ISO node on an ISO bus produces CRC errors, not silence. Leave unset unless a device on the bus is documented as non-ISO." }
}
}
}
}
},
"docker": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"daemon": { "type": "object" },
"sysctl": { "type": "object", "additionalProperties": { "type": ["integer", "string"] } },
"networks": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"driver": { "type": "string" },
"subnet": { "type": "string" },
"gateway": { "type": "string" },
"ipv6": { "type": "boolean" },
"subnet_v6": { "type": "string" },
"gateway_v6": { "type": "string" },
"options": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
}
}
}
},
"podman": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"docker_emulation": { "type": "boolean" },
"sysctl": {
"type": "object",
"additionalProperties": { "type": ["integer", "string"] }
},
"network": {
"type": "object",
"additionalProperties": false,
"properties": {
"default_subnet": { "type": "string" },
"default_subnet_pools": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["base", "size"],
"properties": {
"base": { "type": "string" },
"size": { "type": "integer" }
}
}
},
"ipv6": { "type": "boolean" },
"subnet_v6": { "type": "string" },
"firewall_driver": {
"type": "string",
"enum": ["iptables", "nftables", "none", "firewalld"]
}
}
},
"journald": {
"type": "object",
"additionalProperties": false,
"properties": {
"system_max_use": { "type": "string" },
"system_max_file_size": { "type": "string" }
}
},
"auto_update": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"schedule": {
"type": "object",
"additionalProperties": false,
"properties": {
"start": { "type": "string", "pattern": "^[0-2][0-9]:[0-5][0-9]$" },
"randomized_delay_minutes": { "type": "integer", "minimum": 0 },
"persistent": { "type": "boolean" }
}
}
}
}
}
},
"portainer": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"edition": { "type": "string", "enum": ["ce", "ee"] },
"bind": { "type": "string" },
"image": { "type": "string" },
"ports": {
"type": "object",
"additionalProperties": false,
"properties": {
"edge": { "type": "integer" },
"http": { "type": "integer" },
"https": { "type": "integer" }
}
},
"auto_start": { "type": "boolean" },
"auto_update": { "type": "boolean" },
"backup_before_update": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"keep": { "type": "integer", "minimum": 1 }
}
}
}
},
"ssh": {
"type": "object",
"additionalProperties": false,
"description": "SSH service policy. Detailed auth settings stay under `root.ssh_*`.",
"properties": {
"enabled": { "type": "boolean", "description": "Enable ssh.service at boot (default true)." }
}
},
"banner": {
"type": "object",
"additionalProperties": false,
"description": "Pre-login banners (/etc/issue, /etc/issue.net) + post-login MOTD showing variant, hostname, IPv4/IPv6, CAN, docker + update status.",
"properties": {
"enabled": { "type": "boolean" },
"pre_login_note": { "type": "string", "description": "Optional line appended to /etc/issue(.net), e.g. legal notice." }
}
},
"unattended_upgrades": {
"type": "object",
"additionalProperties": false,
"description": "Unattended security/system updates with configurable maintenance window and reboot policy.",
"properties": {
"enabled": { "type": "boolean" },
"allowed_origins": {
"type": "array",
"items": { "type": "string" },
"description": "APT origin patterns to auto-upgrade (supports ${distro_id} / ${distro_codename} placeholders resolved by unattended-upgrades)."
},
"package_blocklist": {
"type": "array",
"items": { "type": "string" }
},
"remove_unused_dependencies": { "type": "boolean" },
"schedule": {
"type": "object",
"additionalProperties": false,
"description": "Maintenance window during which unattended-upgrades runs. Implemented via systemd timer override with RandomizedDelaySec.",
"properties": {
"start": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", "description": "Window start HH:MM (24h, server local time)." },
"end": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", "description": "Window end HH:MM (24h, server local time). Run fires at a random point in [start,end]." },
"persistent": { "type": "boolean", "description": "If true, missed runs (e.g. offline) execute on next boot." }
}
},
"auto_reboot": {
"type": "object",
"additionalProperties": false,
"description": "Reboot policy when /var/run/reboot-required is present.",
"properties": {
"enabled": { "type": "boolean" },
"window": {
"type": "object",
"additionalProperties": false,
"description": "Reboot is only issued when current time is within this window AND a reboot is required.",
"properties": {
"start": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$" },
"end": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$" }
},
"required": ["start", "end"]
},
"if_required_only": { "type": "boolean", "description": "Reboot only if /var/run/reboot-required exists (recommended)." }
}
},
"mail": {
"type": "object",
"additionalProperties": false,
"properties": {
"address": { "type": "string" },
"on_error_only": { "type": "boolean" }
}
}
}
}
},
"$defs": {
"interface": {
"type": "object",
"properties": {
"interface": { "type": "string" },
"mode": { "type": "string", "enum": ["dhcp", "static", "disabled"] },
"ipv6": { "type": "boolean" },
"address": { "type": "string" },
"prefix": { "type": "integer", "minimum": 0, "maximum": 128 },
"gateway": { "type": "string" },
"dns": { "type": "array", "items": { "type": "string" } },
"address_v6": { "type": "string" },
"prefix_v6": { "type": "integer", "minimum": 0, "maximum": 128 },
"gateway_v6": { "type": "string" }
}
}
}
}