vtysh 0.1.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.
- checksums.yaml +7 -0
- data/README.md +71 -0
- data/bin/vtysh-transform +31 -0
- data/lib/vtysh/diff.rb +324 -0
- data/lib/vtysh/version.rb +3 -0
- data/lib/vtysh.rb +6 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 379ae72d0d97bf2292c0d3b303799a44efcbf35d282628c5ab0ce3f7e49814be
|
4
|
+
data.tar.gz: 87dfb517f5840af6c423ebad534865911598e54835bc1108825eccd0f66f4358
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba2974e8ff1b4afa191d0d2fac607521d101829a573c2b855c23766cbb0c5e1d141d05296768c9287b6cfbd3e87b5054dc4f61c50dacfbf11752e9c19d6200e8
|
7
|
+
data.tar.gz: dee8353a63b67879f848265ba70cb7d5f9becd1780e255d22ab78861a69f1611a94a888eb1cc0eb3e5880535448d2e97ef4d4fbb7d7ea74acf319586ab5aa1c8
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# Vtysh
|
2
|
+
|
3
|
+
A Ruby gem for handling SONiC vtysh configuration diffs. It accepts two configuration states and returns the commands needed to transition from one to the other.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem 'vtysh'
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require 'vtysh'
|
15
|
+
|
16
|
+
source_config = File.read('a.vtysh')
|
17
|
+
target_config = File.read('b.vtysh')
|
18
|
+
|
19
|
+
commands = Vtysh::Diff.commands(source_config, target_config)
|
20
|
+
|
21
|
+
puts commands
|
22
|
+
```
|
23
|
+
|
24
|
+
This will output commands with proper hierarchy preserved:
|
25
|
+
|
26
|
+
```
|
27
|
+
# BGP blocks are fully recreated
|
28
|
+
vtysh -c "configure" -c "no router bgp 65001"
|
29
|
+
vtysh -c "configure" -c "router bgp 65001" -c "bgp router-id 192.168.1.1" -c "neighbor 192.168.1.2 remote-as 65002" -c "exit"
|
30
|
+
|
31
|
+
# Individual commands for non-BGP configuration
|
32
|
+
vtysh -c "configure" -c "interface Ethernet0 description Primary"
|
33
|
+
```
|
34
|
+
|
35
|
+
## Features
|
36
|
+
|
37
|
+
- Generates hierarchical vtysh commands that maintain proper block structure
|
38
|
+
- Properly handles configuration blocks like `router bgp` and `address-family`
|
39
|
+
- Removes and recreates entire BGP blocks for clean configuration
|
40
|
+
- Route-map match commands are split into separate arguments
|
41
|
+
- Adds appropriate exit commands when needed
|
42
|
+
- Correctly removes entire configuration blocks with `no` prefix
|
43
|
+
- Includes `configure` terminal context as required by SONiC vtysh
|
44
|
+
- Ensures `neighbor peer-group` commands come before `bgp listen range` commands
|
45
|
+
- Ensures `neighbor remote-as` commands are processed before other neighbor attributes
|
46
|
+
|
47
|
+
## Command Ordering
|
48
|
+
|
49
|
+
The gem pays special attention to command ordering to meet SONiC vtysh requirements:
|
50
|
+
|
51
|
+
1. Peer-group definitions come before `bgp listen range` commands referencing them
|
52
|
+
2. For neighbors, `remote-as` commands always come before other attributes like route-maps, descriptions, etc.
|
53
|
+
3. When adding new neighbors, the ASN is defined first via the `remote-as` command
|
54
|
+
4. This prevents common errors like `% Specify remote-as or peer-group commands first`
|
55
|
+
|
56
|
+
## Block-Based Processing
|
57
|
+
|
58
|
+
The gem uses a block-based approach where:
|
59
|
+
|
60
|
+
1. BGP blocks are fully removed and recreated with clean settings
|
61
|
+
2. Route-map blocks handle match commands as separate arguments
|
62
|
+
3. Interface and other blocks are processed with proper context
|
63
|
+
|
64
|
+
This ensures:
|
65
|
+
- Complete configuration with proper command ordering
|
66
|
+
- Clean setup of complex structures like BGP routers
|
67
|
+
- Proper handling of nested contexts like address-family blocks
|
68
|
+
|
69
|
+
## License
|
70
|
+
|
71
|
+
The gem is available as open source under the terms of the MIT License.
|
data/bin/vtysh-transform
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'vtysh'
|
4
|
+
|
5
|
+
if ARGV.size != 2
|
6
|
+
puts "Usage: vtysh-transform SOURCE_FILE TARGET_FILE"
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
|
10
|
+
source_file = ARGV[0]
|
11
|
+
target_file = ARGV[1]
|
12
|
+
|
13
|
+
# Ensure files exist
|
14
|
+
unless File.exist?(source_file)
|
15
|
+
puts "Error: Source file #{source_file} does not exist."
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
|
19
|
+
unless File.exist?(target_file)
|
20
|
+
puts "Error: Target file #{target_file} does not exist."
|
21
|
+
exit 1
|
22
|
+
end
|
23
|
+
|
24
|
+
# Read configurations
|
25
|
+
source_config = File.read(source_file)
|
26
|
+
target_config = File.read(target_file)
|
27
|
+
|
28
|
+
# Generate and print commands
|
29
|
+
Vtysh::Diff.commands(source_config, target_config).each do |cmd|
|
30
|
+
puts cmd
|
31
|
+
end
|
data/lib/vtysh/diff.rb
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
module Vtysh
|
2
|
+
class Diff
|
3
|
+
def self.commands(source, target)
|
4
|
+
# Parse configurations into flat commands
|
5
|
+
source_cmds = parse_config(source)
|
6
|
+
target_cmds = parse_config(target)
|
7
|
+
|
8
|
+
# Generate commands to transform source to target
|
9
|
+
commands = []
|
10
|
+
|
11
|
+
# Special case for router-id changes
|
12
|
+
if needs_bgp_recreation?(source_cmds, target_cmds)
|
13
|
+
commands.concat(handle_bgp_recreation(source_cmds, target_cmds))
|
14
|
+
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
|
63
|
+
end
|
64
|
+
|
65
|
+
# Sort commands and apply dependency-based reordering
|
66
|
+
reorder_commands(commands.uniq)
|
67
|
+
end
|
68
|
+
|
69
|
+
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
|
104
|
+
end
|
105
|
+
|
106
|
+
# Then add listen-range commands
|
107
|
+
all_listen_range_cmds.sort.each do |cmd|
|
108
|
+
final_commands << cmd
|
109
|
+
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
|
122
|
+
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
|
157
|
+
end
|
158
|
+
|
159
|
+
false
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.handle_bgp_recreation(source_cmds, target_cmds)
|
163
|
+
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
|
173
|
+
(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}") }
|
180
|
+
}
|
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]}\""
|
196
|
+
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) }
|
209
|
+
af_cmds.each do |cmd|
|
210
|
+
commands << "vtysh -c \"configure\" -c \"router bgp #{asn}\" -c \"#{af_ctx}\" -c \"#{cmd[:command]}\""
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
commands
|
216
|
+
end
|
217
|
+
|
218
|
+
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
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.parse_config(config)
|
234
|
+
lines = config.split("\n").map(&:strip)
|
235
|
+
commands = []
|
236
|
+
context_stack = []
|
237
|
+
|
238
|
+
lines.each do |line|
|
239
|
+
next if line.empty? || line.start_with?('#', '!')
|
240
|
+
|
241
|
+
if line == 'exit' || line == 'exit-vrf' || line == 'exit-address-family'
|
242
|
+
# Exit the current context
|
243
|
+
context_stack.pop unless context_stack.empty?
|
244
|
+
elsif line == 'end'
|
245
|
+
# Exit all contexts
|
246
|
+
context_stack = []
|
247
|
+
elsif is_block_command(line)
|
248
|
+
# Start a new context block
|
249
|
+
context_stack << line
|
250
|
+
commands << { command: line, context: context_stack.dup, depth: context_stack.size }
|
251
|
+
else
|
252
|
+
# Regular command, may be inside a context
|
253
|
+
commands << { command: line, context: context_stack.dup, depth: context_stack.size }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
commands
|
258
|
+
end
|
259
|
+
|
260
|
+
def self.is_block_command(cmd)
|
261
|
+
cmd.start_with?('router ', 'interface ', 'route-map ', 'vrf ', 'address-family ')
|
262
|
+
end
|
263
|
+
|
264
|
+
def self.needs_exit_command(cmd)
|
265
|
+
cmd.start_with?('router ', 'interface ', 'vrf ')
|
266
|
+
end
|
267
|
+
|
268
|
+
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
|
292
|
+
end
|
293
|
+
|
294
|
+
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
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
data/lib/vtysh.rb
ADDED
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vtysh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan Siegel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-03 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A gem that accepts two vtysh configuration states and returns the commands
|
14
|
+
needed to transition from one to the other
|
15
|
+
email:
|
16
|
+
- "<248302+usiegj00@users.noreply.github.com>"
|
17
|
+
executables:
|
18
|
+
- vtysh-transform
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- README.md
|
23
|
+
- bin/vtysh-transform
|
24
|
+
- lib/vtysh.rb
|
25
|
+
- lib/vtysh/diff.rb
|
26
|
+
- lib/vtysh/version.rb
|
27
|
+
homepage: http://github.com/usiegj00/vtysh-gem
|
28
|
+
licenses:
|
29
|
+
- MIT
|
30
|
+
metadata: {}
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 2.6.0
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubygems_version: 3.5.16
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: Handles SONiC vtysh commandfile format
|
50
|
+
test_files: []
|