VICE / ModuleTests / Azure / AzureModule.js
AzureModule.js
Raw
import { QueueClient, QueueServiceClient } from "@azure/storage-queue"
import {TableClient, TableTransaction} from '@azure/data-tables'
//import uuidv1 from "uuid/v1";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const connects = require("./AzureConnects.json");


/*
description: file to house classes/interfaces that will handle many of the
details for interacting with Azure services, currently Table and Queue
*/


/*
Interface class to handle processing messages from Azure Queue. Provides an extra
layer of abstraction so the back-end only has to call simple functions.
*/
export class QueueInterface {

  /*
  Message formats:

  v1: "vmID:currentTest:status",
  v2(TBD): "versionNum:vmID:currentTest:status"
  */

  constructor(queueName) {
    this.connection = QueueServiceClient
                      .fromConnectionString(connects.azureConnect)
                      .getQueueClient(queueName);
  }

  /*
  desc: gets the next message in the queue, dequeues it and returns the message text.
  params: N/A
  returns: String message: message text from the dequeued message
  */
  async getNextMessage() {
    const response = await this.connection.receiveMessages();

    if (response.receivedMessageItems.length == 1) {
      const dequeuedMessage = response.receivedMessageItems[0];
      let message = dequeuedMessage.messageText;
      console.log(`Processing & deleting message with content: ${message}`);

      // do processing here

      const deletedMessage = await this.connection.deleteMessage(
        dequeuedMessage.messageId,
        dequeuedMessage.popReceipt
      );
      console.log(
        `Delete message successfully, service assigned request Id: ${deletedMessage.requestId}`
      );

      return message;
    }

    return null;
  }

  /*
  Desc: Adds a given message to the queue
  params: String message : Message to add to the queue
  returns: Success or failure
  */
  async addMessage(message) {
    const response = await this.connection.sendMessage(message);
    console.log(
      `Sent message successfully, message Id: ${response.messageId}, request Id: ${response.requestId}`
    );

    return true;
  }

  /*
  Desc: checks if there are messages in the queue, logs and returns amount of
        messages and up to 5 messages at the front
  params: N/A
  returns: Promise Object : {length: int, headMessages: array}
  */
  async checkMessages() {
    const messages = await this.connection.peekMessages({numberOfMessages: 5});
    let len = messages.peekedMessageItems.length;
    let headMessages = [];

    console.log("Head messages: ");
    for(let i = 0; i < 5 && i < len; i++)
    {
      headMessages.push(messages.peekedMessageItems[i].messageText);
      console.log(messages.peekedMessageItems[i].messageText);
    }

    return {"headMessages": headMessages};
  }

  /*
  Desc: clears all messages in the queue, none are saved or cached.
  params: N/A
  returns: N/A

  TODO: Use the literal clear messages function provided by azure
  */
  async clearMessages() {

    this.connection.clearMessages();

  }
}


/*

*/
export class TableInterface {

  /*
  Entity formats:

  JSON string data should be stored in each entry using "data" as the indentifier.
  Ex: {
  partitionKey:
  rowKey:
  ...
  data: "{dataObj}"
  }

  Issues Current (v2):
  {
  "id":"VG-11",
  "desc":null,
  "vm": "wi11", // vm row key
  "fw": "4798123" // fw row key
  }

  Issues To Be Added (v2.1):
  {
    "version": "2.1"
    "id":"VG-11",
    "desc":null,
    "vm": "wi11", // vm row key
    "fw": "4798123" // fw row key
  }

  VM Data Example (v1):
  {
    "vm":"wi11",
    "vmlinkWin": "", // rdp link
    "vmlinkLin": "", // other link
    "vmstatus":"In-Progress",
    "owner":"John Deer",
    "vmlocation":"Madison",
    "vmversion":"Windows"
  }

  FW Data Example (v1):
  {
    "product":"WD Red",
    "status":"Passed",
    "fwversion":"2.3.2",
    "fwlink": "", // ffu link
    "commitid":111727,
    "form":"7.5in",
    "fwlocation":"San Francisco",
    "serial":128648
  }
  */

  constructor(tableName) {
    this.connection = TableClient.fromConnectionString(connects.azureConnect, tableName);
  }

