/
cli.js
executable file
·287 lines (249 loc) · 8.53 KB
/
cli.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#!/usr/bin/env node
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)
})
})
}
}