configmonkey_cli 1.0.0

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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +87 -0
  6. data/Rakefile +1 -0
  7. data/VERSION +1 -0
  8. data/bin/configmonkey +9 -0
  9. data/bin/configmonkey.sh +14 -0
  10. data/configmonkey_cli.gemspec +27 -0
  11. data/lib/configmonkey_cli.rb +43 -0
  12. data/lib/configmonkey_cli/application.rb +159 -0
  13. data/lib/configmonkey_cli/application/colorize.rb +22 -0
  14. data/lib/configmonkey_cli/application/configuration.rb +38 -0
  15. data/lib/configmonkey_cli/application/configuration.tpl +0 -0
  16. data/lib/configmonkey_cli/application/core.rb +78 -0
  17. data/lib/configmonkey_cli/application/dispatch.rb +81 -0
  18. data/lib/configmonkey_cli/application/manifest.rb +316 -0
  19. data/lib/configmonkey_cli/application/manifest_actions/base.rb +90 -0
  20. data/lib/configmonkey_cli/application/manifest_actions/chmod.rb +40 -0
  21. data/lib/configmonkey_cli/application/manifest_actions/copy.rb +38 -0
  22. data/lib/configmonkey_cli/application/manifest_actions/custom.rb +52 -0
  23. data/lib/configmonkey_cli/application/manifest_actions/inplace.rb +28 -0
  24. data/lib/configmonkey_cli/application/manifest_actions/invoke.rb +45 -0
  25. data/lib/configmonkey_cli/application/manifest_actions/link.rb +46 -0
  26. data/lib/configmonkey_cli/application/manifest_actions/mkdir.rb +41 -0
  27. data/lib/configmonkey_cli/application/manifest_actions/remove.rb +46 -0
  28. data/lib/configmonkey_cli/application/manifest_actions/rsync.rb +112 -0
  29. data/lib/configmonkey_cli/application/manifest_actions/rtfm.rb +32 -0
  30. data/lib/configmonkey_cli/application/manifest_actions/sync_links.rb +60 -0
  31. data/lib/configmonkey_cli/application/manifest_actions/template.rb +38 -0
  32. data/lib/configmonkey_cli/application/output_helper.rb +30 -0
  33. data/lib/configmonkey_cli/helper.rb +62 -0
  34. data/lib/configmonkey_cli/version.rb +4 -0
  35. metadata +162 -0