  /*
  Desc:
  params: Keys for each partition you want to pull from
  returns: Query String
  */
  getQueryString() {
   var stringArr = arguments[0].split(" ");

   var length = stringArr.length;

   if( length === 0 ){
     console.log("Zero arguments given to \'getQueryString()\'");
     throw "Zero arguments given to \'getQueryString()\'";
   }

   let utilStr = "PartitionKey eq ";
   let returnStr = "";

   for (var index = 0, length; index < length; index++) {

     console.log(stringArr[index])
     if(index == length - 1)
     {
       returnStr = returnStr + utilStr + stringArr[index];
     }
     else {
       returnStr = returnStr + utilStr + stringArr[index] + " or ";
     }

     console.log("returnStr:\t" + returnStr);

   }

   return returnStr;
  }

  /*
  desc: A general function to handle getting data from the database using specific
        types. This will provide a quick way of getting the data in a less friendly
        way, the other specific data functions will use this one to provide a
        friendly interface
  params: Int type : the type of data we want, like the partition key
          String ?role : if the user is requesting issue data, a role must be provided
          String ?id : the row key of the requested data, if not provided, all data
                       from that partition will be fetched
  returns: A object containing the requested data.
  */
  async getData(type, role = -1, id = -1) {

    try {

      var partition = "";
      var filter = "";
      var list = [];
      var data = null;

      switch (type) {
        // Issues
        case 0:

        if(id === -1) {
          filter = this.getQueryString("\'FWIssues\' \'DevIssues\' \'ExeIssues\'");
        }
        else if(role === "FW" || role === "Dev" || role === "Exe") {
          partition = role + "Issues"
        }
        else if (role === -1) {
          return {result: "Failed", desc: "invalid role type"};
        }

        break;

        // VMs
        case 1:

        if(id === -1) {
          filter = this.getQueryString("\'VM\'");
        }
        else {
          partition = "VM";
        }

        break;

        // FW
        case 2:

        if(id === -1) {
          filter = this.getQueryString("\'FW\'");
        }
        else {
          partition = "FW";
        }

        break;

        // System
        case 3:

        if(id === -1) {
          filter = this.getQueryString("\'System\'");
        }
        else {
          partition = "System";
        }

        break;

        // User
        case 4:

        if(id === -1) {
          filter = this.getQueryString("\'User\'");
        }
        else {
          partition = "User";
        }

        break;

        default:
        filter = ""
      }

      if(id === -1) {
        data = this.connection.listEntities({queryOptions: {filter: filter}});

        for await (const entity of data)
        {
          list.unshift(entity);
        }

        return {result: "Success", data: list};
      }
      else {
        data = await this.connection.getEntity(partition, id);

        return {result: "Success", data: data};
      }


    }
    catch(RestError) {
      console.log("failed to get data with id: " + id);
      return {result: "Failed", data: RestError.message};
    }
  }

  /*
  desc: A general function to handle adding data to the database using specific
        types. This will provide a quick way of adding the data in a less friendly
        way, the other specific data functions will use this one to provide a
        friendly interface
  params: Int type : the type of data we want to add, like the partition key
          String id : the row key of the data to be added
          Object data : the data to be added to the database
          String ?role : if the user is adding issue data, a role must be provided
  returns: A object containing the result of the operation
  */
  async addData(type, id, data, role = -1) {
    try {

      var partition = "";
      var result = null;

      switch (type) {
        // Issues
        case 0:
        if(role === "FW" || role === "Dev" || role === "Exe") {
          partition = role + "Issues"
        }
        else {
          return {result: "Failed", desc: "invalid role type"};
        }
        break;

        // VMs
        case 1:
        partition = "VM";
        break;

        // FW
        case 2:
        partition = "FW";
        break;

        // System
        case 3:
        partition = "System";
        break;

        // User
        case 4:
        partition = "User";
        break;

        default:
        return {result: "Failed", desc: "invalid type"};

      }

      //let dataStr = JSON.stringify(data);
      result = await this.connection.createEntity({partitionKey: partition, rowKey: id, ...data});

      console.log("Added data to " + partition + " with id: " + id + "\n");
      return {result: "Success", data: result};

    }
    catch(RestError) {
      if(RestError.details.odataError.code === "EntityAlreadyExists") {
        console.log("Failed to add issue to " + partition + " with id: " + id);
        console.log("entity already exists\n");
      }
      else {
        console.log("Failed to add issue to " + partition + " with id: " + id);
        console.log(RestError.details.odataError.message.value + "\n");
      }
      return {result: "Failed", data: RestError.message};
    }
  }

