inprovise 0.2.2

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 (56) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +28 -0
  4. data/Gemfile +9 -0
  5. data/LICENSE +8 -0
  6. data/README.md +197 -0
  7. data/Rakefile.rb +9 -0
  8. data/bin/rig +5 -0
  9. data/inprovise.gemspec +22 -0
  10. data/lib/inprovise/channel/ssh.rb +202 -0
  11. data/lib/inprovise/cli/group.rb +86 -0
  12. data/lib/inprovise/cli/node.rb +95 -0
  13. data/lib/inprovise/cli/provision.rb +84 -0
  14. data/lib/inprovise/cli.rb +105 -0
  15. data/lib/inprovise/cmd_channel.rb +100 -0
  16. data/lib/inprovise/cmd_helper.rb +150 -0
  17. data/lib/inprovise/control.rb +326 -0
  18. data/lib/inprovise/execution_context.rb +277 -0
  19. data/lib/inprovise/group.rb +67 -0
  20. data/lib/inprovise/helper/cygwin.rb +43 -0
  21. data/lib/inprovise/helper/linux.rb +181 -0
  22. data/lib/inprovise/helper/windows.rb +123 -0
  23. data/lib/inprovise/infra.rb +122 -0
  24. data/lib/inprovise/local_file.rb +120 -0
  25. data/lib/inprovise/logger.rb +79 -0
  26. data/lib/inprovise/node.rb +271 -0
  27. data/lib/inprovise/remote_file.rb +128 -0
  28. data/lib/inprovise/resolver.rb +36 -0
  29. data/lib/inprovise/script.rb +175 -0
  30. data/lib/inprovise/script_index.rb +46 -0
  31. data/lib/inprovise/script_runner.rb +110 -0
  32. data/lib/inprovise/sniff.rb +46 -0
  33. data/lib/inprovise/sniffer/linux.rb +64 -0
  34. data/lib/inprovise/sniffer/platform.rb +46 -0
  35. data/lib/inprovise/sniffer/unknown.rb +11 -0
  36. data/lib/inprovise/sniffer/windows.rb +32 -0
  37. data/lib/inprovise/template/inprovise.rb.erb +92 -0
  38. data/lib/inprovise/template.rb +38 -0
  39. data/lib/inprovise/trigger_runner.rb +36 -0
  40. data/lib/inprovise/version.rb +10 -0
  41. data/lib/inprovise.rb +145 -0
  42. data/test/cli_test.rb +314 -0
  43. data/test/cli_test_helper.rb +19 -0
  44. data/test/dsl_test.rb +43 -0
  45. data/test/fixtures/example.txt +1 -0
  46. data/test/fixtures/include.rb +4 -0
  47. data/test/fixtures/inprovise.rb +1 -0
  48. data/test/fixtures/myscheme.rb +1 -0
  49. data/test/infra_test.rb +189 -0
  50. data/test/local_file_test.rb +64 -0
  51. data/test/remote_file_test.rb +106 -0
  52. data/test/resolver_test.rb +66 -0
  53. data/test/script_index_test.rb +53 -0
  54. data/test/script_test.rb +56 -0
  55. data/test/test_helper.rb +237 -0
  56. metadata +182 -0
