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]: 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> 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> 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> Continue reading...