  /*
  desc: A general function to handle modifying data in the database using specific
        types. This will provide a quick way of modifying the data in a less friendly
        way, the other specific data functions will use this one to provide a
        friendly interface
  params: Int type : the type of data we want, like the partition key
          String id : the row key of the requested data, if not provided, all data
          from that partition will be fetched
          Int op? : the operation to perform, 0 for deletion, 1 for merge update,
                   2 for replace update
          String ?role : if the user is requesting issue data, a role must be provided
          Object ?mods : the modifications to perform in the DB if op is 1 or 2
  returns: A object containing the result of the operation
  */
  async modData(type, id, op = 0, role = -1, mods = null) {

    try {

      var partition = "";
      var result = null;
      var entity = {};

      switch (type) {

        // Issues
        case 0:
        if(role === "FW" || role === "Dev" || role === "Exe") {
          partition = role + "Issues"
        }
        else {
          return {result: "Failed", desc: "invalid role type"};
        }
        break;

        // VMs
        case 1:
        partition = "VM";
        break;

        // FW
        case 2:
        partition = "FW";
        break;

        // System
        case 3:
        partition = "System";
        break;

        // User
        case 4:
        partition = "User";
        break;

        default:
        return {result: "Failed", data: "invalid type"};

      }

      if(op === 0) {
        result = await this.connection.deleteEntity(partition, id);
        console.log("Removed issue in " + partition + " with id: " + id + "\n", result);
        return {result: "Success", data: result};
      }
      else if(op === 1) {
        entity = {partitionKey: partition, rowKey: id, ...mods};
        result = await this.connection.updateEntity(entity);
        return {result: "Success", data: result};
      }
      else if(op === 2) {
        entity = {partitionKey: partition, rowKey: id, ...mods};
        result = await this.connection.updateEntity(entity, "Replace");
        return {result: "Success", data: result};
      }
      else {
        console.log("Failed to modify issue in " + partition + " with id: " + id + "\n");
        return {result: "Failed", data: "Invalid Operation"};
      }

    }
    catch(RestError) {
      console.log("Failed to modify issue in " + partition + " with id: " + id + "\n");
      return {result: "Failed", data: RestError.message};
    }
  }

  /*
  Desc: requests all data about a given issue from the database using the issue
        id as a key.
  params: String role: user role, FW, Dev or Exe
          String id: the ID of the issue we want.
  returns: Promise Object: {
      "vm":
      "vmstatus":
      "owner":
      "vmlocation":
      "vmversion":
      "product":
      "status":
      "fwversion":
      "commitid":
      "form":
      "fwlocation":
      "serial":
    }
  */
  async getIssueByID(role, id) {
    return this.getData(0, role, id);
  }


  /*
  Desc: requests all data about all issues
  params: N/A
  returns: Promise Object: [List of issue objects]
  */
  async getAllIssues() {
    return this.getData(0);
  }

  /*
  Desc: Adds a new issue to the database.
  params: String role: user role, FW, Dev or Exe
  Object issue: {
  "id":
  "desc":
  "data":
    {
      "vm":
      "vmstatus":
      "owner":
      "vmlocation":
      "vmversion":
      "product":
      "status":
      "fwversion":
      "commitid":
      "form":
      "fwlocation":
      "serial":
    }
  }
  returns: Promise Object: {
      "result":
      "data":
    }
  */
  async addIssue(role, issue) {
    return this.addData(0, "" + issue.id, issue, role);
  }

  /*
  desc: Removes a issue from the database
  params: String role: user role for the issue (FW, Dev or Exe)
          String id: Jira id of the issue to remove
  returns: result of the deletion
  */
  async removeIssue(role, id) {
    return this.modData(0, "" + id, 0, role);
  }

  /*
  Desc: Gets data about a VM by using its ID as a key
  params: String id: ID of the VM we want
  returns: Promise Object: {
  TODO
  }
  */
  async getVMByID(id) {
    return this.getData(1, "", id);
  }

  /*
  Desc: Gets data about all VMs
  params: N/A
  returns: Promise Object: { [List of VM object] }
  */
  async getAllVMs() {
    return this.getData(1);
  }

