arvicco-avalon 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: []