Michael Shepard
Michael Shepard

Reputation: 1

Adjust Azure VM power management Python application with Vue Front End

Im trying to change the way an Azure Function App turns on our Azure Virtual Machines in our Dev Envrionment. The backend in in Python and the front end is in Vue/Js/bulma. I'm wanting to change one button that currently "Adds a Schedule". Instead, Im wanting the button to say Turn on and when pressed, the VM is turned on and turns off automatically after 8 hours.

Function app button I'm wanting to change to be on-demand.

Here is the python code thats responsible for setting the schedule.

from azure.identity import DefaultAzureCredential
from azure.mgmt.compute import ComputeManagementClient
import json
import logging
import utilities

schedule_bp = func.Blueprint()


def generateCronSchedule(vmData, timeString):
    # Create stop time chunk
    stopScheduleMinHour = (
        f"{vmData[timeString].split(':')[1]} {vmData[timeString].split(':')[0]}"
    )
    # Create days chunk
    daysString = ""
    for i in range(1, 8):
        if vmData["daysOfWeek"][utilities.daysMapping[i]]:
            daysString += f"{i},"
    daysString = daysString.rstrip(daysString[-1])

    stopSchedule = f"{stopScheduleMinHour} * * {daysString}"
    return stopSchedule


@schedule_bp.function_name(name="SetSchedule")
@schedule_bp.route(route="api/schedule", auth_level=func.AuthLevel.ANONYMOUS)
def set_schedule(req: func.HttpRequest) -> func.HttpResponse:

    vmData = json.loads(req.get_body())

    # Extract subscription id and resource group from vm id
    subscriptionId = vmData["id"].split("/")[2]
    resourceGroup = vmData["id"].split("/")[4]
    vmName = vmData["id"].split("/")[8]

    compute_client = ComputeManagementClient(
        credential=DefaultAzureCredential(exclude_environment_credential=True), subscription_id=subscriptionId
    )

    vmInstance = compute_client.virtual_machines.get(
        resource_group_name=resourceGroup, vm_name=vmName
    )

    # Check the method type to see if we're adding or deleting a schedule
    if req.method == "DELETE":
        logging.info("REMOVING SCHEDULE")
        # Calculate updated tags
        tags = {}
        if vmInstance.tags:
            tags = vmInstance.tags
        tags.pop(utilities.STARTSCHEDULETAG, None)
        tags.pop(utilities.STOPSCHEDULETAG, None)
    else:

        tags = {}
        if vmInstance.tags:
            tags = vmInstance.tags

        stopSchedule = generateCronSchedule(vmData, "stopTime")
        tags[utilities.STOPSCHEDULETAG] = stopSchedule

        if vmData["startTime"]:
            startSchedule = generateCronSchedule(vmData, "startTime")
            tags[utilities.STARTSCHEDULETAG] = startSchedule
        else:
            tags.pop(utilities.STARTSCHEDULETAG, None)

    add_tags_event = compute_client.virtual_machines.begin_create_or_update(
        resource_group_name=resourceGroup,
        vm_name=vmName,
        parameters={"location": vmInstance.location, "tags": tags},
        polling_interval=1,
    )

    add_tags_event.wait()

    return func.HttpResponse("OK")

Here is the code for the startStop code:

import azure.functions as func
from azure.mgmt.compute import ComputeManagementClient
from azure.identity import DefaultAzureCredential
from croniter import croniter
import datetime
import logging
import pytz
import utilities

startstop_bp = func.Blueprint()


