├── Debug.js ├── ProxmoxAPI.js ├── ProxmoxWidget.js └── README.md /Debug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the common logic for both the Node.js and web browser 3 | * implementations of `debug()`. 4 | * https://github.com/debug-js/debug#readme 5 | */ 6 | 7 | function setup(env) { 8 | 9 | createDebug.debug = createDebug; 10 | createDebug.default = createDebug; 11 | createDebug.coerce = coerce; 12 | createDebug.disable = disable; 13 | createDebug.enable = enable; 14 | createDebug.enabled = enabled; 15 | 16 | createDebug.humanize = function(){}; 17 | createDebug.destroy = destroy; 18 | 19 | createDebug.log = log; 20 | createDebug.formatArgs = formatArgs; 21 | createDebug.save = save; 22 | createDebug.load = load; 23 | createDebug.useColors = useColors; 24 | createDebug.init = init; 25 | createDebug.colors = [ 26 | 20, 27 | 21, 28 | 26, 29 | 27, 30 | 32, 31 | 33, 32 | 38, 33 | 39, 34 | 40, 35 | 41, 36 | 42, 37 | 43, 38 | 44, 39 | 45, 40 | 56, 41 | 57, 42 | 62, 43 | 63, 44 | 68, 45 | 69, 46 | 74, 47 | 75, 48 | 76, 49 | 77, 50 | 78, 51 | 79, 52 | 80, 53 | 81, 54 | 92, 55 | 93, 56 | 98, 57 | 99, 58 | 112, 59 | 113, 60 | 128, 61 | 129, 62 | 134, 63 | 135, 64 | 148, 65 | 149, 66 | 160, 67 | 161, 68 | 162, 69 | 163, 70 | 164, 71 | 165, 72 | 166, 73 | 167, 74 | 168, 75 | 169, 76 | 170, 77 | 171, 78 | 172, 79 | 173, 80 | 178, 81 | 179, 82 | 184, 83 | 185, 84 | 196, 85 | 197, 86 | 198, 87 | 199, 88 | 200, 89 | 201, 90 | 202, 91 | 203, 92 | 204, 93 | 205, 94 | 206, 95 | 207, 96 | 208, 97 | 209, 98 | 214, 99 | 215, 100 | 220, 101 | 221 102 | ]; 103 | 104 | createDebug.inspectOpts = Object.keys(env).filter(key => { 105 | return /^debug_/i.test(key); 106 | }).reduce((obj, key) => { 107 | // Camel-case 108 | const prop = key 109 | .substring(6) 110 | .toLowerCase() 111 | .replace(/_([a-z])/g, (_, k) => { 112 | return k.toUpperCase(); 113 | }); 114 | 115 | // Coerce string value into JS value 116 | let val = env[key]; 117 | console.log("Testing: "+val); 118 | if (/^(yes|on|true|enabled)$/i.test(val)) { 119 | val = true; 120 | } else if (/^(no|off|false|disabled)$/i.test(val)) { 121 | val = false; 122 | } else if (val === 'null') { 123 | val = null; 124 | } else { 125 | val = Number(val); 126 | } 127 | 128 | obj[prop] = val; 129 | return obj; 130 | }, {}); 131 | 132 | Object.keys(env).forEach(key => { 133 | createDebug[key] = env[key]; 134 | }); 135 | 136 | /** 137 | * The currently active debug mode names, and names to skip. 138 | */ 139 | 140 | createDebug.names = []; 141 | createDebug.skips = []; 142 | 143 | /** 144 | * Map of special "%n" handling functions, for the debug "format" argument. 145 | * 146 | * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". 147 | */ 148 | createDebug.formatters = {}; 149 | function getDate() { 150 | if (exports.inspectOpts.hideDate) { 151 | return ''; 152 | } 153 | return new Date().toISOString() + ' '; 154 | } 155 | function log(...args) { 156 | let index = 0; 157 | for (let arg of args){ 158 | index += 1; 159 | console.log("["+index+"] "+ arg); 160 | } 161 | } 162 | function useColors() { 163 | return false 164 | } 165 | 166 | function formatArgs(args) { 167 | const {namespace: name, useColors} = this; 168 | args[0] = getDate() + name + ' ' + args[0]; 169 | 170 | } 171 | 172 | /** 173 | * Save `namespaces`. 174 | * 175 | * @param {String} namespaces 176 | * @api private 177 | */ 178 | function save(namespaces) { 179 | if(namespaces == undefined){ 180 | namespaces = env; 181 | return; 182 | } 183 | const LocalStorage = FileManager.local(); 184 | LocalStorage.write("namespaces.json", namespaces); 185 | } 186 | /** 187 | * Load `namespaces`. 188 | * 189 | * @return {String} returns the previously persisted debug modes 190 | * @api private 191 | */ 192 | 193 | function load() { 194 | const LocalStorage = FileManager.local(); 195 | if(LocalStorage.fileExists("namespaces.json")){ 196 | return LocalStorage.readString("namespaces.json"); 197 | } 198 | } 199 | function init(debug) { 200 | debug.inspectOpts = {}; 201 | 202 | const keys = Object.keys(exports.inspectOpts); 203 | for (let i = 0; i < keys.length; i++) { 204 | debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; 205 | } 206 | } 207 | /** 208 | * Selects a color for a debug namespace 209 | * @param {String} namespace The namespace string for the debug instance to be colored 210 | * @return {Number|String} An ANSI color code for the given namespace 211 | * @api private 212 | */ 213 | function selectColor(namespace) { 214 | let hash = 0; 215 | 216 | for (let i = 0; i < namespace.length; i++) { 217 | hash = ((hash << 5) - hash) + namespace.charCodeAt(i); 218 | hash |= 0; // Convert to 32bit integer 219 | } 220 | 221 | return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; 222 | } 223 | createDebug.selectColor = selectColor; 224 | 225 | /** 226 | * Create a debugger with the given `namespace`. 227 | * 228 | * @param {String} namespace 229 | * @return {Function} 230 | * @api public 231 | */ 232 | function createDebug(namespace) { 233 | let prevTime; 234 | let enableOverride = null; 235 | let namespacesCache; 236 | let enabledCache; 237 | 238 | function debug(...args) { 239 | // Disabled? 240 | if (!debug.enabled) { 241 | return; 242 | } 243 | 244 | const self = debug; 245 | 246 | // Set `diff` timestamp 247 | const curr = Number(new Date()); 248 | const ms = curr - (prevTime || curr); 249 | self.diff = ms; 250 | self.prev = prevTime; 251 | self.curr = curr; 252 | prevTime = curr; 253 | 254 | args[0] = createDebug.coerce(args[0]); 255 | 256 | if (typeof args[0] !== 'string') { 257 | // Anything else let's inspect with %O 258 | args.unshift('%O'); 259 | } 260 | 261 | // Apply any `formatters` transformations 262 | let index = 0; 263 | args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { 264 | // If we encounter an escaped % then don't increase the array index 265 | if (match === '%%') { 266 | return '%'; 267 | } 268 | index++; 269 | const formatter = createDebug.formatters[format]; 270 | if (typeof formatter === 'function') { 271 | const val = args[index]; 272 | match = formatter.call(self, val); 273 | 274 | // Now we need to remove `args[index]` since it's inlined in the `format` 275 | args.splice(index, 1); 276 | index--; 277 | } 278 | return match; 279 | }); 280 | 281 | // Apply env-specific formatting (colors, etc.) 282 | createDebug.formatArgs.call(self, args); 283 | 284 | const logFn = self.log || createDebug.log; 285 | logFn.apply(self, args); 286 | } 287 | 288 | debug.namespace = namespace; 289 | debug.useColors = createDebug.useColors(); 290 | debug.color = createDebug.selectColor(namespace); 291 | debug.extend = extend; 292 | debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release. 293 | 294 | Object.defineProperty(debug, 'enabled', { 295 | enumerable: true, 296 | configurable: false, 297 | get: () => { 298 | if (enableOverride !== null) { 299 | return enableOverride; 300 | } 301 | if (namespacesCache !== createDebug.namespaces) { 302 | namespacesCache = createDebug.namespaces; 303 | enabledCache = createDebug.enabled(namespace); 304 | } 305 | 306 | return enabledCache; 307 | }, 308 | set: v => { 309 | enableOverride = v; 310 | } 311 | }); 312 | 313 | // Env-specific initialization logic for debug instances 314 | if (typeof createDebug.init === 'function') { 315 | createDebug.init(debug); 316 | } 317 | 318 | return debug; 319 | } 320 | 321 | function extend(namespace, delimiter) { 322 | const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace); 323 | newDebug.log = this.log; 324 | return newDebug; 325 | } 326 | 327 | /** 328 | * Enables a debug mode by namespaces. This can include modes 329 | * separated by a colon and wildcards. 330 | * 331 | * @param {String} namespaces 332 | * @api public 333 | */ 334 | function enable(namespaces) { 335 | createDebug.save(namespaces); 336 | createDebug.namespaces = namespaces; 337 | 338 | createDebug.names = []; 339 | createDebug.skips = []; 340 | 341 | let i; 342 | const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); 343 | const len = split.length; 344 | 345 | for (i = 0; i < len; i++) { 346 | if (!split[i]) { 347 | // ignore empty strings 348 | continue; 349 | } 350 | 351 | namespaces = split[i].replace(/\*/g, '.*?'); 352 | 353 | if (namespaces[0] === '-') { 354 | createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$')); 355 | } else { 356 | createDebug.names.push(new RegExp('^' + namespaces + '$')); 357 | } 358 | } 359 | } 360 | 361 | /** 362 | * Disable debug output. 363 | * 364 | * @return {String} namespaces 365 | * @api public 366 | */ 367 | function disable() { 368 | const namespaces = [ 369 | ...createDebug.names.map(toNamespace), 370 | ...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace) 371 | ].join(','); 372 | createDebug.enable(''); 373 | return namespaces; 374 | } 375 | 376 | /** 377 | * Returns true if the given mode name is enabled, false otherwise. 378 | * 379 | * @param {String} name 380 | * @return {Boolean} 381 | * @api public 382 | */ 383 | function enabled(name) { 384 | if (name[name.length - 1] === '*') { 385 | return true; 386 | } 387 | 388 | let i; 389 | let len; 390 | 391 | for (i = 0, len = createDebug.skips.length; i < len; i++) { 392 | if (createDebug.skips[i].test(name)) { 393 | return false; 394 | } 395 | } 396 | 397 | for (i = 0, len = createDebug.names.length; i < len; i++) { 398 | if (createDebug.names[i].test(name)) { 399 | return true; 400 | } 401 | } 402 | 403 | return false; 404 | } 405 | 406 | /** 407 | * Convert regexp to namespace 408 | * 409 | * @param {RegExp} regxep 410 | * @return {String} namespace 411 | * @api private 412 | */ 413 | function toNamespace(regexp) { 414 | return regexp.toString() 415 | .substring(2, regexp.toString().length - 2) 416 | .replace(/\.\*\?$/, '*'); 417 | } 418 | 419 | /** 420 | * Coerce `val`. 421 | * 422 | * @param {Mixed} val 423 | * @return {Mixed} 424 | * @api private 425 | */ 426 | function coerce(val) { 427 | if (val instanceof Error) { 428 | return val.stack || val.message; 429 | } 430 | return val; 431 | } 432 | 433 | /** 434 | * XXX DO NOT USE. This is a temporary stub function. 435 | * XXX It WILL be removed in the next major release. 436 | */ 437 | function destroy() { 438 | console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'); 439 | } 440 | 441 | createDebug.enable(createDebug.load()); 442 | 443 | return createDebug; 444 | } 445 | 446 | module.exports = setup; 447 | -------------------------------------------------------------------------------- /ProxmoxWidget.js: -------------------------------------------------------------------------------- 1 | 2 | let pve = importModule("ProxmoxAPI"); 3 | let private = importModule("PrivateData"); 4 | 5 | //You can edit these or create your own private module 6 | let password = private.password; 7 | let username = private.username; 8 | 9 | let proxmoxURL = "https://192.168.1.25"; 10 | let client = new pve.PveClient(proxmoxURL); 11 | 12 | let nodeOrVmName = args.widgetParameter; 13 | 14 | //I only have one node, so enabling this clones the single node to test multi node UI 15 | let debugMulti = false; 16 | if( nodeOrVmName == "multiTest"){ 17 | debugMulti = true; 18 | } 19 | 20 | let colors = [Color.purple(), Color.blue(), Color.orange(), Color.brown(), Color.cyan(), Color.magenta(), Color.yellow()] 21 | 22 | let nodes = await loadNodes(); 23 | let widget = await createWidget(nodes); 24 | 25 | if (config.runsInWidget) { 26 | // The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen. 27 | Script.setWidget(widget) 28 | } else { 29 | // The script runs inside the app, so we preview the widget. 30 | widget.presentLarge() 31 | } 32 | // Calling Script.complete() signals to Scriptable that the script have finished running. 33 | // This can speed up the execution, in particular when running the script from Shortcuts or using Siri. 34 | Script.complete() 35 | 36 | async function createWidget(nodes) { 37 | let widget = new ListWidget() 38 | let now = new Date(); 39 | let seconds_in_future = 10 * 1000; 40 | now.setTime(now.getTime() + seconds_in_future ); 41 | //Hopefully this forces the widget to update more frequently for close to real-time 42 | widget.refreshAfterDate = now; 43 | 44 | // Add background gradient 45 | let gradient = new LinearGradient() 46 | gradient.locations = [0, 1] 47 | gradient.colors = [ 48 | new Color("262626") 49 | ] 50 | widget.backgroundGradient = gradient 51 | let widgetInfo = widget.addStack(); 52 | let updatedStack = widgetInfo.addText("Widget updated @"+new Date().toLocaleTimeString()+ " next: "+now.toLocaleTimeString()); 53 | updatedStack.textColor = Color.orange() 54 | updatedStack.textOpacity = 0.9; 55 | updatedStack.font = Font.mediumSystemFont(13); 56 | 57 | if( nodeOrVmName !== undefined){ 58 | let nodeFound = findNode(nodeOrVmName, nodes); 59 | if ( nodeFound != undefined){ 60 | return oneNodeUI(widget, nodeFound) 61 | } 62 | let vmFound = findVM(nodeOrVmName, nodes); 63 | if (vmFound != undefined){ 64 | return singleVMUI(widget, vmFound.node, vmFound.VM); 65 | } 66 | } 67 | 68 | if(nodes.length > 1){ 69 | await multipleNodesUI(widget, nodes); 70 | }else{ 71 | await oneNodeUI(widget, nodes[0]); 72 | } 73 | return widget 74 | } 75 | async function singleVMUI(widget, node, vm){ 76 | let VM = vm; 77 | //Stack for single VM name and status 78 | let TitleStack = widget.addStack(); 79 | TitleStack.borderColor = Color.orange() 80 | TitleStack.borderWidth = 0.5 81 | let UptimeStack = widget.addStack(); 82 | let nameElement = TitleStack.addText("Status of "+VM.name+":"); 83 | nameElement.textColor = Color.white(); 84 | nameElement.font = Font.mediumSystemFont(25); 85 | let statusElement = TitleStack.addText(" "+VM.status); 86 | statusElement.textColor = (VM.status == "running") ? Color.green() : Color.red(); 87 | statusElement.font = Font.mediumSystemFont(25); 88 | 89 | let Detail1 = widget.addStack(); 90 | let memUsed = VM.mem; 91 | let memTotal = VM.maxmem; 92 | let mem_Text_Size = 14 93 | let mem_opacity = 0.7; 94 | let mem_Default_color = Color.white(); 95 | let mem_Special_color = Color.green(); 96 | let mem = Detail1.addText("mem used:"); 97 | mem.textColor = mem_Default_color 98 | mem.textOpacity = mem_opacity 99 | mem.font = Font.mediumSystemFont(mem_Text_Size) 100 | mem = Detail1.addText(String(Math.floor((memUsed/memTotal)*100))); 101 | mem.textColor = mem_Special_color 102 | mem.textOpacity = mem_opacity 103 | mem.font = Font.mediumSystemFont(mem_Text_Size) 104 | mem = Detail1.addText("% ("); 105 | mem.textColor = mem_Default_color 106 | mem.textOpacity = mem_opacity 107 | mem.font = Font.mediumSystemFont(mem_Text_Size) 108 | mem = Detail1.addText(Math.floor((memUsed / 1000000) * 100) / 100+" MB"); 109 | mem.textColor = mem_Special_color 110 | mem.textOpacity = mem_opacity 111 | mem.font = Font.mediumSystemFont(mem_Text_Size) 112 | mem = Detail1.addText("/"); 113 | mem.textColor = mem_Default_color 114 | mem.textOpacity = mem_opacity 115 | mem.font = Font.mediumSystemFont(mem_Text_Size) 116 | mem = Detail1.addText(Math.floor((memTotal / 1000000) * 100) / 100+" MB"); 117 | mem.textColor = mem_Special_color 118 | mem.textOpacity = mem_opacity 119 | mem.font = Font.mediumSystemFont(mem_Text_Size) 120 | mem = Detail1.addText(")"); 121 | mem.textColor = mem_Default_color 122 | mem.textOpacity = mem_opacity 123 | mem.font = Font.mediumSystemFont(mem_Text_Size) 124 | 125 | let NetDetail = widget.addStack(); 126 | let netin_to_MB = Math.floor((VM.netin / 1000000) * 100) / 100; 127 | let netout_to_MB = Math.floor((VM.netout / 1000000) * 100) / 100; 128 | let net_default_size = 15 129 | let net_opacity = 0.7; 130 | let net_default_color = Color.white(); 131 | let net_special_color = Color.green(); 132 | //Splits text on same text in order to color the numbers 133 | let net = NetDetail.addText("net usage "); 134 | net.textColor = net_default_color 135 | net.textOpacity = net_opacity 136 | net.font = Font.mediumSystemFont(net_default_size) 137 | net = NetDetail.addText(String(netin_to_MB)); 138 | net.textColor = net_special_color 139 | net.textOpacity = net_opacity 140 | net.font = Font.mediumSystemFont(net_default_size) 141 | net = NetDetail.addText(" MB in, "); 142 | net.textColor = net_default_color 143 | net.textOpacity = net_opacity 144 | net.font = Font.mediumSystemFont(net_default_size) 145 | net = NetDetail.addText(String(netout_to_MB)); 146 | net.textColor = net_special_color 147 | net.textOpacity = net_opacity 148 | net.font = Font.mediumSystemFont(net_default_size) 149 | net = NetDetail.addText(" MB out"); 150 | net.textColor = net_default_color 151 | net.textOpacity = net_opacity 152 | net.font = Font.mediumSystemFont(net_default_size) 153 | let Config_net_stack = widget.addStack(); 154 | let config_net = Config_net_stack.addText("Net config: "+VM.config.net0); 155 | config_net.textColor = Color.white() 156 | config_net.textOpacity = 0.7 157 | config_net.font = Font.mediumSystemFont(13) 158 | let Cpu_stack = widget.addStack(); 159 | let cpu = Cpu_stack.addText(VM.cpus+" cpus, cpu usage ") 160 | let cpu_text_size = 13; 161 | let cpu_default_color = Color.white(); 162 | let cpu_special_color = Color.green(); 163 | let cpu_opacity = 0.7 164 | cpu.textColor = cpu_default_color 165 | cpu.textOpacity = cpu_opacity 166 | cpu.font = Font.mediumSystemFont(cpu_text_size) 167 | cpu = Cpu_stack.addText((VM.cpu / VM.cpus).toFixed(3)+ "% ") 168 | cpu.textColor = cpu_special_color 169 | cpu.textOpacity = cpu_opacity 170 | cpu.font = Font.mediumSystemFont(cpu_text_size) 171 | cpu = Cpu_stack.addText("("+VM.cpu.toFixed(4)+" / "+VM.cpus+")"); 172 | cpu.textColor = cpu_default_color 173 | cpu.textOpacity = cpu_opacity 174 | cpu.font = Font.mediumSystemFont(cpu_text_size) 175 | if (VM.status == "running"){ 176 | let uptime = UptimeStack.addText("Uptime: "+getTime(VM.uptime)); 177 | uptime.textColor = Color.white(); 178 | uptime.font = Font.mediumSystemFont(13); 179 | widget.addSpacer(2) 180 | } 181 | return widget; 182 | } 183 | 184 | function findNode(nodeName, inNodes){ 185 | for (let nodeIndex in nodes){ 186 | let node = nodes[nodeIndex]; 187 | if( node.node == nodeName){ 188 | return node; 189 | } 190 | } 191 | } 192 | function findVM(VMName, inNodes){ 193 | for (let nodeIndex in nodes){ 194 | let node = nodes[nodeIndex]; 195 | for( let index in node.VMs){ 196 | let vm = node.VMs[index]; 197 | if(vm.name == VMName){ 198 | return {VM: vm, node: node}; 199 | } 200 | } 201 | } 202 | } 203 | 204 | async function oneNodeUI(widget, node){ 205 | let MAX_NODES_TO_SHOW = 5; 206 | let titleStack = widget.addStack() 207 | let statStack = widget.addStack(); 208 | let newdetailStack = widget.addStack(); 209 | let detailStack = widget.addStack(); 210 | let statData = node.status; 211 | let memUsed = statData.memory.used; 212 | let memTotal = statData.memory.total; 213 | let mem = statStack.addText("mem used: "+Math.floor((memUsed/memTotal)*100)+"% ("+memUsed+"/"+memTotal+")"); 214 | mem.textColor = Color.white() 215 | mem.textOpacity = 0.7 216 | mem.font = Font.mediumSystemFont(13) 217 | let titleElement = titleStack.addText("Only showing "+MAX_NODES_TO_SHOW+" out of "+node.VMs.length+" VMs on node "+node.node); 218 | titleElement.textColor = Color.white() 219 | titleElement.textOpacity = 0.7 220 | titleElement.font = Font.mediumSystemFont(13) 221 | let uptime = detailStack.addText("node uptime: "+getTime(node.uptime)); 222 | uptime.textColor = Color.white(); 223 | uptime.textOpacity = 0.7 224 | uptime.font = Font.mediumSystemFont(13); 225 | let Statuses = await GetAllVMsProperty(node.VMs, "status"); 226 | let statuStack = widget.addStack(); 227 | let statTile = statuStack.addText("VMs "); 228 | statTile.textColor = Color.white(); 229 | statTile.textOpacity = 0.7 230 | statTile.font = Font.mediumSystemFont(13); 231 | for (key in Statuses) { 232 | let occurenceAmount = Statuses[key]; 233 | let statElement = statuStack.addText(" "+key+" : "+occurenceAmount); 234 | statElement.textColor = (key == "running") ? Color.green() : Color.red(); 235 | statElement.textOpacity = 0.7 236 | statElement.font = Font.mediumSystemFont(13) 237 | } 238 | widget.addSpacer(12) 239 | for( let index in node.VMs){ 240 | if (index > MAX_NODES_TO_SHOW){ break; } 241 | let VM = node.VMs[index]; 242 | //Stack for single VM name and status 243 | let VMStack = widget.addStack(); 244 | let nameElement = VMStack.addText(VM.name); 245 | nameElement.textColor = Color.white(); 246 | nameElement.font = Font.mediumSystemFont(13); 247 | let dashes = VMStack.addText(" --- "); 248 | dashes.textColor = Color.white(); 249 | dashes.font = Font.mediumSystemFont(13); 250 | let statusElement = VMStack.addText(VM.status); 251 | statusElement.textColor = (VM.status == "running") ? Color.green() : Color.red(); 252 | statusElement.font = Font.mediumSystemFont(13); 253 | if (VM.status == "running"){ 254 | let uptime = VMStack.addText(" uptime: "+getTime(VM.uptime)); 255 | uptime.textColor = Color.white(); 256 | uptime.font = Font.mediumSystemFont(13); 257 | widget.addSpacer(2) 258 | } 259 | // let net = VMStack.addText(" net: "+VM.config.net0); 260 | // net.textColor = Color.white(); 261 | // net.font = Font.mediumSystemFont(13); 262 | // widget.addSpacer(2) 263 | } 264 | let logs = await GetNodeLogs(node.node); 265 | let Log_title_stack = widget.addStack(); 266 | let title = Log_title_stack.addText("NODE LOGS"); 267 | title.textColor = Color.white(); 268 | title.font = Font.mediumSystemFont(20); 269 | let Num_Logs_To_Show = 6 270 | for ( let i = Num_Logs_To_Show; i > 0; i--){ 271 | console.log(i); 272 | let Log_Line1_Stack = widget.addStack(); 273 | let first_line = Log_Line1_Stack.addText(logs[logs.length - (i + 1)].t); 274 | first_line.textColor = colors[i]; 275 | first_line.font = Font.mediumSystemFont(8); 276 | } 277 | return widget; 278 | } 279 | 280 | async function multipleNodesUI(widget, nodes){ 281 | let colorIndex = 0; 282 | // Show app icon and title 283 | for (let nodeIndex in nodes){ 284 | if (colorIndex == colors.length){ colorIndex = 0; } 285 | let node = nodes[nodeIndex]; 286 | let table = widget 287 | let titleStack = widget.addStack() 288 | let statStack = widget.addStack(); 289 | let statData = node.status; 290 | let memUsed = statData.memory.used; 291 | let memTotal = statData.memory.total; 292 | let mem = statStack.addText("mem used: "+Math.floor((memUsed/memTotal)*100)+"% ("+memUsed+"/"+memTotal+")"); 293 | mem.textColor = colors[colorIndex]; 294 | mem.textOpacity = 0.7 295 | mem.font = Font.mediumSystemFont(13) 296 | let titleElement = titleStack.addText("Status of node "+node.node+" uptime: "+getTime(node.uptime)); 297 | titleElement.textColor = colors[colorIndex]; 298 | titleElement.textOpacity = 0.7 299 | titleElement.font = Font.mediumSystemFont(13) 300 | let Statuses = await GetAllVMsProperty(node.VMs, "status"); 301 | let statuStack = widget.addStack(); 302 | let statTile = statuStack.addText("VMs "); 303 | statTile.textColor = colors[colorIndex]; 304 | statTile.textOpacity = 0.7 305 | statTile.font = Font.mediumSystemFont(13); 306 | for (key in Statuses) { 307 | let occurenceAmount = Statuses[key]; 308 | let statElement = statuStack.addText(" "+key+" : "+occurenceAmount); 309 | statElement.textColor = (key == "running") ? Color.green() : Color.red(); 310 | statElement.textOpacity = 0.7 311 | statElement.font = Font.mediumSystemFont(13) 312 | statElement.shadowColor = colors[colorIndex]; 313 | statElement.shadowRadius = 1; 314 | } 315 | widget.addSpacer(10); 316 | colorIndex += 1; 317 | } 318 | return widget 319 | } 320 | 321 | async function loadNodes() { 322 | 323 | client.logEnabled = true; 324 | //client.apiToken = "3049c8dc-7655-45ad-b494-1ef67c6084fe" 325 | let login = await client.login(username, password, 'pam'); 326 | if (login) { 327 | let serverData = {}; 328 | let allNodes = (await client.nodes.index()).response.data; 329 | for (nodeIndex in allNodes){ 330 | let nodeName = allNodes[nodeIndex].node; 331 | let status = await GetStat(nodeName); 332 | let nodeVMs = await GetVMs(nodeName); 333 | allNodes[nodeIndex].status = status; 334 | allNodes[nodeIndex].VMs = nodeVMs; 335 | } 336 | if( debugMulti) { 337 | //Adds multiple copies of the same node for debugging 338 | let copy = allNodes[0]; 339 | allNodes.push(copy); 340 | allNodes.push(copy); 341 | allNodes.push(copy); 342 | } 343 | // console.warn(allNodes); 344 | return allNodes; 345 | } 346 | return []; 347 | } 348 | async function GetNodeLogs(nodeName){ 349 | return (await client.nodes.get(nodeName).syslog.syslog(5)).response.data; 350 | } 351 | async function GetAllVMsProperty(VMs, property){ 352 | let occurences = {}; 353 | for( let index in VMs){ 354 | let vm = VMs[index]; 355 | let propValue = vm[property]; 356 | if (occurences[propValue] !== undefined){ 357 | occurences[propValue] += 1; 358 | }else{ 359 | occurences[propValue] = 1; 360 | } 361 | } 362 | return occurences; 363 | } 364 | function getTime(fromSeconds){ 365 | let minutes = Math.floor((fromSeconds / 60) * 100) / 100; 366 | let hours = Math.floor((minutes / 60) * 100) / 100; 367 | if( minutes > 60){ 368 | return hours+" hours"; 369 | } 370 | return minutes+" minutes" 371 | } 372 | async function GetStat(onNode){ 373 | return (await client.nodes.get(onNode).status.status()).response.data; 374 | } 375 | async function GetVMs(onNode){ 376 | let VMS = (await client.nodes.get(onNode).qemu.vmlist(0)).response.data; 377 | for (VMIndex in VMS) { 378 | let vm = VMS[VMIndex]; 379 | let vmnodeid = (await client.nodes.get(onNode).qemu.get(vm.vmid)) 380 | let conf = (await vmnodeid.config.vmConfig(true)).response.data; 381 | vm.config = conf; 382 | } 383 | return VMS; 384 | } 385 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScriptableProxmox 2 | A widget script for the Scriptable app on Iphone. You can: 3 | 1. View a limited amount of nodes at a time, its status, uptime and VMs running/down 4 | 2. View a specific node, running VMs, all VMs running/down and a limited amount of logs 5 | 3. View a specific VM details such as status, uptime, net usage, mem usage, net configs and CPU usage 6 | 7 | # How do i get started 8 | You first need to grab your proxmox login info and then either 9 | 10 | A) Create a "PrivateData" module that returns: 11 | 12 | `module.exports = {username: "root", password: "supersecret"}` 13 | 14 | or B) Delete the "PrivateData" variable and edit the username and password variables directly 15 | 16 | You will also need to create new modules for all the scripts in this repo (minus the README of course) and name them just as their filename. If you want to thank the original authors, their github is at the top of the scripts. 17 | 18 | # Different Widgets 19 | To get a different type of widget you need to pass an argument when editing the widget. You can pass a node name or a VM name, it will then look for them, it will first look for the node and then VM list if it's not found in the node list. If you have a VM that conflicts with a node, it will return the node first. 20 | 21 | If you want to view the multi-node list, you can pass "multiTest" as the argument, this will copy the first node multiple times. I suggest you only do this if you don't have more than one node, doing so would make your multi-node list view look not so pretty. 22 | 23 | # ScriptableProxmox pseudo code 24 | ``` 25 | Passed Arguments? 26 | |\_No 27 | | |\_Has More than one node? 28 | | |\_No 29 | | | |\_Display Single Node View 30 | | |\_Yes 31 | | |\_Display multi Node view 32 | |\_Yes 33 | |\_Is arg multiTest? 34 | | |\_Yes 35 | | |\_ Clone first node multiple times 36 | |\_Is arg a node? 37 | | |\_Yes 38 | | |\_Display Single node View 39 | |\_Is arg a VM 40 | |\_Yes 41 | |\_Display Vm view 42 | ``` 43 |
44 |
45 |