Repository navigation
Expand file tree
/
Copy pathdocs.html
More file actions
1537 lines (1371 loc) · 114 KB
/
Copy pathdocs.html
File metadata and controls
1537 lines (1371 loc) · 114 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
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Docs — Pabawi</title>
<meta name="description" content="Pabawi documentation: quick start, configuration, integrations, API reference, and deployment guides." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/style.css" />
<link rel="icon" href="logo.png" type="image/png" />
</head>
<body>
<!-- NAV -->
<nav class="nav">
<div class="nav-inner">
<a href="index.html" class="nav-logo">
<img src="logo.png" width="28" height="28" alt="Pabawi logo" style="border-radius:4px;" />
Pabawi
</a>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="docs.html" class="active">Docs</a></li>
<li><a href="about.html">About</a></li>
<li><a href="support.html">Support</a></li>
<li><a href="https://github.com/example42/pabawi" target="_blank" rel="noopener">GitHub ↗</a></li>
<li><a href="https://github.com/example42/pabawi/releases" target="_blank" rel="noopener" class="nav-cta">Get Started</a></li>
</ul>
<button class="nav-hamburger" aria-label="Menu">
<svg width="22" height="22" fill="none" viewBox="0 0 22 22"><path d="M3 6h16M3 11h16M3 16h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
</nav>
<!-- DOCS LAYOUT -->
<div class="docs-layout">
<!-- SIDEBAR -->
<aside class="docs-sidebar">
<h4>Getting Started</h4>
<ul>
<li><a href="#quickstart" class="active">Quick Start</a></li>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#manual-setup">Manual Setup</a></li>
<li><a href="#initial-setup">Initial Setup</a></li>
<li><a href="#docker">Docker Deployment</a></li>
<li><a href="#kubernetes">Kubernetes</a></li>
</ul>
<h4>Configuration</h4>
<ul>
<li><a href="#configuration">Overview</a></li>
<li><a href="#core-config">Core Server</a></li>
<li><a href="#database-config">Database</a></li>
<li><a href="#auth-config">Authentication & Secrets</a></li>
<li><a href="#sso-config">Entra ID SSO & LDAP</a></li>
<li><a href="#whitelist-config">Command Whitelist</a></li>
<li><a href="#runtime-config">Streaming, Cache & Queue</a></li>
<li><a href="#safety-config">Provisioning Safety</a></li>
</ul>
<h4>Integrations</h4>
<ul>
<li><a href="#integrations">Overview</a></li>
<li><a href="#bolt">Bolt</a></li>
<li><a href="#ansible">Ansible</a></li>
<li><a href="#ssh">SSH</a></li>
<li><a href="#puppetdb">PuppetDB</a></li>
<li><a href="#puppetserver">Puppetserver</a></li>
<li><a href="#hiera">Hiera</a></li>
<li><a href="#checkmk">Checkmk</a></li>
<li><a href="#proxmox">Proxmox</a></li>
<li><a href="#aws">AWS EC2</a></li>
<li><a href="#azure">Azure</a></li>
</ul>
<h4>Using Pabawi</h4>
<ul>
<li><a href="#navigation">Navigation</a></li>
<li><a href="#inventory">Inventory</a></li>
<li><a href="#node-detail">Node Detail & Facts</a></li>
<li><a href="#execution">Command Execution</a></li>
<li><a href="#tasks">Bolt Tasks & Puppet Runs</a></li>
<li><a href="#lifecycle">Provisioning & Lifecycle</a></li>
<li><a href="#history">Execution History</a></li>
<li><a href="#node-journal">Journal</a></li>
<li><a href="#rbac">RBAC & Permissions</a></li>
<li><a href="#status-dashboard">Integration Status</a></li>
<li><a href="#expert-mode">Expert Mode</a></li>
</ul>
<h4>Reference</h4>
<ul>
<li><a href="#api">API Reference</a></li>
<li><a href="#mcp">MCP Server</a></li>
<li><a href="#env-vars">Example .env Files</a></li>
<li><a href="#troubleshooting">Troubleshooting</a></li>
<li><a href="#crash-dumps">Crash Dumps</a></li>
<li><a href="#account-recovery">Account Recovery</a></li>
</ul>
</aside>
<!-- MAIN CONTENT -->
<main class="docs-content">
<h1>Documentation</h1>
<p class="doc-subtitle">Everything you need to install, configure, and use Pabawi to manage your infrastructure.</p>
<!-- ===== QUICK START ===== -->
<div class="docs-section" id="quickstart">
<h2>Quick Start</h2>
<p>The fastest way to get Pabawi running is with the interactive setup script. It handles prerequisite checks, config file generation, dependency installation, and first launch.</p>
<div class="docs-code">
<div class="code-header" style="display:flex;justify-content:space-between;padding:.5rem 1rem;background:rgba(31,41,55,.6);border-bottom:1px solid var(--border);">
<span class="code-lang">bash</span>
<button class="copy-btn">Copy</button>
</div>
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">git clone https://github.com/example42/pabawi</span>
<span class="c-prompt">$ </span><span class="c-cmd">cd pabawi</span>
<span class="c-prompt">$ </span><span class="c-cmd">./scripts/setup.sh</span></pre>
</div>
<p>The setup script checks prerequisites, generates <code class="inline">backend/.env</code>, installs dependencies and starts the app. Once running, open <code class="inline">http://localhost:3000</code> in your browser.</p>
<div class="docs-callout warn">
<p><strong>Before exposing a fresh install:</strong> the first administrator is claimed with a bootstrap token (<code class="inline">PABAWI_BOOTSTRAP_TOKEN</code>). See <a href="#initial-setup">Initial Setup</a>.</p>
</div>
<div class="docs-callout tip">
<p><strong>Tip:</strong> All integrations are optional. You don't need Puppet, Bolt, or Ansible to get started: enable only SSH and Pabawi works as a standalone web UI for your servers.</p>
</div>
</div>
<!-- ===== PREREQUISITES ===== -->
<div class="docs-section" id="prerequisites">
<h2>Prerequisites</h2>
<p>Hard requirements (always needed):</p>
<ul>
<li><strong>Node.js 24 LTS</strong> and <strong>npm 11</strong>, or a container engine (Docker) if you use the container image</li>
</ul>
<p>Soft requirements (only needed for the integrations you enable):</p>
<ul>
<li><strong>Bolt CLI</strong> for the Bolt integration (bundled in the <code class="inline">batteries</code> image)</li>
<li><strong>Ansible CLI</strong> (<code class="inline">ansible</code> and <code class="inline">ansible-playbook</code>) for the Ansible integration</li>
<li><strong>Puppet/OpenVox agent</strong> for Puppet runs and as a source of SSL certs for PuppetDB and Puppetserver</li>
<li><strong>Control repo</strong> for Hiera (local path to your Puppet control repository)</li>
<li><strong>SSH access</strong> with keys and a managed host fingerprint file for the SSH integration</li>
</ul>
</div>
<!-- ===== MANUAL SETUP ===== -->
<div class="docs-section" id="manual-setup">
<h2>Manual Setup</h2>
<p>If you prefer to configure things by hand:</p>
<div class="docs-code">
<div class="code-header" style="display:flex;justify-content:space-between;padding:.5rem 1rem;background:rgba(31,41,55,.6);border-bottom:1px solid var(--border);">
<span class="code-lang">bash</span><button class="copy-btn">Copy</button>
</div>
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">git clone https://github.com/example42/pabawi && cd pabawi</span>
<span class="c-prompt">$ </span><span class="c-cmd">npm run install:all</span> <span class="c-cmt"># installs deps, rebuilds native modules</span>
<span class="c-prompt">$ </span><span class="c-cmd">cp backend/.env.example backend/.env</span>
<span class="c-cmt"># Edit backend/.env with your settings</span>
<span class="c-prompt">$ </span><span class="c-cmd">npm run dev:fullstack</span> <span class="c-cmt"># build + serve everything on port 3000</span></pre>
</div>
<h3>Upgrading</h3>
<div class="docs-code">
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">git fetch --tags && git checkout v<VERSION> && npm run install:all && npm run build</span></pre>
</div>
<p>For Docker use <code class="inline">docker pull</code>; for Kubernetes use <code class="inline">helm upgrade</code>. Migrations run automatically on startup: back up your database first and check the <a href="https://github.com/example42/pabawi/blob/main/CHANGELOG.md" target="_blank" rel="noopener">CHANGELOG</a>.</p>
</div>
<!-- ===== INITIAL SETUP ===== -->
<div class="docs-section" id="initial-setup">
<h2>Initial Setup</h2>
<p>Authentication is mandatory and has no environment switch. Before starting a fresh install, set two secrets in the environment:</p>
<ul>
<li><code class="inline">JWT_SECRET</code>: at least 32 characters of random entropy, not a placeholder. The server refuses to start otherwise.</li>
<li><code class="inline">PABAWI_BOOTSTRAP_TOKEN</code>: an independently generated secret of 32 to 512 characters, used to claim the initial administrator account. When unset, enrollment is disabled.</li>
</ul>
<div class="docs-code">
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">openssl rand -base64 32</span> <span class="c-cmt"># JWT_SECRET</span>
<span class="c-prompt">$ </span><span class="c-cmd">openssl rand -hex 32</span> <span class="c-cmt"># PABAWI_BOOTSTRAP_TOKEN</span></pre>
</div>
<p>Keep access to the instance private until the administrator is claimed, then remove <code class="inline">PABAWI_BOOTSTRAP_TOKEN</code> and restart.</p>
</div>
<!-- ===== DOCKER ===== -->
<div class="docs-section" id="docker">
<h2>Docker Deployment</h2>
<p>Two image profiles are published from the same source and version, for amd64 and arm64:</p>
<table class="docs-table">
<thead><tr><th>Tag</th><th>Contents</th><th>Use when</th></tr></thead>
<tbody>
<tr><td>example42/pabawi:latest</td><td>Pabawi application and Node dependencies</td><td>Integrations use HTTP APIs, cloud SDKs, or the embedded SSH client</td></tr>
<tr><td>example42/pabawi:batteries</td><td>Core plus Bolt, Ansible, Puppet/OpenVox, OpenSSH, Git, rsync and sshpass</td><td>Integrations need local infrastructure commands</td></tr>
</tbody>
</table>
<div class="docs-code">
<div class="code-header" style="display:flex;justify-content:space-between;padding:.5rem 1rem;background:rgba(31,41,55,.6);border-bottom:1px solid var(--border);">
<span class="code-lang">bash</span><button class="copy-btn">Copy</button>
</div>
<pre>
<span class="c-cmt"># Batteries image, because this example mounts a Bolt project</span>
<span class="c-prompt">$ </span><span class="c-cmd">docker run -d \</span>
--name pabawi \
--user "$(id -u):1001" \
-p 127.0.0.1:3000:3000 \
-v "$(pwd)/bolt-project:/opt/pabawi/bolt-project:ro" \
-v "$(pwd)/data:/opt/pabawi/data" \
--env-file .env \
example42/pabawi:batteries</pre>
</div>
<p>Set <code class="inline">HOST=0.0.0.0</code> and <code class="inline">PORT=3000</code> inside the container. The user passed with <code class="inline">--user</code> must be able to read all mounted files; the container runs as UID 1001.</p>
<h3>Volume mounts</h3>
<p>All paths in <code class="inline">.env</code> must be <strong>container paths</strong> (under <code class="inline">/opt/pabawi/</code>), not host paths.</p>
<table class="docs-table">
<thead><tr><th>Data</th><th>Host → Container</th><th>Env setting</th></tr></thead>
<tbody>
<tr><td>SQLite DB</td><td>./data → /opt/pabawi/data</td><td>DATABASE_PATH=/opt/pabawi/data/pabawi.db</td></tr>
<tr><td>Bolt project</td><td>./bolt-project → /opt/pabawi/bolt-project</td><td>BOLT_PROJECT_PATH=/opt/pabawi/bolt-project</td></tr>
<tr><td>Control repo</td><td>./control-repo → /opt/pabawi/control-repo</td><td>HIERA_CONTROL_REPO_PATH=/opt/pabawi/control-repo</td></tr>
<tr><td>Ansible</td><td>./ansible → /opt/pabawi/ansible</td><td>ANSIBLE_PROJECT_PATH=/opt/pabawi/ansible</td></tr>
<tr><td>SSL certs</td><td>./certs → /opt/pabawi/certs</td><td>PUPPETDB_SSL_CA=/opt/pabawi/certs/ca.pem</td></tr>
<tr><td>SSH keys</td><td>~/.ssh → /opt/pabawi/ssh</td><td>SSH_DEFAULT_KEY=/opt/pabawi/ssh/id_rsa</td></tr>
</tbody>
</table>
<div class="docs-code">
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">chown -R 1001:1001 ./data</span>
<span class="c-prompt">$ </span><span class="c-cmd">chmod 755 ./bolt-project ./control-repo</span>
<span class="c-prompt">$ </span><span class="c-cmd">chmod 600 ./certs/*.pem ./certs/*.key</span></pre>
</div>
<h3>Docker Compose</h3>
<div class="docs-code">
<div class="code-header" style="display:flex;justify-content:space-between;padding:.5rem 1rem;background:rgba(31,41,55,.6);border-bottom:1px solid var(--border);">
<span class="code-lang">yaml</span><button class="copy-btn">Copy</button>
</div>
<pre>
<span class="c-key">services</span>:
<span class="c-key">pabawi</span>:
<span class="c-key">image</span>: example42/pabawi:batteries
<span class="c-key">restart</span>: unless-stopped
<span class="c-key">ports</span>:
- <span class="c-val">"127.0.0.1:3000:3000"</span>
<span class="c-key">user</span>: <span class="c-val">"${UID:-1000}:1001"</span>
<span class="c-key">env_file</span>: .env
<span class="c-key">volumes</span>:
- ./bolt-project:/opt/pabawi/bolt-project:ro
- ./control-repo:/opt/pabawi/control-repo:ro
- ./ansible:/opt/pabawi/ansible:ro
- ./certs:/opt/pabawi/certs:ro
- ~/.ssh:/opt/pabawi/ssh:ro
- ./data:/opt/pabawi/data</pre>
</div>
<div class="docs-callout warn">
<p><strong>Port binding:</strong> the repository's <code class="inline">docker-compose.yml</code> publishes the application and PostgreSQL ports on all host interfaces by default. For a private first start, edit the mappings to <code class="inline">127.0.0.1:3000:3000</code> and <code class="inline">127.0.0.1:5432:5432</code>.</p>
</div>
<h3>PostgreSQL backend</h3>
<p>The repository's compose file includes a profile-gated <code class="inline">postgres</code> service, off by default:</p>
<div class="docs-code">
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">docker compose --profile postgres up</span>
<span class="c-cmt"># .env</span>
<span class="c-key">DB_TYPE</span>=<span class="c-val">postgres</span>
<span class="c-key">DATABASE_URL</span>=<span class="c-val">postgres://pabawi:pabawi@postgres:5432/pabawi</span></pre>
</div>
<p>Service credentials default to <code class="inline">pabawi</code> and can be overridden with <code class="inline">POSTGRES_USER</code>, <code class="inline">POSTGRES_PASSWORD</code>, <code class="inline">POSTGRES_DB</code> and <code class="inline">POSTGRES_PORT</code>.</p>
<h3>Building the image</h3>
<div class="docs-code">
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">docker build --target core -t pabawi:core .</span>
<span class="c-prompt">$ </span><span class="c-cmd">docker build --target batteries -t pabawi:batteries .</span>
<span class="c-prompt">$ </span><span class="c-cmd">docker buildx build --platform linux/amd64,linux/arm64 --target core -t pabawi:core .</span></pre>
</div>
<h3>SSL certificates for Puppet</h3>
<div class="docs-code">
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">mkdir -p ./certs</span>
<span class="c-prompt">$ </span><span class="c-cmd">scp puppet.example.com:/etc/puppetlabs/puppet/ssl/certs/ca.pem ./certs/</span>
<span class="c-prompt">$ </span><span class="c-cmd">scp puppet.example.com:/etc/puppetlabs/puppet/ssl/certs/pabawi.pem ./certs/client.crt</span>
<span class="c-prompt">$ </span><span class="c-cmd">scp puppet.example.com:/etc/puppetlabs/puppet/ssl/private_keys/pabawi.pem ./certs/client.key</span>
<span class="c-prompt">$ </span><span class="c-cmd">chmod 644 ./certs/ca.pem ./certs/client.crt; chmod 600 ./certs/client.key</span></pre>
</div>
<h3>Docker troubleshooting</h3>
<table class="docs-table">
<thead><tr><th>Problem</th><th>Fix</th></tr></thead>
<tbody>
<tr><td>Permission denied on volume</td><td>Check ownership (<code class="inline">chown -R 1001:1001 ./data</code>) and mode</td></tr>
<tr><td>"Bolt project not found"</td><td>Check <code class="inline">BOLT_PROJECT_PATH</code> is <code class="inline">/opt/pabawi/bolt-project</code> (container path)</td></tr>
<tr><td>SSL errors</td><td>Verify cert permissions (600) and that the CA matches the server cert</td></tr>
<tr><td>Container exits immediately</td><td>Check <code class="inline">docker logs pabawi</code> for config validation errors</td></tr>
</tbody>
</table>
</div>
<!-- ===== KUBERNETES ===== -->
<div class="docs-section" id="kubernetes">
<h2>Kubernetes Deployment</h2>
<p>The repository includes a Helm chart at <code class="inline">charts/pabawi</code>. Pabawi's execution queue, SSE tickets, MCP transports and console sessions are process-local, so <strong>keep <code class="inline">replicas: 1</code></strong>, with SQLite or PostgreSQL. SQLite chart rollouts always use the <code class="inline">Recreate</code> strategy.</p>
<h3>Helm</h3>
<p>For a fresh installation, put an independently generated bootstrap token in a restricted values file (mode 0600):</p>
<div class="docs-code">
<div class="code-header" style="display:flex;justify-content:space-between;padding:.5rem 1rem;background:rgba(31,41,55,.6);border-bottom:1px solid var(--border);">
<span class="code-lang">bash</span><button class="copy-btn">Copy</button>
</div>
<pre>
<span class="c-cmt"># bootstrap-values.yaml</span>
<span class="c-key">secretEnv</span>:
<span class="c-key">PABAWI_BOOTSTRAP_TOKEN</span>: <span class="c-val">"<independent-random-token>"</span>
<span class="c-cmt"># SQLite, single replica</span>
<span class="c-prompt">$ </span><span class="c-cmd">helm dependency build ./charts/pabawi</span>
<span class="c-prompt">$ </span><span class="c-cmd">helm install pabawi ./charts/pabawi -f bootstrap-values.yaml \</span>
--set secrets.jwtSecret="$(openssl rand -base64 48)"
<span class="c-cmt"># PostgreSQL via an existing Secret containing DATABASE_URL</span>
<span class="c-prompt">$ </span><span class="c-cmd">helm install pabawi ./charts/pabawi -f bootstrap-values.yaml \</span>
--set database.type=postgres \
--set database.postgres.existingSecret=pabawi-db \
--set replicaCount=1 \
--set secrets.jwtSecret="$(openssl rand -base64 48)"</pre>
</div>
<p>The chart can also install a bundled Bitnami PostgreSQL (<code class="inline">--set postgresql.enabled=true</code>), suitable for demos and isolated single-replica environments. For production use managed or operator-managed PostgreSQL.</p>
<h3>Raw manifests</h3>
<ul>
<li><strong>ConfigMap</strong> for non-sensitive settings (<code class="inline">PORT</code>, <code class="inline">HOST</code>, <code class="inline">DB_TYPE</code>, <code class="inline">BOLT_PROJECT_PATH</code>, whitelist mode, cache TTLs, <code class="inline">ALLOW_DESTRUCTIVE_PROVISIONING</code>)</li>
<li><strong>Secret</strong> <code class="inline">pabawi-secrets</code> for <code class="inline">JWT_SECRET</code>, <code class="inline">PABAWI_BOOTSTRAP_TOKEN</code>, <code class="inline">DATABASE_URL</code> (PostgreSQL) and credentials; separate Secrets for SSL certs and SSH keys, mounted read-only</li>
<li><strong>PersistentVolumeClaim</strong> (ReadWriteOnce) mounted at <code class="inline">/opt/pabawi/data</code>. Required for SQLite, not for the application database when <code class="inline">DB_TYPE=postgres</code></li>
<li><strong>Deployment</strong> with <code class="inline">replicas: 1</code>, <code class="inline">strategy: Recreate</code>, <code class="inline">runAsUser: 1001</code>, <code class="inline">fsGroup: 1001</code>, and liveness/readiness probes on <code class="inline">GET /api/health</code> port 3000</li>
<li><strong>Service</strong> exposing port 3000 (use LoadBalancer or Ingress for external access)</li>
</ul>
<div class="docs-code">
<pre>
<span class="c-key">spec</span>:
<span class="c-key">replicas</span>: <span class="c-val">1</span>
<span class="c-key">strategy</span>:
<span class="c-key">type</span>: Recreate
<span class="c-key">template</span>:
<span class="c-key">spec</span>:
<span class="c-key">securityContext</span>:
<span class="c-key">runAsUser</span>: <span class="c-val">1001</span>
<span class="c-key">fsGroup</span>: <span class="c-val">1001</span>
<span class="c-key">containers</span>:
- <span class="c-key">name</span>: pabawi
<span class="c-key">image</span>: example42/pabawi:latest
<span class="c-key">envFrom</span>:
- <span class="c-key">configMapRef</span>: { <span class="c-key">name</span>: pabawi-config }
- <span class="c-key">secretRef</span>: { <span class="c-key">name</span>: pabawi-secrets }
<span class="c-key">livenessProbe</span>:
<span class="c-key">httpGet</span>: { <span class="c-key">path</span>: /api/health, <span class="c-key">port</span>: <span class="c-val">3000</span> }
<span class="c-key">initialDelaySeconds</span>: <span class="c-val">30</span></pre>
</div>
<div class="docs-callout warn">
<p><strong>Config changes need a restart.</strong> For manually applied manifests and external Secrets, updating a resource does not update environment variables in a running process. Run <code class="inline">kubectl rollout restart deployment/pabawi</code> after applying changes. Chart-managed configuration changes trigger a rollout automatically.</p>
</div>
<p>Full manifests: <a href="https://github.com/example42/pabawi/blob/main/docs/deployment/kubernetes.md" target="_blank" rel="noopener">upstream Kubernetes guide</a>.</p>
</div>
<!-- ===== CONFIGURATION ===== -->
<div class="docs-section" id="configuration">
<h2>Configuration</h2>
<p>Configuration is read from the process environment at startup. Source development loads <code class="inline">backend/.env</code>; containers receive variables from their deployment. <code class="inline">ConfigService</code> validates with Zod, and SSH has its own parser. Invalid values make the server exit immediately with <code class="inline">Configuration validation failed: <field>: <reason></code>.</p>
<p>Run <code class="inline">scripts/setup.sh</code> for interactive setup, or use <code class="inline">backend/.env.example</code> as a template. Never commit <code class="inline">.env</code> to version control. Credential values belong in restricted environment files or Secrets. Durations state their units: most settings are milliseconds, SSH and LDAP timeouts are seconds.</p>
<p>Enable integrations with <code class="inline"><PREFIX>_ENABLED=true</code>. Disabled integrations are skipped at startup: no connection attempts, no health check failures.</p>
</div>
<div class="docs-section" id="core-config">
<h2>Core Server</h2>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>NODE_ENV</td><td>unset</td><td><code class="inline">test</code> disables dotenv loading; deployment tooling commonly sets <code class="inline">production</code></td></tr>
<tr><td>PORT</td><td>3000</td><td>HTTP port</td></tr>
<tr><td>HOST</td><td>localhost</td><td>Bind address (<code class="inline">0.0.0.0</code> for all interfaces)</td></tr>
<tr><td>LOG_LEVEL</td><td>info</td><td>error / warn / info / debug</td></tr>
<tr><td>PABAWI_CRASH_DUMP_DIR</td><td><cwd>/crash-dumps</td><td>Where JSON crash dumps and Node diagnostic reports are written (created with mode 0700)</td></tr>
<tr><td>CORS_ALLOWED_ORIGINS</td><td>(none)</td><td>Comma-separated list of allowed origins</td></tr>
<tr><td>UI_SHOW_HOME_PAGE_RUN_CHART</td><td>true</td><td>Show the aggregated Puppet run history chart on the home page (requires PuppetDB)</td></tr>
</tbody>
</table>
</div>
<div class="docs-section" id="database-config">
<h2>Database</h2>
<p>Pabawi runs on SQLite by default and supports PostgreSQL as an alternative backend. The same schema and code path serve both, and migrations run automatically on startup.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>DB_TYPE</td><td>sqlite</td><td><code class="inline">sqlite</code> or <code class="inline">postgres</code></td></tr>
<tr><td>DATABASE_PATH</td><td>./data/pabawi.db</td><td>SQLite file path (directory must exist and be writable)</td></tr>
<tr><td>DATABASE_URL</td><td>(none)</td><td><strong>Required</strong> when <code class="inline">DB_TYPE=postgres</code>, e.g. <code class="inline">postgres://user:pass@host:5432/pabawi</code> (secret)</td></tr>
</tbody>
</table>
<p><code class="inline">DB_TYPE=postgres</code> without a <code class="inline">DATABASE_URL</code> fails fast at startup.</p>
</div>
<div class="docs-section" id="auth-config">
<h2>Authentication & Secrets</h2>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>PABAWI_BOOTSTRAP_TOKEN</td><td>(empty)</td><td>Initial enrollment secret, 32 to 512 characters. Empty disables enrollment. Remove after setup. (secret)</td></tr>
<tr><td>JWT_SECRET</td><td>required</td><td>JWT signing secret: at least 32 chars of random entropy, not a placeholder. Generate with <code class="inline">openssl rand -base64 32</code>. Tokens use <code class="inline">iss=pabawi</code> / <code class="inline">aud=pabawi</code>. (secret)</td></tr>
<tr><td>PABAWI_LIFECYCLE_TOKEN</td><td>(empty)</td><td>Optional machine credential for the generic lifecycle endpoints. (secret)</td></tr>
</tbody>
</table>
<div class="docs-callout warn">
<p><strong>Security:</strong> The server refuses to start with a weak or placeholder <code class="inline">JWT_SECRET</code>. Never reuse it across environments.</p>
</div>
<h3>Lifecycle machine credential</h3>
<p><code class="inline">PABAWI_LIFECYCLE_TOKEN</code> is for unattended clients (scripts, cron jobs, webhooks) that drive <code class="inline">POST /api/inventory/:id/action</code>, <code class="inline">DELETE /api/inventory/:id</code> and <code class="inline">GET /api/inventory/:id/lifecycle-actions</code> without a user session. Send it as <code class="inline">Authorization: Bearer <token></code> instead of a user JWT.</p>
<ul>
<li>When set, the server provisions a built-in <code class="inline">lifecycle-service</code> account with a "Lifecycle Service" role holding <code class="inline">read</code>, <code class="inline">lifecycle</code> and <code class="inline">destroy</code> on <code class="inline">proxmox</code>, <code class="inline">aws</code> and <code class="inline">azure</code>, subject to <code class="inline">ALLOW_DESTRUCTIVE_PROVISIONING</code></li>
<li>The token is accepted on <code class="inline">/api/inventory</code> only</li>
<li>To change its scope, edit the role's permissions (the role is not rewritten on restart; do not delete it)</li>
<li>To revoke: deactivate the <code class="inline">lifecycle-service</code> account (immediate) or unset the variable and restart</li>
</ul>
</div>
<div class="docs-section" id="sso-config">
<h2>Entra ID SSO & LDAP</h2>
<h3>Azure Entra ID SSO</h3>
<p>Optional federated login via OpenID Connect. When enabled, the login page shows "Sign in with Microsoft" alongside local login. New federated users are provisioned automatically with the default viewer role.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>ENTRA_ID_ENABLED</td><td>false</td><td>Set to <code class="inline">"true"</code> to enable; other <code class="inline">ENTRA_ID_*</code> vars are ignored otherwise</td></tr>
<tr><td>ENTRA_ID_TENANT_ID</td><td>required</td><td>Azure tenant (directory) ID</td></tr>
<tr><td>ENTRA_ID_CLIENT_ID</td><td>required</td><td>Application (client) ID</td></tr>
<tr><td>ENTRA_ID_CLIENT_SECRET</td><td>required</td><td>Client secret value (secret)</td></tr>
<tr><td>ENTRA_ID_REDIRECT_URI</td><td>required</td><td><code class="inline">https://your-host/api/auth/entra-id/callback</code>, must match the app registration</td></tr>
<tr><td>ENTRA_ID_SCOPES</td><td>openid,profile,email</td><td>Comma-separated OAuth scopes</td></tr>
<tr><td>ENTRA_ID_GROUP_MAPPING</td><td>(none)</td><td>JSON object mapping Azure group IDs to Pabawi role names, e.g. <code class="inline">{"uuid-1":"administrator"}</code></td></tr>
<tr><td>ENTRA_ID_POST_LOGOUT_REDIRECT_URI</td><td>(app base URL)</td><td>Where Microsoft redirects after SSO logout</td></tr>
<tr><td>ENTRA_ID_JWKS_CACHE_TTL_MS</td><td>86400000</td><td>JWKS signing key cache (24 h)</td></tr>
</tbody>
</table>
<p>With <code class="inline">ENTRA_ID_ENABLED=true</code>, the four required variables must all be set or the server refuses to start. When <code class="inline">ENTRA_ID_GROUP_MAPPING</code> is set, roles are synchronized from Azure group memberships at each SSO login; manual grants are preserved.</p>
<h3>LDAP directory service</h3>
<p>Optional LDAP / Active Directory login with read-only directory operations and group-to-role mapping. LDAP is disabled by default; when enabled, an invalid configuration fails startup, while an unreachable directory does not prevent startup or local access. No role is granted automatically: configure group mappings or grant a role manually after the first login. Changes require a restart.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>LDAP_ENABLED</td><td>false</td><td>Strict boolean</td></tr>
<tr><td>LDAP_DIRECTORY_ID</td><td>required</td><td>Stable identifier (letters, digits, <code class="inline">_</code>, <code class="inline">-</code>)</td></tr>
<tr><td>LDAP_DIRECTORY_TYPE</td><td>required</td><td><code class="inline">ad</code> or <code class="inline">openldap</code></td></tr>
<tr><td>LDAP_HOSTNAME</td><td>required</td><td>FQDN, no protocol, path or credentials</td></tr>
<tr><td>LDAP_PORT</td><td>636 / 389</td><td>636 for LDAPS, 389 for StartTLS</td></tr>
<tr><td>LDAP_FAILOVER_ENDPOINTS</td><td>[]</td><td>JSON array of extra <code class="inline">{"hostname","port"}</code> objects (max four)</td></tr>
<tr><td>LDAP_TLS_MODE</td><td>ldaps</td><td><code class="inline">ldaps</code> or <code class="inline">starttls</code>; TLS 1.2+ and hostname validation always required</td></tr>
<tr><td>LDAP_CA_FILE</td><td>(system trust)</td><td>PEM CA bundle added to system trust</td></tr>
<tr><td>LDAP_LOOKUP_USER / LDAP_LOOKUP_PASSWORD</td><td>required</td><td>Lookup DN and password (secret), unless <code class="inline">LDAP_ANONYMOUS_LOOKUP=true</code></td></tr>
<tr><td>LDAP_BASE_DN</td><td>required</td><td>Base DN</td></tr>
<tr><td>LDAP_USER_ELIGIBILITY_FILTER</td><td>(empty)</td><td>Required for OpenLDAP, e.g. <code class="inline">(employeeType=active)</code>; AD uses a built-in policy</td></tr>
<tr><td>LDAP_SEARCH_NESTED_GROUPS</td><td>false</td><td>Include nested group membership</td></tr>
<tr><td>LDAP_REVALIDATION_INTERVAL_SECONDS</td><td>300</td><td>Maximum age of a successful eligibility check (max 300)</td></tr>
<tr><td>LDAP_CONNECT_TIMEOUT_SECONDS / LDAP_OPERATION_TIMEOUT_SECONDS / LDAP_REQUEST_TIMEOUT_SECONDS</td><td>10 / 10 / 30</td><td>Timeouts (max 30 / 30 / 60)</td></tr>
<tr><td>LDAP_MAX_CONCURRENT_OPERATIONS</td><td>10</td><td>Max 100; excess requests are rejected temporarily</td></tr>
</tbody>
</table>
<p>The user and group attribute mappings (<code class="inline">LDAP_USER_*</code>, <code class="inline">LDAP_GROUP_*</code>) default to sensible values per directory type, and nested-group and search limits are tunable. See the <a href="https://github.com/example42/pabawi/blob/main/docs/configuration.md#ldap-directory-service" target="_blank" rel="noopener">upstream configuration guide</a> for the full list. LDAP administration endpoints require <code class="inline">rbac:admin</code>.</p>
</div>
<div class="docs-section" id="whitelist-config">
<h2>Command Whitelist</h2>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>COMMAND_WHITELIST_ALLOW_ALL</td><td>false</td><td><code class="inline">true</code> skips the whitelist check (dev only)</td></tr>
<tr><td>COMMAND_WHITELIST</td><td>[]</td><td>JSON array of allowed commands</td></tr>
<tr><td>COMMAND_WHITELIST_MATCH_MODE</td><td>exact</td><td><code class="inline">exact</code> (full match) or <code class="inline">prefix</code> (starts-with)</td></tr>
</tbody>
</table>
<div class="docs-code">
<pre>
<span class="c-cmt"># Exact match</span>
<span class="c-key">COMMAND_WHITELIST</span>=<span class="c-val">'["uptime","df -h","free -m","systemctl status nginx"]'</span>
<span class="c-key">COMMAND_WHITELIST_MATCH_MODE</span>=<span class="c-val">exact</span>
<span class="c-cmt"># Prefix match (allows "systemctl status *")</span>
<span class="c-key">COMMAND_WHITELIST</span>=<span class="c-val">'["systemctl status","journalctl -u"]'</span>
<span class="c-key">COMMAND_WHITELIST_MATCH_MODE</span>=<span class="c-val">prefix</span></pre>
</div>
<p>The whitelist is enforced on <strong>every</strong> command-execution path: single-node, multi-node batch, and re-execution. Shell metacharacters (<code class="inline">; | & ` $() {} * ? [] ~ < ></code>, backslash and newlines) and commands beginning with <code class="inline">-</code> are <strong>always</strong> rejected, even with <code class="inline">COMMAND_WHITELIST_ALLOW_ALL=true</code>.</p>
<div class="docs-callout warn">
<p><strong>Never</strong> set <code class="inline">COMMAND_WHITELIST_ALLOW_ALL=true</code> in production, and never whitelist destructive commands (<code class="inline">rm</code>, <code class="inline">dd</code>, <code class="inline">mkfs</code>).</p>
</div>
</div>
<div class="docs-section" id="runtime-config">
<h2>Streaming, Console, Cache & Queue</h2>
<h3>Execution queue</h3>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>CONCURRENT_EXECUTION_LIMIT</td><td>5</td><td>Shared per-process concurrency for commands, tasks, playbooks, packages, Puppet runs, re-execution and batches</td></tr>
<tr><td>MAX_QUEUE_SIZE</td><td>50</td><td>Max queued executions before rejecting</td></tr>
</tbody>
</table>
<h3>Caching</h3>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>CACHE_INVENTORY_TTL</td><td>30000</td><td>Inventory cache TTL in ms (must be positive)</td></tr>
<tr><td>CACHE_FACTS_TTL</td><td>300000</td><td>Node facts cache TTL in ms (must be positive)</td></tr>
</tbody>
</table>
<h3>Streaming</h3>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>STREAMING_BUFFER_MS</td><td>100</td><td>Output batch interval in ms (also the worst-case delay)</td></tr>
<tr><td>STREAMING_MAX_OUTPUT_SIZE</td><td>10485760</td><td>Max output per execution in bytes (10 MB)</td></tr>
<tr><td>STREAMING_MAX_LINE_LENGTH</td><td>10000</td><td>Max characters per line before truncation</td></tr>
</tbody>
</table>
<h3>Console (VNC / Terminal)</h3>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>CONSOLE_SESSION_TIMEOUT_MS</td><td>300000</td><td>Idle session timeout (5 min)</td></tr>
<tr><td>CONSOLE_MAX_SESSION_DURATION</td><td>28800000</td><td>Absolute session lifetime (8 h)</td></tr>
<tr><td>CONSOLE_MAX_CONCURRENT_SESSIONS</td><td>3</td><td>Max sessions per user per process</td></tr>
<tr><td>CONSOLE_HEARTBEAT_INTERVAL_MS</td><td>30000</td><td>Heartbeat interval (must be below the idle timeout)</td></tr>
<tr><td>CONSOLE_VERIFY_UPSTREAM_TLS</td><td>true</td><td>Verify the upstream console host certificate. Set <code class="inline">false</code> only on trusted networks with self-signed certs</td></tr>
</tbody>
</table>
</div>
<div class="docs-section" id="safety-config">
<h2>Provisioning Safety</h2>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>ALLOW_DESTRUCTIVE_PROVISIONING</td><td>false</td><td>Allow destroy/terminate/deallocate across all provisioning plugins. Returns <code class="inline">403 DESTRUCTIVE_ACTION_DISABLED</code> when false</td></tr>
</tbody>
</table>
<p>Keep it <code class="inline">false</code> in production to prevent accidental VM/instance deletion.</p>
</div>
<!-- ===== INTEGRATIONS ===== -->
<div class="docs-section" id="integrations">
<h2>Integrations Overview</h2>
<p>Pabawi uses a plugin architecture. Each integration is independent: a failed initialization is logged and the others keep working. <code class="inline">IntegrationManager</code> merges inventory and facts from all enabled plugins, and higher source priorities win during merging.</p>
<table class="docs-table">
<thead><tr><th>Plugin</th><th>Priority</th><th>Type</th><th>Provides</th></tr></thead>
<tbody>
<tr><td>SSH</td><td>50 (highest)</td><td>Information + execution</td><td>Inventory, commands</td></tr>
<tr><td>PuppetDB</td><td>10</td><td>Information</td><td>Inventory, facts, reports, catalogs, events</td></tr>
<tr><td>Puppetserver</td><td>8</td><td>Information</td><td>Inventory, facts, catalogs, environments, metrics</td></tr>
<tr><td>Checkmk</td><td>8</td><td>Information</td><td>Host inventory, live service status, state-change events</td></tr>
<tr><td>Proxmox</td><td>7</td><td>Information + execution</td><td>Inventory, lifecycle, provisioning (VMs & LXC)</td></tr>
<tr><td>AWS</td><td>7</td><td>Information + execution</td><td>Inventory, lifecycle, provisioning (EC2)</td></tr>
<tr><td>Azure</td><td>7</td><td>Information + execution</td><td>Inventory, lifecycle, provisioning (VMs)</td></tr>
<tr><td>Hiera</td><td>6</td><td>Information</td><td>Hierarchy browsing, lookups, code analysis</td></tr>
<tr><td>Bolt</td><td>5</td><td>Information + execution</td><td>Inventory, commands, tasks, Puppet runs, packages</td></tr>
<tr><td>Ansible</td><td>5</td><td>Information + execution</td><td>Inventory, commands, playbooks, packages</td></tr>
</tbody>
</table>
<p>SSH and Proxmox also accept priority settings (<code class="inline">SSH_PRIORITY</code>, <code class="inline">PROXMOX_PRIORITY</code>). Each integration has an in-app setup guide that generates a ready-to-paste <code class="inline">.env</code> snippet.</p>
</div>
<div class="docs-section" id="bolt">
<h2>Bolt Integration</h2>
<p>Bolt is Pabawi's primary execution tool: command execution, task execution, Puppet runs, and package installation. Pabawi spawns the Bolt CLI directly and uses your existing Bolt project and inventory as-is. The Bolt CLI must be installed on the host (it is bundled in the <code class="inline">batteries</code> image).</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>BOLT_PROJECT_PATH</td><td>cwd</td><td>Bolt project directory containing <code class="inline">inventory.yaml</code> and <code class="inline">bolt-project.yaml</code></td></tr>
<tr><td>BOLT_EXECUTION_TIMEOUT</td><td>300000</td><td>Max execution time per command in ms (5 min)</td></tr>
<tr><td>BOLT_PACKAGE_TASKS</td><td>built-in <code class="inline">package</code></td><td>JSON array of package task definitions backing the Install Package UI</td></tr>
</tbody>
</table>
<div class="docs-code">
<pre>
<span class="c-cmt"># bolt-project.yaml: color: false is critical, ANSI codes break JSON parsing</span>
<span class="c-key">name</span>: my-project
<span class="c-key">modulepath</span>:
- modules
<span class="c-key">color</span>: <span class="c-val">false</span>
<span class="c-cmt"># Package tasks: parameterMapping maps UI fields to task parameter names</span>
<span class="c-key">BOLT_PACKAGE_TASKS</span>=<span class="c-val">'[{"name":"tp::install","label":"Tiny Puppet","parameterMapping":{"packageName":"app","ensure":"ensure","settings":"settings"}}]'</span></pre>
</div>
<div class="docs-callout">
<p><strong>Note:</strong> Commands are validated against the <a href="#whitelist-config">command whitelist</a> before execution. Bolt tasks are discovered from the project's installed modules (<code class="inline">bolt module install</code>).</p>
</div>
</div>
<div class="docs-section" id="ansible">
<h2>Ansible Integration</h2>
<p>Ad-hoc command execution, package installation, and playbook execution on managed hosts. Requires <code class="inline">ansible</code> and <code class="inline">ansible-playbook</code> in <code class="inline">PATH</code> and a reachable inventory (INI or YAML).</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>ANSIBLE_ENABLED</td><td>false</td><td>Set to <code class="inline">true</code> to enable</td></tr>
<tr><td>ANSIBLE_PROJECT_PATH</td><td>cwd</td><td>Ansible project directory (working directory for commands)</td></tr>
<tr><td>ANSIBLE_INVENTORY_PATH</td><td>inventory/hosts</td><td>Inventory file path relative to the project, or absolute</td></tr>
<tr><td>ANSIBLE_EXECUTION_TIMEOUT</td><td>300000</td><td>Max execution time in ms</td></tr>
</tbody>
</table>
<p>Place playbooks in the project (e.g. <code class="inline">playbooks/</code>); they appear in the <strong>Run Playbook</strong> section on node detail pages. Validate from the CLI first:</p>
<div class="docs-code">
<pre>
<span class="c-prompt">$ </span><span class="c-cmd">cd $ANSIBLE_PROJECT_PATH</span>
<span class="c-prompt">$ </span><span class="c-cmd">ansible all -i inventory/hosts -m ping</span>
<span class="c-prompt">$ </span><span class="c-cmd">ansible-playbook -i inventory/hosts playbooks/site.yml --check</span></pre>
</div>
</div>
<div class="docs-section" id="ssh">
<h2>SSH Integration</h2>
<p>Direct SSH execution without Bolt or Ansible, using the embedded SSH client. Hosts are discovered from an OpenSSH-format config file. Ideal for homelabs or nodes outside Puppet/Ansible.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>SSH_ENABLED</td><td>false</td><td>Enable SSH</td></tr>
<tr><td>SSH_CONFIG_PATH</td><td>unset</td><td>OpenSSH inventory config path</td></tr>
<tr><td>SSH_DEFAULT_USER</td><td>required when enabled</td><td>Default remote username</td></tr>
<tr><td>SSH_DEFAULT_PORT</td><td>22</td><td>Remote port</td></tr>
<tr><td>SSH_DEFAULT_KEY</td><td>unset</td><td>Private-key file path</td></tr>
<tr><td>SSH_HOST_KEY_CHECK</td><td>true</td><td>Require a trusted server fingerprint</td></tr>
<tr><td>SSH_HOST_FINGERPRINTS_PATH</td><td>unset</td><td>Managed JSON trust file, required for verified connections</td></tr>
<tr><td>SSH_CONNECTION_TIMEOUT</td><td>30</td><td>Connection timeout in seconds (5-300)</td></tr>
<tr><td>SSH_COMMAND_TIMEOUT</td><td>300</td><td>Command timeout in seconds (10-3600)</td></tr>
<tr><td>SSH_MAX_CONNECTIONS</td><td>50</td><td>Pool size (1-1000)</td></tr>
<tr><td>SSH_MAX_CONNECTIONS_PER_HOST</td><td>5</td><td>Connections per host (1-100)</td></tr>
<tr><td>SSH_IDLE_TIMEOUT</td><td>300</td><td>Pool idle timeout in seconds (10-3600)</td></tr>
<tr><td>SSH_CONCURRENCY_LIMIT</td><td>10</td><td>Parallel SSH commands (1-100)</td></tr>
<tr><td>SSH_PRIORITY</td><td>50</td><td>Source priority</td></tr>
<tr><td>SSH_SUDO_ENABLED</td><td>false</td><td>Enable privilege escalation</td></tr>
<tr><td>SSH_SUDO_COMMAND</td><td>sudo</td><td>Escalation command</td></tr>
<tr><td>SSH_SUDO_PASSWORDLESS</td><td>true</td><td>Use passwordless escalation</td></tr>
<tr><td>SSH_SUDO_PASSWORD</td><td>unset</td><td>Escalation password when passwordless is false (secret)</td></tr>
<tr><td>SSH_SUDO_USER</td><td>root</td><td>Escalation target user</td></tr>
</tbody>
</table>
<h3>SSH config file</h3>
<p>Supported keywords: <code class="inline">Host</code>, <code class="inline">HostName</code>, <code class="inline">User</code>, <code class="inline">Port</code>, <code class="inline">IdentityFile</code>. A comment <code class="inline"># Groups: group1,group2</code> before a <code class="inline">Host</code> block assigns inventory groups.</p>
<div class="docs-code">
<pre>
<span class="c-cmt"># Groups: webservers,production</span>
Host web01
HostName 192.168.1.10
User deploy
Port 22
IdentityFile ~/.ssh/deploy_key</pre>
</div>
<h3>Host trust</h3>
<p>With <code class="inline">SSH_HOST_KEY_CHECK=true</code> (the default), Pabawi uses an operator-managed JSON map of destination to accepted OpenSSH SHA-256 fingerprints. It does not read <code class="inline">known_hosts</code>, implement trust on first use, or accept host certificates. Unknown or changed keys and missing or malformed trust files fail closed before authentication.</p>
<div class="docs-code">
<pre>
{
"[192.168.1.10]:22": ["SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"[db.example.com]:2222": ["SHA256:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"]
}</pre>
</div>
<ul>
<li>Use lowercase names, brackets and an explicit port (including 22); IPv6 uses <code class="inline">[::1]:2222</code></li>
<li>Get fingerprints from a trusted console: <code class="inline">ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256</code>. <code class="inline">ssh-keyscan</code> alone is not proof of identity</li>
<li>Mount the file read-only. Restart the SSH integration after trust changes: updating pins does not revoke already-authenticated pooled sessions</li>
<li>Multiple fingerprints per endpoint allow planned key rotation. <code class="inline">SSH_HOST_KEY_CHECK=false</code> disables protection and logs a warning</li>
</ul>
</div>
<div class="docs-section" id="puppetdb">
<h2>PuppetDB Integration</h2>
<p>Node inventory, facts, Puppet run reports, catalogs, and events. Requires PuppetDB 6.0+, reachable on port 8081 (HTTPS) or 8080 (HTTP, localhost), with client certificates signed by the Puppetserver CA or a PE auth token.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>PUPPETDB_ENABLED</td><td>false</td><td>Set to <code class="inline">true</code> to enable</td></tr>
<tr><td>PUPPETDB_SERVER_URL</td><td>required</td><td>PuppetDB URL (e.g. <code class="inline">https://puppetdb:8081</code>)</td></tr>
<tr><td>PUPPETDB_PORT</td><td>from URL</td><td>Override port</td></tr>
<tr><td>PUPPETDB_TOKEN</td><td>unset</td><td>Authentication token (secret)</td></tr>
<tr><td>PUPPETDB_TIMEOUT</td><td>30000</td><td>Request timeout in ms</td></tr>
<tr><td>PUPPETDB_RETRY_ATTEMPTS / PUPPETDB_RETRY_DELAY</td><td>3 / 1000</td><td>Retries on failure, delay in ms</td></tr>
<tr><td>PUPPETDB_CACHE_TTL</td><td>300000</td><td>Response cache TTL in ms</td></tr>
<tr><td>PUPPETDB_SSL_ENABLED</td><td>true</td><td>Enable SSL verification</td></tr>
<tr><td>PUPPETDB_SSL_CA / _CERT / _KEY</td><td>unset</td><td>Paths to CA, client certificate and client key</td></tr>
<tr><td>PUPPETDB_SSL_REJECT_UNAUTHORIZED</td><td>true</td><td>Reject invalid certs; <code class="inline">false</code> for self-signed</td></tr>
<tr><td>PUPPETDB_CIRCUIT_BREAKER_THRESHOLD</td><td>5</td><td>Failures before opening the circuit</td></tr>
<tr><td>PUPPETDB_CIRCUIT_BREAKER_TIMEOUT / _RESET_TIMEOUT</td><td>60000 / 30000</td><td>Open time and half-open delay in ms</td></tr>
</tbody>
</table>
<div class="docs-code">
<pre>
<span class="c-cmt"># Remote HTTPS with SSL client certs</span>
<span class="c-key">PUPPETDB_ENABLED</span>=<span class="c-val">true</span>
<span class="c-key">PUPPETDB_SERVER_URL</span>=<span class="c-val">https://puppetdb.example.com</span>
<span class="c-key">PUPPETDB_PORT</span>=<span class="c-val">8081</span>
<span class="c-key">PUPPETDB_SSL_CA</span>=<span class="c-val">/opt/pabawi/certs/ca.pem</span>
<span class="c-key">PUPPETDB_SSL_CERT</span>=<span class="c-val">/opt/pabawi/certs/client.crt</span>
<span class="c-key">PUPPETDB_SSL_KEY</span>=<span class="c-val">/opt/pabawi/certs/client.key</span></pre>
</div>
<p>If Pabawi runs on a different host than the Puppetserver, generate a certificate there (<code class="inline">puppetserver ca generate --certname pabawi.example.com</code>) and copy the cert, key and CA. For Puppet Enterprise, set <code class="inline">PUPPETDB_TOKEN</code> instead.</p>
</div>
<div class="docs-section" id="puppetserver">
<h2>Puppetserver Integration</h2>
<p>Node inventory (from the CA), node status, facts, catalog compilation and cross-environment comparison, environment management, and service metrics. Requires Puppetserver 6.x or 7.x on port 8140, with an SSL client certificate or PE RBAC token.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>PUPPETSERVER_ENABLED</td><td>false</td><td>Set to <code class="inline">true</code> to enable</td></tr>
<tr><td>PUPPETSERVER_SERVER_URL</td><td>required</td><td>Puppetserver URL (e.g. <code class="inline">https://puppet:8140</code>)</td></tr>
<tr><td>PUPPETSERVER_PORT</td><td>from URL</td><td>Override port</td></tr>
<tr><td>PUPPETSERVER_TOKEN</td><td>unset</td><td>Authentication token (secret)</td></tr>
<tr><td>PUPPETSERVER_TIMEOUT</td><td>30000</td><td>Request timeout in ms</td></tr>
<tr><td>PUPPETSERVER_RETRY_ATTEMPTS / _RETRY_DELAY</td><td>3 / 1000</td><td>Retries on failure, delay in ms</td></tr>
<tr><td>PUPPETSERVER_INACTIVITY_THRESHOLD</td><td>3600</td><td>Seconds without check-in before a node is considered inactive</td></tr>
<tr><td>PUPPETSERVER_CACHE_TTL</td><td>300000</td><td>Response cache TTL in ms</td></tr>
<tr><td>PUPPETSERVER_SSL_ENABLED</td><td>true</td><td>Enable SSL verification</td></tr>
<tr><td>PUPPETSERVER_SSL_CA / _CERT / _KEY</td><td>unset</td><td>Paths to CA, client certificate and client key</td></tr>
<tr><td>PUPPETSERVER_SSL_REJECT_UNAUTHORIZED</td><td>true</td><td>Reject invalid certs</td></tr>
<tr><td>PUPPETSERVER_CIRCUIT_BREAKER_THRESHOLD / _TIMEOUT / _RESET_TIMEOUT</td><td>5 / 60000 / 30000</td><td>Circuit breaker settings</td></tr>
</tbody>
</table>
<div class="docs-callout">
<p><strong>cli_auth extension:</strong> for Pabawi on a separate host, the client certificate needs the <code class="inline">cli_auth</code> extension (OID <code class="inline">1.3.6.1.4.1.34380.1.3.39</code>) for CA management endpoints; without it, certificate lists fall back to PuppetDB data. Use <code class="inline">./scripts/generate-pabawi-cert.sh</code> from the Pabawi repository on the Puppetserver, then <code class="inline">puppetserver ca sign --certname pabawi</code>. Puppetserver only lists nodes with signed certificates.</p>
</div>
</div>
<div class="docs-section" id="hiera">
<h2>Hiera Integration</h2>
<p>Browse and resolve Hiera data from your Puppet control repository: hierarchy browser, node lookups, key search, cross-node view, code analysis, and optional catalog compilation. Requires read access to a control repo containing <code class="inline">hiera.yaml</code>.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>HIERA_ENABLED</td><td>false</td><td>Set to <code class="inline">true</code> to enable</td></tr>
<tr><td>HIERA_CONTROL_REPO_PATH</td><td>required</td><td>Path to the Puppet control repo</td></tr>
<tr><td>HIERA_CONFIG_PATH</td><td>hiera.yaml</td><td>Hiera config file relative to the control repo</td></tr>
<tr><td>HIERA_ENVIRONMENTS</td><td>["production"]</td><td>JSON array of environments</td></tr>
<tr><td>HIERA_FACT_SOURCE_PREFER_PUPPETDB</td><td>true</td><td>Use PuppetDB for fact interpolation when available</td></tr>
<tr><td>HIERA_FACT_SOURCE_LOCAL_PATH</td><td>unset</td><td>Local per-node JSON facts directory (fallback)</td></tr>
<tr><td>HIERA_CATALOG_COMPILATION_ENABLED</td><td>false</td><td>Enable catalog compilation (requires the Puppet CLI)</td></tr>
<tr><td>HIERA_CATALOG_COMPILATION_TIMEOUT / _CACHE_TTL</td><td>60000 / 300000</td><td>Timeout and cache TTL in ms</td></tr>
<tr><td>HIERA_CACHE_ENABLED</td><td>true</td><td>Enable the Hiera data cache</td></tr>
<tr><td>HIERA_CACHE_TTL</td><td>300000</td><td>Cache TTL in ms</td></tr>
<tr><td>HIERA_CACHE_MAX_ENTRIES</td><td>10000</td><td>Max cache entries</td></tr>
<tr><td>HIERA_CODE_ANALYSIS_ENABLED</td><td>true</td><td>Enable Puppet code analysis</td></tr>
<tr><td>HIERA_CODE_ANALYSIS_LINT_ENABLED</td><td>true</td><td>Run puppet-lint</td></tr>
<tr><td>HIERA_CODE_ANALYSIS_MODULE_UPDATE_CHECK</td><td>true</td><td>Check for module updates</td></tr>
<tr><td>HIERA_CODE_ANALYSIS_INTERVAL</td><td>3600000</td><td>Analysis interval in ms (1 hour)</td></tr>
<tr><td>HIERA_CODE_ANALYSIS_EXCLUSION_PATTERNS</td><td>[]</td><td>JSON array of glob patterns to exclude, e.g. <code class="inline">["vendor/","spec/"]</code></td></tr>
</tbody>
</table>
</div>
<div class="docs-section" id="checkmk">
<h2>Checkmk Integration</h2>
<p>Host inventory (priority 8), live service monitoring status, and state-change events. Inventory and monitoring data are fetched live; health probes are cached. Incomplete credentials skip plugin registration with a warning.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>CHECKMK_ENABLED</td><td>false</td><td>Set to <code class="inline">true</code> to enable</td></tr>
<tr><td>CHECKMK_SERVER_URL</td><td>required</td><td>Checkmk server URL (e.g. <code class="inline">https://checkmk.example.com</code>)</td></tr>
<tr><td>CHECKMK_SITE</td><td>unset</td><td>Site name, needed unless the URL already includes the site or full API path</td></tr>
<tr><td>CHECKMK_USERNAME</td><td>required</td><td>Automation user name</td></tr>
<tr><td>CHECKMK_PASSWORD</td><td>required</td><td>Automation user secret (secret)</td></tr>
<tr><td>CHECKMK_SSL_VERIFY</td><td>true</td><td>Certificate verification for REST and TLS Livestatus</td></tr>
<tr><td>CHECKMK_HEALTHCHECK_INTERVAL_MS</td><td>300000</td><td>Health probe cache interval in ms</td></tr>
<tr><td>CHECKMK_LIVESTATUS_HOST</td><td>unset</td><td>Enables Livestatus history when set</td></tr>
<tr><td>CHECKMK_LIVESTATUS_PORT</td><td>6557</td><td>TCP/TLS listener port</td></tr>
<tr><td>CHECKMK_LIVESTATUS_TLS</td><td>false</td><td>Set to exactly <code class="inline">true</code> for a TLS listener</td></tr>
<tr><td>CHECKMK_LIVESTATUS_TIMEOUT_MS</td><td>5000</td><td>Livestatus query timeout in ms</td></tr>
</tbody>
</table>
<p>Requires the <code class="inline">checkmk:read</code> permission. Without Livestatus, event history is derived from the REST API at reduced fidelity.</p>
</div>
<div class="docs-section" id="proxmox">
<h2>Proxmox Integration</h2>
<p>Discover VMs and LXC containers, manage their lifecycle, and provision new guests. Requires a Proxmox VE cluster with API access on port 8006.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>PROXMOX_ENABLED</td><td>false</td><td>Set to <code class="inline">true</code> to enable</td></tr>
<tr><td>PROXMOX_HOST</td><td>required</td><td>Proxmox VE host, without scheme (e.g. <code class="inline">pve.example.com</code>)</td></tr>
<tr><td>PROXMOX_PORT</td><td>8006</td><td>API port</td></tr>
<tr><td>PROXMOX_TOKEN</td><td>unset</td><td>API token <code class="inline">USER@REALM!TOKENID=UUID</code> (recommended, secret)</td></tr>
<tr><td>PROXMOX_USERNAME</td><td>unset</td><td>Username (e.g. <code class="inline">root@pam</code>) for password auth</td></tr>
<tr><td>PROXMOX_PASSWORD</td><td>unset</td><td>Password (use token auth in production, secret)</td></tr>
<tr><td>PROXMOX_REALM</td><td>unset</td><td>Auth realm override (<code class="inline">pam</code> or <code class="inline">pve</code>)</td></tr>
<tr><td>PROXMOX_TIMEOUT</td><td>30000</td><td>Request timeout in ms</td></tr>
<tr><td>PROXMOX_PRIORITY</td><td>7</td><td>Data source priority</td></tr>
<tr><td>PROXMOX_SSL_REJECT_UNAUTHORIZED</td><td>true</td><td>Reject invalid certs; <code class="inline">false</code> for self-signed</td></tr>
<tr><td>PROXMOX_SSL_CA / _CERT / _KEY</td><td>unset</td><td>Paths to CA, client certificate and client key</td></tr>
</tbody>
</table>
<h3>Authentication</h3>
<p>Create a token in the Proxmox UI under <strong>Datacenter → Permissions → API Tokens</strong>. The token user needs <code class="inline">VM.Allocate</code>, <code class="inline">VM.Config.*</code>, <code class="inline">VM.PowerMgmt</code>, <code class="inline">VM.Audit</code> and <code class="inline">Datastore.Allocate</code>. Password auth obtains a ticket that is refreshed automatically every 2 hours.</p>
<h3>Capabilities</h3>
<ul>
<li><strong>Inventory</strong> of all VMs and LXC containers, grouped by Proxmox node, status or type (qemu/lxc)</li>
<li><strong>Facts</strong>: VM config, status, resource usage</li>
<li><strong>Lifecycle</strong>: start, stop, shutdown, reboot</li>
<li><strong>Provisioning</strong>: create a VM from a template, create an LXC container</li>
<li><strong>Destroy</strong>: blocked unless <code class="inline">ALLOW_DESTRUCTIVE_PROVISIONING=true</code></li>
</ul>
<p>Guest IDs follow the format <code class="inline">proxmox:<node>:<vmid></code> (e.g. <code class="inline">proxmox:pve01:100</code>).</p>
</div>
<div class="docs-section" id="aws">
<h2>AWS EC2 Integration</h2>
<p>Discover EC2 instances across regions, manage their lifecycle, and launch new instances. Requires an AWS account with EC2 access and IAM credentials (access key, instance profile, or assumed role).</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>AWS_ENABLED</td><td>false</td><td>Set to <code class="inline">true</code> to enable</td></tr>
<tr><td>AWS_ACCESS_KEY_ID</td><td>unset</td><td>Access key (or use an instance role)</td></tr>
<tr><td>AWS_SECRET_ACCESS_KEY</td><td>unset</td><td>Secret key (secret)</td></tr>
<tr><td>AWS_DEFAULT_REGION</td><td>us-east-1</td><td>Default region</td></tr>
<tr><td>AWS_REGIONS</td><td>unset</td><td>JSON array or comma-separated list of regions to query</td></tr>
<tr><td>AWS_SESSION_TOKEN</td><td>unset</td><td>Session token for temporary credentials (secret)</td></tr>
<tr><td>AWS_PROFILE</td><td>unset</td><td>AWS CLI profile name</td></tr>
<tr><td>AWS_ENDPOINT</td><td>unset</td><td>Override endpoint URL (for LocalStack etc.)</td></tr>
</tbody>
</table>
<p>Omit the credentials to use the AWS SDK default chain (environment, <code class="inline">~/.aws/credentials</code>, instance profile).</p>
<h3>Required IAM Permissions</h3>
<div class="docs-code">
<pre>
ec2:DescribeInstances, DescribeRegions, DescribeInstanceTypes
ec2:DescribeImages, DescribeVpcs, DescribeSubnets
ec2:DescribeSecurityGroups, DescribeKeyPairs
ec2:RunInstances, StartInstances, StopInstances
ec2:RebootInstances, TerminateInstances
sts:GetCallerIdentity</pre>
</div>
<p>Remove <code class="inline">ec2:TerminateInstances</code> to also enforce <code class="inline">ALLOW_DESTRUCTIVE_PROVISIONING=false</code> at the IAM level.</p>
<h3>Capabilities</h3>
<ul>
<li><strong>Inventory</strong> across all configured regions, grouped by region, VPC or tag</li>
<li><strong>Facts</strong>: instance type, AMI, state, IP, tags, launch time</li>
<li><strong>Lifecycle</strong>: start, stop, reboot</li>
<li><strong>Launch</strong> with AMI, instance type, subnet, security group and key pair</li>
<li><strong>Terminate</strong>: blocked unless <code class="inline">ALLOW_DESTRUCTIVE_PROVISIONING=true</code></li>
</ul>
<p>A failure in one region does not block discovery in the others.</p>
</div>
<div class="docs-section" id="azure">
<h2>Azure Integration</h2>
<p>Discover Virtual Machines across resource groups, manage their lifecycle, and provision new instances. This is independent of the <code class="inline">ENTRA_ID_*</code> login settings.</p>
<table class="docs-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td>AZURE_ENABLED</td><td>false</td><td>Set to exactly <code class="inline">true</code> to enable</td></tr>
<tr><td>AZURE_SUBSCRIPTION_ID</td><td>required when enabled</td><td>Subscription to manage</td></tr>
<tr><td>AZURE_TENANT_ID</td><td>unset</td><td>Tenant ID for service-principal authentication</td></tr>
<tr><td>AZURE_CLIENT_ID</td><td>unset</td><td>Service-principal client ID</td></tr>
<tr><td>AZURE_CLIENT_SECRET</td><td>unset</td><td>Service-principal secret (secret)</td></tr>
<tr><td>AZURE_RESOURCE_GROUPS</td><td>unset</td><td>JSON array or comma-separated list; unset queries all resource groups</td></tr>
</tbody>
</table>
<h3>Authentication</h3>
<p>Service Principal (recommended for production), Managed Identity (Azure-hosted Pabawi), or Azure CLI login (<code class="inline">az login</code>, development), all through <code class="inline">DefaultAzureCredential</code>. When tenant, client ID and secret are all set, <code class="inline">ClientSecretCredential</code> is used directly.</p>
<h3>Required Permissions</h3>
<p>Assign the built-in <strong>Virtual Machine Contributor</strong> role, scoped to the subscription or target resource groups, plus <code class="inline">Microsoft.Subscription/subscriptions/read</code> for health checks. A custom role can use the minimum set of <code class="inline">Microsoft.Compute/virtualMachines/*</code> (read, write, delete, start, powerOff, restart, deallocate, instanceView), network interface and public IP read, and resource group and location read actions. Remove the <code class="inline">deallocate/action</code> permission to also enforce <code class="inline">ALLOW_DESTRUCTIVE_PROVISIONING=false</code> at the Azure RBAC level.</p>
<h3>Capabilities</h3>
<ul>
<li><strong>Inventory</strong> across configured resource groups (or the entire subscription), grouped by location, resource group or tag (Environment, Project, Team, Application)</li>
<li><strong>Facts</strong>: VM size, OS, power state, IPs, tags, disks, provisioning state</li>
<li><strong>Lifecycle</strong>: start, stop, restart</li>
<li><strong>Provisioning</strong>: create VMs with image, size, networking and auth configuration</li>
<li><strong>Deallocate</strong>: blocked unless <code class="inline">ALLOW_DESTRUCTIVE_PROVISIONING=true</code></li>
<li><strong>Resource discovery</strong>: browse locations, VM sizes, marketplace images and resource groups</li>
</ul>
</div>
<!-- ===== USING PABAWI ===== -->
<div class="docs-section" id="navigation">
<h2>Navigation</h2>
<p>All integrations that are enabled and connected are available from the same interface. The top navigation bar has these sections:</p>
<table class="docs-table">
<thead><tr><th>Section</th><th>What it shows</th></tr></thead>
<tbody>
<tr><td>Home</td><td>Integration status, aggregated Puppet run chart, system health</td></tr>
<tr><td>Inventory</td><td>All nodes from all enabled information sources</td></tr>
<tr><td>Executions</td><td>Full execution history with filtering</td></tr>
<tr><td>Manage</td><td>Provisioning (Proxmox/AWS/Azure), visible when a provisioning integration is enabled</td></tr>
<tr><td>Journal</td><td>Cross-node event timeline, visible when at least one journal source is enabled</td></tr>
<tr><td>Expert Mode toggle</td><td>Adds diagnostic detail to all pages</td></tr>
</tbody>
</table>
</div>
<div class="docs-section" id="inventory">
<h2>Inventory</h2>
<p>The Inventory page aggregates nodes from every enabled information source (Bolt, PuppetDB, Puppetserver, Ansible, SSH, Proxmox, AWS, Azure, Checkmk). Nodes that appear in multiple sources are linked and shown as one entry with a <strong>sources</strong> badge.</p>
<ul>
<li><strong>Search</strong> by node name; narrow further with the transport or source dropdown</li>
<li><strong>View modes</strong>: toggle between grid and list view</li>
<li><strong>Virtual scrolling</strong> handles inventories of 1000+ nodes without lag</li>
<li><strong>Color coding</strong>: each integration has its own color on badges and tabs (Bolt orange, PuppetDB purple, Puppetserver dark blue, Hiera dark red, Ansible green, SSH teal, Proxmox blue, AWS cyan, Azure azure blue)</li>
<li>Inventory is cached for 30 s by default (<code class="inline">CACHE_INVENTORY_TTL</code>); the API also accepts a PuppetDB PQL filter</li>
</ul>
</div>
<div class="docs-section" id="node-detail">
<h2>Node Detail & Facts</h2>
<p>Click any node to open its detail page. Sections are collapsible and depend on which integrations are enabled:</p>
<table class="docs-table">
<thead><tr><th>Section</th><th>Requires</th></tr></thead>
<tbody>
<tr><td>Node info</td><td>Any source</td></tr>
<tr><td>Facts</td><td>Bolt, PuppetDB, or Puppetserver</td></tr>
<tr><td>Hiera data</td><td>Hiera</td></tr>
<tr><td>PuppetDB reports</td><td>PuppetDB</td></tr>
<tr><td>Run Puppet / Run task / Install package</td><td>Bolt</td></tr>
<tr><td>Execute command</td><td>Bolt, Ansible or SSH</td></tr>
<tr><td>Execution history</td><td>Always</td></tr>
</tbody>
</table>
<p>To gather facts, open the <strong>Facts</strong> section and click <strong>Gather Facts</strong>. Facts from multiple sources appear in separate tabs labeled with their source color. They are cached per node (default 5 minutes); click again to force a refresh.</p>
</div>
<div class="docs-section" id="execution">
<h2>Command Execution</h2>
<p>Open the <strong>Execute Command</strong> section on a node, type a command (e.g. <code class="inline">uptime</code>), select the execution tool if several are available, and click <strong>Execute</strong>. Output streams live via Server-Sent Events; results show stdout, stderr, exit code and duration.</p>
<ul>
<li><strong>Whitelist</strong>: commands are validated against <a href="#whitelist-config">the whitelist</a>. Rejected commands show <code class="inline">Command not in whitelist</code></li>
<li><strong>SSH</strong>: runs directly on the target without Bolt, using the <code class="inline">SSH_*</code> configuration</li>
<li><strong>Ansible</strong>: runs ad-hoc module calls (the <code class="inline">shell</code> module by default) against the configured inventory; playbooks run from the Run Playbook section</li>
<li><strong>Queue</strong>: executions are limited to <code class="inline">CONCURRENT_EXECUTION_LIMIT</code> parallel operations, up to <code class="inline">MAX_QUEUE_SIZE</code> queued</li>
<li><strong>Streaming</strong>: auto-scroll follows new output; click the output window to pause it. If streaming drops, completed output is preserved</li>
</ul>
<p>Batch execution across multiple nodes or groups, re-execution, and cancellation are available from the Executions page and the API. Cancelling after dispatch records the request but does not guarantee the provider terminates the work.</p>
</div>
<div class="docs-section" id="tasks">