netconfgen 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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