haos-vm.sh 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. #!/usr/bin/env bash
  2. NEXTID=$(pvesh get /cluster/nextid)
  3. YW=`echo "\033[33m"`
  4. BL=`echo "\033[36m"`
  5. RD=`echo "\033[01;31m"`
  6. BGN=`echo "\033[4;92m"`
  7. GN=`echo "\033[1;92m"`
  8. DGN=`echo "\033[32m"`
  9. CL=`echo "\033[m"`
  10. BFR="\\r\\033[K"
  11. HOLD="-"
  12. CM="${GN}✓${CL}"
  13. while true; do
  14. read -p "This will create a New Home Assistant OS VM. Proceed(y/n)?" yn
  15. case $yn in
  16. [Yy]* ) break;;
  17. [Nn]* ) exit;;
  18. * ) echo "Please answer yes or no.";;
  19. esac
  20. done
  21. clear
  22. function header_info {
  23. echo -e "${BL}
  24. _ _ ____ _____
  25. | | | | /\ / __ \ / ____|
  26. | |__| | / \ | | | | (___
  27. | __ | / /\ \| | | |\___ \
  28. | | | |/ ____ \ |__| |____) |
  29. |_| |_/_/ ${CL}${YW}v3${CL}${BL} \_\____/|_____/
  30. ${CL}"
  31. }
  32. header_info
  33. function msg_info() {
  34. local msg="$1"
  35. echo -ne " ${HOLD} ${YW}${msg}..."
  36. }
  37. function msg_ok() {
  38. local msg="$1"
  39. echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
  40. }
  41. function default_settings() {
  42. clear
  43. header_info
  44. echo -e "${BL}Using Default Settings${CL}"
  45. echo -e "${DGN}Using VM ID ${BGN}$NEXTID${CL}"
  46. VMID=$NEXTID
  47. echo -e "${DGN}Using VM Name ${BGN}haos${CL}"
  48. VM_NAME=haos
  49. echo -e "${DGN}Using ${BGN}2vCPU${CL}"
  50. CORE_COUNT="2"
  51. echo -e "${DGN}Using ${BGN}4096MiB${CL}"
  52. RAM_SIZE="4096"
  53. echo -e "${DGN}Start VM when completed ${BGN}yes${CL}"
  54. START_VM="yes"
  55. }
  56. function advanced_settings() {
  57. clear
  58. header_info
  59. echo -e "${RD}Using Advanced Settings${CL}"
  60. echo -e "${YW}Enter the VM ID, or Press [ENTER] to automatically generate (${NEXTID}) "
  61. read VMID
  62. if [ -z $VMID ]; then VMID=$NEXTID; fi;
  63. echo -en "${DGN}Set VM ID To ${BL}$VMID${CL}"
  64. echo -e " ${CM}${CL} \r"
  65. sleep 1
  66. clear
  67. header_info
  68. echo -e "${RD}Using Advanced Settings${CL}"
  69. echo -e "${DGN}Using VM ID ${BGN}$VMID${CL}"
  70. echo -e "${YW}Enter VM Name (no-spaces), or Press [ENTER] for Default: hoas "
  71. read VMNAME
  72. if [ -z $VMNAME ]; then
  73. VM_NAME=haos
  74. else
  75. VM_NAME=$(echo ${VMNAME,,} | tr -d ' ')
  76. fi
  77. echo -en "${DGN}Set CT Name To ${BL}$VM_NAME${CL}"
  78. echo -e " ${CM}${CL} \r"
  79. sleep 1
  80. clear
  81. header_info
  82. echo -e "${RD}Using Advanced Settings${CL}"
  83. echo -e "${DGN}Using VM ID ${BGN}$VMID${CL}"
  84. echo -e "${DGN}Using VM Name ${BGN}$VM_NAME${CL}"
  85. echo -e "${YW}Allocate CPU cores, or Press [ENTER] for Default: 2 "
  86. read CORE_COUNT
  87. if [ -z $CORE_COUNT ]; then CORE_COUNT="2"; fi;
  88. echo -en "${DGN}Set Cores To ${BL}${CORE_COUNT}${CL}"
  89. echo -e " ${CM}${CL} \r"
  90. sleep 1
  91. clear
  92. header_info
  93. echo -e "${RD}Using Advanced Settings${CL}"
  94. echo -e "${DGN}Using VM ID ${BGN}$VMID${CL}"
  95. echo -e "${DGN}Using VM Name ${BGN}$VM_NAME${CL}"
  96. echo -e "${DGN}Using ${BGN}${CORE_COUNT}vCPU${CL}"
  97. echo -e "${YW}Allocate RAM in MiB, or Press [ENTER] for Default: 4096 "
  98. read RAM_SIZE
  99. if [ -z $RAM_SIZE ]; then RAM_SIZE="4096"; fi;
  100. echo -en "${DGN}Set RAM To ${BL}$RAM_SIZE${CL}"
  101. echo -e " ${CM}${CL} \n"
  102. sleep 1
  103. clear
  104. header_info
  105. echo -e "${RD}Using Advanced Settings${CL}"
  106. echo -e "${DGN}Using VM ID ${BGN}$VMID${CL}"
  107. echo -e "${DGN}Using VM Name ${BGN}$VM_NAME${CL}"
  108. echo -e "${DGN}Using ${BGN}${CORE_COUNT}vCPU${CL}"
  109. echo -e "${DGN}Using ${BGN}${RAM_SIZE}MiB${CL}"
  110. echo -e "${YW}Start VM when completed, or Press [ENTER] for Default: yes "
  111. read START_VM
  112. if [ -z $START_VM ]; then START_VM="yes";
  113. else
  114. START_VM="no"; fi;
  115. echo -en "${DGN}Starting VM when completed ${BL}$START_VM${CL}"
  116. echo -e " ${CM}${CL} \n"
  117. sleep 1
  118. clear
  119. header_info
  120. echo -e "${RD}Using Advanced Settings${CL}"
  121. echo -e "${DGN}Using VM ID ${BGN}$VMID${CL}"
  122. echo -e "${DGN}Using VM Name ${BGN}$VM_NAME${CL}"
  123. echo -e "${DGN}Using ${BGN}${CORE_COUNT}vCPU${CL}"
  124. echo -e "${DGN}Using ${BGN}${RAM_SIZE}MiB${CL}"
  125. echo -e "${DGN}Start VM when completed ${BGN}$START_VM${CL}"
  126. read -p "Are these settings correct(y/n)? " -n 1 -r
  127. echo
  128. if [[ ! $REPLY =~ ^[Yy]$ ]]
  129. then
  130. advanced_settings
  131. fi
  132. }
  133. function start_script() {
  134. echo -e "${YW}Type Advanced, or Press [ENTER] for Default Settings "
  135. read SETTINGS
  136. if [ -z $SETTINGS ]; then default_settings;
  137. else
  138. advanced_settings
  139. fi;
  140. }
  141. start_script
  142. set -o errexit
  143. set -o errtrace
  144. set -o nounset
  145. set -o pipefail
  146. shopt -s expand_aliases
  147. alias die='EXIT=$? LINE=$LINENO error_exit'
  148. trap die ERR
  149. trap cleanup EXIT
  150. function error_exit() {
  151. trap - ERR
  152. local DEFAULT='Unknown failure occured.'
  153. local REASON="\e[97m${1:-$DEFAULT}\e[39m"
  154. local FLAG="\e[91m[ERROR] \e[93m$EXIT@$LINE"
  155. msg "$FLAG $REASON"
  156. [ ! -z ${VMID-} ] && cleanup_vmid
  157. exit $EXIT
  158. }
  159. function warn() {
  160. local REASON="\e[97m$1\e[39m"
  161. local FLAG="\e[93m[WARNING]\e[39m"
  162. msg "$FLAG $REASON"
  163. }
  164. function info() {
  165. local REASON="$1"
  166. local FLAG="\e[36m[INFO]\e[39m"
  167. msg "$FLAG $REASON"
  168. }
  169. function msg() {
  170. local TEXT="$1"
  171. echo -e "$TEXT"
  172. }
  173. function cleanup_vmid() {
  174. if $(qm status $VMID &>/dev/null); then
  175. if [ "$(qm status $VMID | awk '{print $2}')" == "running" ]; then
  176. qm stop $VMID
  177. fi
  178. qm destroy $VMID
  179. fi
  180. }
  181. function cleanup() {
  182. popd >/dev/null
  183. rm -rf $TEMP_DIR
  184. }
  185. TEMP_DIR=$(mktemp -d)
  186. pushd $TEMP_DIR >/dev/null
  187. while read -r line; do
  188. TAG=$(echo $line | awk '{print $1}')
  189. TYPE=$(echo $line | awk '{printf "%-10s", $2}')
  190. FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}')
  191. ITEM=" Type: $TYPE Free: $FREE "
  192. OFFSET=2
  193. if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
  194. MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
  195. fi
  196. STORAGE_MENU+=( "$TAG" "$ITEM" "OFF" )
  197. done < <(pvesm status -content images | awk 'NR>1')
  198. if [ $((${#STORAGE_MENU[@]}/3)) -eq 0 ]; then
  199. warn "'Disk image' needs to be selected for at least one storage location."
  200. die "Unable to detect valid storage location."
  201. elif [ $((${#STORAGE_MENU[@]}/3)) -eq 1 ]; then
  202. STORAGE=${STORAGE_MENU[0]}
  203. else
  204. while [ -z "${STORAGE:+x}" ]; do
  205. STORAGE=$(whiptail --title "Storage Pools" --radiolist \
  206. "Which storage pool you would like to use for the container?\n\n" \
  207. 16 $(($MSG_MAX_LENGTH + 23)) 6 \
  208. "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit
  209. done
  210. fi
  211. msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
  212. msg_ok "Container ID is ${CL}${BL}$VMID${CL}."
  213. msg_info "Getting URL for Latest Home Assistant Disk Image"
  214. RELEASE_TYPE=qcow2
  215. URL=$(cat<<EOF | python3
  216. import requests
  217. url = "https://api.github.com/repos/home-assistant/operating-system/releases"
  218. r = requests.get(url).json()
  219. if "message" in r:
  220. exit()
  221. for release in r:
  222. if release["prerelease"]:
  223. continue
  224. for asset in release["assets"]:
  225. if asset["name"].find("$RELEASE_TYPE") != -1:
  226. image_url = asset["browser_download_url"]
  227. print(image_url)
  228. exit()
  229. EOF
  230. )
  231. if [ -z "$URL" ]; then
  232. die "Github has returned an error. A rate limit may have been applied to your connection."
  233. fi
  234. msg_ok "Found URL for Latest Home Assistant Disk Image"
  235. msg_ok "${CL}${BL}${URL}${CL}"
  236. wget -q --show-progress $URL
  237. echo -en "\e[1A\e[0K"
  238. FILE=$(basename $URL)
  239. msg_ok "Downloaded ${CL}${BL}${RELEASE_TYPE}${CL}${GN} Disk Image"
  240. msg_info "Extracting Disk Image"
  241. case $FILE in
  242. *"gz") gunzip -f $FILE ;;
  243. *"zip") gunzip -f -S .zip $FILE ;;
  244. *"xz") xz -d $FILE ;;
  245. *) die "Unable to handle file extension '${FILE##*.}'.";;
  246. esac
  247. STORAGE_TYPE=$(pvesm status -storage $STORAGE | awk 'NR>1 {print $2}')
  248. case $STORAGE_TYPE in
  249. btrfs|nfs|dir)
  250. DISK_EXT=".qcow2"
  251. DISK_REF="$VMID/"
  252. IMPORT_OPT="-format qcow2"
  253. esac
  254. for i in {0,1}; do
  255. disk="DISK$i"
  256. eval DISK${i}=vm-${VMID}-disk-${i}${DISK_EXT:-}
  257. eval DISK${i}_REF=${STORAGE}:${DISK_REF:-}${!disk}
  258. done
  259. msg_ok "Extracted Disk Image"
  260. msg_info "Creating HAOS VM"
  261. qm create $VMID -agent 1 -bios ovmf -cores $CORE_COUNT -memory $RAM_SIZE -name $VM_NAME -net0 virtio,bridge=vmbr0 \
  262. -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
  263. pvesm alloc $STORAGE $VMID $DISK0 128 1>&/dev/null
  264. qm importdisk $VMID ${FILE%.*} $STORAGE ${IMPORT_OPT:-} 1>&/dev/null
  265. qm set $VMID \
  266. -efidisk0 ${DISK0_REF},size=128K \
  267. -scsi0 ${DISK1_REF},size=32G >/dev/null
  268. qm set $VMID \
  269. -boot order=scsi0 >/dev/null
  270. set +o errtrace
  271. (
  272. msg_ok "Created HAOS VM ${CL}${BL}${VM_NAME}"
  273. msg_info "Adding Serial Port and Configuring Console"
  274. trap '
  275. warn "Unable to configure serial port. VM is still functional."
  276. if [ "$(qm config $VMID | sed -n ''/serial0/p'')" != "" ]; then
  277. qm set $VMID --delete serial0 >/dev/null
  278. fi
  279. exit
  280. ' ERR
  281. msg_ok "Added Serial Port and Configured Console"
  282. if [ "$(command -v kpartx)" = "" ]; then
  283. msg_info "Installing kpartx"
  284. apt-get update >/dev/null
  285. apt-get -qqy install kpartx &>/dev/null
  286. msg_ok "Installed kpartx"
  287. fi
  288. DISK1_PATH="$(pvesm path $DISK1_REF)"
  289. DISK1_PART1="$(kpartx -al $DISK1_PATH | awk 'NR==1 {print $1}')"
  290. DISK1_PART1_PATH="/dev/mapper/$DISK1_PART1"
  291. TEMP_MOUNT="${TEMP_DIR}/mnt"
  292. trap '
  293. findmnt $TEMP_MOUNT >/dev/null && umount $TEMP_MOUNT
  294. command -v kpartx >/dev/null && kpartx -d $DISK1_PATH
  295. ' EXIT
  296. kpartx -a $DISK1_PATH
  297. mkdir $TEMP_MOUNT
  298. mount $DISK1_PART1_PATH $TEMP_MOUNT
  299. sed -i 's/$/ console=ttyS0/' ${TEMP_MOUNT}/cmdline.txt
  300. qm set $VMID -serial0 socket >/dev/null
  301. )
  302. if [ "$START_VM" == "yes" ]; then
  303. msg_info "Starting Home Assistant OS VM"
  304. qm start $VMID
  305. msg_ok "Started Home Assistant OS VM"
  306. fi
  307. msg_ok "Completed Successfully!\n"