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.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +48 -0
- data/LICENSE +13 -0
- data/README.md +128 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/bin/image_optimize +22 -0
- data/image_optimize.gemspec +24 -0
- data/lib/api_client_15.rb +86 -0
- data/lib/app/image_optimizer.rb +195 -0
- data/lib/image_bundle.rb +19 -0
- data/lib/image_bundle/image_bundle_base.rb +116 -0
- data/lib/image_bundle/image_bundle_ec2.rb +131 -0
- data/lib/image_bundle/image_bundle_ec2_ebs.rb +77 -0
- data/lib/image_bundle/image_bundle_ec2_s3.rb +114 -0
- data/lib/mixins/command.rb +40 -0
- data/lib/mixins/common.rb +23 -0
- data/rightscale/rightscript.sh +189 -0
- data/setup.sh +40 -0
- data/spec/api_client_spec.rb +39 -0
- data/spec/command_spec.rb +43 -0
- data/spec/image_bundle_ec2_spec.rb +87 -0
- data/spec/image_bundle_spec.rb +97 -0
- data/spec/image_optimizer_spec.rb +194 -0
- data/test/image_optimize_test.rb +50 -0
- metadata +47 -6
data/lib/image_bundle.rb
ADDED
@@ -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
|