  /*
  Desc: Adds a VM to the database
  params: Object vm: {
  TODO
  }
  returns: Promise Object: {
      "result":
      "desc":
    }
  */
  async addVM(vm) {
    return this.addData(1, vm.vm, vm);
  }
  /*

  */
  async removeVM(vm) {
    return this.modData(1, vm);
  }

  /*
  Desc: Gets data about a VM by using its ID as a key
  params: String id: ID of the VM we want
  returns: Promise Object: {
  TODO
  }
  */
  async getFWByID(id) {
    return this.getData(2, "", id + "");
  }

  /*
  Desc: Gets data about all VMs
  params: N/A
  returns: Promise Object: { [List of VM object] }
  */
  async getAllFWs() {
    return this.getData(2);
  }

  /*
  Desc: Adds a VM to the database
  params: Object vm: {
  TODO
  }
  returns: Promise Object: {
      "result":
      "desc":
    }
  */
  async addFW(fw) {
    return this.addData(2, fw.serial + "", fw);
  }

  /*

  */
  async removeFW(fw) {
    return this.modData(2, fw + "");
  }

  /*
  Desc: Adds a new reservation to the calendar, checks for conflicts
  params: Object: {
    "id":
    "vm":
    "startTime":
    "runTime":
  }
  returns: Promise Object: {
      "result":
      "desc":
    }
  */
  async addReservation(system, reservation) {
    let systemData = await this.getSystemByID(system);
    // console.log(systemData);
    let schedule = JSON.parse(systemData.data.schedule);
    let newSchedule = [];
    let startDate = (new Date(reservation.start)).getTime();
    let endDate = (new Date(reservation.end)).getTime();
    let resStart = {};
    let resEnd = {};

    for(let res of schedule) {
      if(res.vm == reservation.vm) {
        resStart = (new Date(res.start)).getTime();
        resEnd = (new Date(res.end)).getTime();
        if(((startDate >= resStart && startDate <= resEnd) ||
            (endDate >= resStart && endDate <= resEnd) ||
            (resStart >= startDate && resStart <= endDate) ||
            (resEnd >= startDate && resEnd <= endDate))) {

          console.log("Conflict\nnew res: " + startDate + "-" + endDate + "\n\
          old res: " + resStart + "-" + resEnd);
        }
        else {
          newSchedule.push(res);
        }
      }
      else {
        newSchedule.push(res);
      }
    }
    newSchedule.push(reservation);

    schedule = JSON.stringify(newSchedule);
    let result = await this.modData(3, system, 1, "", {schedule: schedule});
    return result;
  }

  /*
  Desc: Adds a new reservation to the calendar, checks for conflicts
  params: Object: {
    "id":
    "vm":
    "startTime":
    "runTime":
  }
  returns: Promise Object: {
      "result":
      "desc":
    }
  */
  async removeOldReservations(system) {
    let systemData = await this.getData(3, "", system);
    let schedule = JSON.parse(systemData.data.schedule);
    let newSchedule = [];

    for(const res of schedule) {
      if(res != null) {
        console.log(new Date(res.end).getTime(), new Date().getTime());
        if(new Date(res.end).getTime() > new Date().getTime()) {
          newSchedule.push(res);
        }
        else {
          console.log("Removing old res that ended on: " + res.end);
        }
      }
    }

    newSchedule = JSON.stringify(newSchedule);

    let result = await this.modData(3, system, 1, "", {schedule: newSchedule});
    return result;
  }

  /*

  */
  async getSystemByID(id) {
    return this.getData(3, "", id);
  }

  /*

  */
  async getAllSystems() {
    return this.getData(3);
  }

  /*
  Desc: Adds a System to the database
  params: Object system: {
    id: "VICE1",
    schedule: []
  }
  returns: Promise Object: {
      "result":
      "data":
    }
  */
  async addSystem(system) {
    return this.addData(3, system.id, system);
  }

  /*

  */
  async removeSystem(id) {
    return this.modData(3, id);
  }

  /*

  */
  async getUserByID(id) {
    return this.getData(4, "", id);
  }

  /*

  */
  async addUser(user) {
    return this.addData(4, user.id, user);
  }

  /*

  */
  async removeUser(id) {
    return this.modData(4, id);
  }


}