[Python] Adjust Azure VM power management Python application with Vue Front End

Discussão em 'Python' iniciado por Stack, Fevereiro 3, 2025.

  1. Stack

    Stack Membro Participativo

    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>&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>

    Continue reading...

Compartilhe esta Página