rouster 0.5 → 0.7

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.reek +63 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +17 -0
  6. data/Gemfile.lock +102 -0
  7. data/README.md +233 -7
  8. data/Rakefile +52 -34
  9. data/Vagrantfile +26 -8
  10. data/examples/aws.rb +85 -0
  11. data/examples/openstack.rb +61 -0
  12. data/examples/passthrough.rb +71 -0
  13. data/lib/rouster.rb +380 -262
  14. data/lib/rouster/deltas.rb +470 -138
  15. data/lib/rouster/puppet.rb +155 -26
  16. data/lib/rouster/testing.rb +205 -46
  17. data/lib/rouster/tests.rb +40 -11
  18. data/lib/rouster/vagrant.rb +311 -0
  19. data/path_helper.rb +3 -4
  20. data/plugins/aws.rb +347 -0
  21. data/plugins/openstack.rb +136 -0
  22. data/test/basic.rb +4 -1
  23. data/test/functional/deltas/test_get_crontab.rb +64 -2
  24. data/test/functional/deltas/test_get_groups.rb +74 -2
  25. data/test/functional/deltas/test_get_os.rb +68 -0
  26. data/test/functional/deltas/test_get_packages.rb +73 -6
  27. data/test/functional/deltas/test_get_ports.rb +26 -1
  28. data/test/functional/deltas/test_get_services.rb +43 -5
  29. data/test/functional/deltas/test_get_users.rb +35 -2
  30. data/test/functional/puppet/test_facter.rb +41 -1
  31. data/test/functional/test_caching.rb +2 -2
  32. data/test/functional/test_inspect.rb +1 -1
  33. data/test/functional/test_is_file.rb +17 -1
  34. data/test/functional/test_is_in_file.rb +40 -0
  35. data/test/functional/test_new.rb +233 -22
  36. data/test/functional/test_passthroughs.rb +94 -0
  37. data/test/functional/test_put.rb +2 -2
  38. data/test/functional/test_validate_file.rb +104 -3
  39. data/test/puppet/test_apply.rb +8 -6
  40. data/test/unit/puppet/resources/puppet_run_with_failed_exec +59 -0
  41. data/test/unit/puppet/resources/puppet_run_with_successful_exec +61 -0
  42. data/test/unit/puppet/test_get_puppet_star.rb +27 -4
  43. data/test/unit/puppet/test_puppet_parsing.rb +44 -0
  44. data/test/unit/test_new.rb +88 -0
  45. data/test/unit/test_parse_ls_string.rb +67 -0
  46. data/test/unit/testing/resources/osx-launchd +285 -0
  47. data/test/unit/testing/resources/rhel-systemd +46 -0
  48. data/test/unit/testing/resources/rhel-systemv +41 -0
  49. data/test/unit/testing/resources/rhel-upstart +20 -0
  50. data/test/unit/testing/test_get_services.rb +178 -0
  51. data/test/unit/testing/test_validate_cron.rb +78 -0
  52. data/test/unit/testing/test_validate_package.rb +36 -10
  53. data/test/unit/testing/test_validate_port.rb +5 -0
  54. metadata +42 -21
  55. data/test/puppet/test_roles.rb +0 -186
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/ruby
2
+ ## plugins/aws.rb - provide helper functions for Rouster objects running on AWS/EC2
3
+
4
+ require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
5
+
6
+ require 'fog'
7
+ require 'uri'
8
+
9
+ class Rouster
10
+
11
+ attr_reader :ec2, :elb # expose AWS workers
12
+ attr_reader :instance_data # the result of the runInstances request
13
+
14
+ def aws_get_url(url)
15
+ # convenience method to run curls from inside the VM
16
+ self.run(sprintf('curl -s %s', url))
17
+ end
18
+
19
+ # TODO should this be 'aws_ip'?
20
+ def aws_get_ip (method = :internal, type = :public)
21
+ # allowed methods: :internal (check meta-data inside VM), :aws (ask API)
22
+ # allowed types: :public, :private
23
+ self.aws_describe_instance
24
+
25
+ if method.equal?(:internal)
26
+ key = type.equal?(:public) ? 'public-ipv4' : 'local-ipv4'
27
+ murl = sprintf('http://169.254.169.254/latest/meta-data/%s', key)
28
+ result = self.aws_get_url(murl)
29
+ else
30
+ key = type.equal?(:public) ? 'ipAddress' : 'privateIpAddress'
31
+ result = @instance_data[key]
32
+ end
33
+
34
+ result
35
+ end
36
+
37
+ def aws_get_userdata
38
+ murl = 'http://169.254.169.254/latest/user-data/'
39
+ result = self.aws_get_url(murl)
40
+
41
+ if result.match(/\S=\S/)
42
+ # TODO should we really be doing this?
43
+ userdata = Hash.new()
44
+ result.split("\n").each do |line|
45
+ if line.match(/^(.*?)=(.*)/)
46
+ userdata[$1] = $2
47
+ end
48
+ end
49
+ else
50
+ userdata = result
51
+ end
52
+
53
+ userdata
54
+ end
55
+
56
+ # return a hash containing meta-data items
57
+ def aws_get_metadata
58
+ murl = 'http://169.254.169.254/latest/meta-data/'
59
+ result = self.aws_get_url(murl)
60
+ metadata = Hash.new()
61
+
62
+ # TODO this isn't entirely right.. if the element ends in '/', it's actually another level of hash..
63
+ result.split("\n").each do |element|
64
+ metadata[element] = self.aws_get_url(sprintf('%s%s', murl, element))
65
+ end
66
+
67
+ metadata
68
+ end
69
+
70
+ def aws_get_hostname (method = :internal, type = :public)
71
+ # allowed methods: :internal (check meta-data inside VM), :aws (ask API)
72
+ # allowed types: :public, :private
73
+ self.aws_describe_instance
74
+
75
+ result = nil
76
+
77
+ if method.equal?(:internal)
78
+ key = type.equal?(:public) ? 'public-hostname' : 'local-hostname'
79
+ murl = sprintf('http://169.254.169.254/latest/meta-data/%s', key)
80
+ result = self.aws_get_url(murl)
81
+ else
82
+ key = type.equal?(:public) ? 'dnsName' : 'privateDnsName'
83
+ result = @instance_data[key]
84
+ end
85
+
86
+ result
87
+ end
88
+
89
+ def aws_get_instance ()
90
+ if ! @instance_data.nil? and @instance_data.has_key?('instanceId')
91
+ return @instance_data['instanceId'] # we already know the id
92
+ elsif @passthrough.has_key?(:instance)
93
+ return @passthrough[:instance] # we know the id we want
94
+ else
95
+ @logger.debug(sprintf('unable to determine ami-id from instance_data[%s] or passthrough specification[%s]', @instance_data, @passthrough))
96
+ return nil # we don't have an id yet, likely a up() call
97
+ end
98
+ end
99
+
100
+ def aws_get_ami ()
101
+ if ! @instance_data.nil? and @instance_data.has_key?('ami')
102
+ return @instance_data['ami']
103
+ else
104
+ return @passthrough[:ami]
105
+ end
106
+ end
107
+
108
+ def aws_up
109
+ # wait for machine to transition to running state and become sshable (TODO maybe make the second half optional)
110
+ self.aws_connect
111
+
112
+ status = self.status()
113
+
114
+ if status.eql?('running')
115
+ self.connect_ssh_tunnel
116
+ self.passthrough[:instance] = self.aws_get_instance
117
+ return self.aws_get_instance
118
+ end
119
+
120
+ # TODO provide more context
121
+ @logger.info(sprintf('calling RunInstances ami[%s], size[%s], keypair[%s]',
122
+ self.passthrough[:ami],
123
+ self.passthrough[:size],
124
+ self.passthrough[:keypair]
125
+ ))
126
+
127
+ server = @ec2.run_instances(
128
+ self.passthrough[:ami],
129
+ self.passthrough[:min_count],
130
+ self.passthrough[:max_count],
131
+ {
132
+ 'InstanceType' => self.passthrough[:size],
133
+ 'KeyName' => self.passthrough[:keypair],
134
+ 'SecurityGroup' => self.passthrough[:security_groups],
135
+ 'UserData' => self.passthrough[:userdata],
136
+ },
137
+ )
138
+
139
+ @instance_data = server.data[:body]['instancesSet'][0]
140
+
141
+ # wait until the machine starts
142
+ ceiling = 9
143
+ sleep_time = 20
144
+ status = nil
145
+ 0.upto(ceiling) do |try|
146
+ status = self.aws_status
147
+
148
+ @logger.debug(sprintf('describeInstances[%s]: [%s] [#%s]', self.aws_get_instance, status, try))
149
+
150
+ if status.eql?('running') or status.eql?('16')
151
+ @logger.info(sprintf('[%s] transitioned to state[%s]', self.aws_get_instance, self.aws_status))
152
+ break
153
+ end
154
+
155
+ sleep sleep_time
156
+ end
157
+
158
+ raise sprintf('instance[%s] did not transition to running state, stopped trying at[%s]', self.aws_get_instance, status) unless status.eql?('running') or status.eql?('16')
159
+
160
+ # TODO don't be this hacky
161
+ self.aws_describe_instance # the server.data response doesn't include public hostname/ip
162
+ if @passthrough[:type].eql?(:aws)
163
+ @passthrough[:host] = @instance_data['dnsName']
164
+ else
165
+ @passthrough[:host] = self.find_ssh_elb(true)
166
+ end
167
+
168
+ self.connect_ssh_tunnel
169
+
170
+ self.passthrough[:instance] = self.aws_get_instance
171
+ self.passthrough[:instance]
172
+ end
173
+
174
+ def aws_destroy
175
+ self.aws_connect
176
+
177
+ server = @ec2.terminate_instances(self.aws_get_instance)
178
+
179
+ if self.passthrough[:created_elb] && self.passthrough[:elb_cleanup]
180
+ @logger.info(sprintf('deleting ELB[%s]', self.passthrough[:created_elb]))
181
+ @elb.delete_load_balancer(self.passthrough[:created_elb])
182
+ end
183
+
184
+ self.aws_status
185
+ end
186
+
187
+ def aws_describe_instance(instance = aws_get_instance)
188
+
189
+ if @cache_timeout
190
+ if @cache.has_key?(:aws_describe_instance)
191
+ if (Time.now.to_i - @cache[:aws_describe_instance][:time]) < @cache_timeout
192
+ @logger.debug(sprintf('using cached aws_describe_instance?[%s] from [%s]', @cache[:aws_describe_instance][:instance], @cache[:aws_describe_instance][:time]))
193
+ return @cache[:aws_describe_instance][:instance]
194
+ end
195
+ end
196
+ end
197
+
198
+ return nil if instance.nil?
199
+
200
+ self.aws_connect
201
+ server = @ec2.describe_instances('instance-id' => [ instance ])
202
+ response = server.data[:body]['reservationSet'][0]['instancesSet'][0]
203
+
204
+ if instance.eql?(self.aws_get_instance)
205
+ @instance_data = response
206
+ end
207
+
208
+ if @cache_timeout
209
+ @cache[:aws_describe_instance] = Hash.new unless @cache[:aws_describe_instance].class.eql?(Hash)
210
+ @cache[:aws_describe_instance][:time] = Time.now.to_i
211
+ @cache[:aws_describe_instance][:instance] = response
212
+ @logger.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:aws_describe_instance][:instance], @cache[:aws_decribe_instance][:time]))
213
+ end
214
+
215
+ response
216
+ end
217
+
218
+ def aws_status
219
+ self.aws_describe_instance
220
+ return 'not-created' if @instance_data.nil?
221
+ @instance_data['instanceState']['name'].nil? ? @instance_data['instanceState']['code'] : @instance_data['instanceState']['name']
222
+ end
223
+
224
+ def aws_connect_to_elb (id, elbname, listeners = [{ 'InstancePort' => 22, 'LoadBalancerPort' => 22, 'Protocol' => 'TCP' }])
225
+ self.elb_connect
226
+
227
+ # allow either hash or array of hash specification for listeners
228
+ listeners = [ listeners ] unless listeners.is_a?(Array)
229
+ required_params = [ 'InstancePort', 'LoadBalancerPort', 'Protocol' ] # figure out plan re: InstanceProtocol/LoadbalancerProtocol vs. Protocol
230
+
231
+ listeners.each do |l|
232
+ required_params.each do |r|
233
+ raise sprintf('listener[%s] does not include required parameter[%s]', l, r) unless l[r]
234
+ end
235
+
236
+ end
237
+
238
+ @logger.debug(sprintf('confirming ELB name uniqueness[%s]', elbname))
239
+ response = @elb.describe_load_balancers()
240
+ response.body['DescribeLoadBalancersResult']['LoadBalancerDescriptions'].each do |elb|
241
+ if elb['LoadBalancerName'].eql?(elbname)
242
+ # terminate
243
+ @logger.debug(sprintf('terminating ELB[%s]', elbname))
244
+ @elb.delete_load_balancer(elbname)
245
+ end
246
+ end
247
+
248
+ # create the ELB/VIP
249
+ @logger.debug(sprintf('creating a load balancer[%s] with listeners[%s]', elbname, listeners))
250
+ response = @elb.create_load_balancer(
251
+ [], # availability zones not needed on raiden
252
+ elbname,
253
+ listeners
254
+ )
255
+
256
+ dnsname = response.body['CreateLoadBalancerResult']['DNSName']
257
+
258
+ # string it up to the id passed
259
+ response = @elb.register_instances_with_load_balancer(id, elbname)
260
+
261
+ # i hate this so much.
262
+ @logger.debug(sprintf('sleeping[%s] to allow DNS propagation', self.passthrough[:dns_propagation_sleep]))
263
+ sleep self.passthrough[:dns_propagation_sleep]
264
+
265
+ self.passthrough[:created_elb] = elbname
266
+
267
+ return dnsname
268
+ end
269
+
270
+ # TODO this will throw at the first error - should we catch?
271
+ # run some commands, return an array of the output
272
+ def aws_bootstrap (commands)
273
+ self.aws_connect
274
+ commands = (commands.is_a?(Array)) ? commands : [ commands ]
275
+ output = Array.new
276
+
277
+ commands.each do |command|
278
+ output << self.run(command)
279
+ end
280
+
281
+ return output
282
+ end
283
+
284
+ def aws_connect
285
+ return @ec2 unless @ec2.nil?
286
+
287
+ config = {
288
+ :provider => 'AWS',
289
+ :region => self.passthrough[:region],
290
+ :aws_access_key_id => self.passthrough[:key_id],
291
+ :aws_secret_access_key => self.passthrough[:secret_key],
292
+ }
293
+
294
+ config[:endpoint] = self.passthrough[:ec2_endpoint] unless self.passthrough[:ec2_endpoint].nil?
295
+ @ec2 = Fog::Compute.new(config)
296
+ end
297
+
298
+ def elb_connect
299
+ return @elb unless @elb.nil?
300
+
301
+ if self.passthrough[:elb_endpoint]
302
+ endpoint = URI.parse(self.passthrough[:elb_endpoint])
303
+ elsif self.passthrough[:ec2_endpoint]
304
+ endpoint = URI.parse(self.passthrough[:ec2_endpoint])
305
+ end
306
+
307
+ config = {
308
+ :region => self.passthrough[:region],
309
+ :aws_access_key_id => self.passthrough[:key_id],
310
+ :aws_secret_access_key => self.passthrough[:secret_key],
311
+ }
312
+
313
+ unless endpoint.nil?
314
+ # if not specifying an endpoint, don't add to the config
315
+ config[:host] = endpoint.host
316
+ config[:path] = endpoint.path
317
+ config[:port] = endpoint.port
318
+ config[:scheme] = endpoint.scheme
319
+ end
320
+
321
+ @elb = Fog::AWS::ELB.new(config)
322
+ end
323
+
324
+ def find_ssh_elb (create_if_not_found = false, instance = aws_get_instance)
325
+ # given an instance, see if there is already an ELB that it is connected to - and potentially create one
326
+ self.elb_connect
327
+ result = nil
328
+
329
+ response = @elb.describe_load_balancers
330
+ elbs = response.body['DescribeLoadBalancersResult']['LoadBalancerDescriptions']
331
+
332
+ elbs.each do |elb|
333
+ if elb['Instances'].member?(instance)
334
+ result = elb['DNSName']
335
+ break
336
+ end
337
+ end
338
+
339
+ if create_if_not_found and result.nil?
340
+ result = self.aws_connect_to_elb(instance, sprintf('%s-ssh', self.name))
341
+ end
342
+
343
+ result
344
+
345
+ end
346
+
347
+ end
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/ruby
2
+ ## plugins/openstack.rb - provide helper functions for Rouster objects running on OpenStack/Compute
3
+
4
+ require sprintf('%s/../%s', File.dirname(File.expand_path(__FILE__)), 'path_helper')
5
+
6
+ require 'fog'
7
+ require 'uri'
8
+
9
+ class Rouster
10
+
11
+ attr_reader :nova # expose OpenStack workers
12
+ attr_reader :instance_data # the result of the runInstances request
13
+
14
+ # return a hash containing meta-data items
15
+ def ostack_get_instance_id ()
16
+ # The instance id is kept in @passthrough[:instance] or
17
+ # can be obtained from @instance_data which has all instance
18
+ # details.
19
+ if ! @instance_data.nil? and ! @instance_data.id.nil?
20
+ return @instance_data.id # we already know the id
21
+ elsif @passthrough.has_key?(:instance)
22
+ return @passthrough[:instance] # we know the id we want
23
+ else
24
+ @logger.debug(sprintf('unable to determine id from instance_data[%s] or passthrough specification[%s]', @instance_data, @passthrough))
25
+ return nil # we don't have an id yet, likely a up() call
26
+ end
27
+ end
28
+
29
+ def ostack_up
30
+ # wait for machine to transition to running state and become sshable (TODO maybe make the second half optional)
31
+ self.ostack_connect
32
+ # This will check if instance_id has been provided. If so, it will check on status of the instance.
33
+ status = self.status()
34
+ if status.eql?('running')
35
+ self.passthrough[:instance] = self.ostack_get_instance_id
36
+ @logger.debug(sprintf('Connecting to running instance [%s] while calling ostack_up()', self.passthrough[:instance]))
37
+ self.connect_ssh_tunnel
38
+ else
39
+ server = @nova.servers.create(:name => @name, :flavor_ref => @passthrough[:flavor_ref],
40
+ :image_ref => @passthrough[:image_ref], :key_name => @passthrough[:keypair], :user_data => @passthrough[:user_data])
41
+ server.wait_for { ready? }
42
+ @instance_data = server
43
+ server.addresses.each_key do |address_key|
44
+ if defined?(server.addresses[address_key])
45
+ self.passthrough[:host] = server.addresses[address_key].first['addr']
46
+ break
47
+ end
48
+ end
49
+ self.passthrough[:instance] = self.ostack_get_instance_id
50
+ @logger.debug(sprintf('Connecting to running instance [%s] while calling ostack_up()', self.passthrough[:instance]))
51
+ self.connect_ssh_tunnel
52
+ end
53
+ self.passthrough[:instance]
54
+ end
55
+
56
+ def ostack_get_ip()
57
+ self.passthrough[:host]
58
+ end
59
+
60
+ def ostack_destroy
61
+ server = self.ostack_describe_instance
62
+ raise sprintf("instance[%s] not found by destroy()", self.ostack_get_instance_id) if server.nil?
63
+ server.destroy
64
+ @instance_data = nil
65
+ self.passthrough.delete(:instance)
66
+ end
67
+
68
+ def ostack_describe_instance(instance_id = ostack_get_instance_id)
69
+
70
+ if @cache_timeout
71
+ if @cache.has_key?(:ostack_describe_instance)
72
+ if (Time.now.to_i - @cache[:ostack_describe_instance][:time]) < @cache_timeout
73
+ @logger.debug(sprintf('using cached ostack_describe_instance?[%s] from [%s]', @cache[:ostack_describe_instance][:instance], @cache[:ostack_describe_instance][:time]))
74
+ return @cache[:ostack_describe_instance][:instance]
75
+ end
76
+ end
77
+ end
78
+ # We don't have a instance.
79
+ return nil if instance_id.nil?
80
+ self.ostack_connect
81
+ response = @nova.servers.get(instance_id)
82
+ return nil if response.nil?
83
+ @instance_data = response
84
+
85
+ if @cache_timeout
86
+ @cache[:ostack_describe_instance] = Hash.new unless @cache[:ostack_describe_instance].class.eql?(Hash)
87
+ @cache[:ostack_describe_instance][:time] = Time.now.to_i
88
+ @cache[:ostack_describe_instance][:instance] = response
89
+ @logger.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:ostack_describe_instance][:instance], @cache[:ostack_describe_instance][:time]))
90
+ end
91
+
92
+ @instance_data
93
+ end
94
+
95
+ def ostack_status
96
+ self.ostack_describe_instance
97
+ return 'not-created' if @instance_data.nil?
98
+ if @instance_data.state.eql?('ACTIVE')
99
+ # Make this consistent with AWS response.
100
+ return 'running'
101
+ else
102
+ return @instance_data.state
103
+ end
104
+ end
105
+
106
+
107
+ # TODO this will throw at the first error - should we catch?
108
+ # run some commands, return an array of the output
109
+ def ostack_bootstrap (commands)
110
+ self.ostack_connect
111
+ commands = (commands.is_a?(Array)) ? commands : [ commands ]
112
+ output = Array.new
113
+
114
+ commands.each do |command|
115
+ output << self.run(command)
116
+ end
117
+
118
+ return output
119
+ end
120
+
121
+ def ostack_connect
122
+ # Instantiates an Object which can communicate with OS Compute.
123
+ # No instance specific information is set at this time.
124
+ return @nova unless @nova.nil?
125
+
126
+ config = {
127
+ :provider => 'openstack', # OpenStack Fog provider
128
+ :openstack_auth_url => self.passthrough[:openstack_auth_url], # OpenStack Keystone endpoint
129
+ :openstack_username => self.passthrough[:openstack_username], # Your OpenStack Username
130
+ :openstack_tenant => self.passthrough[:openstack_tenant], # Your tenant id
131
+ :openstack_api_key => self.passthrough[:openstack_api_key], # Your OpenStack Password
132
+ :connection_options => self.passthrough[:connection_options] # Optional
133
+ }
134
+ @nova = Fog::Compute.new(config)
135
+ end
136
+ end