brocadesan 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|