netconfgen 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e74877df80cf028688b27e0712cc3d56b15caa44
4
+ data.tar.gz: 37c95750d391b9f41217cd724f1e08e8e21f5184
5
+ SHA512:
6
+ metadata.gz: 12e26b6ae98233cca7b0534c3affcf99405410c02adb99cf60ea167feba85865e6d294c182709e036ccdd79c342688f2fe4ec19f2b9a84c65c29310eef39616c
7
+ data.tar.gz: e334f1c2070bde07d19a111362fc19ceb87a9778c63da993524ed5d2d5d5cebdbf8d5d8f304ddbed051f155203250b34893c52958895c9ea0c4585601535a8b1
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /vendor/
3
+ /.ruby-version
4
+ /.yardoc
5
+ /Gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fx-tftpd.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'rake/testtask'
2
+
3
+ begin
4
+ require 'rubygems/tasks'
5
+ Gem::Tasks.new
6
+ rescue LoadError => e
7
+ warn e.message
8
+ end
9
+
10
+ begin
11
+ require 'yard'
12
+ YARD::Rake::YardocTask.new
13
+ task :doc => :yard
14
+ rescue LoadError => e
15
+ warn e.message
16
+ end
17
+
18
+ task :default => :test
19
+
20
+ Rake::TestTask.new do |t|
21
+ t.libs = ['lib', 'test']
22
+ t.name = 'test'
23
+ t.warning = true
24
+ t.test_files = FileList['test/*.rb']
25
+ end
26
+
27
+ FileList['test/*.rb'].each do |p|
28
+ name = p.split('/').last.split('.').first
29
+ Rake::TestTask.new do |t|
30
+ t.libs = ['lib', 'test']
31
+ t.name = "test:#{name}"
32
+ t.warning = true
33
+ t.test_files = [p]
34
+ end
35
+ end
data/bin/confgen ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'logger'
6
+ require 'optparse'
7
+ require 'netconfgen'
8
+ require 'pp'
9
+ require 'yaml'
10
+ require 'json'
11
+
12
+ CONFIG_FILE = ["/etc/netconfgen.yaml", "~/.netconfgen.yaml", "netconfgen.yaml"]
13
+ config = {}
14
+ CONFIG_FILE.each do |fname|
15
+ fname = File.expand_path(fname)
16
+ if File.exists?(fname)
17
+ config = YAML::load_file(fname)
18
+ config = config.each_with_object({}) { |(k,v),memo| memo[k.to_sym] = v }
19
+ break
20
+ end
21
+ end
22
+
23
+ config[:variables] ||= {}
24
+ config[:root] ||= 'test/data'
25
+
26
+ # Defaults
27
+ op = OptionParser.new do |o|
28
+ o.banner = "Usage: #{$PROGRAM_NAME} [OPTIONS] MAIN_BLOCK_NAME"
29
+ o.on('-l', '--listen', 'Run a listening server instead') do
30
+ config[:listen] = true
31
+ end
32
+ o.on('-o [OPTION]', 'Pass a template variable') do |arg|
33
+ if m = arg.match(/(.+?)=(.+)/)
34
+ config[:variables][m[1]] = m[2]
35
+ end
36
+ end
37
+ o.on('--json=[VARIABLE=FILE]', 'Load json into a variable') do |arg|
38
+ if m = arg.match(/(.+?)=(.+)/)
39
+ data = File.read(m[2])
40
+ config[:variables][m[1]] = JSON.parse(data)
41
+ end
42
+ end
43
+ o.on('-a', '--address ADDRESS', String,
44
+ 'Address to listen on (default: 0.0.0.0)') do |address|
45
+ config[:address] = address
46
+ end
47
+ o.on('-h', '--http PORT', Integer,
48
+ 'Port to listen on for http (default: 8080)') do |port|
49
+ abort 'Invalid port' if port < 1 || port > 65535
50
+ config[:http] = http
51
+ end
52
+ o.on('-v', '--verbose', 'Enable verbose output') do
53
+ config[:verbose] = true
54
+ end
55
+ end
56
+
57
+ config[:name] = op.parse!.first
58
+ abort 'Last argument must be the main block name' unless config[:name]
59
+
60
+ if config[:listen] && config[:port] < 1024 && !Process.euid.zero?
61
+ abort 'Please run tftpd as root via sudo!'
62
+ end
63
+
64
+ log = Logger.new(STDOUT)
65
+ log.level = config[:verbose] ? "DEBUG" : "INFO"
66
+ log.formatter = lambda do |s, d, p, m|
67
+ "#{d.strftime('%Y-%m-%d %H:%M:%S.%3N')} | #{s.ljust(5)} | #{m}\n"
68
+ end
69
+ config[:logger] = log
70
+
71
+ if config[:verbose]
72
+ PP.pp(config, STDERR)
73
+ end
74
+
75
+ if !config[:listen]
76
+ STDERR.puts "Rendering block #{config[:name]}" if config[:verbose]
77
+ br = NetConfGen::BlockEngine.new(config[:root])
78
+
79
+ config[:variables].each do |k, v|
80
+ br.set(k, v)
81
+ end
82
+ block = br.load(config[:name])
83
+ puts block.render
84
+ exit(0)
85
+ end
86
+
87
+ begin
88
+ log.info "Serving from and to #{config[:path]}"
89
+ srv = TFTP::Server::RWSimple.new(config[:path], config)
90
+ srv.run!
91
+ rescue SignalException => e
92
+ puts if e.is_a? Interrupt
93
+ srv.stop
94
+ end
95
+
96
+ if Thread.list.length > 1
97
+ log.info 'Waiting for outstanding connections'
98
+ Thread.stop
99
+ end
@@ -0,0 +1,469 @@
1
+ # After https://www.ietf.org/rfc/rfc1350.txt
2
+ #
3
+
4
+ require 'erb'
5
+
6
+
7
+ module NetConfGen
8
+
9
+
10
+ class Block
11
+ attr_accessor :code, :blockengine
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ end
16
+
17
+ def render
18
+ t = ERB.new(@code)
19
+ str = t.result(@blockengine.context.instance_eval { binding })
20
+ return str
21
+ end
22
+
23
+ def to_s
24
+ self.render
25
+ end
26
+
27
+ end
28
+
29
+ class BlockContext
30
+ def initialize(blockengine)
31
+ @blockengine = blockengine
32
+ end
33
+
34
+ def include(name)
35
+ block = @blockengine.load(name)
36
+ return block.render
37
+ end
38
+ end
39
+
40
+ class BlockEngine
41
+ attr_reader :context
42
+ def initialize(basepath)
43
+ @basepath = basepath
44
+ if !File.directory?(basepath)
45
+ raise "Basepath #{basepath} does not exists"
46
+ end
47
+ @suffix = '.txt'
48
+
49
+ @blocks = {}
50
+
51
+ @context = BlockContext.new(self)
52
+ end
53
+
54
+ def set(key, value)
55
+ @context.instance_variable_set("@" + key, value)
56
+ end
57
+
58
+ def load(name)
59
+ if @blocks[name]
60
+ return @blocks[name]
61
+ end
62
+
63
+ @code = '';
64
+ File.open(@basepath + '/' + name + @suffix, "r") do |f|
65
+ code_started = false
66
+ code = ''
67
+ f.each_line do |line|
68
+ if line == "<code>\n"
69
+ code_started = true
70
+ elsif line == "</code>\n" || line == "</code>"
71
+ code_started = false
72
+ elsif code_started == true
73
+ code += line
74
+ end
75
+ end
76
+
77
+ block = Block.new(name)
78
+ block.code = code
79
+ block.blockengine = self
80
+ @blocks[name] = block
81
+
82
+ return block
83
+ end
84
+ end
85
+ end
86
+
87
+
88
+ # TFTP-specific errors.
89
+ class Error < Exception; end
90
+ # Packet parsing exception.
91
+ class ParseError < Error; end
92
+
93
+ # Packet can parse a binary string into a lightweight object representation.
94
+ module Packet
95
+ # Base is a thin layer over a Struct.
96
+ class Base < Struct
97
+ # Encode the packet back to binary string.
98
+ # It uses the #to_str method to properly format each packet, and then forces
99
+ # 8bit encoding.
100
+ def encode; to_str.force_encoding('ascii-8bit'); end
101
+ end
102
+
103
+ # Read Request
104
+ RRQ = Base.new(:filename, :mode)
105
+ class RRQ
106
+ # Convert to binary string.
107
+ def to_str; "\x00\x01" + self.filename + "\x00" + self.mode.to_s + "\x00"; end
108
+ end
109
+
110
+ # Write Request
111
+ WRQ = Base.new(:filename, :mode)
112
+ class WRQ
113
+ def to_str; "\x00\x02" + self.filename + "\x00" + self.mode.to_s + "\x00"; end
114
+ end
115
+
116
+ # Data
117
+ DATA = Base.new(:seq, :data)
118
+ class DATA
119
+ def to_str; "\x00\x03" + [self.seq].pack('n') + self.data; end
120
+ # Check if this is the last data packet for this session.
121
+ def last?; self.data.length < 512; end
122
+ end
123
+
124
+ # Acknowledgement
125
+ ACK = Base.new(:seq)
126
+ class ACK
127
+ def to_str; "\x00\x04" + [self.seq].pack('n'); end
128
+ end
129
+
130
+ # Error
131
+ ERROR = Base.new(:code, :msg)
132
+ class ERROR
133
+ def to_str; "\x00\x05" + [self.code].pack('n') + self.msg + "\x00"; end
134
+ end
135
+
136
+ # Parse a binary string into a packet.
137
+ # Does some sanity checking, can raise a ParseError.
138
+ def self.parse(data)
139
+ data = data.force_encoding('ascii-8bit')
140
+
141
+ opcode = data.unpack('n').first
142
+ if opcode < 1 || opcode > 5
143
+ raise ParseError, "Unknown packet opcode '#{opcode.inspect}'"
144
+ end
145
+
146
+ payload = data.slice(2, data.length - 2)
147
+ case opcode
148
+ when 1, 2 # rrq, wrq
149
+ raise ParseError, 'Not null terminated' if payload.slice(payload.length - 1) != "\x00"
150
+ xs = payload.split("\x00")
151
+ raise ParseError, "Not enough elements: #{xs.inspect}" if xs.length < 2
152
+ filename = xs[0]
153
+ mode = xs[1].downcase.to_sym
154
+ raise ParseError, "Unknown mode '#{xs[1].inspect}'" unless [:netascii, :octet].member? mode
155
+ return RRQ.new(filename, mode) if opcode == 1
156
+ return WRQ.new(filename, mode)
157
+ when 3 # data
158
+ seq = payload.unpack('n').first
159
+ block = payload.slice(2, payload.length - 2) || ''
160
+ raise ParseError, "Exceeded block length with #{block.length} bytes" if block.length > 512
161
+ return DATA.new(seq, block)
162
+ when 4 # ack
163
+ raise ParseError, "Wrong payload length with #{payload.length} bytes" if payload.length != 2
164
+ seq = payload.unpack('n').first
165
+ return ACK.new(seq)
166
+ when 5 # error
167
+ raise ParseError, 'Not null terminated' if payload.slice(payload.length - 1) != "\x00"
168
+ code = payload.unpack('n').first
169
+ raise ParseError, "Unknown error code '#{code.inspect}'" if code < 0 || code > 7
170
+ msg = payload.slice(2, payload.length - 3) || ''
171
+ return ERROR.new(code, msg)
172
+ end
173
+ end
174
+ end
175
+
176
+ # Handlers implement session-handling logic.
177
+ module Handler
178
+ # Base handler contains the common methods for real handlers.
179
+ class Base
180
+ # Initialize the handler.
181
+ #
182
+ # Options:
183
+ #
184
+ # - :logger => logger object (e.g. a Logger instance)
185
+ # - :timeout => used while waiting for next DATA/ACK packets (default: 5s)
186
+ #
187
+ # All given options are saved in @opts.
188
+ #
189
+ # @param opts [Hash] Options
190
+ def initialize(opts = {})
191
+ @logger = opts[:logger]
192
+ @timeout = opts[:timeout] || 5
193
+ @opts = opts
194
+ end
195
+
196
+ # Send data over an established connection.
197
+ #
198
+ # Doesn't close neither sock nor io.
199
+ #
200
+ # @param tag [String] Tag used for logging
201
+ # @param sock [UDPSocket] Connected socket
202
+ # @param io [IO] Object to send data from
203
+ def send(tag, sock, io)
204
+ seq = 1
205
+ begin
206
+ while not io.eof?
207
+ block = io.read(512)
208
+ sock.send(Packet::DATA.new(seq, block).encode, 0)
209
+ unless IO.select([sock], nil, nil, @timeout)
210
+ log :warn, "#{tag} Timeout at block ##{seq}"
211
+ return
212
+ end
213
+ msg, _ = sock.recvfrom(4, 0)
214
+ pkt = Packet.parse(msg)
215
+ if pkt.class != Packet::ACK
216
+ log :warn, "#{tag} Expected ACK but got: #{pkt.class}"
217
+ return
218
+ end
219
+ if pkt.seq != seq
220
+ log :warn, "#{tag} Seq mismatch: #{seq} != #{pkt.seq}"
221
+ return
222
+ end
223
+ # Increment with wrap around at 16 bit boundary,
224
+ # because of tftp block number field size limit.
225
+ seq = (seq + 1) & 0xFFFF
226
+ end
227
+ sock.send(Packet::DATA.new(seq, '').encode, 0) if io.size % 512 == 0
228
+ rescue ParseError => e
229
+ log :warn, "#{tag} Packet parse error: #{e.to_s}"
230
+ return
231
+ end
232
+ log :info, "#{tag} Sent file"
233
+ end
234
+
235
+ # Receive data over an established connection.
236
+ #
237
+ # Doesn't close neither sock nor io.
238
+ # Returns true if whole file has been received, false otherwise.
239
+ #
240
+ # @param tag [String] Tag used for logging
241
+ # @param sock [UDPSocket] Connected socket
242
+ # @param io [IO] Object to write data to
243
+ # @return [Boolean]
244
+ def recv(tag, sock, io)
245
+ sock.send(Packet::ACK.new(0).encode, 0)
246
+ seq = 1
247
+ begin
248
+ loop do
249
+ unless IO.select([sock], nil, nil, @timeout)
250
+ log :warn, "#{tag} Timeout at block ##{seq}"
251
+ return false
252
+ end
253
+ msg, _ = sock.recvfrom(516, 0)
254
+ pkt = Packet.parse(msg)
255
+ if pkt.class != Packet::DATA
256
+ log :warn, "#{tag} Expected DATA but got: #{pkt.class}"
257
+ return false
258
+ end
259
+ if pkt.seq != seq
260
+ log :warn, "#{tag} Seq mismatch: #{seq} != #{pkt.seq}"
261
+ return false
262
+ end
263
+ io.write(pkt.data)
264
+ sock.send(Packet::ACK.new(seq).encode, 0)
265
+ break if pkt.last?
266
+ seq = (seq + 1) & 0xFFFF
267
+ end
268
+ rescue ParseError => e
269
+ log :warn, "#{tag} Packet parse error: #{e.to_s}"
270
+ return false
271
+ end
272
+ log :info, "#{tag} Received file"
273
+ true
274
+ end
275
+
276
+ private
277
+ def log(level, msg)
278
+ @logger.send(level, msg) if @logger
279
+ end
280
+ end
281
+
282
+ # Basic read-write session over a 'physical' directory.
283
+ class RWSimple < Base
284
+ # Initialize the handler.
285
+ #
286
+ # Options:
287
+ #
288
+ # - :no_read => deny read access if true
289
+ # - :no_write => deny write access if true
290
+ #
291
+ # @param path [String] Path to serving root directory
292
+ # @param opts [Hash] Options
293
+ def initialize(path, opts = {})
294
+ @path = path
295
+ super(opts)
296
+ end
297
+
298
+ # Handle a session.
299
+ #
300
+ # Has to close the socket (and any other resources).
301
+ # Note that the current version 'guards' against path traversal by a simple
302
+ # substitution of '..' with '__'.
303
+ #
304
+ # @param tag [String] Tag used for logging
305
+ # @param req [Packet] The initial request packet
306
+ # @param sock [UDPSocket] Connected socket
307
+ # @param src [UDPSource] Initial connection information
308
+ def run!(tag, req, sock, src)
309
+ name = req.filename.gsub('..', '__')
310
+ path = File.join(@path, name)
311
+
312
+ case req
313
+ when Packet::RRQ
314
+ if @opts[:no_read]
315
+ log :info, "#{tag} Denied read request for #{req.filename}"
316
+ sock.send(Packet::ERROR.new(2, 'Access denied.').encode, 0)
317
+ sock.close
318
+ return
319
+ end
320
+ log :info, "#{tag} Read request for #{req.filename} (#{req.mode})"
321
+ unless File.exist? path
322
+ log :warn, "#{tag} File not found"
323
+ sock.send(Packet::ERROR.new(1, 'File not found.').encode, 0)
324
+ sock.close
325
+ return
326
+ end
327
+ mode = 'r'
328
+ mode += 'b' if req.mode == :octet
329
+ io = File.open(path, mode)
330
+ send(tag, sock, io)
331
+ sock.close
332
+ io.close
333
+ when Packet::WRQ
334
+ if @opts[:no_write]
335
+ log :info, "#{tag} Denied write request for #{req.filename}"
336
+ sock.send(Packet::ERROR.new(2, 'Access denied.').encode, 0)
337
+ sock.close
338
+ return
339
+ end
340
+ log :info, "#{tag} Write request for #{req.filename} (#{req.mode})"
341
+ mode = 'w'
342
+ mode += 'b' if req.mode == :octet
343
+ io = File.open(path, mode)
344
+ ok = recv(tag, sock, io)
345
+ sock.close
346
+ io.close
347
+ unless ok
348
+ log :warn, "#{tag} Removing partial file #{req.filename}"
349
+ File.delete(path)
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ # Servers customize the Basic server and perhaps combine it with a handler.
357
+ module Server
358
+ # Basic server utilizing threads for handling sessions.
359
+ #
360
+ # It lacks a mutex around access to @clients, in case you'd want to stress
361
+ # test it for 10K or something.
362
+ #
363
+ # @attr handler [Handler] Session handler
364
+ # @attr address [String] Address to listen to
365
+ # @attr port [Integer] Session dispatcher port
366
+ # @attr clients [Hash] Current sessions
367
+ class Base
368
+ attr_reader :handler, :address, :port, :clients
369
+
370
+ # Initialize the server.
371
+ #
372
+ # Options:
373
+ #
374
+ # - :address => address to listen to (default: '0.0.0.0')
375
+ # - :port => dispatcher port (default: 69)
376
+ # - :logger => logger instance
377
+ #
378
+ # @param handler [Handler] Initialized session handler
379
+ # @param opts [Hash] Options
380
+ def initialize(handler, opts = {})
381
+ @handler = handler
382
+
383
+ @address = opts[:address] || '0.0.0.0'
384
+ @port = opts[:port] || 69
385
+ @logger = opts[:logger]
386
+
387
+ @clients = Hash.new
388
+ @run = false
389
+ end
390
+
391
+ # Run the main server loop.
392
+ #
393
+ # This is obviously blocking.
394
+ def run!
395
+ log :info, "UDP server loop at #{@address}:#{@port}"
396
+ @run = true
397
+ Socket.udp_server_loop(@address, @port) do |msg, src|
398
+ break unless @run
399
+
400
+ addr = src.remote_address
401
+ tag = "[#{addr.ip_address}:#{addr.ip_port.to_s.ljust(5)}]"
402
+ log :info, "#{tag} New initial packet received"
403
+
404
+ begin
405
+ pkt = Packet.parse(msg)
406
+ rescue ParseError => e
407
+ log :warn, "#{tag} Packet parse error: #{e.to_s}"
408
+ next
409
+ end
410
+
411
+ log :debug, "#{tag} -> PKT: #{pkt.inspect}"
412
+ tid = get_tid
413
+ tag = "[#{addr.ip_address}:#{addr.ip_port.to_s.ljust(5)}:#{tid.to_s.ljust(5)}]"
414
+ sock = addr.connect_from(@address, tid)
415
+ @clients[tid] = tag
416
+
417
+ unless pkt.is_a?(Packet::RRQ) || pkt.is_a?(Packet::WRQ)
418
+ log :warn, "#{tag} Bad initial packet: #{pkt.class}"
419
+ sock.send(Packet::ERROR.new(4, 'Illegal TFTP operation.').encode, 0)
420
+ sock.close
421
+ next
422
+ end
423
+
424
+ Thread.new do
425
+ @handler.run!(tag, pkt, sock, src)
426
+ @clients.delete(tid)
427
+ log :info, "#{tag} Session ended"
428
+ end
429
+ end
430
+ log :info, 'UDP server loop has stopped'
431
+ end
432
+
433
+ # Stop the main server loop.
434
+ #
435
+ # This will allow the currently pending sessions to finish.
436
+ def stop
437
+ log :info, 'Stopping UDP server loop'
438
+ @run = false
439
+ UDPSocket.new.send('break', 0, @address, @port)
440
+ end
441
+
442
+ private
443
+ # Get the server's TID.
444
+ #
445
+ # The TID is basically a random port number we will use for a session.
446
+ # This actually tries to get a unique TID per session.
447
+ # It uses only ports 1024 - 65535 as not to require root.
448
+ def get_tid
449
+ tid = 1024 + rand(64512)
450
+ tid = 1024 + rand(64512) while @clients.has_key? tid
451
+ tid
452
+ end
453
+
454
+ def log(level, msg)
455
+ @logger.send(level, msg) if @logger
456
+ end
457
+ end
458
+
459
+ # Basic read-write TFTP server.
460
+ #
461
+ # This is what most other TFTPd implementations give you.
462
+ class RWSimple < Base
463
+ def initialize(path, opts = {})
464
+ handler = Handler::RWSimple.new(path, opts)
465
+ super(handler, opts)
466
+ end
467
+ end
468
+ end
469
+ end
@@ -0,0 +1,4 @@
1
+ module TFTP
2
+ # Current version string.
3
+ VERSION = '1.1'
4
+ end
data/lib/netconfgen.rb ADDED
@@ -0,0 +1 @@
1
+ require 'netconfgen/netconfgen'
@@ -0,0 +1,21 @@
1
+ # coding: utf-8
2
+ #
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'netconfgen'
6
+ s.version = '0.0.1'
7
+ s.date = Time.now
8
+
9
+ s.summary = %q{Template based config generation}
10
+ s.files = `git ls-files`.split("\n")
11
+ s.executables = ['confgen']
12
+ s.test_files = s.files.grep(%r{^test/})
13
+ s.require_paths = ['lib']
14
+ s.authors = "Juho Mäkinen juho.makinen@gmail.com"
15
+
16
+ s.required_ruby_version = '>= 2.1.0'
17
+
18
+ s.add_development_dependency 'rubygems-tasks', '~> 0.2'
19
+ s.add_development_dependency 'minitest', '~> 5.4'
20
+ s.add_development_dependency 'rake', '~> 10.0'
21
+ end
data/netconfgen.yaml ADDED
@@ -0,0 +1,4 @@
1
+ verbose: true
2
+ variables:
3
+ foo: 2
4
+ test: test
@@ -0,0 +1,37 @@
1
+ require 'minitest/autorun'
2
+
3
+ require 'netconfgen'
4
+
5
+ class BlockEngine < Minitest::Test
6
+
7
+ def test_block_loading
8
+
9
+ br = NetConfGen::BlockEngine.new('test/data')
10
+ block = br.load('file1')
11
+ assert_equal block.code, "! this is a code module\n"
12
+ end
13
+
14
+ def test_block_variables
15
+
16
+ br = NetConfGen::BlockEngine.new('test/data')
17
+
18
+ br.set('foo', 1)
19
+ br.set('bar', 'hello')
20
+ block = br.load('variables')
21
+ assert_equal block.render, "foo: 1\nbar: hello\n"
22
+ end
23
+
24
+
25
+ def test_recursive_block_rendering
26
+ br = NetConfGen::BlockEngine.new('test/data')
27
+ block = br.load('file2')
28
+
29
+ assert_equal block.render,
30
+ %{! main
31
+ ! this is a code module
32
+
33
+ ! main again
34
+ }
35
+
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ This is a wiki file
2
+
3
+ <code>
4
+ ! this is a code module
5
+ </code>
6
+ Some other comments
@@ -0,0 +1,9 @@
1
+ test
2
+
3
+ <code>
4
+ ! main
5
+ <%= include "file1" %>
6
+ ! main again
7
+ </code>
8
+
9
+ test
@@ -0,0 +1,4 @@
1
+ <code>
2
+ foo: <%= @foo %>
3
+ bar: <%= @bar %>
4
+ </code>
data/test/data.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "message": "Hello, World!",
3
+ "list": [
4
+ 1, 2, 3
5
+ ],
6
+ "map": {
7
+ "A": 1,
8
+ "B": 2
9
+ }
10
+ }
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: netconfgen
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Juho Mäkinen juho.makinen@gmail.com
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubygems-tasks
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.4'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description:
56
+ email:
57
+ executables:
58
+ - confgen
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - Gemfile
64
+ - Rakefile
65
+ - bin/confgen
66
+ - lib/netconfgen.rb
67
+ - lib/netconfgen/netconfgen.rb
68
+ - lib/netconfgen/version.rb
69
+ - netconfgen.gemspec
70
+ - netconfgen.yaml
71
+ - test/blockreader.rb
72
+ - test/data.json
73
+ - test/data/file1.txt
74
+ - test/data/file2.txt
75
+ - test/data/variables.txt
76
+ homepage:
77
+ licenses: []
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 2.1.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 2.5.2
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Template based config generation
99
+ test_files:
100
+ - test/blockreader.rb
101
+ - test/data.json
102
+ - test/data/file1.txt
103
+ - test/data/file2.txt
104
+ - test/data/variables.txt