├── .gitignore ├── .npmignore ├── .travis.yml ├── cli.js ├── download.js ├── get-ip.sh ├── hyperkit ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist/ 4 | bzImage 5 | initrd.gz 6 | hypercore.rsa 7 | hypercore.rsa.pub 8 | CURRENT_HOSTNAME 9 | linux/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tczs 2 | hypercore.rsa 3 | hypercore.rsa.pub 4 | CURRENT_HOSTNAME 5 | vmlinuz64 6 | initrd.gz 7 | corepure64.gz 8 | bzImage 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var child = require('child_process') 4 | var fs = require('fs') 5 | var os = require('os') 6 | var path = require('path') 7 | var daemon = require('daemonspawn') 8 | var catNames = require('cat-names') 9 | var keypair = require('keypair') 10 | var forge = require('node-forge') 11 | var mkdirp = require('mkdirp') 12 | var psjson = require('psjson') 13 | var minimist = require('minimist') 14 | var argv = minimist(process.argv.slice(2), {boolean: true}) 15 | 16 | handle(argv._, argv) 17 | 18 | function handle (cmds, opts) { 19 | // needs yosemite 10.10.3 or above for hyperkit 20 | if (os.platform() !== 'darwin' || os.release() < '14.3.0') return console.error('Error: Mac OS Yosemite 10.10.3 or above required') 21 | 22 | var dir = opts.path || opts.p || path.join(process.cwd(), 'linux') 23 | if (!opts.stderr) opts.stderr = path.join(dir, 'stderr.log') 24 | if (!opts.stdout) opts.stdout = path.join(dir, 'stdout.log') 25 | var linuxPid = opts.pid || path.join(dir, 'linux.pid') 26 | var linuxHostname = path.join(dir, 'hostname') 27 | var keyPath = path.join(dir, 'id_rsa') 28 | var hyperkit = __dirname + '/hyperkit' 29 | 30 | var cmd = cmds[0] 31 | if (typeof cmd === 'undefined') { 32 | return console.log( 33 | 'Usage: linux [args...]\n' + 34 | '\n' + 35 | 'Commands:\n' + 36 | ' init creates a new ./linux folder in this directory to hold config\n' + 37 | ' boot boots up linux from config in ./linux\n' + 38 | ' status checks if linux is running or not\n' + 39 | ' ssh sshes into linux and attaches the session to your terminal\n' + 40 | ' ip get the ip of the linux vm\n' + 41 | ' run runs a single command over ssh\n' + 42 | ' halt runs halt in linux, initiating a graceful shutdown\n' + 43 | ' kill immediately ungracefully kills the linux process with SIGKILL\n' + 44 | ' pid get the pid of the linux process\n' + 45 | ' ps print all linux processes running on this machine' + 46 | '' 47 | ) 48 | } 49 | 50 | if (cmd === 'init') { 51 | if (fs.existsSync(dir)) return console.log('Error: linux config folder already exists, skipping init') 52 | mkdirp.sync(dir) 53 | if (!fs.existsSync(keyPath)) saveNewKeypairSync() 54 | console.log('Created new config folder at', dir) 55 | return 56 | } 57 | 58 | if (cmd === 'boot') { 59 | // capability checks 60 | if (process.getuid() !== 0) return console.error('Error: must run boot with sudo') 61 | 62 | // ensure linux folder exists 63 | if (!fs.existsSync(dir)) return console.log('Error: no linux config folder found, run linux init first') 64 | 65 | // ensure key permissions are correct 66 | if (fs.accessSync) fs.accessSync(keyPath) 67 | 68 | getPid() 69 | 70 | return 71 | } 72 | 73 | if (cmd === 'pid') { 74 | readPid(function (err, pid) { 75 | if (err) throw err 76 | console.log(pid) 77 | }) 78 | return 79 | } 80 | 81 | if (cmd === 'status') { 82 | linuxStatus(function (err, running, pid) { 83 | if (err) throw err 84 | if (running) console.log('Linux is running', {pid: pid}) 85 | else console.log('Linux is not running') 86 | }) 87 | return 88 | } 89 | 90 | if (cmd === 'kill') { 91 | linuxStatus(function (err, running, pid) { 92 | if (err) throw err 93 | if (!running) return console.log('Linux was not running') 94 | daemon.kill(pid, function (err) { 95 | if (err) throw err 96 | console.log('Linux has been killed') 97 | }) 98 | }) 99 | return 100 | } 101 | 102 | if (cmd === 'ip') { 103 | var hostname = fs.readFileSync(linuxHostname).toString() 104 | parseIp(hostname, function (err, ip) { 105 | if (err) throw err 106 | console.log(ip) 107 | }) 108 | return 109 | } 110 | 111 | if (cmd === 'ssh') { 112 | return ssh() 113 | } 114 | 115 | if (cmd === 'run') { 116 | // run is special, we want to forward raw args to ssh 117 | var runIdx 118 | for (var i = 0; i < process.argv.length; i++) { 119 | if (process.argv[i] === 'run') { 120 | runIdx = i 121 | break 122 | } 123 | } 124 | // reparse argv so we don't include any run args 125 | argv = minimist(process.argv.slice(0, runIdx + 1), {boolean: true}) 126 | return ssh(process.argv.slice(runIdx + 1)) 127 | } 128 | 129 | if (cmd === 'halt') { 130 | return ssh(['halt']) 131 | // todo wait till hyperkit actually exits 132 | } 133 | 134 | if (cmd === 'ps') { 135 | return ps() 136 | } 137 | 138 | console.log(cmd, 'is not a valid command') 139 | 140 | function getPid () { 141 | fs.exists(linuxPid, function (exists) { 142 | if (!exists) return boot() 143 | readPid(function (err, pid) { 144 | if (err) throw err 145 | if (!pid) return boot() 146 | getStatus(pid) 147 | }) 148 | }) 149 | } 150 | 151 | function getStatus (pid) { 152 | daemon.status(pid, function (err, running) { 153 | if (err) throw err 154 | if (running) return console.error('Linux is already running') 155 | boot() 156 | }) 157 | } 158 | 159 | function boot () { 160 | var hostname = opts.hostname || [catNames.random(), catNames.random(), catNames.random(), catNames.random()].join('-').toLowerCase().replace(/\s/g, '-') 161 | var bootArgs = createBootArgs(hostname, keyPath) 162 | var launchPath = 'LAUNCHPATH=' + process.cwd() 163 | var cmd = hyperkit + ' ' + bootArgs.join(' ') + ' ' + launchPath 164 | 165 | if (opts.debug) return console.log(cmd) 166 | 167 | // convert filenames to file descriptors 168 | opts.stdio = ['ignore', fs.openSync(opts.stdout, 'a'), fs.openSync(opts.stderr, 'a')] 169 | opts.detached = true 170 | var linux = daemon.spawn(cmd, opts) 171 | var pid = linux.pid 172 | fs.writeFileSync(linuxPid, pid.toString()) 173 | fs.writeFileSync(linuxHostname, hostname) 174 | pollIp(hostname, pid) 175 | } 176 | 177 | function pollIp (hostname, pid) { 178 | var timeout = Date.now() + (opts.timeout || 1000 * 15) 179 | 180 | check() 181 | 182 | function check () { 183 | if (Date.now() > timeout) { 184 | console.error('Error: Timed out waiting for linux to boot') 185 | kill() 186 | return 187 | } 188 | 189 | parseIp(hostname, function (err, ip) { 190 | if (err) { 191 | console.error(err) 192 | kill() 193 | return 194 | } 195 | if (!ip) return setTimeout(check, 1000) 196 | console.log('Linux has booted', {ip: ip, hostname: hostname, pid: pid}) 197 | }) 198 | } 199 | 200 | function kill () { 201 | daemon.kill(pid, function (err) { 202 | if (err) throw err 203 | process.exit(1) 204 | }) 205 | } 206 | } 207 | 208 | function saveNewKeypairSync () { 209 | var pair = keypair() 210 | var publicKey = forge.pki.publicKeyFromPem(pair.public) 211 | var ssh = forge.ssh.publicKeyToOpenSSH(publicKey, 'root@localhost') // todo would whoami + hostname be better? 212 | 213 | fs.writeFileSync(keyPath, pair.private, {mode: 384}) // 0600 214 | fs.writeFileSync(keyPath + '.pub', ssh) 215 | } 216 | 217 | function ssh (commands) { 218 | var hostname = fs.readFileSync(linuxHostname).toString() 219 | parseIp(hostname, function (err, ip) { 220 | if (err) throw err 221 | if (!ip) return console.error('Error: Could not find ip for linux hostname', hostname) 222 | var args = ['-i', keyPath, '-o', 'StrictHostKeyChecking=no', '-o', 'LogLevel=ERROR', 'root@' + ip] 223 | if (argv.tty || argv.t) args.unshift('-t') 224 | if (commands) args = args.concat(commands) 225 | if (opts.debug) console.error('spawning', 'ssh', args) 226 | child.spawn('ssh', args, {stdio: 'inherit'}) 227 | }) 228 | } 229 | 230 | function linuxStatus (cb) { 231 | readPid(function (err, pid) { 232 | if (err) throw err 233 | if (!pid) return cb() 234 | daemon.status(pid, function (err, running) { 235 | cb(err, running, pid) 236 | }) 237 | }) 238 | } 239 | 240 | function parseIp (hostname, cb) { 241 | child.exec(__dirname + '/get-ip.sh ' + hostname, function (err, stdout, stderr) { 242 | if (err) return cb(err) 243 | var ip = stdout.toString().trim() 244 | cb(null, ip) 245 | }) 246 | } 247 | 248 | function createBootArgs (host, key) { 249 | var kernel = opts.kernel || (__dirname + '/bzImage') 250 | var initrd = opts.initrd || (__dirname + '/initrd.gz') 251 | var keyString = '\\"' + fs.readFileSync(key + '.pub').toString().trim() + '\\"' 252 | var cmdline = 'earlyprintk=serial console=ttyS0 host=' + host + ' sshkey=' + keyString 253 | var args = [ 254 | '-A', 255 | '-m', opts.m || '1G', 256 | '-s', '0:0,hostbridge', 257 | '-s', '31,lpc', 258 | '-l', 'com1,stdio', 259 | '-s', '3:0,virtio-net', 260 | '-s', '8,virtio-rnd', 261 | '-f', '"' + ['kexec', kernel, initrd, cmdline].join(',') + '"' 262 | ] 263 | return args 264 | } 265 | 266 | function readPid (cb) { 267 | fs.readFile(linuxPid, function (err, buf) { 268 | if (err) return cb(err) 269 | var pid = +buf.toString() 270 | if (isNaN(pid)) return cb() 271 | cb(null, pid) 272 | }) 273 | } 274 | 275 | function ps () { 276 | psjson.ps('ps -eaf', function (err, procs) { 277 | if (err) return console.error(err) 278 | procs.rows.forEach(function (proc) { 279 | if (proc.pid === process.pid) return // its the ps process 280 | if (proc.CMD.indexOf(hyperkit) === -1) return // was not spawned by us 281 | var procDir = proc.CMD.split('LAUNCHPATH=')[1] 282 | if (opts.json) return console.log(JSON.stringify({pid: proc.PID, dir: procDir, uptime: proc.TIME})) 283 | else console.log('PID: ' + proc.PID + ', ' + 'DIR: ' + procDir + ', ' + 'UPTIME: ' + proc.TIME) 284 | }) 285 | }) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /download.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var initrd = 'https://github.com/maxogden/HyperOS/releases/download/4.2.0/initrd.gz' 3 | var kernel = 'https://github.com/maxogden/HyperOS/releases/download/4.2.0/bzImage' 4 | 5 | var nugget = require('nugget') 6 | 7 | console.log('Downloading HyperOS kernel + fs from https://github.com/maxogden/hyperos/releases\n') 8 | 9 | nugget([kernel, initrd], {resume: true, verbose: true}, function (err) { 10 | if (err) throw err 11 | process.exit(0) 12 | }) 13 | -------------------------------------------------------------------------------- /get-ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # grep the mac os dhcp lease log for the ip leased to a specific hostname 4 | # assumes hostname is unique (e.g. no one host would ever lease twice) 5 | get_ip() { 6 | local uuid=$1 7 | awk ' 8 | { 9 | if ($1 ~ /^name/) { 10 | name=substr($1, 6) 11 | } 12 | if ($1 ~ /^ip_address/) { 13 | ip_address=substr($1, 12) 14 | } 15 | if (name != "" && ip_address != "") { 16 | ip_addresses[name]=ip_address 17 | name=ip_address="" 18 | } 19 | } 20 | END { 21 | print ip_addresses["'$uuid'"] 22 | } 23 | ' /var/db/dhcpd_leases 24 | } 25 | 26 | echo $(get_ip $1) 27 | -------------------------------------------------------------------------------- /hyperkit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/linux/89654231708faa3c01e4e8e1c9ebf3a0e208203d/hyperkit -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linux", 3 | "version": "4.2.1", 4 | "description": "run linux on mac os", 5 | "main": "index.js", 6 | "bin": { 7 | "linux": "cli.js" 8 | }, 9 | "scripts": { 10 | "postinstall": "node download.js", 11 | "test": "standard" 12 | }, 13 | "author": "max ogden", 14 | "license": "ISC", 15 | "dependencies": { 16 | "cat-names": "^1.0.2", 17 | "daemonspawn": "^1.0.1", 18 | "keypair": "^1.0.0", 19 | "minimist": "^1.2.0", 20 | "mkdirp": "^0.5.1", 21 | "node-forge": "^0.6.34", 22 | "nugget": "^1.5.4", 23 | "psjson": "^0.1.0" 24 | }, 25 | "devDependencies": { 26 | "standard": "^5.2.2" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/maxogden/linux.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/maxogden/linux/issues" 34 | }, 35 | "homepage": "https://github.com/maxogden/linux#readme" 36 | } 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # linux 2 | 3 | **beta software! proceed with caution** 4 | 5 | Download, install and run Linux on OS X in less than 60 seconds! 6 | 7 | npm installs [hypercore linux](https://github.com/maxogden/hypercore-linux) and runs it as a daemon using the new Mac OS Yosemite hypervisor (via [hyperkit](https://github.com/moby/hyperkit)). 8 | 9 | See [this youtube video](https://www.youtube.com/watch?v=esNlno79dBw) for a demonstration with a cool soundtrack. 10 | 11 | This module is a low level component that is part of HyperOS, made by the team working on the [Dat](http://dat-data.com/) data version control tool. We are working on integrating the other HyperOS components to support advanced functionality like running containers, sharing filesystems etc. 12 | 13 | Mac OS Yosemite only for now, Windows support coming later through Hyper-V integration (see [this issue](https://github.com/maxogden/linux/issues/4) if you wanna help) 14 | 15 | **WARNING** 16 | ----------- 17 | - hyperkit is a very new project, expect bugs! You must be running OS X 10.10.3 Yosemite or later and 2010 or later Mac for this to work. 18 | - if you happen to be running any version of VirtualBox prior to 4.3.30 or 5.0 then hyperkit will crash your system either if VirtualBox is running or had been run previously after the last reboot (see xhyve's issues [#5](mist64/xhyve#5) and [#9](mist64/xhyve#9) for the full context). So, if you are unable to update VirtualBox to version 4.3.30 or 5, or later, and were using it in your current session please do restart your Mac before attempting to run xhyve. 19 | - (these warnings were borrowed from [coreos-xhyve](https://github.com/coreos/coreos-xhyve)) 20 | 21 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 22 | [![Build Status](https://travis-ci.org/maxogden/linux.svg?branch=master)](https://travis-ci.org/maxogden/linux) 23 | [![dat](http://img.shields.io/badge/Development%20sponsored%20by-dat-green.svg?style=flat)](http://dat-data.com/) 24 | 25 | ### installation 26 | 27 | ``` 28 | npm install linux -g 29 | ``` 30 | 31 | ### usage 32 | 33 | Quickstart: 34 | 35 | 1. Run `linux init` in a folder where you want to store your linux runtime config 36 | 2. Run `sudo linux boot` to start the local linux server daemon 37 | 3. Run `linux ssh` to log in to the server daemon over ssh 38 | 4. Run `linux halt` to stop the server daemon when you're done 39 | 40 | ``` 41 | $ linux 42 | Usage: linux [args...] 43 | 44 | Commands: 45 | init creates a new ./linux folder in this directory to hold config 46 | boot boots up linux from config in ./linux 47 | status checks if linux is running or not 48 | ssh sshes into linux and attaches the session to your terminal 49 | run runs a single command over ssh 50 | halt runs halt in linux, initiating a graceful shutdown 51 | kill immediately ungracefully kills the linux process with SIGKILL 52 | pid get the pid of the linux process 53 | ps print all linux processes running on this machine 54 | ``` 55 | 56 | ### example 57 | 58 | ``` 59 | # initialize a linux folder to hold state 60 | $ linux init 61 | Created new config folder at /Users/max/test/linux 62 | 63 | # starts a linux daemon 64 | $ sudo linux boot 65 | Linux has booted { ip: '192.168.64.127', 66 | hostname: 'simon-mittens-snuggles-toby', 67 | pid: 20665 } 68 | 69 | # ssh login 70 | $ linux ssh 71 | Warning: Permanently added '192.168.64.127' (ECDSA) to the list of known hosts. 72 | __ __ __ 73 | / \__/ \__/ \__ Welcome to HyperOS Linux! 74 | \__/ \__/ \__/ \ 75 | \__/ \__/ \__/ 76 | tc@simon-mittens-snuggles-toby:~$ pwd 77 | /root 78 | tc@simon-mittens-snuggles-toby:~$ exit 79 | Connection to 192.168.64.127 closed. 80 | 81 | # run a single command over ssh 82 | $ linux run hostname 83 | simon-mittens-snuggles-toby 84 | 85 | $ linux status 86 | Linux is running { pid: 20665 } 87 | 88 | # gracefully shutdown 89 | $ linux halt 90 | 91 | $ linux status 92 | Linux is not running 93 | ``` 94 | 95 | # special thanks 96 | 97 | - thanks to [nlf](https://github.com/nlf) (Nathan LaFreniere) for help, if you like docker you should definitely check out his projects [dhyve](https://github.com/nlf/dhyve) and [dhyve-os](https://github.com/nlf/dhyve-os/) 98 | - thanks to [boot2docker](https://github.com/boot2docker/boot2docker) for some stuff I borrowed from their [rootfs folder](https://github.com/boot2docker/boot2docker/tree/master/rootfs/rootfs) 99 | --------------------------------------------------------------------------------