diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/package.json b/package.json index 0260cce..19aa409 100644 --- a/package.json +++ b/package.json @@ -10,4 +10,8 @@ "type" : "git" , "url" : "https://github.com/plainlystated/coffeescript-rrd" } + , "devDependencies" : { + "coffee-script": "~> 1.1.2" + , "vows": "~> 0.5.3" + } } diff --git a/rrd.coffee b/rrd.coffee index 3847eb3..37506ce 100644 --- a/rrd.coffee +++ b/rrd.coffee @@ -4,6 +4,7 @@ spawn = require('child_process').spawn fs = require('fs') RRDRecord = require('./rrdRecord').RRDRecord +RRDInfo = require('./rrdInfo').RRDInfo class RRD constructor: (@filename) -> @@ -74,6 +75,10 @@ class RRD cb(undefined, records) + info: (cb) -> + @rrdSpawn "info", [], (err, results) -> + return cb(null, new RRDInfo(results)) + _rrdTime = (date) -> return Math.round(date.valueOf() / 1000) diff --git a/rrd.js b/rrd.js index 1e04027..1758c68 100644 --- a/rrd.js +++ b/rrd.js @@ -1,15 +1,25 @@ (function() { - var RRD, RRDRecord, exec, fs, spawn, sys; + var RRD, RRDInfo, RRDRecord, exec, fs, spawn, sys; + sys = require('sys'); + exec = require('child_process').exec; + spawn = require('child_process').spawn; + fs = require('fs'); + RRDRecord = require('./rrdRecord').RRDRecord; + + RRDInfo = require('./rrdInfo').RRDInfo; + RRD = (function() { var _rrdTime; + function RRD(filename) { this.filename = filename; } + RRD.prototype.create = function(rrdArgs, options, cb) { var cmdArgs, err, proc, start, _ref; start = (_ref = options.start) != null ? _ref : new Date; @@ -28,12 +38,15 @@ } }); }; + RRD.prototype.destroy = function(cb) { return fs.unlink(this.filename, cb); }; + RRD.prototype.dump = function(cb) { return this.rrdSpawn("dump", [], cb); }; + RRD.prototype.rrdExec = function(command, cmd_args, cb) { var cmd; cmd = "rrdtool " + command + " " + this.filename + " " + cmd_args; @@ -42,6 +55,7 @@ maxBuffer: 500 * 1024 }, cb); }; + RRD.prototype.rrdSpawn = function(command, args, cb) { var err, out, proc; proc = spawn("rrdtool", [command, this.filename].concat(args)); @@ -61,9 +75,11 @@ } }); }; + RRD.prototype.update = function(time, values, cb) { return this.rrdSpawn("update", ["" + (_rrdTime(time)) + ":" + (values.join(':'))], cb); }; + RRD.prototype.fetch = function(start, end, cb) { return this.rrdExec("fetch", "AVERAGE --start " + start + " --end " + end, function(err, data) { var fieldNames, fields, i, line, lines, record, records; @@ -79,12 +95,8 @@ _results = []; for (_i = 0, _len = lines.length; _i < _len; _i++) { line = lines[_i]; - if (line === "") { - continue; - } - if (line.match(" nan ")) { - continue; - } + if (line === "") continue; + if (line.match(" nan ")) continue; fields = line.split(new RegExp("[: ]+")); record = new RRDRecord(fields.shift(), fieldNames); for (i = 0, _ref = fields.length - 1; 0 <= _ref ? i <= _ref : i >= _ref; 0 <= _ref ? i++ : i--) { @@ -97,13 +109,25 @@ return cb(void 0, records); }); }; + + RRD.prototype.info = function(cb) { + return this.rrdSpawn("info", [], function(err, results) { + return cb(null, new RRDInfo(results)); + }); + }; + _rrdTime = function(date) { return Math.round(date.valueOf() / 1000); }; + return RRD; + })(); + RRD.restore = function(filenameXML, filenameRRD, cb) { return exec("rrdtool restore " + filenameXML + " " + filenameRRD, cb); }; + exports.RRD = RRD; + }).call(this); diff --git a/rrdInfo.coffee b/rrdInfo.coffee new file mode 100644 index 0000000..01b4f69 --- /dev/null +++ b/rrdInfo.coffee @@ -0,0 +1,30 @@ +class RRDInfo + constructor: (raw_output) -> + this.parse_line(line) for line in raw_output.split('\n') + + ds_names: () -> + keys = [] + keys.push(k) for k of this['ds'] + return keys + + parse_line: (line) -> + line = line.replace(/["\s]/g, '') + return if line == '' + + [key, value] = line.split('=') + + key_tree = if key.match /[\.\[]/ + key.replace(/\[/g, '.').replace(/[\]\s]/g, '').split('.') + else + [key] + + _add_value(key_tree, value, this) + + _add_value = (key_tree, value, obj) -> + return obj[key_tree[0]] = value if key_tree.length == 1 + new_key = key_tree.shift() + obj[new_key] ?= new Object + + return _add_value(key_tree, value, obj[new_key]) + +exports.RRDInfo = RRDInfo diff --git a/rrdInfo.js b/rrdInfo.js new file mode 100644 index 0000000..1051cd9 --- /dev/null +++ b/rrdInfo.js @@ -0,0 +1,48 @@ +(function() { + var RRDInfo; + + RRDInfo = (function() { + var _add_value; + + function RRDInfo(raw_output) { + var line, _i, _len, _ref; + _ref = raw_output.split('\n'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + line = _ref[_i]; + this.parse_line(line); + } + } + + RRDInfo.prototype.ds_names = function() { + var k, keys; + keys = []; + for (k in this['ds']) { + keys.push(k); + } + return keys; + }; + + RRDInfo.prototype.parse_line = function(line) { + var key, key_tree, value, _ref; + line = line.replace(/["\s]/g, ''); + if (line === '') return; + _ref = line.split('='), key = _ref[0], value = _ref[1]; + key_tree = key.match(/[\.\[]/) ? key.replace(/\[/g, '.').replace(/[\]\s]/g, '').split('.') : [key]; + return _add_value(key_tree, value, this); + }; + + _add_value = function(key_tree, value, obj) { + var new_key, _ref; + if (key_tree.length === 1) return obj[key_tree[0]] = value; + new_key = key_tree.shift(); + if ((_ref = obj[new_key]) == null) obj[new_key] = new Object; + return _add_value(key_tree, value, obj[new_key]); + }; + + return RRDInfo; + + })(); + + exports.RRDInfo = RRDInfo; + +}).call(this); diff --git a/test/info-test.coffee b/test/info-test.coffee new file mode 100644 index 0000000..40f2a5d --- /dev/null +++ b/test/info-test.coffee @@ -0,0 +1,21 @@ +vows = require('vows') +assert = require('assert') +RRDInfo = require('../rrdInfo').RRDInfo +valid_contents = require('fs').readFileSync('./valid.info').toString() + +vows.describe('RRDInfo').addBatch( + 'from a valid rrd file': + topic: new RRDInfo(valid_contents) + + 'should have the filename set': (topic) -> + assert.equal(topic.filename, 'valid.rrd') + + 'should parse multi level lines': (topic) -> + assert.equal(topic.ds.state.last_ds, 0) + + 'should list the ds_names': (topic) -> + assert.include(topic.ds_names(), 'temperature') + assert.include(topic.ds_names(), 'target_temp') + assert.include(topic.ds_names(), 'state') + +).export(module, {error: false}) diff --git a/test/rrd-test.coffee b/test/rrd-test.coffee index 00a4b33..8c4465e 100644 --- a/test/rrd-test.coffee +++ b/test/rrd-test.coffee @@ -1,6 +1,7 @@ vows = require('vows') assert = require('assert') RRD = require('../rrd').RRD +RRDInfo = require('../rrdInfo').RRDInfo exec = require('child_process').exec vows.describe('RRD').addBatch( @@ -99,6 +100,17 @@ vows.describe('RRD').addBatch( # Need to figure out how to test this without messing up subsequent test runs # 'returns no error': (err) -> # assert.equal(err, undefined) + 'when infoing': + topic: (rrd) -> + rrd.info(@callback) + return + + 'returns no error': (err, data) -> + assert.equal(err, null) + + 'returns an RRDInfo object': (err, data) -> + assert.instanceOf(data, RRDInfo) + 'when restoring': 'a valid file': topic: () -> diff --git a/test/valid.info b/test/valid.info new file mode 100644 index 0000000..923d0c3 --- /dev/null +++ b/test/valid.info @@ -0,0 +1,73 @@ +filename = "valid.rrd" +rrd_version = "0003" +step = 300 +last_update = 1310750319 +header_size = 2320 +ds[temperature].index = 0 +ds[temperature].type = "GAUGE" +ds[temperature].minimal_heartbeat = 600 +ds[temperature].min = NaN +ds[temperature].max = NaN +ds[temperature].last_ds = "131" +ds[temperature].value = 2.8608000000e+04 +ds[temperature].unknown_sec = 0 +ds[target_temp].index = 1 +ds[target_temp].type = "GAUGE" +ds[target_temp].minimal_heartbeat = 600 +ds[target_temp].min = NaN +ds[target_temp].max = NaN +ds[target_temp].last_ds = "68" +ds[target_temp].value = 1.4892000000e+04 +ds[target_temp].unknown_sec = 0 +ds[state].index = 2 +ds[state].type = "GAUGE" +ds[state].minimal_heartbeat = 600 +ds[state].min = NaN +ds[state].max = NaN +ds[state].last_ds = "0" +ds[state].value = 0.0000000000e+00 +ds[state].unknown_sec = 0 +rra[0].cf = "AVERAGE" +rra[0].rows = 300 +rra[0].cur_row = 182 +rra[0].pdp_per_row = 1 +rra[0].xff = 5.0000000000e-01 +rra[0].cdp_prep[0].value = NaN +rra[0].cdp_prep[0].unknown_datapoints = 0 +rra[0].cdp_prep[1].value = NaN +rra[0].cdp_prep[1].unknown_datapoints = 0 +rra[0].cdp_prep[2].value = NaN +rra[0].cdp_prep[2].unknown_datapoints = 0 +rra[1].cf = "AVERAGE" +rra[1].rows = 300 +rra[1].cur_row = 38 +rra[1].pdp_per_row = 7 +rra[1].xff = 5.0000000000e-01 +rra[1].cdp_prep[0].value = 6.3484000000e+02 +rra[1].cdp_prep[0].unknown_datapoints = 0 +rra[1].cdp_prep[1].value = 3.4000000000e+02 +rra[1].cdp_prep[1].unknown_datapoints = 0 +rra[1].cdp_prep[2].value = 0.0000000000e+00 +rra[1].cdp_prep[2].unknown_datapoints = 0 +rra[2].cf = "AVERAGE" +rra[2].rows = 300 +rra[2].cur_row = 177 +rra[2].pdp_per_row = 31 +rra[2].xff = 5.0000000000e-01 +rra[2].cdp_prep[0].value = 3.2700500000e+03 +rra[2].cdp_prep[0].unknown_datapoints = 0 +rra[2].cdp_prep[1].value = 1.8360000000e+03 +rra[2].cdp_prep[1].unknown_datapoints = 0 +rra[2].cdp_prep[2].value = 0.0000000000e+00 +rra[2].cdp_prep[2].unknown_datapoints = 0 +rra[3].cf = "AVERAGE" +rra[3].rows = 1500 +rra[3].cur_row = 417 +rra[3].pdp_per_row = 352 +rra[3].xff = 5.0000000000e-01 +rra[3].cdp_prep[0].value = 1.4339880000e+04 +rra[3].cdp_prep[0].unknown_datapoints = 0 +rra[3].cdp_prep[1].value = 9.7240000000e+03 +rra[3].cdp_prep[1].unknown_datapoints = 0 +rra[3].cdp_prep[2].value = 0.0000000000e+00 +rra[3].cdp_prep[2].unknown_datapoints = 0