|
#!/bin/bash |
|
#shellcheck disable=SC2002,SC2155 |
|
|
|
set -eu -o pipefail |
|
|
|
declare -r standard_skus_cache=/tmp/vm-standard-skus.json |
|
declare -r default_output_dir=. |
|
declare -r default_location=japaneast |
|
|
|
output_dir=$default_output_dir |
|
location=$default_location |
|
vm_skus=() |
|
|
|
usage_exit() { |
|
local exit_code="$1" |
|
|
|
cat <<EOF |
|
Usage: $0 [-d output_dir] [vm_sku ...] |
|
Options:" |
|
-o, --output-dir <dir> Directory to save output files (default: $default_output_dir). |
|
-l, --location <loc> Location (default: $default_location). |
|
-h, --help Show this help message and exit. |
|
Example: |
|
$0 -o /tmp/vm_prices D4ads_v6 D4as_v5 D4ds_v5 D4_v4 |
|
EOF |
|
exit "$exit_code" |
|
} |
|
|
|
parse_args() { |
|
local args=() |
|
while [[ $# -ne 0 ]]; do |
|
case "$1" in |
|
-h|--help) usage_exit 0 ;; |
|
-o|--output-dir) output_dir="$2"; shift ;; |
|
-l|--location) location="$2"; shift ;; |
|
-*) log "unknown option: $1"; usage_exit 1 1>&2 ;; |
|
*) args+=("$1") ;; |
|
esac |
|
shift |
|
done |
|
vm_skus=("${args[@]}") |
|
} |
|
|
|
log() { |
|
echo "$@" 1>&2 |
|
} |
|
|
|
join() { |
|
local delim="$1"; shift |
|
printf '%s\n' "$@" | paste -s -d"$delim" |
|
} |
|
|
|
sort_without_header() { |
|
awk 'NR==1 {print; next} {print | "sort"}' |
|
} |
|
|
|
vm_specs() { |
|
local sku_re="Standard_($(join '|' "${vm_skus[@]}"))" |
|
|
|
if ! [[ -f "$standard_skus_cache" ]]; then |
|
log "fetching VM specs from Azure..." |
|
az vm list-skus -l "$location" --size 'standard' --output json > "$standard_skus_cache" |
|
log "fetching VM specs from Azure...done" |
|
fi |
|
|
|
#shellcheck disable=SC2016 |
|
local jq_query=' |
|
.[] |
|
| select(.name | test($sku_re)) |
|
| (.capabilities | map({key:.name,value}) | from_entries) as $full_caps |
|
| ( |
|
$full_caps | { |
|
vCPUs, MemoryGB, PremiumIO, vCPUsAvailable, |
|
EphemeralOSDiskGB:(.NvmeSizePerDiskInMiB//.MaxResourceVolumeMB)|tonumber/1024 |
|
} |
|
) as $caps |
|
| [ |
|
.name, (.locationInfo[0].zones|join(",")), |
|
$caps.vCPUs, $caps.MemoryGB, $caps.EphemeralOSDiskGB, $caps.PremiumIO |
|
] |
|
| @tsv |
|
' |
|
join $'\t' sku zones vcpus memory_gb ephemeral_os_disk_gb premium_io |
|
cat "$standard_skus_cache" | jq --arg sku_re "$sku_re" "$jq_query" -r | sort_without_header |
|
} |
|
|
|
spots_azgraph_query() { |
|
local sku_re="Standard_($(join '|' "${vm_skus[@]}"))" |
|
|
|
cat <<EOF |
|
( |
|
SpotResources |
|
| where type =~ 'microsoft.compute/skuspotpricehistory/ostype/location' |
|
| where properties.osType =~ 'linux' |
|
| where sku.name matches regex "(?i)$sku_re" |
|
| where location =~ "$location" |
|
| project name = strcat(sku.name, "/", location), sku = tostring(sku.name), location, price_usd = properties.spotPrices[0].priceUSD |
|
) |
|
| join kind=leftouter ( |
|
SpotResources |
|
| where type =~ 'microsoft.compute/skuspotevictionrate/location' |
|
| project name = strcat(sku.name, "/", location), eviction_rate = tostring(properties.evictionRate) |
|
) on name |
|
| project sku, price_usd, eviction_rate = coalesce(eviction_rate, "<none>") |
|
| sort by sku |
|
EOF |
|
} |
|
|
|
vm_prices() { |
|
local sku |
|
for sku in "${vm_skus[@]}"; do |
|
( |
|
local arm_filter=" |
|
serviceName eq 'Virtual Machines' |
|
and armRegionName eq '$location' |
|
and isPrimaryMeterRegion eq true |
|
and armSkuName eq 'Standard_$sku'" |
|
curl -sSf https://prices.azure.com/api/retail/prices --url-query "\$filter=$arm_filter" |
|
) |
|
done | ( |
|
#shellcheck disable=SC2016 |
|
local jq_query=' |
|
[ |
|
.Items[] |
|
| select((.type == "Consumption") or (.type == "Reservation" and .reservationTerm == "1 Year")) |
|
| select(.productName|test("Windows|Cloud Services")|not) |
|
| select(.skuName|test("Spot|Low Priority")|not) |
|
] |
|
| group_by(.armSkuName, .armRegionName)[] |
|
| map(select(.type == "Consumption"))[0] as $cn |
|
| map(select(.type == "Reservation"))[0] as $rs |
|
| [$cn.armSkuName, $cn.unitPrice, $rs.unitPrice/365/24]|@tsv |
|
' |
|
join $'\t' sku price_usd reserved_price_usd |
|
jq "$jq_query" -r |
|
) | sort_without_header |
|
} |
|
|
|
disk_prices() { |
|
( |
|
local arm_filter=" |
|
serviceName eq 'Storage' |
|
and (productName eq 'Premium SSD Managed Disks' or productName eq 'Standard SSD Managed Disks') |
|
and armRegionName eq '$location' |
|
and isPrimaryMeterRegion eq true |
|
and type eq 'Consumption' |
|
" |
|
curl -sSf https://prices.azure.com/api/retail/prices --url-query "\$filter=$arm_filter" |
|
) | ( |
|
local jq_query=' |
|
.Items[] |
|
| select(.skuName|test("LRS")) |
|
| select(.skuName|test("10|15|20|30")) |
|
| select(.meterName|test("LRS Disk$")) |
|
| [.skuName, .unitPrice]|@tsv |
|
' |
|
join $'\t' sku price_usd_month |
|
jq "$jq_query" -r |
|
) | sort_without_header |
|
|
|
} |
|
|
|
main() { |
|
parse_args "$@" |
|
log "Retrieving VM spec and price informations to $output_dir..." |
|
|
|
vm_specs > "$output_dir/vm_specs.tsv" |
|
spots_azgraph_query > "$output_dir/spots_azgraph_query.txt" |
|
vm_prices > "$output_dir/vm_prices.tsv" |
|
disk_prices > "$output_dir/disk_prices.tsv" |
|
|
|
log "Retrieving VM spec and price informations to $output_dir...done" |
|
} |
|
|
|
main "$@" |