image_optimize 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ # Author: cary@rightscale.com
2
+ # Copyright 2014 RightScale, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'image_bundle/image_bundle_base'
18
+ require 'image_bundle/image_bundle_ec2_ebs'
19
+ require 'image_bundle/image_bundle_ec2_s3'
@@ -0,0 +1,116 @@
1
+ # Author: cary@rightscale.com
2
+ # Copyright 2014 RightScale, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ require 'mixins/common'
17
+ require 'mixins/command'
18
+
19
+ # Base class for image utils
20
+ #
21
+ module ImageOptimize
22
+
23
+ class ImageBundleBase
24
+
25
+ include ImageOptimize::Common
26
+ include ImageOptimize::Command
27
+
28
+ def initialize(instance_api_client, full_api_client)
29
+
30
+ @dry_run = false
31
+
32
+ @log ||= Logger.new(STDOUT)
33
+ @instance_client = instance_api_client
34
+ unless @instance_client
35
+ @log.error"ERROR: you must pass an instance_api_client parameter."
36
+ end
37
+
38
+ @api_client = full_api_client
39
+ unless @api_client
40
+ @log.error"ERROR: you must pass a full_api_client parameter."
41
+ end
42
+ end
43
+
44
+ # Captures a "snapshot" of the current VM configuration
45
+ def snapshot_instance(name, description)
46
+ not_implemented
47
+ end
48
+
49
+ # Turn the VM "snapshot" into an launchable image
50
+ def register_image(name, description)
51
+ not_implemented
52
+ end
53
+
54
+ def add_image_to_next_instance
55
+ # get next instance
56
+ @log.info "Lookup next_instance for the server."
57
+ next_instance = get_instance_parent.show.next_instance
58
+
59
+ unless @dry_run
60
+ raise "ERROR: image cannot be nil. Be sure to run register_image method first." unless @image
61
+ # set next instance to use our cached image
62
+ @log.info "Update next instance to launch with our new image."
63
+ next_instance.update(:instance => {:image_href => @image.show.href})
64
+ end
65
+ end
66
+
67
+ def log(logger)
68
+ @log = logger
69
+ end
70
+
71
+ protected
72
+
73
+ # Get the resources associated with this instance
74
+ def instance
75
+ @instance ||= @instance_client.get_instance
76
+ end
77
+
78
+ def get_id(resource)
79
+ # the id is the last part of thr href path
80
+ resource.href.split('/').last
81
+ end
82
+
83
+ def fail(message="unspecified error occured", exception=nil)
84
+ if exception
85
+ @log.debug "Exception: #{exception.message}"
86
+ @log.debug exception.backtrace.inspect
87
+ end
88
+ @log.error "FATAL: #{message}"
89
+ exit 1
90
+ end
91
+
92
+ # Get the parent for this instance
93
+ #
94
+ # The parent may be a Server or ServerArray reosurce.
95
+ #
96
+ # XXX: we can't get to the server resource from the instance facing API
97
+ # the server resource holds the reference to the next instance.
98
+ # To deal with this we create an instance resource using the user
99
+ # authenticated @api_client, then grab it's "parent".
100
+ #
101
+ # == Returns:
102
+ # @return [RightApi::Resource] server a server API resource
103
+ def get_instance_parent
104
+ @log.info "Lookup RightScale server for this instance."
105
+ api_cloud = @api_client.clouds(:id => get_id(instance.cloud))
106
+ api_instance = api_cloud.show.instances.index(:id => get_id(instance))
107
+ api_instance.show.parent
108
+ end
109
+
110
+ def debug_mode?
111
+ @log.level == Logger::DEBUG
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,131 @@
1
+ # Author: cary@rightscale.com
2
+ # Copyright 2014 RightScale, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ module ImageOptimize
18
+
19
+ # Base class for snapshoting and registration of EC2 images
20
+ #
21
+ class ImageBundleEc2Base < ImageBundleBase
22
+
23
+ RETRY_TIMEOUT_SEC = 2 * 60 * 60 # 2 hr
24
+
25
+ def initialize(instance_api_client, full_api_client, aws_access_key, aws_secret_key, kernel_id_override = nil)
26
+ load_meta_data
27
+ @aws_access_key = aws_access_key
28
+ @aws_secret_key = aws_secret_key
29
+ @kernel_override = kernel_id_override
30
+ super(instance_api_client, full_api_client)
31
+ end
32
+
33
+ def register_image(name=nil, description=nil)
34
+ cmd = register_command(name, description)
35
+ @log.info "Running register image command..."
36
+ unless @dry_run
37
+ status, cmd_output = execute(cmd)
38
+ raise "FATAL: unable to register new image" unless status.exitstatus == 0
39
+ image_uuid = parse_ami(cmd_output)
40
+ @log.info "New image: #{image_uuid}"
41
+
42
+ # wait for image to be available
43
+ wait_for_image(image_uuid)
44
+ end
45
+ end
46
+
47
+ protected
48
+
49
+ def parse_ami(command_output)
50
+ /(ami-[a-z0-9]+)/.match(command_output).captures[0]
51
+ end
52
+
53
+ def register_command(name, description)
54
+ raise "Not implemented"
55
+ end
56
+
57
+ def wait_for_image(image_uuid)
58
+ # wait for image to be available
59
+ @log.info "Query for the new image"
60
+ start = Time.now
61
+ @image = get_image(image_uuid)
62
+ delay_sec = 10
63
+ Timeout::timeout(RETRY_TIMEOUT_SEC) do
64
+ while @image == nil do
65
+ @log.info " Image #{image_uuid} not yet available. Checking again in #{delay_sec} seconds..."
66
+ sleep delay_sec
67
+ @image = get_image(image_uuid)
68
+ end
69
+ end
70
+ total = Time.new - start
71
+ @log.info "Total time waited: #{total}"
72
+ end
73
+
74
+ def get_image(uuid)
75
+ # use full api client to look for image
76
+ image = get_cloud.show.images.index(:filter => [ "resource_uid==#{uuid}" ]).first
77
+ @log.info "Found image #{uuid} named '#{image.name}'" if image
78
+ image
79
+ end
80
+
81
+ # Get the cloud for this instance
82
+ #
83
+ # XXX: we cannot query cloud image via instance API
84
+ #
85
+ # == Returns:
86
+ # @return [RightApi::Resource] server a server API resource
87
+ def get_cloud
88
+ cloud_href = instance.cloud.href
89
+ cloud_id = cloud_href.split('/').last
90
+ cloud = @api_client.clouds(:id => cloud_id)
91
+ end
92
+
93
+ # detect EC2 region from EC2 metadata
94
+ #
95
+ def region
96
+ ec2_zone = ENV['EC2_PLACEMENT_AVAILABILITY_ZONE'] # ec2_zone = `curl http://169.254.169.254/latest/meta-data/placement/availability-zone/`
97
+ unless ec2_zone
98
+ fail("The EC2_PLACEMENT_AVAILABILITY_ZONE environment variable is not defined. Did you load EC2 metadata?")
99
+ end
100
+ region = /(.*\-.*\-[1-9]*)/.match(ec2_zone).captures.first
101
+ @log.info "detected current region is #{region}"
102
+ region
103
+ end
104
+
105
+ # detect kernel from EC2 metadata
106
+ #
107
+ def kernel_aki
108
+ kernel = @kernel_override # allow user to override
109
+ kernel ||= ENV['EC2_KERNEL_ID'] # use same kernel as VM is running
110
+ end
111
+
112
+ private
113
+
114
+ def load_meta_data
115
+ begin
116
+ require '/var/spool/cloud/meta-data-cache.rb'
117
+ rescue LoadError => e
118
+ puts "FATAL: not a RightScale managed VM, or you already cleaned up your meta-data file."
119
+ exit 1
120
+ end
121
+ end
122
+
123
+ def common_options
124
+ options = ""
125
+ options << "--debug " if debug_mode?
126
+ end
127
+
128
+
129
+ end
130
+
131
+ end
@@ -0,0 +1,77 @@
1
+ # Author: cary@rightscale.com
2
+ # Copyright 2014 RightScale, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'image_bundle/image_bundle_ec2'
18
+
19
+ module ImageOptimize
20
+
21
+ # Handles the snapshoting and registration of EBS based images
22
+ #
23
+ class ImageBundleEc2EBS < ImageBundleEc2Base
24
+
25
+ def snapshot_instance(name=nil, description=nil)
26
+
27
+ # find root volume
28
+ #
29
+ root_volume = nil
30
+ begin
31
+ device_name = "/dev/sda"
32
+ @log.info "Locating root volume attached to #{device_name}..."
33
+ attachment = instance.volume_attachments.index.select { |va| va.device =~ /#{device_name}/ }.first
34
+ root_volume = attachment.volume
35
+ @attachment_device = attachment.device
36
+ @log.info "Found volume #{root_volume.show.name} attached at #{@attachment_device}"
37
+ rescue Exception => e
38
+ fail("FATAL: cannot find root volume. Check device_name which can vary depending on hypervisor and/or kernel. Also instance-store images not currently supported.", e)
39
+ end
40
+
41
+ # create snapshot
42
+ @log.info "Creating snapshot of server."
43
+ options = { :parent_volume_href => root_volume.href }
44
+ options.merge!(:name => name) if name
45
+ options.merge!(:description => description) if description
46
+ snapshot = @instance_client.volume_snapshots.create(:volume_snapshot => options)
47
+ @log.info "Snapshot name '#{name}'" if name
48
+
49
+ # wait for snapshot to complete
50
+ @log.info "Waiting for snapshot to become available"
51
+ current_state = snapshot.show.state
52
+ delay_sec = 10
53
+ Timeout::timeout(RETRY_TIMEOUT_SEC) do
54
+ while current_state != "available" do
55
+ @log.info " snapshot state: #{current_state}. try again in #{delay_sec} seconds..."
56
+ sleep delay_sec
57
+ current_state = snapshot.show.state
58
+ end
59
+ end
60
+ @log.info "Snapshot is now available"
61
+ @snapshot_id = snapshot.show.resource_uid
62
+ end
63
+
64
+ def register_command(name=nil, description=nil)
65
+ unless @snapshot_id
66
+ fail("@snapshot_id cannot be nil. Be sure to run snapshot_instance first.")
67
+ end
68
+
69
+ # use ec2 tools to register snapshot as an image
70
+ # TODO: this command only maps in 4 ephemeral devices to the new image. Use metadata to get actual count.
71
+ cmd = "ec2-register --region #{region} --snapshot #{@snapshot_id} --description '#{@image_description}' --block-device-mapping '/dev/sdb=ephemeral0' --block-device-mapping '/dev/sdc=ephemeral1' --block-device-mapping '/dev/sdd=ephemeral2' --block-device-mapping '/dev/sde=ephemeral3' --kernel #{kernel_aki} --root-device-name #{@attachment_device} --architecture x86_64 --name '#{name}' #{common_options}"
72
+ cmd
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,114 @@
1
+ # Author: cary@rightscale.com
2
+ # Copyright 2014 RightScale, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'image_bundle/image_bundle_ec2'
18
+
19
+ module ImageOptimize
20
+
21
+ # Handles the snapshoting and registration of EBS based images
22
+ #
23
+ class ImageBundleEc2S3 < ImageBundleEc2Base
24
+
25
+ BUNDLE_DIR = "/mnt/ephemeral/bundle"
26
+
27
+ # x509_key_file : [String] Absolute path to file holding AWS x.509 private key file
28
+ # x509_cert_file : [String] Absolute path to file holding AWS x.509 certificate file
29
+ # s3_bucket : [String] place to store image in S3
30
+ #
31
+ def initialize(instance_api_client, full_api_client, aws_access_key, aws_secret_key, aws_account_number, x509_key_file, x509_cert_file, s3_bucket, bundle_dir=nil, no_filter=nil, kernel_id = nil)
32
+ super(instance_api_client, full_api_client, aws_access_key, aws_secret_key, kernel_id)
33
+ @aws_account_number = aws_account_number
34
+ raise "no aws account number specified." unless @aws_account_number
35
+ @key_file = x509_key_file
36
+ raise "no path to x.509 private key file specified." unless @key_file
37
+ @cert_file = x509_cert_file
38
+ raise "no path to x.509 certificate file specified." unless @cert_file
39
+
40
+ @bucket = s3_bucket
41
+
42
+ # Where to place temporary image bundle before upload
43
+ @bundle_dir = bundle_dir
44
+ @bundle_dir ||= BUNDLE_DIR
45
+
46
+ # --no-filter
47
+ @no_filter = true if no_filter
48
+ end
49
+
50
+ # Captures a "snapshot" of the current VM configuration
51
+ def snapshot_instance(name, description)
52
+ bundle_image(name)
53
+ upload_bundle(name)
54
+ cleanup_bundle
55
+ end
56
+
57
+ # Turn the VM "snapshot" into an launchable image
58
+ def register_command(name, description)
59
+ #ec2-register my-s3-bucket/image_bundles/name/image.manifest.xml -n name -O your_access_key_id -W your_secret_access_key
60
+ cmd = "ec2-register #{@bucket}/image_bundles/#{name}/image.manifest.xml -n #{name} -O #{@aws_access_key} -W #{@aws_secret_key} --region #{region} --architecture x86_64 --description '#{description}' #{common_options}"
61
+ cmd
62
+ end
63
+
64
+ private
65
+
66
+ def upload_bundle(name)
67
+ @log.info "Running bundle upload command..."
68
+ cmd = "ec2-upload-bundle -b #{@bucket}/image_bundles/#{name} -m '#{@bundle_dir}/image.manifest.xml' -a #{@aws_access_key} -s #{@aws_secret_key} --retry --batch --region #{region} #{common_options}"
69
+ unless @dry_run
70
+ status, cmd_output = execute(cmd)
71
+ fail "FATAL: unable to upload S3 image bundle" unless status.exitstatus == 0
72
+ end
73
+ end
74
+
75
+ def bundle_image(name)
76
+ @log.info "Using #{@bundle_dir} locally to store image bundle."
77
+ @log.info "Running EC2 Bundle command..."
78
+ FileUtils.rm_rf(@bundle_dir)
79
+ FileUtils.mkdir_p(@bundle_dir)
80
+ cmd="ec2-bundle-vol --privatekey #{@key_file} --cert #{@cert_file} --user #{@aws_account_number} --destination #{@bundle_dir} --arch x86_64 --kernel #{kernel_aki} -B 'ami=sda,root=/dev/sda,ephemeral0=sdb,swap=sda3' #{excludes} --no-inherit #{common_options}"
81
+ cmd << " --no-filter" if @no_filter
82
+ unless @dry_run
83
+ status, cmd_output = execute(cmd)
84
+ fail "FATAL: unable to bundle new image" unless status.exitstatus == 0
85
+ end
86
+ end
87
+
88
+ # What directories should be excluded from bundle
89
+ # NOTE: be sure to exclude the directory containing the key_file and cert_file x.509 creds
90
+ #
91
+ def excludes
92
+ excludes = ""
93
+ [ File.dirname(@key_file), File.dirname(@cert_file), "/mnt", @bundle_dir ].uniq.each do |dir|
94
+ excludes += "--exclude #{dir} "
95
+ end
96
+ excludes
97
+ end
98
+
99
+ # remove bundle directory
100
+ #
101
+ def cleanup_bundle
102
+ FileUtils.rm_rf(@bundle_dir)
103
+ end
104
+
105
+ def s3_url(region)
106
+ if region == "us-east-1"
107
+ "https://s3.amazonaws.com"
108
+ else
109
+ "https://s3-#{region}.amazonaws.com"
110
+ end
111
+ end
112
+
113
+ end
114
+ end