leeroy_app 0.1.0

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 (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