@startstop_bp.function_name(name="StartStop")
@startstop_bp.schedule(
    schedule="*/5 * * * *", arg_name="timer", run_on_startup=False, use_monitor=False
)
def start_stop_vms(timer):

    # Get the set timezone
    current_timezone = utilities.get_setting("Timezone")
    if not current_timezone:
        # Default to UTC if the user hasn't set a timezone
        current_timezone = "UTC"
    
    current_time = datetime.datetime.now(pytz.timezone(current_timezone))
    logging.info(f"Evaluating start/stop at {current_time}")

    for subscription in utilities.get_subscriptions():

        logging.info(f"Processing subscription: {subscription['id']}")

        compute_client = ComputeManagementClient(
            credential=DefaultAzureCredential(exclude_environment_credential=True), subscription_id=subscription["id"]
        )

        events = []

        for vm in compute_client.virtual_machines.list_all():
            logging.info(vm.id)

            if vm.tags and utilities.STOPSCHEDULETAG in vm.tags:
                stop_schedule = croniter(vm.tags[utilities.STOPSCHEDULETAG]).expanded
                # Start and stop tag pair behaviour
                if utilities.STARTSCHEDULETAG in vm.tags:
                    start_schedule = croniter(
                        vm.tags[utilities.STARTSCHEDULETAG]
                    ).expanded
                    # Are we in an on-day?
                    if (
                        current_time.weekday() + 1 in start_schedule[4]
                        or start_schedule[4][0] == "*"
                    ):
                        logging.info(f"[{vm.name}]: has on schedule today")
                        # Are we after the start time?
                        # [[0], [9], ['*'], ['*'], ['*']]
                        start_time = datetime.time(
                            start_schedule[1][0], start_schedule[0][0], 0
                        )
                        stop_time = datetime.time(
                            stop_schedule[1][0], stop_schedule[0][0], 0
                        )
                        logging.info(f"[{vm.name}]: start time {start_time}")
                        logging.info(f"[{vm.name}]: stop time  {stop_time}")
                        # Get the current VM state
                        vm_state = utilities.extract_vm_state(vm, compute_client)
                        logging.info(f"[{vm.name}]: {vm_state}")
                        # Check what the target state of the vm should be, current vm states running/deallocating/deallocated
                        if (
                            current_time.time() > start_time
                            and current_time.time() < stop_time
                        ):
                            logging.info(f"[{vm.name}]: VM should be running")
                            if vm_state != "running":
                                utilities.log_vm_event(vm, "starting")
                                events.append(utilities.set_vm_state('started', vm, compute_client))
                                logging.info(
                                    f"[{vm.name}]: starting..."
                                )
                        else:
                            logging.info(f"[{vm.name}]: VM should be stopped")
                            if vm_state == "running":
                                utilities.log_vm_event(vm, "stopping")
                                events.append(utilities.set_vm_state('stopped', vm, compute_client))
                                logging.info(
                                    f"[{vm.name}]: stopping..."
                                )
                    else:
                        logging.info(f"[{vm.name}]: is not scheduled to be on today")
                # Stop tag only behaviour
                else:
                    stop_schedule = croniter(
                        vm.tags[utilities.STOPSCHEDULETAG]
                    ).expanded
                    # Are we in an on-day?
                    if (
                        current_time.weekday() + 1 in stop_schedule[4]
                        or stop_schedule[4][0] == "*"
                    ):
                        stop_time = datetime.time(
                            stop_schedule[1][0], stop_schedule[0][0], 0
                        )
                        if current_time.time() > stop_time:
                            vm_state = utilities.extract_vm_state(vm, compute_client)
                            if vm_state == "running":
                                events.append(utilities.set_vm_state('stopped', vm, compute_client))
                                logging.warning(
                                    f"[{vm.name}]: stopping..."
                                )
                    else:
                        logging.warning(
                            f"[{vm.name}]: is not scheduled to be stopped today"
                        )
        
        # Wait for all events to complete
        for event in events:
            event.wait()

Right now, when you click on "Add a Schedule", it brings up a page that is a Vue component...Here is the code for it:

<template>
  <div v-if="vmData">
    <h1 class="title">{{ vmData.name }}</h1>

    <hr />

    <progress
      v-if="formDisabled"
      class="progress is-small is-primary"
      max="100"
    >
      15%
    </progress>

    <div class="columns is-mobile">
      <div class="column">
        <label class="label">Start Time</label>
        <div class="field has-addons has-addons-right">
          <div class="control is-expanded">
            <input
              v-model="vmData.startTime"
              class="input"
              type="time"
              :disabled="vmData.stopTime == null || formDisabled"
            />
          </div>
          <p v-if="vmData.startTime" class="control">
            <button
              @click="clearStartTime()"
              class="button is-primary"
              :disabled="formDisabled"
            >
              Clear
            </button>
          </p>
        </div>
      </div>
      <div class="column">
        <div class="field">
          <label class="label">Stop Time</label>
          <div class="control">
            <input
              v-model="vmData.stopTime"
              class="input"
              type="time"
              :disabled="formDisabled"
            />
          </div>
        </div>
      </div>
    </div>

    <div class="field">
      <label class="label">Days of Week</label>
      <div class="columns has-text-centered is-mobile">
        <div class="column">
          <input
            type="checkbox"
            v-model="vmData.daysOfWeek.Mon"
            :disabled="formDisabled"
          />
          Mon
        </div>
        <div class="column">
          <input
            type="checkbox"
            v-model="vmData.daysOfWeek.Tue"
            :disabled="formDisabled"
          />
          Tue
        </div>
        <div class="column">
          <input
            type="checkbox"
            v-model="vmData.daysOfWeek.Wed"
            :disabled="formDisabled"
          />
          Wed
        </div>
        <div class="column">
          <input
            type="checkbox"
            v-model="vmData.daysOfWeek.Thu"
            :disabled="formDisabled"
          />
          Thu
        </div>
        <div class="column">
          <input
            type="checkbox"
            v-model="vmData.daysOfWeek.Fri"
            :disabled="formDisabled"
          />
          Fri
        </div>
        <div class="column">
          <input
            type="checkbox"
            v-model="vmData.daysOfWeek.Sat"
            :disabled="formDisabled"
          />
          Sat
        </div>
        <div class="column">
          <input
            type="checkbox"
            v-model="vmData.daysOfWeek.Sun"
            :disabled="formDisabled"
          />
          Sun
        </div>
      </div>
    </div>

    <div v-if="formErrors.length > 0" class="notification is-warning is-light">
      <p v-for="error in formErrors" :key="error">{{ error }}</p>
    </div>

    <div class="field is-grouped is-grouped-right">
      <div class="control">
        <button
          v-if="emptySchedule == false"
          @click="removeSchedule()"
          class="button is-primary"
          :disabled="formDisabled"
        >
          <i class="fa-solid fa-xmark"></i>&nbsp;Remove Schedule
        </button>
      </div>
      <div class="control">
        <button
          @click="updateSchedule()"
          class="button is-link is-right"
          :disabled="formDisabled || formErrors.length > 0"
        >
          <i class="fa-regular fa-clock"></i>&nbsp;Apply
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import { timeToDate } from "../helper.js";

