rouster 0.57 → 0.61

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.
@@ -459,7 +459,7 @@ class Rouster
459
459
  opts[:manifest_file].each do |file|
460
460
  raise InternalError.new(sprintf('invalid manifest file specified[%s]', file)) unless self.is_file?(file)
461
461
 
462
- cmd = 'puppet apply'
462
+ cmd = 'puppet apply --detailed-exitcodes'
463
463
  cmd << sprintf(' --modulepath=%s', opts[:module_dir]) unless opts[:module_dir].nil?
464
464
  cmd << sprintf(' --hiera_config=%s', opts[:hiera_config]) unless opts[:hiera_config].nil? or puppet_version < '3.0'
465
465
  cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
@@ -481,7 +481,7 @@ class Rouster
481
481
 
482
482
  manifests.each do |m|
483
483
 
484
- cmd = 'puppet apply'
484
+ cmd = 'puppet apply --detailed-exitcodes'
485
485
  cmd << sprintf(' --modulepath=%s', opts[:module_dir]) unless opts[:module_dir].nil?
486
486
  cmd << sprintf(' --hiera_config=%s', opts[:hiera_config]) unless opts[:hiera_config].nil? or puppet_version < '3.0'
487
487
  cmd << sprintf(' --environment %s', opts[:environment]) unless opts[:environment].nil?
@@ -459,17 +459,39 @@ class Rouster
459
459
  local = v.to_s.match(/absent|false/).nil? ? false : true
460
460
  end
461
461
  when :version
462
+ # TODO support determination based on multiple versions of the same package installed (?)
462
463
  if packages.has_key?(name)
