config_curator 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d51715ab0c44b4986703c025cdcddabbc66e21f2
4
- data.tar.gz: a935fba0f244a4fb9bc06033b220228317288f6c
3
+ metadata.gz: 739cd145342395af3f9bd1629d26006d80d92095
4
+ data.tar.gz: f34cd95e0a71591d16f01dfef21c3dc7a191bd0d
5
5
  SHA512:
6
- metadata.gz: e7f3e6fdb5c0842949f058dfd678785154c575035b0aa02fc5eb3355f081f452ea977348550c60cc1353ce2d7aa68e845fa446c146792f1d49312c7fae12720c
7
- data.tar.gz: 454d8175fd6ea0186335920a2b1e159a38fef4dc85cdc2bdbb8c16d5a5f22a95993bb8a483017e4745ab66c837e429e1c0c6a29a17fc97095c9e4620ff55fcec
6
+ metadata.gz: b3fb7a5b09056a397cda636f7975384b023a3012f65f0ca86f3408078faf0634512d0a2b30b37899d41ad23d96384d443c9e10fb904cb8946baac044e0ced7fb
7
+ data.tar.gz: e2e1ee6b3cf5e4e185fc2ba4e2a9259eeae9f176470539fdf040bf2511d0bd80293d660a9fc2705decc4947350b40707d2daa14ce81a97555447c4da70dae680
data/.travis.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  rvm:
3
4
  - 2.0.0
4
5
  - 2.1.1
5
- script: rspec
6
+ script: bundle exec rake travis
data/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Config Curator ChangeLog
2
2
 
3
- ## HEAD
3
+ ## 0.0.1
4
4
 
5
- - Initial release.
5
+ - Initial development release.
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 Palimpsest is this README and the YARD source documentation.
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
@@ -5,3 +5,5 @@ require 'rspec/core/rake_task'
5
5
  RSpec::Core::RakeTask.new :spec
6
6
 
7
7
  task default: :spec
8
+
9
+ task travis: [:spec]
data/bin/curate ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'config_curator'
4
+
5
+ ConfigCurator::CLI.start(ARGV)
@@ -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'
@@ -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