-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvirt-compose.sh
executable file
·1154 lines (979 loc) · 37.2 KB
/
virt-compose.sh
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
#!/usr/bin/env bash
# Copyright 2024 Michael Stovenour
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Out of box user experience
# • install dependent packages (virsh, virt-install, etc.)
# • download a qcow2 vm image and create cloud-init user/meta-data files
# • clone this repository
# • run sudo install.sh
# • run nets builder for example_net
# • create vm definition folder and yaml file (copy example_vm1)
# • run virt-compose install example_vm1
# • run virt-compose start example_vm1
# • run virt-compose shutdown example_vm1
# • run virt-compose undefine example_vm1
# To Do
# [X] Update README.md with dependency information
# [X] Update README.md with getting started steps
# [ ] Update README.md with definition of the vm yaml file
# [ ] Update README.md with libvirt auto-start/stop interactions
# [ ] Script to install kvm packages (dependencies of installer?)
# [ ] Script to build out networks from nets/* or just use virsh?
# [X] Rename virt-compose.sh to just virt-compose
# [X] Create installer
# • Create /etc/virt-compose
# • Install ./virt-compose.yaml /etc/virt-compose/virt-compose.yaml
# • Install ./vm/* /etc/virt-compose/vm
# • Install ./virt-compose /usr/bin
#
# [X] Create Repository
# [X] Code install
# [X] ** Update install to create systemd service
# [X] Code undefine
# [X] ** Update to remove systemd service
# [X] Code attach-device
# [X] Code detach-device
# [x] Code start
# [X] ** Update to create udev links
# [X] Code start-all
# [x] Code shutdown
# [X] ** Update to remove udev links
# [X] Code shutdown-all
# Config bootstrap resolution
# Config file full path; defaults to hardcoded value
# Config file full path; can be overiden on comand line
# All other config defaults to hardcoded values
# All other config can be overidden in the config file GLOBAL map
# Global Variables
BIN_FOLDER="/usr/bin"
SYSTEMD_FOLDER="/etc/systemd/system"
UDEV_RULES_FOLDER="/etc/udev/rules.d"
QEMU_URI="qemu:///system"
BASE_FOLDER="/etc/virt-compose"
CONFIG="virt-compose.yaml"
DOMAIN_METADATA_URI="http://stovenour.net/libvirt"
DOMAIN_METADATA_NS="stovenour"
VOLUME_PATH="/var/lib/libvirt/images"
VM_FOLDER="vm"
METADATA_FOLDER="cloud-init"
SHUTDOWN_WAIT=120
#
# Install libvert VM and add udev hooks but don't start it
#
install() {
echo >&2 "Info: install: using VM definition: ${cfn_vm_def}"
# Ensure domain does not exist; status should return error
domain_running
if [ $? -lt 2 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is already installed, not installing."
return 1
fi
# Retrieve the bootDisk variables
cfn_image_path=$(yq e '.bootDisk.imageLocation' $cfn_vm_def)
if [ $? -ne 0 ] || [ -z "$cfn_image_path" ]; then
echo >&2 "Error: Failed to read bootDisk/imageLocation in config: ${cfn_vm_def}"
return 1
fi
cfn_boot_size=$(yq e '.bootDisk.size' $cfn_vm_def)
if [ $? -ne 0 ] || [ -z "$cfn_boot_size" ]; then
echo >&2 "Error: Failed to read bootDisk/size in config: ${cfn_vm_def}"
return 1
fi
cfn_os_variant=$(yq e '.bootDisk.osVariant' $cfn_vm_def)
if [ $? -ne 0 ] || [ -z "$cfn_os_variant" ]; then
echo >&2 "Error: Failed to read bootDisk/osVariant in config: ${cfn_vm_def}"
return 1
fi
# Create boot device
echo >&2 "Info: Creating boot and metadata images in ${VOLUME_PATH}/${cfn_vm_name}"
mkdir -vp ${VOLUME_PATH}/${cfn_vm_name}
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to make image folder: ${VOLUME_PATH}/${cfn_vm_name}"
return 1
fi
# Make a copy of the image
# TODO: This assumes the input file is a qcow2 and no conversions are necessary
echo >&2 "Info: Copying image from ${cfn_image_path}"
cfn_boot_path=${VOLUME_PATH}/${cfn_vm_name}/${cfn_vm_name}.qcow2
cp ${cfn_image_path} ${cfn_boot_path}
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to copy the image to the VOLUME_PATH: ${VOLUME_PATH}"
return 1
fi
# Resize the image
qemu-img resize ${cfn_boot_path} $cfn_boot_size
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to resize the image to ${boot_size}: ${cfn_boot_path}"
return 1
fi
# Confirm that user-data and meta-data exist
if [ ! -f "${cfn_vm_folder}/${METADATA_FOLDER}/meta-data" ]; then
echo >&2 "Error: meta-data file missing from: ${cfn_vm_folder}/${METADATA_FOLDER}"
return 1
fi
if [ ! -f "${cfn_vm_folder}/${METADATA_FOLDER}/user-data" ]; then
echo >&2 "Error: user-data file missing from: ${cfn_vm_folder}/${METADATA_FOLDER}"
return 1
fi
# Create the cloud-init cdrom iso
pushd ${cfn_vm_folder}/${METADATA_FOLDER} > /dev/null
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to cd to: ${cfn_vm_folder}/${METADATA_FOLDER}"
popd > /dev/null
return 1
fi
cfn_cidata_path=${VOLUME_PATH}/${cfn_vm_name}/${cfn_vm_name}-cidata.iso
sudo xorrisofs -o ${cfn_cidata_path} -V CIDATA -J -r * 2> /dev/null
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to run xorrisofs in folder: ${cfn_vm_folder}/${METADATA_FOLDER}"
popd > /dev/null
return 1
fi
popd > /dev/null
# Array that will hold all the command line options for virt-install
cfn_vm_parms=()
# Define virt-install command parameters
cfn_vm_parms+=("virt-install --import")
cfn_vm_parms+=("--connect ${QEMU_URI}")
cfn_vm_parms+=("--name ${cfn_vm_name}")
cfn_vm_parms+=("--os-variant ${cfn_os_variant}")
# - include --disk for boot and cidata disks
cfn_vm_parms+=("--disk ${cfn_boot_path},format=qcow2")
cfn_vm_parms+=("--disk path=${cfn_cidata_path}")
# - include "--param value" for each entry in install.parameters[]
cfn_install_params=$(yq e --output-format shell '.install.parameters[]' $cfn_vm_def)
if [ $? -ne 0 ] || [ -z "$cfn_install_params" ]; then
echo >&2 "Error: Failed to read install.parameters in config"
return 1
fi
while IFS="=" read -r param value; do
#echo "${param}-->${value}"
cfn_vm_parms+=("--${param} ${value}")
done <<< "${cfn_install_params//\'/}" #remove single quotes from the shell format
cfn_vm_parms+=("--noreboot") #don't start the VM
# Call virt-install
command_str=""
for parm in "${cfn_vm_parms[@]}"; do
#echo "${parm} \\"
command_str+="${parm} "
done
command_str=${command_str::-1}
echo >&2 $command_str
(${command_str}) >> /dev/null
if [ $? -ne 0 ]; then
echo >&2 "Error: virt-installed failed"
return 1
fi
echo >&2 ""
# Create systemd service for USB host device hot-plug events
# NOTE: vm name can not contain hyphen or systemd %j breaks
local systemd_file="${SYSTEMD_FOLDER}/virt-compose-${cfn_vm_name}@.service"
echo >&2 "Info: Creating systemd service: ${systemd_file}"
cat << EOF | sudo tee ${systemd_file} >> /dev/null
[Unit]
Description=virt-compose: Manage device hot-plug event for %j@/%I
[Service]
Type=oneshot
SyslogIdentifier=%N
ExecStart=/bin/echo "Received vm: %j - device: /%I"
ExecStart=${BIN_FOLDER}/virt-compose --config ${BASE_FOLDER}/${CONFIG} attach-device %j /%I
EOF
sudo systemctl daemon-reload
return 0
}
#
# Set DEVNUM and DEVBUS variables for a device given
# the vendor id, product id, and serial number
#
locate_device() {
local vendor_id="$1"; local product_id="$2"; local serial_no="$3"
local device
local return_val=1 #assume failed
for device in /sys/bus/usb/devices/*; do
if [ -f "$device/idVendor" ] && [ -f "$device/idProduct" ]; then
local device_vendor_id=$(cat "$device/idVendor")
local device_product_id=$(cat "$device/idProduct")
if [ "$device_vendor_id" == "$vendor_id" ] && [ "$device_product_id" == "$product_id" ]; then
if [ -f "$device/serial" ]; then
local device_serial_number=$(cat "$device/serial")
if [ "$device_serial_number" == "$serial_no" ]; then
#set DEVBUS and DEVNUM variables
local udevadmOut=$(udevadm info -x --query=property ${device} | grep BUSNUM)
local devbus_status=$?
local value
IFS="=" read -r param value <<< ${udevadmOut//\'/}
DEVBUS=$value
udevadmOut=$(udevadm info -x --query=property ${device} | grep DEVNUM)
local devnum_status=$?
IFS="=" read -r param value <<< ${udevadmOut//\'/}
DEVNUM=$value
return_val=$(( devbus_status && devnum_status ))
if [ $return_val ]; then
echo >&2 "Info: Found device $device, DEVBUS=${DEVBUS}, DEVNUM=${DEVNUM}"
fi
fi
fi
fi
fi
done
return $return_val
}
#
# Returns a device id used as an index key in the metadata
# This function ensures all parts of the code format the same
#
gen_device_id() {
local vendor="$1"; local product="$2"; local serial="$3"
echo "usb-${vendor}-${product}-${serial}"
}
#
# Returns true(0) if the domain is running, false(1) if shutdown, and false(2) on error
#
domain_running() {
local domain_status=$(virsh --connect ${QEMU_URI} domstate ${cfn_vm_name} 2> /dev/null)
if [ $? -ne 0 ] || [ -z "${domain_status}" ]; then
# echo >&2 "Error: Failed to get domain status for ${cfn_vm_name}"
return 2
fi
if [ "$domain_status" == "running" ]; then
#echo >&2 "Info: Domain is running for ${cfn_vm_name}"
return 0
#else
#echo >&2 "Info: Domain is not running for ${cfn_vm_name}"
fi
return 1
}
#
# Writes metadata to --config if domain is shutdown and both --live and --config otherwise
#
metadata_write() {
local new_metadata=$1
local metadata_type="--config"
domain_running
local domain_status=$?
if [ $domain_status -eq 0 ]; then
metadata_type+=" --live"
else
if [ $domain_status -gt 1 ]; then
return 1 #domain doesn't exist or some other error
fi
fi
# Write new_metadata back to VM domain
virsh --connect ${QEMU_URI} metadata ${cfn_vm_name} ${metadata_type} ${DOMAIN_METADATA_URI} ${DOMAIN_METADATA_NS} "${new_metadata}" > /dev/null
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to write metadata for domain ${cfn_vm_name}"
return 1
fi
return 0
}
#
# Retrieves all existing device metadata. Patches the XML with new data for key.
# Rewrites the domain metadata.
#
metadata_add_device() {
local key="$1"; local devbus="$2"; local devnum="$3"
#echo "debug: metadata_add_device( key=${key}, devbus=${devbus}, devnum=${devnum})"
local return_val=0 #assume return true
local new_metadata='<usbdev>'
# Retrieve metadata from VM domain
local domain_metadata=$(virsh --connect ${QEMU_URI} metadata ${cfn_vm_name} ${DOMAIN_METADATA_URI} 2> /dev/null)
if [ ! -z "$domain_metadata" ]; then
# Retrieve metadata keys
local domain_keys=$(yq -p=xml '.usbdev.usb |= ([] + .) | .usbdev.usb | keys[]' <<< $domain_metadata)
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to retrieve keys from domian data for domain ${cfn_vm_name}"
return 1
fi
#echo "domain_metadata='${domain_metadata}'"
# Loop over all existing metadata keys saving the existing entries
if [ ! -z "${domain_keys}" ]; then
while IFS= read -r i; do
#echo "Debug: Processing index: $i"
# - Parse device data
local domain_device=$(yq -p=xml ".usbdev.usb |= ([] + .) | .usbdev.usb[$i] | .+@id + \" \" + .+@bus + \" \" + .+@device" <<< $domain_metadata)
#echo "Debug: domain_device='${domain_device}'"
# - Add existing entry to new_metadata structure #i.e. save all existing entries
# usb-0403_6001_FTF50XXM 002 036
read -r id bus device <<< ${domain_device//\"/} #remove quotes from items
#echo "Debug: Parsed: id=$id bus=$bus device=$device"
# "<usb id='0403_6001_FTF50XXM' bus='002' device='036'/>"
new_metadata+="<usb id='${id}' bus='${bus}' device='${device}'/>"
done <<< "${domain_keys}"
fi
fi
# Add new device
new_metadata+="<usb id='${key}' bus='${devbus}' device='${devnum}'/>"
new_metadata+='</usbdev>'
#echo "Debug: new_metadata=${new_metadata}"
# Write new_metadata back to VM domain
metadata_write "${new_metadata}"
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to write metadata for domain ${cfn_vm_name}"
return_val=1
fi
return $return_val
}
#
# Retrieves all metadata, picks out XML matching $key; setting DEVBUS/DEVNUM
# Removes device from metadata and rewrites the metadata
#
metadata_remove_device() {
local key="$1"
local return_val=1 #return false if entry is not found
# Variables should be undefined if entry is not found in metadata
unset DEVNUM
unset DEVBUS
# Retrieve metadata from VM domain
local domain_metadata=$(virsh --connect ${QEMU_URI} metadata ${cfn_vm_name} ${DOMAIN_METADATA_URI} 2> /dev/null)
if [ -z "$domain_metadata" ]; then
return_val=1
else
#echo "Looking for key: ${key}"
local new_metadata='<usbdev>'
# Retrieve metadata keys
local domain_keys=$(yq -p=xml '.usbdev.usb |= ([] + .) | .usbdev.usb | keys[]' <<< $domain_metadata)
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to retrieve keys from domain data for domain ${cfn_vm_name}"
return 1
fi
#echo "domain_metadata='${domain_metadata}'"
# Loop over all existing metadata keys saving the entries that don't match
local device_index
if [ ! -z "${domain_keys}" ]; then
while IFS= read -r device_index; do
#echo "Processing index: $device_index"
# - Retrieve device data
local domain_device=$(yq -p=xml ".usbdev.usb |= ([] + .) | .usbdev.usb[$device_index] | .+@id + \" \" + .+@bus + \" \" + .+@device" <<< $domain_metadata)
#echo "domain_device='${domain_device}'"
# 0403_6001_FTF50XXM 002 036
read -r id bus device <<< ${domain_device//\"/} #remove quotes from items
#echo "Parsed: id=$id bus=$bus device=$device"
if [ "$key" == "$id" ]; then
DEVBUS=$bus
DEVNUM=$device
return_val=0 #found so return true
else
# - Add existing entry to new_metadata structure #i.e. save all existing entries
# "<usb id='0403_6001_FTF50XXM' bus='002' device='036'/>"
new_metadata+="<usb id='${id}' bus='${bus}' device='${device}'/>"
fi
done <<< "${domain_keys}"
fi
new_metadata+='</usbdev>'
#echo $new_metadata
# Write new_metadata back to VM domain
metadata_write "${new_metadata}"
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to write metadata for domain ${cfn_vm_name}"
return_val=1
fi
fi
return $return_val
}
gen_device_xml() {
local devbus="$((10#$1))"; local devnum="$((10#$2))" #convert strings with leading zeros to base10 ints
cat <<EOF
<hostdev mode='subsystem' type='usb' managed='yes'>
<source>
<address bus='${devbus}' device='${devnum}' />
</source>
</hostdev>
EOF
}
#
# Loop over all entries in vm state (metadata)
# - If entry not already "visited"; then call detach-device and remove metadata
#
metadata_clean_devices() {
# function parameters are a space separated list of visited device ids
local return_val=0
#Loop over the devices as function arguments populating an assciative array for doing key lookups
declare -A visited_devices
local visited_key=""
if [ ! -z "$1" ]; then
for visited_key in "$@"; do
#echo "visited_devices[$visited_key]"
visited_devices["${visited_key}"]="true"
done
fi
# Loop over all entries in vm state (metadata)
# - If entry not already "visited"; then call detach-device and remove metadata
# Retrieve metadata from VM domain
local domain_metadata=$(virsh --connect ${QEMU_URI} metadata ${cfn_vm_name} ${DOMAIN_METADATA_URI} 2> /dev/null)
if [ ! -z "$domain_metadata" ]; then
local new_metadata='<usbdev>'
# Retrieve metadata keys
local domain_keys=$(yq -p=xml '.usbdev.usb |= ([] + .) | .usbdev.usb | keys[]' <<< $domain_metadata)
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to retrieve keys from domian data for domain ${cfn_vm_name}"
return 1
fi
#echo "domain_metadata='${domain_metadata}'"
# Loop over all existing metadata keys saving the entries that are not in visited_devices
local device_index
if [ ! -z "${domain_keys}" ]; then
while IFS= read -r dev_index; do
#echo "Processing index: $dev_index"
# - Parse device data
local domain_device=$(yq -p=xml ".usbdev.usb |= ([] + .) | .usbdev.usb[$dev_index] | .+@id + \" \" + .+@bus + \" \" + .+@device" <<< $domain_metadata)
#echo "domain_device='${domain_device}'"
# usb-0403_6001_FTF50XXM 002 036
local device_id; local devbus; local devnum
read -r device_id devbus devnum <<< ${domain_device//\"/} #remove quotes from items
#echo "Parsed: id=$device_id bus=$devbus device=$devnum"
#If found
if [[ -v visited_devices["${device_id}"] ]]; then
# save the metadata
#echo "Device exists in visited_devices; saving: ${device_id}"
new_metadata+="<usb id='${device_id}' bus='${devbus}' device='${devnum}'/>"
else
if [ ! -z "$device_id" ]; then
# don't save metadata (i.e. delete from metadata)
# call virsh detach-device
echo >&2 "Info: Device not found in config; detaching from VM: ${device_id}"
local old_device_xml=$(gen_device_xml $devbus $devnum)
echo >&2 "Info: Running virsh detach-device ${cfn_vm_name} with bus=${devbus} device=${devnum}:"
#echo $old_device_xml
virsh --connect ${QEMU_URI} detach-device "${cfn_vm_name}" /dev/stdin --persistent <<< "$old_device_xml"
if [ $? -ne 0 ]; then
echo >&2 "Error: virsh detach-device failed for: ${device_id}"
fi
#else
#echo "device_id is blank!; not saving... no other action taken"
fi
fi
unset device
done <<< "${domain_keys}"
fi
new_metadata+='</usbdev>'
#echo "new_metadata=${new_metadata}"
# Write new_metadata back to VM domain
metadata_write "${new_metadata}"
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to write metadata for domain ${cfn_vm_name}"
return_val=1
fi
fi
return $return_val
}
#
# Start VM with virsh start $VM after detach/attach of USB devices
#
start() {
echo >&2 "Info: start: using VM definition: ${cfn_vm_def}"
# Ensure domain exists but is not running
domain_running
local ret_val=$?
if [ $ret_val -eq 0 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is already started."
return 1
elif [ $ret_val -gt 1 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is not installed, use install first."
return 1
fi
local visited_devices=""
# Loop through usbHostDev, if it exists, attaching the usb devices
local usb_host_dev=$(yq e '.usbHostDev | keys[]' $cfn_vm_def 2> /dev/null)
if [ $? -ne 0 ] || [ -z "$usb_host_dev" ]; then
echo >&2 "Info: Did not find usbHostDev entries in config: ${cfn_vm_def}"
else
local dev_index
local udev_rules=""
while IFS= read -r dev_index; do
# Query current bus/dev attachment for configured device
local skip_device=0
declare -A device
local device_params=$(yq e ".usbHostDev[$dev_index] | to_entries[] | .key + \"=\" + .value" $cfn_vm_def)
if [ $? -ne 0 ] || [ -z "$device_params" ]; then
echo >&2 "Error: Failed to read usbHostDev list in config"
skip_device=1
else
local key; local value
while IFS="=" read -r key value; do
#echo "${dev_index}: ${key}=${value}"
device[${key}]=$value
done <<< "${device_params}"
#locate_device() sets DEVBUS and DEVNUM variables
if ! locate_device ${device[vendor]} ${device[product]} ${device[serial]}; then
skip_device=1
echo >&2 "Warning: Could not locate device -> name: ${device[name]}"
fi
fi
if [ $skip_device -eq 0 ]; then
local device_id=$(gen_device_id "${device[vendor]}" "${device[product]}" "${device[serial]}")
local new_DEVNUM=$DEVNUM; local new_DEVBUS=$DEVBUS
# TODO - if $DEVNUM eq $new_DEVNUM and $DEVBUS eq $new_DEVBUS, skip the detach/attach cycle
# Call metadata_remove_device usb-vendor-product-serial to remove from metadata and return old bus/dev
# sets $DEVNUM and $DEVBUS of the old attached device; unset if device not found
metadata_remove_device $device_id
if [ $? ]; then
visited_devices+="${device_id} "
fi
#if dev is in metadata; Call virsh detach-device --persistent with old bus/dev to detach device
if [ ! -z "$DEVNUM" ]; then
local old_device_xml=$(gen_device_xml $DEVBUS $DEVNUM)
echo >&2 "Info: Running virsh detach-device ${cfn_vm_name} with bus=${DEVBUS} device=${DEVNUM}:"
#echo $old_device_xml
virsh --connect ${QEMU_URI} detach-device "${cfn_vm_name}" /dev/stdin --persistent <<< "$old_device_xml"
if [ $? -ne 0 ]; then
echo >&2 "Error: virsh detach-device failed for: ${device_id}"
fi
fi
# Call virsh attach-device --persistent with new bus/dev to attach device
local new_device_xml=$(gen_device_xml $new_DEVBUS $new_DEVNUM)
echo >&2 "Info: Running virsh attach-device ${cfn_vm_name} with bus=${new_DEVBUS} device=${new_DEVNUM}:"
#echo $new_device_xml
virsh --connect ${QEMU_URI} attach-device "${cfn_vm_name}" /dev/stdin --persistent <<< "$new_device_xml"
if [ $? -ne 0 ]; then
echo >&2 "Error: virsh attach-device failed for: ${device_id}"
fi
metadata_add_device $device_id $new_DEVBUS $new_DEVNUM
# Create udev rule for the USB host device hot-plug event
udev_rules+='ACTION=="add", SUBSYSTEM=="usb", DRIVER=="usb", '
udev_rules+="ATTRS{idVendor}==\"${device[vendor]}\", "
udev_rules+="ATTRS{idProduct}==\"${device[product]}\", "
udev_rules+="ATTRS{serial}==\"${device[serial]}\", "
udev_rules+="PROGRAM=\"/usr/bin/systemd-escape -p --template=virt-compose-${cfn_vm_name}@.service \$env{DEVNAME}\", "
udev_rules+='TAG+="systemd", ENV{SYSTEMD_WANTS}+="%c"'
udev_rules+=$'\n'
else
echo >&2 "Warning: Continuing without device"
fi
unset DEVNUM
unset DEVBUS
unset device
done <<< "$usb_host_dev"
fi
#echo "udev_rules="$'\n'"$udev_rules"
local udev_file="${UDEV_RULES_FOLDER}/90-virt-compose-${cfn_vm_name}.rules"
echo >&2 "Info: Creating udev rules file: ${udev_file}"
if [ ! -z "$udev_rules" ]; then
cat << EOF | sudo tee ${udev_file} >> /dev/null
$udev_rules
EOF
sudo udevadm control --reload-rules
fi
# Loop trough all devices in the metadata and remove any that were not in "visited"
# "visited" means the VM configuration exists and the device is present
metadata_clean_devices $visited_devices #intentionally left off "" to pass as a list of cmd parms
# Damn... finally, call virsh start
virsh --connect ${QEMU_URI} start ${cfn_vm_name}
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to start ${cfn_vm_name}"
return 1
fi
return 0
}
#
# systemd service uses this to start all the autoStart VMs on host boot
#
start_all() {
echo >&2 "Info: start-all"
local file
for file in ${BASE_FOLDER}/${VM_FOLDER}/* ; do
if [ -d "$file" ]; then
cfn_vm_name=$(basename "${file}")
cfn_vm_def="${file}/${cfn_vm_name}.yaml"
echo >&2 "Info: Checking vm configuration: ${cfn_vm_def}"
local auto_start=$(yq e '.autoStart' $cfn_vm_def)
if [ $? -ne 0 ] || [ -z "$auto_start" ]; then
echo >&2 "Error: Failed to read autoStart in config ${cfn_vm_def}. Skipping VM."
elif [ "x$auto_start"="xtrue" ]; then
echo >&2 "Info: Starting vm ${cfn_vm_name}"
start || echo >&2 "Error: Failed to auto-start ${cfn_vm_name}"
fi
fi
done
return 0
}
#
# call virsh shutdown
#
shutdown() {
echo >&2 "Info: shutdown: using VM definition: ${cfn_vm_def}"
# Ensure domain exists and is running
domain_running
local ret_val=$?
if [ $ret_val -eq 1 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is already shutdown."
return 1
elif [ $ret_val -gt 1 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is not installed."
return 1
fi
# Remove udev rules for USB host device hot-plug events
local udev_file="${UDEV_RULES_FOLDER}/90-virt-compose-${cfn_vm_name}.rules"
echo >&2 "Info: Removing udev rules file: ${udev_file}"
sudo rm $udev_file
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to remove ${udev_file}"
fi
sudo udevadm control --reload-rules
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to reload udev rules"
fi
virsh --connect ${QEMU_URI} shutdown ${cfn_vm_name}
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to shutdown ${cfn_vm_name}"
return 1
fi
return 0
}
#
# systemd service uses this to shutdown all VMs on host shutdown
#
shutdown_all() {
echo >&2 "Info: shutdown-all"
local wait_list=""
local file
for file in ${BASE_FOLDER}/${VM_FOLDER}/* ; do
if [ -d "$file" ]; then
cfn_vm_name=$(basename "${file}")
cfn_vm_def="${file}/${cfn_vm_name}.yaml"
echo >&2 "Info: Checking vm configuration: ${cfn_vm_def}"
local auto_start=$(yq e '.autoStart' $cfn_vm_def)
if [ $? -ne 0 ] || [ -z "$auto_start" ]; then
echo >&2 "Error: Failed to read autoStart in config ${cfn_vm_def}. Skipping VM."
elif [ "x$auto_start"="xtrue" ]; then
echo >&2 "Info: Shutting down vm ${cfn_vm_name}"
shutdown || echo >&2 "Error: Failed to shutdown ${cfn_vm_name}"
wait_list+="${cfn_vm_name} "
fi
fi
done
local remaining_wait_list
local timeout=$SHUTDOWN_WAIT
if [ -n "$wait_list" ]; then
echo >&2 "Info: Waiting for vms to shutdown. Timeout ${SHUTDOWN_WAIT} seconds."
[ $timeout -lt 0 ] && timeout=0
while [ -n "$wait_list" ] && [ $timeout -gt 0 ]; do
remaining_wait_list=""
for cfn_vm_name in $wait_list; do
if ! domain_running; then
echo >&2 "Info: Shutdown: ${cfn_vm_name}"
else
remaining_wait_list+="${cfn_vm_name} "
fi
done
wait_list=$remaining_wait_list
if [ -n "$wait_list" ]; then
sleep 1
((timeout--))
fi
done
fi
if [ -n "$wait_list" ]; then
echo -n >&2 "Warning: Timeout reached. Remaining VMs:"
for cfn_vm_name in $wait_list; do
echo -n >&2 " ${cfn_vm_name}"
done
echo >&2
fi
return 0
}
#
# Wraps virsh attach-device and detach-device creating correct XML data
# Called by systemd service on udev hot-plug events
# Also can be called manually to add a device
#
attach_device() {
echo >&2 "Info: attach-device device: ${cfn_device_path}"
# Ensure domain exists
domain_running
if [ $? -gt 1 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is not installed."
return 1
fi
local return_val=0
# Use "udevadmin info $cfn_device_path" to retrieve ID_VENDOR_ID,
# ID_MODEL_ID, ID_SERIAL_SHORT, BUSNUM, and DEVNUM
declare -A device
local udevadmOut=$(udevadm info --export --query=env ${cfn_device_path})
local udevadm_status=$?
local param; local value
if [ ! $udevadm_status ] || [ -z "$udevadmOut" ]; then
echo >&2 "Error: udevadm info failed for device ${cfn_device_path}"
else
#echo "udevadm command ok: output: $udevadmOut"
while IFS="=" read -r param value; do
#echo "${dev_index}: ${param}=${value}"
device[${param}]=$value
done <<< ${udevadmOut//\'/}
# Use $cfn_vm_name to remove the old device mapping matching vendor,
# product, serial from the vm metadata
local device_id=$(gen_device_id "${device[ID_VENDOR_ID]}" "${device[ID_MODEL_ID]}" "${device[ID_SERIAL_SHORT]}")
# Call metadata_remove_device usb-vendor-product-serial to remove from metadata and return old bus/dev
# sets $DEVNUM and $DEVBUS of the old attached device; unset if device not found
metadata_remove_device "${device_id}"
if [ ! -z "$DEVNUM" ]; then
local old_device_xml=$(gen_device_xml $DEVBUS $DEVNUM)
echo >&2 "Info: Running virsh detach-device ${cfn_vm_name} with bus=${DEVBUS} device=${DEVNUM}:"
#echo $old_device_xml
virsh --connect ${QEMU_URI} detach-device "${cfn_vm_name}" /dev/stdin --persistent <<< "$old_device_xml"
if [ $? -ne 0 ]; then
echo >&2 "Error: virsh detach-device failed for: ${device_id}"
fi
fi
# Use "virsh attach" to add the new device mapping
local new_device_xml=$(gen_device_xml "${device[BUSNUM]}" "${device[DEVNUM]}")
echo >&2 "Info: Running virsh attach-device ${cfn_vm_name} with bus=${device[BUSNUM]} device=${device[DEVNUM]}:"
#echo $new_device_xml
virsh --connect ${QEMU_URI} attach-device "${cfn_vm_name}" /dev/stdin --persistent <<< "$new_device_xml"
if [ $? -ne 0 ]; then
echo >&2 "Error: virsh attach-device failed for: ${device_id}"
fi
# Add new device mapping to metadata
metadata_add_device $device_id ${device[BUSNUM]} ${device[DEVNUM]}
fi
unset device
return $return_val
}
#
# Wraps virsh detach-device creating correct XML data
#
detach_device() {
echo >&2 "Info: detach-device device: ${cfn_device_path}"
# Ensure domain exists
domain_running
if [ $? -gt 1 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is not installed."
return 1
fi
local return_val=0
# Use "udevadmin info $cfn_device_path" to retrieve ID_VENDOR_ID,
# ID_MODEL_ID, ID_SERIAL_SHORT, BUSNUM, and DEVNUM
declare -A device
local udevadmOut=$(udevadm info --export --query=env ${cfn_device_path})
local udevadm_status=$?
local param; local value
if [ ! $udevadm_status ] || [ -z "$udevadmOut" ]; then
echo >&2 "Error: udevadm info failed for device ${cfn_device_path}"
else
while IFS="=" read -r param value; do
#echo "${dev_index}: ${param}=${value}"
device[${param}]=$value
done <<< ${udevadmOut//\'/}
# Use $cfn_vm_name to remove the old device mapping matching vendor,
# product, serial from the vm metadata
local device_id=$(gen_device_id "${device[ID_VENDOR_ID]}" "${device[ID_MODEL_ID]}" "${device[ID_SERIAL_SHORT]}")
# Call metadata_remove_device usb-vendor-product-serial to remove from metadata and return old bus/dev
# sets $DEVNUM and $DEVBUS of the old attached device; unset if device not found
metadata_remove_device "${device_id}"
if [ ! -z "$DEVNUM" ]; then
local old_device_xml=$(gen_device_xml "${device[BUSNUM]}" "${device[DEVNUM]}")
echo >&2 "Info: Running virsh detach-device ${cfn_vm_name} with bus=${device[BUSNUM]} device=${device[DEVNUM]}:"
#echo $old_device_xml
virsh --connect ${QEMU_URI} detach-device "${cfn_vm_name}" /dev/stdin --persistent <<< "$old_device_xml"
if [ $? -ne 0 ]; then
echo >&2 "Error: virsh detach-device failed for: ${device_id}"
fi
fi
fi
unset device
return $return_val
}
#
# Undefines the VM, removes udev hooks, and removes the image folder
#
undefine() {
echo >&2 "Info: uninstall: using VM definition: ${cfn_vm_def}"
# Ensure domain exists
domain_running
if [ $? -gt 1 ]; then
echo >&2 "Error: VM ${cfn_vm_name} is not installed."
return 1
fi
# Remove systemd service for USB host device hot-plug events
local systemd_file="${SYSTEMD_FOLDER}/virt-compose-${cfn_vm_name}@.service"
echo >&2 "Info: Removing systemd service: ${systemd_file}"
sudo rm $systemd_file
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to remove ${systemd_file}"
fi
sudo systemctl daemon-reload
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to reload systemd after removing service"
fi
# virsh destroy - Hard immediate power off
virsh --connect ${QEMU_URI} destroy ${cfn_vm_name} > /dev/null 2>&1
# ignore error if not running
# if [ $? -ne 0 ]; then
# echo >&2 "Error: Failed to destroy ${cfn_vm_name}"
# return 1
# fi
# virsh undefine - Remove VM definition
virsh --connect ${QEMU_URI} undefine ${cfn_vm_name}
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to undefine ${cfn_vm_name}"
return 1
fi
# Confirm that image folder exists and isn't root before trying to delete it
local boot_path="${VOLUME_PATH}/${cfn_vm_name}"
if [ -z "${VOLUME_PATH}" ]; then
echo >&2 "Error: VOLUME_PATH is not defined; not deleting image folder"
return 1
fi
if [ -z "${cfn_vm_name}" ]; then
echo >&2 "Error: vm name is not defined; not deleting image folder"
return 1
fi
if [ ! -d "${boot_path}" ]; then
echo >&2 "Error: Image folder does not exist: ${boot_path}"
return 1
fi
# remove the install files
echo >&2 "Info: Removing VM boot volume: ${boot_path}"
sudo rm -rf ${boot_path}
if [ $? -ne 0 ]; then
echo >&2 "Error: Failed to remove image folder: ${boot_path}"
return 1
fi
return 0
}
#
# Reads GLOBAL section from ${cfn_config_file} and exports the map of variables