export default {
  props: ["vm"],
  emits: ["applied"],
  watch: {
    vm: function (newVal) {
      this.vmData = newVal;
    },
    vmData: {
      handler: function (newVal) {
        let errors = [];
        if (newVal.stopTime == null) {
          errors.push("Schedule requires a stop time");
        } else {
          // Check if at least one day is defined
          let dayCount = 0;
          Object.keys(newVal.daysOfWeek).forEach(function (value) {
            if (newVal.daysOfWeek[value]) {
              dayCount += 1;
            }
          });
          if (dayCount == 0) {
            errors.push("Schedule requires at least 1 day set");
          }
          // Check if start date is before end date
          if (newVal.startTime && newVal.stopTime) {
            if (timeToDate(newVal.startTime) >= timeToDate(newVal.stopTime)) {
              errors.push("Start time should be before stop time");
            } else if (timeToDate(newVal.stopTime) - timeToDate(newVal.startTime) < 1800000) {
              errors.push("Schedule should be at least 30 minutes long")
            }
          }
        }
        this.formErrors = errors;
      },
      deep: true,
    },
  },
  mounted() {
    // Make a deep copy of this
    this.vmData = JSON.parse(JSON.stringify(this.vm));
    // Work out if it's an empty schedule
    if (!this.vm.stopTime) {
      this.emptySchedule = true;
    } else {
      this.emptySchedule = false;
    }
  },
  methods: {
    clearStartTime: function () {
      this.vmData.startTime = null;
    },
    removeSchedule: function () {
      this.formDisabled = true;

      let headers = new Headers({
        Accept: "application/json, text/plain, */*",
        "Content-Type": "application/json",
      });

      fetch(`/api/schedule`, {
        method: "DELETE",
        headers: headers,
        body: JSON.stringify(this.vmData),
      }).then(() => {
        this.formDisabled = false;
        this.$emit("applied");
      });
    },
    updateSchedule: function () {
      this.formDisabled = true;

      let headers = new Headers({
        Accept: "application/json, text/plain, */*",
        "Content-Type": "application/json",
      });

      fetch(`/api/schedule`, {
        method: "POST",
        headers: headers,
        body: JSON.stringify(this.vmData),
      }).then(() => {
        this.formDisabled = false;
        this.$emit("applied");
      });
    },
  },
  data() {
    return {
      formDisabled: false,
      vmData: null,
      formErrors: [],
      emptySchedule: null,
    };
  },
};
</script>

And here is the Vue.App

<template>
  <section class="hero is-small is-link">
    <div class="hero-body">
      <p class="title"><i class="fa-solid fa-clock"></i> Az. Start Stop</p>
    </div>
    <div class="hero-foot">
      <nav class="tabs is-right">
        <ul>
          <li>
            <a @click="settingsView = true">
              <span class="icon is-small"><i class="fa-solid fa-gear" aria-hidden="true"></i></span>
              <span>Settings</span>
            </a>
          </li>
        </ul>
      </nav>
    </div>
  </section>
  <!-- Settings View -->
  <div v-if="settingsView" class="modal is-active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <div class="box">
        <UpdateSettings @applied="settingsView = false; this.$router.go()"></UpdateSettings>
      </div>
      <button @click="settingsView = false" class="modal-close is-large" aria-label="close"></button>
    </div>
  </div>
  <!-- -->
  <section class="section">
    <UserMessage></UserMessage>
    <router-view />
  </section>
  <!-- -->
  <SignUp v-if="showSignUps"></SignUp>
