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
@@ -0,0 +1,882 @@
1
+ require 'net/ssh'
2
+ require 'yaml'
3
+
4
+ # Brocade namespace
5
+ module Brocade
6
+ # SAN namespace
7
+ module SAN
8
+ # TODO: zoneshow, cfgshow and alishow tests
9
+ # configuration class
10
+ class Configuration #:nodoc:
11
+ def self.cmd_mapping_path(_class)
12
+ File.realpath("../config/#{_class.name.underscore}_cmd_mapping.yml",__FILE__)
13
+ end
14
+
15
+ def self.parser_path
16
+ File.realpath("../config/parser_mapping.yml",__FILE__)
17
+ end
18
+ end
19
+
20
+ # Class to model SAN switch from Brocade
21
+ class Switch
22
+
23
+ include SshDevice
24
+
25
+ # Maps each method name to command to be run to obtain it and to hash key where it ill be stored.
26
+ # As well sets the format of the returned value. This is used only for documentation purposes.
27
+ #
28
+ # See lib/config/brocade/san/switch_cmd_mapping.yml for details
29
+ #
30
+ # Example:
31
+ # :name:
32
+ # :cmd: switchshow
33
+ # :attr: switch_name
34
+ # :format: string
35
+ #
36
+ # This will cause that class will have instance method called name(forced=true).
37
+ # When the method is called first time or +forced+ is true the +switchshow+ command
38
+ # will be queried on the switch.
39
+ # The query response will be then parsed and stored in Response +parsed+ hash under +:switch_name+ key.
40
+ # The +parsed+ hash is then merged into switch +configuration+ hash.
41
+ # At the end the +:switch_name+ key is returned from +configuration+ hash.
42
+ #
43
+ # The parser needs to ensure that it parses the +cmd+ output and stores the proper value into Response +parsed+ under +:attr+ key.
44
+
45
+ CMD_MAPPING=YAML.load(File.read(Configuration::cmd_mapping_path(self)))
46
+
47
+ # Maps each command to the parser method to use
48
+ #
49
+ # See lib/config/parser_mapping.yml for details
50
+ PARSER_MAPPING=YAML.load(File.read(Configuration::parser_path))
51
+
52
+ # zone configuration, zone and zone aliases naming rule
53
+ # must start with an alphabetic character and may contain
54
+ # alphanumeric characters and the underscore ( _ ) character.
55
+ NAME_RULE='^[a-z][a-z_\d]*$'
56
+
57
+ # Used to dynamically create named methods based on CMD_MAPPING
58
+ def self.attributes(args)
59
+ args.each do |arg|
60
+ define_method arg do |forced=false|
61
+ self.get(arg,forced)
62
+ end
63
+ end
64
+ end
65
+
66
+ # verifies if name matches convetion defined in NAME_RULE
67
+ # raises Switch::Error: Incorrect name format if not
68
+ # this method is used internally mostly
69
+
70
+ def self.verify_name(name)
71
+ raise Switch::Error.incorrect(name) if !name.match(/#{NAME_RULE}/i)
72
+ end
73
+
74
+ # Creates a Switch instance and tests a connection.
75
+ #
76
+ # Checks as well if the switch is virtual fabric enabled since that defines the way it will be queried further.
77
+ def initialize(*params)
78
+ super(*params)
79
+ @configuration={}
80
+ vf
81
+ end
82
+
83
+ # Hash containing parsed attributes
84
+ #
85
+ # Can be used to obtain parsed attributes for which there is no named method.
86
+ # These attributes however has to be obtained as colateral of running another public method.
87
+ #
88
+ # Generally this attribute should not be accessed directly but named method for the given attribute should be created.
89
+ #
90
+ # Example:
91
+ #
92
+ # # this will call switchshow (see CMD_MAPPING)
93
+ # # and load :several switchow values into configuration as well
94
+ # # the command however returns only whether the ls_attributtes
95
+ #
96
+ # switch.configuration
97
+ # => nil
98
+ # switch.ls_attributes
99
+ # => {:fid=>"128", :base_switch=>"no", :default_switch=>"yes", :address_mode=>"0"}
100
+ # switch.configuration
101
+ # => {:vf=>"enabled", :parsing_position=>"end", :switch_name=>"H2C04R065-U03-A01", :switch_type=>"62.3", :switch_state=>"Online", ...
102
+ # switch.configuration[:switch_name]
103
+ # => "H2C04R065-U03-A01"
104
+ #
105
+ # # this swichname will be taken from cache as the switchshow was started as part of ls_attributes
106
+ # switch.name
107
+ # => "H2C04R065-U03-A01"
108
+
109
+
110
+ attr_reader :configuration
111
+
112
+ # Fabric id of the switch to be queried. Can be set using +set_context+.
113
+ #
114
+ # If the given +fid+ does not exist default switch for the provided
115
+ # account will be queried.
116
+
117
+ attr_reader :fid
118
+
119
+ attributes CMD_MAPPING.keys
120
+
121
+ # Sets the FID of the switch to be queried and clears cache.
122
+ # Next queries will be done directly on switch.
123
+ #
124
+ # If not +fid+ is given it will set it to default 128.
125
+ # If the switch does not support virtual fabrics this will be ignored.
126
+ #
127
+ # Returns the fid that was set
128
+ def set_context(fid=128)
129
+ @loaded={}
130
+ @fid = fid.to_i==0 ? 128 : fid.to_i
131
+ end
132
+
133
+ # gets the +attr+
134
+ #
135
+ # +attr+ has to be speficied in the CMD_MAPPING
136
+ #
137
+ # named methods are wrappers around this method so you should not use this directly
138
+ def get(attr,forced=false)
139
+ raise Switch::Error.unknown if CMD_MAPPING[attr.to_sym].nil?
140
+
141
+ cmd=CMD_MAPPING[attr.to_sym][:cmd]
142
+
143
+ refresh(cmd,"",forced)
144
+
145
+ @configuration[CMD_MAPPING[attr.to_sym][:attr].to_sym]
146
+ end
147
+
148
+ # returns switches in the fabric in hash form
149
+
150
+ def fabric(forced=false)
151
+ cmd="fabricshow"
152
+ refresh(cmd,"",forced)
153
+ @configuration[:fabric]
154
+ end
155
+
156
+ # If called with +true+ argument it will get the virtual_fabric from the switch instead of cache
157
+ #
158
+ # Returns value in (string) format
159
+
160
+ def vf(forced=false)
161
+ if !@configuration[:vf] || forced
162
+ # using this instead of fosconfig --show as that command does not work everywhere
163
+ # we could user #ls_attributes method but that loaded whole switchshow os this will be a bit faster, especially with big switches
164
+ # it needs to be faster as vf is called during initialization
165
+ # if the switch is vf there will be LS Attributes line, otherwise it will be empty
166
+ response=query("switchshow|grep \"^LS Attributes\"")
167
+ @configuration[:vf] = response.data.split("\n").size == 2 ? "enabled" : "disabled"
168
+ end
169
+ @configuration[:vf]
170
+ end
171
+
172
+ # Returns ZoneConfigurations array
173
+ #
174
+ # If +full+ is set to true it loads full configurations with zones and aliases, otherwise it gets just the names.
175
+ #
176
+ # It laods even zone configurations that were create as part of ongoing transaction.
177
+ def zone_configurations(full=false,forced=false)
178
+ get_configshow(full,forced)[:zone_configurations]
179
+ end
180
+
181
+ # Returns effective ZoneConfiguration
182
+ #
183
+ # If +full+ is set to true it loads full effective configuration with zones and aliases, otherwise it gets just the name
184
+ def effective_configuration(full=false,forced=false)
185
+ zone_configurations(full,forced).select {|z| z.effective == true}.first
186
+ end
187
+
188
+ # returns all zones defined on the switch including zones created in ongoing transaction as array of Zone
189
+
190
+ def zones(forced=false)
191
+ get_configshow(true,forced)[:zones]
192
+ end
193
+
194
+ # returns all aliases defined on the switch including aliases created in ongoing transaction as array of Alias
195
+
196
+ def aliases(forced=false)
197
+ get_configshow(true,forced)[:aliases]
198
+ end
199
+
200
+ # returns Zone with name of +str+ if exists, +nil+ otherwise
201
+ def find_zone(str)
202
+ find(str,:object=>:zone)
203
+ end
204
+
205
+ # returns Zone array of Zones with name matching +regexp+ if exists, [] otherwise
206
+ #
207
+ # find is case insesitive
208
+ def find_zones(regexp)
209
+ zones = find(regexp,:object=>:zone,:find_mode=>:partial)
210
+ return [] if zones==[nil]
211
+ zones
212
+ end
213
+
214
+ # returns Alias with name of +str+ if exists, +nil+ otherwise
215
+ def find_alias(str)
216
+ al = find(str,:object=>:alias)
217
+ end
218
+
219
+ # returns Alias array of Aliases with name matching +regexp+ if exists, [] otherwise
220
+ #
221
+ # find is case insesitive
222
+ def find_aliases(regexp)
223
+ aliases = find(regexp,:object=>:alias,:find_mode=>:partial)
224
+ return [] if aliases==[nil]
225
+ aliases
226
+ end
227
+
228
+ # returns all WWNs
229
+ #
230
+ # +mode+
231
+ #
232
+ # [:local] returns all local WWNs
233
+ # [:cached] returns wwns cached from remote switches
234
+ # [:all] returns all local and remote wwns
235
+
236
+ def wwns(forced=false,mode=:local)
237
+ if mode==:local
238
+ get_ns(true,forced,:local=>true)
239
+ elsif mode==:cached
240
+ get_ns(true,forced,:remote=>true)
241
+ else
242
+ get_ns(true,forced,:local=>true).concat get_ns(true,forced,:remote=>true)
243
+ end
244
+ end
245
+
246
+ # returns WWN of given +value+ if exists, +nil+ if not
247
+ #
248
+ # if +forced+ is true it will load the data from switch instead of the cache
249
+ #
250
+ # +opts+
251
+ #
252
+ # [:fabric_wide] searches whole fabric
253
+ def find_wwn(value,forced=true,opts={:fabric_wide=>false})
254
+ objs = opts[:fabric_wide]==true ? get_ns(true,forced,:local=>true).concat(get_ns(true,forced,:remote=>true)) : get_ns(true,forced,:local=>true)
255
+ objs.find {|k| value.downcase == k.value.downcase}
256
+ end
257
+
258
+ # finds configuration object by +str+. Case insensitive.
259
+ # If the +object+ option is not specified it searches :zone objects. It finds only saved objects. If the object was created but the transaction is not confirmed it will not find it.
260
+ #
261
+ # +opts+
262
+ #
263
+ # [:object] :zones (default) - finds zones
264
+ #
265
+ # :aliases - finds aliases
266
+ # [:find_mode] :partial - find partial matches
267
+ #
268
+ # :exact(default) - finds exact matches
269
+ #
270
+ # Example:
271
+ #
272
+ # switch.find("zone1",:object=>:zone)
273
+ def find(str,opts={})
274
+ # do not change the following 3 lines without writing test for it
275
+ obj = !opts[:object].nil? && [:zone,:alias].include?(opts[:object]) ? opts[:object] : :zone
276
+ mode = !opts[:find_mode].nil? && [:partial,:exact].include?(opts[:find_mode]) ? opts[:find_mode] : :exact
277
+ grep_exp = mode==:exact ? " | grep -i -E ^#{obj}\.#{str}:" : " | grep -i -E ^#{obj}\..*#{str}.*:"
278
+
279
+ response = script_mode do
280
+ query("configshow"+grep_exp)
281
+ end
282
+ response.parse
283
+
284
+ #output of configshow is stored to find_results
285
+
286
+ objs=response.parsed[:find_results]
287
+ objs||=[]
288
+
289
+ result=[]
290
+
291
+ objs.each do |item|
292
+ i = obj==:zone ? Zone.new(item[:obj]) : Alias.new(item[:obj])
293
+ item[:members].split(";").each do |member|
294
+ i.add_member(member)
295
+ end
296
+ result<<i
297
+ end
298
+ # result is array of Zone or Alias instances
299
+ result
300
+ end
301
+
302
+ # finds configuration objects that have member specified by +str+. Case insensitive.
303
+ # If the +object+ option is not specified it searches :zone objects.
304
+ # See find_by_member_from_cfgshow.
305
+ #
306
+ # +opts+
307
+ #
308
+ # [:object] :all (default) - finds all objects (zones, configs, aliases)
309
+ #
310
+ # :aliases - finds aliases
311
+ #
312
+ # :zones - finds zones
313
+ #
314
+ # [:find_mode] :partial - find partial matches
315
+ #
316
+ # :exact(default) - finds exact matches
317
+ #
318
+ # [:transaction] false (defualt) - ignores object in ongoing transaction
319
+ #
320
+ # true - includes objects in ongoing transaction
321
+ #
322
+ # Example:
323
+ #
324
+ # switch.find_by_member("zone1",:object=>:cfg)
325
+ #
326
+ # Note:
327
+ #
328
+ # Zone can be only members of zone configuration and alias can be only members of zones so finding object that have them as members works with default :all :object.
329
+ # However WWNs can be part both of zones and aliases so that is why there is option to speficy the object
330
+
331
+ def find_by_member(str,opts={})
332
+ obj = !opts[:object].nil? && [:zone,:alias,:all].include?(opts[:object]) ? opts[:object] : :all
333
+ obj = "zone|alias|cfg" if obj==:all
334
+ mode = !opts[:find_mode].nil? && [:partial,:exact].include?(opts[:find_mode]) ? opts[:find_mode] : :exact
335
+
336
+ trans_inc = !opts[:transaction].nil? && [true,false].include?(opts[:transaction]) ? opts[:transaction] : false
337
+
338
+ base_grep1 = "^#{obj}\."
339
+ base_grep2 = mode==:exact ? "(:|;)#{str}(;|$)" : ":.*#{str}"
340
+
341
+ if trans_inc
342
+ grep_exp1 = /#{base_grep1}/i
343
+ grep_exp2 = /#{base_grep2}/i
344
+ response = script_mode do
345
+ query("cfgshow")
346
+ end
347
+ response.send :cfgshow_to_configshow, grep_exp1, grep_exp2
348
+ else
349
+ grep_exp = " | grep -i -E \"#{base_grep1}\""
350
+ grep_exp += " | grep -i -E \"#{base_grep2}\""
351
+ response = script_mode do
352
+ query("configshow"+grep_exp)
353
+ end
354
+ end
355
+
356
+ response.parse
357
+ #output of configshow is stored to find_results
358
+
359
+ objs=response.parsed[:find_results]
360
+ objs||=[]
361
+
362
+ result=[]
363
+
364
+ objs.each do |item|
365
+ i = case item[:type]
366
+ when :zone
367
+ Zone.new(item[:obj])
368
+ when :alias
369
+ Alias.new(item[:obj])
370
+ when :cfg
371
+ ZoneConfiguration.new(item[:obj])
372
+ end
373
+ item[:members].split(";").each do |member|
374
+ i.add_member(member)
375
+ end
376
+ result<<i
377
+ end
378
+ # result is array of Zone, Alias, and ZoneConfiguration instances
379
+ result
380
+ end
381
+
382
+ private
383
+
384
+ def should_refresh?(cmd, forced)
385
+ !@loaded || !@loaded[key(cmd)] || forced
386
+ end
387
+
388
+ def get_configshow(full=false,forced=false)
389
+ cmd="cfgshow"
390
+ filter = full==false ? "-e cfg: -e configuration:" : ""
391
+
392
+ refresh(cmd,filter,forced)
393
+
394
+ #storing configs
395
+ tmp_cfg={}
396
+ tmp_cfg[:zone_configurations]=[]
397
+
398
+
399
+ # storing defined
400
+ @configuration[:defined_configuration][:cfg].each do |config,members|
401
+
402
+ effective = @configuration[:effective_configuration][:cfg].keys[0]==config ? true : false
403
+
404
+ cfg=ZoneConfiguration.new(config,:effective=>effective)
405
+ members.each do |member|
406
+ cfg.add_member member
407
+ end
408
+
409
+ tmp_cfg[:zone_configurations]<<cfg
410
+ end
411
+
412
+ if full
413
+ # storing zones
414
+ tmp_cfg[:zones]=[]
415
+
416
+ if @configuration[:defined_configuration][:zone]
417
+ # storing defined
418
+ active_zones = tmp_cfg[:zone_configurations].select {|z| z.effective == true}.first.members
419
+ @configuration[:defined_configuration][:zone].each do |zone,members|
420
+ active = active_zones.include?(zone) ? true : false
421
+ z=Zone.new(zone,:active=>active)
422
+ members.each do |member|
423
+ z.add_member member
424
+ end
425
+ tmp_cfg[:zones]<<z
426
+ end
427
+ end
428
+
429
+
430
+ # storing aliases
431
+ tmp_cfg[:aliases]=[]
432
+
433
+ if @configuration[:defined_configuration][:alias]
434
+ # storing defined
435
+ @configuration[:defined_configuration][:alias].each do |al_name,members|
436
+ al=Alias.new(al_name)
437
+ members.each do |member|
438
+ al.add_member member
439
+ end
440
+ tmp_cfg[:aliases]<<al
441
+ end
442
+ end
443
+ end
444
+
445
+ tmp_cfg
446
+ end
447
+
448
+ def get_ns(full=false,forced=false,opts={:local=>true})
449
+ cmd = opts[:local] ? "nsshow -t" : "nscamshow -t"
450
+ key = opts[:local] ? :wwn_local : :wwn_remote
451
+ filter = "" #full==false ? "-e cfg: -e configuration:" : ""
452
+
453
+ refresh(cmd,filter,forced)
454
+
455
+ #storing wwns
456
+ tmp_wwns=[]
457
+
458
+ # storing defined
459
+ @configuration[key].each do |wwn|
460
+ domain_id=wwn[:domain_id]==0 ? self.domain.to_i : wwn[:domain_id]
461
+ w=Wwn.new(wwn[:value],wwn[:dev_type],domain_id,wwn[:port_index],:symbol=>wwn[:symbol])
462
+ tmp_wwns<<w
463
+ end
464
+
465
+ tmp_wwns
466
+ end
467
+
468
+ def refresh(cmd,filter="",forced=true)
469
+ return @loaded[key(cmd)] if !should_refresh?(cmd+filter,forced)
470
+ grep_exp=filter.empty? ? "" : " | grep #{filter}"
471
+ response=script_mode do
472
+ query(cmd+grep_exp)
473
+ end
474
+ response.parse
475
+
476
+ #puts response.data
477
+
478
+ @configuration||={}
479
+ @configuration.merge!(response.parsed)
480
+
481
+ @loaded||={}
482
+ # when we use filter we need to mark the cmd as false
483
+ @loaded[key(cmd)]=false
484
+ @loaded[key(cmd+filter.to_s)]=true
485
+
486
+ return @loaded[key(cmd)]
487
+ end
488
+
489
+ def key(cmd)
490
+ cmd.gsub(/\s+/,'_').to_sym
491
+ end
492
+
493
+ def fullcmd(cmd)
494
+ if @configuration[:vf]=="enabled" && @fid
495
+ cmds = cmd.split("|")
496
+ if cmds.size>1
497
+ "fosexec --fid #{@fid} \'#{cmds.shift}\' |#{cmds.join("|")}"
498
+ else
499
+ "fosexec --fid #{@fid} \'#{cmds.shift}\'"
500
+ end
501
+ else
502
+ cmd
503
+ end
504
+ end
505
+ end
506
+
507
+ class Switch
508
+ # class extending SshDevice::Response
509
+ class Response < self::Response
510
+
511
+ # Wrapper around SshDevice::Response +parse+ that
512
+ # includes before and after hooks
513
+
514
+ def parse # :nodoc:
515
+ before_parse
516
+ super
517
+ after_parse
518
+ end
519
+
520
+ private
521
+
522
+ # transfers cfgshow to configshow format
523
+ def cfgshow_to_configshow(*greps)
524
+ # check if we have configshow output
525
+ return false if !@data.match(/> cfgshow/m)
526
+ # replace command
527
+ @data.gsub!("cfgshow","configshow")
528
+ # remove all lines below Effective configuration: included and Defined configuration line
529
+ @data.gsub!(/(Defined configuration:\s*|Effective configuration:.*)/im,"")
530
+ # remove spaces followig ; (includes newlines)
531
+ @data.gsub!(/;\s+/m,";")
532
+ # remove newlines before first member
533
+ @data.gsub!(/\t\n\t+/m,":")
534
+ # removes tab before first member
535
+ @data.gsub!(/([a-z_0-9])\t+([a-z_0-9])/im,'\1:\2')
536
+ # substitute alias,zone and cfg name
537
+ @data.gsub!(/^\s*(zone|alias|cfg):\s+/,'\1.')
538
+ #removes any empty character except newline
539
+ @data.gsub!(/[ \t]+\n/,"\n")
540
+ greps.each do |grep_t|
541
+ @data=@data.split("\n").grep(grep_t).join("\n")
542
+ end
543
+ @data="> configshow\n#{@data}"
544
+ true
545
+ end
546
+
547
+ def before_parse
548
+ reset
549
+ @parsed[:find_results]=[]
550
+ @parsed[:base]={}
551
+ end
552
+
553
+ def after_parse
554
+ @parsed[:ports].uniq! if @parsed[:ports]
555
+ to_purge = [:pointer,:last_key,:key,:domain,:was_popped]
556
+ to_purge<<:find_results if @parsed[:find_results].empty?
557
+ to_purge<<:base if @parsed[:base].empty?
558
+ to_purge.each {|k| @parsed.delete(k)}
559
+ end
560
+
561
+ def parse_line(line)
562
+ return if line.empty?
563
+ # we detect which command output we parse - commands start with defined prompt on the XML line
564
+ @parsed[:parsing_position] = case
565
+ # stripping fosexec, all pipes and ' to get pure command
566
+ when line.match(/^#{@prompt}/) then line.gsub(/(fosexec --fid \d+ \')|\'$|\' \|.*$/,"").split(" ")[1]
567
+ else @parsed[:parsing_position]
568
+ end
569
+ #some default processing
570
+ if line.match(/^#{@prompt}/)
571
+ case @parsed[:parsing_position]
572
+ when "islshow"
573
+ @parsed[:isl_links]||=[]
574
+ when "trunkshow"
575
+ @parsed[:trunk_links]||=[]
576
+ when "agshow"
577
+ @parsed[:ag]||=[]
578
+ when "cfgtransshow"
579
+ # if empty hash is returned the response was unexpected
580
+ @parsed[:cfg_transaction]={}
581
+ end
582
+ end
583
+ #do not process if we are on command line
584
+ return if line.match(/^#{@prompt}/)
585
+
586
+ # we parse only certain commands definned in PARSER_MAPPING
587
+ # all other commands are ignored by parser
588
+ # you can call them usign query and parse by yourself if you need some other
589
+ # or define parser mapping, command mapping and update given parser
590
+ case
591
+ when @parsed[:parsing_position].match(/#{PARSER_MAPPING.map{ |k,v| v=='simple' ? k : nil }.compact.join("|")}/i)
592
+ parse_simple(line)
593
+ when @parsed[:parsing_position].match(/#{PARSER_MAPPING.map{ |k,v| v=='oneline' ? k : nil }.compact.join("|")}/i)
594
+ parse_oneline(line)
595
+ when @parsed[:parsing_position].match(/#{PARSER_MAPPING.map{ |k,v| v=='multiline' ? k : nil }.compact.join("|")}/i)
596
+ parse_multiline(line)
597
+ when @parsed[:parsing_position].match(/#{PARSER_MAPPING.map{ |k,v| v=='cfgshow' ? k : nil }.compact.join("|")}/i)
598
+ parse_cfgshow(line)
599
+ when @parsed[:parsing_position].match(/#{PARSER_MAPPING.map{ |k,v| v=='ns' ? k : nil }.compact.join("|")}/i)
600
+ parse_ns(line)
601
+ when @parsed[:parsing_position].match(/#{PARSER_MAPPING.map{ |k,v| v=='trunk' ? k : nil }.compact.join("|")}/i)
602
+ parse_trunk(line)
603
+ end
604
+ end
605
+
606
+ # parser used to parse commands with 1 line output
607
+ def parse_oneline(line)
608
+ @parsed[@parsed[:parsing_position].to_sym]=line
609
+ end
610
+
611
+ # parser used to parse commands with multi lines output
612
+ def parse_multiline(line)
613
+ # switchstatusshow
614
+ case
615
+ when line.match(/^\s*[a-z]+.*:/i)
616
+ arr = line.split(":")
617
+ @parsed[arr[0].strip.gsub(/\s+/,"_").gsub(/([a-z])([A-Z])/,'\1_\2').downcase.to_sym]=arr[1..-1].join(":").strip
618
+ when line.match(/^There is no outstanding/) && @parsed[:parsing_position]=="cfgtransshow"
619
+ @parsed[:cfg_transaction] = {:id=>-1, :abortable=>nil, :msg => "no transaction"}
620
+ when line.match(/^Current transaction token is (.+)$/) && @parsed[:parsing_position]=="cfgtransshow"
621
+ @parsed[:cfg_transaction][:id] = $1
622
+ when line.match(/It is(.+)abortable$/) && @parsed[:parsing_position]=="cfgtransshow"
623
+ @parsed[:cfg_transaction][:abortable] = $1.match(/not/) ? false : true
624
+ else
625
+ #supportshow
626
+ @parsed[@parsed[:parsing_position].to_sym]||=""
627
+ @parsed[@parsed[:parsing_position].to_sym]+=line+"\n"
628
+ end
629
+ end
630
+
631
+ # parser for multiline output where each line is independent
632
+ def parse_simple(line)
633
+ case
634
+ #extra handling
635
+ when line.match(/^zoning/)
636
+ @parsed[:zoning_enabled]=line.match(/:\s+ON/) ? true : false
637
+ @parsed[:active_config]=@parsed[:zoning_enabled] ? line.gsub(/(.*\()|(\).*)/,'') : nil
638
+
639
+ when line.match(/^LS Attributes/)
640
+ @parsed[:ls_attributes]||={}
641
+ sub_ls_attrs=line.gsub(/(.*\[)|(\].*)/,'').split(",")
642
+ sub_ls_attrs.each do |attr|
643
+ if attr.match(/Address Mode/)
644
+ @parsed[:ls_attributes][:address_mode]=attr.gsub(/Address Mode/,'').strip
645
+ else
646
+ key , value = attr.split(":").map {|a| a.strip.downcase}
647
+ @parsed[:ls_attributes][key.gsub(/\s+/,"_").to_sym]=value
648
+ end
649
+ end
650
+ # port line in director
651
+ when line.match(/\s?\d{1,3}\s+\d{1,2}\s+\d{1,2}\s+[\dabcdef-]{6}/i)
652
+ l=line.split
653
+ @parsed[:ports]||=[]
654
+ @parsed[:ports]<<{:index => l[0].to_i,:slot=>l[1].to_i, :port=>l[2].to_i, :address=>l[3].strip, :media => l[4].strip,
655
+ :speed=>l[5].strip, :state=>l[6].strip, :proto=>l[7].strip, :comment=>l[8..-1].join(" ")}
656
+ # port line in san blade
657
+ when line.match(/\s?\d{1,3}\s+\d{1,2}\s+[\dabcdef]{6}/i)
658
+ l=line.split
659
+ @parsed[:ports]||=[]
660
+ @parsed[:ports]<<{:index => l[0].to_i, :port=>l[1].to_i, :address=>l[2].strip, :media => l[3].strip,
661
+ :speed=>l[4].strip, :state=>l[5].strip, :proto=>l[6].strip, :comment=>l[7..-1].join(" ")}
662
+ when line.match(/^Index|^=/)
663
+ ""
664
+ when line.match(/Created switches/)
665
+ @parsed[:created_switches]=line.split(":")[1].strip.split(" ").map {|l1| l1.to_i}
666
+ #fabrics
667
+ when line.match(/^\s*\d+:\s[a-f0-9]{6}/i)
668
+ l=line.split(" ")
669
+ @parsed[:fabric]||=[]
670
+ @parsed[:fabric] << {:domain_id => l[0].gsub(/:/,"").strip, :sid => l[1].strip, :wwn=>l[2].strip, :eth_ip=>l[3].strip, :fc_ip=>l[4].strip, :name=>l[5].strip.gsub(/\"|>/,""), :local => l[5].strip.match(/^>/) ? true : false }
671
+ when line.match(/^(zone|alias|cfg)\./)
672
+ l=line.gsub!(/^(zone|alias|cfg)\./,"").split(":")
673
+ @parsed[:find_results] << {:obj=>l.shift,:members=>l.join(":"), :type => $1.to_sym}
674
+ #agshow
675
+ when line.match(/^([a-f0-9]{2}:){7}[a-f0-9]{2}\s+\d+/i) && @parsed[:parsing_position]=="agshow"
676
+ l=line.split(" ")
677
+ @parsed[:ag]||=[]
678
+ @parsed[:ag] << {:wwn => l[0].strip, :ports => l[1].to_i, :eth_ip=>l[2].strip, :version=>l[3].strip, :local => l[4].strip.match(/^local/) ? true : false, :name => l[5].strip }
679
+ #islshow
680
+ # 1: 0-> 0 10:00:00:05:33:23:86:00 1 H2C04R065-U03-A sp: 8.000G bw: 64.000G TRUNK QOS
681
+ when line.match(/\d+:\s*\d+->.+sp:/)
682
+ l_tricky, l_simple = line.split("->")
683
+ l_t = l_tricky.split(":")
684
+
685
+ l = l_simple.split(" ")
686
+ @parsed[:isl_links]||=[]
687
+ @parsed[:isl_links] << {:id => l_t[0].to_i,
688
+ :source_port_index => l_t[1].to_i,
689
+ :destination_port_index => l[0].to_i,
690
+ :destination_switch_wwn => l[1].strip,
691
+ :destination_switch_domain => l[2].to_i,
692
+ :destination_switch_name => l[3].strip,
693
+ :speed => l[5].to_i,
694
+ :bandwidth => l[7].to_i,
695
+ :trunk => l[8..-1].include?("TRUNK"),
696
+ :qos => l[8..-1].include?("QOS"),
697
+ :cr_recov => l[8..-1].include?("CR_RECOV")
698
+ }
699
+ #default handling if it doesno match specialized match
700
+ # parse lines formated like
701
+ # param: value
702
+ else
703
+ if line.match(/^\s*[a-z]+.*:/i)
704
+ arr = line.split(":")
705
+ @parsed[str_to_key(arr[0])]=arr[1..-1].join(":").strip
706
+ end
707
+ end
708
+ end
709
+
710
+ # parser dedicated to cfgshow format
711
+ def parse_cfgshow(line)
712
+ # once effective_configuration is loaded we ignore rest
713
+ return if @parsed[:effective_configuration] and !@parsed[:effective_configuration][:cfg].nil?
714
+
715
+ # we use array stack to point to the object being parsed
716
+ @parsed[:pointer]||=[]
717
+ @parsed[:pointer].push @parsed[:base] if @parsed[:pointer].empty?
718
+ @parsed[:key]||=[]
719
+ @parsed[:last_key]||=nil
720
+ @parsed[:was_popped]||=false
721
+
722
+ if (matches=line.match(/^\s*([a-z]+\s*[a-z]+):(.*)/i))
723
+ key=str_to_key(matches[1])
724
+ after_colon=matches[2].strip
725
+
726
+ # superkey -> defined_configuration, efective_configuration, cfg, zone, alias
727
+ if after_colon.empty?
728
+ @parsed[key]||={}
729
+ # we have new superkey so we pop the old from pointer stack
730
+ # and we push the new in
731
+ @parsed[:pointer].pop if !@parsed[:pointer].empty?
732
+ @parsed[:pointer].push @parsed[key]
733
+ # subkey
734
+ else
735
+ # sometimes the previous key does not have any members (or they are filtered out)
736
+ # in that case the key and pointer were not poped below so we pop them now
737
+ if @parsed[:last_key]==key && !@parsed[:was_popped]
738
+ @parsed[:pointer].pop
739
+ @parsed[:key].pop
740
+ end
741
+ # we define the last subkey as hash
742
+ # and push the array to pointer stack
743
+ @parsed[:pointer].last[key]||={}
744
+ @parsed[:pointer].push @parsed[:pointer].last[key]
745
+
746
+ # first value is name of the key
747
+ # 2nd is list of key members
748
+ value=after_colon.split(" ")[0].strip
749
+ members=after_colon.split(" ").length>1 ? after_colon.split(" ")[1..-1].join(" ") : ""
750
+
751
+ @parsed[:key].push value
752
+
753
+ # adding value into the current pointer
754
+ @parsed[:pointer].last[value]||=[]
755
+
756
+ # assign members into the last pointer array item
757
+ if !members.empty?
758
+ @parsed[:pointer].last[value]||=[]
759
+ members.split(";").each do |member|
760
+ @parsed[:pointer].last[value]<<member.strip if !member.match(/^\s*$/)
761
+ end
762
+ # if the line does not end with ; next line starts with new key or super key
763
+ # hence we remove last pointer from stack as we are done with it
764
+ if !line.strip.match(/;$/)
765
+ @parsed[:pointer].pop
766
+ @parsed[:key].pop
767
+ @parsed[:was_popped]=true
768
+ else
769
+ @parsed[:was_popped]=false
770
+ end
771
+ else
772
+ @parsed[:was_popped]=false
773
+ end
774
+ end
775
+ @parsed[:last_key]=key
776
+ # this line defines another members of last pointer key array item
777
+ elsif line.match(/^\t/)
778
+ @parsed[:last_key]=nil
779
+ # sometimes it is not defined yet
780
+ # we push the members in
781
+ @parsed[:pointer].last[@parsed[:key].last]||=[]
782
+ line.split(";").each do |member|
783
+ @parsed[:pointer].last[@parsed[:key].last]<<member.strip if !member.match(/^\s*$/)
784
+ end
785
+ # if the line does not end with ; next line starts with new key or super key
786
+ # hence we remove last pointer from stack as we are done with it
787
+ if !line.strip.match(/;$/)
788
+ @parsed[:pointer].pop
789
+ @parsed[:key].pop
790
+ @parsed[:was_popped]=true
791
+ else
792
+ @parsed[:was_popped]=false
793
+ end
794
+ end
795
+ true
796
+ end
797
+
798
+ # name server parser
799
+ def parse_ns(line)
800
+ @parsed[:domain]||=0
801
+ @parsed[:key] = @parsed[:parsing_position]=="nsshow" ? :wwn_local : :wwn_remote
802
+ @parsed[@parsed[:key]]||=[]
803
+
804
+ case
805
+
806
+ # changing domain id
807
+ when line.match(/Switch entry for/)
808
+ @parsed[:domain]=line.split(" ").last.to_i
809
+
810
+ # new WWN
811
+ when line.match(/(^\s+N |^\s+U )/)
812
+ @parsed[@parsed[:key]].push Hash.new
813
+ @parsed[@parsed[:key]].last[:value]=line.split(";")[2]
814
+ @parsed[@parsed[:key]].last[:domain_id]=@parsed[:domain]
815
+ @parsed[@parsed[:key]].last[:symbol]||=""
816
+
817
+ # symbol
818
+ when line.match(/(PortSymb|NodeSymb)/)
819
+ @parsed[@parsed[:key]].last[:symbol]=line.split(":")[1..-1].join(":").strip
820
+
821
+ # detype
822
+ when line.match(/(Device type)/)
823
+ @parsed[@parsed[:key]].last[:dev_type]=line.split(":")[1].strip
824
+ when line.match(/(Port Index)/)
825
+ @parsed[@parsed[:key]].last[:port_index]=line.split(":")[1].strip.to_i
826
+ end
827
+
828
+ end
829
+
830
+ # trunk parser
831
+ def parse_trunk(line)
832
+ return if line.match(/No trunking/i)
833
+ @parsed[:trunk_links]||=[]
834
+ l_tricky, l_simple = line.split("->")
835
+ l_t = l_tricky.split(":")
836
+
837
+ l = l_simple.split(" ")
838
+ case
839
+ when line.match(/\d+:\s*\d+->/)
840
+ @parsed[:trunk_links]<<{:id => l_t[0].to_i, :members => [
841
+ {
842
+ :source_port_index => l_t[1].to_i,
843
+ :destination_port_index => l[0].to_i,
844
+ :destination_switch_wwn => l[1].strip,
845
+ :destination_switch_domain => l[2].to_i,
846
+ :deskew => l[4].to_i,
847
+ :master => l[5]=="MASTER"
848
+ }]
849
+ }
850
+ else
851
+ @parsed[:trunk_links].last[:members]<<{
852
+ :source_port_index => l_t[0].to_i,
853
+ :destination_port_index => l[0].to_i,
854
+ :destination_switch_wwn => l[1].strip,
855
+ :destination_switch_domain => l[2].to_i,
856
+ :deskew => l[4].to_i,
857
+ :master => l[5]=="MASTER"
858
+ }
859
+ end
860
+ end
861
+
862
+ # transforamtino method to define parsed key based on the string
863
+ def str_to_key(str)
864
+ str.strip.gsub(/\s+/,"_").gsub(/([a-z])([A-Z])/,'\1_\2').downcase.to_sym
865
+ end
866
+ end
867
+ end
868
+
869
+ class Switch
870
+ # class extending SshDevice::Error
871
+ class Error < self::Error
872
+ WRONG_FORMAT="Error: Incorrect format"
873
+ def self.incorrect(str) #:nodoc:
874
+ self.new("#{WRONG_FORMAT} - #{str}")
875
+ end
876
+
877
+ def self.unknown
878
+ self.new("Unknown attribute")
879
+ end
880
+ end
881
+ end
882
+ end; end