463
- if v.split("\s").size > 1
464
- ## generic comparator functionality
465
- comp, expectation = v.split("\s")
466
- local = generic_comparator(packages[name], comp, expectation)
467
- else
468
- local = ! v.to_s.match(/#{packages[name]}/).nil?
464
+
465
+ lps = packages[name].is_a?(Array) ? packages[name] : [ packages[name] ]
466
+
467
+ lps.each do |lp|
468
+ if v.split("\s").size > 1
469
+ ## generic comparator functionality
470
+ comp, expectation = v.split("\s")
471
+ local = generic_comparator(lp[:version], comp, expectation)
472
+ break unless local.eql?(true)
473
+ else
474
+ local = ! v.to_s.match(/#{lp[:version]}/).nil?
475
+ break unless local.eql?(true)
476
+ end
469
477
  end
470
478
  else
471
479
  local = false
472
480
  end
481
+ when :arch, :architecture
482
+ if packages.has_key?(name)
483
+ archs = []
484
+ lps = packages[name].is_a?(Array) ? packages[name] : [ packages[name] ]
485
+ lps.each { |p| archs << p[:arch] }
486
+ if v.is_a?(Array)
487
+ v.each do |arch|
488
+ local = archs.member?(arch)
489
+ break unless local.eql?(true) # fail fast - if we are looking for an arch that DNE, bail out
490
+ end
491
+ else
492
+ local = archs.member?(v)
493
+ end
494
+ end
473
495
  when :type
474
496
  # noop allowing parse_catalog() output to be passed directly
475
497
  else
@@ -42,23 +42,52 @@ class Rouster
42
42
 
43
43
  ##
44
44
  # up
45
- # runs `vagrant up` from the Vagrantfile path
45
+ # runs `vagrant up <name>` from the Vagrantfile path
46
46
  # if :sshtunnel is passed to the object during instantiation, the tunnel is created here as well
47
47
  def up
48
48
  @logger.info('up()')
49
- self.vagrant(sprintf('up %s', @name))
49
+
50
+ # don't like putting this here, may be refactored
51
+ if self.is_passthrough? and (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
52
+ self.aws_up()
53
+ else
54
+ self.vagrant(sprintf('up %s', @name))
55
+ end
50
56
 
51
57
  @ssh_info = nil # in case the ssh-info has changed, a la destroy/rebuild
52
58
  self.connect_ssh_tunnel() if @sshtunnel
53
59
  end
54
60
 
61
+ ##
62
+ # halt
63
+ # runs `vagrant halt <name>` from the Vagrantfile path
64
+ def halt
65
+ @logger.info('halt()')
66
+ self.vagrant(sprintf('halt %s', @name))
67
+ end
68
+
69
+ ##
70
+ # package -- though vagrant docs still refer to 'repackage'
71
+ # runs `vagrant package <name> <provider>`
72
+ def package(provider='virtualbox') # TODO get the provider as a first class citizen on the rouster object
73
+ @logger.info(sprintf('package(%s)', provider))
74
+ self.vagrant(sprintf('package %s %s', @name, provider))
75
+ end
76
+
55
77
  ##
56
78
  # destroy
57
79
  # runs `vagrant destroy <name>` from the Vagrantfile path
58
80
  def destroy
59
81
  @logger.info('destroy()')
82
+
83
+ # don't like putting this here, may be refactored
84
+ if self.is_passthrough? and (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
85
+ self.aws_destroy()
86
+ else
87
+ self.vagrant(sprintf('destroy -f %s', @name))
88
+ end
89
+
60
90
  disconnect_ssh_tunnel
61
- self.vagrant(sprintf('destroy -f %s', @name))
62
91
  end
63
92
 
64
93
  ##
@@ -78,21 +107,28 @@ class Rouster
78
107
  end
79
108
  end
80
109
 
110
+ # don't like putting this here, may be refactored
81
111
  @logger.info('status()')
82
- self.vagrant(sprintf('status %s', @name))
83
-
84
- # else case here (both for nil/non-matching output) is handled by non-0 exit code
85
- output = self.get_output()
86
- if output.nil?
87
- if self.is_passthrough?()
88
- status = 'running'
112
+ if self.is_passthrough? and (self.passthrough[:type].equal?(:aws) or self.passthrough[:type].equal?(:raiden))
113
+ status = self.aws_status()
114
+ else
115
+ self.vagrant(sprintf('status %s', @name))
116
+
117
+ # else case here (both for nil/non-matching output) is handled by non-0 exit code
118
+ output = self.get_output()
119
+ if output.nil?
120
+ if self.is_passthrough?() and self.passthrough[:type].eql?(:local)
121
+ status = 'running'
122
+ else
123
+ status = 'not-created'
124
+ end
125
+ elsif output.match(/^#{@name}\s*(.*\s?\w+)\s\((.+)\)$/)
126
+ # vagrant 1.2+, $1 = status, $2 = provider
127
+ status = $1
128
+ elsif output.match(/^#{@name}\s+(.+)$/)
129
+ # vagrant 1.2-, $1 = status
130
+ status = $1
89
131
  end
90
- elsif output.match(/^#{@name}\s*(.*\s?\w+)\s\((.+)\)$/)
91
- # vagrant 1.2+, $1 = status, $2 = provider
92
- status = $1
93
- elsif output.match(/^#{@name}\s+(.+)$/)
94
- # vagrant 1.2-, $1 = status
95
- status = $1
96
132
  end
97
133
 
98
134
  if @cache_timeout
@@ -2,14 +2,13 @@
2
2
 
3
3
  # this gets us Rouster, still need to figure out how to find vagrant
4
4
  $LOAD_PATH << File.join([File.dirname(__FILE__), 'lib'])
5
+ $LOAD_PATH << File.join([File.dirname(__FILE__), 'plugins'])
6
+ $LOAD_PATH << File.expand_path(sprintf('%s/..', File.dirname(__FILE__)))
7
+ $LOAD_PATH << File.dirname(__FILE__)
5
8
 
6
9
  require 'rubygems'
7
10
 
8
11
  # debugging help
9
- begin
10
- require 'debugger'
11
- rescue LoadError
12
- end
13
12
 
14
13
  class Object
15
14
  def my_methods
@@ -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 @passthrough.has_key?(:created_elb)
180
+ elb = @passthrough[:created_elb]
181
+
182
+ @logger.info(sprintf('deleting ELB[%s]', elb))
183
+ @elb.delete_load_balancer(elb)
184
+ end
185
+
186
+ self.aws_status
187
+ end
188
+
189
+ def aws_describe_instance(instance = aws_get_instance)
190
+
191
+ if @cache_timeout
192
+ if @cache.has_key?(:aws_describe_instance)
193
+ if (Time.now.to_i - @cache[:aws_describe_instance][:time]) < @cache_timeout
194
+ @logger.debug(sprintf('using cached aws_describe_instance?[%s] from [%s]', @cache[:aws_describe_instance][:instance], @cache[:aws_describe_instance][:time]))
195
+ return @cache[:aws_describe_instance][:instance]
196
+ end
197
+ end
198
+ end
199
+
200
+ return nil if instance.nil?
201
+
202
+ self.aws_connect
203
+ server = @ec2.describe_instances('instance-id' => [ instance ])
204
+ response = server.data[:body]['reservationSet'][0]['instancesSet'][0]
205
+
206
+ if instance.eql?(self.aws_get_instance)
207
+ @instance_data = response
208
+ end
209
+
210
+ if @cache_timeout
211
+ @cache[:aws_describe_instance] = Hash.new unless @cache[:aws_describe_instance].class.eql?(Hash)
212
+ @cache[:aws_describe_instance][:time] = Time.now.to_i
213
+ @cache[:aws_describe_instance][:instance] = response
214
+ @logger.debug(sprintf('caching is_available_via_ssh?[%s] at [%s]', @cache[:aws_describe_instance][:instance], @cache[:aws_decribe_instance][:time]))
215
+ end
216
+
217
+ response
218
+ end
219
+
220
+ def aws_status
221
+ self.aws_describe_instance
222
+ return 'not-created' if @instance_data.nil?
223
+ @instance_data['instanceState']['name'].nil? ? @instance_data['instanceState']['code'] : @instance_data['instanceState']['name']
224
+ end
225
+
226
+ def aws_connect_to_elb (id, elbname, listeners = [{ 'InstancePort' => 22, 'LoadBalancerPort' => 22, 'Protocol' => 'TCP' }])
227
+ self.elb_connect
228
+
229
+ # allow either hash or array of hash specification for listeners
230
+ listeners = [ listeners ] unless listeners.is_a?(Array)
231
+ required_params = [ 'InstancePort', 'LoadBalancerPort', 'Protocol' ] # figure out plan re: InstanceProtocol/LoadbalancerProtocol vs. Protocol
232
+
233
+ listeners.each do |l|
234
+ required_params.each do |r|
235
+ raise sprintf('listener[%s] does not include required parameter[%s]', l, r) unless l[r]
236
+ end
237
+
238
+ end
239
+
240
+ @logger.debug(sprintf('confirming ELB name uniqueness[%s]', elbname))
241
+ response = @elb.describe_load_balancers()
242
+ response.body['DescribeLoadBalancersResult']['LoadBalancerDescriptions'].each do |elb|
243
+ if elb['LoadBalancerName'].eql?(elbname)
244
+ # terminate
245
+ @logger.debug(sprintf('terminating ELB[%s]', elbname))
246
+ @elb.delete_load_balancer(elbname)
247
+ end
248
+ end
249
+
250
+ # create the ELB/VIP
251
+ @logger.debug(sprintf('creating a load balancer[%s] with listeners[%s]', elbname, listeners))
252
+ response = @elb.create_load_balancer(
253
+ [], # availability zones not needed on raiden
254
+ elbname,
255
+ listeners
256
+ )
257
+
258
+ dnsname = response.body['CreateLoadBalancerResult']['DNSName']
259
+
260
+ # string it up to the id passed
261
+ response = @elb.register_instances_with_load_balancer(id, elbname)
262
+
263
+ # i hate this so much.
264
+ @logger.debug(sprintf('sleeping[%s] to allow DNS propagation', self.passthrough[:dns_propagation_sleep]))
265
+ sleep self.passthrough[:dns_propagation_sleep]
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