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