openwrtbuilder 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. #!/usr/bin/env bash
  2. #
  3. # Build and flash/upgrade OpenWRT devices
  4. #
  5. # Apache 2.0 License
  6. printHelpAndExit() {
  7. debug "${FUNCNAME[0]}"
  8. cat <<-'EOF'
  9. USAGE:
  10. openwrtbuilder [[OPTION] [VALUE]]...
  11. Run and deploy OpenWRT imagebuilder.
  12. OPTIONS
  13. --profile, -p PROFILE
  14. --info, -i PROFILE
  15. --list-profiles, -l
  16. --release, -r RELEASE
  17. --builddir, -b PATH
  18. --ssh-upgrade HOST
  19. Example: root@192.168.1.1
  20. --ssh-backup SSH_PATH
  21. (For testing, enabled by default for --ssh-upgrade)
  22. --flash, -f DEVICE
  23. Example: /dev/sdX
  24. --reset
  25. CLeanup all source and output files
  26. --debug, -d
  27. --help, -h
  28. EOF
  29. # Exit using passed exit code
  30. [[ -z $1 ]] && exit 0 || exit "$1"
  31. }
  32. input() {
  33. debug "${FUNCNAME[0]}"
  34. unset RESET
  35. if _input=$(getopt -o +v:p:i:lb:f:dh -l release:,profile:,info:,list-profiles,builddir:,ssh-upgrade:,ssh-backup:,flash:,reset,debug,help -- "$@"); then
  36. eval set -- "$_input"
  37. while true; do
  38. case "$1" in
  39. --release|-r)
  40. shift && RELEASE="$1"
  41. ;;
  42. --profile|-p)
  43. shift && PROFILE="$1"
  44. ;;
  45. --info|-i)
  46. shift && profileInfo "$1" && exit $?
  47. ;;
  48. --list-profile|-l)
  49. listProfiles && exit $?
  50. ;;
  51. --builddir|-b)
  52. shift && BUILDDIR="$1"
  53. ;;
  54. --ssh-upgrade)
  55. shift && SSH_UPGRADE_PATH="$1"
  56. ;;
  57. --ssh-backup)
  58. shift && SSH_BACKUP_PATH="$1"
  59. ;;
  60. --flash|-f)
  61. shift && FLASH_DEV="$1"
  62. ;;
  63. --reset)
  64. RESET=true
  65. ;;
  66. --debug|-d)
  67. echo "Debugging on"
  68. DEBUG=true
  69. ;;
  70. --help|-h)
  71. printHelpAndExit 0
  72. ;;
  73. --)
  74. shift
  75. break
  76. ;;
  77. esac
  78. shift
  79. done
  80. else
  81. echo "Incorrect options provided"
  82. printHelpAndExit 1
  83. fi
  84. }
  85. profiles() {
  86. debug "${FUNCNAME[0]}"
  87. # Additional packages to install for all profiles
  88. default_packages="\
  89. luci \
  90. luci-ssl \
  91. nano \
  92. htop \
  93. tcpdump \
  94. diffutils \
  95. tar \
  96. iperf \
  97. rsync " # Leave trailing whitespace
  98. # Set the default release
  99. [[ -z $RELEASE ]] && RELEASE="21.02.1"
  100. # Use these tools to add and parse profiles
  101. declare -ag PROFILES
  102. add_profile() {
  103. declare -Ag "$1"
  104. PROFILES+=("$1")
  105. }
  106. add_profile archer
  107. archer['profile']="tplink_archer-c7-v2"
  108. archer['target']="ath79/generic"
  109. archer['filesystem']="squashfs"
  110. # shellcheck disable=SC2034 # Indirect
  111. archer['packages']="\
  112. $default_packages \
  113. -dnsmasq \
  114. -odhcpd \
  115. -iptables \
  116. -ath10k-firmware-qca988x-ct \
  117. ath10k-firmware-qca988x-ct-full-htt"
  118. add_profile linksys
  119. linksys['profile']="linksys_ea8300"
  120. linksys['target']="ipq40xx/generic"
  121. linksys['filesystem']="squashfs"
  122. # shellcheck disable=SC2034 # Indirect
  123. linksys['packages']="\
  124. $default_packages \
  125. -dnsmasq \
  126. -odhcpd \
  127. -iptables"
  128. add_profile rpi4
  129. rpi4['profile']="rpi-4"
  130. rpi4['target']="bcm27xx/bcm2711"
  131. rpi4['filesystem']="ext4"
  132. # shellcheck disable=SC2034 # Indirect
  133. rpi4['packages']="\
  134. $default_packages \
  135. kmod-usb-net-asix-ax88179 \
  136. kmod-usb-net-rtl8152 \
  137. luci-app-upnp \
  138. luci-app-wireguard \
  139. luci-app-vpn-policy-routing \
  140. -dnsmasq \
  141. dnsmasq-full \
  142. luci-app-ddns \
  143. luci-app-sqm"
  144. add_profile r2s
  145. r2s['profile']="friendlyarm_nanopi-r2s"
  146. r2s['target']="rockchip/armv8"
  147. r2s['filesystem']="ext4"
  148. # shellcheck disable=SC2034 # Indirect
  149. r2s['packages']="\
  150. $default_packages \
  151. luci-app-upnp \
  152. luci-app-wireguard \
  153. luci-app-vpn-policy-routing \
  154. -dnsmasq \
  155. dnsmasq-full \
  156. luci-app-ddns \
  157. luci-app-sqm \
  158. luci-app-statistics \
  159. collectd-mod-sensors \
  160. collectd-mod-thermal \
  161. collectd-mod-conntrack \
  162. smcroute \
  163. curl \
  164. ethtool"
  165. add_profile r4s
  166. r4s['release']="snapshot"
  167. r4s['profile']="friendlyarm_nanopi-r4s"
  168. r4s['target']="rockchip/armv8"
  169. r4s['filesystem']="ext4"
  170. # shellcheck disable=SC2034 # Indirect
  171. r4s['packages']="\
  172. $default_packages \
  173. luci-app-upnp \
  174. luci-app-wireguard \
  175. luci-app-vpn-policy-routing \
  176. -dnsmasq \
  177. dnsmasq-full \
  178. luci-app-ddns \
  179. luci-app-sqm \
  180. luci-app-statistics \
  181. collectd-mod-sensors \
  182. collectd-mod-thermal \
  183. collectd-mod-conntrack \
  184. smcroute \
  185. curl \
  186. ethtool"
  187. for PNAME in "${PROFILES[@]}"; do
  188. declare -n ARR="$PNAME"
  189. local _out_prefix
  190. [[ ! -v ARR['release'] ]] && ARR['release']="$RELEASE"
  191. ARR['source_archive']="$BUILDDIR/sources/${ARR[profile]}-${ARR[release]}.tar.xz"
  192. ARR['source_dir']="${ARR[source_archive]%.tar.xz}"
  193. ARR['out_bin_dir']="$BUILDDIR/bin/${ARR[profile]}-${ARR[release]}"
  194. #_patches_dir="$BUILDDIR/patches/"
  195. #_files_dir="$BUILDDIR/files/"
  196. if [[ "${ARR[release]}" == "snapshot" ]]; then
  197. _out_prefix="${ARR[out_bin_dir]}/openwrt-${ARR[target]//\//-}-${ARR[profile]}"
  198. else
  199. _out_prefix="${ARR[out_bin_dir]}/openwrt-${ARR[release]}-${ARR[target]//\//-}-${ARR[profile]}"
  200. fi
  201. ARR['factory_img']="$_out_prefix-${ARR[filesystem]}-factory.img"
  202. ARR['factory_img_gz']="${ARR[factory_img]}.gz"
  203. ARR['sysupgrade_img']="$_out_prefix-${ARR[filesystem]}-sysupgrade.img"
  204. ARR['sysupgrade_img_gz']="${ARR[sysupgrade_img]}.gz"
  205. ARR['sysupgrade_bin']="$_out_prefix-${ARR[filesystem]}-sysupgrade.bin"
  206. ARR['sysupgrade_bin_fname']="${ARR[sysupgrade_bin]##*/}"
  207. ARR['sysupgrade_bin_gz']="${ARR[sysupgrade_bin]}.gz"
  208. ARR['sysupgrade_bin_gz_fname']="${ARR[sysupgrade_bin_gz]##*/}"
  209. done
  210. }
  211. listProfiles() {
  212. debug "${FUNCNAME[0]}"
  213. [[ ! -v PROFILES ]] && profiles
  214. echo "Available profiles:"
  215. for PNAME in "${PROFILES[@]}"; do
  216. declare -n ARR2="$PNAME"
  217. echo "$PNAME: ${ARR2[profile]}"
  218. done
  219. }
  220. profileInfo() {
  221. debug "${FUNCNAME[0]}"
  222. local _profile
  223. _profile="$1"
  224. [[ ! -v PROFILES ]] && profiles
  225. declare -n ARR3="$_profile"
  226. for i in "${!ARR3[@]}"; do
  227. echo "$i: ${ARR3[i]}"
  228. done
  229. }
  230. prerequisites() {
  231. debug "${FUNCNAME[0]}"
  232. local -a _pkg_list
  233. local _pkg_cmd
  234. source /etc/os-release
  235. if [[ "$ID" == "fedora" ]]; then
  236. _pkg_list=(\
  237. "@c-development" \
  238. "@development-tools" \
  239. "@development-libs" \
  240. "perl-FindBin" \
  241. "zlib-static" \
  242. "elfutils-libelf-devel" \
  243. "gawk" \
  244. "unzip" \
  245. "file" \
  246. "wget" \
  247. "python3" \
  248. "python2" \
  249. "axel" \
  250. )
  251. _pkg_cmd="dnf"
  252. elif [[ "$ID" =~ ^(debian|ubuntu)$ ]]; then
  253. _pkg_list=(\
  254. "build-essential" \
  255. "libncurses5-dev" \
  256. "libncursesw5-dev" \
  257. "zlib1g-dev" \
  258. "gawk" \
  259. "git" \
  260. "gettext" \
  261. "libssl-dev" \
  262. "xsltproc" \
  263. "wget" \
  264. "unzip" \
  265. "python" \
  266. "axel" \
  267. )
  268. _pkg_cmd="apt-get"
  269. fi
  270. echo "Installing dependencies"
  271. debug "sudo $_pkg_cmd -y install ${_pkg_list[*]}"
  272. if ! sudo "$_pkg_cmd" -y install "${_pkg_list[@]}" > /dev/null 2>&1; then
  273. echo "Warning: Problem installing prerequisites"
  274. return 1
  275. fi
  276. }
  277. getImageBuilder() {
  278. debug "${FUNCNAME[0]}"
  279. declare -n ARR4="$PROFILE"
  280. local _url _filename
  281. if [[ "${ARR4[release]}" == "snapshot" ]]; then
  282. _filename="openwrt-imagebuilder-${ARR4[target]//\//-}.Linux-x86_64.tar.xz"
  283. _url="https://downloads.openwrt.org/snapshots/targets/${ARR4[target]}/$_filename"
  284. if [[ -f "${ARR4[source_archive]}" ]]; then
  285. if askOk "Update ImageBuilder snapshot?"; then
  286. rm -f "${ARR4[source_archive]}"
  287. else
  288. return 0
  289. fi
  290. fi
  291. else
  292. _filename="openwrt-imagebuilder-${ARR4[release]}-${ARR4[target]//\//-}.Linux-x86_64.tar.xz"
  293. _url="https://downloads.openwrt.org/releases/${ARR4[release]}/targets/${ARR4[target]}/$_filename"
  294. [[ -f "${ARR4[source_archive]}" ]] && return 0 # Reuse existing ImageBuilders
  295. fi
  296. # Make sources directory if it does not exist
  297. [[ ! -d "$BUILDDIR/sources" ]] && mkdir -p "$BUILDDIR/sources"
  298. echo "Downloading imagebuilder archive"
  299. debug "axel -o ${ARR4[source_archive]} $_url"
  300. if ! axel -o "${ARR4[source_archive]}" "$_url" > /dev/null 2>&1; then
  301. echo "Could not download imagebuilder archive"
  302. exit 1
  303. fi
  304. if [[ ! -f "${ARR4[source_archive]}" ]]; then
  305. echo "Archive missing"
  306. exit 1
  307. fi
  308. echo "Extracting image archive"
  309. debug "tar -xf ${ARR4[source_archive]} -C ${ARR4[source_dir]} --strip-components 1"
  310. if ! tar -xf "${ARR4[source_archive]}" -C "${ARR4[source_dir]}" --strip-components 1; then
  311. echo "Extraction failed"
  312. exit 1
  313. fi
  314. }
  315. makeImage() {
  316. debug "${FUNCNAME[0]}"
  317. declare -n ARR5="$PROFILE"
  318. # Reuse the existing output
  319. if [[ -d "${ARR5[out_bin_dir]}" ]]; then
  320. if askOk "${ARR5[out_bin_dir]} exists. Rebuild?"; then
  321. rm -rf "${ARR5[out_bin_dir]}"
  322. else
  323. return 0
  324. fi
  325. fi
  326. [[ ! -d "${ARR5[out_bin_dir]}" ]] && mkdir -p "${ARR5[out_bin_dir]}"
  327. # build image
  328. debug "make -j4 image BIN_DIR=${ARR5[out_bin_dir]} PROFILE=${ARR5[profile]} PACKAGES=${ARR5[packages]} FILES=$FILESDIR --directory=${ARR5[source_dir]} > make.log"
  329. if ! make image BIN_DIR="${ARR5[out_bin_dir]}" PROFILE="${ARR5[profile]}" PACKAGES="${ARR5[packages]}" FILES="$FILESDIR" --directory="${ARR5[source_dir]}" > make.log; then
  330. echo "Make image failed!"
  331. exit 1
  332. fi
  333. }
  334. sshBackup() {
  335. debug "${FUNCNAME[0]}"
  336. local _date _hostname _backup_fname
  337. [[ -d "$FILESDIR" ]] || mkdir -p "$FILESDIR"
  338. printf -v _date '%(%Y-%m-%d-%H-%M-%S)T'
  339. _hostname=$(ssh -qt "$SSH_BACKUP_PATH" echo -n \$HOSTNAME)
  340. _backup_fname="backup-$_hostname-$_date.tar.gz"
  341. # Make backup archive on remote
  342. debug "ssh -t $SSH_BACKUP_PATH sysupgrade -b /tmp/$_backup_fname"
  343. if ! ssh -t "$SSH_BACKUP_PATH" "sysupgrade -b /tmp/$_backup_fname"; then
  344. echo "SSH backup failed"
  345. exit 1
  346. fi
  347. # Move backup archive locally
  348. debug "rsync -avz --remove-source-files $SSH_BACKUP_PATH:/tmp/$_backup_fname $BUILDDIR/"
  349. if ! rsync -avz --remove-source-files "$SSH_BACKUP_PATH":"/tmp/$_backup_fname" "$BUILDDIR/"; then
  350. echo "Could not copy SSH backup"
  351. exit 1
  352. fi
  353. # Extract backup archive
  354. debug "tar -C $FILESDIR -xzf $BUILDDIR/$_backup_fname"
  355. if ! tar -C "$FILESDIR" -xzf "$BUILDDIR/$_backup_fname"; then
  356. echo "Could not extract SSH backup"
  357. exit 1
  358. fi
  359. rm "$BUILDDIR/$_backup_fname"
  360. }
  361. flashImage() {
  362. debug "${FUNCNAME[0]}"
  363. declare -n ARR6="$PROFILE"
  364. local _umount
  365. if [[ ! -e "$FLASH_DEV" ]]; then
  366. echo "The device specified by --flash could not be found"
  367. exit 1
  368. fi
  369. # TODO Roughly chooses the correct image
  370. if [[ -f "${ARR6[factory_img_gz]}" ]]; then
  371. img_gz="${ARR6[factory_img_gz]}"
  372. img="${ARR6[factory_img]}"
  373. elif [[ -f "${ARR6[sysupgrade_img_gz]}" ]]; then
  374. img_gz="${ARR6[sysupgrade_img_gz]}"
  375. img="${ARR6[sysupgrade_img]}"
  376. else
  377. return 1
  378. fi
  379. debug "$img_gz $img"
  380. debug "gunzip -qfk $img_gz"
  381. gunzip -qfk "$img_gz"
  382. echo "Unmounting target device $FLASH_DEV partitions"
  383. _umount=( "$FLASH_DEV"?* )
  384. debug "umount ${_umount[*]}"
  385. sudo umount "${_umount[@]}"
  386. debug "sudo dd if=\"$img\" of=\"$FLASH_DEV\" bs=2M conv=fsync"
  387. if sudo dd if="$img" of="$FLASH_DEV" bs=2M conv=fsync; then
  388. sync
  389. echo "Image flashed sucessfully!"
  390. else
  391. echo "dd failed!"
  392. exit 1
  393. fi
  394. }
  395. sshUpgrade() {
  396. debug "${FUNCNAME[0]}"
  397. declare -n ARR7="$PROFILE"
  398. echo "Copying \"${ARR7[sysupgrade_bin_gz]}\" to $SSH_UPGRADE_PATH/tmp/"
  399. debug "scp \"${ARR7[sysupgrade_bin_gz]}\" \"$SSH_UPGRADE_PATH\":\"/tmp/${ARR7[sysupgrade_bin_gz_fname]}\""
  400. # shellcheck disable=SC2140
  401. if ! scp "${ARR7[sysupgrade_bin_gz]}" "$SSH_UPGRADE_PATH":"/tmp/${ARR7[sysupgrade_bin_gz_fname]}"; then
  402. echo "Could not access the --ssh-upgrade PATH"
  403. exit 1
  404. fi
  405. echo "Executing remote sysupgrade"
  406. debug "ssh \"$SSH_UPGRADE_PATH\" \"sysupgrade -F /tmp/${ARR7[sysupgrade_bin_gz_fname]}\""
  407. # shellcheck disable=SC2029
  408. ssh "$SSH_UPGRADE_PATH" "sysupgrade -F /tmp/${ARR7[sysupgrade_bin_gz_fname]}"
  409. }
  410. debug() { "$DEBUG" && echo "Running: " "$@" ; }
  411. askOk() {
  412. local _response
  413. read -r -p "$* [y/N]" _response
  414. _response=${_response,,}
  415. [[ ! "$_response" =~ ^(yes|y)$ ]] && return 1
  416. return 0
  417. }
  418. reset() {
  419. askOk "Remove $FILESDIR $BUILDDIR/sources $BUILDDIR/bin?" || exit $?
  420. debug "rm -rf $FILESDIR $BUILDDIR/sources $BUILDDIR/bin"
  421. rm -rf "$FILESDIR" "${BUILDDIR:?}/sources" "${BUILDDIR:?}/bin"
  422. }
  423. main() {
  424. : "${DEBUG:=false}" # Set to true to enable debugging by default
  425. scriptdir=$(dirname "$0")
  426. # scriptdir="$PWD"
  427. : "${BUILDDIR:=$scriptdir}"
  428. : "${FILESDIR:=$BUILDDIR/files}"
  429. input "$@"
  430. profiles
  431. [[ -v RESET ]] && reset
  432. prerequisites
  433. getImageBuilder
  434. #copyFiles
  435. [[ -v SSH_BACKUP_PATH ]] && sshBackup
  436. if makeImage; then
  437. [[ -v SSH_UPGRADE_PATH ]] && sshUpgrade
  438. [[ -v FLASH_DEV ]] && flashImage
  439. fi
  440. }
  441. main "$@"
  442. exit $?