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 +62 -0
- data/VERSION.yml +4 -0
- data/bin/appender +61 -0
- data/lib/appender.rb +116 -0
- data/spec/appender_spec.rb +163 -0
- data/spec/spec_helper.rb +8 -0
- metadata +69 -0
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
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
|
data/spec/spec_helper.rb
ADDED
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
|
+
|