template_configurator 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Template Configurator
2
+
3
+ Template Configurator is a utility to write configuration files from ERB templates. When the file's content changes, it can then call an init script to intelligently reload the configuration. Through out the entire process exclusive file locks are used on the output file and json file to help ensure they are unmanipulated during the transformation process.
4
+
5
+ ## Use Cases
6
+
7
+ * Dynamically configure HAProxy to pick up new backends contained in a JSON file. Call "service haproxy reload" when the configuration changes.
8
+ * Dynamically configure NGinx to pick up new backends contained in a JSON file. Call "service nginx reload" when the configuration changes.
9
+ * Dynamically configure Varnish to pick up new backends contained in a JSON file. Call "service varnish reload" when the configuration changes.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'template_configurator'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install template_configurator
24
+
25
+ ## Usage
26
+
27
+ Usage: bin/template_configurator
28
+ --service:command EXECUTABLE action to execute to command service (default: /sbin/service)
29
+ --service:name INITRC initrc used to control service (default: )
30
+ --service:status ACTION action to execute to get status of service (default: status)
31
+ --service:reload ACTION action to execute to reload service (default: reload)
32
+ --service:restart ACTION action to execute to restart service (default: restart)
33
+ --service:start ACTION action to execute to start service (default: start)
34
+ --service:stop ACTION action to execute to stop service (default: stop)
35
+ --service:retries NUMBER number of attempts to reload service (default: 5)
36
+ --service:retry-delay SECS seconds to sleep between retries (default: 2)
37
+ --template:input-file FILE Where to read ERB template
38
+ --template:output-file FILE Where to write the output of the template
39
+ --template:json-file FILE Base port to initialize haproxy listening for mysql clusters
40
+ --log:level LEVEL Logging level
41
+ --log:file FILE Write logs to FILE (default: STDERR)
42
+ --log:age DAYS Rotate logs after DAYS pass (default: 7)
43
+ --log:size SIZE Rotate logs after the grow past SIZE bytes
44
+ --dry-run Dry run (do not commit changes to disk)
45
+ -V, --version Display version information
46
+ -h, --help Display this screen
47
+
48
+ ## Examples
49
+
50
+ Generate a new configuration file in /tmp/test.cfg using /tmp/test.cfg.erb using JSON data from /tmp/test.js. Reload "test" service when configuration changes.
51
+
52
+ template_configurator --template:input-file "/tmp/test.cfg.erb" --template:output-file "/tmp/test.cfg" --template:json-file "/tmp/test.js" --service:name "test"
53
+
54
+ ## Contributing
55
+
56
+ 1. Fork it
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Template Configurator - A utility to generate configuration files from ERB templates and restart
4
+ # services when configuration changes.
5
+ #
6
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
7
+ #
8
+ # This file is part of Template Configurator.
9
+ #
10
+ # Template Configurator is free software: you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation, either version 3 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # Template Configurator is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with Template Configurator. If not, see <http://www.gnu.org/licenses/>.
22
+ #
23
+
24
+ $:.unshift(File.expand_path('.')) # Ruby 1.9 doesn't have . in the load path...
25
+ $:.push(File.expand_path('lib/'))
26
+
27
+ require 'rubygems'
28
+ require 'template_configurator'
29
+
30
+ command_line = TemplateConfigurator::CommandLine.new
31
+ command_line.execute
@@ -0,0 +1,191 @@
1
+ #
2
+ # Template Configurator - A utility to generate configuration files from ERB templates and restart
3
+ # services when configuration changes.
4
+ #
5
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
6
+ #
7
+ # This file is part of Template Configurator.
8
+ #
9
+ # Template Configurator is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # Template Configurator is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with Template Configurator. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ require 'optparse'
23
+ require 'logger'
24
+
25
+ module TemplateConfigurator
26
+ class CommandLine
27
+ def initialize
28
+ @logger = Logger.new STDERR
29
+
30
+ @options = {}
31
+ @options[:log] = {}
32
+ @options[:template] = {}
33
+ @options[:service] = {}
34
+
35
+ args = OptionParser.new do |opts|
36
+ opts.banner = "Usage: #{$0}"
37
+
38
+ #
39
+ # Service options
40
+ #
41
+ @options[:service][:command] = '/sbin/service'
42
+ opts.on("--service:command EXECUTABLE", "action to execute to command service (default: #{@options[:service][:command]})") do |executable|
43
+ @options[:service][:command] = executable
44
+ end
45
+
46
+ @options[:service][:name] = nil
47
+ opts.on("--service:name INITRC", "initrc used to control service (default: #{@options[:service][:name]})") do |initrc|
48
+ @options[:service][:name] = initrc
49
+ end
50
+
51
+ @options[:service][:status] = 'status'
52
+ opts.on("--service:status ACTION", "action to execute to get status of service (default: #{@options[:service][:status]})") do |action|
53
+ @options[:service][:status] = action
54
+ end
55
+
56
+ @options[:service][:reload] = 'reload'
57
+ opts.on("--service:reload ACTION", "action to execute to reload service (default: #{@options[:service][:reload]})") do |action|
58
+ @options[:service][:reload] = action
59
+ end
60
+
61
+ @options[:service][:restart] = 'restart'
62
+ opts.on("--service:restart ACTION", "action to execute to restart service (default: #{@options[:service][:restart]})") do |action|
63
+ @options[:service][:restart] = action
64
+ end
65
+
66
+ @options[:service][:start] = 'start'
67
+ opts.on("--service:start ACTION", "action to execute to start service (default: #{@options[:service][:start]})") do |action|
68
+ @options[:service][:start] = action
69
+ end
70
+
71
+ @options[:service][:stop] = 'stop'
72
+ opts.on("--service:stop ACTION", "action to execute to stop service (default: #{@options[:service][:stop]})") do |action|
73
+ @options[:service][:stop] = action
74
+ end
75
+
76
+
77
+ @options[:service][:retries] = 5
78
+ opts.on("--service:retries NUMBER", "number of attempts to reload service (default: #{@options[:service][:retries]})") do |number|
79
+ @options[:service][:retries] = number.to_i
80
+ end
81
+
82
+ @options[:service][:retry_delay] = 2
83
+ opts.on("--service:retry-delay SECS", "seconds to sleep between retries (default: #{@options[:service][:retry_delay]})") do |seconds|
84
+ @options[:service][:retry_delay] = seconds.to_i
85
+ end
86
+
87
+ #
88
+ # Template options
89
+ #
90
+
91
+ @options[:template][:input_file] = nil
92
+ opts.on("--template:input-file FILE", "Where to read ERB template") do |file|
93
+ @options[:template][:input_file] = file
94
+ end
95
+
96
+ @options[:template][:output_file] = nil
97
+ opts.on("--template:output-file FILE", "Where to write the output of the template") do |file|
98
+ @options[:template][:output_file] = file
99
+ end
100
+
101
+ @options[:template][:json_file] = nil
102
+ opts.on("--template:json-file FILE", "Base port to initialize haproxy listening for mysql clusters") do |file|
103
+ @options[:template][:json_file] = file
104
+ end
105
+
106
+ #
107
+ # Logging
108
+ #
109
+
110
+ @options[:log][:level] = Logger::INFO
111
+ opts.on( '--log:level LEVEL', 'Logging level' ) do|level|
112
+ @options[:log][:level] = Logger.const_get level.upcase
113
+ end
114
+
115
+ @options[:log][:file] = STDERR
116
+ opts.on( '--log:file FILE', 'Write logs to FILE (default: STDERR)' ) do|file|
117
+ @options[:log][:file] = File.open(file, File::WRONLY | File::APPEND | File::CREAT)
118
+ end
119
+
120
+ @options[:log][:age] = 7
121
+ opts.on( '--log:age DAYS', "Rotate logs after DAYS pass (default: #{@options[:log][:age]})" ) do|days|
122
+ @options[:log][:age] = days.to_i
123
+ end
124
+
125
+ @options[:log][:size] = 1024*1024*10
126
+ opts.on( '--log:size SIZE', 'Rotate logs after the grow past SIZE bytes' ) do |size|
127
+ @options[:log][:size] = size.to_i
128
+ end
129
+
130
+
131
+ #
132
+ # General options
133
+ #
134
+
135
+ @options[:dry_run] = false
136
+ opts.on("--dry-run", "Dry run (do not commit changes to disk)") do
137
+ @options[:dry_run] = true
138
+ end
139
+
140
+
141
+ opts.on( '-V', '--version', 'Display version information' ) do
142
+ puts "Template Configurator #{TemplateConfigurator::VERSION}"
143
+ puts "Copyright (C) 2012 Erik Osterman <e@osterman.com>"
144
+ puts "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>"
145
+ puts "This is free software: you are free to change and redistribute it."
146
+ puts "There is NO WARRANTY, to the extent permitted by law."
147
+ exit
148
+ end
149
+
150
+ opts.on( '-h', '--help', 'Display this screen' ) do
151
+ puts opts
152
+ exit
153
+ end
154
+
155
+ end
156
+
157
+ begin
158
+ args.parse!
159
+ raise OptionParser::MissingArgument.new("--template:input-file") if @options[:template][:input_file].nil?
160
+ rescue OptionParser::MissingArgument => e
161
+ puts e.message
162
+ puts args
163
+ exit 1
164
+ rescue OptionParser::InvalidOption => e
165
+ puts e.message
166
+ puts args
167
+ exit 1
168
+ end
169
+ end
170
+
171
+ def execute
172
+ @processor = Processor.new(@options)
173
+ TemplateConfigurator.log = Logger.new(@options[:log][:file], @options[:log][:age], @options[:log][:size])
174
+ TemplateConfigurator.log.level = @options[:log][:level]
175
+ begin
176
+ @processor.render
177
+ rescue Interrupt => e
178
+ TemplateConfigurator.log.info("Aborting")
179
+ rescue NameError, ArgumentError => e
180
+ TemplateConfigurator.log.fatal(e.message)
181
+ TemplateConfigurator.log.debug(e.backtrace.join("\n"))
182
+ exit(1)
183
+ rescue Exception => e
184
+ TemplateConfigurator.log.fatal("#{e.class}: #{e.message}")
185
+ TemplateConfigurator.log.debug(e.backtrace.join("\n"))
186
+ exit(1)
187
+ end
188
+ exit(0)
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,108 @@
1
+ #
2
+ # Template Configurator - A utility to generate configuration files from ERB templates and restart
3
+ # services when configuration changes.
4
+ #
5
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
6
+ #
7
+ # This file is part of Template Configurator.
8
+ #
9
+ # Template Configurator is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # Template Configurator is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with Template Configurator. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ require 'erb'
23
+ require 'digest/sha1'
24
+ require 'json'
25
+
26
+ module TemplateConfigurator
27
+ class Processor
28
+ attr_accessor :service, :options, :locks
29
+
30
+ def lock file
31
+ @locks[:file] = File.open(file, File::RDWR|File::CREAT, 0644)
32
+ @locks[:file].flock(File::LOCK_EX)
33
+ @locks[:file]
34
+ end
35
+
36
+ def unlock
37
+ @locks[:file].flock(File::LOCK_UN)
38
+ @locks[:file]
39
+ end
40
+
41
+ def initialize(options)
42
+ @options = options
43
+ @locks = {}
44
+ @service = Service.new(@options[:service])
45
+ end
46
+
47
+ def reload
48
+ if @options[:service][:name].nil?
49
+ TemplateConfigurator.log.info("service not specified; skipping reload")
50
+ else
51
+ begin
52
+ @service.conditional_reload
53
+ rescue ServiceException => e
54
+ TemplateConfigurator.log.error(e.message)
55
+ TemplateConfigurator.log.error(e.output) unless e.output.nil?
56
+ end
57
+ end
58
+ end
59
+
60
+ def render
61
+ @data = {}
62
+ unless @options[:template][:json_file].nil?
63
+ json_fh = lock(@options[:template][:json_file])
64
+ @data = JSON.parse(json_fh.read)
65
+ end
66
+ TemplateConfigurator.log.debug("json:[#{@data.inspect}]")
67
+ template = ERB.new(File.read(@options[:template][:input_file]), 0, '%<>')
68
+
69
+ if @options[:template][:output_file].nil?
70
+ output_fh = STDOUT
71
+ old_output = ""
72
+ else
73
+ output_fh = lock(@options[:template][:output_file])
74
+ old_output = output_fh.read
75
+ end
76
+
77
+ new_output = template.result(binding)
78
+
79
+ new_sha1 = Digest::SHA1.hexdigest(new_output)
80
+ old_sha1 = Digest::SHA1.hexdigest(old_output)
81
+
82
+ TemplateConfigurator.log.debug("old_sha1:#{old_sha1} new_sha1:#{new_sha1}")
83
+
84
+ if new_sha1 == old_sha1
85
+ TemplateConfigurator.log.debug("SHA1 checksum unchanged")
86
+ else
87
+ TemplateConfigurator.log.info "SHA1 checksum changed"
88
+ # Write the new configuration
89
+ if @options[:dry_run]
90
+ TemplateConfigurator.log.debug("Not attemptig service reload due to dry-run")
91
+ output_fh.write new_output
92
+ elsif !@options[:template][:output_file].nil?
93
+ TemplateConfigurator.log.debug("writing new configuation (#{new_output.length} bytes)")
94
+ output_fh.rewind
95
+ output_fh.write(new_output)
96
+ output_fh.flush
97
+ output_fh.truncate(output_fh.pos)
98
+ output_fh.flush
99
+ reload()
100
+ else
101
+ TemplateConfigurator.log.debug("Not attemptig service reload due to missing output file parameter")
102
+ output_fh.write new_output
103
+ end
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,115 @@
1
+ #
2
+ # Template Configurator - A utility to generate configuration files from ERB templates and restart
3
+ # services when configuration changes.
4
+ #
5
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
6
+ #
7
+ # This file is part of Template Configurator.
8
+ #
9
+ # Template Configurator is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # Template Configurator is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with Template Configurator. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ require 'shellwords'
23
+
24
+ module TemplateConfigurator
25
+ class ServiceException < Exception
26
+ attr_accessor :code, :output
27
+ def initialize(msg, code, output = nil)
28
+ @code = code
29
+ @output = output
30
+ super(msg)
31
+ end
32
+ end
33
+
34
+ class Service
35
+ attr_accessor :options
36
+ def initialize options
37
+ @options = options
38
+ end
39
+
40
+ def command action
41
+ args = []
42
+ args << @options[:command]
43
+ args << @options[:name]
44
+ args << action
45
+ TemplateConfigurator.log.debug("command args: #{args.inspect}")
46
+ Shellwords.join(args)
47
+ end
48
+
49
+ def execute command
50
+ TemplateConfigurator.log.debug("command: #{command}")
51
+ begin
52
+ output = %x{#{command}}
53
+ exit_code = $?.exitstatus
54
+ raise ServiceException.new("execution failed; #{command} exited with status #{exit_code}", exit_code, output) unless exit_code == 0
55
+ rescue Errno::ENOENT => e
56
+ raise ServiceException.new(e.message, 1)
57
+ end
58
+ return output
59
+ end
60
+
61
+ def status
62
+ execute command(@options[:status])
63
+ end
64
+
65
+ def restart
66
+ execute command(@options[:restart])
67
+ end
68
+
69
+ def reload
70
+ execute command(@options[:reload])
71
+ end
72
+
73
+ def start
74
+ execute command(@options[:start])
75
+ end
76
+
77
+ def stop
78
+ execute command(@options[:stop])
79
+ end
80
+
81
+ def conditional_reload
82
+ # Attempt to reload service if it's running, otherwise start it.
83
+ @options[:retries].times do
84
+ begin
85
+ status_output = self.status
86
+ TemplateConfigurator.log.debug("#{@options[:name]} is running")
87
+ # If the configuration has changed, reload config
88
+ begin
89
+ reload_output = self.reload
90
+ TemplateConfigurator.log.debug("Reload command succeeded")
91
+ return reload_output
92
+ rescue ServiceException => e
93
+ TemplateConfigurator.log.error(e.message)
94
+ TemplateConfigurator.log.error(e.output) unless e.output.nil?
95
+ end
96
+ rescue ServiceException => e
97
+ # service is not running
98
+ TemplateConfigurator.log.error(e.message)
99
+ TemplateConfigurator.log.error(e.output) unless e.output.nil?
100
+ begin
101
+ start_output = self.start
102
+ TemplateConfigurator.log.debug("Start command succeeded")
103
+ return start_output
104
+ rescue ServiceException => e
105
+ TemplateConfigurator.log.error(e.message)
106
+ TemplateConfigurator.log.error(e.output) unless e.output.nil?
107
+ end
108
+ end
109
+ sleep(@options[:retry_delay])
110
+ end
111
+ # Everything else failed. Try a restart
112
+ return self.restart
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # Template Configurator - A utility to generate configuration files from ERB templates and restart
3
+ # services when configuration changes.
4
+ #
5
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
6
+ #
7
+ # This file is part of Template Configurator.
8
+ #
9
+ # Template Configurator is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # Template Configurator is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with Template Configurator. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ module TemplateConfigurator
23
+ VERSION = "1.0.0"
24
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # Template Configurator - A utility to generate configuration files from ERB templates and restart
3
+ # services when configuration changes.
4
+ #
5
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
6
+ #
7
+ # This file is part of Template Configurator.
8
+ #
9
+ # Template Configurator is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # Template Configurator is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with Template Configurator. If not, see <http://www.gnu.org/licenses/>.
21
+ #
22
+ require "template_configurator/version"
23
+ require 'template_configurator/service'
24
+ require 'template_configurator/processor'
25
+ require 'template_configurator/command_line'
26
+
27
+ module TemplateConfigurator
28
+ @@logger = nil
29
+
30
+ def self.log=(logger)
31
+ @@logger=logger
32
+ end
33
+
34
+ def self.log
35
+ @@logger
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/template_configurator/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Erik Osterman"]
6
+ gem.email = ["e@osterman.com"]
7
+ gem.summary = %q{Template Configurator is a utility to write configuration files from ERB templates.}
8
+ gem.description = %q{Template Configurator is a utility to write configuration files from ERB templates. When the file's content changes, it can then call an init script to intelligently reload the configuration. Through out the entire process exclusive file locks are used on the output file and json file to help ensure they are unmanipulated during the transformation process.}
9
+ gem.homepage = "https://github.com/osterman/template_configurator"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "template_configurator"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = TemplateConfigurator::VERSION
17
+ gem.add_runtime_dependency 'json', '>= 1.4.3'
18
+ end
data/test/test.cfg ADDED
@@ -0,0 +1,2 @@
1
+ test1: value1
2
+ test2: value2
data/test/test.cfg.erb ADDED
@@ -0,0 +1,2 @@
1
+ test1: <%=@data['key1']%>
2
+ test2: <%=@data['key2']%>
data/test/test.js ADDED
@@ -0,0 +1 @@
1
+ { "key1":"value1", "key2":"value2" }