arvicco-avalon 0.0.23 → 0.0.25

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/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