leeroy_app 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +40 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +103 -0
  7. data/LICENSE +21 -0
  8. data/README.md +40 -0
  9. data/Rakefile +5 -0
  10. data/bin/leeroy +18 -0
  11. data/hierarchy.md +39 -0
  12. data/leeroy.gemspec +42 -0
  13. data/lib/leeroy/app.rb +132 -0
  14. data/lib/leeroy/env.rb +116 -0
  15. data/lib/leeroy/helpers/aws.rb +403 -0
  16. data/lib/leeroy/helpers/dumpable.rb +72 -0
  17. data/lib/leeroy/helpers/env.rb +42 -0
  18. data/lib/leeroy/helpers/logging.rb +36 -0
  19. data/lib/leeroy/helpers/polling.rb +67 -0
  20. data/lib/leeroy/helpers/state.rb +59 -0
  21. data/lib/leeroy/helpers.rb +9 -0
  22. data/lib/leeroy/state.rb +70 -0
  23. data/lib/leeroy/task/base.rb +61 -0
  24. data/lib/leeroy/task/image.rb +126 -0
  25. data/lib/leeroy/task/instantiate.rb +200 -0
  26. data/lib/leeroy/task/sleep.rb +29 -0
  27. data/lib/leeroy/task/stub.rb +40 -0
  28. data/lib/leeroy/task/terminate.rb +49 -0
  29. data/lib/leeroy/task.rb +17 -0
  30. data/lib/leeroy/types/dash.rb +17 -0
  31. data/lib/leeroy/types/image.rb +53 -0
  32. data/lib/leeroy/types/instance.rb +67 -0
  33. data/lib/leeroy/types/mash.rb +17 -0
  34. data/lib/leeroy/types/packedstring.rb +25 -0
  35. data/lib/leeroy/types/phase.rb +20 -0
  36. data/lib/leeroy/types/semaphore.rb +30 -0
  37. data/lib/leeroy/types/statedata.rb +44 -0
  38. data/lib/leeroy/types/statemetadata.rb +29 -0
  39. data/lib/leeroy/types/userdata.rb +13 -0
  40. data/lib/leeroy/version.rb +3 -0
  41. data/lib/leeroy.rb +7 -0
  42. data/spec/spec_helper.rb +105 -0
  43. data/spec/support/aruba.rb +1 -0
  44. data/spec/use_aruba_with_rspec_spec.rb +11 -0
  45. metadata +333 -0
