lib/azure_api/azure_calls.ex

defmodule AzureAPI.AzureCalls do

    @moduledoc """
    This module contains methods to interact with the Azure API.
    These are private functions that are only called from AzureAPI.VirtualMachineController.
    """

    use AzureBillingDashboardWeb, :live_view

    import Ecto.Query, warn: false
    alias AzureBillingDashboard.Repo
    alias AzureBillingDashboard.List_VMs
    alias AzureBillingDashboard.List_VMs.VirtualMachine

    # def iex_setup do
    #     azure_keys = %{"sub_id" => "f2b523ec-c203-404c-8b3c-217fa4ce341e", "tenant_id" => "a6a9eda9-1fed-417d-bebb-fb86af8465d2",
    #     "client_secret" => "oNH8Q~Gw6j0DKSEkJYlz2Cy65AkTxiPsoSLWKbiZ", "client_id" => "4bcba93a-6e11-417f-b4dc-224b008a7385",
    #     "resource_group" => "usyd-12a"}

    #     token = get_token(azure_keys)

    #     azure_keys = %{"sub_id" => "f2b523ec-c203-404c-8b3c-217fa4ce341e", "tenant_id" => "a6a9eda9-1fed-417d-bebb-fb86af8465d2",
    #     "client_secret" => "oNH8Q~Gw6j0DKSEkJYlz2Cy65AkTxiPsoSLWKbiZ", "client_id" => "4bcba93a-6e11-417f-b4dc-224b008a7385",
    #     "resource_group" => "usyd-12a", "token" => token}
    # end

    ################ API FUNCTIONS #######################

    @doc """
    Retrives access token from Azure.

    Token expires every hour so it buffers a :refresh_token message back to
    the genserver every 60 minutes.

    Returns token
    """

    def get_token(azure_keys, pid) do
        # Call Token Endpoint
        HTTPoison.start
        response = HTTPoison.post! "https://login.microsoftonline.com/#{Map.get(azure_keys, "tenant_id")}/oauth2/token", "grant_type=client_credentials&client_id=#{Map.get(azure_keys, "client_id")}&client_secret=#{Map.get(azure_keys, "client_secret")}&resource=https%3A%2F%2Fmanagement.azure.com%2F", [{"Content-Type", "application/x-www-form-urlencoded"}]
        {_status, body} = Poison.decode(response.body)

        # IO.inspect("################# Initizliase API Calls ##################")
        # IO.inspect(body["error"])

        token = body["access_token"]
        # IO.inspect(token)

        # Schedule a new token after 1 hour
        Process.send_after(pid, :refresh_token, 1 * 60 * 60 * 1000)

        token
    end

    # LIST AZURE MACHINES
    @doc """
    Makes a request to get virtual machine data from Azure

    Retrieves the following from /virtualmachines endpoint:
        1. Name
        2. Hardware Profile
        3. OS Type
        4. Maximum Price

    Retrieves the following from /instanceview endpoint
        1. Power State

    Important note for obtaining new attributes:
        1. Determine if attribute comes from the list or instanceView endpoint
            a. If list, ensure it is parsed into general_info variable
            b. If instanceView, ensure it is parsed into the map variable
        2. Ensure that the pattern matches in List_VMs.find_and_update_virtual_machines/1
        3. Ensure field created in List_VMs.VirtualMachine (virtual_machine.ex)
        4. Ensure field is migrated.

    Makes a callback request to the virtual machine controller to
    update the above values.
    """
    def list_azure_machines_and_statuses(azure_keys, pid) do
        # IO.inspect("\n\n############### List azure machines and statuses ###################\n\n")

        # Construct Header
        # IO.inspect(Map.get(azure_keys, "token"))
        header = ['Authorization': "Bearer " <> Map.get(azure_keys, "token")]

        # Call List Endpoint
        response = HTTPoison.get! "https://management.azure.com/subscriptions/#{Map.get(azure_keys, "sub_id")}/resourceGroups/#{Map.get(azure_keys, "resource_group")}/providers/Microsoft.Compute/virtualMachines?api-version=2022-03-01", header, []

        if response.status_code == 200 do
            body = Poison.Parser.parse!(response.body)
            # IO.inspect(body)
            # Extract names
            general_info = Enum.map(body["value"], fn (x) -> {x["name"], x["properties"]["hardwareProfile"]["vmSize"], x["location"], x["properties"]["storageProfile"]["osDisk"]["osType"], x["properties"]["billingProfile"]["maxPrice"]} end)

            # IO.inspect(general_info)
            # IO.inspect(names)

            # IO.inspect(names)

            map = Enum.map(general_info, fn info ->
                {name, vmSize, location, osType, maxPrice} = info
                instance_view = HTTPoison.get! "https://management.azure.com/subscriptions/f2b523ec-c203-404c-8b3c-217fa4ce341e/resourceGroups/usyd-12a/providers/Microsoft.Compute/virtualMachines/#{name}/instanceView?api-version=2022-03-01", header, []
                {_status, body} = Poison.decode(instance_view.body)
                # IO.inspect(body)
                os_disk = Enum.map(body["disks"], fn (x) -> x["name"] end)
                # status_update = body["vmAgent"]["statuses"]["time"]
                case Enum.map(body["statuses"], fn (x) -> x["displayStatus"] end) do
                    [_provision, power] -> {name, power, os_disk, vmSize, location, osType, maxPrice} # Most times there is a provisioning status and power status -> ["Provisioned", "VM Running"]
                    [power] -> {name, power, os_disk, vmSize, location, osType, maxPrice} # Sometimes there's only one status -> "Updating"
                end

            end)

            IO.inspect(map)

            List_VMs.find_and_update_virtual_machines(map)
            # # Save to repo
            # for item <- map do
            #     {name, power, os_disk, vmSize, location, osType, max_price} = item
            #     if Repo.exists?(from vm in VirtualMachine, where: vm.name == ^name) do
            #         # Get Machine
            #         Repo.get_by(VirtualMachine, [name: name])
            #         |> List_VMs.update_virtual_machine(%{name: name, status: power, odisk_name: List.first(os_disk), vmSize: vmSize, location: location, os_type: osType, max_price: max_price})
            #     else
            #         # Create Virtual Machine
            #         List_VMs.create_virtual_machine(%{name: name, status: power, odisk_name: List.first(os_disk), vmSize: vmSize, location: location, os_type: osType, max_price: max_price})
            #     end
            # end

            Process.send_after(pid, :refresh_sync, 5000)

            # assign(socket, :virtualmachines, List_VMs.list_virtualmachines())

            {:ok, map}
        else
           {:error, response.status_code}
        end
    end

    @doc """
    Computes availability for all virtual machines.

    Pings the CostManagement/prices endpoint with response filters
    Skips any virtual machine with a capacity only eviction type
    Compares the maximum price to that obtained from the endpoint
    """
    def get_availability(_azure_keys) do
        # IO.inspect("GETTING AVAILABILITY ----------------------------------")

        # #Obtain all current virtual machines from database
        vms = List_VMs.list_virtualmachines()

        url = "https://prices.azure.com/api/retail/prices?$filter="

        for vm <- vms do
            # IO.inspect(vm.name)
            # IO.inspect(vm.vmSize)

            if vm.max_price > 0 do
                # Construct filter element ["serviceName eq 'Virtual Machines' and armSkuName eq '#{vm.vmSize}' and 'armRegionName' eq '#{vm.location}'"]
                request_filter = "serviceName%20eq%20%27Virtual%20Machines%27%20and%20armSkuName%20eq%20%27#{vm.vmSize}%27%20and%20armRegionName%20eq%20%27#{vm.location}%27"
                request_url = url <> request_filter

                response = HTTPoison.get! request_url, [], []

                body = Poison.decode!(response.body)

                if String.contains?(vm.os_type, "Linux") do

                    spot_vms = Enum.filter(body["Items"], fn x ->
                        if String.contains?(Map.get(x, "skuName"), "Spot") and Map.get(x, "type") == "Consumption" and not String.contains?(x["productName"], "Windows") do
                            x
                        end
                    end)

                    # IO.inspect("Linux")
                    # IO.inspect(spot_vms)

                    unit_price = Float.floor(List.first(spot_vms)["unitPrice"], 5)*:math.pow(10,5)
                    IO.inspect(unit_price)
                    IO.inspect(vm.max_price * :math.pow(10,5))

                    if unit_price <= (vm.max_price * :math.pow(10,5)) do
                        IO.inspect("Available")
                        List_VMs.update_virtual_machine(vm, %{availability: "Available", availability_summary: "Based on price only, this VM is available to run."})
                    else
                        List_VMs.update_virtual_machine(vm, %{availability: "Unavailable", availability_summary: "Based on price, this VM is unable to run."})
                    end

                else
                    spot_vms = Enum.filter(body["Items"], fn x ->
                        if String.contains?(Map.get(x, "skuName"), "Spot") and Map.get(x, "type") == "Consumption" and String.contains?(x["productName"], "Windows") do
                            x
                        end
                    end)

                    # IO.inspect("Windows")
                    # IO.inspect(spot_vms)

                    unit_price = Float.floor(List.first(spot_vms)["unitPrice"], 5)*:math.pow(10,5)
                    IO.inspect(unit_price)
                    IO.inspect(vm.max_price * :math.pow(10,5))

                    if unit_price <= (vm.max_price * :math.pow(10,5)) do
                        List_VMs.update_virtual_machine(vm, %{availability: "Available", availability_summary: "Based on price only, this VM is available to run."})
                    else
                        List_VMs.update_virtual_machine(vm, %{availability: "Unavailable", availability_summary: "Based on price, this VM is unable to run."})
                    end
                end


            else
                # IO.inspect(vm.name <> "Unknown Status Update")
                List_VMs.update_virtual_machine(vm, %{availability: "Unknown", availability_summary: "Eviction on capacity only, unable to determine availability."})

            end

        end
    end

    # START AZURE MACHINES

    @doc """
    Makes a request to start a virtual machine

    Endpoint always returns a 202 - Accepted status

    Makes no availability checks. Azure will start the machine if conditions are met.
    """
    def start_azure_machine(name, azure_keys) do

        # Construct Header
        header = ['Authorization': "Bearer " <> Map.get(azure_keys, "token")]

        # Call Start Endpoint
        HTTPoison.post! "https://management.azure.com/subscriptions/#{Map.get(azure_keys, "sub_id")}/resourceGroups/#{Map.get(azure_keys, "resource_group")}/providers/Microsoft.Compute/virtualMachines/#{name}/start?api-version=2022-08-01", [], header
    end

    # STOP AZURE MACHINES
    @doc """
    Makes a request to deallocate a virtual machine
    """
    def stop_azure_machine(name, azure_keys) do

        # Construct Header
        header = ['Authorization': "Bearer " <> Map.get(azure_keys, "token")]

        # Call Start Endpoint
        HTTPoison.post! "https://management.azure.com/subscriptions/#{Map.get(azure_keys, "sub_id")}/resourceGroups/#{Map.get(azure_keys, "resource_group")}/providers/Microsoft.Compute/virtualMachines/#{name}/deallocate?api-version=2022-08-01", [], header

        # IO.inspect(response)
    end

      # GET COST DATA

    @doc """
    Retrieves cost data for a specific virtual machine from Azure.

    Constructs a body and sends a request to Azure.

    For example, needed in the details page

    eg -> response = AzureAPI.VirtualMachineController.get_cost_data(vmName).
    get_cost_data/1 grabs the token from the genserver and sends it to this function along with the vmName
    """
    def get_azure_cost_data(name, azure_keys) do
        # Construct Header
        header = ['Authorization': "Bearer " <> Map.get(azure_keys, "token"), 'content-type': "application/json"]

        scope = "subscriptions/#{Map.get(azure_keys, "sub_id")}/resourceGroups/#{Map.get(azure_keys, "resource_group")}"

        date = NaiveDateTime.local_now()
        date = NaiveDateTime.to_date(date)
        date = Date.to_string(date)

        body = %{
            :type => "Usage",
            :timeframe => "Custom",
            :timePeriod => %{
                :from => "2022-09-06",
                :to => date
            },
            :dataset => %{
                :granularity => "Daily",
                :filter => %{
                    :dimensions => %{
                        :name => "ResourceId",
                        :operator => "In",
                        :values => [
                            "/subscriptions/f2b523ec-c203-404c-8b3c-217fa4ce341e/resourcegroups/usyd-12a/providers/microsoft.compute/virtualmachines/#{name.name}",
                            "/subscriptions/f2b523ec-c203-404c-8b3c-217fa4ce341e/resourcegroups/usyd-12a/providers/microsoft.compute/disks/#{name.odisk_name}",
                            "/subscriptions/f2b523ec-c203-404c-8b3c-217fa4ce341e/resourcegroups/usyd-12a/providers/microsoft.network/publicipaddresses/#{name.name}-ip"
                        ]
                    }
                },
                :grouping => [
                    %{
                        :type => "Dimension",
                        :name => "ResourceId"
                    }
                ],
                :aggregation => %{
                    :totalCost => %{
                        :name => "PreTaxCost",
                        :function => "sum"
                    }
                }
            }
        }

        options = [recv_timeout: 100000]

        # Call Start Endpoint
        url = "https://management.azure.com/#{scope}/providers/Microsoft.CostManagement/query?api-version=2021-10-01"
        response = HTTPoison.post! url, Poison.encode!(body), header, options

        # Return decoded body
        Poison.decode! response.body

        # IO.inspect(response)
        # "https://management.azure.com/#{scope}/providers/Microsoft.CostManagement/query?api-version=2021-10-01"
    end

    @doc false
    def get_SKU(azure_keys) do

        header = ['Authorization': "Bearer " <> Map.get(azure_keys, "token"), 'content-type': "application/json"]

        response = HTTPoison.get! "https://management.azure.com/subscriptions/#{Map.get(azure_keys, "sub_id")}/providers/Microsoft.CognitiveServices/skus?api-version=2021-10-01", header, []

        body = Poison.decode!(response.body)

        # IO.inspect(body)

        _storage_sku = Enum.filter(body["value"], fn (x) ->
            if String.contains?(x["name"], "20_04-lts-gen2") do
              x
            end
        end)

        # IO.inspect(storage_sku)
    end

    def render(assigns) do
        ~H"""
        ...
        """
      end
end