2023-05-17 21:40:37 +00:00
import { requestPVE , requestAPI , goToPage , goToURL , instances _config , nodes _config , setTitleAndHeader } from "./utils.js" ;
import { alert , dialog } from "./dialog.js" ;
import { PVE } from "../vars.js"
2022-12-11 06:49:10 +00:00
2022-12-11 02:02:29 +00:00
window . addEventListener ( "DOMContentLoaded" , init ) ;
2023-05-17 21:40:37 +00:00
async function init ( ) {
2023-05-16 15:18:36 +00:00
setTitleAndHeader ( ) ;
2023-04-19 05:58:30 +00:00
let cookie = document . cookie ;
if ( cookie === "" ) {
goToPage ( "login.html" ) ;
}
2023-05-17 21:40:37 +00:00
2022-12-18 00:07:18 +00:00
await populateInstances ( ) ;
2023-01-09 23:08:45 +00:00
2023-02-23 21:11:43 +00:00
let addInstanceBtn = document . querySelector ( "#instance-add" ) ;
addInstanceBtn . addEventListener ( "click" , handleInstanceAdd ) ;
2022-12-18 00:07:18 +00:00
}
2023-05-17 21:40:37 +00:00
async function populateInstances ( ) {
2023-05-11 07:13:41 +00:00
let resources = await requestPVE ( "/cluster/resources" , "GET" ) ;
2023-04-19 05:58:30 +00:00
let instanceContainer = document . getElementById ( "instance-container" ) ;
2023-01-17 20:26:19 +00:00
let instances = [ ] ;
2023-01-17 20:24:51 +00:00
resources . data . forEach ( ( element ) => {
if ( element . type === "lxc" || element . type === "qemu" ) {
let nodeName = element . node ;
2023-01-17 20:29:10 +00:00
let nodeStatus = resources . data . find ( item => item . node === nodeName && item . type === "node" ) . status ;
2023-05-17 21:40:37 +00:00
element . node = { name : nodeName , status : nodeStatus } ;
2023-01-17 20:26:19 +00:00
instances . push ( element ) ;
2023-01-17 20:24:51 +00:00
}
} ) ;
2022-12-14 21:37:30 +00:00
instances . sort ( ( a , b ) => ( a . vmid > b . vmid ) ? 1 : - 1 ) ;
2022-12-11 23:06:10 +00:00
2023-04-06 04:21:03 +00:00
instanceContainer . innerHTML = `
2023-05-15 02:56:48 +00:00
< div class = "w3-row w3-hide-small" style = "border-bottom: 1px solid;" >
2023-04-12 22:59:57 +00:00
< div class = "w3-col l1 m2" >
2023-04-11 21:38:51 +00:00
< p > VM ID < / p >
< / d i v >
2023-04-12 22:59:57 +00:00
< div class = "w3-col l2 m3" >
2023-04-11 21:38:51 +00:00
< p > VM Name < / p >
< / d i v >
2023-04-12 22:59:57 +00:00
< div class = "w3-col l1 m2" >
2023-04-11 21:38:51 +00:00
< p > VM Type < / p >
< / d i v >
2023-04-12 22:59:57 +00:00
< div class = "w3-col l2 m3" >
2023-04-11 21:38:51 +00:00
< p > VM Status < / p >
< / d i v >
2023-04-12 22:59:57 +00:00
< div class = "w3-col l2 w3-hide-medium" >
2023-04-11 21:38:51 +00:00
< p > Host Name < / p >
< / d i v >
2023-04-12 22:59:57 +00:00
< div class = "w3-col l2 w3-hide-medium" >
2023-04-11 21:38:51 +00:00
< p > Host Status < / p >
< / d i v >
2023-04-12 22:59:57 +00:00
< div class = "w3-col l2 m2" >
< p > Actions < / p >
2023-04-11 21:38:51 +00:00
< / d i v >
2023-04-06 04:21:03 +00:00
< / d i v >
` ;
2023-05-17 21:40:37 +00:00
for ( let i = 0 ; i < instances . length ; i ++ ) {
2023-05-15 20:36:28 +00:00
let newInstance = new Instance ( ) ;
2022-12-14 21:37:30 +00:00
newInstance . data = instances [ i ] ;
2023-05-15 20:36:28 +00:00
instanceContainer . append ( newInstance . shadowElement ) ;
2022-12-11 02:02:29 +00:00
}
2023-02-23 21:11:43 +00:00
}
2023-05-17 21:40:37 +00:00
async function handleInstanceAdd ( ) {
2023-05-05 21:43:15 +00:00
let header = "Create New Instance" ;
2023-02-23 21:11:43 +00:00
2023-05-05 21:43:15 +00:00
let body = `
2023-02-23 21:11:43 +00:00
< label for = "type" > Instance Type < / l a b e l >
2023-04-12 22:35:17 +00:00
< select class = "w3-select w3-border" name = "type" id = "type" required >
2023-02-23 21:11:43 +00:00
< option value = "lxc" > Container < / o p t i o n >
< option value = "qemu" > Virtual Machine < / o p t i o n >
< / s e l e c t >
2023-02-23 22:14:06 +00:00
< label for = "node" > Node < / l a b e l >
2023-04-12 22:35:17 +00:00
< select class = "w3-select w3-border" name = "node" id = "node" required > < / s e l e c t >
2023-02-23 21:58:42 +00:00
< label for = "name" > Name < / l a b e l >
2023-04-12 22:35:17 +00:00
< input class = "w3-input w3-border" name = "name" id = "name" required > < / i n p u t >
2023-02-23 21:58:42 +00:00
< label for = "vmid" > ID < / l a b e l >
2023-04-12 22:35:17 +00:00
< input class = "w3-input w3-border" name = "vmid" id = "vmid" type = "number" required > < / i n p u t >
2023-02-27 23:03:19 +00:00
< label for = "cores" > Cores ( Threads ) < / l a b e l >
2023-04-12 22:35:17 +00:00
< input class = "w3-input w3-border" name = "cores" id = "cores" type = "number" min = "1" max = "8192" required > < / i n p u t >
2023-02-27 23:03:19 +00:00
< label for = "memory" > Memory ( MiB ) < / l a b e l >
2023-04-12 22:35:17 +00:00
< input class = "w3-input w3-border" name = "memory" id = "memory" type = "number" min = "16" , step = "1" required > < / i n p u t >
2023-02-24 23:27:03 +00:00
< p class = "container-specific none" style = "grid-column: 1 / span 2; text-align: center;" > Container Options < / p >
< label class = "container-specific none" for = "swap" > Swap ( MiB ) < / l a b e l >
2023-04-12 22:35:17 +00:00
< input class = "w3-input w3-border container-specific none" name = "swap" id = "swap" type = "number" min = "0" step = "1" required disabled > < / i n p u t >
2023-02-24 23:27:03 +00:00
< label class = "container-specific none" for = "template-storage" > Template Storage < / l a b e l >
2023-04-12 22:35:17 +00:00
< select class = "w3-select w3-border container-specific none" name = "template-storage" id = "template-storage" required disabled > < / s e l e c t >
2023-02-24 23:27:03 +00:00
< label class = "container-specific none" for = "template-image" > Template Image < / l a b e l >
2023-04-12 22:35:17 +00:00
< select class = "w3-select w3-border container-specific none" name = "template-image" id = "template-image" required disabled > < / s e l e c t >
2023-02-24 23:27:03 +00:00
< label class = "container-specific none" for = "rootfs-storage" > ROOTFS Storage < / l a b e l >
2023-04-12 22:35:17 +00:00
< select class = "w3-select w3-border container-specific none" name = "rootfs-storage" id = "rootfs-storage" required disabled > < / s e l e c t >
2023-02-24 23:27:03 +00:00
< label class = "container-specific none" for = "rootfs-size" > ROOTFS Size ( GiB ) < / l a b e l >
2023-04-12 22:35:17 +00:00
< input class = "w3-input w3-border container-specific none" name = "rootfs-size" id = "rootfs-size" type = "number" min = "0" max = "131072" required disabled > < / i n p u t >
2023-02-27 23:03:19 +00:00
< label class = "container-specific none" for = "password" > Password < / l a b e l >
2023-04-12 22:35:17 +00:00
< input class = "w3-input w3-border container-specific none" name = "password" id = "password" type = "password" required disabled > < / i n p u t >
2023-02-23 21:11:43 +00:00
` ;
2023-05-05 21:43:15 +00:00
let d = dialog ( header , body , async ( result , form ) => {
if ( result === "confirm" ) {
let body = {
node : form . get ( "node" ) ,
type : form . get ( "type" ) ,
name : form . get ( "name" ) ,
vmid : form . get ( "vmid" ) ,
cores : form . get ( "cores" ) ,
memory : form . get ( "memory" )
} ;
if ( form . get ( "type" ) === "lxc" ) {
body . swap = form . get ( "swap" ) ;
body . password = form . get ( "password" ) ;
body . ostemplate = form . get ( "template-image" ) ;
body . rootfslocation = form . get ( "rootfs-storage" ) ;
body . rootfssize = form . get ( "rootfs-size" ) ;
}
let result = await requestAPI ( "/instance" , "POST" , body ) ;
if ( result . status === 200 ) {
populateInstances ( ) ;
}
else {
2023-05-12 21:26:26 +00:00
alert ( result . error ) ;
2023-05-05 21:43:15 +00:00
populateInstances ( ) ;
}
}
} ) ;
let typeSelect = d . querySelector ( "#type" ) ;
2023-02-23 21:11:43 +00:00
typeSelect . selectedIndex = - 1 ;
typeSelect . addEventListener ( "change" , ( ) => {
2023-05-17 21:40:37 +00:00
if ( typeSelect . value === "qemu" ) {
2023-05-05 21:43:15 +00:00
d . querySelectorAll ( ".container-specific" ) . forEach ( ( element ) => {
2023-02-24 23:27:03 +00:00
element . classList . add ( "none" ) ;
element . disabled = true ;
} ) ;
2023-02-23 21:11:43 +00:00
}
else {
2023-05-05 21:43:15 +00:00
d . querySelectorAll ( ".container-specific" ) . forEach ( ( element ) => {
2023-02-24 23:27:03 +00:00
element . classList . remove ( "none" ) ;
element . disabled = false ;
} ) ;
2023-02-23 21:11:43 +00:00
}
} ) ;
2023-02-23 22:14:06 +00:00
let templateContent = "iso" ;
2023-05-05 21:43:15 +00:00
let templateStorage = d . querySelector ( "#template-storage" ) ;
2023-02-24 23:27:03 +00:00
templateStorage . selectedIndex = - 1 ;
2023-02-23 22:14:06 +00:00
let rootfsContent = "rootdir" ;
2023-05-05 21:43:15 +00:00
let rootfsStorage = d . querySelector ( "#rootfs-storage" ) ;
2023-02-24 23:27:03 +00:00
rootfsStorage . selectedIndex = - 1 ;
2023-02-23 22:14:06 +00:00
2023-05-05 21:43:15 +00:00
let nodeSelect = d . querySelector ( "#node" ) ;
2023-05-12 21:14:49 +00:00
let clusterNodes = await requestPVE ( "/nodes" , "GET" ) ;
2023-06-14 22:34:48 +00:00
let allowedNodes = await requestAPI ( "/user/config/nodes" , "GET" ) ;
2023-05-12 21:14:49 +00:00
clusterNodes . data . forEach ( ( element ) => {
2023-06-14 22:34:48 +00:00
if ( element . status === "online" && allowedNodes . includes ( element . node ) ) {
2023-02-24 23:27:03 +00:00
nodeSelect . add ( new Option ( element . node ) ) ;
}
} ) ;
nodeSelect . selectedIndex = - 1 ;
nodeSelect . addEventListener ( "change" , async ( ) => { // change template and rootfs storage based on node
let node = nodeSelect . value ;
let storage = await requestPVE ( ` /nodes/ ${ node } /storage ` , "GET" ) ;
storage . data . forEach ( ( element ) => {
if ( element . content . includes ( templateContent ) ) {
templateStorage . add ( new Option ( element . storage ) ) ;
}
if ( element . content . includes ( rootfsContent ) ) {
rootfsStorage . add ( new Option ( element . storage ) ) ;
}
} ) ;
templateStorage . selectedIndex = - 1 ;
rootfsStorage . selectedIndex = - 1 ;
} ) ;
2023-05-05 21:43:15 +00:00
let templateImage = d . querySelector ( "#template-image" ) ; // populate templateImage depending on selected image storage
2023-02-27 23:03:19 +00:00
templateStorage . addEventListener ( "change" , async ( ) => {
2023-05-05 21:43:15 +00:00
templateImage . innerHTML = ` ` ;
2023-02-27 23:03:19 +00:00
let content = "vztmpl" ;
let images = await requestPVE ( ` /nodes/ ${ nodeSelect . value } /storage/ ${ templateStorage . value } /content ` , "GET" ) ;
images . data . forEach ( ( element ) => {
if ( element . content . includes ( content ) ) {
templateImage . append ( new Option ( element . volid . replace ( ` ${ templateStorage . value } : ${ content } / ` , "" ) , element . volid ) ) ;
}
} ) ;
templateImage . selectedIndex = - 1 ;
} ) ;
2023-05-15 20:16:38 +00:00
let userResources = await requestAPI ( "/user/resources" , "GET" ) ;
2023-06-14 22:34:48 +00:00
let userInstances = await requestAPI ( "/user/config/instances" , "GET" ) ;
2023-05-15 20:16:38 +00:00
d . querySelector ( "#cores" ) . max = userResources . avail . cores ;
d . querySelector ( "#memory" ) . max = userResources . avail . memory ;
d . querySelector ( "#vmid" ) . min = userInstances . vmid . min ;
d . querySelector ( "#vmid" ) . max = userInstances . vmid . max ;
2023-05-15 20:36:28 +00:00
}
2023-05-17 21:43:03 +00:00
class Instance {
2023-05-17 21:40:37 +00:00
constructor ( ) {
2023-05-15 20:36:28 +00:00
let shadowRoot = document . createElement ( "div" ) ;
2023-05-15 20:41:31 +00:00
shadowRoot . classList . add ( "w3-row" ) ;
2023-05-15 20:36:28 +00:00
shadowRoot . innerHTML = `
2023-05-15 20:41:31 +00:00
< div class = "w3-col l1 m2 s6" >
< p id = "instance-id" > < / p >
< / d i v >
< div class = "w3-col l2 m3 s6" >
< p id = "instance-name" > < / p >
< / d i v >
< div class = "w3-col l1 m2 w3-hide-small" >
< p id = "instance-type" > < / p >
< / d i v >
< div class = "w3-col l2 m3 s6 flex row nowrap" >
< img id = "instance-status-icon" >
< p id = "instance-status" > < / p >
< / d i v >
< div class = "w3-col l2 w3-hide-medium w3-hide-small" >
< p id = "node-name" > < / p >
< / d i v >
< div class = "w3-col l2 w3-hide-medium w3-hide-small flex row nowrap" >
< img id = "node-status-icon" >
< p id = "node-status" > < / p >
< / d i v >
< div class = "w3-col l2 m2 s6 flex row nowrap" style = "height: 1lh; margin-top: 15px; margin-bottom: 15px;" >
2023-06-13 03:13:22 +00:00
< img id = "power-btn" >
< img id = "console-btn" >
< img id = "configure-btn" >
< img id = "delete-btn" >
2023-05-15 20:36:28 +00:00
< / d i v >
` ;
this . shadowElement = shadowRoot ;
this . actionLock = false ;
}
2023-05-17 21:40:37 +00:00
set data ( data ) {
2023-05-15 20:36:28 +00:00
if ( data . status === "unknown" ) {
data . status = "stopped" ;
}
this . type = data . type ;
this . status = data . status ;
this . vmid = data . vmid ;
this . name = data . name ;
this . node = data . node ;
this . update ( ) ;
}
2023-05-17 21:40:37 +00:00
update ( ) {
2023-05-15 20:36:28 +00:00
let vmidParagraph = this . shadowElement . querySelector ( "#instance-id" ) ;
vmidParagraph . innerText = this . vmid ;
let nameParagraph = this . shadowElement . querySelector ( "#instance-name" ) ;
nameParagraph . innerText = this . name ? this . name : "" ;
let typeParagraph = this . shadowElement . querySelector ( "#instance-type" ) ;
typeParagraph . innerText = this . type ;
let statusParagraph = this . shadowElement . querySelector ( "#instance-status" ) ;
statusParagraph . innerText = this . status ;
let statusIcon = this . shadowElement . querySelector ( "#instance-status-icon" ) ;
2023-06-13 03:13:22 +00:00
statusIcon . src = instances _config [ this . status ] . status . src ;
statusIcon . alt = instances _config [ this . status ] . status . alt ;
2023-05-15 20:36:28 +00:00
let nodeNameParagraph = this . shadowElement . querySelector ( "#node-name" ) ;
nodeNameParagraph . innerText = this . node . name ;
let nodeStatusParagraph = this . shadowElement . querySelector ( "#node-status" ) ;
nodeStatusParagraph . innerText = this . node . status ;
let nodeStatusIcon = this . shadowElement . querySelector ( "#node-status-icon" ) ;
2023-06-13 03:13:22 +00:00
nodeStatusIcon . src = nodes _config [ this . node . status ] . status . src ;
nodeStatusIcon . alt = nodes _config [ this . node . status ] . status . src ;
2023-05-15 20:36:28 +00:00
let powerButton = this . shadowElement . querySelector ( "#power-btn" ) ;
2023-06-13 03:13:22 +00:00
powerButton . src = instances _config [ this . status ] . power . src ;
powerButton . alt = instances _config [ this . status ] . power . alt ;
powerButton . title = instances _config [ this . status ] . power . alt ;
if ( instances _config [ this . status ] . power . clickable ) {
powerButton . classList . add ( "clickable" ) ;
powerButton . onclick = this . handlePowerButton . bind ( this )
}
2023-05-15 20:36:28 +00:00
let configButton = this . shadowElement . querySelector ( "#configure-btn" ) ;
2023-06-13 03:13:22 +00:00
configButton . src = instances _config [ this . status ] . config . src ;
configButton . alt = instances _config [ this . status ] . config . alt ;
configButton . title = instances _config [ this . status ] . config . alt ;
if ( instances _config [ this . status ] . config . clickable ) {
configButton . classList . add ( "clickable" ) ;
configButton . onclick = this . handleConfigButton . bind ( this ) ;
}
2023-05-15 20:36:28 +00:00
let consoleButton = this . shadowElement . querySelector ( "#console-btn" ) ;
2023-06-13 03:13:22 +00:00
consoleButton . src = instances _config [ this . status ] . console . src ;
consoleButton . alt = instances _config [ this . status ] . console . alt ;
consoleButton . title = instances _config [ this . status ] . console . alt ;
if ( instances _config [ this . status ] . console . clickable ) {
consoleButton . classList . add ( "clickable" ) ;
consoleButton . onclick = this . handleConsoleButton . bind ( this ) ;
}
2023-05-15 20:36:28 +00:00
let deleteButton = this . shadowElement . querySelector ( "#delete-btn" ) ;
2023-06-13 03:13:22 +00:00
deleteButton . src = instances _config [ this . status ] . delete . src ;
deleteButton . alt = instances _config [ this . status ] . delete . alt ;
deleteButton . title = instances _config [ this . status ] . delete . alt ;
if ( instances _config [ this . status ] . delete . clickable ) {
deleteButton . classList . add ( "clickable" ) ;
deleteButton . onclick = this . handleDeleteButton . bind ( this ) ;
}
2023-05-15 20:36:28 +00:00
if ( this . node . status !== "online" ) {
powerButton . classList . add ( "hidden" ) ;
configButton . classList . add ( "hidden" ) ;
consoleButton . classList . add ( "hidden" ) ;
deleteButton . classList . add ( "hidden" ) ;
}
}
2023-05-17 21:40:37 +00:00
async handlePowerButton ( ) {
if ( ! this . actionLock ) {
2023-05-15 20:36:28 +00:00
let header = ` ${ this . status === "running" ? "Stop" : "Start" } VM ${ this . vmid } ` ;
let body = ` <p>Are you sure you want to ${ this . status === "running" ? "stop" : "start" } VM</p><p> ${ this . vmid } </p> `
dialog ( header , body , async ( result , form ) => {
if ( result === "confirm" ) {
this . actionLock = true ;
let targetAction = this . status === "running" ? "stop" : "start" ;
let targetStatus = this . status === "running" ? "stopped" : "running" ;
let prevStatus = this . status ;
this . status = "loading" ;
this . update ( ) ;
2023-05-17 21:40:37 +00:00
let result = await requestPVE ( ` /nodes/ ${ this . node . name } / ${ this . type } / ${ this . vmid } /status/ ${ targetAction } ` , "POST" , { node : this . node . name , vmid : this . vmid } ) ;
2023-05-15 20:36:28 +00:00
const waitFor = delay => new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
while ( true ) {
let taskStatus = await requestPVE ( ` /nodes/ ${ this . node . name } /tasks/ ${ result . data } /status ` , "GET" ) ;
2023-05-17 21:40:37 +00:00
if ( taskStatus . data . status === "stopped" && taskStatus . data . exitstatus === "OK" ) { // task stopped and was successful
2023-05-15 20:36:28 +00:00
this . status = targetStatus ;
this . update ( ) ;
this . actionLock = false ;
break ;
}
else if ( taskStatus . data . status === "stopped" ) { // task stopped but was not successful
this . status = prevStatus ;
alert ( ` attempted to ${ targetAction } ${ this . vmid } but process returned stopped: ${ result . data . exitstatus } ` ) ;
this . update ( ) ;
this . actionLock = false ;
break ;
}
2023-05-17 21:40:37 +00:00
else { // task has not stopped
2023-05-15 20:36:28 +00:00
await waitFor ( 1000 ) ;
}
}
}
} ) ;
}
}
2023-05-17 21:40:37 +00:00
handleConfigButton ( ) {
2023-05-15 20:36:28 +00:00
if ( ! this . actionLock && this . status === "stopped" ) { // if the action lock is false, and the node is stopped, then navigate to the conig page with the node infor in the search query
2023-05-17 21:40:37 +00:00
goToPage ( "config.html" , { node : this . node . name , type : this . type , vmid : this . vmid } ) ;
2023-05-15 20:36:28 +00:00
}
}
2023-05-17 21:40:37 +00:00
handleConsoleButton ( ) {
2023-05-15 20:36:28 +00:00
if ( ! this . actionLock && this . status === "running" ) {
2023-05-17 21:40:37 +00:00
let data = { console : ` ${ this . type === "qemu" ? "kvm" : "lxc" } ` , vmid : this . vmid , vmname : this . name , node : this . node . name , resize : "off" , cmd : "" } ;
2023-05-15 20:36:28 +00:00
data [ ` ${ this . type === "qemu" ? "novnc" : "xtermjs" } ` ] = 1 ;
goToURL ( PVE , data , true ) ;
}
}
2023-05-17 21:40:37 +00:00
handleDeleteButton ( ) {
2023-05-15 20:36:28 +00:00
if ( ! this . actionLock && this . status === "stopped" ) {
let header = ` Delete VM ${ this . vmid } ` ;
let body = ` <p>Are you sure you want to <strong>delete</strong> VM </p><p> ${ this . vmid } </p> `
dialog ( header , body , async ( result , form ) => {
if ( result === "confirm" ) {
this . actionLock = true ;
let prevStatus = this . status ;
this . status = "loading" ;
this . update ( ) ;
let action = { } ;
action . purge = 1 ;
action [ "destroy-unreferenced-disks" ] = 1 ;
let body = {
node : this . node . name ,
type : this . type ,
vmid : this . vmid ,
action : JSON . stringify ( action )
} ;
let result = await requestAPI ( "/instance" , "DELETE" , body ) ;
if ( result . status === 200 ) {
2023-05-15 21:24:43 +00:00
this . shadowElement . parentElement . removeChild ( this . shadowElement ) ;
2023-05-15 20:36:28 +00:00
}
else {
alert ( result . error ) ;
this . status = this . prevStatus ;
this . update ( ) ;
this . actionLock = false ;
}
}
} ) ;
}
}
2022-12-11 02:02:29 +00:00
}