UNPKG

mecano

Version:

Common functions for system deployment.

864 lines (830 loc) 25.7 kB
// 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; } } };