davidrichards-appender 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,62 @@
1
+ == Appender
2
+
3
+ This is a general tool for cleaning configuration files and appending text in them. The need for something like this occurs when some process is changing a configuration file but isn't necessarily the only process that manages that file. If manual changes are allowed, or other deployment scripts are touching the same file, we want to only remove our own lines and append them to the file.
4
+
5
+ To do this with a little extra safety, appender log rotates the configuration file with a time-stamped history. This way, rollbacks can be managed manually.
6
+
7
+ The workflow is very basic:
8
+
9
+ * Rotate the configuration file, in case a mistake has been made
10
+ * Remove lines that match a pattern
11
+ * Append some new configuration at the end of the file
12
+
13
+ This doesn't mean the file looks pretty when we're finished.
14
+
15
+ When possible, a fully-owned configuration file makes more sense. I.e., let Sprinkle, Puppet, Chef, whatever fully own a configuration file and make the changes in the configuration management tool, rather than appending the file.
16
+
17
+ This appender approach can easily break if:
18
+
19
+ * You get your search and replace wrong
20
+ * The configuration file is context sensitive (which is often true)
21
+
22
+
23
+ ==Usage
24
+
25
+ From Ruby:
26
+
27
+ new_content = %[
28
+ destination df_cron { file("/var/log/cron.log"); };
29
+ filter f_cron { facility(cron); };
30
+ ]
31
+ Appender.process(:config_file => '/etc/syslog-ng/syslog-ng.conf', :append => new_content, :remove => 'f_cron')
32
+
33
+ From the command line:
34
+
35
+ appender --file /etc/syslog-ng/syslog-ng.conf --remove "destination df_cron" --append "destination df_cron { file(\"/var/log/cron.log\"); };"
36
+ appender --file /etc/syslog-ng/syslog-ng.conf --remove "filter f_cron" --append "filter f_cron {facility(cron); };"
37
+
38
+ Another way to do this is simply with bash:
39
+
40
+ cat /etc/syslog-ng/syslog-ng.conf | grep -v "destination df_cron" > syslog-ng.conf.tmp
41
+ echo "destination df_cron { file(\"/var/log/cron.log\"); };" >> syslog-ng.conf.tmp
42
+ mv syslog-ng.conf.tmp /etc/syslog-ng/syslog-ng.conf
43
+
44
+ The problem with this bash approach is:
45
+
46
+ * limited regular expression support
47
+ * less error control
48
+ * manual configuration history management
49
+
50
+ Yet, I prefer the simpler bash approach when I can do it that way.
51
+
52
+ ==Installation
53
+
54
+ gem install davidrichards-appender
55
+
56
+ === Dependencies
57
+
58
+ * logrotate
59
+
60
+ ==COPYRIGHT
61
+
62
+ Copyright (c) 2009 David Richards. See LICENSE for details.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 5
data/bin/appender ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby -KU
2
+
3
+ require File.join(File.dirname(__FILE__), %w(.. lib appender))
4
+
5
+ # Make sure OpenStruct has at least this bug fixed.
6
+ class OpenStruct
7
+ def table; @table; end
8
+ end
9
+
10
+ # Gather the command-line arguments and pass them to Appender.process
11
+ class ConfigOptparse
12
+ require 'ostruct'
13
+ require 'optparse'
14
+
15
+ #
16
+ # Return a structure describing the options.
17
+ #
18
+ def self.parse(args)
19
+ # The options specified on the command line will be collected in *options*.
20
+ # We set default values here.
21
+ options = OpenStruct.new
22
+ options.config_file = ''
23
+ options.append = ''
24
+ options.remove = []
25
+
26
+ opts = OptionParser.new do |opts|
27
+ opts.banner = "Usage: appender [options]"
28
+
29
+ opts.separator ""
30
+ opts.separator "Specific options:"
31
+
32
+ opts.on("--file [path]", "Path to the config file") do |config|
33
+ options.config_file = config
34
+ end
35
+
36
+ opts.on("--remove [expression]", Array, "Lines matching expression are removed from the config") do |config|
37
+ options.remove = config
38
+ end
39
+
40
+ opts.on("--append [contents]", "New contents to add to the file") do |config|
41
+ options.append = config
42
+ end
43
+
44
+ # No argument, shows at tail. This will print an options summary.
45
+ # Try it and see!
46
+ opts.on_tail("-h", "--help", "Show this message") do
47
+ puts opts
48
+ exit
49
+ end
50
+
51
+ end
52
+
53
+ opts.parse!(args)
54
+ options
55
+ end # parse()
56
+
57
+ end # class OptparseExample
58
+
59
+ options = ConfigOptparse.parse(ARGV)
60
+
61
+ Appender.process(options.table)
data/lib/appender.rb ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Appends a configuration file, rotating the file for safety.
4
+ class Appender
5
+
6
+ require 'rubygems'
7
+ require 'fileutils'
8
+ require 'yaml'
9
+ require 'ostruct'
10
+ require 'logrotate'
11
+
12
+ def self.process(options={})
13
+ appender = new
14
+ appender.process(options)
15
+ end
16
+
17
+ # Usage:
18
+ # append_text = %{# Some configuration
19
+ # Usually on multiple lines
20
+ # key value
21
+ # etc etc...
22
+ # }
23
+ # Appender.process(
24
+ # :config_file => '/etc/syslog-ng/syslog-ng.conf',
25
+ # :append => append_text,
26
+ # :remove => [/^etc/, /^Usually on multiple lines/]
27
+ # )
28
+ def process(options={})
29
+ rotate_file(options[:config_file])
30
+ clean_file(options[:remove])
31
+ new_content = options[:append]
32
+ new_content ||= options[:content]
33
+ append_file(new_content)
34
+ clean_white_space if options[:clean]
35
+ ensure_file
36
+ end
37
+ alias :call :process
38
+
39
+ # The contents to work with.
40
+ attr_reader :content
41
+
42
+ # The file we are working on.
43
+ attr_reader :config_file
44
+
45
+ # The directory where the configuration file is stored.
46
+ attr_reader :output_directory
47
+
48
+ protected
49
+
50
+ # Makes sure at least the original contents are in place
51
+ def ensure_file
52
+ return if File.exist?(self.config_file)
53
+ File.open(self.config_file, 'w') { |f| f.write(self.content) }
54
+ end
55
+
56
+ def log_options
57
+ {
58
+ :date_time_ext => true,
59
+ :directory => self.output_directory,
60
+ :date_time_format => '%F_%T',
61
+ :count => 5
62
+ }
63
+ end
64
+
65
+ def append_file(content)
66
+ return true unless content
67
+ self.content << "\n" unless self.content[-1].chr == "\n"
68
+ self.content << content
69
+ write_content_to_file
70
+ true
71
+ end
72
+
73
+ def write_content_to_file
74
+ File.open(self.config_file, 'w+') {|f| f.write(self.content)}
75
+ end
76
+
77
+ def clean_file(expressions)
78
+ expressions = Array(expressions)
79
+ expressions.delete_if {|e| e.nil? or (e.is_a?(String) and e.empty?)}
80
+ return true if expressions.empty?
81
+ expressions.map! {|e| Regexp.new(e)}
82
+
83
+ new_content = ''
84
+ self.content.each_line do |line|
85
+ new_content << line unless expressions.any? {|e| line =~ e}
86
+ end
87
+
88
+ # Makes sure the internal state and the external state both reflect the
89
+ # filtered contents.
90
+ @content = new_content
91
+ File.open(self.config_file, 'w') {|f| f.write(new_content)}
92
+ true
93
+ end
94
+
95
+ # Removes multiple blank rows in a row
96
+ def clean_white_space
97
+ @content.gsub!(/\n{2,}/, "\n")
98
+ write_content_to_file
99
+ end
100
+
101
+ # Rotates the config file
102
+ def rotate_file(config_file)
103
+ config_file = nil if config_file.is_a?(String) and config_file.empty?
104
+ raise ArgumentError, "Must provide a configuration file: config_file => 'some_file.conf'" unless
105
+ config_file
106
+
107
+ @content = File.read(config_file)
108
+
109
+ LogRotate.rotate_file(config_file, self.log_options)
110
+
111
+ @config_file = File.expand_path(config_file)
112
+ @output_directory = File.dirname(@config_file)
113
+ true
114
+ end
115
+
116
+ end
@@ -0,0 +1,163 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "Appender" do
4
+
5
+ before do
6
+ @config_file = "/tmp/appender_spec.conf"
7
+ File.open(@config_file, 'w') {|f| f.write contents}
8
+ end
9
+
10
+ it "should be able to append a file with new contents" do
11
+ Appender.process(:config_file => @config_file, :append => "New contents")
12
+ File.read(@config_file).should match(/New contents$/)
13
+ end
14
+
15
+ it "should append new contents on a new line" do
16
+ Appender.process(:config_file => @config_file, :append => "New contents")
17
+ File.read(@config_file).should match(/^New contents$/)
18
+ end
19
+
20
+ it "should not try to work on missing files" do
21
+ missing_file = 'not_a_file'
22
+ File.should_not be_exist(missing_file)
23
+ lambda{Appender.process(:config_file => missing_file)}.should raise_error(/No such file or directory/)
24
+ File.should_not be_exist(missing_file)
25
+ end
26
+
27
+ it "should not complain if not appending anything" do
28
+ lambda{Appender.process(:config_file => @config_file)}.should_not raise_error
29
+ end
30
+
31
+ it "should leave an identical file in place, even if it wasn't changed" do
32
+ Appender.process(:config_file => @config_file)
33
+ read_contents.should eql(contents)
34
+ end
35
+
36
+ it "should remove all lines that match a pattern" do
37
+ Appender.process(:config_file => @config_file, :remove => '#')
38
+ read_contents.should_not match(/\#/)
39
+ end
40
+
41
+ it "should remove the whole line, not just the matching portion of the remove parameter" do
42
+ Appender.process(:config_file => @config_file, :remove => '#')
43
+ read_contents.should eql(comment_less_expectation)
44
+ end
45
+
46
+ it "should change the stored content with the filtered results" do
47
+ a = Appender.new
48
+ a.process(:config_file => @config_file, :remove => '#')
49
+ a.content.should eql(comment_less_expectation)
50
+ end
51
+
52
+ it "should be able to take an array of filters" do
53
+ Appender.process(:config_file => @config_file, :remove => ['#', 'value'])
54
+ read_contents.should eql(comment_and_value_less_expectation)
55
+ end
56
+
57
+ it "should be able to take regular expressions for filters" do
58
+ Appender.process(:config_file => @config_file, :remove => [/#/, /value/])
59
+ read_contents.should eql(comment_and_value_less_expectation)
60
+ end
61
+
62
+ it "should append the new contents after removing the matching contents" do
63
+ read_contents.should match(/^key value/)
64
+ Appender.process(:config_file => @config_file, :remove => /^key value/, :append => "key new_value")
65
+ read_contents.should_not match(/^key value/)
66
+ read_contents.should match(/^key new_value/)
67
+ end
68
+
69
+ it "should be able to remove excessive white space" do
70
+ Appender.process(:config_file => @config_file, :remove => [/#/, /value/], :clean => true)
71
+ read_contents.should eql(clean_expectation)
72
+ end
73
+
74
+ it "should log rotate the file, keep 5 versions of the configuration file in the directory" do
75
+ STDOUT.sync = true
76
+ puts "\n\n******************************\nRunning a 7-second spec\n******************************\n\n"
77
+ 7.times do
78
+ Appender.process(:config_file => @config_file)
79
+ sleep 1 # Ensure a new timestamp
80
+ end
81
+ ls = `ls #{File.dirname(@config_file)}/#{File.split(@config_file).last}.*`.split("\n")
82
+ ls.size.should eql(5)
83
+ end
84
+
85
+ it "should not break the original configuration file if it cannot be log rotated" do
86
+ def LogRotate.rotate_file(*args); raise(StandardError, "Testing rotate_file"); end
87
+ lambda{Appender.process(:config_file => @config_file, :append => "New contents")}.should
88
+ raise_error(StandardError, "Testing rotate_file")
89
+ File.read(@config_file).should eql(contents)
90
+ end
91
+
92
+ after(:all) do
93
+ `rm -rf /tmp/appender_spec.conf*`
94
+ end
95
+ end
96
+
97
+ describe Appender, "Log Rotate" do
98
+
99
+ before do
100
+ @config_file = "/tmp/appender_spec.conf"
101
+ File.open(@config_file, 'w') {|f| f.write contents}
102
+ end
103
+
104
+ # Running this on its own to reduce the side effects of stubbing out log rotation.
105
+ it "should not break the original configuration file if it cannot be log rotated" do
106
+ LogRotate.stub!(:rotate_file).and_return(lambda{raise(StandardError, "Testing rotate_file")})
107
+ # def LogRotate.rotate_file(*args); raise(StandardError, ); end
108
+ lambda{Appender.process(:config_file => @config_file, :append => "New contents")}.should
109
+ raise_error(StandardError, "Testing rotate_file")
110
+ File.read(@config_file).should eql(contents)
111
+ end
112
+
113
+ after(:all) do
114
+ `rm -rf /tmp/appender_spec.conf*`
115
+ end
116
+ end
117
+
118
+
119
+ def read_contents
120
+ File.read(@config_file)
121
+ end
122
+
123
+ def contents
124
+ %{
125
+ # This is like a config file
126
+
127
+ # It has comments (this)
128
+
129
+ # It has key/value pairs:
130
+ key value
131
+
132
+ # It has some more-specific values that may be interesting to test
133
+ destination df_cron { file("/var/log/cron.log"); };
134
+
135
+ # It does NOT have trailing whitespace}
136
+ end
137
+
138
+ def comment_less_expectation
139
+ %{
140
+
141
+
142
+ key value
143
+
144
+ destination df_cron { file("/var/log/cron.log"); };
145
+
146
+ }
147
+ end
148
+
149
+ def comment_and_value_less_expectation
150
+ %{
151
+
152
+
153
+
154
+ destination df_cron { file("/var/log/cron.log"); };
155
+
156
+ }
157
+ end
158
+
159
+ def clean_expectation
160
+ %{
161
+ destination df_cron { file("/var/log/cron.log"); };
162
+ }
163
+ end
@@ -0,0 +1,8 @@
1
+ $: << File.join(File.dirname(__FILE__), "/../lib")
2
+ require 'rubygems'
3
+ require 'spec'
4
+ require 'appender'
5
+
6
+ Spec::Runner.configure do |config|
7
+
8
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: davidrichards-appender
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - David Richards
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-24 00:00:00 -07:00
13
+ default_executable: appender
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: logrotate
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Appends configuration to files
26
+ email: drichards@showcase60.com
27
+ executables:
28
+ - appender
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README.rdoc
35
+ - VERSION.yml
36
+ - bin/appender
37
+ - lib/appender.rb
38
+ - spec/appender_spec.rb
39
+ - spec/spec_helper.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/davidrichards/appender
42
+ licenses:
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --inline-source
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.3.5
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: Appends configuration to files
68
+ test_files: []
69
+