vtysh 0.1.0 → 0.2.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 (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/vtysh/diff.rb +171 -270
  3. data/lib/vtysh/version.rb +1 -1
  4. metadata +3 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 379ae72d0d97bf2292c0d3b303799a44efcbf35d282628c5ab0ce3f7e49814be
4
- data.tar.gz: 87dfb517f5840af6c423ebad534865911598e54835bc1108825eccd0f66f4358
3
+ metadata.gz: f6a2ab5cedc801c0e0297a423aa742d190e356ebe1b1a5d182acb9931a2a6750
4
+ data.tar.gz: d4a2f50962fa26c2b1f0fc45aeb1b59fdf2c30ac2be809071fbf42f64a56c9f0
5
5
  SHA512:
6
- metadata.gz: ba2974e8ff1b4afa191d0d2fac607521d101829a573c2b855c23766cbb0c5e1d141d05296768c9287b6cfbd3e87b5054dc4f61c50dacfbf11752e9c19d6200e8
7
- data.tar.gz: dee8353a63b67879f848265ba70cb7d5f9becd1780e255d22ab78861a69f1611a94a888eb1cc0eb3e5880535448d2e97ef4d4fbb7d7ea74acf319586ab5aa1c8
6
+ metadata.gz: 7fc2560400cac17c55802b0e4c8d9641b6082578ca4644d9378a90781282b7fbec29c7a2d930b05f11c7970020f11b53ef9de2621c0df7d33cbb83cc029b90f0
7
+ data.tar.gz: ff3396f22ee51f40094f86e9de99b64bba9e4f338b13cd15545d703f7473d8566439d065aaa829ad325a061e7e161cbec87b10dd383474b152fe5c04812269bd
data/lib/vtysh/diff.rb CHANGED
@@ -1,324 +1,225 @@
1
1
  module Vtysh
2
2
  class Diff
3
3
  def self.commands(source, target)
4
- # Parse configurations into flat commands
5
4
  source_cmds = parse_config(source)
6
5
  target_cmds = parse_config(target)
7
-
8
- # Generate commands to transform source to target
9
- commands = []
10
-
11
- # Special case for router-id changes
6
+
12
7
  if needs_bgp_recreation?(source_cmds, target_cmds)
13
- commands.concat(handle_bgp_recreation(source_cmds, target_cmds))
8
+ bgp_commands = handle_bgp_recreation(source_cmds, target_cmds)
9
+ non_bgp_commands = handle_non_bgp_changes(source_cmds, target_cmds, skip_bgp_blocks: true)
10
+ bgp_commands + non_bgp_commands
14
11
  else
15
- # Handle removals first (items in source but not in target)
16
- (source_cmds - target_cmds).each do |cmd|
17
- if is_block_command(cmd[:command])
18
- # For block commands like "router bgp X", we need to remove the whole block
19
- commands << format_removal_command(cmd)
20
-
21
- # Skip commands inside this block - they'll be removed automatically
22
- if cmd[:command].start_with?("router bgp")
23
- # We need to specifically exclude commands inside this removed BGP block
24
- # to avoid generating individual "no" commands for them
25
- asn = cmd[:command].split[2]
26
- # Don't generate commands for items in this block
27
- end
28
- elsif cmd[:depth] > 0
29
- # Check if we're removing a command inside a block that's already being removed
30
- block_being_removed = false
31
- cmd[:context].each do |ctx|
32
- # Check if any of the contexts is a block that's being removed
33
- if source_cmds.any? { |s| s[:command] == ctx && (source_cmds - target_cmds).include?(s) }
34
- block_being_removed = true
35
- break
36
- end
37
- end
38
-
39
- # Only generate a command if not inside a block that's being removed
40
- unless block_being_removed
41
- # For commands inside blocks, we need to provide the proper context
42
- commands << format_context_command("no #{cmd[:command]}", cmd[:context])
43
- end
44
- else
45
- # For top-level commands
46
- commands << "vtysh -c \"configure\" -c \"no #{cmd[:command]}\""
47
- end
48
- end
49
-
50
- # Handle additions (items in target but not in source)
51
- (target_cmds - source_cmds).each do |cmd|
52
- if is_block_command(cmd[:command])
53
- # For block commands like "router bgp X", include the block creation
54
- commands << "vtysh -c \"configure\" -c \"#{cmd[:command]}\""
55
- elsif cmd[:depth] > 0
56
- # For commands inside blocks, we need to provide the proper context
57
- commands << format_context_command(cmd[:command], cmd[:context])
58
- else
59
- # For top-level commands
60
- commands << "vtysh -c \"configure\" -c \"#{cmd[:command]}\""
61
- end
62
- end
12
+ handle_non_bgp_changes(source_cmds, target_cmds) +
13
+ handle_incremental_bgp_changes(source_cmds, target_cmds)
63
14
  end
64
-
65
- # Sort commands and apply dependency-based reordering
66
- reorder_commands(commands.uniq)
67
15
  end
68
-
16
+
69
17
  private
70
-
71
- # New improved command ordering function that ensures the proper order for BGP-related commands
72
- def self.reorder_commands(commands)
73
- # First extract all the commands that need to be properly ordered
74
- all_peer_group_cmds = commands.select { |cmd| cmd.include?("neighbor") && cmd.include?("peer-group") && !cmd.include?("no ") }
75
- all_remote_as_cmds = commands.select { |cmd| cmd.include?("remote-as") }
76
- all_listen_range_cmds = commands.select { |cmd| cmd.include?("bgp listen range") }
77
-
78
- # Need to remove these from commands for proper reordering
79
- clean_commands = commands - all_peer_group_cmds - all_remote_as_cmds - all_listen_range_cmds
80
-
81
- # Apply correct ordering for all commands
82
- final_commands = []
83
-
84
- # Handle prefix-lists first (always)
85
- prefix_list_cmds = clean_commands.select { |cmd| cmd.include?("ip prefix-list") }
86
- final_commands.concat(prefix_list_cmds)
87
-
88
- # Handle route-map definitions second
89
- route_map_cmds = clean_commands.select { |cmd| cmd.include?("route-map") && !cmd.include?("neighbor") && !cmd.include?("no ") }
90
- final_commands.concat(route_map_cmds)
91
-
92
- # Include router bgp blocks
93
- bgp_block_cmds = clean_commands.select { |cmd| cmd.include?("router bgp") && !cmd.include?("no ") && cmd.count("\"") <= 6 }
94
- final_commands.concat(bgp_block_cmds)
95
-
96
- # Now add peer-group commands first
97
- all_peer_group_cmds.sort.each do |cmd|
98
- final_commands << cmd
99
- end
100
-
101
- # Then add remote-as commands
102
- all_remote_as_cmds.sort.each do |cmd|
103
- final_commands << cmd
18
+
19
+ def self.handle_non_bgp_changes(source_cmds, target_cmds, skip_bgp_blocks: false)
20
+ commands = []
21
+
22
+ # Removals (non-BGP, plus top-level BGP block removals when not recreating)
23
+ (source_cmds - target_cmds).each do |cmd|
24
+ next if inside_bgp?(cmd) && !cmd[:command].start_with?("router bgp")
25
+ next if skip_bgp_blocks && cmd[:command].start_with?("router bgp")
26
+ if is_block_command(cmd[:command])
27
+ commands << format_removal_command(cmd)
28
+ elsif cmd[:depth] > 0
29
+ next if block_being_removed?(cmd, source_cmds, target_cmds)
30
+ commands << format_context_command("no #{cmd[:command]}", cmd[:context])
31
+ else
32
+ commands << vtysh_cmd("no #{cmd[:command]}")
33
+ end
104
34
  end
105
-
106
- # Then add listen-range commands
107
- all_listen_range_cmds.sort.each do |cmd|
108
- final_commands << cmd
35
+
36
+ # Non-BGP additions
37
+ (target_cmds - source_cmds).each do |cmd|
38
+ next if inside_bgp?(cmd)
39
+ if is_block_command(cmd[:command])
40
+ commands << vtysh_cmd(cmd[:command])
41
+ elsif cmd[:depth] > 0
42
+ commands << format_context_command(cmd[:command], cmd[:context])
43
+ else
44
+ commands << vtysh_cmd(cmd[:command])
45
+ end
109
46
  end
110
-
111
- # Add all remaining commands except removals
112
- remaining_cmds = clean_commands.select { |cmd| !cmd.include?("no ") }
113
- remaining_cmds -= final_commands
114
- final_commands.concat(remaining_cmds)
115
-
116
- # Add removal commands at the end
117
- removal_cmds = clean_commands.select { |cmd| cmd.include?("no ") }
118
- final_commands.concat(removal_cmds)
119
-
120
- # Return unique commands with duplicates removed
121
- final_commands.uniq
47
+
48
+ reorder_non_bgp(commands)
122
49
  end
123
-
124
- def self.needs_bgp_recreation?(source_cmds, target_cmds)
125
- # Get BGP ASNs from both configs
126
- source_bgp = source_cmds.select { |cmd| cmd[:command].start_with?("router bgp") }
127
- target_bgp = target_cmds.select { |cmd| cmd[:command].start_with?("router bgp") }
128
-
129
- # Check if we have the same ASN in both
130
- source_asns = source_bgp.map { |cmd| cmd[:command].split[2] }
131
- target_asns = target_bgp.map { |cmd| cmd[:command].split[2] }
132
-
133
- # If ASNs don't match, no need for special handling
134
- return false if source_asns != target_asns || source_asns.empty?
135
-
136
- # For each ASN that appears in both, check for router-id changes
137
- source_asns.each do |asn|
138
- # Find router-id in source
139
- src_bgp_cmds = source_cmds.select { |cmd|
140
- cmd[:depth] > 0 &&
141
- cmd[:context].any? { |ctx| ctx.start_with?("router bgp #{asn}") } &&
142
- cmd[:command].include?("bgp router-id")
143
- }
144
-
145
- # Find router-id in target
146
- tgt_bgp_cmds = target_cmds.select { |cmd|
147
- cmd[:depth] > 0 &&
148
- cmd[:context].any? { |ctx| ctx.start_with?("router bgp #{asn}") } &&
149
- cmd[:command].include?("bgp router-id")
150
- }
151
-
152
- # If both have router-id and they're different, return true
153
- if !src_bgp_cmds.empty? && !tgt_bgp_cmds.empty? &&
154
- src_bgp_cmds.first[:command] != tgt_bgp_cmds.first[:command]
155
- return true
156
- end
50
+
51
+ def self.handle_incremental_bgp_changes(source_cmds, target_cmds)
52
+ commands = []
53
+
54
+ # BGP removals
55
+ (source_cmds - target_cmds).each do |cmd|
56
+ next unless inside_bgp?(cmd)
57
+ next if cmd[:command].start_with?("router bgp") # don't remove the block itself
58
+ next if block_being_removed?(cmd, source_cmds, target_cmds)
59
+ commands << format_context_command("no #{cmd[:command]}", cmd[:context])
60
+ end
61
+
62
+ # BGP additions
63
+ (target_cmds - source_cmds).each do |cmd|
64
+ next unless inside_bgp?(cmd)
65
+ next if cmd[:command].start_with?("router bgp")
66
+ commands << format_context_command(cmd[:command], cmd[:context])
157
67
  end
158
-
159
- false
68
+
69
+ reorder_bgp(commands)
160
70
  end
161
-
71
+
162
72
  def self.handle_bgp_recreation(source_cmds, target_cmds)
73
+ source_asns = source_cmds.select { |c| c[:command].start_with?("router bgp") }.map { |c| c[:command].split[2] }
74
+ target_asns = target_cmds.select { |c| c[:command].start_with?("router bgp") }.map { |c| c[:command].split[2] }
75
+
163
76
  commands = []
164
-
165
- # Get BGP ASNs
166
- source_bgp = source_cmds.select { |cmd| cmd[:command].start_with?("router bgp") }
167
- target_bgp = target_cmds.select { |cmd| cmd[:command].start_with?("router bgp") }
168
-
169
- source_asns = source_bgp.map { |cmd| cmd[:command].split[2] }
170
- target_asns = target_bgp.map { |cmd| cmd[:command].split[2] }
171
-
172
- # For each ASN, recreate the BGP block
77
+
173
78
  (source_asns & target_asns).each do |asn|
174
- # Remove the BGP block
175
- commands << "vtysh -c \"configure\" -c \"no router bgp #{asn}\""
176
-
177
- # Get the commands for this BGP block in the target config
178
- target_bgp_block = target_cmds.select { |cmd|
179
- cmd[:depth] > 0 && cmd[:context].any? { |ctx| ctx.start_with?("router bgp #{asn}") }
79
+ bgp_ctx = "router bgp #{asn}"
80
+
81
+ # Step 1: Remove the old BGP block
82
+ commands << vtysh_cmd("no #{bgp_ctx}")
83
+
84
+ # Step 2: Create the new BGP block
85
+ commands << vtysh_cmd(bgp_ctx)
86
+
87
+ # Step 3: Add main BGP commands (not inside address-family)
88
+ target_bgp = target_cmds.select { |c|
89
+ c[:depth] > 0 &&
90
+ c[:context].include?(bgp_ctx) &&
91
+ !c[:context].any? { |ctx| ctx.start_with?("address-family") } &&
92
+ !is_block_command(c[:command]) # skip block commands (router bgp, address-family)
180
93
  }
181
-
182
- # Add the BGP block itself
183
- commands << "vtysh -c \"configure\" -c \"router bgp #{asn}\""
184
-
185
- # Add commands within the BGP block
186
- bgp_main_cmds = target_bgp_block.select { |cmd|
187
- !cmd[:context].any? { |ctx| ctx.start_with?("address-family") }
188
- }
189
-
190
- # Sort commands - router-id first, then peer-groups, then remote-as, etc.
191
- bgp_main_cmds.sort_by! { |cmd| bgp_command_priority(cmd[:command]) }
192
-
193
- # Add the main BGP commands
194
- bgp_main_cmds.each do |cmd|
195
- commands << "vtysh -c \"configure\" -c \"router bgp #{asn}\" -c \"#{cmd[:command]}\""
94
+
95
+ target_bgp.sort_by { |c| bgp_command_priority(c[:command]) }.each do |cmd|
96
+ commands << vtysh_cmd(bgp_ctx, cmd[:command])
196
97
  end
197
-
198
- # Add address-family blocks and their commands
199
- af_contexts = target_bgp_block.map { |cmd|
200
- cmd[:context].find { |ctx| ctx.start_with?("address-family") }
201
- }.compact.uniq
202
-
203
- af_contexts.each do |af_ctx|
204
- # Add the address-family context
205
- commands << "vtysh -c \"configure\" -c \"router bgp #{asn}\" -c \"#{af_ctx}\""
206
-
207
- # Add commands in this address-family
208
- af_cmds = target_bgp_block.select { |cmd| cmd[:context].include?(af_ctx) }
98
+
99
+ # Step 4: Add address-family blocks
100
+ af_contexts = target_cmds.select { |c|
101
+ c[:command].start_with?("address-family") && c[:context].include?(bgp_ctx)
102
+ }.map { |c| c[:command] }.uniq
103
+
104
+ af_contexts.each do |af|
105
+ commands << vtysh_cmd(bgp_ctx, af)
106
+
107
+ af_cmds = target_cmds.select { |c|
108
+ c[:depth] > 0 &&
109
+ c[:context].include?(bgp_ctx) &&
110
+ c[:context].include?(af) &&
111
+ c[:command] != af # skip the address-family command itself
112
+ }
113
+
209
114
  af_cmds.each do |cmd|
210
- commands << "vtysh -c \"configure\" -c \"router bgp #{asn}\" -c \"#{af_ctx}\" -c \"#{cmd[:command]}\""
115
+ commands << vtysh_cmd(bgp_ctx, af, cmd[:command])
211
116
  end
212
117
  end
213
118
  end
214
-
119
+
215
120
  commands
216
121
  end
217
-
122
+
123
+ # --- Helpers ---
124
+
125
+ def self.inside_bgp?(cmd)
126
+ cmd[:context].any? { |ctx| ctx.start_with?("router bgp") } ||
127
+ cmd[:command].start_with?("router bgp")
128
+ end
129
+
130
+ def self.block_being_removed?(cmd, source_cmds, target_cmds)
131
+ removals = source_cmds - target_cmds
132
+ cmd[:context].any? { |ctx|
133
+ is_block_command(ctx) && removals.any? { |r| r[:command] == ctx }
134
+ }
135
+ end
136
+
137
+ def self.needs_bgp_recreation?(source_cmds, target_cmds)
138
+ source_asns = source_cmds.select { |c| c[:command].start_with?("router bgp") }.map { |c| c[:command].split[2] }
139
+ target_asns = target_cmds.select { |c| c[:command].start_with?("router bgp") }.map { |c| c[:command].split[2] }
140
+
141
+ return false if source_asns != target_asns || source_asns.empty?
142
+
143
+ source_asns.any? do |asn|
144
+ src_rid = source_cmds.find { |c| c[:command].include?("bgp router-id") && c[:context].any? { |ctx| ctx.include?("router bgp #{asn}") } }
145
+ tgt_rid = target_cmds.find { |c| c[:command].include?("bgp router-id") && c[:context].any? { |ctx| ctx.include?("router bgp #{asn}") } }
146
+ src_rid && tgt_rid && src_rid[:command] != tgt_rid[:command]
147
+ end
148
+ end
149
+
218
150
  def self.bgp_command_priority(cmd)
219
- # Define priority for BGP commands (lower number = higher priority)
220
- if cmd.include?('bgp router-id')
221
- return 1 # router-id should be first
222
- elsif cmd.include?('neighbor') && cmd.include?('peer-group') && !cmd.include?(' route-map ')
223
- return 2 # peer-group definitions second
224
- elsif cmd.include?('remote-as')
225
- return 3 # remote-as commands next
226
- elsif cmd.include?('bgp listen range')
227
- return 4 # bgp listen range commands after
228
- else
229
- return 5 # other commands last
151
+ case
152
+ when cmd.include?('bgp router-id') then 1
153
+ when cmd =~ /neighbor \S+ peer-group$/ then 2
154
+ when cmd.include?('remote-as') then 3
155
+ when cmd =~ /neighbor \S+ local-as/ then 4
156
+ when cmd.include?('bgp listen range') then 5
157
+ else 6
230
158
  end
231
159
  end
232
-
160
+
161
+ def self.reorder_non_bgp(commands)
162
+ prefix_lists = commands.select { |c| c.include?("ip prefix-list") }
163
+ route_maps = commands.select { |c| c.include?("route-map") && !c.include?("no ") }
164
+ removals = commands.select { |c| c.include?("no ") } - prefix_lists
165
+ rest = commands - prefix_lists - route_maps - removals
166
+
167
+ (prefix_lists + route_maps + rest + removals).uniq
168
+ end
169
+
170
+ def self.reorder_bgp(commands)
171
+ peer_groups = commands.select { |c| c =~ /neighbor \S+ peer-group"$/ && !c.include?("no ") }
172
+ remote_as = commands.select { |c| c.include?("remote-as") && !c.include?("no ") }
173
+ listen_range = commands.select { |c| c.include?("bgp listen range") && !c.include?("no ") }
174
+ removals = commands.select { |c| c.include?("no ") }
175
+ rest = commands - peer_groups - remote_as - listen_range - removals
176
+
177
+ (peer_groups + remote_as + listen_range + rest + removals).uniq
178
+ end
179
+
180
+ # --- Parsing ---
181
+
233
182
  def self.parse_config(config)
234
- lines = config.split("\n").map(&:strip)
235
183
  commands = []
236
184
  context_stack = []
237
-
238
- lines.each do |line|
185
+
186
+ config.each_line do |line|
187
+ line = line.strip
239
188
  next if line.empty? || line.start_with?('#', '!')
240
-
241
- if line == 'exit' || line == 'exit-vrf' || line == 'exit-address-family'
242
- # Exit the current context
189
+
190
+ if line =~ /^exit(-address-family|-vrf)?$/
243
191
  context_stack.pop unless context_stack.empty?
244
192
  elsif line == 'end'
245
- # Exit all contexts
246
193
  context_stack = []
247
194
  elsif is_block_command(line)
248
- # Start a new context block
249
195
  context_stack << line
250
196
  commands << { command: line, context: context_stack.dup, depth: context_stack.size }
251
197
  else
252
- # Regular command, may be inside a context
253
198
  commands << { command: line, context: context_stack.dup, depth: context_stack.size }
254
199
  end
255
200
  end
256
-
201
+
257
202
  commands
258
203
  end
259
-
204
+
260
205
  def self.is_block_command(cmd)
261
206
  cmd.start_with?('router ', 'interface ', 'route-map ', 'vrf ', 'address-family ')
262
207
  end
263
-
264
- def self.needs_exit_command(cmd)
265
- cmd.start_with?('router ', 'interface ', 'vrf ')
208
+
209
+ # --- Formatting ---
210
+
211
+ def self.vtysh_cmd(*parts)
212
+ args = ["configure"] + parts
213
+ "vtysh " + args.map { |p| "-c \"#{p}\"" }.join(" ")
266
214
  end
267
-
215
+
268
216
  def self.format_removal_command(cmd)
269
- command = cmd[:command]
270
-
271
- # For block commands with nested structures
272
- if command.start_with?('router bgp')
273
- asn = command.split[2]
274
- return "vtysh -c \"configure\" -c \"no router bgp #{asn}\""
275
- elsif command.start_with?('interface')
276
- iface = command.split[1]
277
- return "vtysh -c \"configure\" -c \"no interface #{iface}\""
278
- elsif command.start_with?('vrf')
279
- vrf_name = command.split[1]
280
- return "vtysh -c \"configure\" -c \"no vrf #{vrf_name}\""
281
- elsif command.start_with?('route-map')
282
- parts = command.split
283
- if parts.size >= 4
284
- return "vtysh -c \"configure\" -c \"no route-map #{parts[1]} #{parts[2]} #{parts[3]}\""
285
- else
286
- return "vtysh -c \"configure\" -c \"no #{command}\""
287
- end
288
- else
289
- # Generic block removal
290
- return "vtysh -c \"configure\" -c \"no #{command}\""
291
- end
217
+ vtysh_cmd("no #{cmd[:command]}")
292
218
  end
293
-
219
+
294
220
  def self.format_context_command(command, context)
295
- result = "vtysh -c \"configure\""
296
-
297
- # Add each context element
298
- context.each do |ctx|
299
- if ctx.start_with?('router bgp')
300
- asn = ctx.split[2]
301
- result += " -c \"router bgp #{asn}\""
302
- elsif ctx.start_with?('interface')
303
- iface = ctx.split[1]
304
- result += " -c \"interface #{iface}\""
305
- elsif ctx.start_with?('address-family')
306
- parts = ctx.split
307
- if parts.size > 2
308
- result += " -c \"address-family #{parts[1]} #{parts[2]}\""
309
- else
310
- result += " -c \"address-family #{parts[1]}\""
311
- end
312
- else
313
- # Generic context
314
- result += " -c \"#{ctx}\""
315
- end
316
- end
317
-
318
- # Add the command itself
319
- result += " -c \"#{command}\""
320
-
321
- result
221
+ parts = context.map { |ctx| ctx }
222
+ vtysh_cmd(*parts, command)
322
223
  end
323
224
  end
324
- end
225
+ end
data/lib/vtysh/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Vtysh
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vtysh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-04-03 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: A gem that accepts two vtysh configuration states and returns the commands
14
13
  needed to transition from one to the other
@@ -28,7 +27,6 @@ homepage: http://github.com/usiegj00/vtysh-gem
28
27
  licenses:
29
28
  - MIT
30
29
  metadata: {}
31
- post_install_message:
32
30
  rdoc_options: []
33
31
  require_paths:
34
32
  - lib
@@ -43,8 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
41
  - !ruby/object:Gem::Version
44
42
  version: '0'
45
43
  requirements: []
46
- rubygems_version: 3.5.16
47
- signing_key:
44
+ rubygems_version: 3.6.9
48
45
  specification_version: 4
49
46
  summary: Handles SONiC vtysh commandfile format
50
47
  test_files: []