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.
Files changed (79) hide show
  1. data/README +113 -0
  2. data/Rakefile +46 -0
  3. data/brocadesan.gemspec +14 -0
  4. data/lib/brocadesan.rb +8 -0
  5. data/lib/brocadesan/alias.rb +64 -0
  6. data/lib/brocadesan/config/brocade/san/switch_cmd_mapping.yml +116 -0
  7. data/lib/brocadesan/config/parser_mapping.yml +21 -0
  8. data/lib/brocadesan/device.rb +296 -0
  9. data/lib/brocadesan/monkey/string.rb +11 -0
  10. data/lib/brocadesan/provisioning.rb +894 -0
  11. data/lib/brocadesan/switch.rb +882 -0
  12. data/lib/brocadesan/wwn.rb +63 -0
  13. data/lib/brocadesan/zone.rb +60 -0
  14. data/lib/brocadesan/zone_configuration.rb +38 -0
  15. data/lib/meta_methods.rb +263 -0
  16. data/test/alias_test.rb +68 -0
  17. data/test/device_test.rb +203 -0
  18. data/test/output_helpers.rb +308 -0
  19. data/test/outputs/agshow_1.txt +7 -0
  20. data/test/outputs/agshow_1.yml +31 -0
  21. data/test/outputs/agshow_2.txt +4 -0
  22. data/test/outputs/agshow_2.yml +3 -0
  23. data/test/outputs/apt_policy_1.txt +6 -0
  24. data/test/outputs/apt_policy_1.yml +3 -0
  25. data/test/outputs/cfgshow_1.txt +5 -0
  26. data/test/outputs/cfgshow_1.yml +7 -0
  27. data/test/outputs/cfgshow_2.txt +31 -0
  28. data/test/outputs/cfgshow_2.yml +32 -0
  29. data/test/outputs/cfgshow_3.txt +9 -0
  30. data/test/outputs/cfgshow_3.yml +12 -0
  31. data/test/outputs/cfgtransshow_1.txt +2 -0
  32. data/test/outputs/cfgtransshow_1.yml +5 -0
  33. data/test/outputs/cfgtransshow_2.txt +3 -0
  34. data/test/outputs/cfgtransshow_2.yml +4 -0
  35. data/test/outputs/cfgtransshow_3.txt +3 -0
  36. data/test/outputs/cfgtransshow_3.yml +4 -0
  37. data/test/outputs/chassisname_1.txt +2 -0
  38. data/test/outputs/chassisname_1.yml +3 -0
  39. data/test/outputs/dlsshow_1.txt +4 -0
  40. data/test/outputs/dlsshow_1.yml +4 -0
  41. data/test/outputs/dlsshow_2.txt +4 -0
  42. data/test/outputs/dlsshow_2.yml +4 -0
  43. data/test/outputs/fabricshow_1.txt +10 -0
  44. data/test/outputs/fabricshow_1.yml +34 -0
  45. data/test/outputs/iodshow_1.txt +4 -0
  46. data/test/outputs/iodshow_1.yml +4 -0
  47. data/test/outputs/islshow_1.txt +6 -0
  48. data/test/outputs/islshow_1.yml +62 -0
  49. data/test/outputs/islshow_2.txt +2 -0
  50. data/test/outputs/islshow_2.yml +2 -0
  51. data/test/outputs/lscfg_show_1.txt +71 -0
  52. data/test/outputs/lscfg_show_1.yml +5 -0
  53. data/test/outputs/ns_1.txt +80 -0
  54. data/test/outputs/ns_1.yml +39 -0
  55. data/test/outputs/ns_2.txt +37 -0
  56. data/test/outputs/ns_2.yml +21 -0
  57. data/test/outputs/putty.log +1867 -0
  58. data/test/outputs/switch_1.txt +25 -0
  59. data/test/outputs/switch_1.yml +73 -0
  60. data/test/outputs/switch_2.txt +18 -0
  61. data/test/outputs/switch_2.yml +42 -0
  62. data/test/outputs/switch_3.txt +14 -0
  63. data/test/outputs/switch_3.yml +46 -0
  64. data/test/outputs/switchstatusshow_1.txt +21 -0
  65. data/test/outputs/switchstatusshow_1.yml +19 -0
  66. data/test/outputs/trunkshow_1.txt +8 -0
  67. data/test/outputs/trunkshow_1.yml +44 -0
  68. data/test/outputs/trunkshow_2.txt +2 -0
  69. data/test/outputs/trunkshow_2.yml +2 -0
  70. data/test/outputs/version_1.txt +6 -0
  71. data/test/outputs/version_1.yml +8 -0
  72. data/test/outputs/vf_switch_1.txt +25 -0
  73. data/test/outputs/vf_switch_1.yml +73 -0
  74. data/test/provisioning_test.rb +1043 -0
  75. data/test/switch_test.rb +476 -0
  76. data/test/wwn_test.rb +41 -0
  77. data/test/zone_configuration_test.rb +65 -0
  78. data/test/zone_test.rb +73 -0
  79. 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
@@ -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,8 @@
1
+ require 'brocadesan/monkey/string'
2
+ require 'brocadesan/device'
3
+ require 'brocadesan/switch'
4
+ require 'brocadesan/zone_configuration'
5
+ require 'brocadesan/zone'
6
+ require 'brocadesan/alias'
7
+ require 'brocadesan/wwn'
8
+ require 'brocadesan/provisioning'
@@ -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