RPC frontend for libvirt #1
52
client.py
Executable file
52
client.py
Executable file
|
@ -0,0 +1,52 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class RPCClient:
|
||||||
|
"""
|
||||||
|
RPCClient Class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_vm(self, **kwargs):
|
||||||
|
|
||||||
|
url = "http://127.0.0.1:8080/create?"
|
||||||
|
if kwargs:
|
||||||
|
q = ""
|
||||||
|
for k in kwargs.keys():
|
||||||
|
q += "&{}={}".format(k, kwargs[k])
|
||||||
|
|
||||||
|
url += q
|
||||||
|
|
||||||
|
print(url)
|
||||||
|
resp = requests.get(url)
|
||||||
|
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
def start_vm(self, vm):
|
||||||
|
"""get a vm and try starting it
|
||||||
|
return the ID or appropriate message
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop_vm(self, id):
|
||||||
|
# stops vm of a given id
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import random
|
||||||
|
|
||||||
|
client = RPCClient()
|
||||||
|
print("Creating VM...")
|
||||||
|
name = "CentOS-8-Demo-{}-{}".format(
|
||||||
|
random.sample("abcdefghijklmn", 1)[0], random.sample(range(10), 1)[0]
|
||||||
|
)
|
||||||
|
iso_src = "/home/coolguy/Downloads/CentOS-8.2.2004-x86_64-boot.iso"
|
||||||
|
img_src = "/var/lib/libvirt/images/vol.img"
|
||||||
|
|
||||||
|
vm_info = client.create_vm(name=name, iso_source=iso_src, img_source=img_src)
|
||||||
|
if vm_info["status"]:
|
||||||
|
print("\n\nGuest '{}' has been created and booted".format(name))
|
||||||
|
print("UUID for {}: {}".format(name, vm_info["uuid"]))
|
||||||
|
print("ID for {} : {}".format(name, vm_info["id"]))
|
||||||
|
else:
|
||||||
|
print(vm_info["err"])
|
7
config.ini
Executable file
7
config.ini
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
[host]
|
||||||
|
# enter comma separated hosts
|
||||||
|
available_hosts = 'qemu:///system', "qemu:///session"
|
||||||
|
|
||||||
|
[server]
|
||||||
|
server_host = 127.0.0.1
|
||||||
|
server_port = 8080
|
9
consumer.sql
Executable file
9
consumer.sql
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
-- This table will store consumer/worker info/configurations
|
||||||
|
|
||||||
|
-- some info about workers
|
||||||
|
CREATE TABLE IF NOT EXISTS worker(
|
||||||
|
worker_id INT UNIQUE NOT NULL,
|
||||||
|
status VARCHAR NOT NULL,
|
||||||
|
--OTHER_DATA
|
||||||
|
|
||||||
|
)
|
199
database.py
Executable file
199
database.py
Executable file
|
@ -0,0 +1,199 @@
|
||||||
|
import sqlite3
|
||||||
|
from sqlite3 import Error
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self):
|
||||||
|
if not os.path.exists("database/"):
|
||||||
|
os.makedirs("database/")
|
||||||
|
|
||||||
|
# .db file
|
||||||
|
self.db = ""
|
||||||
|
|
||||||
|
# Insert into database
|
||||||
|
def insert_into_database(self, tableName, conn, data):
|
||||||
|
|
||||||
|
if conn is not None:
|
||||||
|
try:
|
||||||
|
c = conn.execute("select * from {}".format(tableName))
|
||||||
|
fields = tuple([des[0] for des in c.description][:])
|
||||||
|
|
||||||
|
if "id" in fields:
|
||||||
|
fields = tuple(list(fields)[1:])
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO {}
|
||||||
|
{} VALUES {}
|
||||||
|
""".format(
|
||||||
|
tableName, fields, data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Error:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Update Database
|
||||||
|
def update_database(self, tableName, conn, fields, field_vals, ref, index):
|
||||||
|
if conn is not None:
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
if not isinstance(fields, tuple) and not isinstance(fields, list):
|
||||||
|
fields = list([fields])
|
||||||
|
field_vals = list([field_vals])
|
||||||
|
|
||||||
|
for field, field_val in zip(fields, field_vals):
|
||||||
|
# print(field,field_val)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE {}
|
||||||
|
SET {}= ? WHERE {}= ?
|
||||||
|
""".format(
|
||||||
|
tableName, field, ref
|
||||||
|
),
|
||||||
|
(field_val, index),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# print("Error in updating data: {}".format(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Delete from Database
|
||||||
|
def delete_from_database(self, conn, tableName, condition):
|
||||||
|
if conn is not None:
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
# just to track if deletion was successful
|
||||||
|
count = len(
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM {} WHERE {}
|
||||||
|
""".format(
|
||||||
|
tableName, condition
|
||||||
|
)
|
||||||
|
).fetchall()
|
||||||
|
)
|
||||||
|
if not count:
|
||||||
|
return False
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM {} WHERE {}
|
||||||
|
""".format(
|
||||||
|
tableName, condition
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Error:
|
||||||
|
pass
|
||||||
|
# print("Error in deleting data: {}".format(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Search in the database
|
||||||
|
def search_from_database(self, tableName, conn, prop, value, order_by="id"):
|
||||||
|
if conn is not None:
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
# print("cur: {}".format(cur))
|
||||||
|
filtered_list = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM {} WHERE {} LIKE ? ORDER BY {};
|
||||||
|
""".format(
|
||||||
|
tableName, prop, order_by
|
||||||
|
),
|
||||||
|
(str(value) + "%",),
|
||||||
|
).fetchall()
|
||||||
|
return filtered_list
|
||||||
|
except Error:
|
||||||
|
pass
|
||||||
|
# print("Error in searching: {}".format(e))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search_from_database_many(self, tableName, conn, condition):
|
||||||
|
if conn is not None:
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
filtered_list = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM {} WHERE {}
|
||||||
|
""".format(
|
||||||
|
tableName, condition
|
||||||
|
)
|
||||||
|
).fetchall()
|
||||||
|
return filtered_list
|
||||||
|
except Error:
|
||||||
|
pass
|
||||||
|
# print("Error in deleting data: {}".format(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# connect database
|
||||||
|
def connect_database(self, db_file):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect("database/" + db_file)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
except Error:
|
||||||
|
pass
|
||||||
|
# print("Error in database: {}".format(e))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# create table
|
||||||
|
def create_table(self, conn, table):
|
||||||
|
|
||||||
|
if conn is not None:
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(table)
|
||||||
|
except Error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def delete_table(self, conn, db_file, table_name):
|
||||||
|
|
||||||
|
if conn is not None:
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute("DROP TABLE {}".format(table_name))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def extract_all_data(self, conn, db_file, tableName, order_by="id"):
|
||||||
|
|
||||||
|
if conn is not None:
|
||||||
|
cur = conn.execute(
|
||||||
|
"SELECT * FROM {} ORDER BY {}".format(tableName, order_by)
|
||||||
|
)
|
||||||
|
data = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_all_data(self, conn, db_file, tableName):
|
||||||
|
|
||||||
|
if conn is not None:
|
||||||
|
# conn.commit()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM {};
|
||||||
|
""".format(
|
||||||
|
tableName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pass
|
51
pc.py
Executable file
51
pc.py
Executable file
|
@ -0,0 +1,51 @@
|
||||||
|
from threading import Thread, Condition
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
|
queue = []
|
||||||
|
BUFF_SIZE = 10
|
||||||
|
condition = Condition()
|
||||||
|
|
||||||
|
|
||||||
|
class ProducerThread(Thread):
|
||||||
|
def run(self):
|
||||||
|
nums = range(5)
|
||||||
|
global queue
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
condition.acquire()
|
||||||
|
if len(queue) == BUFF_SIZE:
|
||||||
|
# Queue full, producer is waiting
|
||||||
|
condition.wait()
|
||||||
|
# Space in queue, Consumer notified the producer
|
||||||
|
|
||||||
|
num = random.choice(nums)
|
||||||
|
queue.append(num)
|
||||||
|
print("Produced ", num)
|
||||||
|
condition.notify()
|
||||||
|
condition.release()
|
||||||
|
time.sleep(random.random() * 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsumerThread(Thread):
|
||||||
|
def run(self):
|
||||||
|
global queue
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
condition.acquire()
|
||||||
|
if not queue:
|
||||||
|
# Nothing in queue, consumer is waiting
|
||||||
|
condition.wait()
|
||||||
|
# Producer added something to queue and notified the consumer
|
||||||
|
|
||||||
|
num = queue.pop(0)
|
||||||
|
print("Consumed ", num)
|
||||||
|
condition.notify()
|
||||||
|
condition.release()
|
||||||
|
time.sleep(random.random() * 2)
|
||||||
|
|
||||||
|
|
||||||
|
ProducerThread().start()
|
||||||
|
ConsumerThread().start()
|
10
queue.sql
Executable file
10
queue.sql
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
-- This table will store processes --
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS {}(
|
||||||
|
worker_id INT UNIQUE NOT NULL,
|
||||||
|
start DATETIME NOT NULL, -- timestamps
|
||||||
|
end DATETIME NOT NULL,
|
||||||
|
status VARCHAR NOT NULL,
|
||||||
|
--OTHER_VM_DATA
|
||||||
|
|
||||||
|
)
|
5
tags.sql
Normal file
5
tags.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS tags(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
tag VARCHAR,
|
||||||
|
|
||||||
|
)
|
6
templates/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
6
templates/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
templates/assets/bootstrap/js/bootstrap.min.js
vendored
Normal file
7
templates/assets/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
17
templates/assets/css/Bootstrap-4---Table-Fixed-Header.css
Normal file
17
templates/assets/css/Bootstrap-4---Table-Fixed-Header.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #74ebd5;
|
||||||
|
background: -webkit-linear-gradient(to right, #74ebd5, #ACB6E5);
|
||||||
|
background: linear-gradient(to right, #74ebd5, #ACB6E5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableFixHead{
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableFixHead thead th{
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
55
templates/assets/css/bootstrap-tagsinput.css
vendored
Normal file
55
templates/assets/css/bootstrap-tagsinput.css
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
.bootstrap-tagsinput {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 6px;
|
||||||
|
color: #555;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 100%;
|
||||||
|
line-height: 22px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput input {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
max-width: inherit;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput.form-control input::-moz-placeholder {
|
||||||
|
color: #777;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput.form-control input:-ms-input-placeholder {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput.form-control input::-webkit-input-placeholder {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput input:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput .tag {
|
||||||
|
margin-right: 2px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput .tag [data-role="remove"] {
|
||||||
|
margin-left: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput .tag [data-role="remove"]:after {
|
||||||
|
content: "x";
|
||||||
|
padding: 0px 2px;
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput .tag [data-role="remove"]:hover {
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.bootstrap-tagsinput .tag [data-role="remove"]:hover:active {
|
||||||
|
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||||
|
}
|
646
templates/assets/js/bootstrap-tagsinput.js
vendored
Normal file
646
templates/assets/js/bootstrap-tagsinput.js
vendored
Normal file
|
@ -0,0 +1,646 @@
|
||||||
|
(function ($) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var defaultOptions = {
|
||||||
|
tagClass: function(item) {
|
||||||
|
return 'label label-info';
|
||||||
|
},
|
||||||
|
itemValue: function(item) {
|
||||||
|
return item ? item.toString() : item;
|
||||||
|
},
|
||||||
|
itemText: function(item) {
|
||||||
|
return this.itemValue(item);
|
||||||
|
},
|
||||||
|
itemTitle: function(item) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
freeInput: true,
|
||||||
|
addOnBlur: true,
|
||||||
|
maxTags: undefined,
|
||||||
|
maxChars: undefined,
|
||||||
|
confirmKeys: [13, 44],
|
||||||
|
delimiter: ',',
|
||||||
|
delimiterRegex: null,
|
||||||
|
cancelConfirmKeysOnEmpty: true,
|
||||||
|
onTagExists: function(item, $tag) {
|
||||||
|
$tag.hide().fadeIn();
|
||||||
|
},
|
||||||
|
trimValue: false,
|
||||||
|
allowDuplicates: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor function
|
||||||
|
*/
|
||||||
|
function TagsInput(element, options) {
|
||||||
|
this.itemsArray = [];
|
||||||
|
|
||||||
|
this.$element = $(element);
|
||||||
|
this.$element.hide();
|
||||||
|
|
||||||
|
this.isSelect = (element.tagName === 'SELECT');
|
||||||
|
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
|
||||||
|
this.objectItems = options && options.itemValue;
|
||||||
|
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
|
||||||
|
this.inputSize = Math.max(1, this.placeholderText.length);
|
||||||
|
|
||||||
|
this.$container = $('<div class="bootstrap-tagsinput"></div>');
|
||||||
|
this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
|
||||||
|
|
||||||
|
this.$element.before(this.$container);
|
||||||
|
|
||||||
|
this.build(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
TagsInput.prototype = {
|
||||||
|
constructor: TagsInput,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
|
||||||
|
* updating the elements val()
|
||||||
|
*/
|
||||||
|
add: function(item, dontPushVal, options) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ignore falsey values, except false
|
||||||
|
if (item !== false && !item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Trim value
|
||||||
|
if (typeof item === "string" && self.options.trimValue) {
|
||||||
|
item = $.trim(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw an error when trying to add an object while the itemValue option was not set
|
||||||
|
if (typeof item === "object" && !self.objectItems)
|
||||||
|
throw("Can't add objects when itemValue option is not set");
|
||||||
|
|
||||||
|
// Ignore strings only containg whitespace
|
||||||
|
if (item.toString().match(/^\s*$/))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If SELECT but not multiple, remove current tag
|
||||||
|
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
|
||||||
|
self.remove(self.itemsArray[0]);
|
||||||
|
|
||||||
|
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
|
||||||
|
var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter;
|
||||||
|
var items = item.split(delimiter);
|
||||||
|
if (items.length > 1) {
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
this.add(items[i], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dontPushVal)
|
||||||
|
self.pushVal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemValue = self.options.itemValue(item),
|
||||||
|
itemText = self.options.itemText(item),
|
||||||
|
tagClass = self.options.tagClass(item),
|
||||||
|
itemTitle = self.options.itemTitle(item);
|
||||||
|
|
||||||
|
// Ignore items allready added
|
||||||
|
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
|
||||||
|
if (existing && !self.options.allowDuplicates) {
|
||||||
|
// Invoke onTagExists
|
||||||
|
if (self.options.onTagExists) {
|
||||||
|
var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
|
||||||
|
self.options.onTagExists(item, $existingTag);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if length greater than limit
|
||||||
|
if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// raise beforeItemAdd arg
|
||||||
|
var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
|
||||||
|
self.$element.trigger(beforeItemAddEvent);
|
||||||
|
if (beforeItemAddEvent.cancel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// register item in internal array and map
|
||||||
|
self.itemsArray.push(item);
|
||||||
|
|
||||||
|
// add a tag element
|
||||||
|
|
||||||
|
var $tag = $('<span class="tag ' + htmlEncode(tagClass) + (itemTitle !== null ? ('" title="' + itemTitle) : '') + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
|
||||||
|
$tag.data('item', item);
|
||||||
|
self.findInputWrapper().before($tag);
|
||||||
|
$tag.after(' ');
|
||||||
|
|
||||||
|
// add <option /> if item represents a value not present in one of the <select />'s options
|
||||||
|
if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) {
|
||||||
|
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
|
||||||
|
$option.data('item', item);
|
||||||
|
$option.attr('value', itemValue);
|
||||||
|
self.$element.append($option);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dontPushVal)
|
||||||
|
self.pushVal();
|
||||||
|
|
||||||
|
// Add class when reached maxTags
|
||||||
|
if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
|
||||||
|
self.$container.addClass('bootstrap-tagsinput-max');
|
||||||
|
|
||||||
|
self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given item. Pass true to dontPushVal to prevent updating the
|
||||||
|
* elements val()
|
||||||
|
*/
|
||||||
|
remove: function(item, dontPushVal, options) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (self.objectItems) {
|
||||||
|
if (typeof item === "object")
|
||||||
|
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
|
||||||
|
else
|
||||||
|
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
|
||||||
|
|
||||||
|
item = item[item.length-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
|
||||||
|
self.$element.trigger(beforeItemRemoveEvent);
|
||||||
|
if (beforeItemRemoveEvent.cancel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
|
||||||
|
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
|
||||||
|
if($.inArray(item, self.itemsArray) !== -1)
|
||||||
|
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dontPushVal)
|
||||||
|
self.pushVal();
|
||||||
|
|
||||||
|
// Remove class when reached maxTags
|
||||||
|
if (self.options.maxTags > self.itemsArray.length)
|
||||||
|
self.$container.removeClass('bootstrap-tagsinput-max');
|
||||||
|
|
||||||
|
self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all items
|
||||||
|
*/
|
||||||
|
removeAll: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
$('.tag', self.$container).remove();
|
||||||
|
$('option', self.$element).remove();
|
||||||
|
|
||||||
|
while(self.itemsArray.length > 0)
|
||||||
|
self.itemsArray.pop();
|
||||||
|
|
||||||
|
self.pushVal();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the tags so they match the text/value of their corresponding
|
||||||
|
* item.
|
||||||
|
*/
|
||||||
|
refresh: function() {
|
||||||
|
var self = this;
|
||||||
|
$('.tag', self.$container).each(function() {
|
||||||
|
var $tag = $(this),
|
||||||
|
item = $tag.data('item'),
|
||||||
|
itemValue = self.options.itemValue(item),
|
||||||
|
itemText = self.options.itemText(item),
|
||||||
|
tagClass = self.options.tagClass(item);
|
||||||
|
|
||||||
|
// Update tag's class and inner text
|
||||||
|
$tag.attr('class', null);
|
||||||
|
$tag.addClass('tag ' + htmlEncode(tagClass));
|
||||||
|
$tag.contents().filter(function() {
|
||||||
|
return this.nodeType == 3;
|
||||||
|
})[0].nodeValue = htmlEncode(itemText);
|
||||||
|
|
||||||
|
if (self.isSelect) {
|
||||||
|
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
|
||||||
|
option.attr('value', itemValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the items added as tags
|
||||||
|
*/
|
||||||
|
items: function() {
|
||||||
|
return this.itemsArray;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assembly value by retrieving the value of each item, and set it on the
|
||||||
|
* element.
|
||||||
|
*/
|
||||||
|
pushVal: function() {
|
||||||
|
var self = this,
|
||||||
|
val = $.map(self.items(), function(item) {
|
||||||
|
return self.options.itemValue(item).toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.$element.val(val, true).trigger('change');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the tags input behaviour on the element
|
||||||
|
*/
|
||||||
|
build: function(options) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.options = $.extend({}, defaultOptions, options);
|
||||||
|
// When itemValue is set, freeInput should always be false
|
||||||
|
if (self.objectItems)
|
||||||
|
self.options.freeInput = false;
|
||||||
|
|
||||||
|
makeOptionItemFunction(self.options, 'itemValue');
|
||||||
|
makeOptionItemFunction(self.options, 'itemText');
|
||||||
|
makeOptionFunction(self.options, 'tagClass');
|
||||||
|
|
||||||
|
// Typeahead Bootstrap version 2.3.2
|
||||||
|
if (self.options.typeahead) {
|
||||||
|
var typeahead = self.options.typeahead || {};
|
||||||
|
|
||||||
|
makeOptionFunction(typeahead, 'source');
|
||||||
|
|
||||||
|
self.$input.typeahead($.extend({}, typeahead, {
|
||||||
|
source: function (query, process) {
|
||||||
|
function processItems(items) {
|
||||||
|
var texts = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var text = self.options.itemText(items[i]);
|
||||||
|
map[text] = items[i];
|
||||||
|
texts.push(text);
|
||||||
|
}
|
||||||
|
process(texts);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map = {};
|
||||||
|
var map = this.map,
|
||||||
|
data = typeahead.source(query);
|
||||||
|
|
||||||
|
if ($.isFunction(data.success)) {
|
||||||
|
// support for Angular callbacks
|
||||||
|
data.success(processItems);
|
||||||
|
} else if ($.isFunction(data.then)) {
|
||||||
|
// support for Angular promises
|
||||||
|
data.then(processItems);
|
||||||
|
} else {
|
||||||
|
// support for functions and jquery promises
|
||||||
|
$.when(data)
|
||||||
|
.then(processItems);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updater: function (text) {
|
||||||
|
self.add(this.map[text]);
|
||||||
|
return this.map[text];
|
||||||
|
},
|
||||||
|
matcher: function (text) {
|
||||||
|
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
|
||||||
|
},
|
||||||
|
sorter: function (texts) {
|
||||||
|
return texts.sort();
|
||||||
|
},
|
||||||
|
highlighter: function (text) {
|
||||||
|
var regex = new RegExp( '(' + this.query + ')', 'gi' );
|
||||||
|
return text.replace( regex, "<strong>$1</strong>" );
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeahead.js
|
||||||
|
if (self.options.typeaheadjs) {
|
||||||
|
var typeaheadConfig = null;
|
||||||
|
var typeaheadDatasets = {};
|
||||||
|
|
||||||
|
// Determine if main configurations were passed or simply a dataset
|
||||||
|
var typeaheadjs = self.options.typeaheadjs;
|
||||||
|
if ($.isArray(typeaheadjs)) {
|
||||||
|
typeaheadConfig = typeaheadjs[0];
|
||||||
|
typeaheadDatasets = typeaheadjs[1];
|
||||||
|
} else {
|
||||||
|
typeaheadDatasets = typeaheadjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.$input.typeahead(typeaheadConfig, typeaheadDatasets).on('typeahead:selected', $.proxy(function (obj, datum) {
|
||||||
|
if (typeaheadDatasets.valueKey)
|
||||||
|
self.add(datum[typeaheadDatasets.valueKey]);
|
||||||
|
else
|
||||||
|
self.add(datum);
|
||||||
|
self.$input.typeahead('val', '');
|
||||||
|
}, self));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.$container.on('click', $.proxy(function(event) {
|
||||||
|
if (! self.$element.attr('disabled')) {
|
||||||
|
self.$input.removeAttr('disabled');
|
||||||
|
}
|
||||||
|
self.$input.focus();
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
if (self.options.addOnBlur && self.options.freeInput) {
|
||||||
|
self.$input.on('focusout', $.proxy(function(event) {
|
||||||
|
// HACK: only process on focusout when no typeahead opened, to
|
||||||
|
// avoid adding the typeahead text as tag
|
||||||
|
if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
|
||||||
|
self.add(self.$input.val());
|
||||||
|
self.$input.val('');
|
||||||
|
}
|
||||||
|
}, self));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
self.$container.on('keydown', 'input', $.proxy(function(event) {
|
||||||
|
var $input = $(event.target),
|
||||||
|
$inputWrapper = self.findInputWrapper();
|
||||||
|
|
||||||
|
if (self.$element.attr('disabled')) {
|
||||||
|
self.$input.attr('disabled', 'disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.which) {
|
||||||
|
// BACKSPACE
|
||||||
|
case 8:
|
||||||
|
if (doGetCaretPosition($input[0]) === 0) {
|
||||||
|
var prev = $inputWrapper.prev();
|
||||||
|
if (prev.length) {
|
||||||
|
self.remove(prev.data('item'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
case 46:
|
||||||
|
if (doGetCaretPosition($input[0]) === 0) {
|
||||||
|
var next = $inputWrapper.next();
|
||||||
|
if (next.length) {
|
||||||
|
self.remove(next.data('item'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// LEFT ARROW
|
||||||
|
case 37:
|
||||||
|
// Try to move the input before the previous tag
|
||||||
|
var $prevTag = $inputWrapper.prev();
|
||||||
|
if ($input.val().length === 0 && $prevTag[0]) {
|
||||||
|
$prevTag.before($inputWrapper);
|
||||||
|
$input.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// RIGHT ARROW
|
||||||
|
case 39:
|
||||||
|
// Try to move the input after the next tag
|
||||||
|
var $nextTag = $inputWrapper.next();
|
||||||
|
if ($input.val().length === 0 && $nextTag[0]) {
|
||||||
|
$nextTag.after($inputWrapper);
|
||||||
|
$input.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset internal input's size
|
||||||
|
var textLength = $input.val().length,
|
||||||
|
wordSpace = Math.ceil(textLength / 5),
|
||||||
|
size = textLength + wordSpace + 1;
|
||||||
|
$input.attr('size', Math.max(this.inputSize, $input.val().length));
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
self.$container.on('keypress', 'input', $.proxy(function(event) {
|
||||||
|
var $input = $(event.target);
|
||||||
|
|
||||||
|
if (self.$element.attr('disabled')) {
|
||||||
|
self.$input.attr('disabled', 'disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = $input.val(),
|
||||||
|
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
|
||||||
|
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
|
||||||
|
// Only attempt to add a tag if there is data in the field
|
||||||
|
if (text.length !== 0) {
|
||||||
|
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
|
||||||
|
$input.val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field is empty, let the event triggered fire as usual
|
||||||
|
if (self.options.cancelConfirmKeysOnEmpty === false) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset internal input's size
|
||||||
|
var textLength = $input.val().length,
|
||||||
|
wordSpace = Math.ceil(textLength / 5),
|
||||||
|
size = textLength + wordSpace + 1;
|
||||||
|
$input.attr('size', Math.max(this.inputSize, $input.val().length));
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
// Remove icon clicked
|
||||||
|
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
||||||
|
if (self.$element.attr('disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.remove($(event.target).closest('.tag').data('item'));
|
||||||
|
}, self));
|
||||||
|
|
||||||
|
// Only add existing value as tags when using strings as tags
|
||||||
|
if (self.options.itemValue === defaultOptions.itemValue) {
|
||||||
|
if (self.$element[0].tagName === 'INPUT') {
|
||||||
|
self.add(self.$element.val());
|
||||||
|
} else {
|
||||||
|
$('option', self.$element).each(function() {
|
||||||
|
self.add($(this).attr('value'), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all tagsinput behaviour and unregsiter all event handlers
|
||||||
|
*/
|
||||||
|
destroy: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Unbind events
|
||||||
|
self.$container.off('keypress', 'input');
|
||||||
|
self.$container.off('click', '[role=remove]');
|
||||||
|
|
||||||
|
self.$container.remove();
|
||||||
|
self.$element.removeData('tagsinput');
|
||||||
|
self.$element.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets focus on the tagsinput
|
||||||
|
*/
|
||||||
|
focus: function() {
|
||||||
|
this.$input.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the internal input element
|
||||||
|
*/
|
||||||
|
input: function() {
|
||||||
|
return this.$input;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the element which is wrapped around the internal input. This
|
||||||
|
* is normally the $container, but typeahead.js moves the $input element.
|
||||||
|
*/
|
||||||
|
findInputWrapper: function() {
|
||||||
|
var elt = this.$input[0],
|
||||||
|
container = this.$container[0];
|
||||||
|
while(elt && elt.parentNode !== container)
|
||||||
|
elt = elt.parentNode;
|
||||||
|
|
||||||
|
return $(elt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register JQuery plugin
|
||||||
|
*/
|
||||||
|
$.fn.tagsinput = function(arg1, arg2, arg3) {
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
this.each(function() {
|
||||||
|
var tagsinput = $(this).data('tagsinput');
|
||||||
|
// Initialize a new tags input
|
||||||
|
if (!tagsinput) {
|
||||||
|
tagsinput = new TagsInput(this, arg1);
|
||||||
|
$(this).data('tagsinput', tagsinput);
|
||||||
|
results.push(tagsinput);
|
||||||
|
|
||||||
|
if (this.tagName === 'SELECT') {
|
||||||
|
$('option', $(this)).attr('selected', 'selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init tags from $(this).val()
|
||||||
|
$(this).val($(this).val());
|
||||||
|
} else if (!arg1 && !arg2) {
|
||||||
|
// tagsinput already exists
|
||||||
|
// no function, trying to init
|
||||||
|
results.push(tagsinput);
|
||||||
|
} else if(tagsinput[arg1] !== undefined) {
|
||||||
|
// Invoke function on existing tags input
|
||||||
|
if(tagsinput[arg1].length === 3 && arg3 !== undefined){
|
||||||
|
var retVal = tagsinput[arg1](arg2, null, arg3);
|
||||||
|
}else{
|
||||||
|
var retVal = tagsinput[arg1](arg2);
|
||||||
|
}
|
||||||
|
if (retVal !== undefined)
|
||||||
|
results.push(retVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ( typeof arg1 == 'string') {
|
||||||
|
// Return the results from the invoked function calls
|
||||||
|
return results.length > 1 ? results : results[0];
|
||||||
|
} else {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.tagsinput.Constructor = TagsInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most options support both a string or number as well as a function as
|
||||||
|
* option value. This function makes sure that the option with the given
|
||||||
|
* key in the given options is wrapped in a function
|
||||||
|
*/
|
||||||
|
function makeOptionItemFunction(options, key) {
|
||||||
|
if (typeof options[key] !== 'function') {
|
||||||
|
var propertyName = options[key];
|
||||||
|
options[key] = function(item) { return item[propertyName]; };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function makeOptionFunction(options, key) {
|
||||||
|
if (typeof options[key] !== 'function') {
|
||||||
|
var value = options[key];
|
||||||
|
options[key] = function() { return value; };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* HtmlEncodes the given value
|
||||||
|
*/
|
||||||
|
var htmlEncodeContainer = $('<div />');
|
||||||
|
function htmlEncode(value) {
|
||||||
|
if (value) {
|
||||||
|
return htmlEncodeContainer.text(value).html();
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the position of the caret in the given input field
|
||||||
|
* http://flightschool.acylt.com/devnotes/caret-position-woes/
|
||||||
|
*/
|
||||||
|
function doGetCaretPosition(oField) {
|
||||||
|
var iCaretPos = 0;
|
||||||
|
if (document.selection) {
|
||||||
|
oField.focus ();
|
||||||
|
var oSel = document.selection.createRange();
|
||||||
|
oSel.moveStart ('character', -oField.value.length);
|
||||||
|
iCaretPos = oSel.text.length;
|
||||||
|
} else if (oField.selectionStart || oField.selectionStart == '0') {
|
||||||
|
iCaretPos = oField.selectionStart;
|
||||||
|
}
|
||||||
|
return (iCaretPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns boolean indicates whether user has pressed an expected key combination.
|
||||||
|
* @param object keyPressEvent: JavaScript event object, refer
|
||||||
|
* http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
|
||||||
|
* @param object lookupList: expected key combinations, as in:
|
||||||
|
* [13, {which: 188, shiftKey: true}]
|
||||||
|
*/
|
||||||
|
function keyCombinationInList(keyPressEvent, lookupList) {
|
||||||
|
var found = false;
|
||||||
|
$.each(lookupList, function (index, keyCombination) {
|
||||||
|
if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
|
||||||
|
found = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyPressEvent.which === keyCombination.which) {
|
||||||
|
var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
|
||||||
|
shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
|
||||||
|
ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
|
||||||
|
if (alt && shift && ctrl) {
|
||||||
|
found = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize tagsinput behaviour on inputs and selects which have
|
||||||
|
* data-role=tagsinput
|
||||||
|
*/
|
||||||
|
$(function() {
|
||||||
|
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
|
||||||
|
});
|
||||||
|
})(window.jQuery);
|
2
templates/assets/js/jquery.min.js
vendored
Normal file
2
templates/assets/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
37
templates/config.tmpl.xml
Normal file
37
templates/config.tmpl.xml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<domain type="{{ args.type }}">
|
||||||
|
<name>{{ args.name }}</name>
|
||||||
|
<uuid>{{ args.uuid }}</uuid>
|
||||||
|
<memory unit="{{ args.memory_unit }}">{{ args.memory }}</memory>
|
||||||
|
<currentMemory unit="{{ args.memory_unit }}">{{ args.current_memory }}</currentMemory>
|
||||||
|
<vcpu placement="static">{{ args.vcpu }}</vcpu>
|
||||||
|
<os>
|
||||||
|
<type arch='{{ args.arch }}' machine='{{ args.machine }}'>hvm</type>
|
||||||
|
<!--boot dev='hd'/-->
|
||||||
|
<boot dev='cdrom'/>
|
||||||
|
</os>
|
||||||
|
<clock offset='{{ args.offset }}'/>
|
||||||
|
<on_poweroff>destroy</on_poweroff>
|
||||||
|
<on_reboot>restart</on_reboot>
|
||||||
|
<on_crash>destroy</on_crash>
|
||||||
|
<devices>
|
||||||
|
<emulator>{{ args.emulator }}</emulator>
|
||||||
|
<disk type="file" device="disk">
|
||||||
|
<driver name="qemu" type="raw"/>
|
||||||
|
<source file="{{ args.img_source }}"/>
|
||||||
|
<target dev="vda" bus="virtio"/>
|
||||||
|
</disk>
|
||||||
|
<disk type='file' device='cdrom'>
|
||||||
|
<source file='{{ args.iso_source }}'/>
|
||||||
|
<target dev='sda' bus='sata'/>
|
||||||
|
</disk>
|
||||||
|
<interface type="network">
|
||||||
|
<mac address="52:54:00:b6:8e:76"/>
|
||||||
|
<source network="default"/>
|
||||||
|
<model type="virtio"/>
|
||||||
|
<address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0"/>
|
||||||
|
</interface>
|
||||||
|
<!--graphics will be removed after testing as we probably won't need this-->
|
||||||
|
<graphics type='vnc' port='-1' listen='127.0.0.1'/>
|
||||||
|
</devices>
|
||||||
|
|
||||||
|
</domain>
|
81
templates/guest.html
Normal file
81
templates/guest.html
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
|
<title>Guests Info</title>
|
||||||
|
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/Bootstrap-4---Table-Fixed-Header.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="col ">
|
||||||
|
<h3 class="text-center">Guest Detail</h4>
|
||||||
|
<hr>
|
||||||
|
<div class="tableFixHead">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">S.No.</th>
|
||||||
|
<th scope="col">Guest Name</th>
|
||||||
|
<th scope="col">Host Name</th>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">Allocated<br>Memory(MiB)</th>
|
||||||
|
<th scope="col">Allocated<br>CPUs</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">UUID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="table-body"></tbody>
|
||||||
|
|
||||||
|
{% if guest_data %}
|
||||||
|
{% for data in guest_data %}
|
||||||
|
<tr id="${{ data.guestname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.guestname }}</td>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.id_ }}</td>
|
||||||
|
<td>{{ data.mem }}</td>
|
||||||
|
<td>{{ data.cpu }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Active" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uuid }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${{ data.guestname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.guestname }}</td>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.id_ }}</td>
|
||||||
|
<td>{{ data.mem }}</td>
|
||||||
|
<td>{{ data.cpu }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Active" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uuid }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${{ data.guestname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.guestname }}</td>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.id_ }}</td>
|
||||||
|
<td>{{ data.mem }}</td>
|
||||||
|
<td>{{ data.cpu }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Active" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uuid }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
|
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
182
templates/host.html
Normal file
182
templates/host.html
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
|
<title>Hosts Info</title>
|
||||||
|
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_PREFIX }}assets/css/Bootstrap-4---Table-Fixed-Header.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="col ">
|
||||||
|
<h3 class="text-center">Host Detail</h4>
|
||||||
|
<hr>
|
||||||
|
<div class="tableFixHead">
|
||||||
|
<table class="table">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">S.No.</th>
|
||||||
|
<th scope="col">Host Name</th>
|
||||||
|
<th scope="col">CPUs<br>(Total / Free)</th>
|
||||||
|
<th scope="col">Memory(MiB)<br>(Total / Free)</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">URI</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="table-body"></tbody>
|
||||||
|
|
||||||
|
<!--tr id="${data.hostname}">
|
||||||
|
<th scope="row">1</th>
|
||||||
|
<td>CentOS-8-Demo-a-1</td>
|
||||||
|
<td>4 / 3</td>
|
||||||
|
<td>3800 / 2800</td>
|
||||||
|
<td id="host-stat" class="text-success"><b>Alive</b></td>
|
||||||
|
<td>qemu:///system</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${data.hostname}">
|
||||||
|
<th scope="row">2</th>
|
||||||
|
<td>CentOS-8-Demo-b-2</td>
|
||||||
|
<td>4 / 2</td>
|
||||||
|
<td>3800 / 3800</td>
|
||||||
|
<td id="host-stat" class="text-success"><b>Alive</b></td>
|
||||||
|
<td>qemu+ssh:///system/something/user</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${data.hostname}">
|
||||||
|
<th scope="row">3</th>
|
||||||
|
<td>CentOS-8-Demo-g-0</td>
|
||||||
|
<td>4 / 2</td>
|
||||||
|
<td>3800 / 2726</td>
|
||||||
|
<td id="host-stat" class="text-secondary"><b>Dead</b></td>
|
||||||
|
<td>qemu:///system</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${data.hostname}">
|
||||||
|
<th scope="row">4</th>
|
||||||
|
<td>Win2k19-Demo-gz-30</td>
|
||||||
|
<td>4 / 2</td>
|
||||||
|
<td>3800 / 2726</td>
|
||||||
|
<td id="host-stat" class="text-secondary"><b>Dead</b></td>
|
||||||
|
<td>qemu+ssh://pool@check2.homedevops/system</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${data.hostname}">
|
||||||
|
<th scope="row">5</th>
|
||||||
|
<td>Linux-20-04-rc-302321</td>
|
||||||
|
<td>8 / 5</td>
|
||||||
|
<td>8800 / 6726</td>
|
||||||
|
<td id="host-stat" class="text-success"><b>Alive</b></td>
|
||||||
|
<td>qemu:///system</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${data.hostname}">
|
||||||
|
<th scope="row">6</th>
|
||||||
|
<td>Win2k19-Demo-gz-30</td>
|
||||||
|
<td>4 / 2</td>
|
||||||
|
<td>3800 / 2726</td>
|
||||||
|
<td id="host-stat" class="text-secondary"><b>Dead</b></td>
|
||||||
|
<td>qemu+ssh://pool@reserve.homedevops/system</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${data.hostname}">
|
||||||
|
<th scope="row">7</th>
|
||||||
|
<td>Win2k19-Demo-gz-30</td>
|
||||||
|
<td>4 / 2</td>
|
||||||
|
<td>3800 / 2726</td>
|
||||||
|
<td id="host-stat" class="text-success"><b>Alive</b></td>
|
||||||
|
<td>qemu:///system</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="${data.hostname}">
|
||||||
|
<th scope="row">8</th>
|
||||||
|
<td>Win2k19-Demo-gz-30</td>
|
||||||
|
<td>4 / 2</td>
|
||||||
|
<td>3800 / 2726</td>
|
||||||
|
<td id="host-stat" class="text-secondary"><b>Dead</b></td>
|
||||||
|
<td>qemu+ssh://pool@check3.homedevops/system</td>
|
||||||
|
</tr-->
|
||||||
|
|
||||||
|
{% if host_data %}
|
||||||
|
{% for data in host_data %}
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="${{ data.hostname }}">
|
||||||
|
<th scope="row">{{ data.sn }}</th>
|
||||||
|
<td>{{ data.hostname }}</td>
|
||||||
|
<td>{{ data.cpu.total }} / {{ data.cpu.free }}</td>
|
||||||
|
<td>{{ data.mem.total }} / {{ data.mem.free }}</td>
|
||||||
|
<td id="host-stat" class="{{ "text-success" if data.status=="Alive" else "text-secondary" }}"><b>{{ data.status }}</b></td>
|
||||||
|
<td>{{ data.uri }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/jquery.min.js"></script>
|
||||||
|
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
830
templates/manage.html
Normal file
830
templates/manage.html
Normal file
|
@ -0,0 +1,830 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Manage Hosts</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto|Varela+Round">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_PREFIX }}assets/css/bootstrap-tagsinput.css">
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||||
|
<script src="{{ STATIC_PREFIX }}assets/js/bootstrap-tagsinput.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
color: #566787;
|
||||||
|
background: #74ebd5;
|
||||||
|
background: -webkit-linear-gradient(to right, #74ebd5, #ACB6E5);
|
||||||
|
background: linear-gradient(to right, #74ebd5, #ACB6E5);
|
||||||
|
font-family: 'Varela Round', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
.table-wrapper {
|
||||||
|
min-width: 1000px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px 25px;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 1px 1px rgba(0,0,0,.05);
|
||||||
|
}
|
||||||
|
.table-title {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
background: #435d7d;
|
||||||
|
color: #fff;
|
||||||
|
padding: 16px 30px;
|
||||||
|
margin: -20px -25px 10px;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
}
|
||||||
|
.table-title h2 {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.table-title .btn-group {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.table-title .btn {
|
||||||
|
color: #fff;
|
||||||
|
float: right;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
min-width: 50px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: none;
|
||||||
|
outline: none !important;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.table-title .btn i {
|
||||||
|
float: left;
|
||||||
|
font-size: 21px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.table-title .btn span {
|
||||||
|
float: left;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
table.table tr th, table.table tr td {
|
||||||
|
border-color: #e9e9e9;
|
||||||
|
padding: 12px 15px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
table.table tr th:first-child {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
table.table tr th:last-child {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
table.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: #fcfcfc;
|
||||||
|
}
|
||||||
|
table.table-striped.table-hover tbody tr:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
table.table th i {
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
table.table td:last-child i {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 22px;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
table.table td a {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #566787;
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
table.table td a:hover {
|
||||||
|
color: #2196F3;
|
||||||
|
}
|
||||||
|
table.table td a.edit {
|
||||||
|
color: #FFC107;
|
||||||
|
}
|
||||||
|
table.table td a.delete {
|
||||||
|
color: #F44336;
|
||||||
|
}
|
||||||
|
table.table td i {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
table.table .avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
float: right;
|
||||||
|
margin: 0 0 5px;
|
||||||
|
}
|
||||||
|
.pagination li a {
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 30px;
|
||||||
|
min-height: 30px;
|
||||||
|
color: #999;
|
||||||
|
margin: 0 2px;
|
||||||
|
line-height: 30px;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
.pagination li a:hover {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.pagination li.active a, .pagination li.active a.page-link {
|
||||||
|
background: #03A9F4;
|
||||||
|
}
|
||||||
|
.pagination li.active a:hover {
|
||||||
|
background: #0397d6;
|
||||||
|
}
|
||||||
|
.pagination li.disabled i {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.pagination li i {
|
||||||
|
font-size: 16px;
|
||||||
|
padding-top: 6px
|
||||||
|
}
|
||||||
|
.hint-text {
|
||||||
|
float: left;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
/* Custom checkbox */
|
||||||
|
.custom-checkbox {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.custom-checkbox input[type="checkbox"] {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
margin: 5px 0 0 3px;
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
.custom-checkbox label:before{
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.custom-checkbox label:before {
|
||||||
|
content: '';
|
||||||
|
margin-right: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-top;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.custom-checkbox input[type="checkbox"]:checked + label:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 3px;
|
||||||
|
width: 6px;
|
||||||
|
height: 11px;
|
||||||
|
border: solid #000;
|
||||||
|
border-width: 0 3px 3px 0;
|
||||||
|
transform: inherit;
|
||||||
|
z-index: 3;
|
||||||
|
transform: rotateZ(45deg);
|
||||||
|
}
|
||||||
|
.custom-checkbox input[type="checkbox"]:checked + label:before {
|
||||||
|
border-color: #03A9F4;
|
||||||
|
background: #03A9F4;
|
||||||
|
}
|
||||||
|
.custom-checkbox input[type="checkbox"]:checked + label:after {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
.custom-checkbox input[type="checkbox"]:disabled + label:before {
|
||||||
|
color: #b8b8b8;
|
||||||
|
cursor: auto;
|
||||||
|
box-shadow: none;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
/* Modal styles */
|
||||||
|
.modal .modal-dialog {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.modal .modal-header, .modal .modal-body, .modal .modal-footer {
|
||||||
|
padding: 20px 30px;
|
||||||
|
}
|
||||||
|
.modal .modal-content {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.modal .modal-footer {
|
||||||
|
background: #ecf0f1;
|
||||||
|
border-radius: 0 0 3px 3px;
|
||||||
|
}
|
||||||
|
.modal .modal-title {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.modal .form-control {
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: #dddddd;
|
||||||
|
}
|
||||||
|
.modal textarea.form-control {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.modal .btn {
|
||||||
|
border-radius: 2px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
.modal form label {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#overlay{
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: black;
|
||||||
|
opacity: .5;
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
|
||||||
|
border: 10px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 10px solid #4e729e;
|
||||||
|
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
-webkit-animation: spin .4s linear infinite; /* Safari */
|
||||||
|
animation: spin .4s linear infinite;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safari */
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% { -webkit-transform: rotate(0deg); }
|
||||||
|
100% { -webkit-transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
//import Tagify from '@yaireo/tagify';
|
||||||
|
$(document).ready(function(){
|
||||||
|
|
||||||
|
//window.onload= function(){
|
||||||
|
// Activate tooltip
|
||||||
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
|
||||||
|
//$('input[name=tags]').data('tagify').addTags('aaa, bbb, ccc');
|
||||||
|
//new Tagify(input)
|
||||||
|
|
||||||
|
// Select/Deselect checkboxes
|
||||||
|
var checkbox = $('table tbody input[type="checkbox"]');
|
||||||
|
$("#selectAll").click(function(){
|
||||||
|
if(this.checked){
|
||||||
|
checkbox.each(function(){
|
||||||
|
this.checked = true;
|
||||||
|
});
|
||||||
|
} else{
|
||||||
|
checkbox.each(function(){
|
||||||
|
this.checked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
checkbox.click(function(){
|
||||||
|
if(!this.checked){
|
||||||
|
$("#selectAll").prop("checked", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var actions = '<td>\
|
||||||
|
<a href="#" class="edit" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Edit"></i></a>\
|
||||||
|
<a href="#" class="delete" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Delete"></i></a>\
|
||||||
|
</td>'
|
||||||
|
var row = '<tr>';
|
||||||
|
|
||||||
|
$("#add-hostname").change((obj)=>{
|
||||||
|
console.log(obj.target.value)
|
||||||
|
|
||||||
|
$.post({
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({"hostname": obj.target.value}),
|
||||||
|
url: "/query_host",
|
||||||
|
beforeSend: ()=>{$("#overlay").css("display", "flex");},
|
||||||
|
complete: ()=>{$("#overlay").css("display", "none");},
|
||||||
|
error:function () {
|
||||||
|
alert('An error occured while querying host. Please try again later.');
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
if(response.done){
|
||||||
|
$("table").append(row)
|
||||||
|
|
||||||
|
input= $("#add-modal").find("input");
|
||||||
|
input.each(function(index){
|
||||||
|
var elem = this
|
||||||
|
if(elem.type=="checkbox"){
|
||||||
|
if(response.gpu){
|
||||||
|
$(elem)[0].checked = true
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$(elem)[0].checked= false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
|
||||||
|
elem.value= response.data[index]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#add")[0].disabled = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
alert(`Error: ${response.msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input:checkbox").on("click", false);
|
||||||
|
|
||||||
|
$("#add-form").submit((e)=>{
|
||||||
|
e.preventDefault();
|
||||||
|
$("#add")[0].disabled = true;
|
||||||
|
|
||||||
|
if($("#add-form")[0].checkValidity()){
|
||||||
|
|
||||||
|
var requestData={
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
|
||||||
|
par = $("#add").parent().parent();
|
||||||
|
bdy= par.find(".modal-body");
|
||||||
|
input= bdy.find("input");
|
||||||
|
input.each(function(){
|
||||||
|
var elem = this
|
||||||
|
if(elem.type=="checkbox"){
|
||||||
|
if(elem.checked){
|
||||||
|
requestData.gpu= "Yes"
|
||||||
|
row += '<td class="text-success">Yes</td>'
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
requestData.gpu= "No"
|
||||||
|
row += '<td class="text-secondary">No</td>'
|
||||||
|
}
|
||||||
|
elem.checked= false;
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(elem.placeholder=="Tags"){
|
||||||
|
requestData.data.push(elem.value)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
requestData.data.push(elem.value)
|
||||||
|
row += `<td>${elem.value}</td>`
|
||||||
|
elem.value=""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
requestData.power= $(".sel-add").find(":selected").text()
|
||||||
|
row += `<td>${$(".sel-add").find(":selected").text()}</td>`
|
||||||
|
row += actions + '</tr>';
|
||||||
|
|
||||||
|
//make the AJAX call over POST
|
||||||
|
|
||||||
|
$.post({
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify(requestData),
|
||||||
|
url: "/add_host",
|
||||||
|
error:function () {
|
||||||
|
alert('An error occured while adding host. Please try again later.');
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
if(response.done){
|
||||||
|
$("table").append(row)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
alert(`Error: ${response.msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
row= '<tr>'
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$('.sel-add option:contains("Always On")').prop('selected', true)
|
||||||
|
|
||||||
|
$("#addHostModal").modal('toggle');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var curr_idx= -1
|
||||||
|
$("table").on('click', 'a.edit', function(){
|
||||||
|
//console.log(idx);
|
||||||
|
td= $(this).parents("tr").find("td:not(:last-child)")
|
||||||
|
curr_idx= $(this).parents("tr").index()
|
||||||
|
data= []
|
||||||
|
td.each(function(){
|
||||||
|
data.push($(this).text())
|
||||||
|
});
|
||||||
|
|
||||||
|
bdy= $("#edit-form").find(".modal-body")
|
||||||
|
input= bdy.find("input");
|
||||||
|
input.each(function(index){
|
||||||
|
var elem = this
|
||||||
|
if(elem.type=="checkbox"){
|
||||||
|
if(data[5]=="Yes"){
|
||||||
|
$(elem).prop('checked', true)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$(elem).prop('checked', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(elem.placeholder=="Tags"){
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
elem.value= data[index]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(`.sel-edit option:contains(${data[6]})`).prop('selected', true)
|
||||||
|
$("#editHostModal").modal('toggle');
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#edit-form").submit((e)=>{
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if($("#edit-form")[0].checkValidity()){
|
||||||
|
var requestData={
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
|
||||||
|
par = $("#save").parent().parent();
|
||||||
|
bdy= par.find(".modal-body");
|
||||||
|
input= bdy.find("input");
|
||||||
|
input.each(function(){
|
||||||
|
var elem = this //.children[1];
|
||||||
|
if(elem.type=="checkbox"){
|
||||||
|
if(elem.checked){
|
||||||
|
requestData.gpu= "Yes"
|
||||||
|
row += '<td class="text-success">Yes</td>'
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
requestData.gpu= "No"
|
||||||
|
row += '<td class="text-secondary">No</td>'
|
||||||
|
}
|
||||||
|
elem.checked= false;
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(elem.placeholder=="Tags"){
|
||||||
|
requestData.data.push(elem.value)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
requestData.data.push(elem.value)
|
||||||
|
row += `<td>${elem.value}</td>`
|
||||||
|
elem.value=""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
requestData.power= $(".sel-edit").find(":selected").text()
|
||||||
|
row += `<td>${$(".sel-edit").find(":selected").text()}</td>`
|
||||||
|
row += actions + '</tr>';
|
||||||
|
|
||||||
|
$.post({
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify(requestData),
|
||||||
|
url: "/edit_host",
|
||||||
|
error:function () {
|
||||||
|
alert('An error occured while updating host. Please try again later.');
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
if(response.done){
|
||||||
|
|
||||||
|
$("table tbody tr").eq(curr_idx).remove()
|
||||||
|
if(curr_idx==0){
|
||||||
|
$("table tbody tr:first").before(row)
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$("table tbody tr").eq(curr_idx-1).after(row)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
alert(`Error: ${response.msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
row= '<tr>'
|
||||||
|
$('.sel-edit option:contains("Always On")').prop('selected', true)
|
||||||
|
curr_idx= -1
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$("#editHostModal").modal('toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var del_idx= -1
|
||||||
|
$("table").on('click', 'a.delete', function(){
|
||||||
|
hname= $(this).parents("tr").find("td:first").text()
|
||||||
|
del_idx = $(this).parents("tr").index()
|
||||||
|
bdy= $("#edit-form").find(".modal-body")
|
||||||
|
p= bdy.find(".info")
|
||||||
|
$("#info-text").text(`Are you sure you want to delete ${hname}?`)
|
||||||
|
|
||||||
|
$("#deleteHostModal").modal('toggle')
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#delete-form").submit((e)=>{
|
||||||
|
e.preventDefault();
|
||||||
|
requestData = {
|
||||||
|
hostname: $("table tbody tr").eq(del_idx).find("td:first").text()
|
||||||
|
}
|
||||||
|
$.post({
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify(requestData),
|
||||||
|
url: "/delete_host",
|
||||||
|
error:function () {
|
||||||
|
alert('An error occured while deleting host. Please try again later.');
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
$("table tbody tr").eq(del_idx).remove()
|
||||||
|
del_idx= -1
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$("#deleteHostModal").modal('toggle')
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<div class="table-title">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<h2>Manage <b>Hosts</b></h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<a href="#addHostModal" class="btn btn-success" data-toggle="modal"><i class="material-icons"></i> <span>Add New Host</span></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped table-hover" id="main-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<!--th>
|
||||||
|
<span class="custom-checkbox">
|
||||||
|
<input type="checkbox" id="selectAll">
|
||||||
|
<label for="selectAll"></label>
|
||||||
|
</span>
|
||||||
|
</th-->
|
||||||
|
<th>HostName</th>
|
||||||
|
<th>CPUs</th>
|
||||||
|
<th>Memory</th>
|
||||||
|
<th>Ip Address</th>
|
||||||
|
<th>Mac Address</th>
|
||||||
|
<th>GPU</th>
|
||||||
|
<th>Power Option</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Host1</td>
|
||||||
|
<td>4</td>
|
||||||
|
<td>7826</td>
|
||||||
|
<td>34.10.20.0</td>
|
||||||
|
<td>9C-35-5B-5F-4C-D7</td>
|
||||||
|
<td class="text-secondary">No</td>
|
||||||
|
<td>Always On</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="edit" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Edit"></i></a>
|
||||||
|
<a href="#" class="delete" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Delete"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Host2</td>
|
||||||
|
<td>8</td>
|
||||||
|
<td>7826</td>
|
||||||
|
<td>34.10.20.0</td>
|
||||||
|
<td>9C-35-5B-5F-4C-D7</td>
|
||||||
|
<td class="text-success">Yes</td>
|
||||||
|
<td>Wake on LAN</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="edit" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Edit"></i></a>
|
||||||
|
<a href="#" class="delete" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Delete"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Host3</td>
|
||||||
|
<td>4</td>
|
||||||
|
<td>4096</td>
|
||||||
|
<td>34.10.20.0</td>
|
||||||
|
<td>9C-35-5B-5F-4C-D7</td>
|
||||||
|
<td class="text-secondary">No</td>
|
||||||
|
<td>Tasmota</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="edit" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Edit"></i></a>
|
||||||
|
<a href="#" class="delete" data-toggle="modal"><i class="material-icons" data-toggle="tooltip" title="Delete"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Add Modal -->
|
||||||
|
<div id="addHostModal" class="modal fade">
|
||||||
|
<div id="overlay">
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="add-form">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Add Host</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="add-modal">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>HostName</label>
|
||||||
|
<input id="add-hostname" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>CPUs</label>
|
||||||
|
<input type="number" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Memory</label>
|
||||||
|
<input type="number" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ip Address</label>
|
||||||
|
<input type="text" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Mac Address</label>
|
||||||
|
<input type="text" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>GPU Available </label>
|
||||||
|
<input type="checkbox" id="selectAll">
|
||||||
|
<!--span class="custom-checkbox">
|
||||||
|
<input type="checkbox" id="selectAll">
|
||||||
|
<label for="selectAll"></label>
|
||||||
|
</span-->
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<input type="text" value="" data-role="tagsinput" placeholder="Tags" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Power Option</label>
|
||||||
|
<select class="browser-default custom-select sel-add" type="select">
|
||||||
|
<option selected>Always On</option>
|
||||||
|
<option >Wake on LAN</option>
|
||||||
|
<option >Tasmota</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-default" data-dismiss="modal" value="Cancel">
|
||||||
|
<input type="submit" class="btn btn-success" value="Add" id="add" disabled>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal HTML -->
|
||||||
|
<div id="editHostModal" class="modal fade">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="edit-form">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Edit Host</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>HostName</label>
|
||||||
|
<input type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>CPUs</label>
|
||||||
|
<input type="number" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Memory</label>
|
||||||
|
<input type="number" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ip Address</label>
|
||||||
|
<input type="text" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Mac Address</label>
|
||||||
|
<input type="text" class="form-control" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>GPU Available </label>
|
||||||
|
<input type="checkbox" id="selectAll" disabled>
|
||||||
|
<!--span class="custom-checkbox">
|
||||||
|
<input type="checkbox" id="selectAll">
|
||||||
|
<label for="selectAll"></label>
|
||||||
|
</span-->
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<input type="text" value="hello, world" data-role="tagsinput" placeholder="Tags" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Power Option</label>
|
||||||
|
<select class="browser-default custom-select sel-edit" type="select">
|
||||||
|
<option selected>Always On</option>
|
||||||
|
<option >Wake on LAN</option>
|
||||||
|
<option >Tasmota</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-default" data-dismiss="modal" value="Cancel">
|
||||||
|
<input type="submit" class="btn btn-success" value="Save" id="save">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Delete Modal HTML -->
|
||||||
|
<div id="deleteHostModal" class="modal fade">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="delete-form">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Delete Host</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="info-text"></p>
|
||||||
|
<p class="text-warning"><small>This action cannot be undone.</small></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-default" data-dismiss="modal" value="Cancel">
|
||||||
|
<input type="submit" class="btn btn-danger" value="Delete" id=delete>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
254
virtpool.py
Normal file → Executable file
254
virtpool.py
Normal file → Executable file
|
@ -6,15 +6,50 @@ import sys
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
import libvirt
|
import libvirt
|
||||||
|
import json
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
|
||||||
|
pool = ThreadPool(processes=1)
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
PATH = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read("config.ini")
|
||||||
|
|
||||||
|
TEMPLATE_ENVIRONMENT = Environment(
|
||||||
|
autoescape=False,
|
||||||
|
loader=FileSystemLoader(os.path.join(PATH, "templates")),
|
||||||
|
trim_blocks=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TEMPLATE_ENVIRONMENT.globals['STATIC_PREFIX'] = '/templates/'
|
||||||
|
|
||||||
|
|
||||||
class Client():
|
def render_template(template_filename, context):
|
||||||
|
return TEMPLATE_ENVIRONMENT.get_template(template_filename).render(context)
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
"""Main manager class"""
|
"""Main manager class"""
|
||||||
uris = {
|
|
||||||
'qemu+ssh://pool@reserve.homedevops/system',
|
""" uris = [
|
||||||
'qemu+ssh://root@check2.homedevops/system',
|
#'qemu+ssh://pool@reserve.homedevops/system',
|
||||||
'qemu+ssh://root@check3.homedevops/system'
|
#'qemu+ssh://root@check2.homedevops/system',
|
||||||
}
|
#'qemu+ssh://root@check3.homedevops/system'
|
||||||
|
'qemu:///system'
|
||||||
|
] """
|
||||||
|
|
||||||
|
host_str = config["host"]["available_hosts"]
|
||||||
|
uris = [
|
||||||
|
each.strip()
|
||||||
|
for each in host_str.replace("'", "").replace('"', "").split(",")
|
||||||
|
if each.strip()
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.status = {}
|
self.status = {}
|
||||||
|
@ -43,25 +78,214 @@ class Client():
|
||||||
usedcpus += cpus
|
usedcpus += cpus
|
||||||
|
|
||||||
out += "{}: {}/{} CPUs free,{}MiB/{}MiB memory free<br/>\n".format(
|
out += "{}: {}/{} CPUs free,{}MiB/{}MiB memory free<br/>\n".format(
|
||||||
n,
|
n, cpus - usedcpus, cpus, str((mem - usedmem)), str(mem)
|
||||||
cpus - usedcpus, cpus,
|
)
|
||||||
str((mem - usedmem)),
|
|
||||||
str(mem))
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def create(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
iso_source,
|
||||||
|
img_source,
|
||||||
|
domain_type="kvm",
|
||||||
|
memory_unit="MiB",
|
||||||
|
memory=1024,
|
||||||
|
current_memory=1024,
|
||||||
|
vcpu=2,
|
||||||
|
arch="x86_64",
|
||||||
|
machine="pc-q35-4.2",
|
||||||
|
offset="utc",
|
||||||
|
emulator="/usr/bin/qemu-system-x86_64",
|
||||||
|
):
|
||||||
|
|
||||||
|
"""create a VM with given specs
|
||||||
|
boot it and return the ID for future ref
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Check host resources(cpus, mem, etc.) before creating a VM
|
||||||
|
|
||||||
|
param = {
|
||||||
|
"name": name,
|
||||||
|
"uuid": str(uuid.uuid4()),
|
||||||
|
"iso_source": iso_source,
|
||||||
|
"img_source": img_source,
|
||||||
|
"type": domain_type,
|
||||||
|
"memory_unit": memory_unit,
|
||||||
|
"memory": memory,
|
||||||
|
"current_memory": current_memory,
|
||||||
|
"vcpu": vcpu,
|
||||||
|
"arch": arch,
|
||||||
|
"machine": machine,
|
||||||
|
"offset": offset,
|
||||||
|
"emulator": emulator,
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {"args": param}
|
||||||
|
|
||||||
|
xml = render_template("config.tmpl.xml", context)
|
||||||
|
# print(xml)
|
||||||
|
|
||||||
|
host = self.connections[self.uris[0]]
|
||||||
|
if host:
|
||||||
|
async_result = pool.apply_async(self.__create_vm, (host, xml))
|
||||||
|
return async_result.get()
|
||||||
|
return "No host available"
|
||||||
|
|
||||||
|
def __create_vm(self, host, xml):
|
||||||
|
try:
|
||||||
|
vm = host.createXML(xml)
|
||||||
|
return json.dumps({"uuid": vm.UUIDString(), "id": vm.ID(), "status": 1})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"err": str(e), "status": 0})
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def gethost(self):
|
||||||
|
"""
|
||||||
|
It'll show relevant info about all available hosts
|
||||||
|
"""
|
||||||
|
|
||||||
|
host_data = []
|
||||||
|
|
||||||
|
for i, each in enumerate(self.uris):
|
||||||
|
tmp = {}
|
||||||
|
c = self.connections[each]
|
||||||
|
nodeinfo = c.getInfo()
|
||||||
|
mem = nodeinfo[1]
|
||||||
|
cpus = nodeinfo[2]
|
||||||
|
usedmem = 0
|
||||||
|
usedcpus = 0
|
||||||
|
|
||||||
|
tmp["sn"] = i + 1
|
||||||
|
tmp["hostname"] = c.getHostname()
|
||||||
|
tmp["status"] = "Alive" if c.isAlive() else "Dead"
|
||||||
|
tmp["uri"] = c.getURI()
|
||||||
|
tmp["cpu"] = {}
|
||||||
|
tmp["mem"] = {}
|
||||||
|
|
||||||
|
domain_ids = c.listDomainsID()
|
||||||
|
for did in domain_ids:
|
||||||
|
dom = c.lookupByID(did)
|
||||||
|
state, maxmem, mem2, cpus2, cput = dom.info()
|
||||||
|
usedmem += maxmem / 1024
|
||||||
|
usedcpus += cpus2
|
||||||
|
|
||||||
|
tmp["cpu"]["total"] = cpus
|
||||||
|
tmp["cpu"]["free"] = cpus - usedcpus
|
||||||
|
tmp["mem"]["total"] = mem
|
||||||
|
tmp["mem"]["free"] = int(mem - usedmem)
|
||||||
|
|
||||||
|
host_data.append(tmp)
|
||||||
|
|
||||||
|
return render_template("host.html", {"host_data": host_data})
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def getguest(self):
|
||||||
|
"""
|
||||||
|
It'll show host-wise available guests and detailed info about them
|
||||||
|
"""
|
||||||
|
|
||||||
|
guest_data = []
|
||||||
|
cnt = 0
|
||||||
|
for i, each in enumerate(self.uris):
|
||||||
|
c = self.connections[each]
|
||||||
|
for did in c.listDomainsID():
|
||||||
|
dom = c.lookupByID(did)
|
||||||
|
tmp = {
|
||||||
|
"sn": cnt + 1,
|
||||||
|
"guestname": dom.name(),
|
||||||
|
"hostname": c.getHostname(),
|
||||||
|
"id_": dom.ID(),
|
||||||
|
"mem": int(dom.info()[1] / 1024),
|
||||||
|
"cpu": dom.info()[3],
|
||||||
|
"status": "Active" if dom.isActive() else "Inactive",
|
||||||
|
"uuid": dom.UUIDString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
guest_data.append(tmp)
|
||||||
|
cnt += 1
|
||||||
|
|
||||||
|
return render_template("guest.html", {"guest_data": guest_data})
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def manage(self):
|
||||||
|
# get available hosts from the database(if it's stored in one)
|
||||||
|
# and return the updated template
|
||||||
|
|
||||||
|
return render_template("manage.html", {})
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_in()
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def query_host(self):
|
||||||
|
# make ajax call to query host
|
||||||
|
|
||||||
|
json_obj = cherrypy.request.json
|
||||||
|
print(json_obj)
|
||||||
|
import time
|
||||||
|
|
||||||
|
time.sleep(1) # simulate delay
|
||||||
|
return {
|
||||||
|
"data": [json_obj["hostname"], 4, 4096, "10.56.0.1", "9C-35-5B-5F-4C-D7"],
|
||||||
|
"gpu": True,
|
||||||
|
"done": True,
|
||||||
|
"msg": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_in()
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def add_host(self):
|
||||||
|
# make ajax call to add host, maybe save it in a database
|
||||||
|
|
||||||
|
json_obj = cherrypy.request.json
|
||||||
|
print(json_obj)
|
||||||
|
return {"done": True, "msg": ""}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_in()
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def edit_host(self):
|
||||||
|
# make ajax call to edit host. Update it in database
|
||||||
|
|
||||||
|
json_obj = cherrypy.request.json
|
||||||
|
|
||||||
|
print(json_obj)
|
||||||
|
return {"done": True, "msg": ""}
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_in()
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
|
def delete_host(self):
|
||||||
|
# make ajax call to delete host from database
|
||||||
|
|
||||||
|
json_obj = cherrypy.request.json
|
||||||
|
|
||||||
|
print(json_obj)
|
||||||
|
return {"done": True, "msg": ""}
|
||||||
|
|
||||||
|
|
||||||
ROOT = Client()
|
ROOT = Client()
|
||||||
CONF = os.path.join(os.path.dirname(__file__), 'site.conf')
|
CONF = os.path.join(os.path.dirname(__file__), "site.conf")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
# cherrypy.config.update(CONF)
|
# cherrypy.config.update(CONF)
|
||||||
cherrypy.server.socket_host = '127.0.0.1'
|
cherrypy.server.socket_host = config["server"].get("server_host", "127.0.0.1")
|
||||||
cherrypy.server.socket_port = 8080
|
cherrypy.server.socket_port = config["server"].getint("server_port", 8080)
|
||||||
ENGINE = cherrypy.engine
|
ENGINE = cherrypy.engine
|
||||||
# cherrypy.process.plugins.Daemonizer(engine).subscribe()
|
# cherrypy.process.plugins.Daemonizer(engine).subscribe()
|
||||||
# cherrypy.process.plugins.DropPrivileges(
|
# cherrypy.process.plugins.DropPrivileges(
|
||||||
# ENGINE, uid='cherrypy', gid='cherrypy').subscribe()
|
# ENGINE, uid='cherrypy', gid='cherrypy').subscribe()
|
||||||
cherrypy.tree.mount(Client())
|
cherrypy.tree.mount(
|
||||||
|
Client(),
|
||||||
|
"/",
|
||||||
|
config={
|
||||||
|
"/": {
|
||||||
|
"tools.staticdir.on": True,
|
||||||
|
"tools.staticdir.dir": PATH + "/templates",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
if hasattr(ENGINE, "signal_handler"):
|
if hasattr(ENGINE, "signal_handler"):
|
||||||
ENGINE.signal_handler.subscribe()
|
ENGINE.signal_handler.subscribe()
|
||||||
if hasattr(ENGINE, "console_control_handler"):
|
if hasattr(ENGINE, "console_control_handler"):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user