@@ -0,0 +1,271 @@
1
+ # Infrastructure node for Inprovise
2
+ #
3
+ # Author:: Martin Corino
4
+ # License:: Distributes under the same license as Ruby
5
+
6
+ require 'json'
7
+
8
+ class Inprovise::Infrastructure::Node < Inprovise::Infrastructure::Target
9
+ attr_reader :host, :user
10
+
11
+ def initialize(name, config={})
12
+ @host = config[:host] || name
13
+ @user = config[:user] || 'root'
14
+ @channel = nil
15
+ @helper = nil
16
+ @history = []
17
+ @user_nodes = {}
18
+ super(name, config)
19
+ end
20
+
21
+ def channel
22
+ @channel ||= Inprovise::CmdChannel.open(self, config[:channel])
23
+ end
24
+
25
+ def helper
26
+ @helper ||= Inprovise::CmdHelper.get(self, config[:helper])
27
+ end
28
+
29
+ def disconnect!
30
+ @user_nodes.each_value {|n| n.disconnect! }
31
+ @channel.close if @channel
32
+ self
33
+ end
34
+
35
+ # generic command execution
36
+
37
+ def run(cmd, opts={})
38
+ log.execute("RUN: #{cmd}") if Inprovise.verbosity > 0
39
+ if should_run?(cmd, opts)
40
+ really_run(cmd, opts)
41
+ else
42
+ cached_run(cmd, opts)
43
+ end
44
+ end
45
+
46
+ def sudo(cmd, opts={})
47
+ log.execute("SUDO: #{cmd}") if Inprovise.verbosity > 0
48
+ opts = opts.merge({:sudo => true})
49
+ if should_run?(cmd, opts)
50
+ really_run(cmd, opts)
51
+ else
52
+ cached_run(cmd, opts)
53
+ end
54
+ end
55
+
56
+ # file management
57
+
58
+ def upload(from, to)
59
+ log.execute("UPLOAD: #{from} => #{to}") if Inprovise.verbosity > 0
60
+ helper.upload(from, to)
61
+ end
62
+
63
+ def download(from, to)
64
+ log.execute("DOWLOAD: #{to} <= #{from}") if Inprovise.verbosity > 0
65
+ helper.download(from, to)
66
+ end
67
+
68
+ # basic commands
69
+
70
+ def echo(arg)
71
+ log.execute("ECHO: #{arg}") if Inprovise.verbosity > 0
72
+ out = helper.echo(arg)
73
+ log.execute("ECHO: #{out}") if Inprovise.verbosity > 0
74
+ out
75
+ end
76
+
77
+ def env(var)
78
+ log.execute("ENV: #{var}") if Inprovise.verbosity > 0
79
+ val = helper.env(var)
80
+ log.execute("ENV: #{val}") if Inprovise.verbosity > 0
81
+ val
82
+ end
83
+
84
+ def cat(path)
85
+ log.execute("CAT: #{path}") if Inprovise.verbosity > 0
86
+ out = helper.cat(path)
87
+ log.execute("CAT: #{out}") if Inprovise.verbosity > 0
88
+ out
89
+ end
90
+
91
+ def hash_for(path)
92
+ log.execute("HASH_FOR: #{path}") if Inprovise.verbosity > 0
93
+ hsh = helper.hash_for(path)
94
+ log.execute("HASH_FOR: #{hsh}") if Inprovise.verbosity > 0
95
+ hsh
96
+ end
97
+
98
+ def mkdir(path)
99
+ log.execute("MKDIR: #{path}") if Inprovise.verbosity > 0
100
+ helper.mkdir(path)
101
+ end
102
+
103
+ def exists?(path)
104
+ log.execute("EXISTS?: #{path}") if Inprovise.verbosity > 0
105
+ rc = helper.exists?(path)
106
+ log.execute("EXISTS?: #{rc}") if Inprovise.verbosity > 0
107
+ rc
108
+ end
109
+
110
+ def file?(path)
111
+ log.execute("FILE?: #{path}") if Inprovise.verbosity > 0
112
+ rc = helper.file?(path)
113
+ log.execute("FILE?: #{rc}") if Inprovise.verbosity > 0
114
+ rc
115
+ end
116
+
117
+ def directory?(path)
118
+ log.execute("DIRECTORY?: #{path}") if Inprovise.verbosity > 0
119
+ rc = helper.directory?(path)
120
+ log.execute("DIRECTORY?: #{rc}") if Inprovise.verbosity > 0
121
+ rc
122
+ end
123
+
124
+ def copy(from, to)
125
+ log.execute("COPY: #{from} #{to}") if Inprovise.verbosity > 0
126
+ helper.copy(from, to)
127
+ end
128
+
129
+ def delete(path)
130
+ log.execute("DELETE: #{path}") if Inprovise.verbosity > 0
131
+ helper.delete(path)
132
+ end
133
+
134
+ def permissions(path)
135
+ log.execute("PERMISSIONS: #{path}") if Inprovise.verbosity > 0
136
+ perm = helper.permissions(path)
137
+ log.execute("PERMISSIONS: #{'%o' % perm}") if Inprovise.verbosity > 0
138
+ perm
139
+ end
140
+
141
+ def set_permissions(path, perm)
142
+ log.execute("SET_PERMISSIONS: #{path} #{'%o' % perm}") if Inprovise.verbosity > 0
143
+ helper.set_permissions(path, perm)
144
+ end
145
+
146
+ def owner(path)
147
+ log.execute("OWNER: #{path}") if Inprovise.verbosity > 0
148
+ owner = helper.owner(path)
149
+ log.execute("OWNER: #{owner}") if Inprovise.verbosity > 0
150
+ owner
151
+ end
152
+
153
+ def group(path)
154
+ log.execute("GROUP: #{path}") if Inprovise.verbosity > 0
155
+ group = helper.group(path)
156
+ log.execute("OWNER: #{group}") if Inprovise.verbosity > 0
157
+ group
158
+ end
159
+
160
+ def set_owner(path, user, group=nil)
161
+ log.execute("SET_OWNER: #{path} #{user}#{group ? " #{group}" : ''}") if Inprovise.verbosity > 0
162
+ helper.set_owner(path, user, group)
163
+ end
164
+
165
+ def binary_exists?(bin)
166
+ log.execute("BINARY_EXISTS?: #{bin}") if Inprovise.verbosity > 0
167
+ rc = helper.binary_exists?(bin)
168
+ log.execute("BINARY_EXISTS?: #{rc}") if Inprovise.verbosity > 0
169
+ rc
170
+ end
171
+
172
+ def log
173
+ @log ||= Inprovise::Logger.new(self, nil)
174
+ end
175
+
176
+ def log_to(log)
177
+ @log = log
178
+ end
179
+
180
+ def for_user(new_user, user_key=nil)
181
+ new_user = new_user.to_s
182
+ return self if self.user == new_user
183
+ user_key ||= new_user
184
+ return @user_nodes[user_key] if @user_nodes[user_key]
185
+ new_node = self.dup
186
+ new_node.prepare_connection_for_user!(new_user)
187
+ @user_nodes[user_key] = new_node
188
+ new_node
189
+ end
190
+
191
+ def for_dir(path)
192
+ user_key = "#{self.user}:#{path}"
193
+ return @user_nodes[user_key] if @user_nodes[user_key]
194
+ new_node = self.dup
195
+ new_node.prepare_connection_for_user!(self.user)
196
+ end
197
+
198
+ def prepare_connection_for_user!(new_user)
199
+ @user = new_user
200
+ @channel = nil
201
+ @helper = nil
202
+ @user_nodes = {}
203
+ @history = []
204
+ @log = Inprovise::Logger.new(self, @log.task) if @log
205
+ end
206
+
207
+ def to_s
208
+ "#{name}(#{user}@#{host})"
209
+ end
210
+
211
+ def safe_config
212
+ scfg = config.dup
213
+ scfg.delete :passphrase
214
+ scfg.delete :password
215
+ scfg.delete :credentials
216
+ scfg
217
+ end
218
+ protected :safe_config
219
+
220
+ def to_json(*a)
221
+ {
222
+ JSON.create_id => self.class.name,
223
+ :data => {
224
+ :name => name,
225
+ :config => safe_config
226
+ }
227
+ }.to_json(*a)
228
+ end
229
+
230
+ def self.json_create(o)
231
+ data = o[:data]
232
+ new(data[:name], data[:config])
233
+ end
234
+
235
+ private
236
+
237
+ def cached_run(cmd, opts={})
238
+ cmd = "sudo #{cmd}" if opts[:sudo]
239
+ log.cached(cmd)
240
+ last_output(cmd)
241
+ end
242
+
243
+ def really_run(cmd, opts={})
244
+ exec = opts[:sudo] ? helper.sudo : helper
245
+ cmd = prefixed_command(cmd)
246
+ begin
247
+ output = exec.run(cmd, opts[:log])
248
+ @history << {cmd:cmd, output:output}
249
+ output
250
+ rescue Exception
251
+ raise RuntimeError, "Failed to communicate with [#{self.to_s}]"
252
+ end
253
+ end
254
+
255
+ def should_run?(cmd, opts)
256
+ return true unless opts[:once]
257
+ cmd = "sudo #{cmd}" if opts[:sudo]
258
+ last_output(cmd).nil?
259
+ end
260
+
261
+ def last_output(cmd)
262
+ results = @history.select {|h| h[:cmd] == cmd }
263
+ return nil unless results && results.size > 0
264
+ results.last[:output]
265
+ end
266
+
267
+ def prefixed_command(cmd)
268
+ return cmd unless config[:prefix]
269
+ config[:prefix] + cmd
270
+ end
271
+ end
@@ -0,0 +1,128 @@
1
+ # RemoteFile support for Inprovise
2
+ #
3
+ # Author:: Martin Corino
4
+ # License:: Distributes under the same license as Ruby
5
+
6
+ require 'digest/sha1'
7
+ require 'fileutils'
8
+
9
+ class Inprovise::RemoteFile
10
+ attr_reader :path
11
+
12
+ def initialize(context, path)
13
+ @context = context
14
+ @path = path
15
+ @exists = nil
16
+ @permissions = nil
17
+ @owner = nil
18
+ end
19
+
20
+ def hash
21
+ return nil unless exists?
22
+ @hash ||= @context.node.hash_for(path)
23
+ end
24
+
25
+ def exists?
26
+ return @exists unless @exists.nil?
27
+ @exists = @context.node.exists?(path)
28
+ end
29
+
30
+ def directory?
31
+ @context.node.directory?(path)
32
+ end
33
+
34
+ def file?
35
+ @context.node.file?(path)
36
+ end
37
+
38
+ def content
39
+ @context.node.cat(path)
40
+ end
41
+
42
+ # doesnt check permissions or user. should it?
43
+ def matches?(other)
44
+ self.exists? && other.exists? && self.hash == other.hash
45
+ end
46
+
47
+ def copy_to(destination)
48
+ if destination.is_local?
49
+ download(destination)
50
+ else
51
+ duplicate(destination)
52
+ end
53
+ destination
54
+ end
55
+
56
+ def copy_from(destination)
57
+ destination.copy_to(self)
58
+ end
59
+
60
+ def duplicate(destination)
61
+ @context.copy(path, destination.path)
62
+ destination
63
+ end
64
+
65
+ def download(destination)
66
+ if String === destination || destination.is_local?
67
+ @context.download(path, String === destination ? destination : destination.path)
68
+ else
69
+ @context.copy(path, destination.path)
70
+ end
71
+ String === destination ? @context.local(destination) : destination
72
+ end
73
+
74
+ def upload(source)
75
+ if String === source || source.is_local?
76
+ @context.upload(String === source ? source : source.path, path)
77
+ else
78
+ @context.copy(source.path, path)
79
+ end
80
+ self
81
+ end
82
+
83
+ def delete!
84
+ @context.remove(path) if exists?
85
+ invalidate!
86
+ self
87
+ end
88
+
89
+ def set_permissions(mask)
90
+ @context.set_permissions(path, mask)
91
+ invalidate!
92
+ self
93
+ end
94
+
95
+ def permissions
96
+ @permissions ||= @context.node.permissions(path)
97
+ end
98
+
99
+ def set_owner(user, group=nil)
100
+ user ||= owner[:user]
101
+ @context.set_owner(path, user, group)
102
+ invalidate!
103
+ self
104
+ end
105
+
106
+ def owner
107
+ @owner ||= @context.node.owner(path)
108
+ end
109
+
110
+ def user
111
+ owner[:user]
112
+ end
113
+
114
+ def group
115
+ owner[:group]
116
+ end
117
+
118
+ def is_local?
119
+ false
120
+ end
121
+
122
+ private
123
+
124
+ def invalidate!
125
+ @permissions = nil
126
+ @owner = nil
127
+ end
128
+ end
@@ -0,0 +1,36 @@
1
+ # Script dependency Resolver for Inprovise
2
+ #
3
+ # Author:: Martin Corino
4
+ # License:: Distributes under the same license as Ruby
5
+
6
+ class Inprovise::Resolver
7
+ attr_reader :scripts
8
+ def initialize(script,index=nil)
9
+ @script = script
10
+ @index = index || Inprovise::ScriptIndex.default
11
+ @last_seen = script
12
+ @scripts = [@script]
13
+ end
14
+
15
+ def resolve
16
+ begin
17
+ @script.dependencies.reverse.each do |d|
18
+ @scripts.insert(0, *Inprovise::Resolver.new(@index.get(d), @index).resolve.scripts)
19
+ end
20
+ @script.children.each do |c|
21
+ child = @index.get(c)
22
+ @scripts.concat(Inprovise::Resolver.new(child, @index).resolve.scripts) unless @scripts.include?(child)
23
+ end
24
+ rescue SystemStackError
25
+ raise CircularDependencyError.new
26
+ end
27
+ @scripts.uniq!
28
+ self
29
+ end
30
+
31
+ class CircularDependencyError < StandardError
32
+ def initialize
33
+ super('Circular dependecy detected')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,175 @@
1
+ # Script base class for Inprovise
2
+ #
3
+ # Author:: Martin Corino
4
+ # License:: Distributes under the same license as Ruby
5
+
6
+ require 'ostruct'
7
+
8
+ class Inprovise::Script
9
+ attr_reader :name, :dependencies, :actions, :children, :user
10
+
11
+ class DSL
12
+ def initialize(script)
13
+ @script = script
14
+ end
15
+
16
+ def description(desc)
17
+ @script.description(desc)
18
+ end
19
+
20
+ def configuration(cfg)
21
+ @script.configuration(cfg)
22
+ end
23
+
24
+ def depends_on(*scr_names)
25
+ @script.depends_on(*scr_names)
26
+ end
27
+
28
+ def triggers(*scr_names)
29
+ @script.triggers(*scr_names)
30
+ end
31
+
32
+ def validate(&definition)
33
+ @script.validate(&definition)
34
+ end
35
+
36
+ def apply(&definition)
37
+ @script.apply(&definition)
38
+ end
39
+
40
+ def revert(&definition)
41
+ @script.revert(&definition)
42
+ end
43
+
44
+ def as(user)
45
+ @script.as(user)
46
+ end
47
+
48
+ def action(name, &definition)
49
+ @script.action(name, &definition)
50
+ end
51
+ end
52
+
53
+ def initialize(name)
54
+ @name = name
55
+ @description = nil
56
+ @configuration = nil
57
+ @user = nil
58
+ @dependencies = []
59
+ @children = []
60
+ @actions = {}
61
+ @commands = {}
62
+ @remove = nil
63
+ end
64
+
65
+ def description(desc=nil)
66
+ @description = desc if desc
67
+ @description
68
+ end
69
+
70
+ def describe
71
+ return [self.name] unless self.description
72
+ nm = [self.name]
73
+ self.description.split("\n").collect {|ld| "#{"%-25s" % nm.shift.to_s}\t#{ld.strip}"}
74
+ end
75
+
76
+ def configuration(cfg=nil)
77
+ @configuration = cfg if cfg
78
+ @configuration
79
+ end
80
+
81
+ def copy_config(cfg)
82
+ case cfg
83
+ when Hash, OpenStruct
84
+ cfg.to_h.reduce(OpenStruct.new) { |os, (k,v)| os[k] = copy_config(v); os }
85
+ when Array
86
+ cfg.collect { |e| copy_config(e) }
87
+ else
88
+ cfg.dup rescue cfg
89
+ end
90
+ end
91
+ private :copy_config
92
+
93
+ def merge_config(runcfg, scrcfg)
94
+ return scrcfg unless runcfg
95
+ case runcfg
96
+ when Hash, OpenStruct
97
+ return runcfg unless scrcfg.respond_to?(:to_h)
98
+ return scrcfg.to_h.reduce(runcfg) do |rc, (k,v)|
99
+ case rc[k]
100
+ when Hash,OpenStruct
101
+ rc[k] = merge_config(rc[k], v)
102
+ else
103
+ rc[k] = v unless rc[k]
104
+ end
105
+ rc
106
+ end
107
+ else
108
+ return runcfg
109
+ end
110
+ end
111
+ private :merge_config
112
+
113
+ def merge_configuration(config)
114
+ return unless self.configuration
115
+ script_cfg = copy_config(self.configuration)
116
+ config[self.name.to_sym] = merge_config(config[self.name.to_sym], script_cfg)
117
+ end
118
+
119
+ def depends_on(*scr_names)
120
+ scr_names.each do |scr_name|
121
+ @dependencies << scr_name
122
+ end
123
+ end
124
+
125
+ def triggers(*scr_names)
126
+ scr_names.each do |scr_name|
127
+ @children << scr_name
128
+ end
129
+ end
130
+
131
+ def validate(&definition)
132
+ command(:validate, &definition)
133
+ end
134
+
135
+ def apply(&definition)
136
+ command(:apply, &definition)
137
+ end
138
+
139
+ def revert(&definition)
140
+ command(:revert, &definition)
141
+ end
142
+
143
+ def as(user)
144
+ @user = user
145
+ end
146
+
147
+ def action(name, &definition)
148
+ @actions[name] = definition
149
+ end
150
+
151
+ def command(name, &definition)
152
+ if block_given?
153
+ (@commands[name.to_sym] ||= []) << definition
154
+ else
155
+ @commands[name.to_sym] ||= []
156
+ end
157
+ end
158
+
159
+ def provides_command?(name)
160
+ @commands.has_key?(name.to_sym)
161
+ end
162
+
163
+ def to_s
164
+ self.name
165
+ end
166
+ end
167
+
168
+ Inprovise::DSL.dsl_define do
169
+ def script(name, &definition)
170
+ Inprovise.log.local("Adding provisioning script #{name}") if Inprovise.verbosity > 1
171
+ Inprovise.add_script(Inprovise::Script.new(name)) do |script|
172
+ Inprovise::Script::DSL.new(script).instance_eval(&definition)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,46 @@
1
+ # Script Index for Inprovise
2
+ #
3
+ # Author:: Martin Corino
4
+ # License:: Distributes under the same license as Ruby
5
+
6
+ class Inprovise::ScriptIndex
7
+ attr_reader :index_name
8
+
9
+ def initialize(index_name)
10
+ @index_name = index_name
11
+ @scripts = {}
12
+ end
13
+
14
+ def self.default
15
+ @default ||= new('default')
16
+ end
17
+
18
+ def add(scr)
19
+ @scripts[scr.name] = scr
20
+ end
21
+
22
+ def get(scr_name)
23
+ scr = @scripts[scr_name]
24
+ raise MissingScriptError.new(index_name, scr_name) if scr.nil?
25
+ scr
26
+ end
27
+
28
+ def scripts
29
+ @scripts.keys
30
+ end
31
+
32
+ def clear!
33
+ @scripts = {}
34
+ end
35
+
36
+ class MissingScriptError < StandardError
37
+ def initialize(index_name, script_name)
38
+ @index_name = index_name
39
+ @script_name = script_name
40
+ end
41
+
42
+ def message
43
+ "script #{@script_name} could not be found in the index #{@index_name}"
44
+ end
45
+ end
46
+ end