rouster 0.57 → 0.61

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