@@ -0,0 +1,70 @@
1
+ require 'leeroy/types/dash'
2
+ require 'leeroy/types/statedata'
3
+ require 'leeroy/types/statemetadata'
4
+ require 'leeroy/helpers/logging'
5
+ require 'leeroy/helpers/polling'
6
+ require 'leeroy/helpers/state'
7
+
8
+ module Leeroy
9
+ class State < Leeroy::Types::Dash
10
+ include Leeroy::Helpers::Dumpable
11
+ include Leeroy::Helpers::Logging
12
+ include Leeroy::Helpers::Polling
13
+ include Leeroy::Helpers::State
14
+
15
+ # state properties
16
+ property :data, coerce: Leeroy::Types::StateData, default: {}
17
+ property :metadata, coerce: Leeroy::Types::StateMetadata, default: {}
18
+
19
+ def fetch(*args, &block)
20
+ begin
21
+ self.data.send(:fetch, *args, &block)
22
+
23
+ rescue KeyError => e
24
+ logger.debug e.message
25
+
26
+ not_found = args[0]
27
+ logger.info "property '#{not_found}' not found in statedata, checking state"
28
+
29
+ begin
30
+ self.send(not_found.to_sym, &block)
31
+
32
+ rescue KeyError => e
33
+ logger.debug e.message
34
+
35
+ logger.warn "property '#{not_found}' not found in statedata or state"
36
+ end
37
+
38
+ rescue StandardError => e
39
+ raise e
40
+ end
41
+ end
42
+
43
+ def method_missing(method, *args, &block)
44
+ begin
45
+ self.data.send(method.to_sym, *args, &block)
46
+
47
+ rescue NoMethodError => e
48
+ logger.debug e.message
49
+ if self.respond_to?(method.to_sym)
50
+ self.send(method.to_sym, *args, &block)
51
+ else
52
+ raise e
53
+ end
54
+
55
+ rescue StandardError => e
56
+ raise e
57
+ end
58
+ end
59
+
60
+ def initialize(*args, &block)
61
+ super
62
+
63
+ self.dump_properties = [
64
+ :data,
65
+ :metadata,
66
+ ]
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ require 'leeroy'
2
+ require 'leeroy/env'
3
+ require 'leeroy/helpers'
4
+ require 'leeroy/helpers/env'
5
+ require 'leeroy/helpers/logging'
6
+ require 'leeroy/helpers/state'
7
+ require 'leeroy/state'
8
+ require 'leeroy/task'
9
+ require 'leeroy/types/mash'
10
+
11
+ module Leeroy
12
+ module Task
13
+ class Base
14
+
15
+ include Leeroy::Task
16
+ include Leeroy::Helpers
17
+ include Leeroy::Helpers::Env
18
+ include Leeroy::Helpers::Logging
19
+ include Leeroy::Helpers::State
20
+
21
+ def initialize(params = {})
22
+ begin
23
+ @global_options = params.fetch(:global_options, {})
24
+ logger.debug("global_options: #{self.global_options.to_s}")
25
+
26
+ logger.debug("setting options")
27
+ @options = params.fetch(:options, {})
28
+ logger.debug("options: #{self.options.to_s}")
29
+
30
+ logger.debug("setting args")
31
+ @args = params.fetch(:args, {})
32
+ logger.debug("args: #{self.args.to_s}")
33
+
34
+ logger.debug("setting env")
35
+ @env = Leeroy::Env.new({}, params.fetch(:env, ENV))
36
+ logger.debug("env: #{self.env.to_s}")
37
+
38
+ logger.debug("setting state")
39
+ @state = Leeroy::State.new(state_from_pipe(params.fetch(:state, {})))
40
+ rotate_task_metadata
41
+ logger.debug("state: #{self.state}")
42
+
43
+ logger.debug("base initialization of #{self.class.to_s} complete")
44
+ rescue StandardError => e
45
+ raise e
46
+ end
47
+ end
48
+
49
+ def perform(args = self.args, options = self.options, global_options = self.global_options)
50
+ begin
51
+ self.logger.info("performing #{self.class.to_s}")
52
+ self.logger.debug("args: #{args.inspect}")
53
+ self.logger.debug("options: #{options.inspect}")
54
+
55
+ rescue StandardError => e
56
+ raise e
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,126 @@
1
+ require 'leeroy'
2
+ require 'leeroy/task'
3
+ require 'leeroy/helpers/aws'
4
+ require 'leeroy/helpers/polling'
5
+ require 'leeroy/types/image'
6
+ require 'leeroy/types/phase'
7
+
8
+ module Leeroy
9
+ module Task
10
+ class Image < Leeroy::Task::Base
11
+ include Leeroy::Helpers::AWS
12
+ include Leeroy::Helpers::Polling
13
+
14
+ def perform(args = self.args, options = self.options, global_options = self.global_options)
15
+ begin
16
+ super(args, options, global_options)
17
+
18
+ phase = Leeroy::Types::Phase.new(self.state.fetch('phase', options[:phase]))
19
+ self.state.phase = phase
20
+
21
+ # create image
22
+ image_params = _genImageParams(phase)
23
+
24
+ logger.debug "image_params: #{image_params.inspect}"
25
+
26
+ image = Leeroy::Types::Image.new(image_params)
27
+ resp = ec2Request(:create_image, image.run_params)
28
+
29
+ imageid = resp.image_id
30
+ logger.debug "imageid: #{imageid}"
31
+
32
+ self.state.imageid = imageid
33
+
34
+ _prepImageCreationPolling
35
+ poll(imageid)
36
+
37
+ dump_state
38
+
39
+ logger.debug "done performing for #{self.class}"
40
+
41
+ rescue StandardError => e
42
+ raise e
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def _prepImageCreationPolling
49
+ # poll to make sure image is created
50
+ self.poll_callback = lambda do |imageid|
51
+ begin
52
+ run_params = { :image_ids => [imageid], :owners => ['self'] }
53
+ resp = ec2Request(:describe_images, run_params)
54
+
55
+ state = resp.images[0].state
56
+
57
+ if state == 'pending'
58
+ logger.debug "image #{imageid} still pending"
59
+ nil
60
+ elsif state == 'available'
61
+ logger.debug "image #{imageid} available"
62
+ imageid
63
+ else
64
+ raise "image creation failed: #{resp.images[0].state_reason.message}"
65
+ end
66
+
67
+ rescue Aws::EC2::Errors::InvalidAMIIDNotFound => e
68
+ logger.debug "instance #{instanceid} not found"
69
+ nil
70
+ rescue StandardError => e
71
+ raise e
72
+ end
73
+ end
74
+
75
+ self.poll_timeout = checkEnv('LEEROY_POLL_TIMEOUT').to_i
76
+ self.poll_interval = checkEnv('LEEROY_POLL_INTERVAL').to_i
77
+ end
78
+
79
+ def _genImageParams(phase, state = self.state, env = self.env, ec2 = self.ec2, options = self.options)
80
+ begin
81
+ logger.debug "generating params for creating an EC2 image"
82
+
83
+ image_params = Leeroy::Types::Mash.new
84
+
85
+ image_params.phase = phase
86
+
87
+ # get instance_id from state or options
88
+ instance_id = state.instanceid? ? state.instanceid : options[:instance]
89
+ raise "Unable to determine instance ID, exiting." if instance_id.nil?
90
+ logger.debug "instance_id: #{instance_id}"
91
+ image_params.instance_id = instance_id
92
+
93
+ # were we given an app_name?
94
+ app_name = state.app_name? ? state.app_name : checkEnv('LEEROY_APP_NAME')
95
+ logger.debug "app_name: #{app_name}"
96
+
97
+ # were we given an image index?
98
+ index = _genImageIndex(state, env, ec2, options).to_s
99
+ logger.debug "index: #{index}"
100
+
101
+ # build target depends on phase
102
+ build_target = phase == 'gold_master' ? 'master' : checkEnv('LEEROY_BUILD_TARGET')
103
+
104
+ image_params.name = [app_name, build_target, index].join('-')
105
+
106
+ image_params
107
+
108
+ rescue StandardError => e
109
+ raise e
110
+ end
111
+ end
112
+
113
+ def _genImageIndex(state = self.state, env = self.env, ec2 = self.ec2, options = self.options)
114
+ begin
115
+ logger.debug "determining gold master instance ID"
116
+
117
+ options[:index] or getGoldMasterImageIndex.to_i + 1
118
+
119
+ rescue StandardError => e
120
+ raise e
121
+ end
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,200 @@
1
+ require 'leeroy'
2
+ require 'leeroy/task'
3
+ require 'leeroy/helpers/aws'
4
+ require 'leeroy/helpers/polling'
5
+
6
+ module Leeroy
7
+ module Task
8
+ class Instantiate < Leeroy::Task::Base
9
+ include Leeroy::Helpers::AWS
10
+ include Leeroy::Helpers::Polling
11
+
12
+ def perform(args = self.args, options = self.options, global_options = self.global_options)
13
+ begin
14
+ super(args, options, global_options)
15
+
16
+ phase = Leeroy::Types::Phase.new(self.state.fetch('phase', options[:phase]))
17
+ self.state.phase = phase
18
+
19
+ # resolve various AWS resources from human-readable inputs
20
+ _resolveResources
21
+
22
+ # create instance
23
+ instance = Leeroy::Types::Instance.new(_genInstanceParams)
24
+ resp = ec2Request(:run_instances, instance.run_params)
25
+ instanceid = resp.instances[0].instance_id
26
+ self.state.instanceid = instanceid
27
+
28
+ # wait until instance is starting
29
+ _prepInstanceCreationPolling
30
+ poll(instanceid)
31
+
32
+ # tag instance
33
+ instance_name = phase == 'gold_master' ? getGoldMasterInstanceName : getApplicationInstanceName
34
+ createTags({'Name' => instance_name})
35
+
36
+ # write semaphore
37
+ s3_object = buildS3ObjectName(instanceid, 'semaphores')
38
+ payload = _readSemaphore(phase)
39
+ semaphore = setSemaphore(genSemaphore(s3_object, payload))
40
+ self.state.semaphore = semaphore
41
+
42
+ # wait until instance is done provisioning
43
+ _prepInstanceProvisionPolling
44
+ poll(semaphore)
45
+
46
+ dump_state
47
+
48
+ logger.debug "done performing for #{self.class}"
49
+
50
+ rescue StandardError => e
51
+ raise e
52
+ end
53
+ end
54
+
55
+ def initialize(*args, &block)
56
+ super
57
+
58
+ end
59
+
60
+ private
61
+
62
+ def _prepInstanceCreationPolling
63
+ # poll to make sure instance is created
64
+ self.poll_callback = lambda do |instanceid|
65
+ begin
66
+ run_params = { :instance_ids => [instanceid] }
67
+ resp = ec2Request(:describe_instances, run_params)
68
+
69
+ rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e
70
+ logger.debug "instance #{instanceid} not found"
71
+ nil
72
+ rescue StandardError => e
73
+ raise e
74
+ end
75
+ end
76
+
77
+ self.poll_timeout = 30
78
+ self.poll_interval = 2
79
+ end
80
+
81
+ def _prepInstanceProvisionPolling
82
+ # poll until semaphore has been removed
83
+ self.poll_callback = lambda {|s| checkSemaphore(s).nil?}
84
+ self.poll_timeout = checkEnv('LEEROY_POLL_TIMEOUT').to_i
85
+ self.poll_interval = checkEnv('LEEROY_POLL_INTERVAL').to_i
86
+ end
87
+
88
+ def _readSemaphore(phase)
89
+ begin
90
+ template = File.join(checkEnv('LEEROY_PROVISIONING_TEMPLATE_PREFIX'), "#{phase}.erb")
91
+ logger.debug "processing template '#{template}'"
92
+
93
+ # this is heinous
94
+ # http://stackoverflow.com/a/22777806/17597
95
+ rendered = String.new
96
+
97
+ begin
98
+ old_stdout = $stdout
99
+ $stdout = StringIO.new('','w')
100
+ ERB.new(File.read(template)).run
101
+ rendered = $stdout.string
102
+ ensure
103
+ $stdout = old_stdout
104
+ end
105
+
106
+ rendered
107
+
108
+ rescue StandardError => e
109
+ raise e
110
+ end
111
+ end
112
+
113
+ def _resolveResources(state = self.state, env = self.env, ec2 = self.ec2, options = self.options)
114
+ begin
115
+ # resolve VPC ID
116
+ if state.vpcid?
117
+ vpcid = state.vpcid
118
+ else
119
+ vpcname = checkEnv('LEEROY_BUILD_VPC')
120
+ vpcid = getVpcId(vpcname)
121
+ state.vpcid = vpcid
122
+ end
123
+
124
+ # resolve security group
125
+ if state.sgid?
126
+ sgid = state.sgid
127
+ else
128
+ sgname = checkEnv('LEEROY_BUILD_SECURITY_GROUP')
129
+ sgid = getSgId(sgname, vpcname, vpcid)
130
+ state.sgid = sgid
131
+ end
132
+
133
+ # resolve subnet
134
+ if state.subnetid?
135
+ subnetid = state.subnetid
136
+ else
137
+ subnetname = checkEnv('LEEROY_BUILD_SUBNET')
138
+ subnetid = getSubnetId(subnetname, vpcid)
139
+ state.subnetid = subnetid
140
+ end
141
+
142
+ rescue StandardError => e
143
+ raise e
144
+ end
145
+ end
146
+
147
+ def _genInstanceParams(state = self.state, env = self.env, ec2 = self.ec2, options = self.options)
148
+ begin
149
+ logger.debug "generating params for creating an EC2 instance"
150
+
151
+ # gather the necessary parameters
152
+ instance_params = Leeroy::Types::Mash.new
153
+
154
+ instance_params.security_group_ids = Array(state.sgid)
155
+ instance_params.subnet_id = state.subnetid
156
+
157
+ instance_params.key_name = checkEnv('LEEROY_BUILD_SSH_KEYPAIR')
158
+ instance_params.instance_type = checkEnv('LEEROY_BUILD_INSTANCE_TYPE')
159
+
160
+ instance_params.min_count = 1
161
+ instance_params.max_count = 1
162
+
163
+ instance_params.store('iam_instance_profile', {:name => checkEnv('LEEROY_BUILD_PROFILE_NAME')})
164
+
165
+ # some parameters depend on phase
166
+ phase = options[:phase]
167
+ logger.debug "phase is #{phase}"
168
+
169
+ # AMI id depends on phase
170
+ if phase == 'gold_master'
171
+ image_id = checkEnv('LEEROY_AWS_LINUX_AMI')
172
+ elsif phase == 'application'
173
+ image_id = state.imageid
174
+ end
175
+
176
+ instance_params.phase = phase
177
+
178
+ raise "unable to determine image ID for phase '#{phase}'" if image_id.nil?
179
+
180
+ instance_params.image_id = image_id
181
+
182
+ # user_data file depends on phase
183
+ user_data = File.join(checkEnv('LEEROY_USER_DATA_PREFIX'), phase)
184
+ if File.readable?(user_data)
185
+ instance_params.user_data = IO.readlines(user_data).join('')
186
+ else
187
+ raise "You must provide a readable user data script at #{user_data}."
188
+ end
189
+
190
+ logger.debug "instance_params: #{instance_params.inspect}"
191
+
192
+ instance_params
193
+
194
+ rescue StandardError => e
195
+ raise e
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,29 @@
1
+ require 'leeroy/task'
2
+
3
+ module Leeroy
4
+ module Task
5
+ class Sleep < Leeroy::Task::Base
6
+
7
+ def perform(args = self.args, options = self.options, global_options = self.global_options)
8
+ super(args, options, global_options)
9
+
10
+ begin
11
+ logger.debug "performing for #{self.class}"
12
+ logger.debug "state: #{self.state}"
13
+
14
+ interval = self.options[:interval].to_i
15
+ logger.debug "sleeping: #{interval} seconds"
16
+
17
+ sleep interval
18
+
19
+ dump_state
20
+
21
+ logger.debug "done performing for #{self.class}"
22
+
23
+ rescue StandardError => e
24
+ raise e
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ require 'leeroy/task'
2
+
3
+ module Leeroy
4
+ module Task
5
+ class Stub < Leeroy::Task::Base
6
+
7
+ def perform(args = self.args, options = self.options, global_options = self.global_options)
8
+ super
9
+
10
+ begin
11
+ logger.debug "performing for #{self.class}"
12
+ logger.debug "state: #{self.state}"
13
+ message = self.state.message
14
+
15
+ increment = self.options[:increment].to_i
16
+ logger.debug "increment: #{increment}"
17
+
18
+ logger.info "old message: #{message}"
19
+
20
+ if message.nil?
21
+ message = increment
22
+ else
23
+ message = message.to_i + increment
24
+ end
25
+
26
+ state.message = message
27
+
28
+ logger.info "new message: #{message}"
29
+
30
+ dump_state
31
+
32
+ logger.debug "done performing for #{self.class}"
33
+
34
+ rescue StandardError => e
35
+ raise e
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,49 @@
1
+ require 'leeroy'
2
+ require 'leeroy/task'
3
+ require 'leeroy/helpers/aws'
4
+
5
+ module Leeroy
6
+ module Task
7
+ class Terminate < Leeroy::Task::Base
8
+ include Leeroy::Helpers::AWS
9
+
10
+ def perform(args = self.args, options = self.options, global_options = self.global_options)
11
+ begin
12
+ super(args, options, global_options)
13
+
14
+ # destroy instance
15
+ terminated = destroyInstance
16
+
17
+ instanceid = self.state.instanceid
18
+ if terminated.include?(instanceid)
19
+ # clean up semaphore if present
20
+ semaphore = self.state.semaphore
21
+
22
+ if semaphore.nil?
23
+ # guess at semaphore from instance ID
24
+ s3_object = buildS3ObjectName(instanceid, 'semaphores')
25
+ bucket = checkEnv('LEEROY_S3_BUCKET')
26
+ semaphore = Leeroy::Types::Semaphore.new(bucket: bucket, object: s3_object, payload: '')
27
+ end
28
+
29
+ unless semaphore.nil?
30
+ logger.debug "clearing semaphore #{semaphore}"
31
+ clearSemaphore(semaphore)
32
+ end
33
+
34
+ logger.debug "clearing instanceid #{instanceid} from state"
35
+ self.state.instanceid = nil
36
+ end
37
+
38
+ dump_state
39
+
40
+ logger.debug "done performing for #{self.class}"
41
+
42
+ rescue StandardError => e
43
+ raise e
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ require 'leeroy'
2
+ require 'leeroy/helpers'
3
+ require 'leeroy/helpers/env'
4
+ require 'leeroy/helpers/logging'
5
+ require 'leeroy/helpers/state'
6
+ require 'leeroy/task/base'
7
+
8
+ module Leeroy
9
+ module Task
10
+ include Leeroy::Helpers
11
+ include Leeroy::Helpers::Env
12
+ include Leeroy::Helpers::State
13
+ include Leeroy::Helpers::Logging
14
+ attr_reader :global_options, :options, :args
15
+
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'hashie'
2
+
3
+ module Leeroy
4
+ module Types
5
+ class Dash < Hashie::Dash
6
+ include Hashie::Extensions::KeyConversion
7
+ include Hashie::Extensions::MethodAccess
8
+ include Hashie::Extensions::IndifferentAccess
9
+ include Hashie::Extensions::Dash::Coercion
10
+
11
+ def dumper
12
+ self.to_hash
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,53 @@
1
+ require 'leeroy'
2
+ require 'leeroy/types/dash'
3
+ require 'leeroy/types/mash'
4
+ require 'leeroy/types/phase'
5
+ require 'leeroy/helpers/dumpable'
6
+ require 'leeroy/helpers/logging'
7
+
8
+ module Leeroy
9
+ module Types
10
+ class Image < Leeroy::Types::Dash
11
+ include Leeroy::Helpers::Dumpable
12
+ include Leeroy::Helpers::Logging
13
+
14
+ property :phase, required: true, coerce: Leeroy::Types::Phase
15
+ property :aws_params
16
+
17
+ # AWS-specific params
18
+ property :instance_id, required: true
19
+ property :name, required: true
20
+
21
+ def initialize(*args, &block)
22
+ self.aws_params = [
23
+ :instance_id,
24
+ :name,
25
+ ]
26
+
27
+ self.dump_properties = self.aws_params
28
+
29
+ super
30
+ end
31
+
32
+ def run_params
33
+ begin
34
+ run_params = Leeroy::Types::Mash.new
35
+
36
+ self.aws_params.each {|key| run_params.store(key.to_s, self.fetch(key))}
37
+
38
+ logger.debug "run_params: #{run_params.inspect}"
39
+
40
+ run_params
41
+
42
+ rescue StandardError => e
43
+ raise e
44
+ end
45
+ end
46
+
47
+ def to_s
48
+ self.instance_id
49
+ end
50
+
51
+ end
52
+ end
53
+ end