mecano
Version:
Common functions for system deployment.
864 lines (830 loc) • 25.7 kB
JavaScript
// Generated by CoffeeScript 1.9.1
var Stream, crypto, each, exec, fs, glob, ini, misc, path, rimraf, ssh2fs, string, tilde, util,
slice = [].slice;
crypto = require('crypto');
fs = require('fs');
path = require('path');
each = require('each');
util = require('util');
Stream = require('stream');
exec = require('ssh2-exec');
rimraf = require('rimraf');
ini = require('ini');
tilde = require('tilde-expansion');
ssh2fs = require('ssh2-fs');
glob = require('./glob');
string = require('./string');
misc = module.exports = {
array: {
intersect: function(array) {
var argument, i, item, j, len, len1, m, n, result;
if (array === null) {
return [];
}
result = [];
for (i = m = 0, len = array.length; m < len; i = ++m) {
item = array[i];
if (result.indexOf(item) !== -1) {
continue;
}
for (j = n = 0, len1 = arguments.length; n < len1; j = ++n) {
argument = arguments[j];
if (argument.indexOf(item) === -1) {
break;
}
}
if (j === arguments.length) {
result.push(item);
}
}
return result;
},
unique: function(array) {
var el, len, m, o;
o = {};
for (m = 0, len = array.length; m < len; m++) {
el = array[m];
o[el] = true;
}
return Object.keys(o);
},
merge: function() {
var array, arrays, el, len, len1, m, n, r;
arrays = 1 <= arguments.length ? slice.call(arguments, 0) : [];
r = [];
for (m = 0, len = arrays.length; m < len; m++) {
array = arrays[m];
for (n = 0, len1 = array.length; n < len1; n++) {
el = array[n];
r.push(el);
}
}
return r;
}
},
regexp: {
escape: function(str) {
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
},
object: {
equals: function(obj1, obj2, keys) {
var k, keys1, keys2, len, m;
keys1 = Object.keys(obj1);
keys2 = Object.keys(obj2);
if (keys) {
keys1 = keys1.filter(function(k) {
return keys.indexOf(k) !== -1;
});
keys2 = keys2.filter(function(k) {
return keys.indexOf(k) !== -1;
});
} else {
keys = keys1;
}
if (keys1.length !== keys2.length) {
return false;
}
for (m = 0, len = keys.length; m < len; m++) {
k = keys[m];
if (obj1[k] !== obj2[k]) {
return false;
}
}
return true;
},
diff: function(obj1, obj2, keys) {
var diff, k, keys1, keys2, v;
if (!keys) {
keys1 = Object.keys(obj1);
keys2 = Object.keys(obj2);
keys = misc.array.merge(keys1, keys2, misc.array.unique(keys1));
}
diff = {};
for (k in obj1) {
v = obj1[k];
if (!(keys.indexOf(k) >= 0)) {
continue;
}
if (obj2[k] === v) {
continue;
}
diff[k] = [];
diff[k][0] = v;
}
for (k in obj2) {
v = obj2[k];
if (!(keys.indexOf(k) >= 0)) {
continue;
}
if (obj1[k] === v) {
continue;
}
if (diff[k] == null) {
diff[k] = [];
}
diff[k][1] = v;
}
return diff;
},
clone: function(obj) {
return misc.merge({}, obj);
}
},
path: {
normalize: function(location, callback) {
return tilde(location, function(location) {
return callback(path.normalize(location));
});
},
resolve: function() {
var callback, locations, m, normalized;
locations = 2 <= arguments.length ? slice.call(arguments, 0, m = arguments.length - 1) : (m = 0, []), callback = arguments[m++];
normalized = [];
return each(locations).run(function(location, next) {
return misc.path.normalize(location, function(location) {
normalized.push(location);
return next();
});
}).then(function() {
return callback(path.resolve.apply(path, normalized));
});
}
},
mode: {
stringify: function(mode) {
if (typeof mode === 'number') {
return mode.toString(8);
} else {
return mode;
}
},
compare: function() {
var i, l, m, mode, modes, ref, ref1;
modes = 1 <= arguments.length ? slice.call(arguments, 0) : [];
ref = misc.mode.stringify(modes[0]);
for (i = m = 1, ref1 = modes.length; 1 <= ref1 ? m < ref1 : m > ref1; i = 1 <= ref1 ? ++m : --m) {
mode = misc.mode.stringify(modes[i]);
l = Math.min(ref.length, mode.length);
if (mode.substr(-l) !== ref.substr(-l)) {
return false;
}
}
return true;
}
},
file: {
copyFile: function(ssh, source, destination, callback) {
var s;
s = function(ssh, callback) {
if (!ssh) {
return callback(null, fs);
} else {
return ssh.sftp(callback);
}
};
return s(ssh, function(err, fs) {
var rs, ws;
if (err) {
return callback(err);
}
rs = fs.createReadStream(source);
ws = rs.pipe(fs.createWriteStream(destination));
ws.on('close', function() {
var modified;
if (fs.end) {
fs.end();
}
modified = true;
return callback();
});
return ws.on('error', callback);
});
},
/*
Compare modes
-------------
*/
cmpmod: function() {
var modes, ref1;
modes = 1 <= arguments.length ? slice.call(arguments, 0) : [];
console.log('Deprecated, use `misc.mode.compare`');
return (ref1 = misc.mode.compare).call.apply(ref1, [this].concat(slice.call(modes)));
},
copy: function(ssh, source, destination, callback) {
if (!ssh) {
source = fs.createReadStream(u.pathname);
source.pipe(destination);
destination.on('close', callback);
return destination.on('error', callback);
} else {
return callback(new Error('Copy over SSH not yet implemented'));
}
},
/*
`files.hash(file, [algorithm], callback)`
-----------------------------------------
Retrieve the hash of a supplied file in hexadecimal
form. If the provided file is a directory, the returned hash
is the sum of all the hashs of the files it recursively
contains. The default algorithm to compute the hash is md5.
Throw an error if file does not exist unless it is a directory.
misc.file.hash ssh, '/path/to/file', (err, md5) ->
md5.should.eql '287621a8df3c3f6c99c7b7645bd09ffd'
*/
hash: function(ssh, file, algorithm, callback) {
var hasher, hashs;
if (arguments.length === 3) {
callback = algorithm;
algorithm = 'md5';
}
hasher = function(ssh, path, callback) {
var shasum;
shasum = crypto.createHash(algorithm);
if (!ssh) {
return ssh2fs.createReadStream(ssh, path, function(err, stream) {
if (err) {
return callback(err);
}
return stream.on('data', function(data) {
return shasum.update(data);
}).on('error', function(err) {
if (err.code === 'EISDIR') {
return callback();
}
return callback(err);
}).on('end', function() {
return callback(err, shasum.digest('hex'));
});
});
} else {
return ssh2fs.stat(ssh, path, function(err, stat) {
if (err) {
return callback(err);
}
if (stat.isDirectory()) {
return callback();
}
return exec({
cmd: "openssl " + algorithm + " " + path,
ssh: ssh
}, function(err, stdout) {
if (err) {
callback(err);
}
return callback(err, /.*\s([\w\d]+)$/.exec(stdout.trim())[1]);
});
});
}
};
hashs = [];
return ssh2fs.stat(ssh, file, function(err, stat) {
var compute;
if ((err != null ? err.code : void 0) === 'ENOENT') {
return callback(new Error("Does not exist: " + file));
}
if (err) {
return callback(err);
}
if (stat.isFile()) {
return hasher(ssh, file, callback);
} else if (stat.isDirectory()) {
compute = function(files) {
files.sort();
return each(files).run(function(item, next) {
return hasher(ssh, item, function(err, h) {
if (err) {
return next(err);
}
if (h != null) {
hashs.push(h);
}
return next();
});
}).then(function(err) {
if (err) {
return callback(err);
}
switch (hashs.length) {
case 0:
if (stat.isFile()) {
return callback(new Error("Does not exist: " + file));
} else {
return callback(null, crypto.createHash(algorithm).update('').digest('hex'));
}
break;
case 1:
return callback(null, hashs[0]);
default:
hashs = crypto.createHash(algorithm).update(hashs.join('')).digest('hex');
return callback(null, hashs);
}
});
};
return glob(ssh, file + "/**", function(err, files) {
if (err) {
return callback(err);
}
return compute(files);
});
} else {
return callback(Error("File type not supported"));
}
});
},
/*
`files.compare(files, callback)`
--------------------------------
Compare the hash of multiple file. Return the file md5
if the file are the same or false otherwise.
*/
compare: function(ssh, files, callback) {
var result;
if (files.length < 2) {
return callback(new Error('Minimum of 2 files'));
}
result = null;
return each(files).run(function(file, next) {
return misc.file.hash(ssh, file, function(err, md5) {
if (err) {
return next(err);
}
if (result === null) {
result = md5;
} else if (result !== md5) {
result = false;
}
return next();
});
}).then(function(err) {
if (err) {
return callback(err);
}
return callback(null, result);
});
},
/*
remove(ssh, path, callback)
---------------------------
Remove a file or directory
*/
remove: function(ssh, path, callback) {
var child;
if (!ssh) {
return rimraf(path, callback);
} else {
child = exec(ssh, "rm -rf " + path);
return child.on('exit', function(code) {
return callback(null, code);
});
}
}
},
ssh: {
/*
passwd(ssh, [user], callback)
----------------------
Return information present in '/etc/passwd' and cache the
result in the provided ssh instance as "passwd".
Result look like:
{ root: {
uid: '0',
gid: '0',
comment: 'root',
home: '/root',
shell: '/bin/bash' }, ... }
*/
passwd: function(ssh, username, callback) {
if (arguments.length === 3) {
if (!username) {
return callback(null, null);
}
if (ssh.passwd && ssh.passwd[username]) {
return callback(null, ssh.passwd[username]);
}
ssh.passwd = null;
return misc.ssh.passwd(ssh, function(err, users) {
var user;
if (err) {
return callback(err);
}
user = users[username];
return callback(null, user);
});
}
callback = username;
username = null;
if (ssh.passwd) {
return callback(null, ssh.passwd);
}
return ssh2fs.readFile(ssh, '/etc/passwd', 'ascii', function(err, lines) {
var info, len, line, m, passwd, ref1;
if (err) {
return callback(err);
}
passwd = [];
ref1 = string.lines(lines);
for (m = 0, len = ref1.length; m < len; m++) {
line = ref1[m];
info = /(.*)\:\w\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)/.exec(line);
if (!info) {
continue;
}
passwd[info[1]] = {
uid: parseInt(info[2]),
gid: parseInt(info[3]),
comment: info[4],
home: info[5],
shell: info[6]
};
}
ssh.passwd = passwd;
return callback(null, passwd);
});
},
/*
group(ssh, [group], callback)
----------------------
Return information present in '/etc/group' and cache the
result in the provided ssh instance as "group".
Result look like:
{ root: {
password: 'x'
gid: 0,
user_list: [] },
bin: {
password: 'x',
gid: 1,
user_list: ['bin','daemon'] } }
*/
group: function(ssh, group, callback) {
if (arguments.length === 3) {
if (!group) {
return callback(null, null);
}
if (ssh.cache_group && ssh.cache_group[group]) {
return callback(null, ssh.cache_group[group]);
}
ssh.cache_group = null;
return misc.ssh.group(ssh, function(err, groups) {
var gid;
if (err) {
return err;
}
gid = groups[group];
return callback(null, gid);
});
}
callback = group;
group = null;
if (ssh.cache_group) {
return callback(null, ssh.cache_group);
}
return ssh2fs.readFile(ssh, '/etc/group', 'ascii', function(err, lines) {
var info, len, line, m, ref1;
if (err) {
return callback(err);
}
group = [];
ref1 = string.lines(lines);
for (m = 0, len = ref1.length; m < len; m++) {
line = ref1[m];
info = /(.*)\:(.*)\:(.*)\:(.*)/.exec(line);
if (!info) {
continue;
}
group[info[1]] = {
password: info[2],
gid: parseInt(info[3]),
user_list: info[4] ? info[4].split(',') : []
};
}
ssh.cache_group = group;
return callback(null, group);
});
}
},
/*
`isPortOpen(port, host, callback)`: Check if a port is already open
*/
isPortOpen: function(port, host, callback) {
if (arguments.length === 2) {
callback = host;
host = '127.0.0.1';
}
return exec("nc " + host + " " + port + " < /dev/null", function(err, stdout, stderr) {
if (!err) {
return callback(null, true);
}
if (err.code === 1) {
return callback(null, false);
}
return callback(err);
});
},
/*
`merge([inverse], obj1, obj2, ...]`: Recursively merge objects
--------------------------------------------------------------
On matching keys, the last object take precedence over previous ones
unless the inverse arguments is provided as true. Only objects are
merge, arrays are overwritten.
Enrich an existing object with a second one:
obj1 = { a_key: 'a value', b_key: 'b value'}
obj2 = { b_key: 'new b value'}
result = misc.merge obj1, obj2
assert.eql result, obj1
assert.eql obj1.b_key, 'new b value'
Create a new object from two objects:
obj1 = { a_key: 'a value', b_key: 'b value'}
obj2 = { b_key: 'new b value'}
result = misc.merge {}, obj1, obj2
assert.eql result.b_key, 'new b value'
Using inverse:
obj1 = { b_key: 'b value'}
obj2 = { a_key: 'a value', b_key: 'new b value'}
misc.merge true, obj1, obj2
assert.eql obj1.a_key, 'a value'
assert.eql obj1.b_key, 'b value'
*/
merge: function() {
var clone, copy, from, i, inverse, m, name, options, ref1, ref2, src, target, to;
target = arguments[0];
from = 1;
to = arguments.length;
if (typeof target === 'boolean') {
inverse = !!target;
target = arguments[1];
from = 2;
}
if (typeof target !== "object" && typeof target !== 'function') {
target = {};
}
for (i = m = ref1 = from, ref2 = to; ref1 <= ref2 ? m < ref2 : m > ref2; i = ref1 <= ref2 ? ++m : --m) {
if ((options = arguments[i]) !== null) {
for (name in options) {
src = target[name];
copy = options[name];
if (target === copy) {
continue;
}
if ((copy != null) && typeof copy === 'object' && !Array.isArray(copy) && !(copy instanceof RegExp)) {
clone = src && (src && typeof src === 'object' ? src : {});
target[name] = misc.merge(false, clone, copy);
} else if (copy !== void 0) {
if (!(inverse && typeof target[name] !== 'undefined')) {
target[name] = copy;
}
}
}
}
}
return target;
},
kadmin: function(options, cmd) {
var realm;
realm = options.realm ? "-r " + options.realm : '';
if (options.kadmin_principal) {
return "kadmin " + realm + " -p " + options.kadmin_principal + " -s " + options.kadmin_server + " -w " + options.kadmin_password + " -q '" + cmd + "'";
} else {
return "kadmin.local " + realm + " -q '" + cmd + "'";
}
},
ini: {
clean: function(content, undefinedOnly) {
var k, v;
for (k in content) {
v = content[k];
if (v && typeof v === 'object') {
content[k] = misc.ini.clean(v, undefinedOnly);
continue;
}
if (typeof v === 'undefined') {
delete content[k];
}
if (!undefinedOnly && v === null) {
delete content[k];
}
}
return content;
},
safe: function(val) {
if (typeof val !== "string" || val.match(/[\r\n]/) || val.match(/^\[/) || (val.length > 1 && val.charAt(0) === "\"" && val.slice(-1) === "\"") || val !== val.trim()) {
return JSON.stringify(val);
} else {
return val.replace(/;/g, '\\;');
}
},
dotSplit: function (str) {
return str.replace(/\1/g, '\2LITERAL\\1LITERAL\2')
.replace(/\\\./g, '\1')
.split(/\./).map(function (part) {
return part.replace(/\1/g, '\\.')
.replace(/\2LITERAL\\1LITERAL\2/g, '\1')
})
},
parse: function(content, options) {
return ini.parse(content);
},
/*
Each category is surrounded by one or several square brackets. The number of brackets indicates
the depth of the category.
Options are
* `comment` Default to ";"
*/
parse_multi_brackets: function(str, options) {
var comment, current, data, lines, stack;
if (options == null) {
options = {};
}
lines = string.lines(str);
current = data = {};
stack = [current];
comment = options.comment || ';';
lines.forEach(function(line, _, __) {
var depth, match, parent;
if (!line || line.match(/^\s*$/)) {
return;
}
if (match = line.match(/^\s*(\[+)(.+?)(\]+)\s*$/)) {
depth = match[1].length;
if (depth === stack.length) {
parent = stack[depth - 1];
parent[match[2]] = current = {};
stack.push(current);
}
if (depth > stack.length) {
throw new Error("Invalid child " + match[2]);
}
if (depth < stack.length) {
stack.splice(depth, stack.length - depth);
parent = stack[depth - 1];
parent[match[2]] = current = {};
return stack.push(current);
}
} else if (comment && (match = line.match(RegExp("^\\s*(" + comment + ".*)$")))) {
return current[match[1]] = null;
} else if (match = line.match(/^\s*(.+?)\s*=\s*(.+)\s*$/)) {
return current[match[1]] = match[2];
} else if (match = line.match(/^\s*(.+?)\s*$/)) {
return current[match[1]] = null;
}
});
return data;
},
stringify: function(obj, section, options) {
var children, dotSplit, eol, out, safe;
if (options == null) {
options = {};
}
if (arguments.length === 2) {
options = section;
section = void 0;
}
if (options.separator == null) {
options.separator = ' = ';
}
eol = process.platform === "win32" ? "\r\n" : "\n";
safe = misc.ini.safe;
dotSplit = misc.ini.dotSplit;
children = [];
out = "";
Object.keys(obj).forEach(function(k, _, __) {
var val;
val = obj[k];
if (val && Array.isArray(val)) {
return val.forEach(function(item) {
return out += safe(k + "[]") + options.separator + safe(item) + "\n";
});
} else if (val && typeof val === "object") {
return children.push(k);
} else {
return out += safe(k) + options.separator + safe(val) + eol;
}
});
if (section && out.length) {
out = "[" + safe(section) + "]" + eol + out;
}
children.forEach(function(k, _, __) {
var child, nk;
nk = dotSplit(k).join('\\.');
child = misc.ini.stringify(obj[k], (section ? section + "." : "") + nk, options);
if (out.length && child.length) {
out += eol;
}
return out += child;
});
return out;
},
stringify_square_then_curly: function(content, depth, options) {
var element, i, indent, isArray, isBoolean, isNull, isObj, k, len, m, n, out, outa, prefix, ref1, v;
if (depth == null) {
depth = 0;
}
if (options == null) {
options = {};
}
if (arguments.length === 2) {
options = depth;
depth = 0;
}
if (options.separator == null) {
options.separator = ' = ';
}
out = '';
indent = ' ';
prefix = '';
for (i = m = 0, ref1 = depth; 0 <= ref1 ? m < ref1 : m > ref1; i = 0 <= ref1 ? ++m : --m) {
prefix += indent;
}
for (k in content) {
v = content[k];
isBoolean = typeof v === 'boolean';
isNull = v === null;
isArray = Array.isArray(v);
isObj = typeof v === 'object' && !isNull && !isArray;
if (isObj) {
if (depth === 0) {
out += prefix + "[" + k + "]\n";
out += misc.ini.stringify_square_then_curly(v, depth + 1, options);
out += "\n";
} else {
out += "" + prefix + k + options.separator + "{\n";
out += misc.ini.stringify_square_then_curly(v, depth + 1, options);
out += prefix + "}\n";
}
} else {
if (isArray) {
outa = [];
for (n = 0, len = v.length; n < len; n++) {
element = v[n];
outa.push("" + prefix + k + options.separator + element);
}
out += outa.join('\n');
} else if (isNull) {
out += "" + prefix + k + options.separator + "null";
} else if (isBoolean) {
out += "" + prefix + k + options.separator + (v ? 'true' : 'false');
} else {
out += "" + prefix + k + options.separator + v;
}
out += '\n';
}
}
return out;
},
/*
Each category is surrounded by one or several square brackets. The number of brackets indicates
the depth of the category.
*/
stringify_multi_brackets: function(content, depth, options) {
var i, indent, isBoolean, isNull, isObj, k, m, out, prefix, ref1, v;
if (depth == null) {
depth = 0;
}
if (options == null) {
options = {};
}
if (arguments.length === 2) {
options = depth;
depth = 0;
}
if (options.separator == null) {
options.separator = ' = ';
}
out = '';
indent = ' ';
prefix = '';
for (i = m = 0, ref1 = depth; 0 <= ref1 ? m < ref1 : m > ref1; i = 0 <= ref1 ? ++m : --m) {
prefix += indent;
}
for (k in content) {
v = content[k];
isBoolean = typeof v === 'boolean';
isNull = v === null;
isObj = typeof v === 'object' && !isNull;
if (isObj) {
continue;
}
if (isNull) {
out += "" + prefix + k;
} else if (isBoolean) {
out += "" + prefix + k + options.separator + (v ? 'true' : 'false');
} else {
out += "" + prefix + k + options.separator + v;
}
out += '\n';
}
for (k in content) {
v = content[k];
isNull = v === null;
isObj = typeof v === 'object' && !isNull;
if (!isObj) {
continue;
}
out += "" + prefix + (string.repeat('[', depth + 1)) + k + (string.repeat(']', depth + 1)) + "\n";
out += misc.ini.stringify_multi_brackets(v, depth + 1, options);
}
return out;
}
}
};