arvicco-avalon 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ config
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use ruby-1.9.3@avalon
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in avalon.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Avalon
2
+
3
+ Helper scripts for Bitcoin and Avalon miners management...
4
+
5
+ ## Usage
6
+
7
+ Scripts:
8
+
9
+ $ bin/monitor [last_ip]
10
+
11
+ - Monitors all miners from 151 to last_ip (default 182)
12
+
13
+ $ mtgox_tx
14
+
15
+ - Transcodes raw transaction from base64 (mtgox) to hex (blockchain) format
16
+
17
+ ## Configuration
18
+
19
+ Sample monitor config file for production environment below. Modify it, add your own nodes to be monitored.
20
+
21
+ ------- config/monitor.yml --------
22
+ # Prod configuration
23
+ prod:
24
+ :bitcoind:
25
+ :ip: 192.168.1.13
26
+ :rpcuser: jbond
27
+ :rpcpassword: youcannotguessitdonteventry
28
+ :monitor:
29
+ :verbose: true
30
+ :timeout: 30
31
+ :nodes:
32
+ - [miner, 192.168.1.151, 70] # type, ip, gh/s
33
+ - [miner, 192.168.1.152, 70]
34
+ - [eloipool, 192.168.1.13, 4] # frequency of old block updates (once per X polls)
35
+ - [internet, www.google.com, www.speedtest.net]
36
+
37
+ ## License
38
+
39
+ Copyright (c) 2013 Arvicco
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
42
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
44
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
45
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
46
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
47
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/avalon.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'avalon/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "arvicco-avalon"
8
+ gem.version = Avalon::VERSION
9
+ gem.authors = ["arvicco"]
10
+ gem.email = ["arvicco@gmail.com"]
11
+ gem.description = %q{Avalon miners monitor}
12
+ gem.summary = %q{Avalon miners monitor and set of helper scripts}
13
+ gem.homepage = "https://github.com/arvicco/avalon"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'faraday', '~> 0.8'
21
+
22
+ end
data/bin/monitor ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # Script to monitor Avalon miners status and alert if something's wrong
3
+
4
+ lib = File.expand_path('../../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require 'yaml'
8
+ require 'avalon'
9
+
10
+ env = ARGV[0] || 'prod'
11
+
12
+ Avalon::Config.load env
13
+
14
+ ### Monitor the nodes with 30-seconds interval
15
+ Avalon::Monitor.new(Avalon::Config[:monitor]).run
data/bin/mtgox_tx ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Convert base64-encoded transaction from MtGox: https://data.mtgox.com/api/0/bitcoin_tx.php
4
+ # into hex-encoded blockchain.info broadcast format: http://blockchain.info/pushtx
5
+
6
+ b = "AQAAAA5gpTBEnBaUTfIcLUAN8e6jsee37c97ftHtfH\/Xl7CUbwAAAACLSDBFAiEApkpegGIi\/a3eJMxhE\/B2Jgj8D9NsLKG0nCuuVqlZXDQCIHCS9CprboyheH2i\/0RKAR1N537kL8ObqS6YELKuNlJjAUEEl7kJqMY\/kPzUHYC9UZpoUjWk9q0GaCDT\/As9Oyj4k8Omna5PfXEOlgmoMGPeuaPdvrX5YvoIOgDt5YmGQlLU5P\/\/\/\/\/+BCVUK+snqr5aa5FHM5KCbObNyhMZO+ySTI4Hx5oVTwEAAACMSTBGAiEAw4Lr2YOhhzzAxcAon6RJpfnWkGPddxGhOZ9Z4B7Smr0CIQCZm1U6gUoB51O6CWNylmFXwS6CqJxSpauMLl55SWGG8QFBBME1N4MNJW3dGcwTc60X+q7Em6b23GoK+QnqA44LKDZNO1Z\/cK9bHCjnY\/QbljZM\/AabmG\/t8yZhLjGbYF8O9Gf\/\/\/\/\/w46DSYRF+4K203jp\/0wlB+YQMpF7BkOlJk61wbb2DlwBAAAAi0gwRQIgEK8POkLafi+rx8vdn2rI0Vx12e88R8hZiOQU+72W1IICIQCyDZxRvAWr\/aMdVnsMrwJ51C7D9+KNi7r+D\/RoIMhuuAFBBEhvV2zNdw4iqviZW+wnjhvnvzv6lFQRYoXCrf9orlqsWd3e83ifsabtGjgXgpqeEg7zqIheZl1oHNdPqWMFFfL\/\/\/\/\/lIiz4bFDwM0eerwMY5tmFUihgQzWBOW\/6IFZLXSYbRgAAAAAi0gwRQIgAkJaS+FQbpkRepZDnnAqnhvNFfNifPdhnYoL9+jyRoQCIQDy+GxDNBCRqqS6zI7ml7KhMvxlbFlH9tp66ABAL\/subAFBBCcjzZjgYZDXq1EcamMm17BeqOU1hTmaQic20TczNzOfDdVBEs5e6GhzCz5e8n8IGGM4kcNVLJDqWCKKAeMKzsr\/\/\/\/\/zoy8oH55hQs0dt71D71CCcpsvo++hnw0EReQy8N6\/6IAAAAAi0gwRQIgA0A0+S9GWJr95nmv\/nDsAMtMbY9A4nqRqJkjbF8\/SF8CIQD10rhtOw6V4oI69mMwPaEl2lwF0UztgzfgJNA\/V4yVQQFBBAJOvTvxWvvfvhXNFAytGq7tnBaau9kZg1odjv5VdJG3CD\/6JogGP+cuN8CGifOpheMy9wVCFPQBy\/NXPG\/+8Az\/\/\/\/\/8XNv\/XVGH1TrAR4eP83dzai7av1hpZ2NL116gfF8pHAAAAAAjEkwRgIhAIL5S5UaY25WX7ft9CUFzEo4c39\/QGuMy9bqi+lONG0jAiEA0PbPiTtQWYTbVMsqPJX0G9glnOLbF2BOjjwLgA6X3wQBQQT8txFQDpfQw6qeLq9JRY6aLCz2eUXI6yN9s44xXlo4z2NUX051QEKlCTwQSB6+z85Yh9gydgbZ0bX327rVeqvJ\/\/\/\/\/7pko4DJvRRmPsOeNLvh9dp00jihpNtQEi7eWnB\/UxfbAAAAAItIMEUCIA5Npskmq7m2bikd5fSA2iE4TKeCFrsUuSYt\/Ytvh3arAiEA8rPmnROglaJLJwGmcpPhrRBztCAfFnvYAij5f1G3zlwBQQRYSdO\/ZQYYCElyzEt4HvJfSw14RGI2rfs66xWDLvx7504OvOvtP+VjQkc6uFzVFcrgVt1Ng\/3f9i0\/7j1K41Og\/\/\/\/\/wZTWCmCUxT9x7W6FCMVvFWudPK9u5wlfuzTQgpJ9NHwAQAAAItIMEUCIHX3i9K1cqci4woVWkVt94LugBUU\/yx6ghZ6o7wLCcMIAiEA1JhkVnBT+03rqZUmFWpSycZOYaDOtZLfu7QG20MrZVABQQSrBUVYA6sOL92BX1anH3fxscYDn+iRxaFxw3q4scYnS\/eOzbIIor64FC27yuSwuZoDGAx8ejtTQae3ndLVNsnU\/\/\/\/\/+6i7RytXE10GgjoTfjZM1M4QaMn9VVj9wY65peWjnk0AAAAAItIMEUCIQDTJS478QKXlqEu5GXVcA0pjzmecRRqELxMhHladaQ6cQIgWMdgdbOWDaQAvafQKYi27YXl54LhfGwrupnJXUZXGA0BQQRmxjXA0Zl0oiod2DlB4Zql9VuuTcr7SAvdlbYxToa11ZDaqdgt5B7vK2SI7WsbDpyX1sagCZCSGSAGffs7BxkR\/\/\/\/\/7fl+i7qQb4SBNXvp3ZzpRRTcPMEALc+g7MuJVsOoSvfAQAAAItIMEUCIG+jFJgPeOm1TyDjubg7l72dYpHXLG9eHg3CVtJ\/zLl+AiEA9\/LtvLC2v0yA\/VTNWDRhYiAxkiCpVI2vfzBQBMf2LhEBQQS6ToEAnqnBObbloDppxbd3aQpVtAw2F+zhIP6HjxKxvgWPKuq0YQeBY62nGV1tDWwN153HCqMyqO+ZKte5qOL1\/\/\/\/\/3Bjm8m6g\/76VclwaeOc0NlkmatEeqYeLMgqKsFbFgApAAAAAIpHMEQCIGjASb\/nomXiF8m4yj42ksVSYZesPOYKnPIl504uA5v0AiASYPUKbAw97e2\/HGZtbJM+Elcej2Vpn9I85krlNVOOwwFBBEOknAa1OfGgDE1D\/LrnXU8Ppls3zCPHjMGg2DoBQRaFps1PII+QwMURMfmBSLMjkRemOLMIeIo+5Gu7LjB01Ez\/\/\/\/\/n\/CkvjAcz0wqZ3d7o3MEjASn+qpIjmbjyDrUujETB+DCAgAAjEkwRgIhAPbDogS+vEJ40jKYzpTBKf51\/eQG3fgzn+SQaTSRKylzAiEAyP\/NKXD\/M2RMuSgAGZIe8\/9KHas0HlNEDV5YMShx7A4BQQQ\/Qi+jJvRe1gciz0ZiEpoxwIcbrXgkKCmJ+duj8hfubekG3KKgIiojoLjpXa9FmeCSSaDdncY+PAi+n+\/\/tXcC\/\/\/\/\/5ONoUXKF1jbsXhZfwOOGZah3obDD1lLmBRo61MP8WE9AAAAAItIMEUCIDmMP\/3bMs89aUDcm7lB\/M3YlXtzVs1PqrQC8MV1HhNlAiEArUfI1\/ZVadC2gOiFBiueoMUy976WeClieb3OiVq9tD8BQQQ55n1QcmOGUf94QJXh82S7AhcPO4HdcVOtvgA1qFFGAHuBeb0qONwOKaK4oJbp81A7MHYj63a30WLjzT9pSr0p\/\/\/\/\/0yylb0nKeL0ZUGQtygVvA0JDDPmQJJyjWF5JjakVdWWAQAAAIpHMEQCIGmniWhlsq63a0gaUhFTQnADiImVBDCoVI9uW1ZvIbwjAiBrQV8+dN4Vw\/HDo6tp2VO2T1miFmOdUCPQvJBE2B4AZAFBBOlWw4rmRrly\/o7p35MupcMzIVGhI8R\/BwHop56Lqh33g8EPH5lhZvDyrQn91YG1xG7hXmBBv0IGT0hc\/IBZMZL\/\/\/\/\/AudXc18AAAAAGXapFIlXbWdn1WY40zObghcE8hk3V96\/iKwA0O2QLgAAABl2qRQUTbSqbmLg8UjL5lI\/bZU3KaVo8YisAAAAAA=="
7
+
8
+ s = b.unpack('m*')
9
+ p s
10
+ p s.first.unpack('H*').first
11
+
12
+ # require 'base64'
13
+ # i = Base64.decode64(b)
14
+ # p i.unpack('q*').first
15
+ # => 1297036692682702848
@@ -0,0 +1,22 @@
1
+ require 'json'
2
+
3
+ module Avalon
4
+
5
+ # Block contains details about block found by the pool
6
+ class Bitcoind
7
+
8
+ # Class methods
9
+ class << self
10
+
11
+ def method_missing *args
12
+ rpc = "bitcoind -rpcuser=#{config[:rpcuser]} -rpcpassword=#{config[:rpcpassword]}"
13
+ result = `ssh #{config[:ip]} "#{rpc} #{args.join(' ')}"`
14
+ JSON.parse(result) unless result.empty?
15
+ end
16
+
17
+ def config
18
+ Avalon::Config[:bitcoind]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,89 @@
1
+ module Avalon
2
+
3
+ # Block contains details about block found by the pool
4
+ class Block
5
+
6
+ extend Extractable
7
+
8
+ attr_accessor :data
9
+
10
+ # Field formats: name => [width, original name, type/conversion]
11
+ FIELDS = {
12
+ :height => [6, "height", :i], # not in miner status string...
13
+ :time => [19, "time", ->(t){ my_time(t, :absolute_date)}],
14
+ :received => [8, "received_time", ->(t){ my_time(t)}],
15
+ :reward => [6, "fee", ->(x){ (25 + x/100_000_000.0).round(3)}],
16
+ :txns => [4, "n_tx", :i],
17
+ :size => [3, "size", ->(x){ x/1024 }],
18
+ :relayed_by => [14, "relayed_by", :s],
19
+ :chain => [5, "main_chain", ->(x){ x ? 'main' : 'orphan' }],
20
+ :conf => [4, "confirmations", :i],
21
+ :hash => [56, "hash", ->(x){ x.sub(/^0*/,'')}],
22
+ }
23
+
24
+ def self.print_headers
25
+ puts FIELDS.map {|name, (width,_,_ )| name.to_s.ljust(width)}.join(' ')
26
+ end
27
+
28
+ def bitcoind_update
29
+ # {"hash" : "0000000000000029714fcc1f7bcd43cd13286b665f759eb018cfc539841623a4",
30
+ # "previousblockhash" : "0000000000000004d388fd4e7bd6aa1c3f3eeae0ceadd7a0bc51ee1fee0be910",
31
+ # "merkleroot" : "844f841c3fc276023a561503fc21d064bd898d6c9530d34a864efe70f67bfde8",
32
+ # "version" : 2,
33
+ # "size" : 249181,
34
+ # "height" : 238605,
35
+ # "time" : 1369877473,
36
+ # "bits" : "1a016164",
37
+ # "tx" : [..]
38
+ # ---------
39
+ # "nonce" : 3370559418,
40
+ # "confirmations" : 0,
41
+ # "difficulty" : 12153411.70977583}
42
+ bitcoind_info = Bitcoind.getblock @data[:hash], "| grep -v -E '^ .*,'"
43
+ @data.merge! self.class.extract_data_from( bitcoind_info ) if bitcoind_info
44
+ end
45
+
46
+ def blockchain_update
47
+ #{"hash"=>"00000000000000783be7e82df4d8a71bf1fd8073d2bbd60f2b8638e4d042d32c",
48
+ # "prev_block"=> "00000000000000b277cace9f2556fb8e0545038e83d20846cc4e3a3f61d0f2f2",
49
+ # "mrkl_root"=> "0a9a292c93ad732e55eeb25633d3e7580894faea908876f2d17589e127e904cd",
50
+ # "ver"=>2,
51
+ # "size"=>232091,
52
+ # "height"=>238557,
53
+ # "time"=>1369850827,
54
+ # "bits"=>436298084,
55
+ # ---------
56
+ # "fee"=>41058500,
57
+ # "nonce"=>825834732,
58
+ # "n_tx"=>504,
59
+ # "block_index"=>386930,
60
+ # "main_chain"=>true,
61
+ # "received_time"=>1369850888,
62
+ # "relayed_by"=>"37.251.86.21"}
63
+ blockchain_info = Blockchain.rawblock @data[:hash].rjust(64, '0')
64
+ @data.merge! self.class.extract_data_from( blockchain_info ) if blockchain_info
65
+ end
66
+
67
+ def pending?
68
+ @data[:reward].nil? && @data[:received].nil?
69
+ end
70
+
71
+ def initialize hash
72
+ case hash
73
+ when Hash
74
+ @data = hash
75
+ when String
76
+ @data = {:hash => hash}
77
+ bitcoind_update
78
+ blockchain_update
79
+ else
80
+ raise ArgumentError, "Wrong argument to Block.new: #{hash}"
81
+ end
82
+ end
83
+
84
+ def to_s
85
+ FIELDS.map {|key, (width, _, _ )| @data[key].to_s.ljust(width)}.join(" ")
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,30 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module Avalon
5
+
6
+ # Block contains details about block found by the pool
7
+ class Blockchain
8
+
9
+ # Class methods
10
+ class << self
11
+
12
+ # Establish Faraday connection on first call
13
+ def conn
14
+ @conn ||= Faraday.new(:url => 'http://blockchain.info') do |faraday|
15
+ # faraday.response :logger # log requests to STDOUT
16
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
17
+ end
18
+ end
19
+
20
+ def rawblock block_hash
21
+ get "rawblock/#{block_hash}"
22
+ end
23
+
24
+ def get path
25
+ reply = conn.get "#{path}?format=json"
26
+ JSON.parse(reply.body) if reply.success?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ module Avalon
2
+ # Global Config
3
+ class Config
4
+
5
+ def self.load env
6
+ config_file = File.expand_path('../../../config/monitor.yml', __FILE__)
7
+ raise "No config file: #{config_file}" unless File.exist? config_file
8
+
9
+ @config = YAML::load_file(config_file)[env]
10
+ end
11
+
12
+ def self.[] key
13
+ @config[key]
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,103 @@
1
+ module Avalon
2
+ # Pool is a node encapsulating pool software
3
+ class Eloipool < Node
4
+
5
+ def initialize ip, frequency
6
+ @ip = ip
7
+ @update_frequency = frequency
8
+ @update_num = 0
9
+ @block_file = File.expand_path('../../../config/blocks.yml', __FILE__)
10
+ @blocks = load_blocks || {}
11
+ super()
12
+ end
13
+
14
+ def poll verbose=true
15
+ self[:ping] = ping @ip
16
+
17
+ self[:found] = `ssh #{@ip} "cat solo/logs/pool.log | grep BLKHASH | wc -l"`.to_i
18
+
19
+ update_old_block
20
+
21
+ puts "#{self}" if verbose
22
+ end
23
+
24
+ # Check for any exceptional situations, sound alarm if any
25
+ def report
26
+ if self[:ping].nil?
27
+ alarm "Eloipool at #{@ip} not responding to ping"
28
+ elsif self[:found] > @blocks.size
29
+ add_new_blocks `ssh #{@ip} "cat solo/logs/pool.log | grep BLKHASH"`
30
+ alarm "Eloipool found #{@found} blocks", "Dog.aiff", "Purr.aiff", "Dog.aiff"
31
+ elsif @blocks[@blocks.keys.last].pending?
32
+ update_block @blocks[@blocks.keys.last] do
33
+ alarm "Eloipool last block updated", "Purr.aiff", "Purr.aiff", "Purr.aiff"
34
+ end
35
+ end
36
+ end
37
+
38
+ def save_blocks
39
+ dump = @blocks.values.map(&:data)
40
+ File.open(@block_file, "w") {|file| YAML.dump(dump, file)}
41
+ end
42
+
43
+ def load_blocks print = true
44
+ if File.exist?(@block_file)
45
+ Block.print_headers
46
+ dump = YAML::load_file(@block_file)
47
+ Hash[
48
+ *dump.map do |data|
49
+ block = Block.new data
50
+ puts block
51
+ [data[:hash], block]
52
+ end.flatten
53
+ ]
54
+ end
55
+ end
56
+
57
+ def update_old_block
58
+ if rand(@update_frequency) == 0 # update once per @frequency polls
59
+ hash = @blocks.keys[@update_num]
60
+ if @blocks[hash]
61
+ @update_num += 1
62
+ update_block(@blocks[hash], true)
63
+ else
64
+ @update_num = 0
65
+ end
66
+ end
67
+ end
68
+
69
+ def update_block block, print = true
70
+ if (block.pending? ? block.blockchain_update : block.bitcoind_update )
71
+ if print
72
+ Block.print_headers
73
+ puts block
74
+ end
75
+ save_blocks
76
+ yield block if block_given?
77
+ end
78
+ end
79
+
80
+ # Add new blocks from pool log
81
+ def add_new_blocks pool_log, print = true
82
+ Block.print_headers if print
83
+ pool_log.split(/\n/).each do |line|
84
+ hash = line.chomp.match(/\h*$/).to_s
85
+ unless @blocks[hash]
86
+ @blocks[hash] = Block.new(hash)
87
+ puts @blocks[hash] if print
88
+ @pending_hash = hash
89
+ end
90
+ end
91
+ save_blocks
92
+ end
93
+
94
+ def to_s
95
+ "Eloipool: " + @data.map {|name, value| "#{name}:#{value}"}.join(" ")
96
+ end
97
+
98
+ def inspect
99
+ @data.map {|name, value| "#{name}:#{value}"}.join(" ")
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,52 @@
1
+ module Avalon
2
+
3
+ # Mix-in for extraction of properties from a given input String or Hash
4
+ # Depeds on #fields method hook in the host class to enumerate property fields,
5
+ # in the format: {:name => [width, pattern, type/conversion]}
6
+ module Extractable
7
+
8
+ # type = :absolute_time | :absolute_date | :relative_time
9
+ def my_time t, type=:absolute_time
10
+ time = Time.at(t.to_f)
11
+ case type
12
+ when :absolute_date
13
+ time.getlocal.strftime("%Y-%m-%d %H:%M:%S")
14
+ when :absolute_time
15
+ time.getlocal.strftime("%H:%M:%S")
16
+ when :relative_time
17
+ time.utc.strftime("#{(time.day-1)*24+time.hour}:%M:%S")
18
+ end
19
+ end
20
+
21
+ def print_headers
22
+ puts self::FIELDS.map {|name, (width,_,_ )| name.to_s.ljust(width)}.join(' ')
23
+ end
24
+
25
+
26
+ # Extract data from String OR Hash
27
+ def extract_data_from input
28
+ if input.nil? || input.empty?
29
+ {}
30
+ else
31
+ # Convert the input into usable data pairs
32
+ pairs = self::FIELDS.map do |name, (_, pattern, type)|
33
+ val = input[pattern] # works for both pattern and key
34
+ unless val.nil?
35
+ case type
36
+ when Symbol
37
+ [name, val.send("to_#{type}")]
38
+ when Proc
39
+ [name, type.call(val)]
40
+ when '' # no conversion
41
+ [name, val]
42
+ else
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ Hash[*pairs.compact.flatten]
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ module Avalon
2
+
3
+ # Internet is a node encapsulating information about Internet connectivity
4
+ class Internet < Node
5
+
6
+ def initialize *sites
7
+ @sites = Hash[ *sites.map {|site| [site.split(/\./)[-2].to_sym, site]}.flatten ]
8
+ super()
9
+ end
10
+
11
+ def poll verbose=true
12
+ @sites.each {|name, site| self[name] = ping site }
13
+ puts "#{self}" if verbose
14
+ end
15
+
16
+ # Check for any exceptional situations with Node, sound alarm if any
17
+ def report
18
+ @data.each do |target, ping|
19
+ alarm "Ping #{target} failed, check your Internet connection" unless ping
20
+ end
21
+ end
22
+
23
+ def to_s
24
+ "Internet: " + @data.map {|name, value| "#{name}:#{value}"}.join(" ")
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,90 @@
1
+ module Avalon
2
+ # "STATUS=S,When=1368715953,Code=11,Msg=Summary,Description=cgminer 2.10.5|
3
+ # SUMMARY,Elapsed=58219,MHS av=70827.84,Found Blocks=0,Getworks=1899,Accepted=14755,
4
+ # Rejected=162,Hardware Errors=2352,Utility=15.21,Discarded=3529,Stale=0,Get Failures=0,
5
+ # Local Work=1068039,Remote Failures=0,Network Blocks=107,Total MH=4123494727.5140,
6
+ # Work Utility=992.58,Difficulty Accepted=944320.00000000,Difficulty Rejected=10368.00000000,
7
+ # Difficulty Stale=0.00000000,Best Share=7493180|\u0000"
8
+
9
+ # Miner is a node encapsulating a single Avalon unit
10
+ class Miner < Node
11
+
12
+ extend Extractable
13
+
14
+ # Field formats: name => [width, pattern, type/conversion]
15
+ FIELDS = {
16
+ :ping => [8, /./, nil], # not in miner status string...
17
+ :mhs => [6, /(?<=MHS av=)[\d\.]*/, :i],
18
+ # :uptime => [6, /(?<=Elapsed=)[\d\.]*/, ->(x){ (x.to_i/60.0/60.0).round(2)}],
19
+ :uptime => [8, /(?<=Elapsed=)[\d\.]*/, ->(x){ my_time(x, :relative_time)}],
20
+ :last => [8, /(?<=Last Share Time=)[\d\.]*/,
21
+ ->(x){ my_time(Time.now.getgm-x.to_i, :relative_time)}],
22
+ # ->(x){ my_time(Time.now-Time.at(x.to_i), :relative_time)}],
23
+ :utility => [7, /(?<=,Utility=)[\d\.]*/, :f],
24
+ :getworks => [8, /(?<=Getworks=)[\d\.]*/, :i],
25
+ :accepted => [8, /(?<=,Accepted=)[\d\.]*/, :i],
26
+ :rejected => [8, /(?<=Rejected=)[\d\.]*/, :i],
27
+ :stale => [6, /(?<=Stale=)[\d\.]*/, :i],
28
+ :errors => [6, /(?<=Hardware Errors=)[\d\.]*/, :i],
29
+ :blocks => [6, /(?<=Network Blocks=)[\d\.]*/, :i],
30
+ :found => [2, /(?<=Found Blocks=)[\d\.]*/, :i],
31
+ }
32
+
33
+ def self.print_headers
34
+ puts "\nMiner status as of #{Time.now.getlocal.asctime}:\n# " +
35
+ FIELDS.map {|name, (width,_,_ )| name.to_s.ljust(width)}.join(' ')
36
+ end
37
+
38
+ def initialize ip, min_speed
39
+ @ip = ip
40
+ @num = ip.split('.').last.to_i
41
+ @min_speed = min_speed * 1000 # Gh/s to Mh/s
42
+ @blanks = 0
43
+ super()
44
+ end
45
+
46
+ def get_api call
47
+ self[:ping] ? `bash -ic "echo -n '#{call}' | nc #{@ip} 4028"` : ""
48
+ end
49
+
50
+ def poll verbose=true
51
+ self[:ping] = ping @ip
52
+
53
+ status = get_api('summary') + get_api('pools')
54
+ data = self.class.extract_data_from(status)
55
+
56
+ if data.empty?
57
+ @data = {}
58
+ else
59
+ @data.merge! data
60
+ end
61
+
62
+ puts "#{self}" if verbose
63
+ end
64
+
65
+ def upminutes
66
+ hour, min, _ = *self[:uptime].split(/:/).map(&:to_i)
67
+ hour*60 + min
68
+ end
69
+
70
+ # Check for any exceptional situations in stats, sound alarm if any
71
+ def report
72
+ if data[:ping].nil?
73
+ @blanks += 1
74
+ alarm "Miner #{@num} did not respond to status query" if @blanks > 2
75
+ else
76
+ @blanks = 0
77
+ if self[:mhs] < @min_speed*0.95 and upminutes > 5
78
+ alarm "Miner #{@num} performance is #{self[:mhs]}, should be #{@min_speed}"
79
+ elsif upminutes < 2
80
+ alarm "Miner #{@num} restarted", "Frog.aiff"
81
+ end
82
+ end
83
+ end
84
+
85
+ def to_s
86
+ "#{@num}: " + FIELDS.map {|key, (width, _, _ )| @data[key].to_s.ljust(width)}.join(" ")
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,29 @@
1
+ module Avalon
2
+
3
+ class Monitor
4
+
5
+ # List of nodes to monitor
6
+ def initialize opts
7
+ @nodes = opts[:nodes].map {|args| Avalon::Node.create(*args)}
8
+ @timeout = opts[:timeout] || 30
9
+ @verbose = opts[:verbose]
10
+ end
11
+
12
+ def run
13
+ loop do
14
+
15
+ Avalon::Miner.print_headers if @verbose
16
+
17
+ # Check status for all nodes
18
+ @nodes.each {|node| node.poll(@verbose)}
19
+
20
+ # Report node errors (if any)
21
+ @nodes.each {|node| node.report}
22
+
23
+ sleep @timeout
24
+
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,60 @@
1
+ module Avalon
2
+
3
+ # Node is a single object to be monitored
4
+ # It should implement simple interface, only 2 required methods: #poll and #report
5
+ class Node
6
+
7
+ # Builder method for creating Node subclasses from config arrays
8
+ def Node.create *args
9
+ subclass = Avalon.const_get(args.first.capitalize)
10
+ subclass.new *args.drop(1)
11
+ end
12
+
13
+ attr_accessor :data
14
+
15
+ def initialize
16
+ @data = {}
17
+ end
18
+
19
+ # Get a specific data point about this Node
20
+ def [] key
21
+ @data[key]
22
+ end
23
+
24
+ # Set a specific data point
25
+ def []= key, value
26
+ @data[key] = value
27
+ end
28
+
29
+ # Helper method: sound alarm with message
30
+ def alarm message, *tunes
31
+ puts message
32
+
33
+ tunes.push('Glass.aiff') if tunes.empty?
34
+
35
+ tunes.each do |tune|
36
+ File.exist?(tune) ? `afplay #{tune}` : `afplay /System/Library/Sounds/#{tune}`
37
+ end
38
+ end
39
+
40
+ # Helper method: ping the Node
41
+ def ping ip
42
+ ping_result = `ping -c 1 #{ip}`
43
+ if ping_result =~ / 0.0% packet loss/
44
+ ping_result.match(/time=([\.\d]*) ms/)[1].to_f
45
+ end
46
+ end
47
+
48
+ # Abstract: Check node status
49
+ # If verbose, the Node should print out its state after the status update
50
+ def poll verbose
51
+ raise "#{self.class} should implement #poll"
52
+ end
53
+
54
+ # Abstract: Report node errors or special situations (if any)
55
+ def report
56
+ raise "#{self.class} should implement #report"
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module Avalon
2
+ VERSION = "0.0.2"
3
+ end
data/lib/avalon.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'pp'
2
+ require 'time'
3
+
4
+ require "avalon/version"
5
+ require "avalon/blockchain"
6
+ require "avalon/bitcoind"
7
+ require "avalon/config"
8
+ require "avalon/extractable"
9
+
10
+ require "avalon/block"
11
+ require "avalon/node"
12
+ require "avalon/miner"
13
+ require "avalon/internet"
14
+ require "avalon/eloipool"
15
+ require "avalon/monitor"
16
+
17
+ module Avalon
18
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arvicco-avalon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - arvicco
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.8'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.8'
30
+ description: Avalon miners monitor
31
+ email:
32
+ - arvicco@gmail.com
33
+ executables:
34
+ - monitor
35
+ - mtgox_tx
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - .gitignore
40
+ - .rvmrc
41
+ - Gemfile
42
+ - README.md
43
+ - Rakefile
44
+ - avalon.gemspec
45
+ - bin/monitor
46
+ - bin/mtgox_tx
47
+ - lib/avalon.rb
48
+ - lib/avalon/bitcoind.rb
49
+ - lib/avalon/block.rb
50
+ - lib/avalon/blockchain.rb
51
+ - lib/avalon/config.rb
52
+ - lib/avalon/eloipool.rb
53
+ - lib/avalon/extractable.rb
54
+ - lib/avalon/internet.rb
55
+ - lib/avalon/miner.rb
56
+ - lib/avalon/monitor.rb
57
+ - lib/avalon/node.rb
58
+ - lib/avalon/version.rb
59
+ homepage: https://github.com/arvicco/avalon
60
+ licenses: []
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 1.8.24
80
+ signing_key:
81
+ specification_version: 3
82
+ summary: Avalon miners monitor and set of helper scripts
83
+ test_files: []