arvicco-avalon 0.0.23 → 0.0.25

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -14,7 +14,7 @@ Scripts:
14
14
 
15
15
  $ monitor [environment]
16
16
 
17
- Monitors all the nodes (miners, pools, Internet connections) that are listed in config/monitor.yml file. Sounds alarm is anything is wrong with the monitored nodes. TODO: takes action to correct errors found (like restarting the failing miners etc).
17
+ Monitors all the nodes (miners, pools, Internet connections) that are listed in config/monitor.yml file. Sounds alarm if anything goes wrong with the monitored nodes. Alarm sounds are configurable. TODO: takes action to correct errors found (like restarting the failing miners etc).
18
18
 
19
19
  $ reboot_miner 145 146 192.168.0.150
20
20
 
@@ -40,14 +40,17 @@ Monitor script is periodically polling the mining units and other types of objec
40
40
  # Prod configuration (default)
41
41
  prod:
42
42
  :alert_after: 2 # missed pings or status reports from a miner
43
- :alert_temp_high: 52 # degrees C and above
43
+ :alert_temp_high: 55 # degrees C and above
44
44
  :alert_temp_low: 30 # degrees C and below, Avalon miners only
45
45
  :alert_last_share: 2 # minutes since last share hashed
46
46
  :alert_sounds:
47
- :failure: Glass.aiff # [] for no sound
48
47
  :restart: Frog.aiff # [] for no sound
49
- :temp_high: Ping.aiff # [] for no sound
50
- :block_found: [Dog.aiff, Purr.aiff, Dog.aiff] # [] for no sound
48
+ :failure: Glass.aiff
49
+ :perf_low: Glass.aiff
50
+ :last_share: Glass.aiff
51
+ :temp_high: Ping.aiff
52
+ :temp_low: Ping.aiff
53
+ :block_found: [Dog.aiff, Purr.aiff, Dog.aiff]
51
54
  :block_updated: [Purr.aiff, Purr.aiff, Purr.aiff] # [] for no alert sound
52
55
  :bitcoind:
53
56
  :ip: 192.168.1.13
@@ -56,9 +59,12 @@ Monitor script is periodically polling the mining units and other types of objec
56
59
  :monitor:
57
60
  :verbose: true
58
61
  :timeout: 30
62
+ :per_hour: true # If true, getworks, rejects... metrics will be displayed PER HOUR
59
63
  :nodes:
60
- - [miner, 192.168.1.151, 70] # type, ip, gh/s
61
- - [miner, 192.168.1.152, 70]
64
+ - [btcguild, stratum.btcguild.com, apikey] # ping url, your API key
65
+ - [miner, 192.168.1.151, 70, jbond_151] # type, ip, gh/s, pool_name(optional)
66
+ - [miner, 192.168.1.152, 82, jbond_15]
67
+ - [miner, 192.168.1.153, 40]
62
68
  - [internet, www.google.com, www.speedtest.net]
63
69
  - [eloipool, 192.168.1.13, 4] # frequency of old block updates (once per X polls)
