├── README.md ├── ctl.js ├── index.js ├── lib ├── VippyConfig.js ├── VippyController.js ├── VippyManager.js ├── VippyNetwork.js └── console-logger.js ├── package.json ├── smf ├── vippy-svc └── vippy.xml ├── vippy.conf └── vippyd.js /README.md: -------------------------------------------------------------------------------- 1 | ## Vippy ## 2 | 3 | Vippy will manage a set of (M) IP addresses over a set of (N) nodes. It uses IP multicast to chat amongst the nodes to decide who owns which interface. 4 | 5 | ### Basic Config ### 6 | 7 | { 8 | "chat": "224.1.1.1:5007", 9 | "chat_interface": "all", 10 | "secret": "knsdfkjnsdfkjnweroib0u3", 11 | "version": 5, 12 | "vips": [ 13 | [ { "interface": "e1000g0", "ip": "10.8.3.195/24" } ], 14 | [ 'node2', { "interface": "e1000g0", "ip": "10.8.99.147/24" } ], 15 | [ { "interface": "e1000g0", "ip": "10.8.99.149/24" }, 16 | { "interface": "e1000g0", "ip": "10.8.3.196/24" } ] 17 | ], 18 | "nodes": [ 19 | "node1", "node2" 20 | ], 21 | "interval": 0.5, 22 | "mature": 5, 23 | "stale": 5, 24 | "arp-cache": "90", 25 | "notify": [ 26 | "arp-cache", 27 | { "interface": "e1000g0", "ip": "10.8.99.149/24" }, 28 | { "interface": "e1000g0", "ip": "10.8.3.1/29" } 29 | ], 30 | "management": "/var/run/vippy.socket", 31 | "logging": { 32 | "driver": "console", 33 | "facility": "local7", 34 | "mask": { "emerg": true, "alert": true, "crit": true, "err": true, 35 | "warning": false, "notice": true, "info": false, "debug": false } 36 | } 37 | } 38 | 39 | It is critical that different vippy nodes agree on their configuration. The **vips** and **nodes** must all match on all nodes (sans the **interface** attribute). They must contain all the same preferences, ips and nodes all in the same order or "bad things" will happen. The node with the highest "version" will domninate the cluster's configuration (all other nodes will adopt their revised config). 40 | 41 | #### chat #### 42 | 43 | This is the IP multicast address/port on which vippy will communicate. It will broadcast updates to this address and listen for updates on this address. 44 | 45 | #### secret #### 46 | 47 | This must be the same on all nodes. It is used to create a secure hash of the announcement message so that other parties on the network cannot interfere. 48 | 49 | #### version #### 50 | 51 | The version of the config (this applies to the **vips** and **nodes**, specifically. As these are changed, the version needs to be incremented. This allows peers to "graduate" to a new config automatically on change. 52 | 53 | #### vips #### 54 | 55 | This is an array of Virtual Interface Groups that vippy is responsible for managing. Each group is a list of Virtual Interfaces (w/ netmask) on a specific system network interface (*e.g.* **{ "interface": "eth0", "10.1.2.3/24" }**. If multiple Virtual Interfaces are specified, they are still treated as an inseparable group (they move together). 56 | 57 | The first item in a Virtual Interface Group list *may* be a string representing the name of a node (from the *nodes* list) that prefers this group. If a group is preferred, the group will be assigned to to the preferred node if the node is up and active. 58 | 59 | #### nodes #### 60 | 61 | The list of hosts participating in the configuration. 62 | 63 | #### interval #### 64 | 65 | The interval (in seconds) between announcements. 66 | 67 | #### mature #### 68 | 69 | How long (in seconds) a vippy node must be up before it is considered "ready to play." 70 | 71 | #### stale #### 72 | 73 | How old (in seconds) the latest announcement can be before we consider a node unavailable. 74 | 75 | #### arp-cache #### 76 | 77 | How often (in seconds) to rescan the local arp-cache for sharing. 78 | 79 | #### notify #### 80 | 81 | A list of of whom to notify (via gratuitous ARPing) when an interface is added to the local machine. This is specified in the same syntax as Virtual Interfaces, but the **ip** considers the whole network (as determined by the CIDR mask (*e.g. /24*). The special token "arp-cache* tells vippy to notify everyone in the cluster's collective ARP tables. 82 | 83 | #### management #### 84 | 85 | The socket on which to listen for management control (via vippyctl). 86 | 87 | #### logging #### 88 | 89 | Designed around syslog, this tells vippy what and where to log. vippy ships with a driver called "console" that may be used if "posix" is unavailable. 90 | 91 | ### Operation ### 92 | 93 | Vippy must know the node that it is. By default it will use the os.hostname(), but if that does not match a named host in the **nodes** list, a different node name may be specified with the **-n** command line option. 94 | 95 | /path/to/vippyd -c /etc/vippy.conf -n node1 96 | 97 | ### Operation ### 98 | 99 | Vippy must know the node that it is. By default it will use the os.hostname(), but if that does not match a named host in the **nodes** list, a different node name may be specified with the **-n** command line option. 100 | 101 | /path/to/vippyd -c /etc/vippy.conf -n node1 102 | 103 | Sending a 'HUP' signal to vippyd will cause it to reread the configuration file and adapt to the newly specified configuration. You can add or remove nodes and change the vips section; just make sure you bump the version number. 104 | 105 | ### Plugins ### 106 | 107 | Vippy can load plugins, which are just normal node.js programs. These programs have three important global variables in their context: **config**, **manager**, and **network** which represent the VippyConfig, VippyManager and VippyNetwork instances that are driving the system. 108 | 109 | A plugin must listen for the 'stop' message on **config** and shutdown any activity such that Node.js can exit. 110 | 111 | config.on('stop', function() { 112 | // stop listeners 113 | // clearIntervals 114 | // clearTimeouts 115 | // etc. 116 | }); 117 | 118 | Plugins can be used to implement periodic local health-checks: 119 | 120 | var job = setInterval(function() { 121 | var state = doSomething(); // true or false 122 | config.active(state, "doSomething"); 123 | }, 5000); 124 | 125 | Plugins can also respond to the plumbing and unplumbing of Virtual Interaces by listening to the 'up' and'down' events from either **manager** (representing intention) or **network** (representing virtual network changes). 126 | 127 | 128 | ### vippyctl ### 129 | 130 | vippyctl allows you perform basic administrative functions. If you are not using the default management socket: "/var/run/vippy.socket", then you may specific a **-s /path/to/socket** on the command line. 131 | 132 | #### status #### 133 | 134 | /path/to/vippyctl status 135 | 136 | Will display the current status of the cluster from the local node's perspective. 137 | -------------------------------------------------------------------------------- /ctl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | Copyright (c) 2013, OmniTI Computer Consulting, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | * Neither the name OmniTI Computer Consulting, Inc. nor the names 17 | of its contributors may be used to endorse or promote products 18 | derived from this software without specific prior written 19 | permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | 34 | var http = require('http'), 35 | util = require('util'), 36 | fs = require('fs'), 37 | socketPath = '/var/run/vippy.socket', 38 | cmd = process.argv[2]; 39 | 40 | if(cmd == '-s') { 41 | socketPath=process.argv[3]; 42 | cmd = process.argv[4]; 43 | } 44 | 45 | try { 46 | var sb = fs.statSync(socketPath); 47 | if(!sb.isSocket()) throw new Error("is not a socket '" + socketPath +"'"); 48 | } catch(e) { 49 | console.log("control socket: " + e); 50 | process.exit(-1); 51 | } 52 | 53 | function runCmd(cmd, handler) { 54 | var req = http.request({ 55 | socketPath: socketPath, 56 | path: '/' + cmd, 57 | }, function(res) { 58 | var body = ''; 59 | res.on('data', function(data) { body = body + data; }); 60 | res.on('end', function() { 61 | handler(JSON.parse(body)); 62 | }); 63 | }); 64 | req.end(); 65 | } 66 | 67 | function IP2nm(ip) { 68 | var nm = 0; 69 | ip.split(/\./) 70 | .map(function(s) {return parseInt(s); }) 71 | .forEach(function (octet) { while(octet) { nm++; octet = octet >> 1; } }); 72 | return nm; 73 | } 74 | function prettyVIF(vif) { 75 | var out = vif.filter(function(x) { return typeof(x) === "object"; }) 76 | .map(function(v) { return v.hasOwnProperty("netmask") ? v.ip + "/" + IP2nm(v.netmask) : v.ip; }) 77 | .join(" "); 78 | if(typeof(vif[0]) === "string") return out + " P(" + vif[0] + ")"; 79 | return out; 80 | } 81 | function statusOutput(info) { 82 | if(info.state && info.config) { 83 | var s = info.state, c = info.config, vifown = {}; 84 | console.log("State: " + (s.active ? "active" : "inactive")); 85 | if(!s.active) console.log("Reason: " + s.inactive_reason); 86 | for (var owner in s.ownership) { 87 | s.ownership[owner].forEach(function (vif) { 88 | vifown[prettyVIF(vif)] = (owner == "_") ? '(unowned)' : owner; 89 | }); 90 | } 91 | c.vips.forEach(function(vif) { 92 | var pretty = prettyVIF(vif); 93 | var out = util.format("%s%s: %s", 94 | (vifown[pretty] == info.identity) ? "(*) " : " ", 95 | vifown[pretty] ? vifown[pretty] : "???", 96 | pretty); 97 | console.log(out); 98 | }); 99 | } 100 | else console.log(info['config']); 101 | } 102 | 103 | switch(cmd) { 104 | case 'status': 105 | runCmd('status', statusOutput); 106 | break; 107 | default: 108 | runCmd(cmd, console.log); 109 | } 110 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.Config = require('./lib/VippyConfig'); 2 | module.exports.Manager = require('./lib/VippyManager'); 3 | module.exports.Network = require('./lib/VippyNetwork'); 4 | module.exports.Controller = require('./lib/VippyController'); 5 | -------------------------------------------------------------------------------- /lib/VippyConfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, OmniTI Computer Consulting, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are 7 | * met: 8 | * 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * * Neither the name OmniTI Computer Consulting, Inc. nor the names 16 | * of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written 18 | * permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | 33 | var events = require('events'), 34 | util = require('util'), 35 | os = require('os'), 36 | net = require('net'), 37 | fs = require('fs'), 38 | dgram = require('dgram'), 39 | crypto = require('crypto'), 40 | ifeDriver = require('ife'), 41 | ife = new ifeDriver(), 42 | VippyConf; 43 | 44 | VippyConfig = function(file, hostname) { 45 | var self = this; 46 | this._hostname = hostname || os.hostname(); 47 | this._config = { version: 0 }; 48 | this._secret = ''; 49 | if(file) { 50 | this._file = file; 51 | var data = fs.readFileSync(file, 'utf-8'); 52 | this.configure(JSON.parse(data)); 53 | } 54 | }; 55 | util.inherits(VippyConfig, events.EventEmitter); 56 | 57 | VippyConfig.prototype.reconfigure = function(file) { 58 | var self = this; 59 | self.log('notice', 'rereading configuration'); 60 | fs.readFile(file, 'utf-8', function(err, data) { 61 | if(err) return self.log('crit', 'reconfigure error: ' + err); 62 | try { 63 | var jsondata = JSON.parse(data); 64 | self.configure(jsondata); 65 | } catch(e) { 66 | self.log('crit', 'JSON error: ' + e); 67 | } 68 | }); 69 | } 70 | VippyConfig.prototype.configure = function(jsondata) { 71 | if(jsondata == undefined) return; 72 | var self = this; 73 | this._config = jsondata; 74 | if(!this._config.version) this._config.version = 0; 75 | 76 | // Setup logging, if any 77 | if(this._config.logging.driver == "console") { 78 | this._logger = require('./console-logger'); 79 | } 80 | else if(this._config.logging.driver == "syslog") { 81 | this._logger = require('posix'); 82 | } 83 | if(this._logger) { 84 | this._logger.openlog(process.argv[1].split(/\//).pop(), {}, this._config.logging.facility || 'local7') 85 | if(this._config.logging.mask) this._logger.setlogmask(this._config.logging.mask); 86 | } 87 | 88 | if(this._config.nodes.filter(function(x) { 89 | return x == self._hostname; 90 | }).length == 0) { 91 | this.log('crit', this._hostname + " is not in this configuration"); 92 | } 93 | 94 | // Normalize the VIP entries. 95 | for (var i in this._config.vips) { 96 | var vlist = this._config.vips[i]; 97 | for (var j in vlist) { 98 | if(typeof(vlist[j]) === "string") continue; 99 | var replace = ip2iface(vlist[j].ip); 100 | replace.name = vlist[j].interface; 101 | replace.state = vlist[j].state; 102 | vlist[j] = replace; 103 | } 104 | } 105 | var vlist = this._config.notify; 106 | for (var j in vlist) { 107 | if(typeof(vlist[j]) === "string") continue; 108 | var replace = ip2iface(vlist[j].ip); 109 | replace.name = vlist[j].interface; 110 | vlist[j] = replace; 111 | } 112 | 113 | // Stash the secret 114 | this._secret = this._config.secret; 115 | delete this._config.secret; 116 | 117 | this._plugins = this._config.plugins || []; 118 | }; 119 | 120 | var ip2iface = function (ip_w_mask) { 121 | var parts = ip_w_mask.split(/\//); 122 | if(parts == null) return null; 123 | if(net.isIPv4(parts[0])) { 124 | var ip = parts[0], mask = (parts[1] === undefined) ? 32 : parseInt(parts[1]); 125 | if(mask > 32 || mask < 0) return null; 126 | var p = ip.split(/\./).map(function (x) { return parseInt(x); }); 127 | var nm = [ 0, 0, 0, 0 ]; 128 | for (var i=0; i<4; i++) { 129 | var bits = mask - (i * 8); 130 | if(bits > 7) nm[i] = 255; 131 | else if(bits > 0) nm[i] = 255 >> (8-bits) << (8-bits); 132 | } 133 | var bc = p.map(function(x,i) { return x | (~nm[i] & 0xff); }); 134 | var nw = p.map(function(x,i) { return x & nm[i]; }); 135 | 136 | return { 'ip': p.join('.'), 137 | 'netmask': nm.join('.'), 138 | 'broadcast': bc.join('.'), 139 | 'network': nw.join('.') }; 140 | } 141 | else if(net.isIPv6(parts[0])) { 142 | return { 'ip': parts[0], 'prefixlen': parts[1] }; 143 | } 144 | return null; 145 | }; 146 | 147 | VippyConfig.prototype.add_plugin = function(file) { 148 | this._plugins.push(file); 149 | } 150 | 151 | VippyConfig.prototype.plugins = function() { 152 | return this._plugins; 153 | } 154 | 155 | VippyConfig.prototype.management_socket = function() { 156 | return this._config.management || '/var/run/vippy.socket'; 157 | }; 158 | 159 | VippyConfig.prototype.hostname = function() { 160 | return this._hostname; 161 | }; 162 | 163 | VippyConfig.prototype.log = function(pri, mess) { 164 | if(this._logger) this._logger.syslog(pri, mess); 165 | }; 166 | 167 | VippyConfig.prototype.generation = function() { 168 | return this._lastConfigChange; 169 | }; 170 | 171 | VippyConfig.prototype.mature = function() { 172 | var age = new Date() - this._lastConfigChange; 173 | var mature = this._config.mature || 5; 174 | return age > (mature * 1000); 175 | }; 176 | 177 | VippyConfig.prototype.stale = function() { 178 | return this._config.stale || 3; 179 | }; 180 | 181 | VippyConfig.prototype.active = function(v, reason) { 182 | if(v !== undefined) 183 | this._active[reason || 'administrative'] = v; 184 | if(!this.mature()) return false; 185 | for(var m in this._active) { 186 | if(this._active[m] === false) return false; 187 | } 188 | return true; 189 | }; 190 | 191 | VippyConfig.prototype.inactive_reason = function() { 192 | if(!this.mature()) return 'booting'; 193 | for(var m in this._active) { 194 | if(this._active[m] === false) return m; 195 | } 196 | return null; 197 | }; 198 | 199 | VippyConfig.prototype.vips = function() { 200 | return this._config.vips; 201 | }; 202 | 203 | VippyConfig.prototype.nodes = function() { 204 | return this._config.nodes; 205 | }; 206 | 207 | VippyConfig.prototype.notify = function() { 208 | return this._config.notify; 209 | }; 210 | 211 | VippyConfig.prototype.configHash = function(conf, nonce, skipupdate) { 212 | var sorter = function(a,b) { 213 | if(a['ip'] < b['ip']) return -1; 214 | if(a['ip'] > b['ip']) return 1; 215 | return 0; 216 | }; 217 | str = conf.version; 218 | str = str + "\n" + conf.vips.map(function(vif) { 219 | return vif.sort(sorter).map(function(vip) { 220 | if(typeof(vip) === "string") return vip; 221 | return vip['ip'] 222 | }).join(','); 223 | }).join("\n"); 224 | str = str + "\n" + conf.nodes.join('|'); 225 | str = str + "\n" + this._secret + "\n"; 226 | var shasum = crypto.createHash('sha256'); 227 | if(!skipupdate && this._lastConfig != str) { 228 | this._lastConfig = str; 229 | this._lastConfigChange = +(new Date()); 230 | } 231 | str = str + nonce; 232 | shasum.update(str); 233 | return shasum.digest('hex'); 234 | }; 235 | 236 | VippyConfig.prototype.refresh_arpcache = function() { 237 | var t = ife.arpcache(), count = 0; 238 | this._private_arp_cache = {}; 239 | for(var ip in t) { 240 | var octets = ip.split(/\./); 241 | // skip IP multicast 242 | if(octets[0] >= 224 && octets[0] <= 239) continue; 243 | this._private_arp_cache[ip] = t[ip]; 244 | this.log('debug', 'arp-cache ' + ip + ' is ' + t[ip]); 245 | count++; 246 | } 247 | this.log('info', 'arp-cache repopulated with ' + count + ' entries'); 248 | } 249 | 250 | VippyConfig.prototype.announce = function() { 251 | var nonce = crypto.pseudoRandomBytes(32).toString('hex'); 252 | var message = { 253 | me: this._hostname, 254 | active: this.active(), 255 | version: this._config.version, 256 | vips: this._config.vips, 257 | nodes: this._config.nodes, 258 | hash: this.configHash(this._config, nonce), 259 | nonce: nonce, 260 | arpcache: this._private_arp_cache, 261 | }; 262 | var payload = new Buffer(JSON.stringify(message)); 263 | this.emit('send-announce', message); 264 | this.log('debug', 'sending announcement'); 265 | this._client.send(payload, 0, payload.length, this._chatport, this._chataddr); 266 | } 267 | 268 | VippyConfig.prototype.version = function() { 269 | return this._config.version; 270 | }; 271 | 272 | VippyConfig.prototype.process_new_config = function(p) { 273 | var expect_hash = this.configHash(p, p.nonce, true); 274 | if(expect_hash != p.hash) return false; 275 | if(p.version <= this._config.version) { 276 | if(this._laststate[p.me]) 277 | this.log('crit', p.me + ' presenting old config v' +p.version); 278 | return false; 279 | } 280 | this._config.vips = p.vips; 281 | this._config.nodes = p.nodes; 282 | this.log('notice', 'config v' + this._config.version + ' -> v' + p.version); 283 | this._config.version = p.version; 284 | this.emit('config-change'); 285 | return true; 286 | }; 287 | 288 | VippyConfig.prototype.run = function() { 289 | var vc = this; 290 | if(vc._running) return; 291 | process.on('SIGHUP', function () { vc.reconfigure(vc._file); }); 292 | vc.refresh_arpcache(); 293 | vc._running = true; 294 | vc._active = {}; 295 | vc._laststate = {}; 296 | vc._client = dgram.createSocket("udp4"); 297 | var addr = vc._config.chat.split(':'); 298 | vc._chatport = parseInt(addr[1]); 299 | vc._chataddr = addr[0]; 300 | var completeBind = function() { 301 | if(typeof(vc._config.chat_interface) !== 'undefined' && vc._config.chat_interface && vc._config.chat_interface !== 'all') { 302 | vc.log('notice', 'Adding membership to ' + vc._chataddr + ' on '+vc._config.chat_interface); 303 | vc._client.addMembership(vc._chataddr, vc._config.chat_interface); 304 | } 305 | else { 306 | vc.log('notice', 'Adding membership to ' + vc._chataddr + ' on all nics'); 307 | vc._client.addMembership(vc._chataddr); 308 | } 309 | vc._client.setBroadcast(true); 310 | vc._client.setMulticastTTL(128); 311 | vc._client.on('message', function(data, rinfo) { 312 | var inbound; 313 | try { 314 | inbound = JSON.parse(data.toString('utf-8')); 315 | } 316 | catch(err) { 317 | vc.log('info', 'announcement ignored [' + rinfo.address + ']: malformed: ' + err); 318 | return; 319 | } 320 | var expected = vc.configHash(vc._config, inbound.nonce); 321 | if(inbound['hash'] != expected) { 322 | if(!vc.process_new_config(inbound)) { 323 | vc._laststate[inbound.me] = false; 324 | vc.log('info', 'announcement ignored [' + rinfo.address + ']: bad hash'); 325 | vc.emit('ignore', 'hash', inbound, rinfo); 326 | } 327 | } 328 | else { 329 | var good = false; 330 | for(var idx in vc._config.nodes) { 331 | if(vc._config.nodes[idx] == inbound['me']) { 332 | vc.log('debug', 'announcement from ' + rinfo.address); 333 | vc.emit('recv-announce', inbound, rinfo); 334 | vc._laststate[inbound.me] = true; 335 | good = true; 336 | } 337 | } 338 | if(!good) { 339 | vc.log('info', 'announcement ignored [' + rinfo.address + ']: bad node ' + inbound['me']); 340 | vc.emit('ignore', 'node', inbound, rinfo); 341 | } 342 | } 343 | }); 344 | vc._chatJob = setInterval(function() { 345 | vc.announce(); 346 | }, 1000 * (vc._config.interval || 1)); 347 | }; 348 | vc._client.bind(vc._chatport, completeBind); 349 | vc._arpJob = setInterval(function() { 350 | vc.refresh_arpcache(); 351 | }, (vc._config['arp-cache'] || 15) * 1000); 352 | } 353 | 354 | VippyConfig.prototype.stop = function() { 355 | if(this._running) { 356 | this._active = { 'shutdown': false }; 357 | this.announce(); 358 | clearInterval(this._arpJob); 359 | clearInterval(this._chatJob); 360 | this._client.close(); 361 | delete this._client; 362 | this._running = false; 363 | this.emit('stop'); 364 | } 365 | } 366 | 367 | module.exports = VippyConfig; 368 | -------------------------------------------------------------------------------- /lib/VippyController.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, OmniTI Computer Consulting, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are 7 | * met: 8 | * 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * * Neither the name OmniTI Computer Consulting, Inc. nor the names 16 | * of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written 18 | * permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | 33 | var events = require('events'), 34 | util = require('util'), 35 | http = require('http'), 36 | VippyController; 37 | 38 | VippyController = function(config, manager) { 39 | var self = this; 40 | this._config = config; 41 | this._manager = manager 42 | 43 | this._stats = { 44 | 'net': { 'ignore': 0, 45 | 'send-announce': 0, 46 | 'recv-announce': 0, 47 | }, 48 | 'state-changes': 0 49 | }; 50 | ['ignore','send-announce','recv-announce'].forEach(function (x) { 51 | self._config.on(x, function() { self._stats.net[x]++; }); 52 | }); 53 | this._manager.on('state-change', 54 | function() { self._stats['state-changes']++ }); 55 | 56 | var s = this._config.management_socket(); 57 | this._server = http.createServer(); 58 | this._server.on('request', function(req,res) { 59 | self.handle(req,res); 60 | }); 61 | process.on('SIGINT', function() { self._server.close(); }); 62 | process.on('SIGQUIT', function() { self._server.close(); }); 63 | process.on('SIGTERM', function() { self._server.close(); }); 64 | this._server.listen(s); 65 | }; 66 | util.inherits(VippyController, events.EventEmitter); 67 | 68 | VippyController.prototype.handle = function(request, response) { 69 | response.setHeader('Content-Type', 'text/json'); 70 | if(request.url == "/status") return this.status(request, response); 71 | response.statusCode = 404; 72 | response.end(JSON.stringify({error: 'no such command'})); 73 | }; 74 | 75 | VippyController.prototype.status = function (request, response) { 76 | var s = { 77 | identity: this._config.hostname(), 78 | stats: this._stats, 79 | config: { 80 | version: this._config.version(), 81 | vips: this._config.vips(), 82 | nodes: this._config.nodes() 83 | }, 84 | state: { 85 | mature: this._config.mature(), 86 | active: this._config.active(), 87 | inactive_reason: this._config.inactive_reason(), 88 | ownership: this._manager.state() 89 | } 90 | }; 91 | 92 | response.write(JSON.stringify(s)); 93 | response.end(); 94 | }; 95 | module.exports = VippyController; 96 | -------------------------------------------------------------------------------- /lib/VippyManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, OmniTI Computer Consulting, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are 7 | * met: 8 | * 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * * Neither the name OmniTI Computer Consulting, Inc. nor the names 16 | * of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written 18 | * permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | 33 | var events = require('events'), 34 | util = require('util'), 35 | crypto = require('crypto'), 36 | ifeDriver = require('ife'), 37 | ife = new ifeDriver(), 38 | VippyManager; 39 | 40 | VippyManager = function(vc, hostname) { 41 | var self = this; 42 | this.vc = vc; 43 | this.hostname = hostname || vc.hostname() || os.hostname(); 44 | this.secret = ''; 45 | this.age = {}; 46 | this.active = {}; 47 | this.arps = {} 48 | this.ownership = {}; 49 | vc.on('recv-announce', function(p) { self.update(p); }); 50 | }; 51 | util.inherits(VippyManager, events.EventEmitter); 52 | 53 | VippyManager.prototype.update = function(p) { 54 | this.arps[p.me] = p.arpcache; 55 | this.active[p.me] = p.active; 56 | this.age[p.me] = +(new Date()); 57 | this.solve(); 58 | }; 59 | 60 | VippyManager.prototype.drop = function(n) { 61 | delete this.arps[n]; 62 | delete this.age[n]; 63 | }; 64 | 65 | VippyManager.prototype.arpcache = function() { 66 | var agg = {}; 67 | for (var host in this.arps) { 68 | for (var ip in this.arps[host]) { 69 | agg[ip] = this.arps[host][ip]; 70 | } 71 | } 72 | return agg; 73 | }; 74 | 75 | var hasChanged = function(s1, s2) { 76 | if(s1 == null && s2 == null) return false; 77 | if(s1 == null || s2 == null) return true; 78 | if(s1.length != s2.length) return true; 79 | for(var i=0; i stale) { 103 | this.vc.log('notice', node + ' aged out'); 104 | this.active[node] = false; 105 | } 106 | } 107 | 108 | var orig_vips = this.vc.vips(), vips = [], 109 | nodes = []; 110 | this.last_ownership = this.ownership; 111 | this.ownership = { '_': [] }; 112 | for(var node in this.active) { 113 | if(this.active[node]) { 114 | this.ownership[node] = []; 115 | nodes.push(node); 116 | } 117 | } 118 | // First handle explicit preferences 119 | for(var idx in orig_vips) { 120 | var vip = orig_vips[idx]; 121 | if(typeof(vip[0]) === 'string' && this.active[vip[0]]) { 122 | this.ownership[vip[0]].push(vip); 123 | } else { 124 | vips.push(orig_vips[idx]); 125 | } 126 | } 127 | 128 | // Apply a loose variant of consistent hashing to distribute 129 | // remaining interfaces. 130 | for(var idx in vips) { 131 | var vip = vips[idx]; 132 | var ip = (typeof(vip[0]) === "string") ? vip[1].ip : vip[0].ip; 133 | var n = nodes.map(function(x) { 134 | var shasum = crypto.createHash('md5'); 135 | shasum.update(x + "|" + ip); 136 | return shasum.digest('hex') + ':' + x; 137 | }).sort(); 138 | if(n.length == 0) { 139 | this.ownership['_'].push(vip); 140 | } 141 | else { 142 | var node = n[0].substr(n[0].indexOf(':')+1); 143 | this.ownership[node].push(vip); 144 | } 145 | } 146 | 147 | if(hasChanged(this.last_ownership[this.hostname], 148 | this.ownership[this.hostname])) { 149 | this.vc.log('info', 'state changed...'); 150 | console.log(this.ownership); 151 | this.emit('state-change', this.ownership, this.last_ownership); 152 | this.reconcile_interfaces(); 153 | } 154 | }; 155 | 156 | VippyManager.prototype.reconcile_interfaces = function() { 157 | var self = this, current_ips = {}, should_have = {}, removed_ips = {}, 158 | current = ife.list(); 159 | // Make a map of currently plumbed IPs. 160 | for(var i in current) current_ips[current[i].ip] = current[i]; 161 | 162 | for(var owner in this.ownership) { 163 | var up = (owner == this.hostname); 164 | var vips = this.ownership[owner]; 165 | vips.forEach(function (vip) { 166 | vip.forEach(function (iface) { 167 | if(typeof(iface) === "object") { 168 | should_have[iface.ip] = up; 169 | if(up && !current_ips[iface.ip]) { 170 | self.vc.log('notice', 'bringing up ' + iface.name + '/' + iface.ip); 171 | self.emit('up', iface); 172 | } 173 | else if(!up && current_ips[iface.ip]) { 174 | self.vc.log('notice', 'bringing down ' + iface.name + '/' + iface.ip); 175 | self.emit('down', iface); 176 | removed_ips[iface.ip] = true; 177 | } 178 | } 179 | }); 180 | }); 181 | } 182 | 183 | // If our config changed, we might have old VIPs 184 | if(this.last_ownership[this.hostname]) { 185 | this.last_ownership[this.hostname].forEach(function (vip) { 186 | vip.forEach(function (iface) { 187 | if(typeof(iface) === "object") { 188 | // If used to have it, and haven't removed it and shouldn't have it 189 | // then remove it 190 | if(current_ips[iface.ip] && 191 | !removed_ips[iface.ip] && !should_have[iface.ip]) { 192 | self.vc.log('notice', 'bringing down ' + iface.name + '/' + iface.ip); 193 | self.emit('down', iface); 194 | } 195 | } 196 | }); 197 | }); 198 | } 199 | }; 200 | 201 | module.exports = VippyManager; 202 | -------------------------------------------------------------------------------- /lib/VippyNetwork.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, OmniTI Computer Consulting, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are 7 | * met: 8 | * 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * * Neither the name OmniTI Computer Consulting, Inc. nor the names 16 | * of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written 18 | * permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | 33 | var events = require('events'), 34 | net = require('net'), 35 | util = require('util'), 36 | ifeDriver = require('ife'), 37 | ife = new ifeDriver(), 38 | VippyNetwork; 39 | 40 | VippyNetwork = function(config, manager) { 41 | var self = this; 42 | this._vc = config 43 | this._manager = manager; 44 | this._arpjobs = {}; 45 | this._manager.on('up', function(iface) { self.up(iface); }); 46 | this._manager.on('down', function(iface) { self.down(iface); }); 47 | process.on('SIGINT', function() { self.shutdown(); }); 48 | process.on('SIGQUIT', function() { self.shutdown(); }); 49 | process.on('SIGTERM', function() { self.shutdown(); }); 50 | process.on('uncaughtException', function(err) { 51 | console.error(err.stack); 52 | self.shutdown(); 53 | }); 54 | } 55 | util.inherits(VippyNetwork, events.EventEmitter); 56 | 57 | VippyNetwork.prototype.shutdown_vips = function() { 58 | var self = this; 59 | self._vc.vips().forEach(function (vip) { 60 | vip.forEach(function (iface) { 61 | if(typeof(iface) === "object" && self.down(iface)) 62 | self._vc.log('notice', 'dropping ' + iface.ip); 63 | }); 64 | }); 65 | }; 66 | 67 | VippyNetwork.prototype.shutdown = function() { 68 | this._vc.log('notice', 'shutting down'); 69 | this.shutdown_vips(); 70 | this._vc.stop(); 71 | }; 72 | 73 | VippyNetwork.prototype.up = function(iface) { 74 | if(ife.up(iface)) { 75 | this.emit('up', true, iface); 76 | this.start_arp_accouncements(iface); 77 | } 78 | else { 79 | this.emit('up', false, iface); 80 | this._vc.log('crit', 'if_up(' + iface.name + '/' + iface.ip + ') failed'); 81 | } 82 | }; 83 | 84 | VippyNetwork.prototype.down = function(iface) { 85 | var ret; 86 | if(iface.state === undefined) 87 | ret = ife.down(iface.ip) 88 | else 89 | ret = ife.down(iface.ip, iface.state) 90 | if(ret) 91 | this.emit('down', true, iface); 92 | else { 93 | this.emit('down', false, iface); 94 | this._vc.log('crit', 'if_down(' + iface.name + '/' + iface.ip + ') failed'); 95 | } 96 | this._arpjobs[iface.ip] = false; 97 | }; 98 | 99 | var cmpIPv4 = function(a, b) { 100 | if(typeof(a) === "string") a = a.split('\.').map(function(x) { return parseInt(x); }); 101 | if(typeof(b) === "string") b = b.split('\.').map(function(x) { return parseInt(x); }); 102 | for (var i=0; i<4; i++) { 103 | if(a[i] < b[i]) return -1; 104 | if(a[i] > b[i]) return 1; 105 | } 106 | return 0; 107 | } 108 | var explodeIPs = function(start, end) { 109 | var results = {}; 110 | if(!net.isIPv4(start) || !net.isIPv4(end)) return []; 111 | var s = start.split('\.').map(function(x) { return parseInt(x); }); 112 | var e = end.split('\.').map(function(x) { return parseInt(x); }); 113 | var plus = function(a) { 114 | a[3]++; 115 | for (var i=3; i>0; i--) { 116 | if(a[i] > 255) { 117 | a[i]=0; 118 | a[i-1]++; 119 | } 120 | } 121 | } 122 | for(;cmpIPv4(s,e) <= 0;plus(s)) 123 | results[s.join('.')] = ''; 124 | return results; 125 | } 126 | VippyNetwork.prototype.gratarp_hitlist = function(iface) { 127 | var vectors = this._vc.notify(); 128 | var tgts = {}; 129 | var hitlist = []; 130 | if(!net.isIPv4(iface.ip)) return hitlist; 131 | for(var idx in vectors) { 132 | var vector = vectors[idx]; 133 | var subtgt = {} 134 | // Get a list of IP targets 135 | if(vector === "arp-cache") 136 | subtgt = this._manager.arpcache(); 137 | else if(typeof(vector) === "object") 138 | subtgt = explodeIPs(vector.network, vector.broadcast); 139 | // Add, but don't replace values (mac) 140 | for(var ip in subtgt) 141 | if(!tgts[ip]) tgts[ip] = subtgt[ip]; 142 | } 143 | for(var ip in tgts) { 144 | if(cmpIPv4(ip, iface.network) >= 0 && 145 | cmpIPv4(ip, iface.broadcast) <= 0) { 146 | hitlist.push([ip,tgts[ip]]); 147 | } 148 | } 149 | return hitlist; 150 | }; 151 | 152 | VippyNetwork.prototype.start_arp_accouncements = function(iface) { 153 | var hitlist = this.gratarp_hitlist(iface), 154 | self = this, do_one_gratarp; 155 | this._arpjobs[iface.ip] = true; 156 | do_one_gratarp = function() { 157 | var nip = hitlist.shift(); 158 | if(nip === undefined || self._arpjobs[iface.ip] == false) return; 159 | self._vc.log('debug', 'gratarp from ' + iface.name + '/' + iface.ip + 160 | ' to ' + nip[0] + '[' + nip[1] + ']'); 161 | var arpresponse = { 162 | name: iface.name, 163 | local_ip: iface.ip, 164 | remote_ip: nip[0], 165 | }; 166 | if(nip[1]) arpresponse.remote_mac = nip[1]; 167 | var sent = ife.gratarp(arpresponse, 2, nip[1] ? true : false); 168 | setTimeout(do_one_gratarp, 20); /* 50/s per interface */ 169 | }; 170 | do_one_gratarp(); 171 | }; 172 | 173 | module.exports = VippyNetwork; 174 | -------------------------------------------------------------------------------- /lib/console-logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, OmniTI Computer Consulting, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are 7 | * met: 8 | * 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * * Neither the name OmniTI Computer Consulting, Inc. nor the names 16 | * of its contributors may be used to endorse or promote products 17 | * derived from this software without specific prior written 18 | * permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | var program = process.argv[1], 33 | facility = 'user', 34 | opened = false, 35 | mask = { 36 | "emerg": true, 37 | "alert": true, 38 | "crit": true, 39 | "err": true, 40 | "warning": false, 41 | "notice": true, 42 | "info": false, 43 | "debug": false 44 | }; 45 | 46 | module.exports.openlog = function(_program, opts, _facility) { 47 | program = _program; 48 | if(_facility) facility = _facility; 49 | opened = true; 50 | } 51 | module.exports.closelog = function() { 52 | opened = false; 53 | } 54 | module.exports.setlogmask = function(_mask) { 55 | mask = _mask; 56 | } 57 | module.exports.syslog = function(pri, message) { 58 | if(!opened) return; 59 | if(mask[pri]) { 60 | console.log("[" + new Date() + "] [" + pri + "] " + 61 | program + "[" + process.pid + "]", message); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "vippy" 2 | , "description" : "Virtual IP Management and Failover" 3 | , "keywords" : [ "vip", "ha" ] 4 | , "version" : "0.0.11" 5 | , "preferGlobal" : true 6 | , "author" : { "name" : "Theo Schlossnagle" } 7 | , "repository" : 8 | { "type" : "git" 9 | , "url" : "git://github.com/postwait/vippy.git" 10 | } 11 | , "bugs" : 12 | { "url" : "http://github.com/postwait/vippy/issues" 13 | } 14 | , "main" : "./index" 15 | , "bin": 16 | { "vippyd": "./vippyd.js" 17 | , "vippyctl": "./ctl.js" 18 | } 19 | , "dependencies" : { "ife": ">=0.0.9", "posix": ">=1.0.0" } 20 | , "engines" : { "node" : "0.8 || 0.10" } 21 | , "licenses" : 22 | [ { "type" : "BSD" 23 | , "url" : "http://github.com/postwait/vippy/raw/master/vippy" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /smf/vippy-svc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Instructions: 4 | # 1. copy this script into /opt/local/sbin 5 | # 2. copy vippy.xml to /var/svc/manifest/network/ha/vippy.xml 6 | # 3. import the manifest with svccfg import /var/svc/manifest/network/ha/vippy.xml 7 | # 8 | # Optionally if required the nodename can be specified via /opt/local/etc/vippy.node 9 | 10 | export PATH=/usr/local/sbin:/usr/local/bin:/opt/local/sbin:/opt/local/bin:/usr/sbin:/usr/bin:/sbin 11 | 12 | socket="/var/run/vippy.socket" 13 | name="/opt/local/etc/vippy.node" 14 | conf="/opt/local/etc/vippy.conf" 15 | 16 | if [ -e /opt/local/etc/vippy.node ] ; then 17 | node=$(cat $name) 18 | start_cmd="vippyd -c $conf -n $node" 19 | else 20 | start_cmd="vippyd -c $conf" 21 | fi 22 | 23 | case $1 in 24 | start) 25 | if [ ! -e $socket ] ; then 26 | $start_cmd & 27 | exit 0 28 | else 29 | echo "ERROR: vippy $socket found, exiting.." 30 | exit 1 31 | fi 32 | ;; 33 | stop) 34 | pkill -2 -f .*vippy.* && rm -f $socket 35 | ;; 36 | esac 37 | -------------------------------------------------------------------------------- /smf/vippy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 11 | 12 | 17 | 18 | 22 | 23 | 24 | 29 | 30 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /vippy.conf: -------------------------------------------------------------------------------- 1 | { 2 | "chat": "224.1.1.1:5007", 3 | "chat_interface": "all", 4 | "secret": "knsdfkjnsdfkjnweroib0u3", 5 | "version": 5, 6 | "vips": [ 7 | [ { "interface": "e1000g0", "ip": "10.8.3.195/24" } ], 8 | [ { "interface": "e1000g0", "ip": "10.8.3.196/24" } ], 9 | [ 'node2', { "interface": "e1000g0", "ip": "10.8.99.147/24" } ], 10 | [ { "interface": "e1000g0", "ip": "10.8.99.149/24" } ] 11 | ], 12 | "nodes": [ 13 | "node1", "node2" 14 | ], 15 | "arp-cache": "90", 16 | "notify": [ 17 | "arp-cache", 18 | { "interface": "e1000g0", "ip": "10.8.99.149/24" }, 19 | { "interface": "e1000g0", "ip": "10.8.3.1/29" } 20 | ], 21 | "interval": 0.5, 22 | "mature": 5, 23 | "stale": 5, 24 | "management": "/var/run/vippy.socket", 25 | "logging": { 26 | "driver": "console", 27 | "facility": "local7", 28 | "mask": { 29 | "emerg": true, 30 | "alert": true, 31 | "crit": true, 32 | "err": true, 33 | "warning": false, 34 | "notice": true, 35 | "info": false, 36 | "debug": false 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vippyd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | Copyright (c) 2013, OmniTI Computer Consulting, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | * Neither the name OmniTI Computer Consulting, Inc. nor the names 17 | of its contributors may be used to endorse or promote products 18 | derived from this software without specific prior written 19 | permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | */ 33 | var vippy = require('vippy'), 34 | os = require('os'), 35 | fs = require('fs'), 36 | vm = require('vm'), 37 | hostname = os.hostname(), 38 | config_file = '/etc/vippy.conf', 39 | plugins = []; 40 | 41 | function usage(error) { 42 | if(error) console.log("Error: " + error + "\n"); 43 | console.log(process.argv[1] + ":"); 44 | console.log("\t-h"); 45 | console.log("\t-n "); 46 | console.log("\t-c "); 47 | console.log("\t-p "); 48 | process.exit(error ? -1 : 0); 49 | } 50 | for(var i=2; i