@@ -0,0 +1,78 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module Core
4
+ # ===================
5
+ # = Signal trapping =
6
+ # ===================
7
+ def trap_signals
8
+ debug "Trapping INT signal..."
9
+ $interruptable_threads = []
10
+ Signal.trap("INT") do
11
+ $cm_runtime_exiting = true
12
+ $interruptable_threads.each{|thr| thr.raise(Interrupt) if thr.alive? }
13
+ Kernel.puts "Interrupting..."
14
+ end
15
+ # Signal.trap("TERM") do
16
+ # $cm_runtime_exiting = true
17
+ # Kernel.puts "Terminating..."
18
+ # end
19
+ end
20
+
21
+ def release_signals
22
+ debug "Releasing INT signal..."
23
+ Signal.trap("INT", "DEFAULT")
24
+ # Signal.trap("TERM", "DEFAULT")
25
+ end
26
+
27
+ def haltpoint thr = Thread.current
28
+ thr.raise Interrupt if $cm_runtime_exiting
29
+ end
30
+
31
+ def interruptable &block
32
+ Thread.new do
33
+ begin
34
+ thr = Thread.current
35
+ $interruptable_threads << Thread.current
36
+ thr[:return_value] = block.call(thr)
37
+ ensure
38
+ $interruptable_threads.delete(Thread.current)
39
+ end
40
+ end.join[:return_value]
41
+ end
42
+
43
+
44
+ # ==========
45
+ # = Events =
46
+ # ==========
47
+ def hook *which, &hook_block
48
+ which.each do |w|
49
+ @hooks[w.to_sym] ||= []
50
+ @hooks[w.to_sym] << hook_block
51
+ end
52
+ end
53
+
54
+ def fire which, *args
55
+ return if @disable_event_firing
56
+ sync { debug "[Event] Firing #{which} (#{@hooks[which].try(:length) || 0} handlers) #{args.map(&:class)}", 99 }
57
+ @hooks[which] && @hooks[which].each{|h| h.call(*args) }
58
+ end
59
+
60
+
61
+ # ==========
62
+ # = Logger =
63
+ # ==========
64
+ def logger_filename
65
+ "#{cm_cfg_path}/logs/configmonkey.log"
66
+ end
67
+
68
+ def logger
69
+ sync do
70
+ @logger ||= begin
71
+ FileUtils.mkdir_p(File.dirname(@opts[:logfile]))
72
+ Logger.new(@opts[:logfile], 10, 1024000)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,81 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ module Dispatch
4
+ def dispatch action = (@opts[:dispatch] || :help)
5
+ if respond_to?("dispatch_#{action}")
6
+ send("dispatch_#{action}")
7
+ else
8
+ abort("unknown action #{action}", 1)
9
+ end
10
+ end
11
+
12
+ def dispatch_help
13
+ puts @optparse.to_s
14
+ end
15
+
16
+ def dispatch_generate_manifest
17
+ puts c("Not implemented", :red)
18
+ # puts c("Generating example config `#{cfg_name}'")
19
+ # if File.exist?(cfg_file)
20
+ # abort "Conflict, file already exists: #{cfg_file}", 1
21
+ # else
22
+ # generate_config(cfg_name)
23
+ # puts c("Writing #{cfg_file}...", :green)
24
+ # end
25
+ end
26
+
27
+ def dispatch_index
28
+ Thread.abort_on_exception = true
29
+ trap_signals
30
+
31
+ @running = true
32
+ load_and_execute_manifest
33
+ rescue Manifest::ExecutionError => ex
34
+ error "\nTraceback (most recent call last):"
35
+ ex.backtrace.reverse.each_with_index {|l, i| error "\t#{"#{ex.backtrace.length - i}:".rjust(4)} #{l}" }
36
+ error "\n" << "[#{ex.class}] #{ex.message}".strip
37
+ ensure
38
+ @running = false
39
+ release_signals
40
+ end
41
+
42
+ def dispatch_info
43
+ your_version = Gem::Version.new(ConfigmonkeyCli::VERSION)
44
+ puts c ""
45
+ puts c(" Your version: ", :yellow) << c("#{your_version}", :magenta)
46
+
47
+ print c(" Current version: ", :yellow)
48
+ if @opts[:check_for_updates]
49
+ require "net/http"
50
+ print c("checking...", :blue)
51
+
52
+ begin
53
+ current_version = Gem::Version.new Net::HTTP.get_response(URI.parse(ConfigmonkeyCli::UPDATE_URL)).body.strip
54
+
55
+ if current_version > your_version
56
+ status = c("#{current_version} (consider update)", :red)
57
+ elsif current_version < your_version
58
+ status = c("#{current_version} (ahead, beta)", :green)
59
+ else
60
+ status = c("#{current_version} (up2date)", :green)
61
+ end
62
+ rescue
63
+ status = c("failed (#{$!.message})", :red)
64
+ end
65
+
66
+ print "#{"\b" * 11}#{" " * 11}#{"\b" * 11}" # reset line
67
+ puts status
68
+ else
69
+ puts c("check disabled", :red)
70
+ end
71
+
72
+ # more info
73
+ puts c ""
74
+ puts c " Configmonkey CLI is brought to you by #{c "bmonkeys.net", :green}"
75
+ puts c " Contribute @ #{c "github.com/2called-chaos/configmonkey_cli", :cyan}"
76
+ puts c " Eat bananas every day!"
77
+ puts c ""
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,316 @@
1
+ module ConfigmonkeyCli
2
+ class Application
3
+ class Manifest
4
+ MANIFEST_ACTIONS = [:chmod, :copy, :custom, :inplace, :invoke, :link, :mkdir, :rsync, :remove, :rtfm, :sync_links, :template]
5
+
6
+ class ExecutionError < ::RuntimeError
7
+ def initialize file, original_exception
8
+ @file = file
9
+ @original_exception = original_exception
10
+ end
11
+
12
+ def message
13
+ ln = ex.message[@file] && ex.message.match(/#{Regexp.escape(@file)}:([0-9]+)/)&.to_a&.second
14
+ ln ||= backtrace.reverse.detect{|l| l[@file] }&.split(":")&.second
15
+ "#{@file}#{":#{ln}" if ln}\n --- #{ex.message.gsub(@file, "<manifest>")}"
16
+ end
17
+
18
+ def backtrace
19
+ ex.backtrace
20
+ end
21
+
22
+ def original_exception
23
+ @original_exception
24
+ end
25
+ alias_method :ex, :original_exception
26
+ end
27
+
28
+ class Invalid < ExecutionError
29
+ end
30
+
31
+ class ThorHelperApp < Thor
32
+ include Thor::Actions
33
+ end
34
+
35
+ attr_reader :app, :directory, :manifest_file, :actions, :thor, :padding
36
+
37
+ def initialize app, directory, manifest_file = "manifest.rb"
38
+ @app = app
39
+ @directory = directory
40
+ init_thor!
41
+ @padding = 26
42
+ @manifest_file = File.join(directory, manifest_file || "manifest.rb")
43
+ @actions = []
44
+ @host_constraint = []
45
+ @target_directory = app.opts[:target_directory]
46
+ all do
47
+ begin
48
+ eval File.read(@manifest_file, encoding: "utf-8"), binding, @manifest_file
49
+ rescue Exception => ex
50
+ raise Invalid.new(@manifest_file, ex)
51
+ end
52
+ end
53
+ app.debug "§constraint-final:#{@host_constraint}", 120
54
+ end
55
+
56
+ def checksum *args
57
+ opts = args.extract_options!
58
+ if opts[:soft]
59
+ @_checksum_soft ||= begin
60
+ to_c = args.map(&:to_s)
61
+ to_c.unshift @manifest_file
62
+ Digest::SHA1.hexdigest(to_c.to_s)
63
+ end
64
+ else
65
+ @_checksum_hard ||= begin
66
+ to_c = args.map(&:to_s)
67
+ to_c.unshift Digest::SHA1.file(@manifest_file)
68
+ Digest::SHA1.hexdigest(to_c.to_s)
69
+ end
70
+ end
71
+ end
72
+
73
+ def to_s
74
+ "#<…::Manifest @directory=#{@directory} @actions=#{@actions.length}>"
75
+ end
76
+
77
+ def init_thor!
78
+ ThorHelperApp.source_root(directory)
79
+ @thor = ThorHelperApp.new([], { pretend: app.opts[:simulation] })
80
+
81
+ @thor.shell.class_eval do
82
+ def say_status(status, message, log_status = true)
83
+ return if quiet? || log_status == false
84
+ spaces = " " * (padding + 1)
85
+ color = log_status.is_a?(Symbol) ? log_status : :green
86
+
87
+ status = status.to_s.rjust(12)
88
+ status = set_color status, color, true if color
89
+
90
+ if($cm_current_action_name)
91
+ cm_action = $cm_current_action_name.to_s.rjust(12)
92
+ cm_action = set_color cm_action, $cm_current_action_color, true if $cm_current_action_color
93
+ end
94
+
95
+ buffer = "#{cm_action}#{status}#{spaces}#{message}"
96
+ buffer = "#{buffer}\n" unless buffer.end_with?("\n")
97
+
98
+ stdout.print(buffer)
99
+ stdout.flush
100
+ end
101
+
102
+ def ask q, *args, &block
103
+ ConfigmonkeyCli::Application.instance_method(:interruptable).bind(Object.new).call do
104
+ begin
105
+ q = "\a" << q if ENV["THOR_ASK_BELL"]
106
+ super(q, *args, &block)
107
+ rescue Interrupt => ex
108
+ Thread.main.raise(ex)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def _dump!
116
+ @actions.each do |constraint, action, instance|
117
+ begin
118
+ $cm_current_action_name = action
119
+ $cm_current_action_color = :magenta
120
+ thor.say_status :dump, instance, :black
121
+ ensure
122
+ $cm_current_action_name = $cm_current_action_color = nil
123
+ end
124
+ end
125
+ end
126
+
127
+ def _simulate!
128
+ _execute!(true)
129
+ end
130
+
131
+ def _execute! simulate = false
132
+ set_destination_root(@target_directory, false)
133
+
134
+ if simulate
135
+ thor.say_status :info, thor.set_color("---> !!! SIMULATION ONLY !!! <---", :green), :cyan
136
+ else
137
+ thor.say_status :info, thor.set_color("---> !!! HOT HOT HOT !!! <---", :red), :cyan
138
+ end
139
+ thor.say_status :info, (thor.set_color("Source Dir: ", :magenta) << thor.set_color(directory, :blue)), :cyan
140
+ thor.say_status :info, (thor.set_color(" Dest Root: ", :magenta) << thor.set_color(thor.destination_root, :blue)), :cyan
141
+ @actions.each_with_index do |(constraint, action, instance), index|
142
+ begin
143
+ $cm_current_action_index = index
144
+ $cm_current_action_name = action
145
+ $cm_current_action_color = :magenta
146
+ instance.prepare
147
+ simulate ? instance.simulate : instance.destructive
148
+ ensure
149
+ $cm_current_action_index = $cm_current_action_name = $cm_current_action_color = nil
150
+ app.haltpoint
151
+ end
152
+ end
153
+ rescue Interrupt, SystemExit
154
+ raise
155
+ rescue Exception => ex
156
+ raise(ExecutionError.new(@manifest_file, ex))
157
+ end
158
+
159
+ def _with_constraint *constraint
160
+ if @host_constraint.last == constraint
161
+ return yield if block_given?
162
+ end
163
+ if _breached_constraint?(constraint)
164
+ app.debug "§constraint-ignore:#{constraint}", 119
165
+ return
166
+ end
167
+ begin
168
+ @host_constraint << constraint
169
+ app.debug "§constraint-push:#{constraint}", 120
170
+ app.debug "§constraint-now:#{@host_constraint}", 121
171
+ yield if block_given?
172
+ ensure
173
+ app.debug "§constraint-pop:#{@host_constraint.pop}", 120
174
+ app.debug "§constraint-now:#{@host_constraint}", 121
175
+ end
176
+ end
177
+
178
+ def _breached_constraint? constraint = nil
179
+ in_constraint = catch :return_value do
180
+ [@host_constraint, [constraint || []]].each do |list|
181
+ list.each do |act, args|
182
+ case act
183
+ when :any then next
184
+ when :on
185
+ args.include?(app.opts[:hostname]) ? next : throw(:return_value, false)
186
+ when :not_on
187
+ args.include?(app.opts[:hostname]) ? throw(:return_value, false) : next
188
+ end
189
+ end
190
+ end
191
+ true
192
+ end
193
+ !in_constraint
194
+ end
195
+
196
+ def push_action *args
197
+ if $cm_current_action_index
198
+ @actions.insert $cm_current_action_index + 1, [@host_constraint.dup] + args
199
+ else
200
+ @actions.push [@host_constraint.dup] + args
201
+ end
202
+ end
203
+
204
+ def set_destination_root drpath, from_manifest = true
205
+ if from_manifest
206
+ base = File.realpath(File.expand_path(@directory))
207
+ xpath = File.expand_path(drpath[0] == "/" ? drpath : File.join(base, drpath))
208
+ if app.opts[:target_directory] != "/"
209
+ thor.say_status :warn, (thor.set_color(" Dest Root: ", :magenta) << thor.set_color(xpath, :blue) << thor.set_color(" IGNORED! -o parameter will take precedence", :red)), :red
210
+ else
211
+ @target_directory = xpath
212
+ end
213
+ else
214
+ thor.destination_root = File.realpath(File.expand_path(drpath))
215
+ end
216
+ end
217
+
218
+
219
+ # =======
220
+ # = DSL =
221
+ # =======
222
+
223
+ def padded str, *color
224
+ "".rjust(padding, " ") << (color.any? ? c(str.to_s, *color) : str.to_s)
225
+ end
226
+
227
+ def c str, *color
228
+ thor.set_color(str, *color)
229
+ end
230
+
231
+ def say str, *color
232
+ thor.say((color.any? ? c(str.to_s, *color) : str.to_s))
233
+ end
234
+
235
+ # do block no matter the `hostname`
236
+ def all &block
237
+ _with_constraint(:any, &block)
238
+ end
239
+
240
+ # do block only if `hostname` is in *hosts
241
+ def on *hosts, &block
242
+ _with_constraint(:on, hosts.flatten.map(&:to_s), &block)
243
+ end
244
+
245
+ # do block except if `hostname` is in *hosts
246
+ def not_on *hosts, &block
247
+ _with_constraint(:not_on, hosts.flatten.map(&:to_s), &block)
248
+ end
249
+
250
+ MANIFEST_ACTIONS.each do |meth|
251
+ define_method(meth) do |*args, &block|
252
+ push_action(meth, "ConfigmonkeyCli::Application::ManifestAction::#{meth.to_s.camelize}".constantize.new(app, self, *args, &block))
253
+ app.haltpoint
254
+ end
255
+ end
256
+
257
+ def ask question, opts = {}
258
+ if opts[:use_thread] != false
259
+ return app.interruptable { ask(question, opts.merge(use_thread: false)) }
260
+ end
261
+ opts[:limited_to] = opts.delete(:choose) if opts[:choose]
262
+ opts[:add_to_history] = true unless opts.key?(:add_to_history)
263
+ color = opts.delete(:color)
264
+ spaces = "".ljust(opts[:padding]) if opts[:padding]
265
+ begin
266
+ @thor.ask("#{spaces}#{question}", color, opts).presence
267
+ rescue Interrupt
268
+ app.haltpoint(Thread.main)
269
+ end
270
+ end
271
+
272
+ def yes? question, opts = {}
273
+ opts[:quit] = true unless opts.key?(:quit)
274
+ opts[:default] = true unless opts.key?(:default)
275
+ opts[:padding] = @padding unless opts.key?(:padding)
276
+ return true if app.opts[:default_yes]
277
+ return opts[:default] if app.opts[:default_accept]
278
+ o = "#{opts[:default] ? :Yn : :yN}"
279
+ o << "h" if opts[:help]
280
+ o << "q" if opts[:quit]
281
+ q = "#{question} [#{o}]"
282
+ c = opts[:color].presence || (opts[:default] ? :red : :yellow)
283
+ askopts = opts.slice(:padding, :limited_to, :choose, :add_to_history).merge(color: c, use_thread: false)
284
+ if askopts[:padding] && askopts[:padding] > 10
285
+ qq = thor.set_color(q, askopts[:color]) if askopts[:color]
286
+ q = "#{thor.set_color("?", :red)} #{qq || q}"
287
+ askopts[:padding] -= 3
288
+ end
289
+
290
+ app.interruptable do
291
+ catch :return_value do
292
+ loop {
293
+ x = (ask(q, askopts) || (opts[:default] ? :y : :n)).to_s.downcase.strip
294
+
295
+ if ["y", "yes", "1", "t", "true"].include?(x)
296
+ throw :return_value, true
297
+ elsif ["n", "no", "0", "f", "false"].include?(x)
298
+ throw :return_value, false
299
+ elsif ["h", "help", "?"].include?(x)
300
+ @thor.say_status :help, "#{opts[:help]}", :cyan
301
+ elsif ["q", "quit", "exit"].include?(x)
302
+ raise SystemExit
303
+ else
304
+ @thor.say_status :warn, "choose one of y|yes|1|t|true|n|no|0|f|false#{"|q|quit|exit" if opts[:quit]}#{"|?|h|help" if opts[:help]}", :red
305
+ end
306
+ }
307
+ end
308
+ end
309
+ end
310
+
311
+ def no? question, opts = {}
312
+ !yes?(question, opts.merge(default: false))
313
+ end
314
+ end
315
+ end
316
+ end