├── .gitignore ├── .gitmodules ├── README.md ├── ToDo.md ├── cc ├── cw ├── disks └── .gitignore ├── doc ├── disks.png ├── guest_creation.png ├── guests.png ├── host.png └── isos.png ├── index.js ├── isos └── .gitignore ├── lib ├── app.js ├── config.coffee ├── drives.js ├── events.js ├── host.js ├── parser.js ├── qemu.coffee ├── socketServer.coffee ├── src │ ├── args.coffee │ ├── disk.coffee │ ├── disk │ │ ├── create.coffee │ │ ├── delete.coffee │ │ └── info.coffee │ ├── host.coffee │ ├── parser.coffee │ ├── pin.coffee │ ├── process.coffee │ ├── qmp.coffee │ ├── usb.coffee │ ├── version.coffee │ ├── vm.coffee │ ├── vmCfg.coffee │ └── vmHandlerExtensions │ │ ├── RESET.coffee │ │ ├── RESUME.coffee │ │ ├── SHUTDOWN.coffee │ │ ├── START.coffee │ │ └── STOP.coffee ├── uuid.js ├── vm.js ├── vmHandler.coffee ├── vms.js ├── webServer.coffee └── webService.js ├── package.json ├── public ├── .gitignore ├── 0.html ├── abc.html ├── app.js ├── aql │ ├── aqlController.js │ └── aqlView.html ├── collection │ ├── collectionController.js │ └── collectionView.html ├── collections │ ├── collectionsController.js │ └── collectionsView.html ├── collectionsBar │ ├── collectionsBarController.js │ ├── collectionsBarView.html │ └── collectionsController.js ├── css │ ├── bootstrap-4-0-0.css │ ├── bootstrap-4-0-0.css.map │ ├── custom.css │ ├── custom2.css │ ├── font-awesome.css │ ├── img │ │ └── jsoneditor-icons.svg │ ├── jsoneditor-flex.css │ └── jsoneditor.css ├── directives │ ├── aqlResultTable.js │ ├── feedbackDirective.js │ └── journalSizeDirective.js ├── document │ ├── documentController.js │ ├── documentRouteController.js │ └── documentView.html ├── drives │ ├── drivesController.js │ └── drivesView.html ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── graph │ ├── graphController.js │ └── graphView.html ├── home │ ├── homeController.js │ └── homeView.html ├── index.html ├── lib │ ├── angular-1-4-9.js │ ├── angular-1-5-0.js │ ├── angular-1-5-5.js │ ├── angular-animate-1-4-9.js │ ├── angular-animate-1-5-0.js │ ├── angular-animate-1-5-5.js │ ├── angular-route-1-4-9.js │ ├── angular-route-1-5-0.js │ ├── angular-route-1-5-5.js │ ├── angular-sanitize-1-5-0.js │ ├── angular-sanitize-1-5-5.js │ ├── bootstrap-4-0-0.js │ ├── jquery-2-2-0.js │ ├── jquery-2-2-3.js │ ├── jsoneditor-5-1-3.js │ ├── ngjsoneditor-1-0-0.js │ ├── requirejs-2-1-11.js │ └── requirejs-2-2-0.js ├── main.js ├── manage │ ├── collectionsController.js │ └── collectionsView.html ├── navBar │ ├── navBarController.js │ └── navBarView.html ├── services │ ├── fastFilterService.js │ ├── formatService.js │ ├── messageBrokerService.js │ ├── queriesService.js │ ├── queryService.js │ └── testService.js └── vms │ ├── 0.html │ ├── 1.html │ ├── 2.html │ ├── 3.html │ ├── 4.html │ ├── 5.html │ ├── 6.html │ ├── 7.html │ ├── 8.html │ ├── from6.html │ ├── vmsController.js │ └── vmsView.html ├── qemu ├── config.js ├── img.js ├── process.js └── qmp.js ├── server.coffee ├── test ├── args.coffee ├── args.json └── confs.coffee ├── tests.js ├── toYaml.coffee ├── vmConfigs └── .gitignore └── webSrc ├── 0.html ├── app.js ├── aql ├── aqlController.js └── aqlView.html ├── collection ├── collectionController.js └── collectionView.html ├── collectionsBar ├── collectionsBarController.js └── collectionsBarView.html ├── css ├── bootstrap-4-0-0.css ├── bootstrap-4-0-0.css.map ├── custom.css ├── custom2.css ├── font-awesome.css ├── img │ └── jsoneditor-icons.svg ├── jsoneditor-flex.css └── jsoneditor.css ├── directives ├── aqlResultTable.js ├── feedbackDirective.js └── journalSizeDirective.js ├── document ├── documentController.js ├── documentRouteController.js └── documentView.html ├── drives ├── drivesController.js └── drivesView.html ├── graph ├── graphController.js └── graphView.html ├── home ├── homeController.js └── homeView.html ├── index.html ├── main.js ├── manage ├── collectionsController.js └── collectionsView.html ├── navBar ├── navBarController.js └── navBarView.html ├── services ├── fastFilterService.js ├── formatService.js ├── messageBrokerService.js ├── queriesService.js ├── queryService.js └── testService.js └── vms ├── 0.html ├── 1.html ├── 2.html ├── 3.html ├── 4.html ├── 5.html ├── 6.html ├── 7.html ├── 8.html ├── vmsController.js └── vmsView.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test.* 3 | /node_modules 4 | /test_old 5 | /pids.json 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "public/vendor"] 2 | path = public/vendor 3 | url = https://github.com/baslr/web-client-vendor 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-qemu-server 2 | ================ 3 | node-qemu-server lets you control virtual machines in your webbrowser. 4 | 5 | ### Requirements 6 | 7 | #### Linux 8 | * LSB Linux x86_64 (tested with Debian (Sid) GNU/Linux) 9 | * qemu-system-x86 v1.6.1 10 | * nodejs v0.10.21 11 | * npm 12 | * packages: {numactl, usbutils} (usb and numa are only available on Linux) 13 | 14 | #### OS X 15 | * v10.8 / v10.9 x86_64 16 | * macports qemu v1.6.1 17 | 18 | ### Installation 19 | Install node-qemu-server on Debian GNU/Linux and OS X (assume you have installed qemu, node, npm and numactl) 20 | 21 | $ git clone https://github.com/baslr/node-qemu-server 22 | $ cd node-qemu-server 23 | $ npm install 24 | $ git submodule init 25 | $ git submodule update 26 | $ cd public/vendor/ 27 | $ bower install 28 | $ cd ../../ 29 | $ ./cc 30 | $ node server 31 | 32 | Now open your HTML5 Webbrowser and open http://127.0.0.1:4224 33 | 34 |  35 |  36 |  37 |  38 |  39 | 40 | --- 41 | setup and control qemu instances with Node.js 42 | 43 | more to come in the future 44 | 45 | vision: 46 | setup and control qemu instances via web gui, lean and simple 47 | 48 | 49 | ### implemented qmp commands 50 | 51 | #### system commands 52 | ##### pause, reset, resume, shutdown, stop 53 | 54 | node-qemu command | qmp command 55 | :--------------|:------------------- 56 | qVm.pause() | {"name": "stop"} 57 | qVm.reset() | {"name": "system_reset"} 58 | qVm.resume() | {"name": "cont"} 59 | qVm.shutdown() | {"name": "system_powerdown"} 60 | qVm.stop() | {"name": "quit"} 61 | qVm.status() | {"name": "query-status"} 62 | 63 | 64 | # in work 65 | {"name": "qom-list-types"} 66 | {"name": "change-vnc-password"} 67 | {"name": "qom-get"} 68 | {"name": "qom-set"} 69 | {"name": "qom-list"} 70 | {"name": "query-block-jobs"} 71 | {"name": "query-balloon"} 72 | {"name": "query-migrate"} 73 | {"name": "query-uuid"} 74 | {"name": "query-name"} 75 | {"name": "query-spice"} 76 | {"name": "query-vnc"} 77 | {"name": "query-mice"} 78 | {"name": "query-kvm"} 79 | {"name": "query-pci"} 80 | {"name": "query-cpus"} 81 | {"name": "query-blockstats"} 82 | {"name": "query-block"} 83 | {"name": "query-chardev"} 84 | {"name": "query-commands"} 85 | {"name": "query-version"} 86 | {"name": "human-monitor-command"} 87 | {"name": "qmp_capabilities"} 88 | {"name": "add_client"} 89 | {"name": "expire_password"} 90 | {"name": "set_password"} 91 | {"name": "block_set_io_throttle"} 92 | {"name": "block_passwd"} 93 | {"name": "closefd"} 94 | {"name": "getfd"} 95 | {"name": "set_link"} 96 | {"name": "balloon"} 97 | {"name": "blockdev-snapshot-sync"} 98 | {"name": "transaction"} 99 | {"name": "block-job-cancel"} 100 | {"name": "block-job-set-speed"} 101 | {"name": "block-stream"} 102 | {"name": "block_resize"} 103 | {"name": "netdev_del"} 104 | {"name": "netdev_add"} 105 | {"name": "client_migrate_info"} 106 | {"name": "migrate_set_downtime"} 107 | {"name": "migrate_set_speed"} 108 | {"name": "migrate_cancel"} 109 | {"name": "migrate"} 110 | {"name": "xen-save-devices-state"} 111 | {"name": "inject-nmi"} 112 | {"name": "pmemsave"} 113 | {"name": "memsave"} 114 | {"name": "cpu"} 115 | {"name": "device_del"} 116 | {"name": "device_add"} 117 | 118 | {"name": "system_wakeup"} 119 | {"name": "screendump"} 120 | {"name": "change"} 121 | {"name": "eject"} 122 | -------------------------------------------------------------------------------- /ToDo.md: -------------------------------------------------------------------------------- 1 | #### different ways to crate an image 2 | qemu = require './lib/qemu' 3 | conf = {name:'myImage', size:10} # 10GiByte 4 | 5 | qemu.createImage conf, (ret) -> 6 | ret.image # image object 7 | ret.status # status 8 | 9 | qImage = new qemu.Image conf 10 | qImage.create (ret) -> 11 | if ret.status is 'success' 12 | # creation ok 13 | 14 | qImage = new qemu.Image() 15 | qImage.create conf, (ret) -> 16 | if ret.status is 'success' 17 | # creation ok 18 | 19 | 20 | 21 | @ToDo 22 | 23 | 24 | 25 | vmHandler.scanForRunningVms 26 | - qmp port scann de ports die belegt sind 27 | 28 | 29 | Delete disk 30 | - click on button 31 | - client emits delete-disk 32 | - server trys to delete disk 33 | - server emits delete-disk 34 | - client removes it from gui -------------------------------------------------------------------------------- /cc: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | 4 | ./node_modules/coffee-script/bin/coffee -c . 5 | -------------------------------------------------------------------------------- /cw: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | 4 | ./node_modules/coffee-script/bin/coffee -cw . 5 | -------------------------------------------------------------------------------- /disks/.gitignore: -------------------------------------------------------------------------------- 1 | *.img 2 | *.json 3 | -------------------------------------------------------------------------------- /doc/disks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baslr/node-qemu-server/154d9c1a3b0eca0ddcd59e93fead5c17d29d13ed/doc/disks.png -------------------------------------------------------------------------------- /doc/guest_creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baslr/node-qemu-server/154d9c1a3b0eca0ddcd59e93fead5c17d29d13ed/doc/guest_creation.png -------------------------------------------------------------------------------- /doc/guests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baslr/node-qemu-server/154d9c1a3b0eca0ddcd59e93fead5c17d29d13ed/doc/guests.png -------------------------------------------------------------------------------- /doc/host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baslr/node-qemu-server/154d9c1a3b0eca0ddcd59e93fead5c17d29d13ed/doc/host.png -------------------------------------------------------------------------------- /doc/isos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baslr/node-qemu-server/154d9c1a3b0eca0ddcd59e93fead5c17d29d13ed/doc/isos.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!./6-node 2 | 'use strict'; 3 | 4 | const qemuConfig = require('./qemu/config'); 5 | 6 | 7 | // read node version 8 | const versions = process.version.split('.'); 9 | const major = Number( versions[0].match(/\d+/)[0] ); 10 | const minor = Number( versions[1].match(/\d+/)[0] ); 11 | const patch = Number( versions[2].match(/\d+/)[0] ); 12 | 13 | if (versions.length != 3 || major < 5 || (major == 5 && minor < 11) ) { 14 | console.warn('Minimum supported node version is v5.11.0'); 15 | process.exit(); 16 | } 17 | console.log(`Found node version ${major}.${minor}.${patch}, OK`); 18 | 19 | 20 | if (qemuConfig.versions.major < 2 || (qemuConfig.versions.major == 2 && qemuConfig.versions.minor < 5) ) { 21 | console.log('Minimum supported qemu version is v2.5.0'); 22 | process.exit(); 23 | } 24 | console.log(`Found qemu version ${qemuConfig.version}, OK`); 25 | 26 | if (!qemuConfig.vncSupport) { 27 | console.log('qemu has no vnc support'); 28 | process.exit(); 29 | } 30 | console.log('Found qemu vnc, OK'); 31 | 32 | if (!qemuConfig.spiceSupport) { 33 | console.log('qemu has no spice support'); 34 | } else { 35 | console.log('Found qemu spice, OK'); 36 | } 37 | 38 | 39 | const app = require('./lib/app'); 40 | 41 | app.start(); 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | /* 50 | 51 | read qemu images 52 | read qemu isos 53 | 54 | read qemu machines aka configs 55 | -> check if all requirements are mett 56 | 57 | 58 | start webservice 59 | 60 | TODO: 61 | webservice: 62 | file management 63 | -create vm image 64 | -upload vm image 65 | -weget vm image 66 | 67 | -wget iso 68 | -upload iso 69 | 70 | mv management 71 | // result is emited to all connected clients 72 | -create 73 | -edit 74 | -delete (with image deletion) 75 | 76 | -start 77 | -pause / resume 78 | -stop 79 | -reboot 80 | 81 | -(migrate) 82 | 83 | */ 84 | 85 | 86 | /* 87 | 88 | TODO: 89 | - display network i/o 90 | - display hdd usage x of y GB 91 | - autorestart at x o'clock 92 | 93 | */ -------------------------------------------------------------------------------- /isos/.gitignore: -------------------------------------------------------------------------------- 1 | *.iso 2 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vms = require('./vms'); 4 | const webService = require('./webService'); 5 | 6 | class App { 7 | constructor() { 8 | this.vms = vms; 9 | this.webService = webService; 10 | 11 | vms.on('event', (msg) => { 12 | console.log(`VMS:on:event:${msg.vmUuid}:${msg.event}`); 13 | webService.send('vm-event', msg); 14 | }); 15 | vms.on('status', (msg) => { 16 | console.log(`VMS:on:status:${msg.vmUuid}:${msg.status}`); 17 | webService.send('vm-status', msg); 18 | }); 19 | vms.on('generic', (msg) => { 20 | console.log(`VMS:on:generic:${JSON.stringify(msg)}`); 21 | webService.send('vm-generic', msg); 22 | }); 23 | vms.on('proc-status', (msg) => { 24 | console.log(`VMS:on:proc-status:${JSON.stringify(msg)})`); 25 | webService.send('proc-status', msg); 26 | }); 27 | } // constructor 28 | 29 | start() { 30 | this.vms.restore(); 31 | 32 | this.webService.start(); 33 | } 34 | } 35 | 36 | const app = new App(); 37 | 38 | module.exports = app; 39 | -------------------------------------------------------------------------------- /lib/config.coffee: -------------------------------------------------------------------------------- 1 | 2 | # TODO implement ps for linux 3 | 4 | fs = require 'fs' 5 | os = require 'os' 6 | exec = require('child_process').exec 7 | 8 | vncPorts = {} 9 | qmpPorts = {} 10 | spicePorts = {} 11 | try 12 | pids = require "#{process.cwd()}/pids.json" 13 | catch 14 | pids = {} 15 | 16 | vncPorts[Number port] = false for port in [1..255] 17 | qmpPorts[Number port+15000] = false for port in [1..255] 18 | spicePorts[Number port+15300]= false for port in [1..255] 19 | 20 | module.exports.setToUsed = (proto, port) -> 21 | switch proto 22 | when 'qmp' then qmpPorts[Number port] = true 23 | when 'vnc' then vncPorts[Number port] = true 24 | when 'spice' then spicePorts[Number port] = true 25 | 26 | module.exports.getFreeQMPport = -> 27 | for port,used of qmpPorts 28 | if not used 29 | @setToUsed 'qmp', port 30 | return Number port 31 | 32 | module.exports.getFreeVNCport = -> 33 | for port,used of vncPorts 34 | if not used 35 | @setToUsed 'vnc', port 36 | return Number port 37 | 38 | module.exports.getFreeSPICEport = -> 39 | for port,used of spicePorts 40 | if not used 41 | @setToUsed 'spice', port 42 | return Number port 43 | 44 | 45 | module.exports.getIsoFiles = -> 46 | isoFiles = fs.readdirSync "#{process.cwd()}/isos/" 47 | isos = [] 48 | for isoName in isoFiles 49 | isos.push isoName if 0 < isoName.search /\.iso$/ 50 | return isos 51 | 52 | 53 | module.exports.getDiskFiles = -> 54 | diskFiles = fs.readdirSync "#{process.cwd()}/disks/" 55 | disks = [] 56 | for diskName in diskFiles 57 | disks.push diskName if 0 < diskName.search /\.img$/ 58 | return disks 59 | 60 | 61 | module.exports.getVmConfigs = -> 62 | guestConfs = fs.readdirSync "#{process.cwd()}/vmConfigs" 63 | guests = [] 64 | for name in guestConfs 65 | guests.push name if 0 < name.search /\.yml$/ 66 | return guests 67 | 68 | 69 | module.exports.getVmHandlerExtensions = -> 70 | filesIn = fs.readdirSync "#{process.cwd()}/lib/src/vmHandlerExtensions" 71 | files = {} 72 | 73 | files[file.split('.')[0]] = true for file in filesIn 74 | 75 | return (i for i of files) 76 | 77 | 78 | savePids = () -> fs.writeFileSync "#{process.cwd()}/pids.json", JSON.stringify pids 79 | 80 | module.exports.setPid = (pid, guestName) -> 81 | console.log "CONFIG: set pid:#{pid} for #{guestName}" 82 | pids[pid] = guestName 83 | 84 | savePids() 85 | 86 | module.exports.removePid = (pid) -> 87 | return if ! pids[pid]? 88 | 89 | delete pids[pid] 90 | console.log "CONFIG: removed pid:#{pid}" 91 | savePids() 92 | 93 | module.exports.getGuestNameByPid = (pid) -> pids[pid] if pids[pid]? 94 | 95 | module.exports.getRunningPids = (cb) -> 96 | if 'darwin' is os.type().toLowerCase() 97 | cmd = 'ps ax -o pid,etime,start,lstart,time,comm|grep qemu-system-x86_64' 98 | else if 'linux' is os.type().toLowerCase() 99 | cmd = 'ps --no-headers -o pid,etime,start,lstart,time,comm -C qemu-system-x86_64' 100 | 101 | exec cmd, (err, stdout, stderr) -> 102 | return cb [] if err 103 | 104 | tmpPids = stdout.trim().split '\n' 105 | tmpPids.pop() if '' is tmpPids[tmpPids.length-1] 106 | 107 | retPids = (Number pid.split(' ')[0] for pid in tmpPids) 108 | cb retPids 109 | 110 | console.log 'running pids found:' 111 | console.dir retPids 112 | 113 | # ls #{process.cwd()}/isos/*.iso|sort -f 114 | -------------------------------------------------------------------------------- /lib/drives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | class Drives { 6 | constructor() {} 7 | 8 | get all() { 9 | return fs.readdirSync(`${__dirname}/../disks/`) 10 | .filter(file => ~file.search(/\.json$/)).map(file => JSON.parse(fs.readFileSync(`${__dirname}/../disks/${file}`))); 11 | } 12 | } 13 | 14 | 15 | const drives = new Drives(); 16 | 17 | module.exports = drives; 18 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const events = require('events'); 4 | 5 | class Events extends events {} 6 | 7 | const qemuEvents = new Events(); 8 | 9 | module.exports = qemuEvents; 10 | -------------------------------------------------------------------------------- /lib/host.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * get next free port 5 | * scan for virtual machines running 6 | * TODO: network interfaces 7 | */ 8 | 9 | const execSync = require('child_process').execSync; 10 | 11 | 12 | class Host { 13 | 14 | constructor() { 15 | 16 | } 17 | 18 | isPortUsed(port) { 19 | if ( isNaN(port) ) throw "Not a number"; 20 | return '0' == execSync(`nc -z 0.0.0.0 ${port} &> /dev/null; echo $?;`).toString('utf8').split('\n')[0]; 21 | } 22 | 23 | nextFreePort(start) { 24 | do { 25 | this.isPortUsed(start); 26 | if (!this.isPortUsed(start)) return start; 27 | start++; 28 | } while(65536 > start); 29 | 30 | throw "No free port"; 31 | } 32 | 33 | isVmProcRunning(uuid, procs) { return this.isVmRunning(uuid, procs); } 34 | 35 | isVmRunning(uuid, procsIn = null) { 36 | const procs = procsIn || this.vmProcesses(); 37 | const reg = new RegExp(`-uuid\\ (${uuid})`.replace('-', '\\-')); 38 | 39 | return 1 === procs.filter( (proc) => { 40 | const match = reg.exec(proc); 41 | return (match && uuid === match[1]); 42 | }).length; 43 | } // isVmRunning() 44 | 45 | 46 | vmProcesses() { 47 | let slice = -1; 48 | return execSync('ps aux').toString('utf8').split('\n').reduce( (prev, line) => { 49 | if (-1 === slice) { 50 | slice = line.search(/COMMAND$/); 51 | } else { // if 52 | if (~line.indexOf('qemu-system-x86_64')) { prev.push(line.slice(slice)); } 53 | } // else 54 | return prev; 55 | }, []); 56 | } // qemuProcess() 57 | } 58 | 59 | 60 | var host = new Host(); 61 | 62 | module.exports = host; 63 | 64 | 65 | // L I N U X 66 | // ps ax -o pid,etimes,lstart,cputime|less 67 | // cat /proc/{pid}/cmdline 68 | 69 | // O S X 70 | // ps ax -o pid,etime,lstart,cputime,comm 71 | -------------------------------------------------------------------------------- /lib/qemu.coffee: -------------------------------------------------------------------------------- 1 | 2 | Vm = require('./src/vm').Vm 3 | 4 | 5 | exports.Vm = Vm 6 | exports.Disk = require './src/disk' 7 | 8 | 9 | createVm = (cfg) -> 10 | vm = new Vm cfg 11 | return vm 12 | 13 | exports.createVm = createVm 14 | -------------------------------------------------------------------------------- /lib/socketServer.coffee: -------------------------------------------------------------------------------- 1 | 2 | vmHandler = require './vmHandler' 3 | ioServer = undefined 4 | Disk = require './src/disk' 5 | usb = require './src/usb' 6 | 7 | socks = {} 8 | 9 | module.exports.start = (httpServer) -> 10 | ioServer = require('socket.io').listen httpServer 11 | ioServer.set('log level', 1) 12 | 13 | ioServer.sockets.on 'connection', (sock) -> 14 | socks[sock.id] = sock 15 | console.log "SOCK -> CON #{sock.handshake.address.address} #{sock.id}" 16 | console.log "SOCK -> count: #{Object.keys(socks).length}" 17 | 18 | sock.emit('set-iso', iso) for iso in vmHandler.getIsos() # emit iso names, client drops duplicates 19 | 20 | for disk in vmHandler.getDisks() # emit disks, client drops duplicates 21 | Disk.info disk, (ret) -> 22 | sock.emit 'set-disk', ret.data 23 | 24 | sock.emit('set-vm', vm.cfg) for vm in vmHandler.getVms() # emit vms, client drops duplicates 25 | 26 | # # # # # # # # # # # # # # # # # # # # 27 | 28 | sock.on 'disconnect', -> 29 | console.log "SOCK -> DIS #{sock.id} #{sock.handshake.address.address}" 30 | delete socks[sock.id] 31 | 32 | sock.on 'qmp-command', (qmpCmd, vmName) -> 33 | console.log "QMP-Command #{qmpCmd}" 34 | vmHandler.qmpCommand qmpCmd, vmName 35 | 36 | sock.on 'relist-usb', -> 37 | console.log 'socket: relist-usb' 38 | usb.scan (usbs) -> sock.emit 'set-usbs', usbs 39 | 40 | sock.on 'create-disk', (disk) -> 41 | vmHandler.createDisk disk, (ret) -> 42 | sock.emit 'msg', ret 43 | if ret.status is 'success' 44 | sock.emit 'reset-create-disk-form' 45 | ioServer.sockets.emit 'set-disk', ret.data.data 46 | 47 | sock.on 'create-VM', (vmConf) -> 48 | vmHandler.createVm vmConf, (ret) -> 49 | sock.emit 'msg', ret 50 | sock.emit 'reset-create-vm-form' if ret.status is 'success' 51 | 52 | 53 | sock.on 'change-guest-conf-entry', (guestName, conf) -> 54 | vmHandler.changeGuestConfEntry guestName, conf 55 | 56 | 57 | 58 | sock.on 'delete-disk', (diskName) -> 59 | if vmHandler.deleteDisk diskName 60 | sock.emit 'msg', {type:'success', msg:'image deleted'} 61 | ioServer.sockets.emit 'delete-disk', diskName 62 | else 63 | sock.emit 'msg', {type:'error', msg:'cant delete image'} 64 | 65 | sock.on 'delete-iso', (isoName) -> 66 | if vmHandler.deleteIso isoName 67 | sock.emit 'msg', {type:'success', msg:"Deleted iso #{isoName}."} 68 | ioServer.sockets.emit 'delete-iso', isoName 69 | else 70 | sock.emit 'msg', {type:'error', msg:"Can't delete iso #{isoName}."} 71 | 72 | sock.on 'delete-guest', (guestName) -> 73 | if vmHandler.deleteGuest guestName 74 | sock.emit 'msg', {type:'success', msg:"Deleted guest #{guestName}."} 75 | ioServer.sockets.emit 'delete-guest', guestName 76 | else 77 | sock.emit 'msg', {type:'error', msg:"Can't delete iso #{guestName}."} 78 | 79 | 80 | module.exports.toAll = (msg, args...) -> 81 | ioServer.sockets.emit msg, args... if ioServer?.sockets? 82 | -------------------------------------------------------------------------------- /lib/src/disk.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports[mod] = require "./disk/#{mod}" for mod in ['info', 'create', 'delete'] 3 | 4 | 5 | # module.exports.create = require './disk/info' 6 | # module.exports.info = require './disk/create' 7 | # module.exports.delete = require './disk/delete' -------------------------------------------------------------------------------- /lib/src/disk/create.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | exec = require('child_process').exec 3 | 4 | module.exports = (disk, cb) -> 5 | if not fs.existsSync "#{process.cwd()}/disks/#{disk.name}.img" 6 | exec "qemu-img create -f qcow2 disks/#{disk.name}.img #{disk.size}G", (err, stdout, stderr) => 7 | if err? or stderr isnt '' 8 | cb {status:'error', data:[err,stderr]} 9 | else 10 | cb {status:'success', data:disk} 11 | else 12 | cb {status:'error', data:['disk already existing']} 13 | -------------------------------------------------------------------------------- /lib/src/disk/delete.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | 4 | module.exports = (diskName) -> 5 | try 6 | fs.unlinkSync "#{process.cwd()}/disks/#{diskName}.img" 7 | return true 8 | catch e 9 | return false 10 | -------------------------------------------------------------------------------- /lib/src/disk/info.coffee: -------------------------------------------------------------------------------- 1 | exec = require('child_process').exec 2 | version = require '../version' 3 | 4 | module.exports = (name, cb) -> 5 | ver = version.getVersion() 6 | 7 | if (1 is ver[0] and 6 <= ver[1]) or 8 | (1 < ver[0]) 9 | exec "qemu-img info --output=json #{process.cwd()}/disks/#{name}.img", (err, stdout, stderr) => 10 | if err? or stderr isnt '' 11 | cb {status:'error', data:[err,stderr]} 12 | return 13 | 14 | info = JSON.parse stdout.split('\n').join '' 15 | 16 | for i,n of info 17 | if 0 < i.search /\-/ 18 | info[i.replace '-', '_'] = n 19 | delete info[i] 20 | 21 | info['disk_size'] = info['actual_size'] 22 | info['name'] = name 23 | info['percentUsed'] = 100/info['virtual_size']*info['disk_size'] 24 | 25 | cb status:'success', data:info 26 | else 27 | exec "qemu-img info #{process.cwd()}/disks/#{name}.img", (err, stdout, stderr) => 28 | if err? or stderr isnt '' 29 | cb {status:'error', data:[err,stderr]} 30 | return 31 | 32 | b = {} 33 | for row in stdout.split('\n') 34 | if row is '' 35 | continue 36 | b[row.split(':')[0].replace(' ', '_')] = row.split(':')[1].replace ' ', '' 37 | b['cluster_size'] = Number b['cluster_size'] 38 | b['name'] = name 39 | b['virtual_size'] = Number b['virtual_size'].split('(')[1].split(' ')[0] 40 | 41 | size = Number b['disk_size'].slice 0, -1 42 | letter = b['disk_size'].slice -1 43 | 44 | if letter is 'K' 45 | size = size * 1024 46 | else if letter is 'M' 47 | size = size * 1024 * 1024 48 | else if letter is 'G' 49 | size = size * 1024 * 1024 * 1024 50 | b['disk_size'] = size 51 | b['percentUsed'] = 100/b['virtual_size']*b['disk_size'] 52 | 53 | cb status:'success', data:b 54 | -------------------------------------------------------------------------------- /lib/src/host.coffee: -------------------------------------------------------------------------------- 1 | 2 | os = require 'os' 3 | 4 | host = {hostname:'', cpus:0, ram:'', freeRam:'', load:[]} 5 | 6 | host.hostname = os.hostname() 7 | host.cpus = os.cpus().length 8 | host.ram = os.totalmem() 9 | 10 | 11 | module.exports = -> 12 | host.freeRam = os.freemem() 13 | host.l = os.loadavg() 14 | return host -------------------------------------------------------------------------------- /lib/src/parser.coffee: -------------------------------------------------------------------------------- 1 | os = require 'os' 2 | qemu = require '../qemu' 3 | Args = require('./args').Args 4 | 5 | 6 | # 7 | # @call conf, cb 8 | # 9 | # @return cb ret, new args Obj 10 | # 11 | module.exports.guestConfToArgs = (conf) -> 12 | osType = os.type().toLowerCase() 13 | if typeof conf isnt 'object' 14 | throw 'conf must be an object' 15 | else if typeof conf.name isnt 'string' 16 | throw 'conf.name must be an string' 17 | else if typeof conf.hardware isnt 'object' 18 | throw 'conf.hardware must be an object' 19 | else if typeof conf.settings isnt 'object' 20 | throw 'conf.settings must be an object' 21 | 22 | hw = conf.hardware # h-w 23 | st = conf.settings # s-t 24 | 25 | args = new Args() 26 | 27 | args.nodefconfig() 28 | .nodefaults() 29 | # .noStart() 30 | # .noShutdown() 31 | 32 | args.accel('kvm').kvm() if osType is 'linux' 33 | 34 | args.ram( hw.ram) 35 | .vga( hw.vgaCard) 36 | .qmp( st.qmpPort) 37 | .keyboard(st.keyboard) 38 | 39 | # CPU CONF 40 | # cpu: Object 41 | # cores: n 42 | # model: s 43 | # sockets: n 44 | # threads: n 45 | cpu = hw.cpu 46 | 47 | # MODEL 48 | args.cpuModel cpu.model 49 | 50 | # CPU architecture 51 | args.cpus cpu.cores, cpu.threads, cpu.sockets 52 | 53 | # NET CONF 54 | if hw.net? 55 | net = hw.net 56 | 57 | if net.opts? 58 | args.net net.mac, net.nic, net.mode, net.opts 59 | else 60 | args.net net.mac, net.nic, net.mode 61 | 62 | ipAddr = [] 63 | for interfaceName,intfce of os.networkInterfaces() 64 | for m in intfce 65 | if m.family is 'IPv4' and m.internal is false and m.address isnt '127.0.0.1' 66 | ipAddr.push m.address 67 | console.log ipAddr 68 | 69 | if osType is 'linux' 70 | args.spice st.spice, ipAddr[0] if st.spice # -spice port=5900,addr=192.168.178.63,disable-ticketing 71 | 72 | args.usbOn() 73 | args.usbDevice u.vendorId, u.productId for u in (if hw.usb? then hw.usb else []) 74 | 75 | # NUMA CONF 76 | # numa: Object 77 | # cpuNode: n 78 | # memNode: n 79 | numa = st.numa 80 | args.hostNuma numa.cpuNode, numa.memNode 81 | 82 | 83 | if hw.disk 84 | args.hd hw.disk 85 | else if hw.partition 86 | args.partition hw.partition 87 | 88 | args.cd hw.iso if hw.iso 89 | args.vnc st.vnc if st.vnc 90 | 91 | # SNAPSHOT 92 | args.snapshot() if st.snapshot 93 | 94 | 95 | if st.boot then switch st.bootDevice 96 | when 'disk' then args.boot 'hd', false 97 | when 'iso' then args.boot 'cd', false 98 | 99 | return args 100 | -------------------------------------------------------------------------------- /lib/src/pin.coffee: -------------------------------------------------------------------------------- 1 | 2 | os = require 'os' 3 | exec = require('child_process').exec 4 | 5 | curPin = 0 6 | pinMask = for [1..os.cpus().length] then 0 7 | 8 | module.exports = (pid, cpuCount) -> 9 | if os.platform() isnt 'linux' 10 | console.log "PIN: pinning is only supported with gnu/linux" 11 | return 12 | 13 | cpuList = '' 14 | for [1..cpuCount] 15 | for pin,i in pinMask 16 | if curPin is pin 17 | if cpuList.length 18 | cpuList = "#{cpuList},#{i}" 19 | else cpuList = "#{i}" 20 | pinMask[i]++ 21 | 22 | if i is pinMask.length-1 23 | curPin++ 24 | break 25 | 26 | exec "taskset -c -p #{cpuList} #{pid}", {maxBuffer: 10*1024}, (e, stdout, stderr) -> 27 | if e? then console.dir e 28 | else console.log "taskset for pid #{pid} with cpulist #{cpuList} executed" 29 | -------------------------------------------------------------------------------- /lib/src/process.coffee: -------------------------------------------------------------------------------- 1 | proc = require 'child_process' 2 | parser = require './parser' 3 | config = require '../config' 4 | vmHandler = require '../vmHandler' 5 | 6 | class Process 7 | constructor: () -> 8 | @process = undefined 9 | 10 | getPid: () -> 11 | return @process.pid if @process 12 | 0 13 | 14 | start: (vmConf) -> 15 | try 16 | args = parser.guestConfToArgs vmConf 17 | console.log "QEMU-Process: Start-Parameters: #{args.args.join(' ')}" 18 | 19 | # shift first array element, its qemu-system-x86_64 xor numactl 20 | @process = proc.spawn args.args.shift(), args.args, {stdio: 'inherit', detached: true} 21 | 22 | @process.on 'exit', (code, signal) -> 23 | config.removePid @pid 24 | if code is 0 then console.log 'QEMU-Process: exit clean.' 25 | else console.error "QEMU-Process: exit with error: #{code}, signal: #{signal}" 26 | vmHandler.SHUTDOWN vmConf.name 27 | catch e 28 | console.error 'process:start:e' 29 | console.dir e 30 | vmHandler.SHUTDOWN vmConf.name 31 | vmHandler.stopQmp vmConf.name 32 | 33 | module.exports.Process = Process 34 | -------------------------------------------------------------------------------- /lib/src/qmp.coffee: -------------------------------------------------------------------------------- 1 | 2 | net = require 'net' 3 | vmHandler = require '../vmHandler' 4 | 5 | class Qmp 6 | constructor:(@vmName) -> 7 | @socket = undefined 8 | @port = undefined 9 | @dataCb = undefined 10 | 11 | connect: (port, cb) -> 12 | if typeof port is 'function' 13 | cb = port 14 | else if typeof port is 'number' 15 | @port = port 16 | 17 | console.log "QMP: try to connect to VM #{@vmName} with port #{@port}" 18 | 19 | tryConnect = () => 20 | @socket = net.connect @port 21 | 22 | @handleDataEvent() 23 | 24 | @socket.on 'connect', => 25 | console.log "qmp connected to #{@vmName}" 26 | @socket.write '{"execute":"qmp_capabilities"}' 27 | cb status:'success' 28 | 29 | @socket.on 'error', (e) => 30 | @socket = undefined 31 | if e.message is 'This socket has been ended by the other party' 32 | console.log 'QEMU closed connection' 33 | else 34 | console.error 'QMP: ConnectError try reconnect' 35 | @connect @port, cb 36 | 37 | @socket.on 'close', () => 38 | console.log 'QMP: socket closed' 39 | @socket = undefined 40 | 41 | intervalId = setInterval => 42 | console.log 'interval' 43 | if ! @socket 44 | console.log 'socket is undefined try connect' 45 | return tryConnect() 46 | 47 | console.log 'clear interval' 48 | clearInterval intervalId 49 | , 100 50 | 51 | handleDataEvent: () -> 52 | @socket.on 'data', (data) => 53 | jsons = data.toString().split '\r\n' 54 | jsons.pop() # remove last '' 55 | 56 | for json in jsons 57 | try 58 | parsedData = JSON.parse json.toString() 59 | if parsedData.event? then event = parsedData.event else event = undefined 60 | 61 | console.log " - - - QMP-START-DATA - - -" 62 | console.dir parsedData 63 | console.log " - - - QMP-END-DATA - - -" 64 | 65 | if parsedData.return?.status? and 66 | parsedData.return?.singlestep? and 67 | parsedData.return?.running? 68 | parsedData.timestamp = new Date().getTime() 69 | if parsedData.return.status is 'paused' 70 | event = 'STOP' 71 | else if parsedData.return.status is 'running' and parsedData.return.running is true 72 | parsedData.timestamp = new Date().getTime() 73 | event = 'RESUME' 74 | 75 | # handle events 76 | if parsedData.timestamp? and event? 77 | if vmHandler[event]? 78 | console.log "QMP: call vmHandler[#{event}] for VM #{@vmName}" 79 | vmHandler[event] @vmName 80 | 81 | if @dataCb? 82 | if parsedData.error? 83 | @dataCb 'error':parsedData.error 84 | else if parsedData.timestamp? 85 | continue 86 | else if parsedData.return? 87 | if 0 is Object.keys(parsedData.return).length 88 | @dataCb status:'success' 89 | else 90 | @dataCb 'data':parsedData.return 91 | else 92 | console.error 'Cant process Data:' 93 | console.dir parsedData 94 | @dataCb = undefined 95 | else 96 | # console.log "no callback defined:" 97 | # console.dir parsedData 98 | catch e 99 | console.error "cant parse returned json, Buffer is:" 100 | console.error json.toString() 101 | console.error "error is:" 102 | console.dir e 103 | 104 | sendCmd: (cmd, args, cb) -> 105 | if typeof args is 'function' 106 | @dataCb = args 107 | @socket.write JSON.stringify execute:cmd 108 | else 109 | @dataCb = cb 110 | @socket.write JSON.stringify {execute:cmd, arguments: args } 111 | 112 | reconnect: (port, cb) -> 113 | @connect port, cb 114 | 115 | ### 116 | # QMP commands 117 | ### 118 | pause: (cb) -> 119 | @sendCmd 'stop', cb 120 | 121 | reset: (cb) -> 122 | @sendCmd 'system_reset', cb 123 | 124 | resume: (cb) -> 125 | @sendCmd 'cont', cb 126 | 127 | shutdown: (cb) -> 128 | @sendCmd 'system_powerdown', cb 129 | 130 | stop: (cb) -> 131 | @sendCmd 'quit', cb 132 | 133 | status: -> 134 | @sendCmd 'query-status', -> 135 | 136 | balloon: (mem, cb) -> 137 | @sendCmd 'balloon', {value:mem}, cb 138 | 139 | exports.Qmp = Qmp 140 | -------------------------------------------------------------------------------- /lib/src/usb.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Bus 005 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 3 | # Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 4 | # Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 5 | # Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 6 | # Bus 001 Device 002: ID 05e3:0610 Genesys Logic, Inc. 7 | # Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 8 | # 01234567890123456789012345678901234567890 9 | # 00000000001111111111222222222233333333334 10 | # host:vendor_id:product_id 11 | 12 | # lsusb -v |grep -E '(Bus\ [0-9]{3,3}|iProduct|bInterfaceClass)' 13 | # Bus 004 Device 004: ID 13fd:1240 Initio Corporation 14 | # iProduct 2 External 15 | # bInterfaceClass 8 Mass Storage 16 | # Bus 004 Device 008: ID 0529:0001 Aladdin Knowledge Systems HASP v0.06 17 | # iProduct 2 HASP HL 2.16 18 | # bInterfaceClass 255 Vendor Specific Class 19 | # Bus 004 Device 007: ID 062a:7223 Creative Labs 20 | # iProduct 2 Full-Speed Mouse 21 | # bInterfaceClass 3 Human Interface Device 22 | # bInterfaceClass 3 Human Interface Device 23 | # Bus 004 Device 006: ID 05ac:9223 Apple, Inc. 24 | # iProduct 2 (error) 25 | # bInterfaceClass 3 Human Interface Device 26 | # Bus 004 Device 005: ID 05af:0802 Jing-Mold Enterprise Co., Ltd 27 | # iProduct 2 USB Keyboard 28 | # bInterfaceClass 3 Human Interface Device 29 | # bInterfaceClass 3 Human Interface Device 30 | # Bus 004 Device 003: ID 05ac:9131 Apple, Inc. 31 | # iProduct 0 32 | # bInterfaceClass 9 Hub 33 | # bInterfaceClass 9 Hub 34 | # Bus 004 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching Hub 35 | # iProduct 0 36 | # bInterfaceClass 9 Hub 37 | # Bus 004 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 38 | # iProduct 2 EHCI Host Controller 39 | # bInterfaceClass 9 Hub 40 | # Bus 003 Device 003: ID 0557:2221 ATEN International Co., Ltd Winbond Hermon 41 | # iProduct 2 (error) 42 | # bInterfaceClass 3 Human Interface Device 43 | # bInterfaceClass 3 Human Interface Device 44 | # Bus 003 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching Hub 45 | # iProduct 0 46 | # bInterfaceClass 9 Hub 47 | # Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 48 | # iProduct 2 EHCI Host Controller 49 | # bInterfaceClass 9 Hub 50 | # Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub 51 | # iProduct 2 xHCI Host Controller 52 | # bInterfaceClass 9 Hub 53 | # Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 54 | # iProduct 2 xHCI Host Controller 55 | # bInterfaceClass 9 Hub 56 | 57 | exec = require('child_process').exec 58 | os = require 'os' 59 | 60 | usbs = [{text:'Test Device. Dont use it!', vendorId:'ffff', productId:'ffff'}] 61 | 62 | module.exports.scan = (cb) -> 63 | if os.type().toLowerCase() isnt 'linux' 64 | console.log "USB DEVICES only supported under linux." 65 | cb usbs if cb? 66 | return 67 | 68 | usbs = [] 69 | 70 | exec "lsusb -v |grep -E '(Bus\ [0-9]{3,3}|iProduct|bInterfaceClass)'", (e, stdout, sterr) -> 71 | tmpUsbs = [] 72 | cur = '' 73 | for line in stdout.split('\n')[0...-1] 74 | line = line.trim() 75 | if -1 < line.search(/^Bus\ [0-9]{3}\ Device\ [0-9]{3}\:\ ID\ [a-z0-9]{4}\:[a-z0-9]{4}\ /) 76 | cur = line 77 | tmpUsbs[line] = {} 78 | else 79 | tmpUsbs[cur][line] = true 80 | 81 | for i,n of tmpUsbs 82 | for j of n 83 | if j is 'bInterfaceClass 9 Hub' 84 | delete tmpUsbs[i] 85 | break 86 | 87 | usbs = [] 88 | 89 | for i,n of tmpUsbs 90 | usb = {text: i.slice(33).trim(), vendorId:i.substring(23,32).split(':')[0], productId:i.substring(23,32).split(':')[1]} 91 | for j of n 92 | usb.text = "#{usb.text}, #{j.slice 26}" 93 | usbs.push usb 94 | 95 | console.log "USB DEVICES:" 96 | console.dir usbs 97 | 98 | cb usbs if cb? 99 | 100 | module.exports.getDevices = -> usbs 101 | 102 | @scan() 103 | -------------------------------------------------------------------------------- /lib/src/version.coffee: -------------------------------------------------------------------------------- 1 | exec = require('child_process').exec 2 | 3 | version = [0,0,0] 4 | 5 | exec 'qemu-system-x86_64 --version', (e, stdout, sterr) -> 6 | version = stdout.slice(22,27).split '.' 7 | 8 | module.exports.getVersion = -> version 9 | -------------------------------------------------------------------------------- /lib/src/vm.coffee: -------------------------------------------------------------------------------- 1 | config = require '../config' 2 | proc = require './process' 3 | qmp = require './qmp' 4 | vmConf = require('./vmCfg') 5 | 6 | class Vm 7 | constructor: (@cfg) -> 8 | @name = @cfg.name 9 | @process = new proc.Process() 10 | @qmp = new qmp.Qmp @name 11 | @saveConf() 12 | 13 | saveConf: () -> vmConf.save @cfg 14 | 15 | setStatus: (status) -> 16 | @cfg.status = status 17 | @saveConf() 18 | 19 | start: (cb) -> 20 | @process.start @cfg 21 | config.setPid @process.getPid(), @name 22 | 23 | @qmp.connect @cfg.settings.qmpPort, (ret) => 24 | cb ret 25 | @status() 26 | 27 | connectQmp: (cb) -> 28 | @qmp.connect @cfg.settings.qmpPort, (ret) => 29 | cb ret 30 | @status() 31 | 32 | stopQMP: -> 33 | console.log "VM #{@name}: stopQMP called" 34 | delete @qmp 35 | @qmp = new qmp.Qmp @name 36 | 37 | ### 38 | # QMP commands 39 | ### 40 | pause: (cb) -> 41 | @qmp.pause cb 42 | 43 | reset: (cb) -> 44 | @qmp.reset cb 45 | 46 | resume: (cb) -> 47 | @qmp.resume cb 48 | 49 | shutdown: (cb) -> 50 | @qmp.shutdown cb 51 | 52 | stop: (cb) -> 53 | @qmp.stop cb 54 | 55 | status: -> 56 | @qmp.status() 57 | 58 | exports.Vm = Vm 59 | 60 | 61 | 62 | # qVM.gfx() 63 | # .ram(1024) 64 | # .cpus(2) 65 | # .accel('kvm') 66 | # .vnc(2) 67 | # .mac('52:54:00:12:34:52') 68 | # .net() 69 | # .qmp(4442) 70 | # .hd('ub1210.img') 71 | # .keyboard('de') 72 | 73 | 74 | # console.dir qVM.startArgs 75 | # 76 | # # qVM.start -> 77 | # # console.log "qemu VM startet" 78 | # 79 | # qVM.reconnectQmp 4442, -> 80 | # console.log "reconnected to qmp" 81 | 82 | # qemu -cpu kvm64 83 | 84 | 85 | ### 86 | 87 | # @qmpSocket.write '{"execute":"query-commands"}' 88 | 89 | ### 90 | -------------------------------------------------------------------------------- /lib/src/vmCfg.coffee: -------------------------------------------------------------------------------- 1 | 2 | fs = require 'fs' 3 | yaml = require 'js-yaml' 4 | 5 | save = (vmCfg) -> 6 | try 7 | ymlObj = yaml.safeDump vmCfg 8 | fs.writeFileSync "#{process.cwd()}/vmConfigs/#{vmCfg.name}.yml", ymlObj 9 | catch e 10 | console.error 'save error' 11 | console.dir e 12 | return false 13 | return true 14 | 15 | open = (confName, cb) -> 16 | try 17 | conf = yaml.safeLoad fs.readFileSync "#{process.cwd()}/vmConfigs/#{confName}", 'utf8' 18 | catch e 19 | console.log 'open error' 20 | console.dir e 21 | return false 22 | return conf 23 | 24 | exports.save = save 25 | exports.open = open 26 | -------------------------------------------------------------------------------- /lib/src/vmHandlerExtensions/RESET.coffee: -------------------------------------------------------------------------------- 1 | 2 | socketServer = require '../../socketServer' 3 | 4 | module.exports = (vm) -> 5 | console.log "vmHandler Extension received RESET event" 6 | 7 | vm.setStatus 'running' 8 | socketServer.toAll 'set-vm-status', vm.name, 'running' 9 | socketServer.toAll 'msg', {type:'success', msg:"VM #{vm.name} reset."} 10 | -------------------------------------------------------------------------------- /lib/src/vmHandlerExtensions/RESUME.coffee: -------------------------------------------------------------------------------- 1 | 2 | socketServer = require '../../socketServer' 3 | 4 | module.exports = (vm) -> 5 | console.log "vmHandler Extension received RESUME event" 6 | 7 | vm.setStatus 'running' 8 | socketServer.toAll 'set-vm-status', vm.name, 'running' 9 | socketServer.toAll 'msg', {type:'success', msg:"VM #{vm.name} resume."} 10 | -------------------------------------------------------------------------------- /lib/src/vmHandlerExtensions/SHUTDOWN.coffee: -------------------------------------------------------------------------------- 1 | 2 | socketServer = require '../../socketServer' 3 | 4 | module.exports = (vm) -> 5 | console.log "vmHandler Extension received SHUTDOWN event" 6 | console.log "qemu process exit VM #{vm.name}" 7 | 8 | vm.setStatus 'stopped' 9 | socketServer.toAll 'set-vm-status', vm.name, 'stopped' 10 | socketServer.toAll 'msg', {type:'success', msg:"VM #{vm.name} shutdown."} 11 | -------------------------------------------------------------------------------- /lib/src/vmHandlerExtensions/START.coffee: -------------------------------------------------------------------------------- 1 | 2 | socketServer = require '../../socketServer' 3 | 4 | module.exports = (vm) -> 5 | console.log "vmHandler Extension received START event" 6 | 7 | vm.setStatus 'running' 8 | socketServer.toAll 'set-vm-status', vm.name, 'running' 9 | socketServer.toAll 'msg', {type:'success', msg:"VM #{vm.name} start."} 10 | -------------------------------------------------------------------------------- /lib/src/vmHandlerExtensions/STOP.coffee: -------------------------------------------------------------------------------- 1 | 2 | socketServer = require '../../socketServer' 3 | 4 | module.exports = (vm) -> 5 | console.log "vmHandler Extension received STOP event" 6 | 7 | vm.setStatus 'paused' 8 | socketServer.toAll 'set-vm-status', vm.name, 'paused' 9 | socketServer.toAll 'msg', {type:'success', msg:"VM #{vm.name} paused."} 10 | -------------------------------------------------------------------------------- /lib/uuid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | class Uuid { 6 | new() { 7 | return crypto.randomBytes(32).toString('hex').match(/(.{8})(.{4})(.{4})(.{4})(.{12})/).slice(1).join('-'); 8 | } 9 | } 10 | 11 | const uuid = new Uuid(); 12 | 13 | module.exports = uuid; 14 | -------------------------------------------------------------------------------- /lib/vm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const spawn = require('child_process').spawn; 5 | 6 | const Qmp = require('../qemu/qmp'); 7 | const Img = require('../qemu/img'); 8 | const Parser = require('./parser'); 9 | const uuid = require('./uuid'); 10 | const vms = require('./vms'); 11 | const host = require('./host'); 12 | 13 | 14 | class Vm { 15 | constructor(name) { 16 | this.conf = {name:name}; 17 | this.readConf(); 18 | 19 | if (this.conf.settings.vnc && this.conf.settings.vnc.enabled && !this.conf.settings.vnc.port) { 20 | this.conf.settings.vnc.port = vms.nextFreeVncPort; 21 | this.saveConf(); 22 | } // if 23 | 24 | if (!this.conf.settings.qmp) { 25 | this.conf.settings.qmp = {port:vms.nextFreeQmpPort}; 26 | this.saveConf(); 27 | } // if 28 | 29 | this.qmp = new Qmp(this.uuid); 30 | 31 | if (host.isVmRunning(this.uuid) ) { 32 | console.log(`VM:${this.conf.uuid}:reconnect-qmp`); 33 | this.qmp.attach(this.conf.settings.qmp); 34 | } else if (this.conf.settings.autoBoot) { 35 | this.start(); 36 | } // else if 37 | 38 | vms.on('event', (msg) => { 39 | if (msg.vmUuid == this.uuid && msg.event == 'SHUTDOWN' && this.conf.settings.autoBoot) { 40 | const tryRestart = () => { 41 | if (!this.conf.settings.noShutdown && host.isVmRunning(this.uuid)) { 42 | console.log('settimeout lol'); 43 | setTimeout(tryRestart, 1000); 44 | } // if 45 | else { 46 | this.start(); 47 | } // else 48 | } // tryRestart() 49 | tryRestart(); 50 | } // if 51 | }); 52 | } // constructor() 53 | 54 | 55 | readConf() { 56 | this.conf = JSON.parse( fs.readFileSync(`${__dirname}/../vmConfigs/${this.conf.name}.json`) ); 57 | 58 | if (this.conf.hardware.drives[0].create) { 59 | const img = new Img(this.conf.hardware.drives[0]); 60 | this.conf.hardware.drives[0] = img.config; 61 | } // if 62 | 63 | if (!this.conf.uuid) { 64 | this.conf.uuid = uuid.new(); 65 | this.saveConf(); 66 | } // if 67 | } 68 | 69 | 70 | saveConf() { 71 | fs.writeFileSync(`${__dirname}/../vmConfigs/${this.conf.name}.json`, JSON.stringify(this.conf, false, 2)); 72 | } // saveConf() 73 | 74 | 75 | get uuid() { return this.conf.uuid; } 76 | 77 | 78 | start() { 79 | if (host.isVmRunning(this.uuid)) { 80 | console.log(`VM:${this.conf.name}:start:proc-already-running`); 81 | } else { 82 | console.log(`VM:${this.conf.name}:start:via:process:spawn`); 83 | this.startProcess(); 84 | } // else 85 | } // start() 86 | 87 | 88 | startProcess() { 89 | try { 90 | const args = (new Parser()).vmConfToArgs(this.conf); 91 | console.log(`VM:${this.conf.name}:startProcess:args:${args.join(' ')}`); 92 | const proc = spawn(args.shift(), args, {stdio: 'pipe', detached:true}); 93 | proc.on('error', (e) => { 94 | console.log(`process error ${e}`); 95 | }); 96 | proc.stdout.on('data', (d) => console.log(`Proc.stdout: ${d.toString()}`) ); 97 | proc.stderr.on('data', (d) => console.log(`Proc.stderr: ${d.toString()}`) ); 98 | proc.on('exit', (code, signal) => { 99 | if (0 == signal) { 100 | console.log(`Process exited with Code 0, Signal ${signal}. OK`); 101 | } else { 102 | console.log(`Process exited with Code ${code}, Signal ${signal}`); 103 | } // else 104 | 105 | if (host.isVmRunning(this.conf.uuid)) { 106 | vms.emit('proc-status', {status:'running', vmUuid:this.conf.uuid}); 107 | } else { 108 | vms.emit('proc-status', {status:'terminated', vmUuid:this.conf.uuid}); 109 | } 110 | 111 | if (this.conf.settings.autoBoot) { this.start(); } 112 | }); 113 | vms.emit('proc-status', {status:'running', vmUuid:this.conf.uuid}); 114 | this.qmp.attach(this.conf.settings.qmp); 115 | 116 | console.log(`VM:${this.conf.name}:startProcess:booted`); 117 | } catch(e) { 118 | console.log(e); 119 | console.log(e.stack); 120 | console.error(`Cant start vm ${this.conf.name}`); 121 | } 122 | } // startProcess() 123 | 124 | qmpCmd(cmd, args) { console.log(`VM:${this.conf.name}:qmpCmd:${cmd}:${args}`); if (this.qmp) this.qmp.cmd(cmd, args); } 125 | } 126 | 127 | module.exports = Vm; 128 | -------------------------------------------------------------------------------- /lib/vms.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const events = require('events'); 5 | const host = require('./host'); 6 | 7 | class Vms extends events { 8 | constructor() { 9 | super(); 10 | this.vms = []; 11 | } 12 | 13 | restore() { 14 | this.vms = fs.readdirSync(`${__dirname}/../vmConfigs`).reduce( (prev, file) => { 15 | if (~file.search(/\.json$/) ) { 16 | const vmName = file.slice(0,-5); 17 | console.log(`VMS:restore:vm:${vmName}`); 18 | try { 19 | const vm = new (require('./vm'))(vmName); 20 | prev.push(vm); 21 | console.log(`VMS:restore:loaded:${vmName}`); 22 | } catch(e) { 23 | console.log(e); 24 | console.log(e.stack.split('\n')); 25 | console.log(`VMS:restore:load:e:${vmName}`); 26 | } 27 | } // if 28 | return prev; 29 | }, []); 30 | } // restore() 31 | 32 | add(vmConfig) { 33 | fs.writeFileSync(`${__dirname}/../vmConfigs/${vmConfig.name}.json`, JSON.stringify(vmConfig, false, 2)); 34 | const vm = new (require('./vm'))(vmConfig.name, vmConfig); 35 | this.vms.push(vm); 36 | } 37 | 38 | 39 | start(uuid) { 40 | const vm = this.vm(uuid); 41 | vm && vm.start(); 42 | } 43 | 44 | 45 | procStatusToAll() { 46 | const procsRunning = host.vmProcesses(); 47 | this.vms.forEach( (vm) => { 48 | const msg = {vmUuid:vm.uuid, status:'terminated'}; 49 | if (host.isVmProcRunning(vm.uuid, procsRunning)) { 50 | msg.status = 'running'; 51 | } // if 52 | this.emit('proc-status', msg); 53 | }); 54 | } 55 | 56 | 57 | vm(uuid) { return this.vms.find( (vm) => vm.uuid === uuid); } 58 | 59 | get confs() { return this.vms.reduce( (prev, vm) => {prev.push(vm.conf); return prev;}, []); } 60 | 61 | _nextFreePort(start, fnc) { 62 | let idx = start; 63 | const portMap = {}; 64 | 65 | for(const vm of this.vms) { 66 | try { portMap[fnc(vm.conf)] = true; } catch(e) {} 67 | } 68 | 69 | do { 70 | if (!portMap[idx]) 71 | return idx; 72 | idx++; 73 | } while (idx < 65536) 74 | throw 'no more free ports'; 75 | } 76 | 77 | get nextFreeVncPort() { 78 | return this._nextFreePort(0, (conf) => conf.settings.vnc.port); 79 | } 80 | 81 | get nextFreeQmpPort() { 82 | return this._nextFreePort(15000, (conf) => conf.settings.qmp.port); 83 | } 84 | 85 | 86 | qmpToAll(cmd, args) { this.vms.forEach( (vm) => vm.qmpCmd(cmd, args) ); } 87 | qmpTo(uuid, cmd, args) { 88 | const vm = this.vm(uuid); 89 | vm && vm.qmpCmd(cmd, args); 90 | console.log('qmpTo', uuid, cmd, args); 91 | } // qmpTo() 92 | } 93 | 94 | const vms = new Vms(); 95 | 96 | module.exports = vms; 97 | -------------------------------------------------------------------------------- /lib/webServer.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | http = require 'http' 3 | nodeStatic = require 'node-static' 4 | vmHandler = require './vmHandler' 5 | 6 | 7 | webServer = undefined 8 | 9 | module.exports.start = -> 10 | staticS = new nodeStatic.Server "./public" 11 | 12 | webServer = http.createServer() 13 | webServer.listen 4224, '0.0.0.0' 14 | 15 | webServer.on 'error', (e) -> 16 | console.error "webServer error:" 17 | console.dir e 18 | 19 | webServer.on 'request', (req, res) -> 20 | if 0 is req.url.search '/iso-upload' 21 | isoName = req.url.split('/').pop() 22 | console.log "UPLOAD #{isoName}" 23 | req.pipe fs.createWriteStream "#{process.cwd()}/isos/#{isoName}" 24 | req.on 'end', -> 25 | res.end JSON.stringify {status:'ok'} 26 | vmHandler.newIso isoName 27 | 28 | else if -1 is req.url.search '/socket.io/1' # request to us 29 | staticS.serve req, res # we only serve files, all other stuff via websockets 30 | 31 | module.exports.getHttpServer = -> 32 | return webServer -------------------------------------------------------------------------------- /lib/webService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const vms = require('./vms'); 6 | const qemuConfig = require('../qemu/config'); 7 | const Img = require('../qemu/img'); 8 | const drives = require('./drives'); 9 | 10 | 11 | class WebService { 12 | constructor() { 13 | this.sseClients = []; 14 | } 15 | 16 | start() { 17 | this.setup(); 18 | } // start() 19 | 20 | setup() { 21 | this.app = express(); 22 | this.app.use( bodyParser.json() ); 23 | this.app.use( express.static(`${__dirname}/../public`) ); 24 | 25 | this.app.use( (req, res, next) => { 26 | console.log(`WEB:${req.method}:${req.url}`); 27 | next(); 28 | }); 29 | 30 | // add new vm 31 | this.app.post('/api/vms', (req, res) => { 32 | vms.add(req.body); 33 | res.sendStatus(204); 34 | }); 35 | 36 | this.app.get('/api/vms/confs', (req, res) => { 37 | res.json(vms.confs); 38 | }); 39 | 40 | this.app.get('/api/vms/qmp/:action', (req, res) => { 41 | vms.qmpToAll(req.params.action); 42 | res.sendStatus(204); 43 | }); 44 | 45 | this.app.get('/api/vms/proc/status', (req, res) => { 46 | vms.procStatusToAll(); 47 | res.sendStatus(204); 48 | }); 49 | 50 | this.app.get('/api/vm/:uuid/:action', (req, res) => { 51 | vms.qmpTo(req.params.uuid, req.params.action); 52 | res.sendStatus(204); 53 | }); 54 | 55 | this.app.get('/api/vm/:uuid/proc/start', (req, res) => { 56 | vms.start(req.params.uuid); 57 | res.sendStatus(204); 58 | }); 59 | 60 | 61 | // D R I V E S 62 | this.app.post('/api/drives', (req, res) => { 63 | console.log(req.body); 64 | const img = new Img(); 65 | img.create(req.body); 66 | res.sendStatus(204); 67 | }); 68 | 69 | this.app.get('/api/drives', (req, res) => res.json(drives.all)); 70 | 71 | 72 | this.app.get('/a', (req, res) => { 73 | const removeFromSse = (res) => { 74 | for(var idx in this.sseClients) { 75 | const r = this.sseClients[idx]; 76 | 77 | if (r === res) { 78 | console.log('remove response'); 79 | this.sseClients.splice(idx, 1); 80 | break; 81 | } // if 82 | } // for 83 | } // removeFromSse() 84 | 85 | res.on('error', () => removeFromSse(res) ); 86 | res.on('close', () => removeFromSse(res) ); 87 | res.setTimeout(0, () => console.log('response timeout out') ); 88 | 89 | this.sseClients.push(res); 90 | 91 | res.setHeader('content-type', 'text/event-stream'); 92 | 93 | res.write('data: initial response\n\n'); 94 | 95 | ['cpus', 'machines', 'nics'].forEach( (itm) => this.send('qemu-config', {selection:itm, data:qemuConfig[itm]}) ); 96 | }); 97 | 98 | this.app.use( (req, res) => { 99 | res.sendFile('index.html', {root:`${__dirname}/../public/`}); 100 | }); 101 | 102 | 103 | this.app.listen(4224, '0.0.0.0'); 104 | } // setup() 105 | 106 | send(event, data={}) { 107 | this.sseClients.forEach((res) => { 108 | console.log(`WEB:sse:${event}`); 109 | res.write(`event: ${event}\n`); 110 | res.write(`data: ${JSON.stringify(data)}\n\n`); 111 | }); 112 | } // send() 113 | } 114 | 115 | const webService = new WebService(); 116 | 117 | 118 | module.exports = webService; 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "babel": { 3 | "moduleRoot": "./", 4 | "plugins": [ 5 | "transform-es2015-modules-amd" 6 | ], 7 | "presets": [ 8 | "es2015" 9 | ] 10 | }, 11 | "scripts": { 12 | "build": "babel webSrc -d public --copy-files", 13 | "watch": "babel --watch webSrc -d public --copy-files" 14 | }, 15 | "dependencies": { 16 | "body-parser": "^1.15.2", 17 | "express": "^4.14.0", 18 | "serve-static": "^1.11.1" 19 | }, 20 | "devDependencies": { 21 | "babel-preset-es2015": "6.3.13", 22 | "babel-plugin-transform-es2015-modules-amd": "6.4.3", 23 | "babel-cli": "6.4.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /src/node_modules -------------------------------------------------------------------------------- /public/0.html: -------------------------------------------------------------------------------- 1 | from 0 -------------------------------------------------------------------------------- /public/abc.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 |{{result.resultJson}}10 |
content | 40 |_key | 41 | 42 | 43 |
45 | 46 | |