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 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