vm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. #!/bin/bash
  2. read -r -d '' USAGE <<-'USAGE_END'
  3. USAGE:
  4. vm [ -h | --help ]
  5. vm [ -l | --list ]
  6. vm [ <img> | <img-dir> | <n> ] [ <qemu-args> ]
  7. where: <img> is the path to an .img file
  8. where: <img-dir> is one of `ls -d $VM_DIR/*/`
  9. where: <n> is one of the numbered selections of `vm --list`
  10. where: <qemu-args> are passed to qemu
  11. ENVIRONMENT:
  12. * VM_DIR - (mandatory) full path to parent directory of VM subdirectories
  13. * ARCH - (optional) launches qemu-system-$ARCH (default: 'x86_64 + kvm + smp')
  14. * MEM - (optional) specifies VM memory size (default: 2G)
  15. * AUDIODEV - (optional) specifies virtual audo device
  16. * ISO - (optional) full path to .iso file for first optical disc - set as primary boot media (default: none)
  17. * IMG_B - (optional) full path to .img file for second hard disk (default: none)
  18. * SMP - (optional) SMP options as CSV: N_CORES,N_THREADS,N_SOCKETS (default: 2,1,1)
  19. * VM_SSH_PORT - (optional) host port to bind to guest port tcp/22
  20. * VM_HTTP_PORT - (optional) host port to bind to guest port tcp/80
  21. * VM_SSH_LOGIN - (optional) guest SSH user to login as (if VM_SSH_PORT defined)
  22. * VM_WAIT_SSH - (optional) pause before SSH login (if VM_SSH_PORT and VM_SSH_LOGIN defined)
  23. EXAMPLES:
  24. vm # present an alphabetical list of VMs, and wait for choice
  25. vm /home/me/vm.img # launch a VM by path to an image file (absolute or relative)
  26. vm img-dir # launch $VM_DIR/img-dir/img-dir.img if it exists
  27. vm -h (or --help) # present this message then quit
  28. vm -l (or --list) # present an alphabetical list of VMs, then quit
  29. vm 1 # launch a VM by it's current order number (per `vm --list`)
  30. ARCH=i386 vm # launch 32-bit x86 VM
  31. MEM=1G vm # launch VM using 1GB of system memory
  32. ISO=/path/to/iso vm # boot from .iso file
  33. IMG_B=/path/to/img vm # mount second hard disk
  34. VM_SSH_PORT= vm # do not forward SSH
  35. VM_HTTP_PORT= vm # do not forward HTTP
  36. VM_SSH_LOGIN= vm # forward SSH but do not login
  37. VM_WAIT_SSH=60 vm # wait N seconds to login
  38. USAGE_END
  39. readonly HAS_LIST_SWITCH=$([ "$1" == "-l" -o "$1" == "--list" ] && echo 1 || echo 0)
  40. readonly HAS_HELP_SWITCH=$([ "$1" == "-h" -o "$1" == "--help" ] && echo 1 || echo 0)
  41. readonly IMG_NAME_OR_N=$1 # pending validation
  42. readonly IMG_FULL_PATH="$VM_DIR/$IMG_NAME_OR_N/$IMG_NAME_OR_N.img" # pending validation
  43. readonly QEMU_ARGS=$(args="$*" ; echo $args | grep ' -' > /dev/null && echo "-${args#* -}" )
  44. readonly IS_HEADLESS=$( [[ ! -f /usr/lib/qemu/ui-sdl.so ]] && echo 1 || echo 0 )
  45. readonly VM_DIR_NULL_ERR_MSG="\$VM_DIR must be defined in the environment"
  46. readonly VM_DIR_MISSING_ERR_MSG="\$VM_DIR not found: '$VM_DIR'"
  47. readonly IMG_MOUNTED_ERR_MSG="\$Img is mounted"
  48. readonly ISOS_DIR_NAME='isos'
  49. readonly SCRATCH_IMG_NAME='scratch'
  50. readonly SCRATCH_IMG_SIZE=1G
  51. readonly ISO_NONE='None'
  52. readonly ISOS_DIR=$VM_DIR/$ISOS_DIR_NAME
  53. readonly SCRATCH_IMG_DIR=$VM_DIR/$SCRATCH_IMG_NAME
  54. readonly SCRATCH_IMG=$SCRATCH_IMG_DIR/$SCRATCH_IMG_NAME.img
  55. readonly SMP_REGEX="^[0-9],[0-9],[0-9]$"
  56. readonly DEF_MEM='2G'
  57. readonly DEF_AUDIODEV='alsa'
  58. readonly DEF_VGA='std' # cirrus, qxl, std, vmware
  59. readonly USE_VIRTIO=0
  60. readonly USE_VIRTFS=0
  61. readonly USE_VIRTSMB=0
  62. IMG_DIRS=() # deferred
  63. ISOS=() # deferred
  64. N_IMGS=0 # deferred
  65. N_ISOS=0 # deferred
  66. ImgDir= # deferred
  67. OverridesFile= # deferred
  68. Img= # deferred
  69. Iso= # deferred
  70. SHOULD_CLR_SCREEN=1
  71. Clear() { (( SHOULD_CLR_SCREEN )) && clear ; }
  72. PopulateImgDirs()
  73. {
  74. (( ${#IMG_DIRS[@]} == 0 )) || return
  75. local img_full_path dir img
  76. IMG_DIRS=( $(
  77. [[ -d "$SCRATCH_IMG_DIR/" ]] || mkdir -p "$SCRATCH_IMG_DIR" >&2
  78. [[ -f "$SCRATCH_IMG" ]] || qemu-img create "$SCRATCH_IMG" $SCRATCH_IMG_SIZE >&2
  79. [[ -f "$SCRATCH_IMG" ]] && [[ "$IMG_B" != "$SCRATCH_IMG" ]] && echo $SCRATCH_IMG_NAME
  80. for img_full_path in $(ls -d $VM_DIR/*/*.img 2> /dev/null)
  81. do dir=$(basename $(dirname $img_full_path))
  82. img=$(basename $img_full_path )
  83. [[ "$dir" != "$SCRATCH_IMG_NAME" ]] && [[ "$img_full_path" != "$IMG_B" ]] || continue
  84. grep "^$dir\.img$" <<<$img > /dev/null && echo $dir
  85. done
  86. ) )
  87. readonly IMG_DIRS
  88. readonly N_IMGS=${#IMG_DIRS[@]}
  89. }
  90. PopulateIsos()
  91. {
  92. (( ${#ISOS[@]} == 0 )) || return
  93. local unsorted_isos sorted_isos iso_n sorted_iso_n
  94. unsorted_isos=( $(
  95. [[ -d "$ISOS_DIR/" ]] || mkdir -p "$ISOS_DIR"
  96. find $ISOS_DIR/ -name *.iso 2> /dev/null
  97. ) )
  98. sorted_isos=( $(
  99. for (( iso_n = 0 ; iso_n < ${#unsorted_isos[@]} ; ++iso_n ))
  100. do echo "${unsorted_isos[$iso_n]##*\/}/$iso_n"
  101. done | sort
  102. ) )
  103. for (( iso_n = 0 ; iso_n < ${#sorted_isos[@]} ; ++iso_n ))
  104. do sorted_iso_n=${sorted_isos[$iso_n]#*\/}
  105. ISOS[$iso_n]="${unsorted_isos[$sorted_iso_n]}"
  106. done
  107. ISOS[$iso_n]=${ISO_NONE}
  108. readonly ISOS
  109. readonly N_ISOS=${#ISOS[@]}
  110. }
  111. DoesImgExist() { [[ -f "$1" ]] && [[ "$(grep -E '^.+\.img$' <<<$1)" == "$1" ]] ; }
  112. DoesIsoExist() { [[ -f "$1" ]] && [[ "$(grep -E '^.+\.iso$' <<<$1)" == "$1" ]] ; }
  113. IsImgMounted() { DoesImgExist "$1" && grep "$1" < <(losetup -l) ; return $(( $? )) ; }
  114. IsInteger() { [[ "$1" =~ ^([0-9]+)$ ]] ; }
  115. IsValidSelection() # (option_n , n_options)
  116. {
  117. local option_n=$( IsInteger $1 && echo $1 || echo '-1')
  118. local n_options=$(IsInteger $2 && echo $2 || echo '0' )
  119. [[ "$option_n" -ge "0" ]] && [[ "$option_n" -le "$n_options" ]]
  120. }
  121. PrintImgOptions()
  122. {
  123. local img_dir_n
  124. for img_dir_n in "${!IMG_DIRS[@]}" ; do echo "$(( $img_dir_n + 1 )) ${IMG_DIRS[$img_dir_n]}" ; done ;
  125. }
  126. PrintIsoOptions()
  127. {
  128. local iso_n
  129. for iso_n in "${!ISOS[@]}" ; do echo "$(( $iso_n + 1 )) ${ISOS[$iso_n]##*/}" ; done ;
  130. }
  131. VmDlg() # (err_msg dialog_args*)
  132. {
  133. local err_msg=$( [[ -n "$1" ]] && echo -n "\Z1$1\Zn" ) ; shift ;
  134. dialog --stdout --backtitle "KISS VM Manager" --colors --title "$err_msg" "$@"
  135. }
  136. SelectImage() # (img_n dlg_err_msg)
  137. {
  138. local img_n=$1
  139. local dlg_err_msg="$( [[ -z "$ISO" ]] || DoesIsoExist "$ISO" || echo "\$ISO not found")"
  140. local img_selection=$(IsValidSelection $img_n $N_IMGS && echo $img_n || echo '-1')
  141. if (( $N_IMGS > 0 ))
  142. then IsValidSelection $img_selection $N_IMGS || \
  143. img_selection=$( VmDlg "$dlg_err_msg" \
  144. --menu "Select a primary disk:" 20 70 50 \
  145. $(PrintImgOptions) )
  146. Clear
  147. [[ "$img_selection" != '' ]] || return 1
  148. else echo "no conventionally-named images found in \$VM_DIR '$VM_DIR/'"
  149. return 1
  150. fi
  151. ImgDir=${IMG_DIRS[$(( $img_selection - 1 ))]}
  152. OverridesFile=$VM_DIR/$ImgDir/vm-config
  153. Img=$VM_DIR/$ImgDir/$ImgDir.img
  154. }
  155. SelectIso()
  156. {
  157. local iso_selection='-1'
  158. if (( $N_ISOS > 0 ))
  159. then IsValidSelection $iso_selection $N_ISOS || \
  160. iso_selection=$( VmDlg '' --menu "Select a boot ISO:" 20 70 50 \
  161. $(PrintIsoOptions) )
  162. Clear
  163. [[ "$iso_selection" != '' ]] || return 1
  164. else echo "no ISOs found in \$ISOS_DIR: '$ISOS_DIR/'"
  165. return 1
  166. fi
  167. Iso=${ISOS[$(( $iso_selection - 1 ))]}
  168. }
  169. WaitSsh()
  170. {
  171. ssh-keygen -f "~/.ssh/known_hosts" -R [localhost]:$VM_SSH_PORT
  172. while (( WAIT_SSH > 0 ))
  173. do WAIT_SSH=($WAIT_SSH-1)
  174. Clear ; echo "logging in ssh in $(($WAIT_SSH+1)) seconds" ;
  175. sleep 1
  176. done
  177. }
  178. ## main entry ##
  179. (( $HAS_LIST_SWITCH )) && PrintImgOptions | sed 's|^\([0-9]*\) |\1) |' && exit
  180. (( $HAS_HELP_SWITCH )) && echo "$USAGE" && exit
  181. [[ -z "$VM_DIR" ]] && echo "$VM_DIR_NULL_ERR_MSG" && exit
  182. [[ ! -d "$VM_DIR" ]] && echo "$VM_DIR_MISSING_ERR_MSG" && exit
  183. # initialize data
  184. PopulateImgDirs ; PopulateIsos ;
  185. # determine which VM image to launch
  186. if [[ -n "$IMG_NAME_OR_N" ]]
  187. then if ! IsInteger $IMG_NAME_OR_N
  188. then # find image by img name
  189. for img in "$IMG_NAME_OR_N" "$IMG_FULL_PATH" ; do DoesImgExist $img && Img=$img ; done ;
  190. [[ "$Img" ]] || echo "\$Img not found at '$IMG_NAME_OR_N' or '$IMG_FULL_PATH'"
  191. else IsValidSelection $IMG_NAME_OR_N $N_IMGS || echo "\$Selection ($IMG_NAME_OR_N) out of range"
  192. fi
  193. fi
  194. # find image by img_n, or prompt
  195. DoesImgExist $Img || SelectImage "$IMG_NAME_OR_N" || exit 1
  196. IsImgMounted $Img && echo "$IMG_MOUNTED_ERR_MSG" && exit 1
  197. # determine which ISO to boot
  198. Iso="$ISO"
  199. if ! DoesIsoExist "$Iso" && [[ "$ImgDir" == "$SCRATCH_IMG_NAME" ]]
  200. then (( $(ls *.iso | wc -l) == 1 )) && Iso=$(ls *.iso) || true # pwd override
  201. until DoesIsoExist "$Iso" || [[ "$Iso" == "${ISO_NONE}" ]] ; do SelectIso || exit 1 ; done ;
  202. fi
  203. # log results
  204. echo -e "Selected:$( [[ "$Iso" ]] && echo "\n\tIso: $Iso")\n\tImg: $Img"
  205. # prepare the environment
  206. [[ -f $OverridesFile ]] && source $OverridesFile
  207. [[ "$SMP" =~ $SMP_REGEX ]] && N_CORES=${BASH_REMATCH[1]} N_THREADS=${BASH_REMATCH[2]} N_SOCKETS=${BASH_REMATCH[3]} || \
  208. N_CORES=2 N_THREADS=1 N_SOCKETS=1
  209. [[ -z "$ARCH" ]] && ARCH="x86_64"
  210. [[ "$ARCH" == 'x86_64' ]] && CPU="-cpu host -smp cores=$N_CORES,threads=$N_THREADS,sockets=$N_SOCKETS"
  211. [[ "$ARCH" == 'x86_64' ]] && ARCH="x86_64 -enable-kvm"
  212. [[ "$ImgDir" ]] && NAME="-name $ImgDir"
  213. [[ "$IMG_B" ]] && HD="-drive file=$IMG_B,format=raw,cache=writeback"
  214. DoesIsoExist "$Iso" && CD="-cdrom $Iso" BOOT="-boot order=d" || CD="" BOOT=""
  215. [[ "$MEM" ]] && MEM="-m $MEM" || MEM="-m $DEF_MEM"
  216. [[ "$VGA" ]] && VGA="-vga $VGA" || VGA="-vga $DEF_VGA"
  217. [[ "$AUDIODEV" ]] && AUDIODEV="-audiodev $AUDIODEV" || AUDIODEV="-audiodev $DEF_AUDIODEV"
  218. [[ "$VM_SSH_PORT" ]] && FWD_SSH=",hostfwd=tcp::$VM_SSH_PORT-:22" || FWD_SSH=''
  219. [[ "$VM_HTTP_PORT" ]] && FWD_HTTP=",hostfwd=tcp::$VM_HTTP_PORT-:$VM_HTTP_PORT" || FWD_HTTP=''
  220. QEMU="qemu-system-$ARCH $NAME"
  221. HD="-drive file=$Img,format=raw,cache=writeback $HD"
  222. # BOOT="-boot menu=on"
  223. # prepare networking
  224. SHARED_DIR_DEFAULT=$HOME/.config/vm-shared
  225. [[ ! -d "$SHARED_DIR" ]] && [[ -d "$SHARED_DIR_DEFAULT" ]] && SHARED_DIR=$SHARED_DIR_DEFAULT
  226. NET_VIRTIO="-netdev user,id=vmnic$FWD_SSH$FWD_HTTP -device virtio-net,netdev=vmnic"
  227. NET_VIRTSMB="-net user,smb=\"$SHARED_DIR\" -net nic,model=virtio"
  228. NET_VIRTFS="-virtfs local,path=\"$SHARED_DIR\",mount_tag=host0,security_model=passthrough,id=host0"
  229. # SSH="-redir tcp:$VM_SSH_PORT::22" # no virtio - deprecated
  230. if [[ "$VM_SSH_PORT" ]] || [[ "$VM_HTTP_PORT" ]]
  231. then [[ "$VM_SSH_PORT" ]] && [[ "$VM_SSH_LOGIN" ]] && SSH="ssh -o StrictHostKeyChecking=no -p $VM_SSH_PORT $VM_SSH_LOGIN@localhost"
  232. [[ "`echo $VM_WAIT_SSH | grep -E '^[1-9]+$'`" ]] || WAIT_SSH=30
  233. NET=$( ( (( $USE_VIRTIO )) && echo "$NET_VIRTIO" )
  234. ( (( $USE_VIRTSMB )) && echo "$NET_VIRTSMB" )
  235. ( (( $USE_VIRTFS )) && echo "$NET_VIRTFS" ) ) # guest fstab entry: host0 /a-mountpoint 9p trans=virtio,version=9p2000.L 0 0
  236. fi
  237. # prepare A/V
  238. # AUDIO='-device intel-hda -device hda-duplex'
  239. AUDIO="${AUDIODEV},id=myaudiodev -device AC97,audiodev=myaudiodev"
  240. VIDEO_VNC='-display vnc=:0'
  241. VIDEO_SDL='-display sdl' # ,show-cursor=on -no-frame
  242. (( ${IS_HEADLESS} )) && VIDEO="${VIDEO_VNC}" || VIDEO="${VGA} ${VIDEO_SDL}"
  243. MISC=''
  244. # load virtio kernel modules
  245. if (( $USE_VIRTIO ))
  246. then virtio_modules=`lsmod | grep virtio`
  247. [[ "`echo $virtio_modules | grep virtio_net`" == "" ]] && su -c 'modprobe virtio-net'
  248. #[[ "`echo $virtio_modules | grep virtio_blk`" == "" ]] && su -c 'modprobe virtio-blk'
  249. #[[ "`echo $virtio_modules | grep virtio_scsi`" == "" ]] && su -c 'modprobe virtio-scsi'
  250. #[[ "`echo $virtio_modules | grep virtio_serial`" == "" ]] && su -c 'modprobe virtio-serial'
  251. #[[ "`echo $virtio_modules | grep virtio_balloon`" == "" ]] && su -c 'modprobe virtio-balloon'
  252. fi
  253. # concatenate command line and report
  254. [[ "$FWD_SSH" ]] && ( [[ "$NET" ]] && echo "SSH port 22 forwarded to localhost:$VM_SSH_PORT" || \
  255. echo "no USE_VIRT* enabled - SSH will not be forwarded" )
  256. [[ "$FWD_HTTP" ]] && ( [[ "$NET" ]] && echo "HTTP port $VM_HTTP_PORT forwarded to localhost:$VM_HTTP_PORT" || \
  257. echo "no USE_VIRT* enabled - HTTP will not be forwarded" )
  258. CMD="$QEMU $CPU $MEM $HD $CD $BOOT $NET $AUDIO $VIDEO $MISC $QEMU_ARGS"
  259. msg="launching vm: '$(echo "$Img" | grep -E '^.+\.img$' | sed -e 's/^\(.*\/\)\?\(.*\).img$/\2/')'"
  260. # launch VM and optionally login ssh
  261. if [[ "$1" == '--cmd' ]]
  262. then echo $CMD
  263. elif [[ "$SSH" ]] && [[ "$WAIT_SSH" ]]
  264. then echo $msg ; $CMD & WaitSsh && $SSH ;
  265. else echo $msg ; $CMD ;
  266. fi