configmonkey_cli 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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