|
#!/bin/bash |
|
|
|
# the .pem file is a key created by following: |
|
# kn="id_rsa-${USER}-azure" |
|
# kf="$HOME/.ssh/$kn" |
|
# ssh-keygen -f "$kf" -C "$kn" |
|
# openssl req -key $kf -new -x509 -days 999 > $kf.pem |
|
# chmod 600 $kf.pem |
|
# openssl x509 -outform der -in $kf.pem -out $kf.cer |
|
|
|
# pt_optargs created from: |
|
# $ azure vm create --help | |
|
# sed -n 's/.*\(-.\), \(--[a-z-]*\) \(<[a-z|-]*>\).*/\1 \2/p' | |
|
# sort -k 2 |
|
pt_optargs=( |
|
-a --affinity-group |
|
-A --availability-set |
|
-u --blob-url |
|
-d --custom-data |
|
-l --location |
|
-t --ssh-cert |
|
-b --subnet-names |
|
-s --subscription |
|
-w --virtual-network-name |
|
-n --vm-name |
|
-z --vm-size |
|
) |
|
|
|
TEMP_D="" |
|
INVALIDATE_CACHE=${AZURE_INVALIDATE_CACHE:-0} |
|
CANONICAL_ID="b39f27a8b8c64d52b05eac6a62ebad85" |
|
VERBOSITY=0 |
|
REL_VER_MAP=$(ubuntu-distro-info --supported --fullname 2>/dev/null | |
|
sed -e 's,Ubuntu ,,; s, LTS,,; s,",,g' -e '/Lucid/d' | |
|
awk '{print $2 ":" $1}' | tr '[A-Z]' '[a-z]' | tr '\n' ' ') |
|
|
|
[ -n "$REL_VER_MAP" ] || |
|
REL_VER_MAP="precise:12.04 trusty:14.04 utopic:14.10 vivid:15.04" |
|
|
|
SSTREAM_PAIRS=( |
|
"released|http://cloud-images.ubuntu.com/releases/streams/v1/com.ubuntu.cloud:released:azure.sjson" |
|
"daily|http://cloud-images.ubuntu.com/daily/streams/v1/com.ubuntu.cloud:daily:azure.sjson" |
|
) |
|
|
|
# defaults |
|
user=$(id -un) |
|
key_pem="$HOME/.ssh/id_rsa-${user}-azure@$(hostname).pem" |
|
image="trusty-released" |
|
flavor="Basic_A0" |
|
location="us-east" |
|
password="${AZURE_PASSWORD}" |
|
name_prefix="${user}" |
|
|
|
error() { echo "$@" 1>&2; } |
|
fail() { [ $# -eq 0 ] || error "$@"; exit 1; } |
|
debug() { |
|
local level=${1}; shift; |
|
[ "${level}" -gt "${VERBOSITY}" ] && return |
|
error "${@}" |
|
} |
|
cache_valid() { |
|
local file="$1" date="$2" tf="/tmp/ts.$$" ret="" |
|
[ -n "$file" -a -e "$file" ] || return 1 |
|
touch --date "${date}" "$tf" |
|
[ "$file" -nt "$tf" ] |
|
ret=$? |
|
rm -f "$tf" |
|
return $ret |
|
} |
|
|
|
cleanup() { |
|
[ -n "${TEMP_D}" ] || return |
|
[ ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}" |
|
} |
|
|
|
get_temp_d() { |
|
if [ -z "${TEMP_D}" ]; then |
|
TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") || return |
|
trap cleanup EXIT |
|
fi |
|
_RET=${TEMP_D} |
|
} |
|
|
|
render_userdata_tmpl() { |
|
local udata="$1" tmpd="" now="" |
|
_RET="$udata" |
|
[ -n "$udata" ] || return 0 |
|
grep -q "LAUNCH_SECONDS" "$udata" || return 0 |
|
get_temp_d && tmpd="$_RET" || return 1 |
|
now=$(date "+%s") || return 1 |
|
_RET="$tmpd/ud-rendered" |
|
sed "s/LAUNCH_SECONDS/$now/" "$udata" > "$_RET" |
|
} |
|
|
|
get_images_sstream() { |
|
local cache_d="$1" out_f="$2" pair="" name="" url="" tmpf="" |
|
local keyring="/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg" |
|
local tmpf="$out_f.$$" |
|
for pair in "${SSTREAM_PAIRS[@]}"; do |
|
name="${pair%%|*}" |
|
url="${pair#*|}" |
|
sstream-mirror "--keyring=$keyring" "$url" "$cache_d/$name" && |
|
sstream-query "--keyring=$keyring" '--output-format=%(id)s' \ |
|
"$cache_d/$name/streams/v1/"*.sjson "crsn=East US" || |
|
{ error "failed query $pair"; rm -f "$tmp_f"; return 1; } |
|
done > "$tmpf" |
|
mv "$tmpf" "$out_f" |
|
} |
|
|
|
parse_image_list() { |
|
# this scrapes a list of azure formated images |
|
# and returns a space delimited list of label:image_id |
|
local fname="$1" |
|
local rel="" n="" tok="" label="" |
|
for tok in $REL_VER_MAP; do |
|
rel=${tok%:*} |
|
n=${tok#*:} |
|
for label in "" "daily"; do |
|
if [ "$label" = "daily" ]; then |
|
grepstr="DAILY.*$n" |
|
alias="${rel}-daily" |
|
else |
|
grepstr="Ubuntu-$n" |
|
alias="${rel}" |
|
fi |
|
img=$(grep "$grepstr" "$fname" | sort | tail -n 1 | awk '{print $1}') |
|
[ -n "$img" ] || continue |
|
if [ "$label" = "daily" ]; then |
|
add="${rel}-daily:$img ${rel}:$img" |
|
else |
|
add="${rel}-released:$img" |
|
fi |
|
imgs="${imgs:+${imgs} }$add" |
|
done |
|
done |
|
_RET="${imgs}" |
|
} |
|
|
|
get_images() { |
|
local cache_d="$HOME/.cache/${0##*/}" |
|
local flist="$cache_d/images.list" method="" |
|
[ -d "$cache_d" ] || mkdir -p "${cache_d}" || |
|
{ error "failed make ${cache_dir}"; return 1; } |
|
if [ "${INVALIDATE_CACHE:-0}" != "0" ] || |
|
! cache_valid "$flist" "now - $((60*60*24)) seconds"; then |
|
method="get_images_vmlist" |
|
command -v sstream-sync >/dev/null 2>&1 && method="get_images_sstream" |
|
error "updating image-cache in $flist via $method" |
|
"$method" "$cache_d" "$flist" || |
|
{ error "updating cache via $method failed"; return 1; } |
|
fi |
|
|
|
parse_image_list "$flist" |
|
IMAGES=$_RET |
|
return |
|
} |
|
|
|
get_images_vmlist() { |
|
local cache_d="$1" out_f="$2" tmpf="" imgs="" |
|
tmpf="$out_f.$$" |
|
azure vm image list > "$tmpf" || { rm -f "$tmpf" ; return 1; } |
|
sed -i -e '/^data:/!d' -e "/${CANONICAL_ID}__/!d" -e "s/^data:[ ]*//" "$tmpf" |
|
grep "${CANONICAL_ID}" "$tmpf" || |
|
{ error "id '$CANONICAL_ID' not found"; return 1; } |
|
mv "$tmpf" "$out_f" |
|
} |
|
|
|
# from azure vm location list |
|
LOCATIONS=( |
|
"ap-east:East Asia" |
|
"ap-southeast:Southeast Asia" |
|
"br-south:Brazil South" |
|
"eu-north:North Europe" |
|
"eu-west:West Europe" |
|
"ja-east:Japan East" |
|
"ja-west:Japan West" |
|
"us-central:Central US" |
|
"us-east-1:East US" |
|
"us-east-2:East US 2" |
|
"us-east:East US" |
|
"us-northcentral:North Central US" |
|
"us-southcentral:South Central US" |
|
"us-west:West US" |
|
) |
|
|
|
declare -A FLAVOR_ALIASES FLAVOR_DATA |
|
|
|
FLAVOR_ALIASES=( |
|
[A0]=ExtraSmall [A1]=Small [A2]=Medium [A3]=Large [A4]=ExtraLarge |
|
) |
|
|
|
## These are cores/ram/disk/price |
|
## from http://azure.microsoft.com/en-us/pricing/details/virtual-machines/ |
|
## Prices are in West US updated 2016-03-01. |
|
## Get a list of api names by specifing a bad one in cli. |
|
FLAVOR_DATA=( |
|
# General purpose compute: Basic tier |
|
[Basic_A0]='1/0.75/20/$0.018' |
|
[Basic_A1]='1/1.75/40/$0.047' |
|
[Basic_A2]='2/3.5/60/$0.094' |
|
[Basic_A3]='4/7/120/$0.188' |
|
[Basic_A4]='8/14/240/$0.376' |
|
|
|
# General purpose compute: Standard tier |
|
[ExtraSmall]='1/0.75/20/$0.02' |
|
[Small]='1/1.75/70/$0.06' |
|
[Medium]='2/3.5/135/$0.12' |
|
[Large]='4/7/285/$0.24' |
|
[ExtraLarge]='8/14/605/$0.48' |
|
[A5]='2/14/135/$0.25' |
|
[A6]='2/28/285/$0.50' |
|
[A7]='2/15/605/$1.00' |
|
|
|
# Optimized compute |
|
[Standard_D1]='1/3.5/50/$0.077' |
|
[Standard_D2]='2/7/100/$0.154' |
|
[Standard_D3]='4/14/200/$0.308' |
|
[Standard_D4]='8/28/400/$0.616' |
|
[Standard_D11]='2/14/100/$0.195' |
|
[Standard_D12]='4/28/200/$0.390' |
|
[Standard_D13]='8/56/400/$0.780' |
|
[Standard_D14]='16/112/800/$1.542' |
|
|
|
# Optimized compute |
|
[Standard_D1_v2]='1/3.5/50/$0.073' |
|
[Standard_D2_v2]='2/7/100/$0.146' |
|
[Standard_D3_v2]='4/14/200/$0.293' |
|
[Standard_D4_v2]='8/28/400/$0.585' |
|
[Standard_D5_v2]='16/56/800/$1.17' |
|
[Standard_D11_v2]='2/14/100/$0.185' |
|
[Standard_D12_v2]='4/28/200/$0.371' |
|
[Standard_D13_v2]='8/56/400/$0.741' |
|
[Standard_D14_v2]='16/112/800/$1.482' |
|
|
|
# Performance optimized compute |
|
[Standard_G1]='2/28/412/$0.61' |
|
[Standard_G2]='4/56/825/$1.22' |
|
[Standard_G3]='8/112/1,650/$2.44' |
|
[Standard_G4]='16/224/3,300/$4.88' |
|
[Standard_G5]='32/448/6,600/$8.69' |
|
|
|
# Network Optimized |
|
[A8]='8/56/382/$0.975' |
|
[A9]='16/112/382/$1.95' |
|
[A10]='8/56/382/0.78' |
|
[A11]='16/112/382/1.56' |
|
) |
|
|
|
get_images || fail "failed to get image lists" |
|
IMAGES=${_RET} |
|
|
|
# search list of 'key:value[whitespace]' |
|
inargs() { |
|
local needle="$1" c="" |
|
shift; |
|
for c in "$@"; do |
|
[ "$c" = "$needle" ] && return 0 |
|
done |
|
return 1 |
|
} |
|
kvget() { |
|
local key="$1" tok="" |
|
shift |
|
for tok in "$@"; do |
|
[ "${tok#${key}:}" != "${tok}" ] && _RET=${tok#${key}:} && return 0 |
|
done |
|
return 1 |
|
} |
|
kvgetk() { |
|
local val="$1" tok="" |
|
shift |
|
for tok in "$@"; do |
|
[ "${tok%:${val}}" != "${tok}" ] && _RET=${tok%:${val}} && return 0 |
|
done |
|
return 1 |
|
} |
|
|
|
Usage() { |
|
cat <<EOF |
|
Usage: ${0##*/} [-n name] [image|flavor|location] [image|flavor|[.|auto]] |
|
|
|
Give a image a flavor and a name and boot an instance. |
|
|
|
options: |
|
--flavor F specify flavor |
|
--image I specify image |
|
-n | --name N specify name |
|
--key K use ssh key in K (must be .pem file) |
|
--password P set default user's password to 'P' |
|
--dry-run only report what would be done |
|
--user-data-file F specify user-data read from file 'F' |
|
--custom-data F alias for user-data-file |
|
-v | --verbose be more verbose |
|
--user U initial user named 'U' (default: $(id -un)) |
|
|
|
EOF |
|
local i |
|
echo " images:" |
|
for i in $IMAGES; do |
|
# if they gave -v --help, give more help |
|
if [ "$VERBOSITY" -ge 1 ]; then |
|
echo " ${i%:*} [${i##*__}]" |
|
else |
|
echo " ${i%:*}" |
|
fi |
|
done |
|
echo " flavors:" |
|
local flavors=$(for i in "${!FLAVOR_DATA[@]}"; do echo "$i"; done | sort) |
|
for i in ${flavors}; do |
|
printf "%7s%-18s%s\n" "" "$i" "${FLAVOR_DATA[$i]}" |
|
done |
|
echo " locations:" |
|
for i in "${LOCATIONS[@]}"; do |
|
echo " ${i/:/: }" |
|
done |
|
|
|
echo |
|
cat <<EOF |
|
Examples: |
|
* launch trusty instance in US East 1 size Basic_A0 (default location/size) |
|
azure-ubuntu trusty --name=foo |
|
* launch larger vivid instance in "Brazil South" |
|
azure-ubuntu vivid --name=vivid-foo Basic_A4 --user-data=my-userdata |
|
EOF |
|
|
|
} |
|
bad_Usage() { |
|
Usage 1>&2; |
|
error "$@" |
|
exit 1 |
|
} |
|
|
|
name="" |
|
image_id="" |
|
flavor_id="" |
|
location_id="" |
|
pt=() |
|
dry=false |
|
udarg="" |
|
def_userdata="$HOME/data/my-userdata-azure" |
|
[ -f "$def_userdata" ] && udarg="$def_userdata" |
|
|
|
while [ $# -ne 0 ]; do |
|
cur="$1" |
|
next="$2" |
|
if kvget "$cur" $IMAGES; then |
|
image_id=$_RET; rel=$cur; shift; continue; |
|
fi |
|
if [ -n "${FLAVOR_ALIASES[$cur]}" ]; then |
|
flavor_id=${FLAVOR_ALIASES[$cur]}; shift; continue; |
|
fi |
|
if inargs "$cur" "${!FLAVOR_DATA[@]}"; then |
|
flavor_id=$cur; shift; continue; |
|
fi |
|
if kvget "$cur" "${LOCATIONS[@]}"; then |
|
location_id="$_RET"; shift; continue |
|
fi |
|
if inargs "$cur" "${pt_optargs[@]}"; then |
|
pt[${#pt[@]}]="$cur"; |
|
pt[${#pt[@]}]="$next"; |
|
shift 2 || bad_Usage "Expected argument for $cur"; |
|
debug 1 "passing through: $cur $next"; |
|
continue |
|
fi |
|
case "$1" in |
|
-h|--help) Usage; exit 0;; |
|
--flavor) |
|
[ -n "${FLAVOR_ALIASES[$next]}" ] && next="${FLAVOR_ALIASES[$next]}" |
|
inargs "$next" "${!FLAVOR_DATA[@]}" || |
|
fail "bad argument for flavor: $next" |
|
flavor_id=$next; |
|
shift;; |
|
--flavor=*) |
|
next="${cur#*=}" |
|
[ -n "${FLAVOR_ALIASES[$next]}" ] && next="${FLAVOR_ALIASES[$next]}" |
|
inargs "$next" "${!FLAVOR_DATA[@]}" || |
|
fail "bad argument for flavor: $next" |
|
flavor_id=$next;; |
|
--image) image_id=${next}; shift;; |
|
--image=*) image_id=${cur#*=};; |
|
--key=*) key_pem=${cur#*=};; |
|
--key) key_pem=$next; shift;; |
|
--password=*) password=${cur#*=};; |
|
--password) password=$next; shift;; |
|
--dry-run) dry=true;; |
|
--user-data-file|--custom-data) |
|
udarg=$next; shift;; |
|
--user-data-file=*|--custom-data=*) |
|
udarg=${cur#--user-data-file=} |
|
[ "${udarg#[~]/}" != "$udarg" ] && udarg="$HOME/${udarg#[~]/}";; |
|
-v|--verbose) |
|
VERBOSITY=$(($VERBOSITY+1)); |
|
pt[${#pt[@]}]=$cur;; |
|
.|auto|${name_prefix}*) |
|
[ -z "$name" ] || |
|
bad_Usage "name already set to '$name'. confused by '$cur'." 1>&2; |
|
name="$cur" |
|
[ "$name" = "." ] && name="auto" |
|
;; |
|
--user=*) user="${cur#*=}";; |
|
--user) user="$next"; shift;; |
|
-n|--name) name="$next"; shift;; |
|
--name=*) name="${cur#*=}";; |
|
--*=*|-[a-z]*) |
|
pt[${#pt[@]}]=$cur |
|
debug 1 "passing through: $cur";; |
|
--*) |
|
pt[${#pt[@]}]=$cur; |
|
debug 1 "passing through: $cur";; |
|
*) bad_Usage "confused by argument '$cur'";; |
|
esac |
|
shift |
|
done |
|
|
|
[ -n "$name" ] || |
|
bad_Usage "must give a name. use '.' or 'auto' for generated"; |
|
|
|
[ -z "$image_id" ] && { kvget $image $IMAGES; image_id=$_RET; rel=$image; } |
|
[ -z "$flavor_id" ] && { flavor_id=$flavor; } |
|
[ -z "$location_id" ] && |
|
{ kvget $location "${LOCATIONS[@]}"; location_id=$_RET; } |
|
|
|
[ -z "$key_pem" -o -f "$key_pem" ] || |
|
fail "--key-pem argument ($key_pem) is not a file" |
|
|
|
[ -n "$password" -o -n "$key_pem" ] || |
|
fail "must provide --password (AZURE_PASSWORD) or --key-pem" |
|
|
|
if [ "$name" = "auto" ]; then |
|
daterel=$(date --utc "+%m%d${rel:0:1}") |
|
dnsname="${name_prefix}${daterel}" |
|
else |
|
dnsname="$name" |
|
fi |
|
|
|
no_pass="--no-ssh-password" |
|
[ -n "$password" ] && no_pass="" |
|
|
|
kvgetk "$image_id" $IMAGES && image="$_RET" || image="${image_id}" |
|
kvgetk "$location_id" "${LOCATIONS[@]}" && |
|
location="$_RET" || location="${location_id}" |
|
|
|
if [ -n "$udarg" -a "$udarg" != "none" ]; then |
|
[ -f "$udarg" ] || |
|
{ error "userdata '$udarg': not a file"; return 1; } |
|
render_userdata_tmpl "$udarg" || |
|
{ error "failed to render $udarg"; return 1; } |
|
pt[${#pt[@]}]="--custom-data=$_RET" |
|
fi |
|
|
|
cmd=( azure vm create |
|
"--vm-size=$flavor_id" "--vm-name=$dnsname" "--location=${location_id}" |
|
${key_pem:+"--ssh-cert=${key_pem}"} $no_pass --ssh=22 |
|
"${pt[@]}" |
|
"$dnsname" "$image_id" "$user" ${password:+"${password}"} ) |
|
|
|
error "flavor=${flavor} image=${image} location=${location}" |
|
pcmd="" |
|
for c in "${cmd[@]}"; do |
|
[ "${c#* }" = "$c" ] && pcmd="$pcmd $c" || pcmd="$pcmd \"$c\"" |
|
done |
|
error "${pcmd# }" |
|
sdate="$(date -R)" |
|
stime=$(date "+%s") |
|
$dry && exit 0 |
|
"${cmd[@]}" |
|
ret=$? |
|
[ $ret -eq 0 ] || fail "command failed [$ret]. maybe check azure_error" |
|
etime=$(date "+%s") |
|
error "Launched in $(($etime-$stime)) seconds [started at $sdate]" |
|
error "ssh $user@$dnsname.cloudapp.net" |
|
|
|
# vi: ts=4 expandtab |