linux
Version:
run linux on mac os
288 lines (249 loc) • 8.73 kB
JavaScript
var child = require('child_process')
var fs = require('fs')
var os = require('os')
var path = require('path')
var daemon = require('daemonspawn')
var catNames = require('cat-names')
var keypair = require('keypair')
var forge = require('node-forge')
var mkdirp = require('mkdirp')
var psjson = require('psjson')
var minimist = require('minimist')
var argv = minimist(process.argv.slice(2), {boolean: true})
handle(argv._, argv)
function handle (cmds, opts) {
// needs yosemite 10.10.3 or above for hyperkit
if (os.platform() !== 'darwin' || os.release() < '14.3.0') return console.error('Error: Mac OS Yosemite 10.10.3 or above required')
var dir = opts.path || opts.p || path.join(process.cwd(), 'linux')
if (!opts.stderr) opts.stderr = path.join(dir, 'stderr.log')
if (!opts.stdout) opts.stdout = path.join(dir, 'stdout.log')
var linuxPid = opts.pid || path.join(dir, 'linux.pid')
var linuxHostname = path.join(dir, 'hostname')
var keyPath = path.join(dir, 'id_rsa')
var hyperkit = __dirname + '/hyperkit'
var cmd = cmds[0]
if (typeof cmd === 'undefined') {
return console.log(
'Usage: linux <command> [args...]\n' +
'\n' +
'Commands:\n' +
' init creates a new ./linux folder in this directory to hold config\n' +
' boot boots up linux from config in ./linux\n' +
' status checks if linux is running or not\n' +
' ssh sshes into linux and attaches the session to your terminal\n' +
' ip get the ip of the linux vm\n' +
' run runs a single command over ssh\n' +
' halt runs halt in linux, initiating a graceful shutdown\n' +
' kill immediately ungracefully kills the linux process with SIGKILL\n' +
' pid get the pid of the linux process\n' +
' ps print all linux processes running on this machine' +
''
)
}
if (cmd === 'init') {
if (fs.existsSync(dir)) return console.log('Error: linux config folder already exists, skipping init')
mkdirp.sync(dir)
if (!fs.existsSync(keyPath)) saveNewKeypairSync()
console.log('Created new config folder at', dir)
return
}
if (cmd === 'boot') {
// capability checks
if (process.getuid() !== 0) return console.error('Error: must run boot with sudo')
// ensure linux folder exists
if (!fs.existsSync(dir)) return console.log('Error: no linux config folder found, run linux init first')
// ensure key permissions are correct
if (fs.accessSync) fs.accessSync(keyPath)
getPid()
return
}
if (cmd === 'pid') {
readPid(function (err, pid) {
if (err) throw err
console.log(pid)
})
return
}
if (cmd === 'status') {
linuxStatus(function (err, running, pid) {
if (err) throw err
if (running) console.log('Linux is running', {pid: pid})
else console.log('Linux is not running')
})
return
}
if (cmd === 'kill') {
linuxStatus(function (err, running, pid) {
if (err) throw err
if (!running) return console.log('Linux was not running')
daemon.kill(pid, function (err) {
if (err) throw err
console.log('Linux has been killed')
})
})
return
}
if (cmd === 'ip') {
var hostname = fs.readFileSync(linuxHostname).toString()
parseIp(hostname, function (err, ip) {
if (err) throw err
console.log(ip)
})
return
}
if (cmd === 'ssh') {
return ssh()
}
if (cmd === 'run') {
// run is special, we want to forward raw args to ssh
var runIdx
for (var i = 0; i < process.argv.length; i++) {
if (process.argv[i] === 'run') {
runIdx = i
break
}
}
// reparse argv so we don't include any run args
argv = minimist(process.argv.slice(0, runIdx + 1), {boolean: true})
return ssh(process.argv.slice(runIdx + 1))
}
if (cmd === 'halt') {
return ssh(['halt'])
// todo wait till hyperkit actually exits
}
if (cmd === 'ps') {
return ps()
}
console.log(cmd, 'is not a valid command')
function getPid () {
fs.exists(linuxPid, function (exists) {
if (!exists) return boot()
readPid(function (err, pid) {
if (err) throw err
if (!pid) return boot()
getStatus(pid)
})
})
}
function getStatus (pid) {
daemon.status(pid, function (err, running) {
if (err) throw err
if (running) return console.error('Linux is already running')
boot()
})
}
function boot () {
var hostname = opts.hostname || [catNames.random(), catNames.random(), catNames.random(), catNames.random()].join('-').toLowerCase().replace(/\s/g, '-')
var bootArgs = createBootArgs(hostname, keyPath)
var launchPath = 'LAUNCHPATH=' + process.cwd()
var cmd = hyperkit + ' ' + bootArgs.join(' ') + ' ' + launchPath
if (opts.debug) return console.log(cmd)
// convert filenames to file descriptors
opts.stdio = ['ignore', fs.openSync(opts.stdout, 'a'), fs.openSync(opts.stderr, 'a')]
opts.detached = true
var linux = daemon.spawn(cmd, opts)
var pid = linux.pid
fs.writeFileSync(linuxPid, pid.toString())
fs.writeFileSync(linuxHostname, hostname)
pollIp(hostname, pid)
}
function pollIp (hostname, pid) {
var timeout = Date.now() + (opts.timeout || 1000 * 15)
check()
function check () {
if (Date.now() > timeout) {
console.error('Error: Timed out waiting for linux to boot')
kill()
return
}
parseIp(hostname, function (err, ip) {
if (err) {
console.error(err)
kill()
return
}
if (!ip) return setTimeout(check, 1000)
console.log('Linux has booted', {ip: ip, hostname: hostname, pid: pid})
})
}
function kill () {
daemon.kill(pid, function (err) {
if (err) throw err
process.exit(1)
})
}
}
function saveNewKeypairSync () {
var pair = keypair()
var publicKey = forge.pki.publicKeyFromPem(pair.public)
var ssh = forge.ssh.publicKeyToOpenSSH(publicKey, 'root@localhost') // todo would whoami + hostname be better?
fs.writeFileSync(keyPath, pair.private, {mode: 384}) // 0600
fs.writeFileSync(keyPath + '.pub', ssh)
}
function ssh (commands) {
var hostname = fs.readFileSync(linuxHostname).toString()
parseIp(hostname, function (err, ip) {
if (err) throw err
if (!ip) return console.error('Error: Could not find ip for linux hostname', hostname)
var args = ['-i', keyPath, '-o', 'StrictHostKeyChecking=no', '-o', 'LogLevel=ERROR', 'root@' + ip]
if (argv.tty || argv.t) args.unshift('-t')
if (commands) args = args.concat(commands)
if (opts.debug) console.error('spawning', 'ssh', args)
child.spawn('ssh', args, {stdio: 'inherit'})
})
}
function linuxStatus (cb) {
readPid(function (err, pid) {
if (err) throw err
if (!pid) return cb()
daemon.status(pid, function (err, running) {
cb(err, running, pid)
})
})
}
function parseIp (hostname, cb) {
child.exec(__dirname + '/get-ip.sh ' + hostname, function (err, stdout, stderr) {
if (err) return cb(err)
var ip = stdout.toString().trim()
cb(null, ip)
})
}
function createBootArgs (host, key) {
var kernel = opts.kernel || (__dirname + '/bzImage')
var initrd = opts.initrd || (__dirname + '/initrd.gz')
var keyString = '\\"' + fs.readFileSync(key + '.pub').toString().trim() + '\\"'
var cmdline = 'earlyprintk=serial console=ttyS0 host=' + host + ' sshkey=' + keyString
var args = [
'-A',
'-m', opts.m || '1G',
'-s', '0:0,hostbridge',
'-s', '31,lpc',
'-l', 'com1,stdio',
'-s', '3:0,virtio-net',
'-s', '8,virtio-rnd',
'-f', '"' + ['kexec', kernel, initrd, cmdline].join(',') + '"'
]
return args
}
function readPid (cb) {
fs.readFile(linuxPid, function (err, buf) {
if (err) return cb(err)
var pid = +buf.toString()
if (isNaN(pid)) return cb()
cb(null, pid)
})
}
function ps () {
psjson.ps('ps -eaf', function (err, procs) {
if (err) return console.error(err)
procs.rows.forEach(function (proc) {
if (proc.pid === process.pid) return // its the ps process
if (proc.CMD.indexOf(hyperkit) === -1) return // was not spawned by us
var procDir = proc.CMD.split('LAUNCHPATH=')[1]
if (opts.json) return console.log(JSON.stringify({pid: proc.PID, dir: procDir, uptime: proc.TIME}))
else console.log('PID: ' + proc.PID + ', ' + 'DIR: ' + procDir + ', ' + 'UPTIME: ' + proc.TIME)
})
})
}
}