first
This commit is contained in:
parent
9026010aa1
commit
9ce292e020
60
app.js
Normal file
60
app.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
var express = require('express');
|
||||||
|
var path = require('path');
|
||||||
|
var favicon = require('serve-favicon');
|
||||||
|
var logger = require('morgan');
|
||||||
|
var cookieParser = require('cookie-parser');
|
||||||
|
var bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
var routes = require('./routes/index');
|
||||||
|
var users = require('./routes/users');
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
// view engine setup
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
// uncomment after placing your favicon in /public
|
||||||
|
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
|
||||||
|
app.use(logger('dev'));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: false }));
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
app.use('/', users);
|
||||||
|
app.use('/api', routes);
|
||||||
|
|
||||||
|
// catch 404 and forward to error handler
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
var err = new Error('Not Found');
|
||||||
|
err.status = 404;
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// error handlers
|
||||||
|
|
||||||
|
// development error handler
|
||||||
|
// will print stacktrace
|
||||||
|
if (app.get('env') === 'development') {
|
||||||
|
app.use(function(err, req, res, next) {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
res.render('error', {
|
||||||
|
message: err.message,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// production error handler
|
||||||
|
// no stacktraces leaked to user
|
||||||
|
app.use(function(err, req, res, next) {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
res.render('error', {
|
||||||
|
message: err.message,
|
||||||
|
error: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = app;
|
90
bin/www
Executable file
90
bin/www
Executable file
@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var app = require('../app');
|
||||||
|
var debug = require('debug')('manager:server');
|
||||||
|
var http = require('http');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port from environment and store in Express.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var port = normalizePort(process.env.PORT || '2000');
|
||||||
|
app.set('port', port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HTTP server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var server = http.createServer(app);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen on provided port, on all network interfaces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
server.listen(port);
|
||||||
|
server.on('error', onError);
|
||||||
|
server.on('listening', onListening);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a port into a number, string, or false.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizePort(val) {
|
||||||
|
var port = parseInt(val, 10);
|
||||||
|
|
||||||
|
if (isNaN(port)) {
|
||||||
|
// named pipe
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port >= 0) {
|
||||||
|
// port number
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP server "error" event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
if (error.syscall !== 'listen') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bind = typeof port === 'string'
|
||||||
|
? 'Pipe ' + port
|
||||||
|
: 'Port ' + port;
|
||||||
|
|
||||||
|
// handle specific listen errors with friendly messages
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
console.error(bind + ' requires elevated privileges');
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
console.error(bind + ' is already in use');
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP server "listening" event.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onListening() {
|
||||||
|
var addr = server.address();
|
||||||
|
var bind = typeof addr === 'string'
|
||||||
|
? 'pipe ' + addr
|
||||||
|
: 'port ' + addr.port;
|
||||||
|
debug('Listening on ' + bind);
|
||||||
|
}
|
125
lxc.js
Normal file
125
lxc.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
module.exports = function(config){
|
||||||
|
|
||||||
|
var obj = {};
|
||||||
|
var child = require('child'),
|
||||||
|
sshBind = config.sshBind || false;
|
||||||
|
|
||||||
|
//http://stackoverflow.com/questions/10530532/
|
||||||
|
function textToArgs(s){
|
||||||
|
var words = [];
|
||||||
|
s.replace(/"([^"]*)"|'([^']*)'|(\S+)/g,function(g0,g1,g2,g3){ words.push(g1 || g2 || g3 || '')});
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sysExec = function(command, onData, onClose){
|
||||||
|
|
||||||
|
onData = onData || function(){};
|
||||||
|
onClose = onClose || function(){};
|
||||||
|
|
||||||
|
if (sshBind != false)
|
||||||
|
{
|
||||||
|
var runCommand = sshBind.slice();
|
||||||
|
runCommand.push(command);
|
||||||
|
} else {
|
||||||
|
var runCommand = textToArgs(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors = '';
|
||||||
|
|
||||||
|
child({
|
||||||
|
command: runCommand.slice(0,1)[0],
|
||||||
|
args: runCommand.slice(1),
|
||||||
|
cbStdout: function(data){ onData(''+data) },
|
||||||
|
cbStderr: function(data){ errors+=data; onData(''+data) },
|
||||||
|
cbClose: function(exitCode){ onClose(exitCode == 0 ? null:exitCode, errors) }
|
||||||
|
}).start();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
obj.create = function(name, template, config, cbComplete, cbData){
|
||||||
|
sysExec('lxc-create -n '+name+' -t '+template, cbComplete, cbData);
|
||||||
|
};
|
||||||
|
|
||||||
|
obj.clone = function(name, base_name, cbComplete, cbData){
|
||||||
|
sysExec('lxc-clone -o '+base_name+ ' -n '+name +' -B overlayfs -s', cbComplete, cbData);
|
||||||
|
};
|
||||||
|
|
||||||
|
obj.destroy = function(name, cbComplete, cbData){
|
||||||
|
sysExec('lxc-destroy -n '+ name, cbComplete, cbData);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
obj.start = function(name, cbComplete, cbData){
|
||||||
|
var cmd = 'lxc-start --name '+name+' --daemon';
|
||||||
|
console.log('start cmd\n', cmd, '\n');
|
||||||
|
sysExec(cmd, cbComplete, cbData);
|
||||||
|
};
|
||||||
|
|
||||||
|
obj.startEphemeral = function(name, base_name, cbComplete, cbData){
|
||||||
|
|
||||||
|
var output = '';
|
||||||
|
sysExec('lxc-start-ephemeral -o '+base_name+ ' -n '+name +' --union-type overlayfs -d', function(data){output+=data}, function(error){
|
||||||
|
if(output.match("doesn't exist.")) return cbData({status: 500, error: "doesn't exist."});
|
||||||
|
if(output.match("already exists.")) return cbData({status: 500, error: "already exists"});
|
||||||
|
if(output.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)) return cbData({status: 200, state:'RUNNING', ip: output.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)[0]});
|
||||||
|
cbData({'?': '?', data: output, name: name, base_name: base_name});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
obj.stop = function(name, cbComplete, cbData){
|
||||||
|
sysExec('lxc-stop -n '+ name, cbComplete, cbData);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
obj.freeze = function(name, cbComplete, cbData){
|
||||||
|
sysExec('lxc-freeze -n '+name, cbComplete, cbData);
|
||||||
|
};
|
||||||
|
|
||||||
|
obj.unfreeze = function(name, cbComplete, cbData){
|
||||||
|
sysExec('lxc-unfreeze -n '+name, cbComplete, cbData);
|
||||||
|
};
|
||||||
|
|
||||||
|
obj.info = function(name, cbComplete, cbData){
|
||||||
|
|
||||||
|
var output = '';
|
||||||
|
sysExec('lxc-info -n'+name, function(data){output+=data}, function(error){
|
||||||
|
if(output.match("doesn't exist")) return cbData({state: 'NULL'});
|
||||||
|
var info = {};
|
||||||
|
output = output.replace(/\suse/ig, '').replace(/\sbytes/ig, '').split("\n").slice(0,-1);
|
||||||
|
for(var i in output){
|
||||||
|
var temp = output[i].split(/\:\s+/);
|
||||||
|
info[temp[0].toLowerCase().trim()] = temp[1].trim();
|
||||||
|
}
|
||||||
|
cbData(info);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
obj.list = function(cbData){
|
||||||
|
|
||||||
|
var output = '';
|
||||||
|
sysExec('lxc-ls --fancy', function(data){output+=data}, function(error){
|
||||||
|
|
||||||
|
output = output.split("\n");
|
||||||
|
var keys = output.splice(0,1)[0].split(/\s+/).slice(0,-1);
|
||||||
|
keys = keys.map(function(v){return v.toLowerCase()});
|
||||||
|
output = output.slice(0).splice(1).slice(0,-1);
|
||||||
|
|
||||||
|
var info = [];
|
||||||
|
|
||||||
|
for (var i in output)
|
||||||
|
{
|
||||||
|
|
||||||
|
var aIn = output[i].split(/\s+/).slice(0,-1),
|
||||||
|
mapOut = {};
|
||||||
|
aIn.map( function(v,i){ mapOut[keys[i]] = v; } );
|
||||||
|
info.push(mapOut);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
cbData(info);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "manager",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ./bin/www"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "~1.13.2",
|
||||||
|
"cookie-parser": "~1.3.5",
|
||||||
|
"debug": "~2.2.0",
|
||||||
|
"ejs": "~2.3.3",
|
||||||
|
"express": "~4.13.1",
|
||||||
|
"morgan": "~1.6.1",
|
||||||
|
"serve-favicon": "~2.3.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/gears.gif
Normal file
BIN
public/images/gears.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
24
public/images/load.gif
Normal file
24
public/images/load.gif
Normal file
File diff suppressed because one or more lines are too long
8
public/stylesheets/style.css
Normal file
8
public/stylesheets/style.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
body {
|
||||||
|
padding: 50px;
|
||||||
|
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #00B7FF;
|
||||||
|
}
|
81
routes/index.js
Normal file
81
routes/index.js
Normal file
File diff suppressed because one or more lines are too long
9
routes/users.js
Normal file
9
routes/users.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
var express = require('express');
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
/* GET users listing. */
|
||||||
|
router.get('/', function(req, res, next) {
|
||||||
|
res.render('index')
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
5
stuff.md
Normal file
5
stuff.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
## Auto start
|
||||||
|
`@reboot /usr/local/bin/forever start -o /var/log/forver.out.log -e /var/log/sorver.err.log -c "/usr/local/bin/codebox run -p 5000" /workspace/`
|
||||||
|
http://stackoverflow.com/a/13388741/3140931
|
||||||
|
|
||||||
|
|
3
views/error.ejs
Normal file
3
views/error.ejs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h1><%= message %></h1>
|
||||||
|
<h2><%= error.status %></h2>
|
||||||
|
<pre><%= error.stack %></pre>
|
151
views/index.ejs
Normal file
151
views/index.ejs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-MfvZlkHCEqatNoGiOXveE8FIwMzZg4W85qfrfIFBfYc= sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous">
|
||||||
|
<link rel='stylesheet' href='/stylesheets/style.css' />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.input-xs {
|
||||||
|
height: 22px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5; //If Placeholder of the input is moved up, rem/modify this.
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||||
|
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" integrity="sha256-Sk3nkD6mLTMOF0EOpNtsIry+s1CsaqQC1rVLTAy+0yc= sha512-K1qjQ+NcF2TYO/eI3M6v8EiNYZfA95pQumfvcVrTHtwQVDG+aHRqLi/ETn2uB+1JqwYqVG3LIvdm9lj6imS/pQ==" crossorigin="anonymous"></script>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/mustache.js/0.8.1/mustache.min.js"></script>
|
||||||
|
<script src="//dev.718it.biz/repeat/js/repeat.js"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var vmDataParse = function(data){
|
||||||
|
data.ip = data.ip || (data.ipv4 || '').replace('-', '') || null ;
|
||||||
|
data.url = null;
|
||||||
|
data.clone = null;
|
||||||
|
if(data.state === 'RUNNING'){
|
||||||
|
var url = '//'+data.name+'.vm42.us';
|
||||||
|
data.url = '<a href="'+url+'" target="_blank">'+url+'</a>';
|
||||||
|
}else if( data.state !== "RUNNING" && data.name.match(/_template/) ){
|
||||||
|
data.clone = $('#cloneFormTemplate').html();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
};
|
||||||
|
|
||||||
|
var getVmList = function(){
|
||||||
|
$.scope.vm.splice(0);
|
||||||
|
$.getJSON('api/list',function(data){
|
||||||
|
data.forEach(function(value){
|
||||||
|
value = vmDataParse(value);
|
||||||
|
$.scope.vm.push(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateVmList = function(name){
|
||||||
|
$.getJSON('api/info/'+name, function(data){
|
||||||
|
if(data.state === 'NULL') return $.scope.vm.splice(name,1);
|
||||||
|
data = vmDataParse(data);
|
||||||
|
$.scope.vm.update(name, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var addMessage = function(name, message, time){
|
||||||
|
time = time || 10000;
|
||||||
|
var index = $.scope.vm.indexOf(name);
|
||||||
|
$.extend(true, $.scope.vm[index], {message: message})
|
||||||
|
//$.scope.vm.update(name, {message: message})
|
||||||
|
setTimeout(function() {
|
||||||
|
$.scope.vm.update(name, {message: null})
|
||||||
|
}, time);
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready(function(){
|
||||||
|
// set up animation for adding to taks list
|
||||||
|
$.scope.vm.__put = function(){
|
||||||
|
this.slideDown( 'fast' );
|
||||||
|
};
|
||||||
|
|
||||||
|
// set up animation for removing taks from list
|
||||||
|
$.scope.vm.__take = function(){
|
||||||
|
this.slideUp( 'fast', function(){
|
||||||
|
this.remove();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$.scope.vm.__index = 'name';
|
||||||
|
|
||||||
|
getVmList();
|
||||||
|
|
||||||
|
$('ol').on('click', 'button', function(event){
|
||||||
|
var $closest = $(this).closest('[data-name]');
|
||||||
|
var name = $closest.data('name');
|
||||||
|
var call = $(this).data('call');
|
||||||
|
|
||||||
|
$closest.find('span.options').html('<img src="images/gears.gif"/>');
|
||||||
|
|
||||||
|
$.getJSON('api/'+call+'/'+name, function(data){
|
||||||
|
if(data.status !== 200) addMessage(name, data.message);
|
||||||
|
updateVmList(name);
|
||||||
|
|
||||||
|
});
|
||||||
|
}).on('submit', 'form', function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
var $closest = $(this).closest('[data-name]');
|
||||||
|
var templateName = $closest.data('name');
|
||||||
|
var name = $(this).find('[name="name"]').val();
|
||||||
|
var call = $(this).find('[name="live"]').prop('checked') ? 'live' : 'clone';
|
||||||
|
|
||||||
|
if(!name) return alert('Please add a name!');
|
||||||
|
else if($.scope.vm.indexOf(name) !== -1 ) return alert('Name allready in use!')
|
||||||
|
else $closest.find('span.options').html('<img src="images/gears.gif"/>');
|
||||||
|
|
||||||
|
$.getJSON('api/'+call+'/'+templateName+'/'+name, function(data){
|
||||||
|
console.log(data.status, data);
|
||||||
|
|
||||||
|
if(data.status == 200){
|
||||||
|
getVmList();
|
||||||
|
}else{
|
||||||
|
addMessage(name, data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script id="cloneFormTemplate" type="x-mustache-template">
|
||||||
|
<form class="form-inline" style="display: inline-block; max-height: 2em;">
|
||||||
|
[<input name="live" type="checkbox" /> Start Live]
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="row" style="margin-left:1px;">
|
||||||
|
<input type="text" class="form-control input-xs" name="name" placeholder="Clone name">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<input class="btn btn-default btn-xs" type="submit" value="Clone" data-call="clone" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Start will take atlest 5 seconds!</h2>
|
||||||
|
<!--<input type="text" name="domain" value="vm42.us"/>-->
|
||||||
|
<ol>
|
||||||
|
<li jq-repeat="vm" data-name="{{ name }}" style="height:2em">
|
||||||
|
{{ name }} | {{ state }} | {{ ip }}
|
||||||
|
<span class="options">
|
||||||
|
<div class="btn-group btn-group-xs">
|
||||||
|
<button class="btn btn-warning" data-call="stop">Stop</button>
|
||||||
|
<button class="btn btn-success" data-call="start">Start</button>
|
||||||
|
<button class="btn btn-danger" data-call="destroy">Destroy</button>
|
||||||
|
</div>
|
||||||
|
{{{ url }}}
|
||||||
|
{{{ clone }}}
|
||||||
|
{{{ message }}}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user