synaccess_connect 0.2.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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZmRjYmQ0NmU2ZTE2Y2IwZTZiOGNjZGUwYTgyM2M5ZTk2MTIxNGI1OQ==
5
+ data.tar.gz: !binary |-
6
+ YTkyZGExYmZiMjA3ZDcxMTRlZjk4NjA0YzRlMTU5NThlYTU2YjUwNg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MmU2NWFhMGVjMDg2MTdkMWZiMTA0MThlZmY2MTU4OGE1OGMzNGY0Y2ZmZjQ0
10
+ NzRhMGMzZTExNGRjYjZmMjBhZDdiNGZkNDA0MDEyZWU5OWRjYzI0NGVhNDQ0
11
+ MjM1YWJlNWYwODQxZTUyZDVjNTNiZWM4ZjZkNjMzMjBhZDI2ZGE=
12
+ data.tar.gz: !binary |-
13
+ ODg5NGEwNjdjYTIyMzU2YzgwNjQwNmY1NTVmZDdlOGMyZDk4MTAyODcwOTE2
14
+ ZDcyOTQzZjE0MWIxY2YxN2VjMzgzYTgzYzJhYmZmNzhmY2E0YWMwNzIxOGU5
15
+ MjJkNDE4OGM2OWZmMzJiNDQ0YWNkMzNiN2VjNGU5MGRkYWNjNjg=
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Synaccess Connect
2
+
3
+ Communication wrappers for Synaccess netBooter power relays.
4
+
5
+ ## Supported Firmware:
6
+ * NP-02
7
+ * NP-02B
8
+
9
+ ## Available interfaces
10
+ ```
11
+ NetBooter
12
+ Http
13
+ RevA
14
+ RevB
15
+ Telnet
16
+ RevA
17
+ RevB
18
+ ```
19
+
20
+ ## New connections
21
+
22
+ `connection = NetBooter::PROTOCOL::RevX.new('XXX.XXX.XXX.XXX', options)`
23
+
24
+ Options:
25
+
26
+ * username
27
+ * password
28
+
29
+ ## Interface
30
+
31
+ All classes should implement the following methods:
32
+
33
+ * `initialize(host, options)`
34
+ * `status(outlet) => boolean`
35
+ * `statuses => Hash of { outlet => boolean }`
36
+ * `toggle(new_status, outlet) => new status boolean`
37
+
38
+
39
+
@@ -0,0 +1,4 @@
1
+ module NetBooter
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,128 @@
1
+ require "net/http"
2
+ require 'nokogiri'
3
+
4
+ class NetBooter::HttpConnection
5
+ def initialize(host, options = {})
6
+ @host = host
7
+ @options = {
8
+ :port => 80
9
+ }.merge(options)
10
+ end
11
+
12
+ # Get the status of an outlet
13
+ #
14
+ # Example:
15
+ # >> netbooter.status(2)
16
+ # => false
17
+ #
18
+ # Arguments:
19
+ # +outlet+ Outlet number you want to check (1-based)
20
+ #
21
+ # Returns:
22
+ # boolean - on/off status of the outlet
23
+ def status(outlet = 1)
24
+ statuses.fetch outlet do
25
+ raise NetBooter::Error.new('Error communicating with relay')
26
+ end
27
+ end
28
+
29
+ # Turn on the specified outlet
30
+ # Example:
31
+ # >> netbooter.toggle_on(2)
32
+ # => true
33
+ #
34
+ # Arguments:
35
+ # +outlet+ Outlet you want to turn on (1-based)
36
+ #
37
+ # Returns:
38
+ # boolean - The new status of the outlet (should be true)
39
+ def toggle_on(outlet = 1)
40
+ toggle(outlet, true)
41
+ end
42
+
43
+ # Turn off the specified outlet
44
+ # Example:
45
+ # >> netbooter.toggle_off(2)
46
+ # => true
47
+ #
48
+ # Arguments:
49
+ # +outlet+ Outlet you want to turn off (1-based)
50
+ #
51
+ # Returns:
52
+ # boolean - The new status of the outlet (should be false)
53
+ def toggle_off(outlet = 1)
54
+ toggle(outlet, false)
55
+ end
56
+
57
+ # Toggle the status of an outlet
58
+ #
59
+ # Example:
60
+ # >> netbooter.toggle(1, false)
61
+ # => false
62
+ #
63
+ # Arguments:
64
+ # +outlet+ The outlet you want to toggle
65
+ # +status+ Boolean. true to turn on, false to turn off
66
+ #
67
+ # Returns:
68
+ # boolean - The new status of the outlet
69
+ def toggle(outlet, status)
70
+ current_status = status(outlet)
71
+ toggle_relay(outlet) if current_status != status
72
+ status
73
+ end
74
+
75
+ # Get the status of all outlets on the device. Needs to be defined
76
+ # in a subclass.
77
+ #
78
+ # Example:
79
+ # >> netbooter.statuses
80
+ # => { 1 => true, 2 => false }
81
+ #
82
+ # Arguments: none
83
+ #
84
+ # Returns:
85
+ # A hash containing the outlet numbers and their respective status
86
+ def statuses
87
+ raise NotImplementedError.new
88
+ end
89
+
90
+ # The method that contains the network command the relay. Needs to be defined
91
+ # in a subclass.
92
+ def toggle_relay(outlet)
93
+ raise NotImplementedError.new('Must implement toggle_relay in subclass')
94
+ end
95
+
96
+ private
97
+
98
+ # Make an http request and return the result.
99
+ def get_request(path)
100
+ resp = nil
101
+ begin
102
+ Timeout::timeout(5) do
103
+ resp = do_http_request(path)
104
+ end
105
+ rescue => e
106
+ raise NetBooter::Error.new("Error connecting to relay: #{e.message}")
107
+ end
108
+ resp
109
+ end
110
+
111
+ def do_http_request(path)
112
+ resp = nil
113
+ Net::HTTP.start(@host) do |http|
114
+ req = Net::HTTP::Get.new(path)
115
+
116
+ req.basic_auth @options[:username], @options[:password] if @options[:username] && @options[:password]
117
+
118
+ resp = http.request(req)
119
+
120
+ # Error checking. Allow 200s and 302s
121
+ unless ['200', '302'].include? resp.code
122
+ raise NetBooter::Error.new "Relay responded with #{resp.code}. Perhaps you have the wrong relay type specified."
123
+ end
124
+ end
125
+ resp
126
+ end
127
+
128
+ end
@@ -0,0 +1,24 @@
1
+ class NetBooter::Http::RevA < NetBooter::HttpConnection
2
+ # So here's the deal... When you hit the switch page (pwrSw1.cgi) nothing happens.
3
+ # It's only when hitting the status page AFTER visiting a swith page that
4
+ # the relay is toggled.
5
+ def toggle_relay(outlet)
6
+ get_request("/pwrSw#{outlet}.cgi")
7
+ get_request("/synOpStatus.shtml")
8
+ end
9
+
10
+ def statuses
11
+ resp = get_request('/synOpStatus.shtml')
12
+
13
+ doc = Nokogiri::HTML(resp.body)
14
+ nodes = doc.xpath('//img')
15
+ status = {}
16
+
17
+ nodes.each do |node|
18
+ if node.values[0].match(/^led(off|on).gif$/)
19
+ status[node.parent.parent.xpath('th').first.content.to_i] = $1 == 'on' ? true : false
20
+ end
21
+ end
22
+ status
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ class NetBooter::Http::RevB < NetBooter::HttpConnection
2
+ def toggle_relay(outlet)
3
+ get_request("/cmd.cgi?rly=#{outlet - 1}")
4
+ end
5
+
6
+ def statuses
7
+ resp = get_request('/status.xml')
8
+
9
+ doc = Nokogiri::XML(resp.body)
10
+ nodes = doc.xpath('/response/*')
11
+
12
+ status = {}
13
+ nodes.each do |node|
14
+ if node.name.match(/^rly(\d+)$/)
15
+ status[$1.to_i + 1] = node.content == '1' ? true : false
16
+ end
17
+ end
18
+ status
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ class NetBooter::Telnet::RevA < NetBooter::TelnetConnection
2
+
3
+ def override_options
4
+ {
5
+ :prompt => /^\r>$/n,
6
+ :status_string => 'On'
7
+ }
8
+ end
9
+
10
+ def local_statuses
11
+ parse_status @connection.cmd("pshow")
12
+ end
13
+
14
+ private
15
+ def authenticate
16
+ sleep 0.1
17
+ @connection.puts(@options[:username])
18
+ sleep 0.1
19
+ @connection.puts(@options[:password])
20
+ @connection.cmd('')
21
+ sleep 0.1
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ class NetBooter::Telnet::RevB < NetBooter::TelnetConnection
2
+ def local_statuses
3
+ @connection.cmd("")
4
+ sleep 0.25
5
+ parse_status @connection.cmd("pshow")
6
+ end
7
+
8
+ private
9
+
10
+ def authenticate
11
+ @connection.cmd('login')
12
+ sleep 0.25
13
+ @connection.puts(@options[:username])
14
+ sleep 0.25
15
+ @connection.puts(@options[:password])
16
+ end
17
+ end
@@ -0,0 +1,119 @@
1
+ require 'net/telnet'
2
+
3
+ class NetBooter::TelnetConnection
4
+ def initialize(host, options = {})
5
+ @host = host
6
+ @options = { :port => 23,
7
+ :binmode => false,
8
+ :prompt => /^[>]/n,
9
+ :timeout => 1,
10
+ :status_string => 'ON'
11
+ }.merge(override_options).merge(options)
12
+ end
13
+
14
+ def override_options
15
+ {}
16
+ end
17
+
18
+ def status(outlet = 1)
19
+ statuses.fetch outlet do
20
+ raise NetBooter::Error.new('Error communicating with relay')
21
+ end
22
+ end
23
+
24
+ def statuses
25
+ with_connection :default => {} do
26
+ local_statuses
27
+ end
28
+ end
29
+
30
+ def toggle_on(outlet = 1)
31
+ toggle(true, outlet)
32
+ end
33
+
34
+ def toggle_off(outlet = 1)
35
+ toggle(false, outlet)
36
+ end
37
+
38
+ def toggle(outlet, status)
39
+ status_string = status ? '1' : '0'
40
+ with_connection do
41
+ @connection.cmd("pset #{outlet} #{status_string}")
42
+ end
43
+ status
44
+ end
45
+
46
+ private
47
+
48
+ def authenticate
49
+ raise NotImplementedException.new('Must implement in subclass')
50
+ end
51
+
52
+ # Parse the text that we get back from pshow
53
+ # returns hash of { channel => boolean_status }
54
+ def parse_status(response)
55
+ statuses = {}
56
+ return statuses unless response
57
+
58
+ response.split("\n").each do |line|
59
+ parts = line.split('|').map &:strip
60
+ next unless parts[0] =~ /^\d+/
61
+ statuses[parts[0].to_i] = parts[2] == @options[:status_string]
62
+ end
63
+ statuses
64
+ end
65
+
66
+ def connect
67
+ retry_block 5 do
68
+ @connection = Net::Telnet::new(
69
+ 'Host' => @host,
70
+ 'Port' => @options[:port],
71
+ 'Binmode' => @options[:binmode],
72
+ 'Prompt' => @options[:prompt],
73
+ 'Timeout' => @options[:timeout]
74
+ )
75
+ authenticate if @options[:username] && @options[:password]
76
+ end
77
+ end
78
+
79
+ def disconnect
80
+ begin
81
+ @connection.close if @connection
82
+ rescue
83
+ # do nothing
84
+ end
85
+ # needs a little bit of delay after disconnecting
86
+ # if so the next connection doesn't get refused while
87
+ # this one is closing. Number arrived at by experimentation
88
+ sleep 0.02
89
+ end
90
+
91
+ def with_connection options = {}
92
+ begin
93
+ connect
94
+ raise NetBooter::Error.new('Unable to connect') if @connection.nil?
95
+ output = yield
96
+ rescue => e
97
+ puts "ERROR! #{e.message}"
98
+ # puts e.backtrace.join("\n")
99
+ output = options[:default] if options[:default]
100
+ ensure
101
+ disconnect
102
+ end
103
+
104
+ output
105
+ end
106
+
107
+ def retry_block max_attempts, options = {}
108
+ max_attempts.times do |i|
109
+ begin
110
+ yield
111
+ break # if we made it through the yield without an error we can break
112
+ rescue => e
113
+ puts e.message
114
+ raise e if i + 1 >= max_attempts
115
+ end
116
+ end
117
+ end
118
+
119
+ end
@@ -0,0 +1,34 @@
1
+ module NetBooter
2
+ module Telnet
3
+ end
4
+ module Http
5
+ end
6
+ end
7
+
8
+ require 'synaccess_connect/net_booter/error'
9
+ require 'synaccess_connect/net_booter/telnet/telnet_connection'
10
+ require 'synaccess_connect/net_booter/telnet/rev_a'
11
+ require 'synaccess_connect/net_booter/telnet/rev_b'
12
+
13
+ require 'synaccess_connect/net_booter/http/http_connection'
14
+ require 'synaccess_connect/net_booter/http/rev_a'
15
+ require 'synaccess_connect/net_booter/http/rev_b'
16
+
17
+
18
+
19
+ # if defined?(Rails)
20
+ # module SynaccessConnect
21
+ # class Railtie < Rails::Railtie
22
+ # # initializer "carrierwave.setup_paths" do
23
+ # # CarrierWave.root = Rails.root.join(Rails.public_path).to_s
24
+ # # CarrierWave.base_path = ENV['RAILS_RELATIVE_URL_ROOT']
25
+ # # end
26
+
27
+ # # initializer "carrierwave.active_record" do
28
+ # # ActiveSupport.on_load :active_record do
29
+ # # require 'carrierwave/orm/activerecord'
30
+ # # end
31
+ # # end
32
+ # end
33
+ # end
34
+ # end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: synaccess_connect
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Jason Hanggi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: vcr
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - <
60
+ - !ruby/object:Gem::Version
61
+ version: '1.16'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - <
67
+ - !ruby/object:Gem::Version
68
+ version: '1.16'
69
+ description: Ruby interface to connecting to Synaccess netBooter power relays
70
+ email:
71
+ - jason@tablexi.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - README.md
77
+ - lib/synaccess_connect.rb
78
+ - lib/synaccess_connect/net_booter/error.rb
79
+ - lib/synaccess_connect/net_booter/http/http_connection.rb
80
+ - lib/synaccess_connect/net_booter/http/rev_a.rb
81
+ - lib/synaccess_connect/net_booter/http/rev_b.rb
82
+ - lib/synaccess_connect/net_booter/telnet/rev_a.rb
83
+ - lib/synaccess_connect/net_booter/telnet/rev_b.rb
84
+ - lib/synaccess_connect/net_booter/telnet/telnet_connection.rb
85
+ homepage: https://github.com/tablexi/synaccess
86
+ licenses: []
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.2.2
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: ''
108
+ test_files: []