</template>

<script>
import UpdateSettings from './components/UpdateSettings.vue'
import UserMessage from './components/UserMessage.vue'
import SignUp from './components/SignUp.vue'

export default {
  components: {
    UpdateSettings,
    UserMessage,
    SignUp
  },
  methods: {
    signUp: function () {
      this.signUpDisabled = true
      let headers = new Headers({
        Accept: "application/json, text/plain, */*",
        "Content-Type": "application/json",
      });
      fetch(`/api/signup`, {
        method: "POST",
        headers: headers,
        body: JSON.stringify({ email: this.userEmail }),
      }).then(() => {
        this.signedUp = true
      });
    },
  },
  data() {
    return {
      userEmail: null,
      signUpDisabled: false,
      signedUp: false,
      settingsView: false,
      showSignUps: false
    };
  },
};
</script>

Upvotes: 0

Views: 40

Answers (1)

Sampath
Sampath

Reputation: 3533

Adjust Azure VM power management Python application with Vue Front End

Locate the component or section where the “Add Schedule” button is defined in your Vue.js frontend (likely in the SetSchedule.vue file). Replace the button text and attach a new method to handle the VM’s “Turn On” functionality.

Vue:

<template>
  <table class="table is-fullwidth">
    <thead>
      <tr>
        <th>Name</th>
        <th>Resource Group</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="vm in vms" :key="vm.name">
        <td>{{ vm.name }}</td>
        <td>{{ vm.resourceGroup }}</td>
        <td>
          <button
            class="button is-primary"
            @click="turnOnVM(vm.name, vm.resourceGroup)"
          >
            Turn On
          </button>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
import axios from "axios";

export default {
  data() {
    return {
      vms: [], // List of VMs to display
    };
  },
  methods: {
    async turnOnVM(vmName, resourceGroupName) {
      try {
        const response = await axios.post("/api/turn-on-vm", {
          vm_name: vmName,
          resource_group_name: resourceGroupName,
        });

        if (response.status === 200) {
          alert(`Successfully turned on ${vmName}. It will turn off automatically in 8 hours.`);
        } else {
          alert(`Failed to turn on ${vmName}.`);
        }
      } catch (error) {
        console.error("Error turning on VM:", error);
        alert("An error occurred while turning on the VM.");
      }
    },
  },
  async mounted() {
    try {
      const response = await axios.get("/api/get-vms");
      this.vms = response.data.vms; // Fetch VM data from the backend
    } catch (error) {
      console.error("Error fetching VM data:", error);
    }
  },
};
</script>

Now, Starts the VM schedule it to shut down after 8 hours.

Function app code:

import logging
import datetime
from azure.identity import DefaultAzureCredential
from azure.mgmt.compute import ComputeManagementClient
from azure.mgmt.resource import ResourceManagementClient
import azure.functions as func

def main(req: func.HttpRequest) -> func.HttpResponse:
    try:
        data = req.get_json()
        vm_name = data.get("vm_name")
        resource_group = data.get("resource_group_name")

        if not vm_name or not resource_group:
            return func.HttpResponse("Missing VM name or resource group.", status_code=400)

        credential = DefaultAzureCredential()
        subscription_id = "<YOUR_SUBSCRIPTION_ID>"

        # Initialize Azure clients
        compute_client = ComputeManagementClient(credential, subscription_id)

        # Start the VM
        logging.info(f"Starting VM: {vm_name} in resource group: {resource_group}")
        start_poller = compute_client.virtual_machines.begin_start(resource_group, vm_name)
        start_poller.result()  # Wait for completion
        logging.info(f"VM {vm_name} started successfully.")

        # Schedule stop after 8 hours
        shutdown_time = datetime.datetime.utcnow() + datetime.timedelta(hours=8)
        logging.info(f"Scheduling VM {vm_name} to shut down at {shutdown_time} UTC.")

        stop_poller = compute_client.virtual_machines.begin_deallocate(resource_group, vm_name)
        # Using a delay mechanism to emulate scheduling (external job schedulers or TimerTriggers are more robust)
        stop_poller.result()  # You can replace this with a queue message or event

        return func.HttpResponse(
            f"VM {vm_name} has been started and will shut down automatically after 8 hours.",
            status_code=200,
        )

    except Exception as e:
        logging.error(f"Error in starting VM: {str(e)}")
        return func.HttpResponse(f"Error: {str(e)}", status_code=500)

Update the Azure Function to handle the /api/turn-on-vm POST request.

enter image description here

Upvotes: 0

Related Questions