inprovise 0.2.2

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