├── .gitignore ├── README.md ├── TODO.md ├── cleanup.sh └── create-image.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Cruft 2 | ._* 3 | 4 | # Ignore downloaded archives 5 | FETCH_* 6 | 7 | # Ignore temporary image files 8 | temporary.img 9 | 10 | # Ignore created Images 11 | FreeBSD-GCE-* 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FreeBSD Google Compute Engine Tools 2 | =================================== 3 | 4 | This script will create a FreeBSD image suitable for booting in the Google Compute Engine (GCE). 5 | 6 | ## Now With ZFS! 7 | Use the -z option to create a bootable Google Compue Engine image with root on ZFS! 8 | 9 | ## Creating Images 10 | Use the `create-image.sh` script to create images suitable for writing to GCE disks and booting into FreeBSD. Run it with the -h switch for usage information. Use `cleanup.sh` to clean up infrastructure created during the image creation process, which should only be needed if the script fails for some reason while the image is attached as a device and/or is mounted. 11 | 12 | ## Usage 13 | 14 | Usage: # ./create-image.sh [options] 15 | 16 | -h This help 17 | 18 | -c Compress the image. 19 | -k Path to public key for new user. Will be added to authorized_keys so you can log in. Required. 20 | -K Path to private key. Implies install public and private keys for new user. 21 | -p Password for new user. Default: passw0rd. 22 | -P Packages to install, in a space-separated string (you will need to use quotes). 23 | -r Release of FreeBSD to use. Default: 10.2-RELEASE 24 | -s Image size. Specify units as G or M. Default: 2G. 25 | -w Swap size. Specify in same units as Image size. Added to image size. Default none. 26 | -u Username for new user. Default: gceuser. 27 | -z Use ZFS filesystem for root. 28 | 29 | ### Keys 30 | * If you provide only a public key, it will be added to .ssh/authorized_keys for the new user so that you can log in via ssh. 31 | * If you provide both public and private keys, those keys will be installed under .ssh/ for the new user as well. 32 | 33 | ### Other Notes 34 | * The script will use the current directory as a working directory. 35 | * Note that you are not required to have the specified image size available as free space on your local hard drive. The truncate(1) command "does not cause space to be allocated" unless written to. This script only requires about 1GB to run. 36 | * If you use ZFS, the above no longer applies; you will need to have the storage available for your specificed image size. 37 | * If installing packages, the script will create a temporary `/etc/resolv.conf` file pointing to Google's public DNS so that the FreeBSD repositories can be accessed from within the chroot. 38 | 39 | ## Writing Images 40 | Use any running *nix machine in the GCE to write your image to a new GCE disk. If this is your first image, spin up an instance of debian, copy your new image to it, attach a new disk as big as your image target size, and write your image to the new disk. Detach the new disk, and then you can create a new instance using your new disk. 41 | 42 | **NOTE: The example commands below will _destroy data_ unless you are careful to specify the correct target device! 43 | Simply write the image directly to a blank GCE disk, like so: 44 | 45 | gunzip [Image File] > [GCE Blank Disk] 46 | 47 | Example for Debian or FreeBSD: 48 | 49 | sudo sh -c 'gunzip -c [TheImageFile.gz] > [Your device, e.g. /dev/daX]' 50 | 51 | ## Thanks 52 | * Thanks to vascokk for the original Gist posted here: https://gist.github.com/vascokk/b17f8c59446399db5c97. 53 | * Thanks to calomel.org for the zfs example posted here: https://calomel.org/zfs_freebsd_root_install.html. -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | ## Important 5 | * Allow for more ZFS options (in a config file) 6 | 7 | ## Nice 8 | * ~~Allow for package installation~~ 9 | * Allow for a script to run in the chroot for other customizations of the image. 10 | * Stop syslog from listening: echo 'syslogd_flags="-ss"' >> ${TMPMNTPNT}/etc/rc.conf 11 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sudo umount /dev/md0p2 3 | sudo mdconfig -d -u md0 4 | sudo rm temporary.img 5 | sudo rm -rf /tmp/freebsd-gce-tools-tmp.* 6 | -------------------------------------------------------------------------------- /create-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Stop for errors 4 | set -e 5 | 6 | # Usage Message 7 | usage() { 8 | echo " 9 | Usage: # ${0} [options] 10 | 11 | -h This help 12 | 13 | -c Compress the image. 14 | -k Path to public key for new user. Will be added to authorized_keys so you can log in. Required. 15 | -K Path to private key. Implies install public and private keys for new user. 16 | -p Password for new user. Default: passw0rd. 17 | -P Packages to install, in a space-separated string (you will need to use quotes). 18 | -r Release of FreeBSD to use. Default: 10.2-RELEASE 19 | -s Image size. Specify units as G or M. Default: 2G. 20 | -w Swap size. Specify in same units as Image size. Added to image size. Default none. 21 | -u Username for new user. Default: gceuser. 22 | -z Use ZFS filesystem for root. 23 | " 24 | } 25 | 26 | # Show usage if no parameters 27 | if [ -z $1 ]; then 28 | usage 29 | exit 0 30 | fi 31 | 32 | # Defaults 33 | VERBOSE='' 34 | COMPRESS='' 35 | IMAGESIZE='2G' 36 | SWAPSIZE='' 37 | DEVICEID='' 38 | RELEASE='10.2-RELEASE' 39 | RELEASEDIR='' 40 | TMPMNTPNT='' 41 | TMPCACHE='' 42 | TMPMNTPREFIX='freebsd-gce-tools-tmp' 43 | NEWUSER='gceuser' 44 | NEWPASS='passw0rd' 45 | PUBKEYFILE='' 46 | PRIKEYFILE='' 47 | USEZFS='' 48 | FILETYPE='UFS' 49 | PACKAGES='' 50 | 51 | # Switches 52 | while getopts ":chk:K:p:P:r:s:w:u:vz" opt; do 53 | case $opt in 54 | c) 55 | COMPRESS='YES' 56 | ;; 57 | h) 58 | usage 59 | exit 0 60 | ;; 61 | k) 62 | PUBKEYFILE="${OPTARG}" 63 | ;; 64 | K) 65 | PRIKEYFILE="${OPTARG}" 66 | ;; 67 | p) 68 | NEWPASS="${OPTARG}" 69 | ;; 70 | P) 71 | PACKAGES="${OPTARG}" 72 | ;; 73 | r) 74 | RELEASE="${OPTARG}" 75 | ;; 76 | s) 77 | IMAGESIZE="${OPTARG}" 78 | ;; 79 | w) 80 | SWAPSIZE="${OPTARG}" 81 | ;; 82 | u) 83 | NEWUSER="${OPTARG}" 84 | ;; 85 | v) 86 | VERBOSE="YES" 87 | echo "Verbose output enabled." 88 | ;; 89 | z) 90 | USEZFS='YES' 91 | FILETYPE='ZFS' 92 | ;; 93 | \?) 94 | echo "Invalid option: -${OPTARG}" >&2 95 | exit 1 96 | ;; 97 | :) 98 | echo "Option -${OPTARG} requires an argument." >&2 99 | exit 1 100 | ;; 101 | esac 102 | done 103 | shift $((OPTIND-1)) 104 | 105 | # Infrastructure Checks 106 | echo " " 107 | if [ -n "${PRIKEYFILE}" ] && [ -z "${PUBKEYFILE}" ]; then 108 | echo "If you provide a private key file, a public key file is also required." 109 | usage 110 | exit 1 111 | fi 112 | if [ -z "${PUBKEYFILE}" ]; then 113 | echo "A public key file is required. You will need this key to log in to your instance when you launch it." 114 | usage 115 | exit 1 116 | fi 117 | if [ ! -f "${PUBKEYFILE}" ]; then 118 | echo "Cannot read public key file: ${PUBKEYFILE}" 119 | usage 120 | exit 1 121 | fi 122 | if [ -z "${PUBKEYFILE}" ] && [ ! -f "${PRIKEYFILE}" ]; then 123 | echo "Cannot read private key file: ${PRIKEYFILE}" 124 | usage 125 | exit 1 126 | fi 127 | if [ -z "${NEWUSER}" ]; then 128 | echo "New username for the image cannot be empty." 129 | usage 130 | exit 1 131 | fi 132 | if [ -z "${NEWPASS}" ]; then 133 | echo "New password for the image cannot be empty." 134 | usage 135 | exit 1 136 | fi 137 | 138 | # Check for root credentials 139 | if [ `whoami` != "root" ]; then 140 | echo "Execute as root only!" 141 | exit 1 142 | fi 143 | 144 | [ ${VERBOSE} ] && echo "Started at `date date '+%Y-%m-%d %r'`" 145 | 146 | # Size Setup 147 | if [ -n "${SWAPSIZE}" ]; then 148 | IMAGEUNITS=$( echo "${IMAGESIZE}" | sed 's/[0-9.]//g' ) 149 | SWAPUNITS=$( echo "${SWAPSIZE}" | sed 's/[0-9.]//g' ) 150 | if [ "$IMAGEUNITS" != "$SWAPUNITS" ]; then 151 | echo "Image size and swap size units must match, e.g. 10G, 2G."; 152 | exit 1 153 | fi 154 | IMAGENUM=$( echo "${IMAGESIZE}" | sed 's/[a-zA-Z]//g' ) 155 | #echo "Image: ${IMAGENUM}" 156 | SWAPNUM=$( echo "${SWAPSIZE}" | sed 's/[a-zA-Z]//g' ) 157 | #echo "Swap: ${SWAPNUM}" 158 | TOTALSIZE=$(( ${IMAGENUM} + ${SWAPNUM} ))"${IMAGEUNITS}" 159 | echo "${IMAGESIZE} Image + ${SWAPSIZE} Swap = ${TOTALSIZE}"; 160 | else 161 | TOTALSIZE=$IMAGESIZE 162 | fi 163 | 164 | # Create The Image 165 | echo "Creating image..." 166 | truncate -s $TOTALSIZE temporary.img 167 | 168 | # Get a device ID for the image 169 | DEVICEID=$( mdconfig -a -t vnode -f temporary.img ) 170 | 171 | # Create a temporary mount point 172 | TMPMNTPNT=$( mktemp -d "/tmp/${TMPMNTPREFIX}.XXXXXXXX" ) 173 | 174 | if [ $USEZFS ]; then 175 | 176 | TMPCACHE=$( mktemp -d "/tmp/${TMPMNTPREFIX}.XXXXXXXX" ) 177 | ZLABELID=$( hexdump -n 4 -v -e '/1 "%02X"' /dev/urandom ) 178 | ZLABEL="zroot-${ZLABELID}-0" 179 | ZNAME="zroot-${ZLABELID}" 180 | 181 | [ ${VERBOSE} ] && echo "ZLABEL: ${ZLABEL}" 182 | [ ${VERBOSE} ] && echo "ZNAME: ${ZNAME}" 183 | 184 | echo "Creating ZFS boot root partitions..." 185 | gpart create -s gpt ${DEVICEID} 186 | gpart add -a 4k -s 512k -t freebsd-boot ${DEVICEID} 187 | gpart add -a 4k -t freebsd-zfs -l ${ZLABEL} ${DEVICEID} 188 | gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 1 ${DEVICEID} 189 | 190 | echo "Creating zroot pool..." 191 | gnop create -S 4096 /dev/gpt/${ZLABEL} 192 | zpool create -f -o altroot=${TMPMNTPNT} -o cachefile=${TMPCACHE}/zpool.cache ${ZNAME} /dev/gpt/${ZLABEL}.nop 193 | zpool export ${ZNAME} 194 | gnop destroy /dev/gpt/${ZLABEL}.nop 195 | zpool import -o altroot=${TMPMNTPNT} -o cachefile=${TMPCACHE}/zpool.cache ${ZNAME} 196 | mount | grep ${ZNAME} 197 | 198 | echo "Setting ZFS properties..." 199 | zpool set bootfs=${ZNAME} ${ZNAME} 200 | # zpool set listsnapshots=on ${ZNAME} 201 | # zpool set autoreplace=on ${ZNAME} 202 | # #zpool set autoexpand=on ${ZNAME} 203 | # zfs set checksum=fletcher4 ${ZNAME} 204 | # zfs set compression=lz4 ${ZNAME} 205 | # zfs set atime=off ${ZNAME} 206 | # zfs set copies=2 ${ZNAME} 207 | # #zfs set mountpoint=/ ${ZNAME} 208 | 209 | if [ -n "${SWAPSIZE}" ]; then 210 | echo "Adding swap space..." 211 | zfs create -V ${SWAPSIZE} ${ZNAME}/swap 212 | zfs set org.freebsd:swap=on ${ZNAME}/swap 213 | fi 214 | 215 | # Add the extra component to the path for root 216 | TMPMNTPNT="${TMPMNTPNT}/${ZNAME}" 217 | 218 | else 219 | 220 | # Partition the image 221 | echo "Adding partitions..." 222 | gpart create -s gpt /dev/${DEVICEID} 223 | echo -n "Adding boot: " 224 | gpart add -s 222 -t freebsd-boot -l boot0 ${DEVICEID} 225 | echo -n "Adding root: " 226 | gpart add -t freebsd-ufs -s ${IMAGESIZE} -l root0 ${DEVICEID} 227 | gpart bootcode -b /boot/pmbr -p /boot/gptboot -i 1 ${DEVICEID} 228 | if [ -n "${SWAPSIZE}" ]; then 229 | echo -n "Adding swap: " 230 | gpart add -t freebsd-swap -l swap0 ${DEVICEID} 231 | fi 232 | 233 | # Create and mount file system 234 | echo "Creating and mounting filesystem..." 235 | newfs -U /dev/${DEVICEID}p2 236 | mount /dev/${DEVICEID}p2 ${TMPMNTPNT} 237 | 238 | fi 239 | 240 | # Fetch FreeBSD into the image 241 | RELEASEDIR="FETCH_${RELEASE}" 242 | mkdir -p ${RELEASEDIR} 243 | if [ ! -f ${RELEASEDIR}/base.txz ]; then 244 | echo "Fetching base..." 245 | fetch -q -o ${RELEASEDIR}/base.txz http://ftp.freebsd.org/pub/FreeBSD/releases/amd64/${RELEASE}/base.txz < /dev/tty 246 | fi 247 | echo "Extracting base..." 248 | tar -C ${TMPMNTPNT} -xpf ${RELEASEDIR}/base.txz < /dev/tty 249 | if [ ! -f ${RELEASEDIR}/doc.txz ]; then 250 | echo "Fetching doc..." 251 | fetch -q -o ${RELEASEDIR}/doc.txz http://ftp.freebsd.org/pub/FreeBSD/releases/amd64/${RELEASE}/doc.txz < /dev/tty 252 | fi 253 | echo "Extracting doc..." 254 | tar -C ${TMPMNTPNT} -xpf ${RELEASEDIR}/doc.txz < /dev/tty 255 | if [ ! -f ${RELEASEDIR}/games.txz ]; then 256 | echo "Fetching games..." 257 | fetch -q -o ${RELEASEDIR}/games.txz http://ftp.freebsd.org/pub/FreeBSD/releases/amd64/${RELEASE}/games.txz < /dev/tty 258 | fi 259 | echo "Extracting games..." 260 | tar -C ${TMPMNTPNT} -xpf ${RELEASEDIR}/games.txz < /dev/tty 261 | if [ ! -f ${RELEASEDIR}/kernel.txz ]; then 262 | echo "Fetching kernel..." 263 | fetch -q -o ${RELEASEDIR}/kernel.txz http://ftp.freebsd.org/pub/FreeBSD/releases/amd64/${RELEASE}/kernel.txz < /dev/tty 264 | fi 265 | echo "Extracting kernel..." 266 | tar -C ${TMPMNTPNT} -xpf ${RELEASEDIR}/kernel.txz < /dev/tty 267 | if [ ! -f ${RELEASEDIR}/lib32.txz ]; then 268 | echo "Fetching lib32..." 269 | fetch -q -o ${RELEASEDIR}/lib32.txz http://ftp.freebsd.org/pub/FreeBSD/releases/amd64/${RELEASE}/lib32.txz < /dev/tty 270 | fi 271 | echo "Extracting lib32..." 272 | tar -C ${TMPMNTPNT} -xpf ${RELEASEDIR}/lib32.txz < /dev/tty 273 | 274 | # ZFS on Root Configuration 275 | if [ $USEZFS ]; then 276 | echo "Configuring for ZFS..." 277 | cp ${TMPCACHE}/zpool.cache ${TMPMNTPNT}/boot/zfs/zpool.cache 278 | ## /etc/rc.conf 279 | cat >> $TMPMNTPNT/etc/rc.conf << __EOF__ 280 | # ZFS On Root 281 | zfs_enable="YES" 282 | __EOF__ 283 | ## /boot/loader.conf 284 | cat >> $TMPMNTPNT/boot/loader.conf << __EOF__ 285 | # ZFS On Root 286 | vfs.root.mountfrom="zfs:${ZNAME}" 287 | zfs_load="YES" 288 | # ZFS On Root: use gpt ids instead of gptids or disks idents 289 | kern.geom.label.disk_ident.enable="0" 290 | kern.geom.label.gpt.enable="1" 291 | kern.geom.label.gptid.enable="0" 292 | __EOF__ 293 | fi 294 | 295 | # Configure new user 296 | echo "Creating ${NEWUSER} and home dir..." 297 | echo $NEWPASS | pw -V $TMPMNTPNT/etc useradd -h 0 -n $NEWUSER -c $NEWUSER -s /bin/csh -m 298 | pw -V $TMPMNTPNT/etc groupmod wheel -m $NEWUSER 299 | NEWUSER_UID=`pw -V $TMPMNTPNT/etc usershow $NEWUSER | cut -f 3 -d :` 300 | NEWUSER_GID=`pw -V $TMPMNTPNT/etc usershow $NEWUSER | cut -f 4 -d :` 301 | NEWUSER_HOME=$TMPMNTPNT/home/$NEWUSER 302 | mkdir -p $NEWUSER_HOME 303 | chown $NEWUSER_UID:$NEWUSER_GID $NEWUSER_HOME 304 | 305 | ## Set SSH authorized keys && optionally install key pair 306 | echo "Setting authorized ssh key for ${NEWUSER}..." 307 | mkdir $NEWUSER_HOME/.ssh 308 | chmod -R 700 $NEWUSER_HOME/.ssh 309 | cat "${PUBKEYFILE}" > $NEWUSER_HOME/.ssh/authorized_keys 310 | chmod 644 $NEWUSER_HOME/.ssh/authorized_keys 311 | if [ -n "${PRIKEYFILE}" ]; then 312 | echo "Installing ssh key pair for ${NEWUSER}..." 313 | cp "${PUBKEYFILE}" $NEWUSER_HOME/.ssh/ 314 | chmod 644 $NEWUSER_HOME/.ssh/$( basename "${PUBKEYFILE}" ) 315 | cp "${PRIKEYFILE}" $NEWUSER_HOME/.ssh/ 316 | chmod 600 $NEWUSER_HOME/.ssh/$( basename "${PRIKEYFILE}" ) 317 | fi 318 | chown -R $NEWUSER_UID:$NEWUSER_GID $NEWUSER_HOME/.ssh 319 | 320 | # Config File Changes 321 | echo "Configuring image for GCE..." 322 | 323 | ## Create a Local etc 324 | mkdir $TMPMNTPNT/usr/local/etc 325 | 326 | ## /etc/fstab 327 | if [ $USEZFS ]; then 328 | # touch the /etc/fstab else freebsd will not boot properly" 329 | touch $TMPMNTPNT/etc/fstab 330 | else 331 | cat >> $TMPMNTPNT/etc/fstab << __EOF__ 332 | /dev/da0p2 / ufs rw,noatime,suiddir 1 1 333 | __EOF__ 334 | if [ -n $SWAPSIZE ]; then 335 | cat >> $TMPMNTPNT/etc/fstab << __EOF__ 336 | /dev/da0p3 none swap sw 0 0 337 | __EOF__ 338 | fi 339 | fi 340 | 341 | ## /boot.config 342 | echo -Dh > $TMPMNTPNT/boot.config 343 | 344 | ### /boot/loader.conf 345 | cat >> $TMPMNTPNT/boot/loader.conf << __EOF__ 346 | # GCE Console 347 | console="comconsole" 348 | # No Boot Delay 349 | autoboot_delay="0" 350 | __EOF__ 351 | 352 | ## /etc/rc.conf 353 | cat >> $TMPMNTPNT/etc/rc.conf << __EOF__ 354 | console="comconsole" 355 | hostname="freebsd" 356 | ifconfig_vtnet0="DHCP" 357 | ntpd_enable="YES" 358 | ntpd_sync_on_start="YES" 359 | sshd_enable="YES" 360 | __EOF__ 361 | 362 | ## /etc/ssh/sshd_config 363 | /usr/bin/sed -Ei.original 's/^#UseDNS yes/UseDNS no/' $TMPMNTPNT/etc/ssh/sshd_config 364 | /usr/bin/sed -Ei '' 's/^#UsePAM yes/UsePAM no/' $TMPMNTPNT/etc/ssh/sshd_config 365 | 366 | ## /etc/ntp.conf > /usr/local/etc/ntp.conf 367 | cp $TMPMNTPNT/etc/ntp.conf $TMPMNTPNT/usr/local/etc/ntp.conf 368 | /usr/bin/sed -Ei.original 's/^server/#server/' $TMPMNTPNT/usr/local/etc/ntp.conf 369 | cat >> $TMPMNTPNT/etc/ntp.conf << __EOF__ 370 | # GCE NTP Server 371 | server 169.254.169.254 burst iburst 372 | __EOF__ 373 | 374 | ## /etc/dhclient.conf 375 | cat >> $TMPMNTPNT/etc/dhclient.conf << __EOF__ 376 | # GCE DHCP Client 377 | interface "vtnet0" { 378 | supersede subnet-mask 255.255.0.0; 379 | } 380 | __EOF__ 381 | 382 | ## /etc/rc.local 383 | cat > $TMPMNTPNT/etc/rc.local << __EOF__ 384 | # GCE MTU 385 | ifconfig vtnet0 mtu 1460 386 | __EOF__ 387 | 388 | ## Time Zone 389 | chroot $TMPMNTPNT /bin/sh -c 'ln -s /usr/share/zoneinfo/America/Vancouver /etc/localtime' 390 | 391 | ## Install packages 392 | if [ -n "$PACKAGES" ]; then 393 | echo "Installing packages..." 394 | chroot $TMPMNTPNT /bin/sh -c 'echo "nameserver 8.8.8.8" > /etc/resolv.conf' 395 | pkg -c $TMPMNTPNT install -y ${PACKAGES} 396 | chroot $TMPMNTPNT /bin/sh -c 'rm /etc/resolv.conf' 397 | fi 398 | 399 | # Clean up image infrastructure 400 | echo "Detaching image..." 401 | if [ $USEZFS ]; then 402 | zfs unmount ${ZNAME} 403 | zpool export ${ZNAME} 404 | else 405 | umount $TMPMNTPNT 406 | fi 407 | mdconfig -d -u ${DEVICEID} 408 | 409 | # Name/Compress the image 410 | mv temporary.img FreeBSD-GCE-${RELEASE}-${FILETYPE}.img 411 | if [ ${COMPRESS} ]; then 412 | echo "Compressing image..." 413 | gzip FreeBSD-GCE-${RELEASE}-${FILETYPE}.img 414 | shasum FreeBSD-GCE-${RELEASE}-${FILETYPE}.img.gz > FreeBSD-GCE-${RELEASE}-${FILETYPE}.img.gz.sha 415 | else 416 | shasum FreeBSD-GCE-${RELEASE}-${FILETYPE}.img > FreeBSD-GCE-${RELEASE}-${FILETYPE}.img.sha 417 | fi 418 | 419 | [ ${VERBOSE} ] && echo "Started at `date date '+%Y-%m-%d %r'`" 420 | 421 | echo "Done." 422 | --------------------------------------------------------------------------------