config_curator 0.0.0 → 0.0.1
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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGELOG.md +2 -2
- data/Guardfile +10 -0
- data/README.md +1 -1
- data/Rakefile +2 -0
- data/bin/curate +5 -0
- data/config_curator.gemspec +3 -0
- data/lib/config_curator.rb +8 -0
- data/lib/config_curator/cli.rb +71 -0
- data/lib/config_curator/collection.rb +137 -0
- data/lib/config_curator/package_lookup.rb +77 -0
- data/lib/config_curator/unit.rb +122 -0
- data/lib/config_curator/units/component.rb +76 -0
- data/lib/config_curator/units/config_file.rb +76 -0
- data/lib/config_curator/units/symlink.rb +31 -0
- data/lib/config_curator/version.rb +2 -1
- data/spec/cli_spec.rb +69 -0
- data/spec/collection_spec.rb +280 -0
- data/spec/package_lookup_spec.rb +59 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit_spec.rb +190 -0
- data/spec/units/component_spec.rb +59 -0
- data/spec/units/config_file_spec.rb +84 -0
- data/spec/units/symlink_spec.rb +46 -0
- metadata +55 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 739cd145342395af3f9bd1629d26006d80d92095
|
4
|
+
data.tar.gz: f34cd95e0a71591d16f01dfef21c3dc7a191bd0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b3fb7a5b09056a397cda636f7975384b023a3012f65f0ca86f3408078faf0634512d0a2b30b37899d41ad23d96384d443c9e10fb904cb8946baac044e0ced7fb
|
7
|
+
data.tar.gz: e2e1ee6b3cf5e4e185fc2ba4e2a9259eeae9f176470539fdf040bf2511d0bd80293d660a9fc2705decc4947350b40707d2daa14ce81a97555447c4da70dae680
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
guard :rspec, cmd: 'bundle exec rspec --color --format Fuubar' do
|
2
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
3
|
+
watch(%r{^lib/config_curator/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
4
|
+
watch(%r{^spec/.+_spec\.rb$})
|
5
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
6
|
+
end
|
7
|
+
|
8
|
+
guard :yard do
|
9
|
+
watch(%r{^lib/(.+)\.rb$})
|
10
|
+
end
|
data/README.md
CHANGED
@@ -32,7 +32,7 @@ $ gem install config_curator
|
|
32
32
|
````
|
33
33
|
## Documentation
|
34
34
|
|
35
|
-
The primary documentation for
|
35
|
+
The primary documentation for Config Curator is this README and the YARD source documentation.
|
36
36
|
|
37
37
|
YARD documentation for all gem versions is hosted on the
|
38
38
|
[Config Curator gem page](https://rubygems.org/gems/config_curator).
|
data/Rakefile
CHANGED
data/bin/curate
ADDED
data/config_curator.gemspec
CHANGED
@@ -20,6 +20,9 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.required_ruby_version = '>= 2.0.0'
|
22
22
|
|
23
|
+
spec.add_dependency 'activesupport', '~> 4.1.0'
|
24
|
+
spec.add_dependency 'thor', '~> 0.19.1'
|
25
|
+
|
23
26
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
24
27
|
spec.add_development_dependency 'rake', '~> 10.3.1'
|
25
28
|
spec.add_development_dependency 'bump', '~> 0.5.0'
|
data/lib/config_curator.rb
CHANGED
@@ -1,4 +1,12 @@
|
|
1
1
|
require 'config_curator/version'
|
2
|
+
require 'config_curator/package_lookup'
|
3
|
+
require 'config_curator/collection'
|
4
|
+
require 'config_curator/cli'
|
5
|
+
require 'config_curator/unit'
|
6
|
+
require 'config_curator/units/config_file'
|
7
|
+
require 'config_curator/units/symlink'
|
8
|
+
require 'config_curator/units/component'
|
2
9
|
|
10
|
+
# Simple and intelligent configuration file management.
|
3
11
|
module ConfigCurator
|
4
12
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
module ConfigCurator
|
5
|
+
|
6
|
+
class CLI < Thor
|
7
|
+
|
8
|
+
class_option :verbose, type: :boolean, aliases: %i(v)
|
9
|
+
class_option :quiet, type: :boolean, aliases: %i(q)
|
10
|
+
class_option :debug, type: :boolean
|
11
|
+
|
12
|
+
# Installs the collection.
|
13
|
+
# @param manifest [String] path to the manifest file to use
|
14
|
+
# @return [Boolean] value of {Collection#install} or {Collection#install?}
|
15
|
+
desc "install", "Installs all units in collection."
|
16
|
+
option :dryrun, type: :boolean, aliases: %i(n),
|
17
|
+
desc: %q{Only simulate the install. Don't make any actual changes.}
|
18
|
+
def install manifest='manifest.yml'
|
19
|
+
unless File.exists? manifest
|
20
|
+
logger.fatal { "Manifest file '#{manifest}' does not exist." }
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
|
24
|
+
collection.load_manifest manifest
|
25
|
+
result = options[:dryrun] ? collection.install? : collection.install
|
26
|
+
|
27
|
+
msg = "Install #{'simulation ' if options[:dryrun]}" + \
|
28
|
+
if result
|
29
|
+
'completed without error.'
|
30
|
+
elsif result.nil?
|
31
|
+
'failed.'
|
32
|
+
else
|
33
|
+
'failed. No changes were made.'
|
34
|
+
end
|
35
|
+
|
36
|
+
if result then logger.info msg else logger.error msg end
|
37
|
+
return result
|
38
|
+
end
|
39
|
+
|
40
|
+
no_commands do
|
41
|
+
|
42
|
+
# Makes a collection object to use for the instance.
|
43
|
+
# @return [Collection] the collection object
|
44
|
+
def collection
|
45
|
+
@collection ||= Collection.new logger: logger
|
46
|
+
end
|
47
|
+
|
48
|
+
# Logger instance to use.
|
49
|
+
# @return [Logger] logger instance
|
50
|
+
def logger
|
51
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
52
|
+
log.progname = 'curate'
|
53
|
+
log.formatter = proc do |severity, _, _, msg|
|
54
|
+
"#{severity} -- #{msg}\n"
|
55
|
+
end
|
56
|
+
log.level = \
|
57
|
+
if options[:debug]
|
58
|
+
Logger::DEBUG
|
59
|
+
elsif options[:verbose]
|
60
|
+
Logger::INFO
|
61
|
+
elsif options[:quiet]
|
62
|
+
Logger::FATAL
|
63
|
+
else
|
64
|
+
Logger::WARN
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'active_support/core_ext/string'
|
2
|
+
require 'logger'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module ConfigCurator
|
6
|
+
|
7
|
+
class Collection
|
8
|
+
|
9
|
+
# Supported unit types.
|
10
|
+
UNIT_TYPES = %i(unit component config_file symlink)
|
11
|
+
|
12
|
+
# The possible attributes specific to each unit type.
|
13
|
+
# This should not include generic attributes
|
14
|
+
# such as {Unit#source} and {Unit#destination}.
|
15
|
+
UNIT_ATTRIBUTESS = {
|
16
|
+
unit: %i(hosts packages),
|
17
|
+
component: %i(hosts packages fmode dmode owner group),
|
18
|
+
config_file: %i(hosts packages fmode owner group),
|
19
|
+
symlink: %i(hosts packages),
|
20
|
+
}
|
21
|
+
|
22
|
+
attr_accessor :logger, :manifest, :units
|
23
|
+
|
24
|
+
def initialize manifest_path: nil, logger: nil
|
25
|
+
self.logger = logger unless logger.nil?
|
26
|
+
self.load_manifest manifest_path unless manifest_path.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Logger instance to use.
|
30
|
+
# @return [Logger] logger instance
|
31
|
+
def logger
|
32
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
33
|
+
log.progname = self.class.name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Loads the manifest from file.
|
38
|
+
# @param file [Hash] the yaml file to load
|
39
|
+
# @return [Hash] the loaded manifest
|
40
|
+
def load_manifest file
|
41
|
+
self.manifest = YAML.load_file file
|
42
|
+
end
|
43
|
+
|
44
|
+
# Unit objects defined by the manifest and organized by type.
|
45
|
+
# @return [Hash] keys are pluralized unit types from {UNIT_TYPES}
|
46
|
+
def units
|
47
|
+
@units ||= {}.tap do |u|
|
48
|
+
UNIT_TYPES.each do |type|
|
49
|
+
k = type.to_s.pluralize.to_sym
|
50
|
+
u[k] = []
|
51
|
+
|
52
|
+
if manifest
|
53
|
+
manifest[k].each do |v|
|
54
|
+
u[k] << create_unit(type, attributes: v)
|
55
|
+
end unless manifest[k].nil?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Installs all units from the manifest.
|
62
|
+
# @return [Boolean, nil] if units were installed or nil when fails mid-install
|
63
|
+
def install
|
64
|
+
return false unless install? quiet: !(logger.level == Logger::DEBUG)
|
65
|
+
|
66
|
+
UNIT_TYPES.each do |t|
|
67
|
+
type_name = t.to_s.humanize capitalize: false
|
68
|
+
|
69
|
+
units[t.to_s.pluralize.to_sym].each do |unit|
|
70
|
+
begin
|
71
|
+
if unit.install
|
72
|
+
logger.info { "Installed #{type_name}: #{unit.source} ⇨ #{unit.destination_path}" }
|
73
|
+
end
|
74
|
+
rescue Unit::InstallFailed => e
|
75
|
+
logger.fatal { "Halting install! Install attempt failed for #{type_name}: #{e}" }
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
# Checks all units in the manifest for any detectable install issues.
|
84
|
+
# @param quiet [Boolean] suppress some {#logger} output
|
85
|
+
# @return [Boolean] if units can be installed
|
86
|
+
def install? quiet: false
|
87
|
+
result = true
|
88
|
+
UNIT_TYPES.each do |t|
|
89
|
+
type_name = t.to_s.humanize capitalize: false
|
90
|
+
|
91
|
+
units[t.to_s.pluralize.to_sym].each do |unit|
|
92
|
+
begin
|
93
|
+
if unit.install?
|
94
|
+
logger.info { "Testing install for #{type_name}: #{unit.source} ⇨ #{unit.destination_path}" }
|
95
|
+
end unless quiet
|
96
|
+
rescue Unit::InstallFailed => e
|
97
|
+
result = false
|
98
|
+
logger.error { "Cannot install #{type_name}: #{e}" }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
result
|
103
|
+
end
|
104
|
+
|
105
|
+
# Creates a new unit object for the collection.
|
106
|
+
# @param type [Symbol] a unit type in {UNIT_TYPES}
|
107
|
+
# @param attributes [Hash] attributes for the unit from {UNIT_ATTRIBUTESS}
|
108
|
+
# @return [Unit] the unit object of the appropriate subclass
|
109
|
+
def create_unit type, attributes: {}
|
110
|
+
options = {}
|
111
|
+
%i(root).each do |k|
|
112
|
+
options[k] = manifest[k] unless manifest[k].nil?
|
113
|
+
end if manifest
|
114
|
+
|
115
|
+
"#{self.class.name.split('::').first}::#{type.to_s.camelize}".constantize
|
116
|
+
.new(options: options, logger: logger).tap do |unit|
|
117
|
+
{src: :source, dst: :destination}.each do |k, v|
|
118
|
+
unit.send "#{v}=".to_sym, attributes[k] unless attributes[k].nil?
|
119
|
+
end
|
120
|
+
|
121
|
+
UNIT_ATTRIBUTESS[type].each do |v|
|
122
|
+
unit.send "#{v}=".to_sym, defaults[v] unless defaults[v].nil?
|
123
|
+
unit.send "#{v}=".to_sym, attributes[v] unless attributes[v].nil?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
# Hash of any defaults given in the manifest.
|
131
|
+
def defaults
|
132
|
+
return {} unless manifest
|
133
|
+
manifest[:defaults].nil? ? {} : manifest[:defaults]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
module ConfigCurator
|
5
|
+
|
6
|
+
class PackageLookup
|
7
|
+
|
8
|
+
# Error when a package lookup cannot be completed.
|
9
|
+
class LookupFailed < RuntimeError; end
|
10
|
+
|
11
|
+
# Default list of supported package tools.
|
12
|
+
TOOLS = %i(dpkg pacman)
|
13
|
+
|
14
|
+
attr_accessor :tool, :tools
|
15
|
+
|
16
|
+
def initialize tool: nil
|
17
|
+
self.tool = tool
|
18
|
+
end
|
19
|
+
|
20
|
+
# Package tools that support package lookup ordered by preference.
|
21
|
+
# @return [Array] list of supported package tools
|
22
|
+
def tools
|
23
|
+
@tools ||= TOOLS
|
24
|
+
end
|
25
|
+
|
26
|
+
# The package tool to use for this instance.
|
27
|
+
# @return [Symbol] tool to use
|
28
|
+
def tool
|
29
|
+
return @tool if @tool
|
30
|
+
|
31
|
+
tools.each do |cmd|
|
32
|
+
if command? cmd
|
33
|
+
return @tool = cmd
|
34
|
+
end
|
35
|
+
end
|
36
|
+
@tool
|
37
|
+
end
|
38
|
+
|
39
|
+
# Checks if package is installed.
|
40
|
+
# @param package [String] package name to check
|
41
|
+
# @return [Boolean] if package is installed
|
42
|
+
def installed? package
|
43
|
+
fail LookupFailed, 'No supported package tool found.' if tool.nil?
|
44
|
+
send tool, package
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Checks if command exists.
|
50
|
+
# @param command [String] command name to check
|
51
|
+
# @return [String, nil] full path to command or nil if not found
|
52
|
+
def command? command
|
53
|
+
MakeMakefile::Logging.instance_variable_set :@logfile, File::NULL
|
54
|
+
MakeMakefile::Logging.quiet = true
|
55
|
+
MakeMakefile.find_executable command.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Tool specific package lookup methods below.
|
60
|
+
#
|
61
|
+
|
62
|
+
def dpkg package
|
63
|
+
cmd = command? 'dpkg'
|
64
|
+
Open3.popen3 cmd, '-s', package do |_, _ , _, wait_thr|
|
65
|
+
wait_thr.value.to_i == 0
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def pacman package
|
70
|
+
cmd = command? 'pacman'
|
71
|
+
Open3.popen3 cmd, '-qQ', package do |_, _ , _, wait_thr|
|
72
|
+
wait_thr.value.to_i == 0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module ConfigCurator
|
5
|
+
|
6
|
+
class Unit
|
7
|
+
|
8
|
+
# Error if the unit will fail to install.
|
9
|
+
class InstallFailed < RuntimeError; end
|
10
|
+
|
11
|
+
attr_accessor :logger, :source, :destination, :hosts, :packages
|
12
|
+
|
13
|
+
# Default {#options}.
|
14
|
+
DEFAULT_OPTIONS = {
|
15
|
+
# Unit installed relative to this path.
|
16
|
+
root: Dir.home,
|
17
|
+
|
18
|
+
# Package tool to use. See #package_lookup.
|
19
|
+
package_tool: nil,
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize options: {}, logger: nil
|
23
|
+
self.options options
|
24
|
+
self.logger = logger unless logger.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Uses {DEFAULT_OPTIONS} as initial value.
|
28
|
+
# @param options [Hash] merged with current options
|
29
|
+
# @return [Hash] current options
|
30
|
+
def options options = {}
|
31
|
+
@options ||= DEFAULT_OPTIONS
|
32
|
+
@options = @options.merge options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Logger instance to use.
|
36
|
+
# @return [Logger] logger instance
|
37
|
+
def logger
|
38
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
39
|
+
log.progname = self.class.name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Full path to source.
|
44
|
+
# @return [String] expanded path to source
|
45
|
+
def source_path
|
46
|
+
File.expand_path source unless source.nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Full path to destination.
|
50
|
+
# @return [String] expanded path to destination
|
51
|
+
def destination_path
|
52
|
+
File.expand_path File.join(options[:root], destination) unless destination.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Unit will be installed on these hosts.
|
56
|
+
# If empty, installed on any host.
|
57
|
+
# @return [Array] list of hostnames
|
58
|
+
def hosts
|
59
|
+
@hosts ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
# Unit installed only if listed packages are installed.
|
63
|
+
# @return [Array] list of package names
|
64
|
+
def packages
|
65
|
+
@packages ||= []
|
66
|
+
end
|
67
|
+
|
68
|
+
# A {PackageLookup} object for this unit.
|
69
|
+
def package_lookup
|
70
|
+
@package_lookup ||= PackageLookup.new tool: options[:package_tool]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Installs the unit.
|
74
|
+
def install
|
75
|
+
return false unless install?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Checks if the unit should be installed.
|
80
|
+
# @return [Boolean] if the unit should be installed
|
81
|
+
def install?
|
82
|
+
return false unless allowed_host?
|
83
|
+
return false unless packages_installed?
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
# Checks if the unit should be installed on this host.
|
88
|
+
# @return [Boolean] if the hostname is in {#hosts}
|
89
|
+
def allowed_host?
|
90
|
+
return true if hosts.empty?
|
91
|
+
hosts.include? hostname
|
92
|
+
end
|
93
|
+
|
94
|
+
# Checks if the packages required for this unit are installed.
|
95
|
+
# @return [Boolean] if the packages in {#packages} are installed
|
96
|
+
def packages_installed?
|
97
|
+
packages.map(&method(:pkg_exists?)).delete_if{ |e| e }.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# @return [String] the machine hostname
|
103
|
+
def hostname
|
104
|
+
Socket.gethostname
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [Boolean] if the package exists on the system
|
108
|
+
def pkg_exists? pkg
|
109
|
+
package_lookup.installed? pkg
|
110
|
+
end
|
111
|
+
|
112
|
+
# Checks if command exists.
|
113
|
+
# @param command [String] command name to check
|
114
|
+
# @return [String, nil] full path to command or nil if not found
|
115
|
+
def command? command
|
116
|
+
MakeMakefile::Logging.instance_variable_set :@logfile, File::NULL
|
117
|
+
MakeMakefile::Logging.quiet = true
|
118
|
+
MakeMakefile.find_executable command.to_s
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|