brocadesan 0.4.0
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 +113 -0
- data/Rakefile +46 -0
- data/brocadesan.gemspec +14 -0
- data/lib/brocadesan.rb +8 -0
- data/lib/brocadesan/alias.rb +64 -0
- data/lib/brocadesan/config/brocade/san/switch_cmd_mapping.yml +116 -0
- data/lib/brocadesan/config/parser_mapping.yml +21 -0
- data/lib/brocadesan/device.rb +296 -0
- data/lib/brocadesan/monkey/string.rb +11 -0
- data/lib/brocadesan/provisioning.rb +894 -0
- data/lib/brocadesan/switch.rb +882 -0
- data/lib/brocadesan/wwn.rb +63 -0
- data/lib/brocadesan/zone.rb +60 -0
- data/lib/brocadesan/zone_configuration.rb +38 -0
- data/lib/meta_methods.rb +263 -0
- data/test/alias_test.rb +68 -0
- data/test/device_test.rb +203 -0
- data/test/output_helpers.rb +308 -0
- data/test/outputs/agshow_1.txt +7 -0
- data/test/outputs/agshow_1.yml +31 -0
- data/test/outputs/agshow_2.txt +4 -0
- data/test/outputs/agshow_2.yml +3 -0
- data/test/outputs/apt_policy_1.txt +6 -0
- data/test/outputs/apt_policy_1.yml +3 -0
- data/test/outputs/cfgshow_1.txt +5 -0
- data/test/outputs/cfgshow_1.yml +7 -0
- data/test/outputs/cfgshow_2.txt +31 -0
- data/test/outputs/cfgshow_2.yml +32 -0
- data/test/outputs/cfgshow_3.txt +9 -0
- data/test/outputs/cfgshow_3.yml +12 -0
- data/test/outputs/cfgtransshow_1.txt +2 -0
- data/test/outputs/cfgtransshow_1.yml +5 -0
- data/test/outputs/cfgtransshow_2.txt +3 -0
- data/test/outputs/cfgtransshow_2.yml +4 -0
- data/test/outputs/cfgtransshow_3.txt +3 -0
- data/test/outputs/cfgtransshow_3.yml +4 -0
- data/test/outputs/chassisname_1.txt +2 -0
- data/test/outputs/chassisname_1.yml +3 -0
- data/test/outputs/dlsshow_1.txt +4 -0
- data/test/outputs/dlsshow_1.yml +4 -0
- data/test/outputs/dlsshow_2.txt +4 -0
- data/test/outputs/dlsshow_2.yml +4 -0
- data/test/outputs/fabricshow_1.txt +10 -0
- data/test/outputs/fabricshow_1.yml +34 -0
- data/test/outputs/iodshow_1.txt +4 -0
- data/test/outputs/iodshow_1.yml +4 -0
- data/test/outputs/islshow_1.txt +6 -0
- data/test/outputs/islshow_1.yml +62 -0
- data/test/outputs/islshow_2.txt +2 -0
- data/test/outputs/islshow_2.yml +2 -0
- data/test/outputs/lscfg_show_1.txt +71 -0
- data/test/outputs/lscfg_show_1.yml +5 -0
- data/test/outputs/ns_1.txt +80 -0
- data/test/outputs/ns_1.yml +39 -0
- data/test/outputs/ns_2.txt +37 -0
- data/test/outputs/ns_2.yml +21 -0
- data/test/outputs/putty.log +1867 -0
- data/test/outputs/switch_1.txt +25 -0
- data/test/outputs/switch_1.yml +73 -0
- data/test/outputs/switch_2.txt +18 -0
- data/test/outputs/switch_2.yml +42 -0
- data/test/outputs/switch_3.txt +14 -0
- data/test/outputs/switch_3.yml +46 -0
- data/test/outputs/switchstatusshow_1.txt +21 -0
- data/test/outputs/switchstatusshow_1.yml +19 -0
- data/test/outputs/trunkshow_1.txt +8 -0
- data/test/outputs/trunkshow_1.yml +44 -0
- data/test/outputs/trunkshow_2.txt +2 -0
- data/test/outputs/trunkshow_2.yml +2 -0
- data/test/outputs/version_1.txt +6 -0
- data/test/outputs/version_1.yml +8 -0
- data/test/outputs/vf_switch_1.txt +25 -0
- data/test/outputs/vf_switch_1.yml +73 -0
- data/test/provisioning_test.rb +1043 -0
- data/test/switch_test.rb +476 -0
- data/test/wwn_test.rb +41 -0
- data/test/zone_configuration_test.rb +65 -0
- data/test/zone_test.rb +73 -0
- metadata +170 -0
data/README
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
= BrocadeSAN
|
2
|
+
|
3
|
+
== What is BrocadeSAN?
|
4
|
+
|
5
|
+
BrocadeSAN provides a simple wrapper API to communicate with Brocade SAN switches using SSH connection.
|
6
|
+
You have option to either run the command manualy or query the switch with pre-defined set of methods.
|
7
|
+
|
8
|
+
Additionally you can use Brocade::SAN::Provisioning::Agent for zoning provisioning tasks.
|
9
|
+
|
10
|
+
== Basic Usage
|
11
|
+
|
12
|
+
You can use BrocadeSAN in 2 different ways:
|
13
|
+
|
14
|
+
=== 1. Query the SAN switch directly using 1 connection per command query
|
15
|
+
|
16
|
+
# this will query the switch name and version and open connection to switch twice
|
17
|
+
|
18
|
+
switch=Brocade::SAN::Switch.new("address","user","password")
|
19
|
+
|
20
|
+
switch.name
|
21
|
+
switch.firmware
|
22
|
+
|
23
|
+
=== 2. Query the SAN switch in session block using 1 connection per session
|
24
|
+
|
25
|
+
# this will query the switch name and version and open connection to switch only once
|
26
|
+
|
27
|
+
switch=Brocade::SAN::Switch.new("address","user","password")
|
28
|
+
|
29
|
+
switch.session do
|
30
|
+
switch.name
|
31
|
+
switch.firmare
|
32
|
+
end
|
33
|
+
|
34
|
+
== Special Usage
|
35
|
+
|
36
|
+
If the API is not sufficient for your need you can always utilize the Brocade::SAN::Switch#query method to execute arbitrary commands
|
37
|
+
|
38
|
+
# sends command to switch
|
39
|
+
response=switch.query("portshow | grep Online ")
|
40
|
+
|
41
|
+
# sends several commands to switch
|
42
|
+
response=switch.query("switchshow","cfgshow")
|
43
|
+
|
44
|
+
# calls interactive command and sends response along the way
|
45
|
+
# the mode has to be set to interactive
|
46
|
+
# the mode will persist across queries
|
47
|
+
# change it back to :script when you want to run non-interactive command
|
48
|
+
switch.set_mode :interactive
|
49
|
+
response=switch.query("cfgsave","y")
|
50
|
+
|
51
|
+
Response is type of Brocade::SAN::Switch::Response. You can get the data by calling +data+ and errors by calling +errors+.
|
52
|
+
The data will be raw output from the switch with each cmd prefixed by defined/default prompt.
|
53
|
+
|
54
|
+
== Provisioning
|
55
|
+
|
56
|
+
This is wrapper API for provisioning tasks with added control. This wrapper expects some basic understanding of zoning provisioning tasks and should be used to build
|
57
|
+
specialized provisioning clients.
|
58
|
+
|
59
|
+
# creates a agent, user must have provisioning rights
|
60
|
+
agent=Brocade::SAN::Provisoning::Agent.create("address","user","password")
|
61
|
+
|
62
|
+
# create a zone instance with aliases
|
63
|
+
zone = Brocade::SAN::Zone.new("host_array_zone")
|
64
|
+
zone.add_member "host_alias"
|
65
|
+
zone.add_member "array_alias"
|
66
|
+
|
67
|
+
# gets effecive configuration (false gets name only without members)
|
68
|
+
cfg=agent.effective_configuration(false)
|
69
|
+
|
70
|
+
# creates zone and adds it to the configuration in transaction
|
71
|
+
# transaction saves configuration at the end, it does not enable effective configuration
|
72
|
+
# agent methods outside of transaction will save configuration immediately
|
73
|
+
agent.transaction do
|
74
|
+
agent.zone_create zone
|
75
|
+
agent.cfg_add cfg, zone
|
76
|
+
end
|
77
|
+
|
78
|
+
# enable effective configuration
|
79
|
+
agent.cfg_enable cfg
|
80
|
+
|
81
|
+
== Download and Installation
|
82
|
+
|
83
|
+
Installation *with* RubyGems:
|
84
|
+
# gem install brocadesan
|
85
|
+
|
86
|
+
== Author
|
87
|
+
|
88
|
+
Written 2015 by Tomas Trnecka <mailto:trnecka@gmail.com>.
|
89
|
+
|
90
|
+
== License
|
91
|
+
|
92
|
+
Copyright (c) 2015 Tomas Trnecka
|
93
|
+
|
94
|
+
Permission is hereby granted, free of charge, to any person
|
95
|
+
obtaining a copy of this software and associated documentation
|
96
|
+
files (the "Software"), to deal in the Software without
|
97
|
+
restriction, including without limitation the rights to use,
|
98
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
99
|
+
copies of the Software, and to permit persons to whom the
|
100
|
+
Software is furnished to do so, subject to the following
|
101
|
+
conditions:
|
102
|
+
|
103
|
+
The above copyright notice and this permission notice shall be
|
104
|
+
included in all copies or substantial portions of the Software.
|
105
|
+
|
106
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
107
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
108
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
109
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
110
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
111
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
112
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
113
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/notes/rake_task'
|
3
|
+
require 'rdoc/task'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs = ["lib","test"]
|
8
|
+
t.warning = false
|
9
|
+
t.verbose = true
|
10
|
+
t.test_files = FileList['test/**/*_test.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
Rake::RDocTask.new do |rd|
|
14
|
+
rd.main = "README"
|
15
|
+
rd.title = "BrocadeSAN"
|
16
|
+
rd.rdoc_files.include("README","lib/**/*.rb")
|
17
|
+
|
18
|
+
rd.before_running_rdoc do
|
19
|
+
Rake::Task["generate_meta"].invoke
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
task :generate_meta do
|
24
|
+
files=Dir.glob("lib/brocadesan/config/brocade/san/*cmd_mapping.yml")
|
25
|
+
doc=""
|
26
|
+
files.each do |file|
|
27
|
+
doc+="class Brocade::SAN::#{File.basename(file.gsub("_cmd_mapping.yml","")).split('_').map{|e| e.capitalize}.join}\n"
|
28
|
+
hash=YAML.load(File.read(file))
|
29
|
+
hash.each do |method,v|
|
30
|
+
doc+="##\n"
|
31
|
+
doc+="# :method: #{method.to_s}\n"
|
32
|
+
doc+="# :call-seq:\n"
|
33
|
+
doc+="# #{method.to_s}(forced=true)\n"
|
34
|
+
doc+="#\n"
|
35
|
+
doc+="# If called with +true+ argument it will get the #{v[:attr]} from the switch instead of cache\n"
|
36
|
+
doc+="#\n"
|
37
|
+
doc+="# Returns value in (#{v[:format]}) format\n"
|
38
|
+
doc+="\n"
|
39
|
+
end
|
40
|
+
doc+="end\n"
|
41
|
+
File.open("lib/meta_methods.rb", 'w') {|f| f.write(doc) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Run tests"
|
46
|
+
task :default => :test
|
data/brocadesan.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'brocadesan'
|
3
|
+
s.version = '0.4.0'
|
4
|
+
s.date = '2015-02-05'
|
5
|
+
s.summary = "Brocade SAN library"
|
6
|
+
s.description = "Gem to manipulate FABOS based devices"
|
7
|
+
s.authors = ["Tomas Trnecka"]
|
8
|
+
s.email = 'trnecka@gmail.com'
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
s.homepage = 'http://rubygems.org/gems/brocadesan'
|
11
|
+
s.add_runtime_dependency "net-ssh", ">= 2.9.2"
|
12
|
+
s.add_development_dependency "minitest",[">= 5.0.0"]
|
13
|
+
s.add_development_dependency "rake-notes",[">= 0.2.0"]
|
14
|
+
end
|
data/lib/brocadesan.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Brocade module SAN
|
2
|
+
|
3
|
+
# Alias model
|
4
|
+
class Alias
|
5
|
+
# returns name of the alias
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
# alias member naming rule (regular expresion)
|
9
|
+
#
|
10
|
+
# member can be WWN or Domain,Index port notation
|
11
|
+
#
|
12
|
+
# allowed examples:
|
13
|
+
#
|
14
|
+
# 50:00:10:20:30:40:50:60
|
15
|
+
#
|
16
|
+
# 2,61
|
17
|
+
|
18
|
+
MEMBER_RULE='([\da-f]{2}:){7}[\da-f]{2}|\d{1,3},\d{1,3}'
|
19
|
+
|
20
|
+
# inititialize new alias with +name+
|
21
|
+
#
|
22
|
+
# +opts+ reserved for future use
|
23
|
+
def initialize(name,opts={})
|
24
|
+
# checked against alias name rule - not alias member
|
25
|
+
Switch::verify_name(name)
|
26
|
+
@name=name
|
27
|
+
@members=[]
|
28
|
+
end
|
29
|
+
|
30
|
+
# returns array of members
|
31
|
+
|
32
|
+
def members
|
33
|
+
@members
|
34
|
+
end
|
35
|
+
|
36
|
+
# add new member to the alias
|
37
|
+
#
|
38
|
+
# members of aliases are WWNs or Domain,Index port notation
|
39
|
+
#
|
40
|
+
# +member+ is name of the zone
|
41
|
+
#
|
42
|
+
# return all members, otherwises raises error
|
43
|
+
|
44
|
+
def add_member(member)
|
45
|
+
Alias::verify_member_name(member)
|
46
|
+
@members<<member
|
47
|
+
end
|
48
|
+
|
49
|
+
# verifies if +str+ matches convetion defined in Alias::MEMBER_RULE
|
50
|
+
#
|
51
|
+
# raises Switch::Error: Incorrect name format "+str+" if not
|
52
|
+
#
|
53
|
+
# this method is used mostly internally
|
54
|
+
|
55
|
+
def self.verify_member_name(str)
|
56
|
+
raise Switch::Error.incorrect(str) if !str.match(/#{MEMBER_RULE}/i)
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
@name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end; end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
:name:
|
2
|
+
:cmd: switchshow
|
3
|
+
:attr: switch_name
|
4
|
+
:format: string
|
5
|
+
:state:
|
6
|
+
:cmd: switchshow
|
7
|
+
:attr: switch_state
|
8
|
+
:format: string
|
9
|
+
:mode:
|
10
|
+
:cmd: switchshow
|
11
|
+
:attr: switch_mode
|
12
|
+
:format: string
|
13
|
+
:role:
|
14
|
+
:cmd: switchshow
|
15
|
+
:attr: switch_role
|
16
|
+
:format: string
|
17
|
+
:domain:
|
18
|
+
:cmd: switchshow
|
19
|
+
:attr: switch_domain
|
20
|
+
:format: integer
|
21
|
+
:id:
|
22
|
+
:cmd: switchshow
|
23
|
+
:attr: switch_id
|
24
|
+
:format: string
|
25
|
+
:wwn:
|
26
|
+
:cmd: switchshow
|
27
|
+
:attr: switch_wwn
|
28
|
+
:format: string
|
29
|
+
:zoning_enabled:
|
30
|
+
:cmd: switchshow
|
31
|
+
:attr: zoning_enabled
|
32
|
+
:format: boolean
|
33
|
+
:active_config:
|
34
|
+
:cmd: switchshow
|
35
|
+
:attr: active_config
|
36
|
+
:format: string
|
37
|
+
:switch_beacon:
|
38
|
+
:cmd: switchshow
|
39
|
+
:attr: switch_beacon
|
40
|
+
:format: string
|
41
|
+
:fc_router:
|
42
|
+
:cmd: switchshow
|
43
|
+
:attr: fc_router
|
44
|
+
:format: string
|
45
|
+
:allow_xisl_use:
|
46
|
+
:cmd: switchshow
|
47
|
+
:attr: allow_xisl_use
|
48
|
+
:format: string
|
49
|
+
:ls_attributes:
|
50
|
+
:cmd: switchshow
|
51
|
+
:attr: ls_attributes
|
52
|
+
:format: string
|
53
|
+
:kernel:
|
54
|
+
:cmd: version
|
55
|
+
:attr: kernel
|
56
|
+
:format: string
|
57
|
+
:firmware:
|
58
|
+
:cmd: version
|
59
|
+
:attr: fabric_os
|
60
|
+
:format: string
|
61
|
+
:logical_switches:
|
62
|
+
:cmd: "lscfg --show"
|
63
|
+
:attr: created_switches
|
64
|
+
:format: array
|
65
|
+
:ports:
|
66
|
+
:cmd: "switchshow"
|
67
|
+
:attr: ports
|
68
|
+
:format: array
|
69
|
+
:aptpolicy:
|
70
|
+
:cmd: "aptpolicy"
|
71
|
+
:attr: current_policy
|
72
|
+
:format: integer
|
73
|
+
:chassisname:
|
74
|
+
:cmd: "chassisname"
|
75
|
+
:attr: chassisname
|
76
|
+
:format: string
|
77
|
+
:dls:
|
78
|
+
:cmd: "dlsshow"
|
79
|
+
:attr: dlsshow
|
80
|
+
:format: string
|
81
|
+
:iod:
|
82
|
+
:cmd: "iodshow"
|
83
|
+
:attr: iodshow
|
84
|
+
:format: string
|
85
|
+
:status:
|
86
|
+
:cmd: "switchstatusshow"
|
87
|
+
:attr: switch_state
|
88
|
+
:format: string
|
89
|
+
:status_details:
|
90
|
+
:cmd: "switchstatusshow"
|
91
|
+
:attr: switchstatusshow
|
92
|
+
:format: string
|
93
|
+
:ip:
|
94
|
+
:cmd: "switchstatusshow"
|
95
|
+
:attr: ip_address
|
96
|
+
:format: string
|
97
|
+
:supportshow:
|
98
|
+
:cmd: "supportshow"
|
99
|
+
:attr: supportshow
|
100
|
+
:format: string
|
101
|
+
:isls:
|
102
|
+
:cmd: "islshow"
|
103
|
+
:attr: isl_links
|
104
|
+
:format: array
|
105
|
+
:trunks:
|
106
|
+
:cmd: "trunkshow"
|
107
|
+
:attr: trunk_links
|
108
|
+
:format: array
|
109
|
+
:access_gateways:
|
110
|
+
:cmd: "agshow"
|
111
|
+
:attr: ag
|
112
|
+
:format: array
|
113
|
+
:cfg_transaction:
|
114
|
+
:cmd: "cfgtransshow"
|
115
|
+
:attr: cfg_transaction
|
116
|
+
:format: hash
|
@@ -0,0 +1,21 @@
|
|
1
|
+
:switchshow: simple
|
2
|
+
:version: simple
|
3
|
+
:fosconfig: simple
|
4
|
+
:lscfg: simple
|
5
|
+
:aptpolicy: simple
|
6
|
+
:chassisname: oneline
|
7
|
+
:dlsshow: oneline
|
8
|
+
:iodshow: oneline
|
9
|
+
:switchstatusshow: multiline
|
10
|
+
:supportshow: multiline
|
11
|
+
:cfgshow: cfgshow
|
12
|
+
:nsshow: ns
|
13
|
+
:nscamshow: ns
|
14
|
+
:fabricshow: simple
|
15
|
+
:agshow: simple
|
16
|
+
:configshow: simple
|
17
|
+
:islshow: simple
|
18
|
+
:trunkshow: trunk
|
19
|
+
:cfgtransshow: multiline
|
20
|
+
:zoneshow: cfgshow
|
21
|
+
:alishow: cfgshow
|
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
# Basic wrapper class that runs SSH queries on the device and returns Response
|
4
|
+
#
|
5
|
+
# It is used to extend further classes with SSH query mechanism
|
6
|
+
|
7
|
+
module SshDevice
|
8
|
+
|
9
|
+
# default query prompt that will preceed each command started by +query+ in the Response +data+
|
10
|
+
#
|
11
|
+
# value is "> "
|
12
|
+
#
|
13
|
+
# This way the parser can separate from commands and data. The default will work everwhere where the retunned data is not prepended by "> "
|
14
|
+
#
|
15
|
+
# Ignore the ssh prompt like this:
|
16
|
+
# super_server(admin)> cmd
|
17
|
+
# super_server(admin)> data
|
18
|
+
#
|
19
|
+
# This on the other hand would be problem. In this case override the prompt
|
20
|
+
# super_server(admin)> cmd
|
21
|
+
# super_server(admin)>> data
|
22
|
+
|
23
|
+
DEFAULT_QUERY_PROMPT="> "
|
24
|
+
|
25
|
+
attr_reader :prompt
|
26
|
+
|
27
|
+
# Initialization method
|
28
|
+
#
|
29
|
+
# +opts+ can be:
|
30
|
+
#
|
31
|
+
# [:interactive] +true+ / +false+
|
32
|
+
# will use interactive query
|
33
|
+
# can be set later
|
34
|
+
# [:prompt] +prompt+
|
35
|
+
# will override the DEFAULT_QUERY_PROMPT
|
36
|
+
def initialize(address,user,password,opts={})
|
37
|
+
@address=address
|
38
|
+
@user=user
|
39
|
+
@password=password
|
40
|
+
@opts=opts
|
41
|
+
@session=nil
|
42
|
+
@session_level=0
|
43
|
+
@prompt = opts[:prompt] ? opts[:prompt].to_s : DEFAULT_QUERY_PROMPT
|
44
|
+
end
|
45
|
+
|
46
|
+
# get current query mode
|
47
|
+
#
|
48
|
+
# returns either +interactive+ or +script+
|
49
|
+
#
|
50
|
+
# default mode is +script+
|
51
|
+
def get_mode
|
52
|
+
[true,false].include?(@opts[:interactive]) ? (@opts[:interactive]==true ? "interactive" : "script") : "script"
|
53
|
+
end
|
54
|
+
|
55
|
+
# sets current query mode
|
56
|
+
#
|
57
|
+
# +mode+: interactive or script
|
58
|
+
#
|
59
|
+
# interactive - used to do interactive queries in scripted manner by providing all responses in advance, see #query
|
60
|
+
|
61
|
+
def set_mode(mode)
|
62
|
+
@opts[:interactive] = mode.to_s == "interactive" ? true : false
|
63
|
+
get_mode
|
64
|
+
end
|
65
|
+
|
66
|
+
# Queries +cmds+ commands directly.
|
67
|
+
# This method is to be used to implement higlevel API
|
68
|
+
# or
|
69
|
+
# to do more difficult queries that have to be parsed separately and for which there is now highlevel API
|
70
|
+
#
|
71
|
+
# When started in interactive mode be sure the command is followed by inputs for that command.
|
72
|
+
# If command will require additional input and there is not one provided the prompt will receive +enter+.
|
73
|
+
# It will do this maximum of 100 times, then it gives up and returns whatever it got until then.
|
74
|
+
#
|
75
|
+
# Query command will open connection to device if called outside session block
|
76
|
+
# or use existing session if called within session block.
|
77
|
+
#
|
78
|
+
# Example:
|
79
|
+
# >> class Test
|
80
|
+
# >> include SshDevice
|
81
|
+
# >> end
|
82
|
+
# >> device = Test.new("address","user","password")
|
83
|
+
# >> device.query("switchname")
|
84
|
+
# => #<Test::Response:0x2bb1e00 @errors="", @data="> switchname\nsanswitchA\n", @parsed={:parsing_position=>"end"}>
|
85
|
+
#
|
86
|
+
#
|
87
|
+
# Returns instance of Response or raises Error if the connection cannot be opened
|
88
|
+
#
|
89
|
+
# Raises Error if SSH returns error. SSH error will be available in the exception message
|
90
|
+
def query(*cmds)
|
91
|
+
output=nil
|
92
|
+
if session_exist?
|
93
|
+
output=exec(@session,cmds)
|
94
|
+
else
|
95
|
+
Net::SSH.start @address, @user, :password=>@password do |ssh|
|
96
|
+
output=exec(ssh,cmds)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
raise self.class::Error.new(output.errors) if !output.errors.empty?
|
101
|
+
|
102
|
+
return output
|
103
|
+
end
|
104
|
+
|
105
|
+
# Opens a session block
|
106
|
+
#
|
107
|
+
# All queries within the session block use the same connection. This speeds up the query processing.
|
108
|
+
#
|
109
|
+
# The connection is closed at the end of the block
|
110
|
+
#
|
111
|
+
# The command supports session blocks within session blocks. Session will be closed only at the last block
|
112
|
+
#
|
113
|
+
# Example:
|
114
|
+
# device.session do
|
115
|
+
# device.query("switchname")
|
116
|
+
# device.version
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# Must receive block, raises SshDevice::Error otherwise
|
120
|
+
def session(&block)
|
121
|
+
raise Error.no_block if !block_given?
|
122
|
+
begin
|
123
|
+
@session_level+=1
|
124
|
+
if !session_exist?
|
125
|
+
@session=Net::SSH.start @address, @user, :password=>@password
|
126
|
+
end
|
127
|
+
yield
|
128
|
+
rescue => e
|
129
|
+
raise e
|
130
|
+
ensure
|
131
|
+
@session_level-=1
|
132
|
+
@session.close if @session && @session_level==0 && !@session.closed?
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Defines a block of code to run in script mode
|
137
|
+
#
|
138
|
+
# the mode will be reverted back to initial mode after leaving the block
|
139
|
+
def script_mode(&block)
|
140
|
+
run_in_mode :script, &block
|
141
|
+
end
|
142
|
+
|
143
|
+
# Defines a block of code to run in interactive mode
|
144
|
+
#
|
145
|
+
# the mode will be reverted back to initial mode after leaving the block
|
146
|
+
def interactive_mode(&block)
|
147
|
+
run_in_mode :interactive, &block
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def run_in_mode(mode,&block)
|
153
|
+
old_mode = get_mode
|
154
|
+
set_mode mode
|
155
|
+
begin
|
156
|
+
yield
|
157
|
+
rescue => e
|
158
|
+
raise e
|
159
|
+
ensure
|
160
|
+
set_mode old_mode
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def session_exist?
|
165
|
+
@session && !@session.closed?
|
166
|
+
end
|
167
|
+
|
168
|
+
def exec(ssh_session,cmds)
|
169
|
+
|
170
|
+
@opts[:interactive]||=false
|
171
|
+
|
172
|
+
if @opts[:interactive]==true
|
173
|
+
interactive_exec(ssh_session,cmds)
|
174
|
+
else
|
175
|
+
standard_exec(ssh_session,cmds)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def standard_exec(ssh_session,cmds)
|
180
|
+
output=new_output
|
181
|
+
cmds.each do |cmd|
|
182
|
+
output.data+=@prompt+cmd+"\n"
|
183
|
+
ssh_session.exec! cmd do |ch, stream, data|
|
184
|
+
if stream == :stderr
|
185
|
+
output.errors+=data
|
186
|
+
else
|
187
|
+
output.data+=data
|
188
|
+
end
|
189
|
+
end
|
190
|
+
output.errors+="\n" if !output.errors.empty?
|
191
|
+
output.data+="\n"
|
192
|
+
end
|
193
|
+
return output
|
194
|
+
end
|
195
|
+
|
196
|
+
# interactive exec considers cmds as inputs, only first item is considered as command
|
197
|
+
|
198
|
+
def interactive_exec(ssh_session,cmds)
|
199
|
+
output=new_output
|
200
|
+
# instance variable is used for pure testability, this could do with local variable otherwise
|
201
|
+
@retries = 0
|
202
|
+
cmd=cmds.shift
|
203
|
+
output.data+=@prompt+cmd+"\n"
|
204
|
+
ssh_session.open_channel do |channel|
|
205
|
+
channel.request_pty
|
206
|
+
channel.exec cmd do |ch, success|
|
207
|
+
abort "could not execute #{cmd}" unless success
|
208
|
+
ch.on_data do |ch1, data|
|
209
|
+
output.data+=data
|
210
|
+
# data is multiline, if the very last character is not newline then the command expects response
|
211
|
+
|
212
|
+
if !data.match(/\n$/)
|
213
|
+
@retries+=1 if cmds.empty?
|
214
|
+
stdin = cmds.empty? ? "\n" : cmds.shift+"\n"
|
215
|
+
ch1.send_data stdin
|
216
|
+
output.data+=stdin
|
217
|
+
end
|
218
|
+
# we do not want to send newline to infinity so we exit
|
219
|
+
# if we are still getting response after 100 empty command
|
220
|
+
# if the command is newline explicitely this will not increase the @retriy
|
221
|
+
ch.close if @retries==100
|
222
|
+
end
|
223
|
+
|
224
|
+
ch.on_extended_data do |ch1, type, data|
|
225
|
+
output.errors+=data
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
ssh_session.loop
|
230
|
+
return output
|
231
|
+
end
|
232
|
+
|
233
|
+
def new_output
|
234
|
+
# this approach is used to use Response of the calling class
|
235
|
+
self.class::Response.new(@prompt)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
module SshDevice
|
241
|
+
# This class defines the device response and it should not be manipulated directly
|
242
|
+
# Only exception is direct usage of query method which returns instance of this class
|
243
|
+
|
244
|
+
class Response
|
245
|
+
# contains output of the command
|
246
|
+
attr_accessor :data
|
247
|
+
# contains errors raised by SSH exec
|
248
|
+
attr_accessor :errors
|
249
|
+
# contains parsed information after the parse method ran
|
250
|
+
attr_accessor :parsed # :nodoc:
|
251
|
+
|
252
|
+
#initialization method
|
253
|
+
def initialize(prompt) # :nodoc:
|
254
|
+
@errors=""
|
255
|
+
@data=""
|
256
|
+
@parsed = {
|
257
|
+
:parsing_position=>nil
|
258
|
+
}
|
259
|
+
@prompt=prompt
|
260
|
+
end
|
261
|
+
|
262
|
+
# Resets all parsed data
|
263
|
+
#
|
264
|
+
def reset # :nodoc:
|
265
|
+
@parsed = {
|
266
|
+
:parsing_position=>nil
|
267
|
+
}
|
268
|
+
end
|
269
|
+
|
270
|
+
# Parse the current data and stores result to +parsed+.
|
271
|
+
#
|
272
|
+
# Any class that inherits from this class should override the private parse_line method and store results into +parsed+ hash
|
273
|
+
def parse # :nodoc:
|
274
|
+
reset if !@parsed.kind_of? Hash
|
275
|
+
|
276
|
+
@data.split("\n").each do |line|
|
277
|
+
parse_line line
|
278
|
+
end
|
279
|
+
|
280
|
+
@parsed[:parsing_position]="end"
|
281
|
+
end
|
282
|
+
|
283
|
+
private
|
284
|
+
|
285
|
+
def parse_line(line)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Class using for raising specific errors
|
290
|
+
class Error < StandardError;
|
291
|
+
SESSION_WTIHOUT_BLOCK = "Error: Session can run only with block"
|
292
|
+
def self.no_block
|
293
|
+
self.new(SESSION_WTIHOUT_BLOCK)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|