#!/bin/bash # # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)oci-pricelist 1.0.0 18.09.2024 (c)2024 Standby.cloud # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ # # This script can be used free of charge. Use it as is or customize as needed. It is not guaranteed to be # error free and you will not be reimbursed for any damage it may cause. # #@ Shows listprice (PAYG) for SKU and compare with rete card price, #@ #@Usage: )oci-pricelist [options] [action] [param] #@ Options: #@ -h, --help : Displays helptext. #@ -v, --version : Displays the version of the script. #@ -c, --currency: One currency or comma separated list of currencies e.g. "USD,EUR". #@ -a, --account : Cloud account or subscription id. #@ Actions: #@ download: Force download the currend PAYG pricelist. #@ import : Import rate-card specified by param (a filename). #@ compare : Get all items from a cost-report specified by param (a filename) and compare with rate-card #@ show : Show all online prices (PAYG) for partnumber (SKU) specified by param (could be one or more items separated by comma) #@ #@Examples: #@ #@ Show BYOL price for SKUs: #@ oci-pricelist show B108188,B108189,B108190 #@ #@ Import downloaded csv file with rate card infos and convert it to tsv: #@ oci-pricelist import rate-card-9_18_2024.csv # # Exit codes: # 01: Unknown or wrong parameter. # 02: Could not create pricelist folder # 03: Could not download OCI pricelist from internet. # 04: Invalid JSON file. # 05: Unable to import rate-card # 06: Unknown SKU # 99: User interrupt. # # See also: # **do-action**(1) # # Update history: # # V 1.0.0 18.09.2024 New version # # Doc: https://docs.oracle.com/en-us/iaas/Content/Billing/Tasks/signingup_topic-Estimating_Costs.htm#accessing_list_pricing # Search: https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/?partNumber=B108190¤cyCode=EUR # # ToDo: https://docs.oracle.com/en/cloud/get-started/subscriptions-cloud/meter/index.html # ToDo: https://www.ateam-oracle.com/post/a-first-look-at-the-oracle-cloud-metering-api # ToDo: Supprt rewards # # | sed 's|ocid1.tenancy.oc1..aaaaaaaaon2bq4ilfhn7xjf3wg7zhg3jx2myaiffht6fj4nj2fdtv6c6nk5q|metrodigital|g' # B90573 B95704 # # Find executable bash library and source it lib=`which lib.bash 2>/dev/null | sed 's|^no 'lib.bash' in .*||'` if [ "$lib" != "" ]; then source "$lib" else progdir=`dirname "$0"` if [ -r "${progdir}/lib.bash" ]; then source "${progdir}/lib.bash" else echo "Unexpected error: Unable to locate bash library 'lib.bash'." exit 1 fi fi # Defaults: Pricelist file and preferred currency readonly method="offline" # Could be online or offline readonly baseurl="https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/" readonly configdir="${HOME}/.config/admintools" readonly pricelistdir="${configdir}/pricelists" readonly pricelistname="oci-payg" readonly pricelistnameobs="oci-obsolete" readonly ratecardext="rate-card.tsv" readonly pricelist="${pricelistdir}/${pricelistname}.json" readonly pricelistobs="${pricelistdir}/${pricelistnameobs}.json" # Do extra cleanup function ExtraCleanup() { filecheck -rm ${scratchfile}.rep # Infos from cost report filecheck -rm ${scratchfile}.pre # Prepare the file filecheck -rm ${scratchfile}.tsv # TSV file with all relevant data filecheck -rm ${scratchfile}.sku # JSON file with sku number } # Specify cloud account ot subscription id to get filename function GetRateCardFilename() { local id=${1} local filename="" if [ "$id" != "" ]; then filename=`ls "${pricelistdir}/"*${id}* 2>/dev/null` if [ "$filename" != "" ]; then if [ `echo $filename | wc -w` -ne 1 ]; then filename="" else if [ ! -r "$filename" ]; then filename="" fi fi fi fi echo "$filename" } # Create forst line in TSV file with all the attribute names function CreateTSVFile() { local outfile=${1} local currencyL=${2} local currency="" if [ "$outfile" != "" -a "$currencyL" != "" ]; then # Create outfile (tsv file) printf "partNumber\tstatus\tdisplayName\tdescription\tmetricName\tserviceCategory" > $outfile for currency in $currencyL; do printf "\t%s" "$currency" >> $outfile done printf "\tcomment\n" >> $outfile fi } # Convert one item from JSON (infile) to TSV (outfile) function ConvertJSON() { local infile=${1} local outfile=${2} local status=${3} local extra=${4} local line="" local sku="" local name="" local desc="" local metric="" local category="" local model="" local value="" local rmin="" local rUnit="" local result="" local titems=0 local tcurrency=0 local i=0 local j=0 local stat=1 result=`filecheck -sl "$infile"` if [ "$result" != "" ]; then result=`filecheck -s "$outfile"` if [ "$result" != "" ]; then titems=`cat "$infile" | jq -r '. | length'` while [ $i -lt $titems ]; do line=`cat "$infile" | jq -r '(.['$i'] | [.partNumber, .displayName, if has("description") then .description else "NONE" end, .metricName, .serviceCategory]) | @tsv' 2>/dev/null` stat=$? if [ $stat -eq 0 ]; then # write first values to output file sku=`echo "$line" | cut -d$'\t' -f1` name=`echo "$line" | cut -d$'\t' -f2` desc=`echo "$line" | cut -d$'\t' -f3` desc=`ShrinkString "$desc" 40` metric=`echo "$line" | cut -d$'\t' -f4` category=`echo "$line" | cut -d$'\t' -f5` printf "%s\t%s\t%s\t%s\t%s\t%s" "$sku" "$status" "$name" "$desc" "$metric" "$category" >> "$outfile" tcurrency=`cat "$infile" | jq -r '.['$i'].currencyCodeLocalizations | length'` while [ $j -lt $tcurrency ]; do result=`cat "$infile" | jq -r '(.['$i'].currencyCodeLocalizations['$j'].prices[] | [.model, .value, if has("rangeMin") then .rangeMin else "NONE" end, if has("rangeUnit") then .rangeUnit else "NONE" end]) | @tsv' 2>/dev/null` stat=$? if [ $stat -eq 0 ]; then result=`echo "$result" | sort -t$'\t' -k2 -g | tail -n 1` # -g = numeric sort model=`echo "$result" | cut -d$'\t' -f1` value=`echo "$result" | cut -d$'\t' -f2` rmin=`echo "$result" | cut -d$'\t' -f3` rUnit=`echo "$result" | cut -d$'\t' -f4` if [ "$rmin" = "NONE" ]; then result="$model" else if [ "$rUnit" = "NONE" ]; then result="$model (starting at: $rmin)" else result="$model (starting at: $rmin $rUnit)" fi fi # write value to output file printf "\t%s" "$value" >> "$outfile" fi let j++ done # write result to output file printf "\t%s\t%s\n" "$result" "$extra" >> "$outfile" fi let i++ done fi fi return $stat } # Get info about part number (SKU) function GetPartNumber() { local sku=${1} local currencyL=${2} local currency="" local status="UNKNOWN" local stat=0 if [ "$sku" != "" -a "$currencyL" != "" ]; then sku=`echo "$sku" | toupper` if [ -r "$pricelist" ]; then cat "$pricelist" | jq '.[] | select (.partNumber == "'$sku'")' > $scratchfile else # We can only select one currency whith online check currencyL=`echo "$currencyL" | cut -d' ' -f1` # Get SKU from Online APEX DB transfer "${baseurl}?partNumber=${sku}¤cyCode=$currencyL" --export $scratchfile stat=$? if [ $stat -eq 0 ]; then cat $scratchfile | jq '.items[]' > ${scratchfile}.pre 2>/dev/null stat=$? if [ $stat -eq 0 ]; then mv -f ${scratchfile}.pre $scratchfile else filecheck -rm $scratchfile fi else filecheck -rm $scratchfile fi fi result=`filecheck -sl $scratchfile` if [ "$result" != "" ]; then status="ACTIVE" else cat "$pricelistobs" | jq '.[] | select (.partNumber == "'$sku'")' > $scratchfile result=`filecheck -sl $scratchfile` if [ "$result" = "" ]; then stat=4 errormsg $stat "SKU '$sku' not found." else status="INACTIVE" fi fi fi if [ "$status" != "UNKNOWN" ]; then # Create JSON with single SKU printf '[\n' > ${scratchfile}.sku j=`grep -n '^ "' $scratchfile | wc -l` let j++ head -n $j $scratchfile >> ${scratchfile}.sku # We may are interested in more than one currency for currency in $currencyL; do cat $scratchfile | jq -r '.currencyCodeLocalizations[].currencyCode' > ${scratchfile}.pre result=`grep -n "$currency" ${scratchfile}.pre` i=`echo "$result" | cut -d ':' -f 1` if [ "$i" = "" ]; then stat=5 errormsg $stat "Currency '$currency' not found." else result=`echo "$currencyL" | cut -d' ' -f1` if [ "$result" != "$currency" ]; then printf "," >> ${scratchfile}.sku fi cat $scratchfile | jq '.currencyCodeLocalizations' | norm-json --raw --select $i >> ${scratchfile}.sku fi done # print rest of JSON printf ']\n}\n]\n' >> ${scratchfile}.sku # Check if JSON syntax is correct cat ${scratchfile}.sku | jq '.' > ${scratchfile}.pre stat=$? if [ $stat -eq 0 ]; then mv -f ${scratchfile}.pre ${scratchfile}.sku else rm -f ${scratchfile}.sku fi fi echo "$status" } # Compare SKU list with rate card function CompareSKUListWithRatecard() { local skulist=${1} local ratecard=${2} local extra=${3} local line="" local dummy="" local sku="" local status="" local dpname="" local lp="" local start="" local end="" local np="" local iv="" if [ "$skulist" != "" -a "" != "$ratecard" ]; then dummy=`echo "${skulist##*.}"` if [ "$dummy" = "tsv" ]; then if [ -r "$skulist" -a -r "$ratecard" ]; then if [ "$extra" = true ]; then printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "partNumber" "status" "displayName" "listPrice" "rcPrice" "discount" "startDate" "endDate" "invoicedPrice" "match" else printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "partNumber" "status" "displayName" "listPrice" "rcPrice" "discount" "startDate" "endDate" fi while IFS=$'\t' read -r sku status dpname dummy dummy dummy lp dummy iv; do line=`grep "^$sku " "$ratecard" | tail -n 1` # part number from rate card tsv if [ "$line" != "" ]; then start=`echo "$line" | cut -d$'\t' -f3` end=`echo "$line" | cut -d$'\t' -f4` np=`echo "$line" | cut -d$'\t' -f6` if [ "$lp" = "" -o "$lp" = "0" ]; then line=0 else # line=`echo "100 - (100 * ${np}) / ${lp}" | bc -l` line=`echo "100 - ((100 * ${np}) / ${lp})" | bc -l` fi else start="" end="" np="" line=0 fi dummy="" if [ "$extra" = true ]; then if [ "$iv" != "" ]; then iv=`printf "%.4f" $iv` if [ "$np" != "" ]; then if [ "$np" = "$iv" ]; then dummy="Yes" else dummy="No" fi fi fi else iv="" fi printf "%s\t%s\t%s\t%s\t%s\t%.2f %%\t%s\t%s\t%s\t%s\n" "$sku" "$status" "$dpname" "$lp" "$np" $line "$start" "$end" "$iv" "$dummy" done < <(cat $skulist | tailfromline2 | sort -u -t$'\t' -k1) fi fi fi } # Compare 2 JSON files function CompareFiles() { local newfile=${1} local oldfile=${2} local result="" local sku="" local i=1 local stat=0 if [ "$newfile" != "" -a "" != "$oldfile" ]; then if [ -r "$newfile" -a -r "$oldfile" ]; then result=`diff -q "$newfile" "$oldfile"` stat=$? if [ $stat -eq 0 ]; then # Files were equal - Keep new download (for timestamp reason) echo "Files are equal." mv -f "$newfile" "$oldfile" else echo "$result" # Delete array from obsolete pricelist cat "$pricelistobs" | jq '.[]' > $scratchfile mv -f $scratchfile "$pricelistobs" # To check if a SKU is missing in newfile, create index of all SKUs in oldfile cat "$oldfile" | grep ' "partNumber": ' | cut -d'"' -f4 > $scratchfile while read -r sku; do result=`grep ' "'$sku'"' "$newfile"` if [ "$result" = "" ]; then # SKU is no longer in pricelist - copy it to obsolete list cat "$oldfile" | norm-json --raw --select $i >> "$pricelistobs" fi let i++ done < $scratchfile # Create array for obsolete pricelist cat "$pricelistobs" | norm-json --raw > $scratchfile mv -f $scratchfile "$pricelistobs" # Rename oldfile and mv newfile result=`date '+%Y-%m-%d'` mv -f "$oldfile" "${pricelistdir}/${result}-${pricelistname}.json" mv -f "$newfile" "$oldfile" fi fi fi } # Download JSON file from APEX DB function DownloadPricelist() { local force=${1} local result="" local stat=0 # Create home of pricelist, if it does not exists if [ ! -d "$pricelistdir" ]; then mkdir -p "$pricelistdir" stat=$? fi if [ $stat -gt 0 ]; then exitcode=2 errormsg $exitcode "Could not create pricelist folder '$pricelistdir'." Cleanup exit $exitcode else # Create empty obsolete file (if not exists) if [ ! -f "$pricelistobs" ]; then echo "[]" > "$pricelistobs" fi # Download new pricelist from internet if it is oƶder then one day if [ "$force" = true ]; then result="" else result=`filecheck -fy "$pricelist"` fi if [ "$result" = "" ]; then printf "\nDownloading pricelist from APEX DB...\n" # Download current pricelist transfer "$baseurl" --export $scratchfile stat=$? if [ $stat -gt 0 ]; then exitcode=3 errormsg $exitcode "Could not download OCI pricelist from internet." Cleanup exit $exitcode else cat $scratchfile | jq '.items' > ${scratchfile}.pre 2>/dev/null stat=$? if [ $stat -gt 0 ]; then exitcode=4 errormsg $exitcode "Invalid JSON file. Errorcode: $stat." Cleanup exit $exitcode else # Check if the new file is different form the old downloaded file if [ -f "$pricelist" ]; then CompareFiles ${scratchfile}.pre "$pricelist" else # First time the script runs, pricelist will not exist mv -f ${scratchfile}.pre "$pricelist" fi fi fi fi fi } # Convert RateCard csv to tsv function ConvertRateCard() { local infile=${1} local stat=1 local product="" local startDate="" local endDate="" local unitCurrency="" local netUnitPrice="" local maxQuantitiy="" local metric="" local cloudAccount="" local subscriptionId="" local sku="" if [ "$infile" != "" ]; then if [ -r "$infile" ]; then # Create Header printf "sku\tproduct\tstartDate\tendDate\tunitCurrency\tnetUnitPrice\tmaxQuantitiy\tmetric\n" > $scratchfile while IFS=$'\t' read -r product startDate endDate unitCurrency netUnitPrice maxQuantitiy metric; do if [ "$product" != "" ]; then case "$product" in "Cloud cccount") cloudAccount=`echo "$startDate" | tr -d '"'` ;; "Subscription ID") subscriptionId=`echo "$startDate" | tr -d '"'` ;; User | Product) ;; *) sku=`echo "$product" | cut -d':' -f1` product=`echo "$product" | cut -d':' -f2-` startDate=`date --date="$startDate" '+%Y-%m-%d'` endDate=`date --date="$endDate" '+%Y-%m-%d'` metric=`echo "$metric" | tr -d '"'` printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \ "$sku" "$product" "$startDate" "$endDate" "$unitCurrency" "$netUnitPrice" "$maxQuantitiy" "$metric" >> $scratchfile esac fi done < <(cat "$infile" | grep . | sed 's/\("\([^"]*\)"\)\?,/\2\t/g' | sed 's|[ ]*- |:|g') if [ "$cloudAccount" != "" -a "$subscriptionId" != "" ]; then mv -f $scratchfile "${pricelistdir}/${cloudAccount}-${subscriptionId}-$ratecardext" stat=$? fi fi fi return $stat } # Preset status="" # ACTIVE or INACTIVE action="" subscription="" param="" ratecardfilename="" currencyList="" stat=0 # Check parameters: Loop until all parameters are used up while [ $# -gt 0 ]; do pname=${1} case "$pname" in -v | --version) shift showversion=true ;; -h | --help) shift showhelp=true ;; -c | --currency) shift if [ "$currencyList" = "" ]; then if [ "$1" != "" ]; then currencyList=`echo "$1" | tr ',' ' '` shift else errstr="Please specify a currency after option '$pname'." fi else errstr="Option '$pname' used more then once." fi ;; -a | --account) shift if [ "$ratecardfilename" = "" ]; then if [ "$1" != "" ]; then ratecardfilename=`GetRateCardFilename "$1"` if [ "$ratecardfilename" = "" ]; then errstr="Cloud-Account or Subscription-ID not found '$pname'. Was the rate-card already imported?" fi shift else errstr="Please specify an account name or subscription id after option '$pname'." fi else errstr="Option '$pname' used more then once." fi ;; *) shift paramck=`echo "$pname" | grep '^-'` # Keys don't begin with '-' if [ "$paramck" != "" ]; then errstr="Unknown option '$pname'." else if [ "$action" = "" ]; then action=`echo "$pname" | tolower` else if [ "$param" = "" ]; then param="$pname" else errstr="Action and paramaeter were already specified '$action $param'. Unknown additional parameter: '$pname'." fi fi fi esac done # Plausibility check case "$action" in import) # Import ratecard if [ "$param" != "" ]; then if [ ! -r "$param" ]; then errstr="Unable to read filename '$param'." else result=`echo "${param##*.}"` if [ "$result" != "csv" ]; then errstr="Filename '$param' does not end with extension 'csv'." fi fi else errstr="Please specify the filename of the rate-card to import after action '$action'." fi ;; compare) # Compare cost report with rate card if [ "$param" != "" ]; then if [ ! -r "$param" ]; then errstr="Unable to read filename '$param'." else result=`echo "${param##*.}"` if [ "$result" = "gz" ]; then gunzip "$param" stat=$? if [ $stat -eq 0 ]; then param=`echo "$param" | sed 's|.gz$||'` else errstr="Unable to gunzip '$param'." fi fi result=`echo "${param##*.}"` if [ "$result" = "csv" ]; then subscription=`tail -n 1 "$param" | cut -d ',' -f13` ratecardfilename=`GetRateCardFilename "$subscription"` else errstr="Filename '$param' does not end with extension 'csv'." fi fi else errstr="Please specify the filename of the cost-report to compare after action '$action'." fi ;; show) if [ "$param" != "" ]; then param=`echo "$param" | tr ',' ' '` else errstr="Please specify one ore more partnumbers (SKU) after action '$action'." fi ;; download) # Print start curdate=`date` printf "\n" printf "Starting : %s\nAction : %s\n" "$curdate" "$action" | print-header DownloadPricelist true Cleanup exit $exitcode ;; *) errstr="Unknown action: '$action'." esac # Display help or error message DisplayHelp ### Main # Get currency, account and subscription from ratecard file if [ "$ratecardfilename" != "" ]; then currencyList=`tail -n 1 "$ratecardfilename" | cut -d$'\t' -f5` result=`basename "$ratecardfilename"` if [ "$result" != "" ]; then account=`echo "$result" | cut -d "-" -f1` subscription=`echo "$result" | cut -d "-" -f2` else account="" subscription="" fi else account="" subscription="" if [ "$currencyList" = "" ]; then currencyList="USD EUR" fi fi # Ensure, that we are working with actual PAYG prices (download at least every day DownloadPricelist case "$action" in import) # Import ratecard ConvertRateCard $param stat=$? if [ $stat -gt 0 ]; then exitcode=5 errormsg $exitcode "Unable to import rate-card '$param'." "Status: '$stat'" fi ;; compare) if [ -r "$pricelist" ]; then printf "subscriptionId\tpartNumber\tproductDescription\tinvoicedPrice\tcurrency\n" > ${scratchfile}.rep # cat reports_cost-csv_*.csv cat "$param" | awk -v FS=',' -v OFS='\t' '{print $13, $14, $15, $16, $20}' | grep ' '$currencyList'$' | sort -t$'\t' -k2 -u >> ${scratchfile}.rep ### Check printf "\n" printf "Used SKUs in Cost Report\nFilename: '%s'\n" "$param" | print-header --color $blue printf "\n" cat ${scratchfile}.rep | print-table # Create outfile (tsv file) CreateTSVFile ${scratchfile}.tsv "$currencyList" while IFS=$'\t' read -r dummy sku dummy invoiced dummy; do if [ "$invoiced" != "0.000000000000" ]; then status=`GetPartNumber "$sku" "$currencyList"` if [ "$status" != "UNKNOWN" ]; then # Convert JSON to TSV ConvertJSON ${scratchfile}.sku ${scratchfile}.tsv "$status" "$invoiced" else exitcode=6 errormsg $exitcode "SKU '$sku' not found." fi fi done < <(cat ${scratchfile}.rep | tailfromline2) result=`filecheck -sl ${scratchfile}.tsv` if [ "$result" != "" ]; then exitcode=0 printf "\n" printf "SKU pricelist (currency: %s)\nCloud-Account: %s\nSubscription-ID: %s\n" "$currencyList" "$account" "$subscription" | print-header --color $blue printf "\n" CompareSKUListWithRatecard ${scratchfile}.tsv "$ratecardfilename" true > ${scratchfile}.rep print-table --import ${scratchfile}.rep --green "ACTIVE,Yes" --red "INACTIVE,No" fi fi ;; show) if [ -r "$pricelist" ]; then # Create outfile (tsv file) CreateTSVFile ${scratchfile}.tsv "$currencyList" # Search for each SKU that was specified for sku in $param; do status=`GetPartNumber "$sku" "$currencyList"` if [ "$status" != "UNKNOWN" ]; then # Convert JSON to TSV ConvertJSON ${scratchfile}.sku ${scratchfile}.tsv "$status" else exitcode=6 errormsg $exitcode "SKU '$sku' not found." fi done result=`filecheck -sl ${scratchfile}.tsv` if [ "$result" != "" ]; then exitcode=0 printf "\n" if [ "$ratecardfilename" = "" ]; then result=`echo "$currencyList" | wc -w` if [ $result -eq 1 ]; then result="(currency: $currencyList)" else result=`echo "$currencyList" | tr ' ' ','` result="(currencies: $result)" fi echo "SKU pricelist $result" | print-header --color $blue printf "\n" print-table --import ${scratchfile}.tsv --green "ACTIVE" --red "INACTIVE" else printf "SKU pricelist (currency: %s)\nCloud-Account: %s\nSubscription-ID: %s\n" "$currencyList" "$account" "$subscription" | print-header --color $blue printf "\n" CompareSKUListWithRatecard ${scratchfile}.tsv "$ratecardfilename" > ${scratchfile}.rep print-table --import ${scratchfile}.rep --green "ACTIVE" --red "INACTIVE" fi fi fi ;; esac Cleanup exit $exitcode