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 +18 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +47 -0
- data/Rakefile +1 -0
- data/avalon.gemspec +22 -0
- data/bin/monitor +15 -0
- data/bin/mtgox_tx +15 -0
- data/lib/avalon/bitcoind.rb +22 -0
- data/lib/avalon/block.rb +89 -0
- data/lib/avalon/blockchain.rb +30 -0
- data/lib/avalon/config.rb +17 -0
- data/lib/avalon/eloipool.rb +103 -0
- data/lib/avalon/extractable.rb +52 -0
- data/lib/avalon/internet.rb +28 -0
- data/lib/avalon/miner.rb +90 -0
- data/lib/avalon/monitor.rb +29 -0
- data/lib/avalon/node.rb +60 -0
- data/lib/avalon/version.rb +3 -0
- data/lib/avalon.rb +18 -0
- metadata +83 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create use ruby-1.9.3@avalon
|
data/Gemfile
ADDED
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
|
data/lib/avalon/block.rb
ADDED
@@ -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
|
data/lib/avalon/miner.rb
ADDED
@@ -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
|
data/lib/avalon/node.rb
ADDED
@@ -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
|
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: []
|