image_optimize 1.0.0 → 1.0.1

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.
@@ -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