-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlanparty
More file actions
executable file
·1267 lines (1101 loc) · 43.3 KB
/
lanparty
File metadata and controls
executable file
·1267 lines (1101 loc) · 43.3 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
#! /bin/bash
set -euo pipefail
# ========================================================================================
# Implementation functions
bold() {
# Like `echo` but use ANSI terminal codes to make the text bold -- unless the output is not
# a terminal.
if [ -t 1 ]; then
echo -ne '\033[1m'
echo -n "$@"
echo -e '\033[0m'
else
echo "$@"
fi
}
usage() {
bold 'Usage:'
echo " $SCRIPT_NAME [-c CONFIG] [-n] COMMAND"
echo
echo 'If -n is specified, no actions will be taken; the script will only print out'
echo 'the commands it would normally execute. If -c is given, it specifies the'
echo "location of the config file. The default is: /etc/$SCRIPT_NAME.conf"
echo
echo 'COMMAND may be:'
bold ' init [HOSTS...]'
echo ' Initialize the given hosts (default: all hosts), splitting all unallocated'
echo ' disk space in the volume group evenly among their copy-on-write overlays.'
echo ' If any specified hosts are already initialized, they are destroyed first.'
echo
bold ' destroy [HOSTS...]'
echo ' Wipe all machines and discard any unmerged updates. If HOSTS is specified,'
echo ' only destroy those specific hosts.'
echo
bold ' boot [HOSTS...]'
echo ' Sends ethernet wake-on-LAN magic packet to the given hosts (default: all'
echo ' hosts), hopefully causing them to power up.'
echo
bold ' shutdown [HOSTS...]'
echo ' Shuts down the given hosts (default: all running hosts) by connecting to'
echo ' each via SSH and issuing the configured shutdown command.'
echo
bold ' start-updates HOST'
echo ' Initialize the given host for installing updates, using all unallocated'
echo ' disk space in the volume group for a copy-on-write overlay. Once the'
echo ' machine is updated and powered down, use "merge" to merge changes back'
echo ' into the master image, or "destroy" to discard all changes.'
echo
bold ' merge'
echo ' Merges updates (started with start-updates) into the master image.'
echo
bold ' authorize-addr HOST ADDR'
echo ' Authorize the IP address ADDR to attach to the iSCSI volume for HOST.'
echo " Normally, only the host's own address is allowed. This command exists"
echo ' for troubleshooting purposes and should rarely be needed. This change'
echo ' will be reverted the next time you run "destry" or "merge".'
echo
bold ' status'
echo ' Shows the current state of all machines'
echo
bold ' configure [dhcp|dns|dns-zone|dns-reverse|ipxe]'
echo ' Generates configuration snippets, written to standard output. The argument'
echo ' specifies what to configure:'
bold ' (no argument)'
echo ' Generates a template config file for this script itself. This should'
echo " typically be saved to /etc/$SCRIPT_NAME.conf and then edited to enter"
echo ' your specific configuration.'
bold ' dhcp'
echo ' Generates configuration for ISC DHCP server, derived from'
echo " $SCRIPT_NAME.conf. Typically you would place this in:"
echo ' /etc/dhcp/dhcp.conf'
bold ' dns'
echo ' Generates configuration for BIND 9 DNS server, derived from'
echo " $SCRIPT_NAME.conf. Append this to (on Debian-derived systems):"
echo ' /etc/bind/named.conf.local'
bold ' dns-zone'
echo ' Generates zone definition for BIND 9 DNS server, derived from'
echo " $SCRIPT_NAME.conf. Place this at:"
echo ' /etc/bind/zones/lanparty.db'
bold ' dns-reverse'
echo ' Generates reverse lookup for BIND 9 DNS server, derived from'
echo " $SCRIPT_NAME.conf. Place this at:"
echo ' /etc/bind/zones/lanparty-reverse.db'
bold ' ipxe'
echo ' Generates a useful iPXE script that makes it easy to netboot an'
echo ' ISO image. Place this in:'
echo ' /var/lib/tftpboot/boot.ipxe'
}
yesno() {
# Echo the arguments and prompt for a yes/no answer.
echo -n "$@ (y/n) " >&2
while read ANSWER; do
case $ANSWER in
y | Y | yes | Yes | YES )
return 0
;;
n | N | no | No | NO )
return 1
;;
* )
# try again
echo -n "$@ (y/n) " >&2
;;
esac
done
# EOF. Not a terminal? Fail out.
# TODO: Maybe we should assume "yes" when run non-interactively?
echo "ERROR: Can't continue without user input."
exit 1
}
is-updating() {
# Check if we're currently in update mode.
test -e "/dev/$VGROUP/updates"
}
is-local-mount-point-configured() {
# Check if we're configured to mount the master image read-only at a local mount point.
test "${LOCAL_MOUNT_POINT:-}" != ""
}
is-master-mounted-locally() {
# Check if the master image is currently mounted locally at the configured local mount point.
findmnt "$LOCAL_MOUNT_POINT" > /dev/null
}
is-caching-enabled() {
# Check if the page caching layer is currently configured.
losetup "$CACHE_LOOP_DEVICE" > /dev/null 2>&1
}
validate-hostnames() {
# Validate that each argument is a valid hostname defined in our config file.
for MACHINE in "$@"; do
if [ "${HOST_TO_NUMBER[$MACHINE]:-none}" == "none" ]; then
echo "ERROR: No such host configured: $MACHINE" >&2
exit 1
fi
if [ "$MACHINE" == "updates" ]; then
echo "ERROR: You cannot name a machine 'updates'." >&2
exit 1
fi
done
}
compute-extents() {
# Compute how many extents to assign to each machine's overlay, if there are $1 machines.
if [ "${OVERLAY_DEVICE:-}" != "" ]; then
# The admin has configured a specific overlay device, so query free space on that device.
FREE_EXTENTS=$(pvdisplay $OVERLAY_DEVICE -c | cut -d: -f10)
else
# The admin has not configured a specific overlay device, so query all free space on the
# volume group.
FREE_EXTENTS=$(vgdisplay $VGROUP -c | cut -d: -f16)
fi
MACHINE_COUNT=${1:-1}
echo "$(( FREE_EXTENTS / MACHINE_COUNT ))"
}
# Regex detecting any character that Bash treats specially.
NEEDS_QUOTING_PATTERN='[][$&|"\#!<>;()*?~`'"'"']'
echo-command() {
# Echo a shell command to standard output, making sure to quote it appropriately so that it can
# be copied and pasted.
# We'd like to echo something that could actually be copy/pasted into a shell and would work
# correctly. To that end, we should make sure arguments are properly quoted.
local -a EXPANSION
local -a OUTPUT
for ARG in "$@"; do
EXPANSION=( $ARG )
if [ "${EXPANSION[0]}" != "$ARG" ] ||
[[ "$ARG" =~ $NEEDS_QUOTING_PATTERN ]]; then
# This argument contains characters that need quoting or escaping.
# Hack: Don't escape trailing &, we probably intended to print it like that.
if [ "$ARG" != "&" ]; then
# OK, wrap the whole argument in single quotes. If it contains any single quotes, replace
# that with '"'"'... ugh.
ARG="'$(sed -e "s/'/'\"'\"'/g" <<< "$ARG")'"
fi
fi
OUTPUT+=( "$ARG" )
done
# If we're echoing to a terminal, and we're not in dry-run mode, make the command bold. That way,
# echoed commands are easy to distinguish from the output of the commands, if they have any.
if [ $DRY_RUN == no -a -t 1 ]; then
# Make it bold.
echo -ne '\033[1m'
echo -n "${OUTPUT[*]}"
echo -e '\033[0m'
else
echo "${OUTPUT[*]}"
fi
}
doit() {
# Echo and then execute the given command -- unless we're in dry-run mode, in which case we only
# echo.
echo-command "$@"
if [ $DRY_RUN == no ]; then
"$@"
fi
}
unmount-master-locally() {
# If the master image is mounted locally, unmount it. This is invoked before making any change
# that can't be done while the local mount is up, such as merging updates or switching between
# cached and uncached modes.
if is-local-mount-point-configured; then
if is-master-mounted-locally; then
doit umount "$LOCAL_MOUNT_POINT"
fi
fi
}
mount-master-locally() {
# If we're configured to mount the master image locally (read-only), mount it.
if is-local-mount-point-configured; then
if [ ! -e "$LOCAL_MOUNT_POINT" ]; then
doit mkdir -p "$LOCAL_MOUNT_POINT"
fi
if is-caching-enabled; then
# Mount from the loop device, because apparently we can't directly mount the base image
# when a loop device is using it (and anyway sharing the cache is good).
doit mount -o "ro,$LOCAL_MOUNT_OPTIONS" "$CACHE_LOOP_DEVICE" "$LOCAL_MOUNT_POINT"
else
doit mount -o "ro,$LOCAL_MOUNT_OPTIONS" /dev/$VGROUP/$BASE_IMAGE "$LOCAL_MOUNT_POINT"
fi
fi
}
stop-iscsi() {
# Instruct the iSCSI daemon to delist the given hostnames' volumes. Only hosts passed as
# parameters to this function are affected; the rest are left alone.
#
# This is idempotent -- it's not an error if the targets are already unpublished.
bold "================ stop iscsi ================"
# If tgtd is running, remove the specific targets. (Since we configure tgtd dynamically, we can
# assume if it's not running, then any state related to our hosts is already lost.)
if pidof tgtd > /dev/null; then
NEED_SLEEP=no
for MACHINE in "$@"; do
TID=${HOST_TO_TID[$MACHINE]}
# Check if the target is currently online.
if tgtadm -C 0 --lld iscsi --op show --mode target --tid $TID > /dev/null 2>&1; then
# It is, so disable it.
doit tgtadm -C 0 --lld iscsi --op delete --force --mode target --tid $TID
NEED_SLEEP=yes
fi
done
if [ $NEED_SLEEP == yes ]; then
# Give tgtd time to close resources. Otherwise, if we try to delete the underlying volumes
# too soon, we may get "device or resource busy" errors. It's too bad tgtadm itself doesn't
# wait until the resources are fully closed...
doit sleep 1
fi
fi
}
delete-overlays() {
# Delete the overlay volumes for the given hostnames, and the updates overlay if present.
#
# This is idempotent -- it's not an error if the volumes don't exist.
bold "================ delete overlays ================"
for MACHINE in "$@"; do
if [ -e /dev/mapper/cached-$MACHINE ]; then
doit dmsetup remove /dev/mapper/cached-$MACHINE
fi
if [ -e /dev/$VGROUP/$MACHINE-cow ]; then
doit lvremove -f /dev/$VGROUP/$MACHINE-cow
fi
done
# Also delete the updates image, if present.
if [ -e /dev/$VGROUP/updates ]; then
doit lvremove -f /dev/$VGROUP/updates
fi
}
create-overlays() {
# Set up disposable (non-updates) copy-on-write overlays for the given hostnames.
bold "================ create overlays ================"
MASTER_SIZE=$(blockdev --getsz /dev/$VGROUP/$BASE_IMAGE)
EXTENTS=$(compute-extents $#)
# Create the loopback layer -- for page caching -- over the master image, if it doesn't exist
# already.
if ! is-caching-enabled; then
unmount-master-locally
doit losetup --direct-io=off --read-only $CACHE_LOOP_DEVICE /dev/$VGROUP/$BASE_IMAGE
mount-master-locally
elif is-local-mount-point-configured && ! is-master-mounted-locally; then
mount-master-locally
fi
for MACHINE in "$@"; do
# Create a regular volume with LVM.
doit lvcreate -n "$MACHINE-cow" -l $EXTENTS $VGROUP ${OVERLAY_DEVICE:-}
# Use it as a raw devicemapper COW device.
doit dmsetup create cached-$MACHINE --table "0 $MASTER_SIZE snapshot $CACHE_LOOP_DEVICE /dev/$VGROUP/$MACHINE-cow N 128"
done
}
merge-updates() {
# Merge updates back into the master image.
bold "================ merge overlay ================"
unmount-master-locally
doit lvconvert --merge /dev/$VGROUP/updates
mount-master-locally
}
start-updates() {
# Set up a copy-on-write overlay for installing updates.
bold "================ create overlay ================"
EXTENTS=$(compute-extents 1)
# Remove the loopback layer, if it currently exists.
if is-caching-enabled; then
unmount-master-locally
doit losetup -d $CACHE_LOOP_DEVICE
mount-master-locally
fi
# Creating the updates machine. Use a regular LVM snapshot so that we can easily merge it back
# later.
doit lvcreate -c 64k -n updates -l $EXTENTS -s /dev/$VGROUP/$BASE_IMAGE ${OVERLAY_DEVICE:-}
}
start-iscsi() {
# Instruct the iSCSI server (tgtd) to publish the volumes for the given hostnames. If tgtd isn't
# already running, starts it. If it is running, already-published volumes are not affected.
#
# The first argument to start-iscsi must be either "updates" if we're publishing the update
# volume, or "cached" if we're publishing disposable volumes. The remaining arguments are the
# hostnames. (There should normally be only one hostname when publishing updates.)
bold "================ start iscsi ================"
local SOURCE=$1
shift
# Start tgtd if not running.
if ! pidof tgtd > /dev/null; then
doit tgtd
# By default tgtd will bind to all addresses. We don't want that, especially if this server
# is also being used as a router and therefore has both public and private interfaces. We don't
# have any way to authenticate machines, so we need to rely on the network being private.
# Tell tgtd to unbind from the wildcards (IPv4 and IPv6) and instead bind only to the
# configured server address.
doit tgtadm --op update --mode sys --name State -v offline
doit tgtadm --lld iscsi --op delete --mode portal --param portal=0.0.0.0:3260
doit tgtadm --lld iscsi --op delete --mode portal --param portal=[::]:3260
doit tgtadm --lld iscsi --op new --mode portal --param portal=$SERVER_ADDR:3260
doit tgtadm --op update --mode sys --name State -v ready
fi
for MACHINE in "$@"; do
# Apparently we must assign a "target ID" number to each machine even though it also has a
# name. Why? Probably because the names are IQNs which are so unweildy that no one wants to
# use them. Fine, whatever, we have ID numbers for each host, we'll use those.
TID=${HOST_TO_TID[$MACHINE]}
# Create the target.
doit tgtadm -C 0 --lld iscsi --op new --mode target --tid $TID -T $ISCSI_TARGET_PREFIX:$MACHINE
# Decide which device we're publishing.
case $SOURCE in
updates )
DEVICE=/dev/$VGROUP/updates
;;
cached )
DEVICE=/dev/mapper/cached-$MACHINE
;;
* )
echo "internal error: start-iscsi bad parameters" >&2
exit 1;
esac
# Add the "logical unit" to the target. Why can a target have multiple LUNs? Why not instead
# tell people to have multiple targets? Because iSCSI is over-engineered, that's why. Have you
# ever looked at nbd? THAT is how a block device protocol should be. Oh well.
doit tgtadm -C 0 --lld iscsi --op new --mode logicalunit --tid $TID --lun 1 -b "$DEVICE"
# Permit the designated machine's IP (and no others) to access this target. Note that this in
# itself doesn't provide any security since typically anyone on the internal network can claim
# to be this IP address. It does, however, prevent mistakes.
doit tgtadm -C 0 --lld iscsi --op bind --mode target --tid $TID -I $IP_PREFIX.${HOST_TO_NUMBER[$MACHINE]}
done
}
authorize-addr() {
doit tgtadm -C 0 --lld iscsi --op bind --mode target --tid ${HOST_TO_TID[$1]} -I $2
}
boot-hosts() {
# Send ethernet wake-on-LAN packet to the given hostnames.
bold "================ boot hosts ================"
for MACHINE in "$@"; do
doit etherwake -i $SERVER_NETWORK_INTERFACE ${HOST_TO_MACADDR[$MACHINE]}
done
}
shutdown-hosts() {
# Send SSH shutdown command to the given hostnames.
bold "================ shutdown hosts ================"
for MACHINE in "$@"; do
# We inline doit() here so that we can let the SSH commands run in the background with &.
# This is important because if a machine is already shut down, `ssh` will hang for a while
# before failing. We disable "StrictHostKeyChecking" because the prompt won't work when ssh is
# running in the background. Since we're relying on public-key authentication and will only
# be executing one command that contains no secrets, there's no security risk from MITM. Also,
# this should be on a private network anyway.
#
# While we're at it, we take the opportunity to properly quote the arguments in the console
# output.
COMMAND=( ssh -o "StrictHostKeyChecking no" "$SHUTDOWN_USERNAME@$IP_PREFIX.${HOST_TO_NUMBER[$MACHINE]}" -- 'shutdown /p /f' )
echo-command "${COMMAND[@]}" "&"
if [ $DRY_RUN == no ]; then
"${COMMAND[@]}" &
fi
done
# Wait for all ssh commands to complete.
if [ $DRY_RUN == no ]; then
bold wait
wait
else
echo wait
fi
}
ratio-to-percent() {
# $1 should be a ratio, like "123/456". We'll return a percentage to 2 decimal places.
# Bash doesn't have floating-point math but we can do it all with integers.
# Calculate ten-thousanths as an integer.
PER10K=$(( 10000 * $1 ))
# Zero-pad up to at least three digits.
PER10K=$(printf "%03d" $PER10K)
# Separate digits after decimal.
[[ "$PER10K" =~ ^(.*)(..)$ ]]
# Output!
echo ${BASH_REMATCH[1]}.${BASH_REMATCH[2]}
}
show-status() {
# Show the status of all overlays, in particular what percentage of the overlay space has been
# consumed.
if is-updating; then
# In updates mode, the overlay is an LVM snapshot. Query using lvdisplay.
PERCENT=$(lvdisplay "/dev/$VGROUP/updates" |
grep "Allocated to snapshot" |
sed -e 's/ *Allocated to snapshot *//')
echo "updates: ${PERCENT}"
else
# In regular mode, the overlays are raw device-mapper devices. Query using dmsetup.
for MACHINE in "$@"; do
if [ -e "/dev/mapper/cached-$MACHINE" ]; then
STATUS=$(dmsetup status "cached-$MACHINE" | cut -d' ' -f 4)
if [ "$STATUS" = Invalid ]; then
# This machine's overlay was filled up. The volume no longer works.
echo "$MACHINE: exhausted"
else
# dmsetup gives us a ratio of extents used vs. total, like "1234/4321". We want a
# percentage.
PERCENT=$(ratio-to-percent "$STATUS")
echo "$MACHINE: $PERCENT%"
fi
else
# This overlay is not currently configured.
echo "$MACHINE: offline"
fi
done
fi
}
# ========================================================================================
generate-config() {
cat << __EOF__
#! /bin/bash
# Configuration file for '$SCRIPT_NAME' tool.
# Place this at: /etc/$SCRIPT_NAME.conf
#
# This file is intended to be sourced by a bash script.
MACHINE_TABLE="
cutman 3 00:00:00:00:00:00
gutsman 4 00:00:00:00:00:00
iceman 5 00:00:00:00:00:00
bombman 6 00:00:00:00:00:00
fireman 7 00:00:00:00:00:00
elecman 8 00:00:00:00:00:00
metalman 9 00:00:00:00:00:00
airman 10 00:00:00:00:00:00
bubbleman 11 00:00:00:00:00:00
quickman 12 00:00:00:00:00:00
crashman 13 00:00:00:00:00:00
flashman 14 00:00:00:00:00:00
my-phone 51 00:00:00:00:00:00
my-laptop 52 00:00:00:00:00:00"
# Table of machines and devices on your network, especially those managed
# by this script. Each line contains three columns:
# - The machine's hostname. The script will help you set up DNS for these!
# - The machine's ID number. This will be used as the last octet of the
# machine's IP address, as well as other places where a numeric identifier
# is needed. These do not need to be sequential, but each machine must have
# a unique number in the range 1 to 99. (Larger numbers are possible but
# require manual changes to the generated DHCP configuration.)
# - The machine's MAC address, needed for DHCP configuration and wake-on-LAN.
# Usually you can find this written somewhere in or on the device's
# packaging. If not, you can probably find it by booting the machine and
# looking through the BIOS. Or, you might try configuring the BIOS to boot
# from the network (PXE-boot), and then see what text is displayed when it
# tries; often, the MAC address will be displayed here.
#
# The example shows a 12-machine setup named after bosses from Mega Man. The
# ID numbers are chosen to match the "serial numbers" assigned to each boss
# in the game itself, allowing you to use megaman.fandom.com as a makeshift
# DNS service...
#
# We've also defined "my-phone" and "my-laptop", which are some other
# machines that commonly connect to the network. These are optional, but if
# you list them, the "$SCRIPT_NAME configure" command will include these
# when generating DHCP and DNS configuration, which is convenient.
#
# The server machine should NOT be listed on this table.
NETBOOT_MACHINES="cutman gutsman iceman bombman fireman elecman metalman airman bubbleman quickman crashman flashman"
# The set of machines from the above table which will netboot over iSCSI.
# Any machines not listed here are assumed to be other devices on your
# network. DHCP and DNS config will be generated for them, but no disk
# images will be allocated. In this example, "my-phone" and "my-laptop"
# are not netboot machines, so we don't list them here.
IP_PREFIX=10.0.0
# The first three octets of each machine's IP address.
#
# The last octet is the machine's ID number from MACHINE_TABLE.
DOMAIN=example.com
# The domain name under which the above hostnames are to be found.
#
# Note that this doesn't have to be a real domain! You can invent your own
# TLD if you want, like "lanparty". Then your machines would have names like
# "cutman.lanparty", etc. If you set up a DNS server, you can make these
# names actually resolvable (within your private network, at least). If you
# aren't setting up a DNS server, then this domain is not really used for
# anything at all, so then it matters even less if it's real.
SERVER_ADDR=10.0.0.1
# The internal-network address of the server. Clients will be configured to
# connect to this address, and server daemons will be configured to bind to
# this address (and only this address).
SERVER_HOSTNAME=server
# The hostname of the server machine (within the domain specified above).
ISCSI_TARGET_PREFIX=iqn.2019-12.com.example.server
# This probably doesn't matter.
#
# Prefix for iSCSI volume names. Unfortuantely, the designers of iSCSI were
# afflicted with global namespace envy. Systems architects with this condition
# believe that the resources in their system really must have globally-unique
# names, and decide to invent a new namespace to that end. Of course, there is
# in fact absolutely nothing stopping two iSCSI servers from exporting volumes
# with the same names, and no problem would actually be caused by doing so.
# Clients do not magically look up these global names in some global registry;
# they connect to a specific server by configured IP address and then ask
# that server for the volume they want. So in fact the names are server-local,
# but iSCSI demands that you add a bunch of noise to the name to make it
# appear globally unique for no good reason.
#
# Anyway, this is the format they want. It starts with "iqn." to indicate that
# this is an iSCSI Qualified Name. Since every single node name in the world
# starts with "iqn.", this prefix is 100% redundant, but there you go. The next
# part is the date on which you registered your domain name, followed by the
# fully-qualified DNS hostname of the server in reverse order.
#
# The purpose of all this is just to make sure your name will never be the
# same as anyone else's -- for no practical reason. Neither clients nor
# servers will actually parse the contents of this string. It's just a
# string. You can probably put whatever you want in it and it'll be fine.
SERVER_NETWORK_INTERFACE=eth0
# Network interface corresponding to the above server address.
#
# More specifically, this is the interface on which Wake-on-LAN packets
# should be sent in order to boot the client machines.
VGROUP=lanparty-disks
# LVM volume group name which will contain both the base disk image and all
# overlays.
#
# It is up to you to configure LVM. You must create this volume group
# yourself, and add some physical disks to it. I recommend having at least
# two disks in the volume: one to host the base image itself, and a separate
# one to host the overlays. The overlay disk should be optimized for speed
# while the base image disk is optimized for size. For example, you might use
# a 4TB spinning rust disk for the base image and then a 512MB SSD for the
# overlays. Note, though, that if the overlay disk is too small, you may run
# into problems where a machine becomes inoperable when someone makes enough
# local changes to the machine that it runs out of overlay space; in this
# case you will have to shut down, detroy, and re-init the machine in
# question, thus wiping any local changes that had been made.
BASE_IMAGE=master
# LVM volume name for the base image.
#
# It is up to you to create this volume using LVM. It must be created within
# the volume group specified above. You will want to be careful to instruct
# LVM to place the base image on the appropriate physical disk within the
# volume group, otherwise it might decide to use the disk you had intended
# for overlay space.
#
# You do not need to format this volume. We will initially export it as an
# unformated volume, and you can then run the Windows 10 (or other OS)
# installer directly on the client machine.
OVERLAY_DEVICE=/dev/sdb
# Physical disk on which per-machine copy-on-write overlays should be
# allocated.
#
# This disk must be a member of the LVM volume group specified above. You can
# also leave this setting blank if you'd rather let LVM choose a physical
# volume from the group automatically; this might be useful if you have
# multiple disks in the volume group that you want to use for overlays, but
# in this case you should make sure that the base image's disk is completely
# full so that it doesn't get used. (Placing an overlay on the same disk as
# the base image could hurt performance.)
CACHE_LOOP_DEVICE=/dev/loop7
# Loopback device that can be used to create a page-caching layer on top of
# the base image.
#
# This just has to reference a loop device that isn't being used for anything
# else on the system. /dev/loop7 worked well for me.
#
# Normally a loop device is used to make a file on disk appear to be a disk
# drive itself, which can then be mounted. But, the loop device we create
# will actually point to the base image device itself as the "file". The
# effect of this is to cause all block operations to be converted to
# filesystem read operations, which are then applied to the block device
# itself, and so are converted back into block operations. Why do this?
# Because the filesystem layer adds caching. Without a loopback layer, the
# base image won't be cached at all in the server's memory. In normal iSCSI
# scenarios, that is fine, because the client will do its own caching. But,
# in our scenario, there are potentially many clients reading the exact same
# data. For example, at a LAN party, everyone might open the same game at
# roughly the same time. We don't want to have to read the game all the way
# from disk for every client; we want all clients after the first to get the
# cached copy. Using a loopback device stacked on top of a block device node
# accomplishes this.
#LOCAL_MOUNT_POINT=/mnt/master
# OPTIONAL: Mount the base image, read-only, to this directory on the server.
# Leave commented out to skip mounting locally.
#
# This is useful, for example, to allow you to share the base image on the
# network using Samba. If a guest brings their own laptop to the LAN party, and
# they don't already have the necessary games installed, they will be able to
# copy the game data from the Steam app cache via this network share, avoiding
# a lengthy download. (They will still need to purchase the game on Steam in
# order to play it, of course.)
#
# NOTE: Mounting will, of course, fail if the disk is not yet formatted. You
# may therefore want to leave this unconfigured until after you've installed
# your operating system.
#
# NOTE: The device will need to be unmounted briefly when switching between
# update mode and party mode, as well as when merging updates. This unmount
# will fail if the device is in use, which will prevent the script from
# proceeding.
LOCAL_MOUNT_OPTIONS=offset=1048576
# Extra mount options to use when mounting the base image.
#
# Typically, the base image includes a partition table, and it's actually the
# first partition that you want to mount. Since modern partitioning tools like
# to align partitions to megabyte boundaries, the first partition is almost
# always offset 1 MiB from the start of the disk, hence the default offset
# shown above. However, if this is wrong, you can determine the correct value
# using fdisk. For example, if you do:
#
# fdisk -l /dev/lanparty-disks/master
#
# You should see a table like:
#
# Device Boot Start End Sectors Size Id Type
# /dev/example * 2048 2582638591 2582636544 1.2T 7 HPFS/NTFS/exFAT
#
# Notice that "Start" shows sector 2048. Normally, each sector is 512 bytes
# (though fdisk will also list the exact sector size earlier), so 2048 * 512
# equals 1048576 (1 MiB).
SHUTDOWN_COMMAND="shutdown /p /f"
# SSH command to send to a client machine to shut it down.
#
# The default is appropriate for Windows. Yes, Windows has SSH now. Note that
# you'll need to enable it in the control panel, and you'll need to configure
# it with an appropriate public key so that your server can connect to it
# without a password.
SHUTDOWN_USERNAME="LAN Party Guest"
# User name to use when issuing the shutdown command over SSH.
#
# Windows user names often contain spaces. That's fine.
__EOF__
}
configure-dhcp() {
cat << __EOF__
# Configuration auto-generated by '$SCRIPT_NAME'.
# This text should be placed in: /etc/dhcp/dhcpd.conf
option domain-name "$DOMAIN";
option domain-name-servers $SERVER_ADDR;
# Tell all clients to use this server as the DNS server, and set the search
# domain. This allows computers in the network to reference each other by name.
#
# You will need to run a DNS server -- probably BIND 9 -- on this machine.
# You can use '$SCRIPT_NAME configure dns' to help you generate config for it.
#option domain-name-servers 1.1.1.1, 1.0.0.1; # use Cloudflare DNS
#option domain-name-servers 8.8.8.8, 8.8.4.4; # use Google DNS
# You don't strictly have to run a DNS resolver. Un-comment one of these to use
# it instead, or add your own. However, your machines won't be able to refer to
# each other by name, unless you manually configure your DNS in The Cloud,
# which would be weird since you'd be referencing private-network IP
# addresses...
next-server $SERVER_ADDR;
# Set IP address for TFTP downloads.
default-lease-time 86400;
max-lease-time 604800;
authoritative;
# Boilerplate.
option space ipxe;
option ipxe.keep-san code 8 = unsigned integer 8;
# Teach dhcpd.conf about the ipxe.keep-san option.
subnet $IP_PREFIX.0 netmask 255.255.255.0 {
# DHCP requests received on the network interface bound to this subnet
# will be handled according to this block.
range $IP_PREFIX.100 $IP_PREFIX.254;
# For unrecognized devices, give them an IP in the range .100 to .254.
option subnet-mask 255.255.255.0;
option broadcast-address $IP_PREFIX.255;
# Basic network settings.
option routers $SERVER_ADDR;
# Set ourselves as the router. This assumes that the server has been
# configured as a router, which in turn requires that it have two network
# interfaces (one internal, one external). I highly recommend using Linux as
# a router and setting whatever terrible modem your ISP gave you to "bridge"
# mode; your network will be more reliable! However, if you'd rather not set
# that up, you can change this line to reference your modem's IP address
# instead, and have your modem sitting on the internal network. Note that you
# will have to turn off your modem's internal DHCP server, otherwise it will
# fight with this server.
}
group {
# Declare parameters specific to our netboot machines.
if exists user-class and option user-class = "iPXE" {
# The client is PXE-booting using iPXE, which they probably downloaded in
# a previous stage using their regular PXE boot ROM (see below). We can now
# have them boot via iSCSI, which iPXE supports.
filename "";
# Do not download an executable, since we'll be using iSCSI.
option ipxe.keep-san 1;
# Tell iPXE to hand off the iSCSI connection to the operating system
# kernel. Yes that's right, just pass off a live TCP connection and Layer 7
# protocol session to a whole differen kernel. How does that work? Magic.
# Just go with it.
# We will configure the option "root-path" on a per-machine basis below to
# specify what iSCSI volume to boot from.
} else {
# The client is NOT iPXE. Most likely, it is trying to boot using its
# built-in PXE boot ROM that came with the network adapter firmware.
# Standard PXE boot firmware usually cannot handle iSCSI boot. Typically
# all it knows how to do is download a single file via tftp (trivial file
# transfer protocol, a UDP-based abomination) and execute it. So we tell
# them to download iPXE, a more advanced, open source PXE bootloader, and
# run that.
#
# Note that, in theory, you can avoid this step entirely by flashing iPXE
# directly into your network cards' ROM, which is... terrifying, to be
# honest.
filename "ipxe";
}
# Declare each known host and assign it a fixed address.
__EOF__
for HOSTNAME in "${HOSTNAMES[@]}"; do
echo " host $HOSTNAME {"
echo " hardware ethernet ${HOST_TO_MACADDR[$HOSTNAME]};"
echo " fixed-address $IP_PREFIX.${HOST_TO_NUMBER[$HOSTNAME]};"
echo " option root-path \"iscsi:$SERVER_ADDR:::1:$ISCSI_TARGET_PREFIX:$HOSTNAME\";"
echo " }"
done
cat << __EOF__
}
group {
# Declare parameters for non-netboot machines.
#
# Normally, we expect such machines will not attempt to PXE-boot. However,
# if they do, let's give them something useful. Specifically, we'll give them
# the ability to boot any ISO image by entering the URL. This is super-useful
# for installing operating systems -- no need to use a separate machine to
# download the ISO nor create a USB stick.
if exists user-class and option user-class = "iPXE" {
filename "boot.ipxe";
# Run the iPXE script in /var/lib/tftpboot/boot.ipxe.
#
# '$SCRIPT_NAME configure ipxe' will generate a useful script for you.
} else {
filename "ipxe";
}
# Declare each known host and assign it a fixed address.
__EOF__
for HOSTNAME in "${UNMANAGED_HOSTNAMES[@]}"; do
echo " host $HOSTNAME {"
echo " hardware ethernet ${HOST_TO_MACADDR[$HOSTNAME]};"
echo " fixed-address $IP_PREFIX.${HOST_TO_NUMBER[$HOSTNAME]};"
echo " }"
done
echo "}"
}
configure-ipxe() {
cat << '__EOF__'
#!ipxe
# This is an iPXE script which prompts the user to enter a name of any ISO
# image found in the server's /var/lib/tftpboot/, then boots that image.
# This is super-useful for installing operating systems.
# Put this in: /var/lib/tftpboot/boot.ipxe
echo -n Enter ISO to boot:
read image
initrd ${image}
kernel memdisk iso raw
__EOF__
}
reverse-ip() {
IFS=. read -a OCTETS <<< "$1"
declare -a REVERSE_OCTETS
for OCTET in "${OCTETS[@]}"; do
REVERSE_OCTETS=("$OCTET" "${REVERSE_OCTETS[@]}")
done
echo "${REVERSE_OCTETS[*]}" | tr ' ' '.'
}
configure-dns() {
REVERSE_IP_PREFIX=$(reverse-ip "$IP_PREFIX")
cat << __EOF__
// Configuration auto-generated by '$SCRIPT_NAME'.
// This should be added to named.conf. On Debian-derived systems, append to:
// /etc/bind/named.conf.local
zone "$DOMAIN" {
type master;
file "/etc/bind/zones/lanparty.db";
// Generate this file using: $SCRIPT_NAME configure dns-zone
};
zone "$REVERSE_IP_PREFIX.in-addr.arpa" {
type master;
file "/etc/bind/zones/lanparty-reverse.db";
// Generate this file using: $SCRIPT_NAME configure dns-reverse
};
// End configuration auto-generated by '$SCRIPT_NAME'.
__EOF__
}
configure-dns-zone() {
cat << __EOF__
; Configuration auto-generated by '$SCRIPT_NAME'.
; On Debian-derived systems, we suggest placing this in:
; /etc/bind/zones/lanparty.db
;
; You must then add the following to /etc/bind/named.conf.local
; ("lanparty configure dns" will generate this for you):
; zone "$DOMAIN" {
; type master;
; file "/etc/bind/zones/lanparty.db";
; };
\$ORIGIN $DOMAIN.
\$TTL 3600
@ IN SOA $SERVER_HOSTNAME.$DOMAIN. root.$DOMAIN. (
2020010101 ; Irrelevant for non-replicated servers.
86400 ; Irrelevant for non-replicated servers.
3600 ; Irrelevant for non-replicated servers.
86400 ; Irrelevant for non-replicated servers.
3600 ; TTL for NXDOMAIN responses ("not found").
)
@ IN NS $SERVER_HOSTNAME.$DOMAIN.
$SERVER_HOSTNAME IN A $SERVER_ADDR
__EOF__
for HOSTNAME in "${ALL_HOSTNAMES[@]}"; do
echo "$HOSTNAME IN A $IP_PREFIX.${HOST_TO_NUMBER[$HOSTNAME]}"
done
}
configure-dns-reverse() {
REVERSE_IP_PREFIX=$(reverse-ip "$IP_PREFIX")
cat << __EOF__
; Configuration auto-generated by '$SCRIPT_NAME'.
; On Debian-derived systems, this text should be placed in:
; /etc/bind/zones/lanparty-reverse.db
;
; You must then add the following to /etc/bind/named.conf.local
; ("lanparty configure dns" will generate this for you):
; zone "$REVERSE_IP_PREFIX.in-addr.arpa" {
; type master;
; file "/etc/bind/zones/lanparty-reverse.db";
; };
\$ORIGIN $REVERSE_IP_PREFIX.in-addr.arpa.
\$TTL 3600
@ IN SOA $SERVER_HOSTNAME.$DOMAIN. root.$DOMAIN. (
2020010101 ; Irrelevant for non-replicated servers.
86400 ; Irrelevant for non-replicated servers.
3600 ; Irrelevant for non-replicated servers.
86400 ; Irrelevant for non-replicated servers.
3600 ; Timeout for NXDOMAIN responses ("not found").
)
@ IN NS $SERVER_HOSTNAME.$DOMAIN.
__EOF__
SERVER_ID="${SERVER_ADDR#$IP_PREFIX.}"
if [ "$SERVER_ID" != "$SERVER_ADDR" ]; then
echo "$SERVER_ID IN PTR $SERVER_HOSTNAME.$DOMAIN."
echo
fi
for HOSTNAME in "${ALL_HOSTNAMES[@]}"; do
echo "${HOST_TO_NUMBER[$HOSTNAME]} IN PTR $HOSTNAME.$DOMAIN."
done
}