64
70
 
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # Script to reboot Avalon miner
3
+
4
+ lib = File.expand_path('../../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require 'avalon'
8
+
9
+ Avalon::Config.load 'prod'
10
+
11
+ monitor = Avalon::Monitor.new Avalon::Config[:monitor]
12
+
13
+ monitor.switches.each do |sw|
14
+ sw.off
15
+ sleep 2
16
+ sw.on
17
+ end
18
+
19
+ exit
20
+
21
+ ARGV.each do |id|
22
+ miner = monitor.nodes.find {|m| m.num == id}
23
+ miner ||= monitor.nodes.find {|m| m.ip =~ Regexp.new(id)}
24
+ if miner
25
+ miner.reset
26
+ puts "Miner #{id} reset"
27
+ else
28
+ puts "Unable to reset miner #{id}"
29
+ end
30
+ end
@@ -10,7 +10,9 @@ require "avalon/config"
10
10
  require "avalon/extractable"
11
11
 
12
12
  require "avalon/block"
13
+ require "avalon/switch"
13
14
  require "avalon/node"
15
+ require "avalon/btcguild"
14
16
  require "avalon/miner"
15
17
  require "avalon/internet"
16
18
  require "avalon/eloipool"
@@ -0,0 +1,93 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module Avalon
5
+
6
+ # Extracts Btcguild pool info
7
+ class Btcguild < Node
8
+
9
+ API_URL = 'http://www.btcguild.com'
10
+ API_PATH = 'api.php?api_key='
11
+
12
+ def initialize monitor, ping_url, api_key
13
+ @ping_url, @api_key = ping_url, api_key
14
+
15
+ @conn ||= Faraday.new(:url => API_URL) do |faraday|
16
+ # faraday.response :logger # log requests to STDOUT
17
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
18
+ end
19
+
20
+ super()
21
+ end
22
+
23
+ def get
24
+ reply = @conn.get "#{API_PATH}#{@api_key}"
25
+ if reply.success? && !(reply.body =~ /too many API requests/)
26
+ JSON.parse(reply.body, :symbolize_names => true)
27
+ else
28
+ {}
29
+ end
30
+ end
31
+
32
+ def poll verbose=true
33
+ @data[:ping] = ping @ping_url
34
+
35
+ if @data[:ping]
36
+ @data.merge!(get || {})
37
+ if @data[:workers] && @data[:workers].keys.include?(:'1')
38
+ @data[:workers] = Hash[*@data[:workers].map {|_, h| [h.delete(:worker_name), h]}.flatten]
39
+ end
40
+ end
41
+ puts "#{self}" if verbose
42
+ end
43
+
44
+ # Check for any exceptional situations, sound alarm if any
45
+ def report
46
+ if self[:ping].nil?
47
+ alarm "BTC Guild not responding to ping"
48
+ else
49
+ end
50
+ end
51
+
52
+ def to_s
53
+ "\nBTC Guild: #{pool_speed}TH/s ping:#{self[:ping]} diff:#{diff}M " +
54
+ "unpaid(24h) btc: #{unpaid}(#{past24}) nmc: #{unpaid_nmc}(#{past24_nmc})"
55
+ end
56
+
57
+ ### Convenience data accessors
58
+ def access key1, key2, precision=nil, divider=1
59
+ if @data[key1] && @data[key1][key2]
60
+ if precision
61
+ (@data[key1][key2]/divider).round(precision)
62
+ else
63
+ (@data[key1][key2]/divider)
64
+ end
65
+ end
66
+ end
67
+
68
+ def past24
69
+ access :user, :past_24h_rewards, 2
70
+ end
71
+
72
+ def unpaid
73
+ access :user, :unpaid_rewards, 3
74
+ end
75
+
76
+ def past24_nmc
77
+ access :user, :past_24h_rewards_nmc, 1
78
+ end
79
+
80
+ def unpaid_nmc
81
+ access :user, :unpaid_rewards_nmc, 2
82
+ end
83
+
84
+ def pool_speed
85
+ access :pool, :pool_speed, 1, 1000.0
86
+ end
87
+
88
+ def diff
89
+ access :pool, :difficulty, 1, 1000_000.0
90
+ end
91
+
92
+ end
93
+ end
@@ -2,9 +2,8 @@ module Avalon
2
2
  # Pool is a node encapsulating pool software
3
3
  class Eloipool < Node
4
4
 
5
- def initialize ip, frequency
6
- @ip = ip
7
- @update_frequency = frequency
5
+ def initialize monitor, ip, frequency
6
+ @ip, @frequency = ip, frequency
8
7
  @update_num = 0
9
8
  @block_file = Avalon::Config[:block_file]
10
9
  @blocks = load_blocks || {}
@@ -55,7 +54,7 @@ module Avalon
55
54
  end
56
55
 
57
56
  def update_old_block
58
- if rand(@update_frequency) == 0 # update once per @frequency polls
57
+ if rand(@frequency) == 0 # update once per @frequency polls
59
58
  hash = @blocks.keys[@update_num]
60
59
  if @blocks[hash]
61
60
  @update_num += 1
@@ -5,7 +5,8 @@ module Avalon
5
5
 
6
6
  # type = :absolute_time | :absolute_date | :relative_time
7
7
  def my_time t, type=:absolute_time
8
- time = Time.at(t.to_f)
8
+ t = t.to_f < 0 ? 0 : t.to_f
9
+ time = Time.at(t)
9
10
  case type
10
11
  when :absolute_date
11
12
  time.getlocal.strftime("%Y-%m-%d %H:%M:%S")
@@ -3,8 +3,14 @@ module Avalon
3
3
  # Internet is a node encapsulating information about Internet connectivity
4
4
  class Internet < Node
5
5
 
6
- def initialize *sites
7
- @sites = Hash[ *sites.map {|site| [site.split(/\./)[-2].to_sym, site]}.flatten ]
6
+ IP_REGEXP = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/
7
+
8
+ def initialize monitor, *sites
9
+ sites.map! do |site|
10
+ name = site =~ IP_REGEXP ? site : site.split(/\./)[-2]
11
+ [name.to_sym, site]
12
+ end
13
+ @sites = Hash[ *sites.flatten ]
8
14
  super()
9
15
  end
10
16
 
@@ -15,8 +15,10 @@ module Avalon
15
15
 
16
16
  # Field formats: name => [width, pattern, type/conversion]
17
17
  FIELDS = {
18
- :ping => [8, /./, nil], # not in miner status string...
19
- :mhs => [6, /(?<=MHS av=)[\d\.]*/, :i],
18
+ :unit => [6, /(?<=MHS av=)[\d\.]*/, :i],
19
+ :pool => [6, /./, nil], # not in miner status string...
20
+ :ping => [6, /./, nil], # not in miner status string...
21
+ :rst => [3, /./, nil], # not in miner status string...
20
22
  :uptime => [9, /(?<=Elapsed=)[\d\.]*/, ->(x){ my_time(x, :relative_time)}],
21
23
  :last => [8, /(?<=Status=Alive,).*?Last Share Time=[\d\.]*/,
22
24
  ->(x){ convert_last(x)}],
@@ -25,35 +27,36 @@ module Avalon
25
27
  :'°C' => [2, /(?<=Temperature=)[\d\.]*/, :i],
26
28
  :fan2 => [4, /(?<=fan2=)[\d\.]*/, :i],
27
29
  :fan3 => [4, /(?<=fan3=)[\d\.]*/, :i],
28
- :wu => [4, /(?<=,Work Utility=)[\d\.]*/, :i],
29
- :getworks => [8, /(?<=Getworks=)[\d\.]*/, :i],
30
- :accepted => [8, /(?<=,Accepted=)[\d\.]*/, :i],
31
- :rejected => [8, /(?<=Rejected=)[\d\.]*/, :i],
32
- :stale => [6, /(?<=Stale=)[\d\.]*/, :i],
33
- :errors => [6, /(?<=Hardware Errors=)[\d\.]*/, :i],
34
- :blocks => [6, /(?<=Network Blocks=)[\d\.]*/, :i],
30
+ :WU => [4, /(?<=,Work Utility=)[\d\.]*/, :i],
31
+ :getwork => [7, /(?<=Getworks=)[\d\.]*/, :i],
32
+ :accept => [6, /(?<=,Accepted=)[\d\.]*/, :i],
33
+ :reject => [6, /(?<=Rejected=)[\d\.]*/, :i],
34
+ :stale => [5, /(?<=Stale=)[\d\.]*/, :i],
35
+ :error => [6, /(?<=Hardware Errors=)[\d\.]*/, :i],
36
+ # :block => [5, /(?<=Network Blocks=)[\d\.]*/, :i],
35
37
  # :found => [2, /(?<=Found Blocks=)[\d\.]*/, :i],
36
38
  }
37
39
 
38
40
  # Last share converter (Miner-specific)
39
41
  def self.convert_last x
40
42
  y = x[/(?<=Last Share Time=)[\d\.]*/]
43
+
41
44
  if y.nil? || y == '0'
42
45
  "never"
43
46
  else
44
- my_time(Time.now.getgm-y.to_i, :relative_time)
47
+ my_time(Time.now.getgm.to_i-y.to_i, :relative_time)
45
48
  end
46
49
  end
47
50
 
48
51
  def self.print_headers
49
- puts "\nMiner status as of #{Time.now.getlocal.asctime}:\n# " +
50
- FIELDS.map {|name, (width,_,_ )| name.to_s.ljust(width)}.join(' ')
52
+ puts "\nMiner status as of #{Time.now.getlocal.asctime}:\nmhs: " +
53
+ FIELDS.map {|name, (width,_,_ )| name.to_s.rjust(width)}.join(' ')
51
54
  end
52
55
 
53
- def initialize ip, min_speed, config=Avalon::Config.config
54
- @ip = ip
55
- @min_speed = min_speed * 1000 # Gh/s to Mh/s
56
- @config = config
56
+ def initialize monitor, ip, min_mhs, worker_name=nil
57
+ @ip, @min_mhs, @worker_name = ip, min_mhs*1000 , worker_name
58
+ @monitor = monitor
59
+ @config = Avalon::Config.config # TODO: monitor.config?
57
60
  @fails = 0
58
61
  super()
59
62
  end
@@ -66,20 +69,24 @@ module Avalon
66
69
  self[:ping] = ping @ip
67
70
 
68
71
  status = get_api('summary') + get_api('pools') + get_api('devs') + get_api('stats')
72
+ @poll_time = Time.now
69
73
  # p get_api('summary')
70
- # pools = get_api('pools')
71
- # p pools[FIELDS[:last][1]]
72
- # devs = get_api('devs')
73
- # p devs
74
74
 
75
75
  data = self.class.extract_data_from(status)
76
76
 
77
77
  if data.empty?
78
- @data = {}
78
+ @data = {:ping => self[:ping], :rst => self[:rst]}
79
79
  else
80
80
  @data.merge! data
81
+ if @config[:monitor][:per_hour]
82
+ [:getwork, :accept, :reject, :stale, :error].each do |key|
83
+ self[key] = (self[key]/upminutes*60).round(1) if self[key]
84
+ end
85
+ end
81
86
  end
82
87
 
88
+ self[:pool] = pool_hash
89
+
83
90
  puts "#{self}" if verbose
84
91
  end
85
92
 
@@ -87,26 +94,49 @@ module Avalon
87
94
  duration(self[:uptime])
88
95
  end
89
96
 
97
+ def last
98
+ duration(self[:last])
99
+ end
100
+
101
+ def restart_time
102
+ @poll_time - upminutes * 60.0
103
+ end
104
+
90
105
  def temp
91
106
  self[:'°C']
92
107
  end
93
108
 
109
+ def unit_hash
110
+ self[:unit] || 0
111
+ end
112
+
113
+ def pool_hash
114
+ if @monitor.pool && @worker_name && @monitor.pool[:workers] && @monitor.pool[:workers][@worker_name]
115
+ @monitor.pool[:workers][@worker_name][:hash_rate].round(0)
116
+ end
117
+ end
118
+
94
119
  # Check for any exceptional situations in stats, sound alarm if any
95
120
  def report
96
- if data[:ping].nil?
121
+ if data[:ping].nil? || data[:unit].nil?
97
122
  @fails += 1
98
123
  if @fails >= @config[:alert_after]
99
124
  alarm "Miner #{num} did not respond to status query", :failure
100
125
  end
101
126
  else
102
127
  @fails = 0
103
- if duration(self[:uptime]) < 2
128
+ @last_restart ||= restart_time
129
+
130
+ # Detect Miner reset correctly
131
+ if (restart_time - @last_restart) > 20
132
+ @last_restart = restart_time
133
+ self[:rst] = (self[:rst] || 0) + 1
104
134
  alarm "Miner #{num} restarted", :restart
105
- elsif duration(self[:uptime]) > 5 # Miner settled down
106
- if self[:mhs] < @min_speed
107
- alarm "Miner #{num} performance is #{self[:mhs]}, should be #{@min_speed}", :perf_low
108
- elsif self[:last] == 'never' || duration(self[:last]) > @config[:alert_last_share]
109
- alarm "Miner #{num} last shares was #{duration(self[:last])} min ago", :last_share
135
+ elsif upminutes > 5 # Miner settled down
136
+ if unit_hash < @min_mhs
137
+ alarm "Miner #{num} performance is #{unit_hash}, should be #{@min_mhs}", :perf_low
138
+ elsif last == 'never' || last > @config[:alert_last_share]
139
+ alarm "Miner #{num} last shares was #{last} min ago", :last_share
110
140
  elsif temp >= @config[:alert_temp_high]
111
141
  alarm "Miner #{num} too hot at #{temp}°C, needs cooling", :temp_high
112
142
  elsif self[:freq] && temp <= @config[:alert_temp_low]
@@ -122,7 +152,7 @@ module Avalon
122
152
  end
123
153
 
124
154
  def to_s
125
- "#{num}: " + FIELDS.map {|key, (width, _, _ )| @data[key].to_s.ljust(width)}.join(" ")
155
+ "#{num}: " + FIELDS.map {|key, (width, _, _ )| @data[key].to_s.rjust(width)}.join(" ")
126
156
  end
127
157
 
128
158
  end
@@ -2,29 +2,38 @@ module Avalon
2
2
 
3
3
  class Monitor
4
4
 
5
- attr_reader :nodes
5
+ attr_reader :nodes, :switches, :pool
6
6
 
7
7
  # List of nodes to monitor
8
8
  def initialize opts
9
- @nodes = opts[:nodes].map {|args| Avalon::Node.create(*args)}
10
9
  @timeout = opts[:timeout] || 30
11
10
  @verbose = opts[:verbose]
11
+ @switches = (opts[:switches] || []).map {|args| Avalon::Switch.new(*args)}
12
+ @nodes = opts[:nodes].map {|args| Avalon::Node.create(self, *args)}
13
+ @pool = @nodes.find {|node| node.is_a?(Avalon::Btcguild)}
12
14
  end
13
15
 
14
16
  def run
15
17
  loop do
16
18
 
17
- Avalon::Miner.print_headers if @verbose
18
-
19
19
  # Check status for all nodes
20
- @nodes.each {|node| node.poll(@verbose)}
20
+ @nodes.inject(false) do |headers_printed, node|
21
+ # Print miners headers once first miner encountered
22
+ if @verbose && node.is_a?(Avalon::Miner) && !headers_printed
23
+ Avalon::Miner.print_headers
24
+ headers_printed = true
25
+ end
26
+ node.poll(@verbose)
27
+ headers_printed
28
+ end
21
29
 
22
30
  # Report node errors (if any)
23
31
  @nodes.each {|node| node.report}
24
32
 
25
33
  if @verbose
26
- total_hash = @nodes.reduce(0) {|hash, node| hash + (node[:mhs] || 0)}
27
- puts "Total hash rate: #{total_hash} MHash/sec"
34
+ unit_hash = @nodes.reduce(0) {|hash, node| hash + (node.unit_hash || 0)}
35
+ pool_hash = @nodes.reduce(0) {|hash, node| hash + (node.pool_hash || 0)}
36
+ puts "Total hash rate (from pool): #{unit_hash} (#{pool_hash}) MH/s"
28
37
  end
29
38
 
30
39
  sleep @timeout
@@ -6,9 +6,9 @@ module Avalon
6
6
  include Utils # Helper methods
7
7
 
8
8
  # Builder method for creating Node subclasses from config arrays
9
- def Node.create *args
9
+ def Node.create monitor, *args
10
10
  subclass = Avalon.const_get(args.first.capitalize)
11
- subclass.new *args.drop(1)
11
+ subclass.new monitor, *args.drop(1)
12
12
  end
13
13
 
14
14
  attr_reader :ip, :data
@@ -31,6 +31,14 @@ module Avalon
31
31
  @data[key] = value
32
32
  end
33
33
 
34
+ ### Node API: following methods should be defined:
35
+
36
+ def unit_hash
37
+ end
38
+
39
+ def pool_hash
40
+ end
41
+
34
42
  # Abstract: Check node status
35
43
  # If verbose, the Node should print out its state after the status update
36
44
  def poll verbose
@@ -0,0 +1,20 @@
1
+ module Avalon
2
+
3
+ # Encapsulates DLI Web Power Switch
4
+ class Switch
5
+
6
+ def initialize user, pass, ip, outlet
7
+ @user, @pass, @ip, @outlet = user, pass, ip, outlet
8
+ raise 'Please install curl: sudo apt-get install curl' if `which curl`.empty?
9
+ end
10
+
11
+ def on
12
+ `curl -s http://#{@user}:#{@pass}@#{@ip}/outlet?#{@outlet}=ON`
13
+ end
14
+
15
+ def off
16
+ `curl -s http://#{@user}:#{@pass}@#{@ip}/outlet?#{@outlet}=OFF`
17
+ end
18
+
19
+ end
20
+ end
@@ -38,7 +38,7 @@ module Avalon
38
38
  'never'
39
39
  else
40
40
  hour, min, sec = *time_string.split(/:/).map(&:to_i)
41
- hour*60.0 + min + sec/60.0
41
+ (hour*60.0 + min + sec/60.0).round(2)
42
42
  end
43
43
  end
44
44
 
@@ -46,7 +46,7 @@ module Avalon
46
46
  def ping ip
47
47
  ping_result = `ping -c 1 #{ip}`
48
48
  if ping_result =~ /( | 0.)0% packet loss/
49
- ping_result.match(/time=([\.\d]*) ms/)[1].to_f
49
+ ping_result.match(/time=([\.\d]*) ms/)[1].to_f.round(1)
50
50
  end
51
51
  end
52
52
 
@@ -1,3 +1,3 @@
1
1
  module Avalon
2
- VERSION = "0.0.23"
2
+ VERSION = "0.0.25"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arvicco-avalon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.23
4
+ version: 0.0.25
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-26 00:00:00.000000000 Z
12
+ date: 2013-07-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -34,6 +34,7 @@ executables:
34
34
  - monitor
35
35
  - mtgox_tx
36
36
  - reboot_miner
37
+ - test_switch
37
38
  extensions: []
38
39
  extra_rdoc_files: []
39
40
  files:
@@ -46,10 +47,12 @@ files:
46
47
  - bin/monitor
47
48
  - bin/mtgox_tx
48
49
  - bin/reboot_miner
50
+ - bin/test_switch
49
51
  - lib/avalon.rb
50
52
  - lib/avalon/bitcoind.rb
51
53
  - lib/avalon/block.rb
52
54
  - lib/avalon/blockchain.rb
55
+ - lib/avalon/btcguild.rb
53
56
  - lib/avalon/config.rb
54
57
  - lib/avalon/eloipool.rb
55
58
  - lib/avalon/extractable.rb
@@ -57,6 +60,7 @@ files:
57
60
  - lib/avalon/miner.rb
58
61
  - lib/avalon/monitor.rb
59
62
  - lib/avalon/node.rb
63
+ - lib/avalon/switch.rb
60
64
  - lib/avalon/utils.rb
61
65
  - lib/avalon/version.rb
62
66
  - sound/Dog.aiff