sapluuna 0.1.4

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f176e218e8e443cec9eaa8e94f6ca233ae9f8014
4
+ data.tar.gz: 3f783c2bc0f48f9297a052bd0d8b55352b4a2833
5
+ SHA512:
6
+ metadata.gz: f174827d07bc20c043cb03f6c792518dc2b7ef42d2bb86aa6b53b521792aa57a892f3e7d38263b8a5ba2756c006eea10e24f0bbf518e90a12eaff416136f1b78
7
+ data.tar.gz: 364fc6803738df805c191364e8c8a9eecb1693e8b3159b003fda9aba93d8a908006247b3289d9bd746eff16d5883dd588bb0203a56c4876fa6aa50ffadd93415
@@ -0,0 +1,2 @@
1
+ gems
2
+ cfg*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,11 @@
1
+ # Sapluuna
2
+
3
+ Silly little template based configuration maker
4
+
5
+ * Anything outside {{{ ... }}} is comment and won't be in generated file
6
+ * Inside {{{ }}} you can have <% foo %> which is just ruby
7
+ * For method_missing in <% foo %> we try variable[name] hash, given to constrcutor, i.e. Sapluuna.new variables: {replace_this: 'with_this'} .... <% replace_this %> works
8
+ * {{{ can be followed by negative or positive labels, if labels match to those given to constructor {{{ }}} is evaluated, otherwise ignored
9
+ * rationale for labels is {{{ PE ..... }}} or {{{ Finland Sweden ..... }}} to conditionally evaluate blocks
10
+ * You can query the instance on what variables are needed when labels X are set, use-case is in say in webUI to automatically generate form with all variables template needs
11
+
@@ -0,0 +1,47 @@
1
+ begin
2
+ require 'rake/testtask'
3
+ require 'bundler'
4
+ # Bundler.setup
5
+ rescue LoadError
6
+ warn 'bundler missing'
7
+ end
8
+
9
+ gemspec = eval(File.read(Dir['*.gemspec'].first))
10
+ file = [gemspec.name, gemspec.version].join('-') + '.gem'
11
+
12
+ desc 'Validate gemspec'
13
+ task :gemspec do
14
+ gemspec.validate
15
+ end
16
+
17
+ desc 'Run minitest'
18
+ task :test do
19
+ Rake::TestTask.new do |t|
20
+ t.libs.push "lib"
21
+ t.test_files = FileList['spec/*_spec.rb']
22
+ t.verbose = true
23
+ end
24
+ end
25
+
26
+ desc 'Build gem'
27
+ task :build do
28
+ system "gem build #{gemspec.name}.gemspec"
29
+ FileUtils.mkdir_p 'gems'
30
+ FileUtils.mv file, 'gems'
31
+ end
32
+
33
+ desc 'Install gem'
34
+ task :install => :build do
35
+ system "sudo -Es sh -c \'umask 022; gem install gems/#{file}\'"
36
+ end
37
+
38
+ desc 'Remove gems'
39
+ task :clean do
40
+ FileUtils.rm_rf 'gems'
41
+ end
42
+
43
+ desc 'Push to rubygems'
44
+ task :push do
45
+ system "gem push gems/#{file}"
46
+ system "git tag #{gemspec.version}"
47
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require_relative '../lib/sapluuna/cli'
5
+ k = Sapluuna::CLI.new
6
+ @debug = k.debug
7
+ k.run
8
+ rescue => error
9
+ warn "ERROR: #{error.message} (#{error.class})"
10
+ raise if @debug
11
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'ip'
2
+
3
+ class Sapluuna
4
+ module ClassHelpers
5
+
6
+ class ::String
7
+ def ip
8
+ IP.new(self).ip
9
+ end
10
+ def acl
11
+ IP.new(self).acl
12
+ end
13
+ def net
14
+ IP.new(self).net
15
+ end
16
+ def cidr
17
+ IP.new(self).cidr
18
+ end
19
+ end
20
+
21
+ class ::Array
22
+ def as_ip format_string
23
+ as_method 'ip', format_string
24
+ end
25
+
26
+ def as_acl format_string
27
+ as_method 'acl', format_string
28
+ end
29
+
30
+ def as_cidr format_string
31
+ as_method 'cidr', format_string
32
+ end
33
+
34
+ private
35
+
36
+ def as_method method, format_string
37
+ map do |str|
38
+ format_string % str.send(method)
39
+ end.join("\n")
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'sapluuna'
2
+ begin
3
+ require 'slop'
4
+ rescue LoadError
5
+ warn "sudo gem install slop ## required by sapluuna CLI"
6
+ exit 42
7
+ end
8
+
9
+
10
+
11
+ class Sapluuna
12
+ class CLI
13
+ ROOT = '.'
14
+ attr_reader :debug
15
+
16
+ def initialize
17
+ @opts = opts_parse
18
+ args = @opts.arguments
19
+ @file = args.shift
20
+ @labels = @opts[:label].split(/[,\s]+/) if @opts[:label]
21
+ @vars = {}
22
+ @disco = @opts[:variables]
23
+ args.each do |var|
24
+ name, value = var.split '='
25
+ @vars[name.to_sym] = value
26
+ end
27
+ if @opts.debug?
28
+ @debug = true
29
+ Log.level = Logger::DEBUG
30
+ end
31
+ end
32
+
33
+ def run
34
+ raise MissingOption, 'File is mandatory argument' unless @file
35
+ sap = Sapluuna.new labels: @labels, variables: @vars,
36
+ discover_variables: @disco,
37
+ root_directory: (@opts[:root] or ROOT)
38
+ cfg = sap.parse File.read(@file)
39
+ puts @disco ? sap.discovered_variables.keys : cfg
40
+ rescue => error
41
+ crash error
42
+ raise
43
+ end
44
+
45
+ private
46
+
47
+ def opts_parse
48
+ Slop.parse do |o|
49
+ o.banner = 'Usage: sapluuna [OPTIONS] FILE [variables]'
50
+ o.bool '-d', '--debug', 'turn on debugging'
51
+ o.string '-l', '--label', 'commma separated list of labels'
52
+ o.bool '-v', '--variables', 'displays required variables'
53
+ o.string '-r', '--root', 'root directory for template import'
54
+ o.on '-h', '--help' do puts o; exit; end
55
+ end
56
+ end
57
+
58
+ def crash error
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,94 @@
1
+ class Sapluuna
2
+ class Context
3
+ attr_reader :discovered_variables, :variables
4
+
5
+ RootDirectory = '.'
6
+
7
+ class VariableMissing < Error; end
8
+
9
+ def initialize opts
10
+ @opts = opts.dup
11
+ @discover_variables = opts.delete :discover_variables
12
+ @variables = (opts.delete(:variables) or {})
13
+ @root_directory = (opts.delete(:root_directory) or RootDirectory)
14
+ @output = ''
15
+ @discovered_variables = {}
16
+ end
17
+
18
+ def cfg value
19
+ @output << value
20
+ end
21
+
22
+ def code value
23
+ @output << eval(value).to_s
24
+ end
25
+
26
+ def str
27
+ @output
28
+ end
29
+
30
+ def are value
31
+ [:are, value]
32
+ end
33
+
34
+ def is value
35
+ [:is, value]
36
+ end
37
+
38
+ def silent *args
39
+ ""
40
+ end
41
+
42
+ private
43
+
44
+ def import file
45
+ template = File.read resolve_file(file)
46
+ @opts[:variables] = @variables
47
+ @opts[:root_directory] = @root_directory.dup
48
+ Log.debug "importing #{file}"
49
+ sapl = Sapluuna.new @opts
50
+ output = sapl.parse template
51
+ @discovered_variables.merge! sapl.discovered_variables
52
+ add_indent output.lines, @output.lines.last
53
+ rescue => error
54
+ raise error, "#{error.message} (while reading #{file})"
55
+ end
56
+
57
+ def resolve_file file
58
+ # should we avoid ../../../etc/passwd style input here?
59
+ # why? we control templates?
60
+ File.join @root_directory, file
61
+ end
62
+
63
+ def add_indent output, indent_hint
64
+ return output.join.chomp if output.size < 2 or not indent_hint
65
+ indent_size = indent_hint.match(/\A\s*/)[0].size
66
+ first_line = output[0]
67
+ output = output[1..-1].map { |line| ' ' * indent_size + line }
68
+ output.unshift(first_line).join.chomp
69
+ end
70
+
71
+ def method_missing method, *args
72
+ if Array === args.first
73
+ value = args.first.last
74
+ case args.first.first
75
+ when :is
76
+ @variables[method] = value
77
+ when :are
78
+ @variables[method] = value.to_s.strip.split(/\s+/)
79
+ end
80
+ ""
81
+ elsif @variables[method]
82
+ @variables[method]
83
+ else
84
+ if @discover_variables
85
+ args ||= ['']
86
+ @discovered_variables[method] = args[0] unless @discovered_variables.has_key? method
87
+ ""
88
+ else
89
+ raise VariableMissing, "variable '#{method}' required, but not given"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,51 @@
1
+ require 'ipaddr'
2
+
3
+ class Sapluuna
4
+ class IP < IPAddr
5
+
6
+ def initialize(addr = '::', family = Socket::AF_UNSPEC)
7
+ addr_org, _family_org = addr, family
8
+ prefix, _prefixlen = addr_org.split('/')
9
+ @addr_org = prefix
10
+ super
11
+ if @family == Socket::AF_UNSPEC or @family == Socket::AF_INET
12
+ @addr_org = in_addr(@addr_org)
13
+ else
14
+ @addr_org = in6_addr(@addr_org)
15
+ end
16
+ end
17
+
18
+ def ip
19
+ addr_tmp = @addr
20
+ @addr = @addr_org
21
+ ip = self.to_s
22
+ @addr = addr_tmp
23
+ ip
24
+ end
25
+
26
+ def mask_cidr
27
+ @mask_addr.to_s(2).delete('0').size
28
+ end
29
+
30
+ def mask_wild
31
+ _to_string ~@mask_addr
32
+ end
33
+
34
+ def mask_net
35
+ _to_string @mask_addr
36
+ end
37
+
38
+ def cidr
39
+ ip + '/' + mask_cidr.to_s
40
+ end
41
+
42
+ def acl
43
+ ip + ' '+ mask_wild
44
+ end
45
+
46
+ def net
47
+ ip + ' ' + mask_net
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,74 @@
1
+ require 'strscan'
2
+
3
+ class Sapluuna
4
+ class Parser
5
+ CODE_OPEN = '<%\s*'
6
+ CODE_CLOSE = '\s*%>'
7
+ TEMPLATE_OPEN = '^\s*{{{[\t ]*'
8
+ TEMPLATE_CLOSE = '\s*}}}\s*$'
9
+ NEW_LINE = "\n"
10
+ class ParserError < Error; end
11
+ class UnterminatedBlock < ParserError; end
12
+
13
+ def initialize
14
+ @sc = StringScanner.new ''
15
+ end
16
+
17
+ def parse input
18
+ level = 0
19
+ cfg = []
20
+ @sc.string = input
21
+ loop do
22
+ if @sc.scan_until Regexp.new(TEMPLATE_OPEN)
23
+ cfg << [:template, get_labels, get_template(level)]
24
+ else
25
+ break
26
+ end
27
+ end
28
+ @sc.string = '' # no need to keep it in memory
29
+ cfg
30
+ end
31
+
32
+ private
33
+
34
+ def get_labels
35
+ labels = @sc.scan_until Regexp.new(NEW_LINE)
36
+ labels.strip.split(/\s+/)
37
+ end
38
+
39
+ def re_combine *args
40
+ re = args.map do |arg|
41
+ '(?:%s)' % arg
42
+ end.join('|')
43
+ Regexp.new re
44
+ end
45
+
46
+ def clean_scan scan
47
+ endstr = -(@sc.matched.size+1)
48
+ scan[0..endstr]
49
+ end
50
+
51
+ def get_template level
52
+ cfg = []
53
+ loop do
54
+ scan = @sc.scan_until re_combine(CODE_OPEN, TEMPLATE_OPEN, TEMPLATE_CLOSE)
55
+ raise UnterminatedBlock, "template at #{level}, #{@sc.pos} runs forever" unless scan
56
+ cfg << [:cfg, clean_scan(scan)]
57
+ case @sc.matched
58
+
59
+ when Regexp.new(CODE_OPEN)
60
+ scan = @sc.scan_until Regexp.new(CODE_CLOSE)
61
+ raise UnterminatedBlock, "code at #{level}, #{@sc.pos} runs forever" unless scan
62
+ cfg << [:code, clean_scan(scan)]
63
+
64
+ when Regexp.new(TEMPLATE_OPEN)
65
+ cfg << [:template, get_labels, get_template(level+1)]
66
+
67
+ when Regexp.new(TEMPLATE_CLOSE)
68
+ return cfg
69
+ end
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,91 @@
1
+ require 'logger'
2
+ require_relative 'class_helpers'
3
+
4
+ class Sapluuna
5
+
6
+ include ClassHelpers
7
+ class Error < StandardError; end
8
+ class InvalidOption < StandardError; end
9
+ class MissingOption < StandardError; end
10
+ class InvalidType < Error; end
11
+ Log = Logger.new STDERR
12
+ Log.level = Logger::WARN
13
+
14
+ def initialize opts
15
+ @context = (opts[:context] or Context)
16
+ @want_labels = read_labels (opts[:labels] or [])
17
+ @context = @context.new(opts) if @context.class == Class
18
+ @parser = Parser.new
19
+ end
20
+
21
+ def discovered_variables
22
+ @context.discovered_variables
23
+ end
24
+
25
+ def variables
26
+ @context.variables
27
+ end
28
+
29
+ def parse input
30
+ @parser.parse(input).each do |cfg|
31
+ type = cfg.shift
32
+ case type
33
+ when :template
34
+ template cfg
35
+ else
36
+ raise InvalidType, "#{type} was not recognized by parser"
37
+ end
38
+ end
39
+ @context.str
40
+ end
41
+
42
+ private
43
+
44
+ def read_labels labels
45
+ pos, neg = [], []
46
+ labels.each do |label|
47
+ label[0] == '!' ? neg.push(label[1..-1]) : pos.push(label)
48
+ end
49
+ [pos, neg]
50
+ end
51
+
52
+ def template templ
53
+ return unless valid_labels? read_labels(templ.shift)
54
+ templ.shift.each do |t|
55
+ type = t.shift
56
+ case type
57
+ when :code
58
+ @context.code t.last
59
+ when :cfg
60
+ @context.cfg t.last
61
+ when :template
62
+ template t
63
+ else
64
+ raise InvalidType, "#{type} was not recognized by parser"
65
+ end
66
+ end
67
+ end
68
+
69
+ def valid_labels? got_labels
70
+ # first/[0] is our positive labels (label)
71
+ # last/[1] is our negative labels (!label)
72
+
73
+ # template has label which we explcitly don't want
74
+ return false if (got_labels[0] & @want_labels[1]).size > 0
75
+
76
+ # template forbids label which we explicitly want
77
+ return false if (got_labels[1] & @want_labels[0]).size > 0
78
+
79
+ # template requires no labels
80
+ return true if got_labels[0].empty?
81
+
82
+ # template requires labels, do we want at least one of them?
83
+ return false unless (got_labels[0] & @want_labels[0]).size > 0
84
+
85
+ true
86
+ end
87
+
88
+ end
89
+
90
+ require_relative 'parser'
91
+ require_relative 'context'
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'sapluuna'
3
+ s.version = '0.1.4'
4
+ s.licenses = %w( Apache-2.0 )
5
+ s.platform = Gem::Platform::RUBY
6
+ s.authors = [ 'Saku Ytti' ]
7
+ s.email = %w( saku@ytti.fi )
8
+ s.homepage = 'http://github.com/ytti/sapluuna'
9
+ s.summary = 'Template parser'
10
+ s.description = 'Template based network configuration generator'
11
+ s.rubyforge_project = s.name
12
+ s.files = `git ls-files`.split("\n")
13
+ s.executables = %w( sapluuna )
14
+ s.require_path = 'lib/sapluuna'
15
+
16
+ s.required_ruby_version = '>= 2.0.0'
17
+ s.add_runtime_dependency 'slop', '~> 4.0'
18
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sapluuna
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: ruby
6
+ authors:
7
+ - Saku Ytti
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: slop
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ description: Template based network configuration generator
28
+ email:
29
+ - saku@ytti.fi
30
+ executables:
31
+ - sapluuna
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - Gemfile
37
+ - README.md
38
+ - Rakefile
39
+ - bin/sapluuna
40
+ - lib/sapluuna/class_helpers.rb
41
+ - lib/sapluuna/cli.rb
42
+ - lib/sapluuna/context.rb
43
+ - lib/sapluuna/ip.rb
44
+ - lib/sapluuna/parser.rb
45
+ - lib/sapluuna/sapluuna.rb
46
+ - sapluuna.gemspec
47
+ homepage: http://github.com/ytti/sapluuna
48
+ licenses:
49
+ - Apache-2.0
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib/sapluuna
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.0.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project: sapluuna
67
+ rubygems_version: 2.2.2
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Template parser
71
+ test_files: []