linecook-gem 0.6.10 → 0.7.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.
@@ -1,373 +0,0 @@
1
- require 'net/http'
2
- require 'open-uri'
3
- require 'timeout'
4
- require 'tmpdir'
5
- require 'securerandom'
6
-
7
- require 'linecook-gem/util/ssh'
8
- require 'linecook-gem/util/executor'
9
-
10
- require 'aws-sdk'
11
-
12
- module Linecook
13
- # Installs a linecook image on a target device
14
- module Packager
15
- class EBS
16
- include Executor
17
-
18
- def initialize(hvm: true, size: 10, region: nil, copy_regions: [], account_ids: [])
19
- @hvm = hvm
20
- @size = size
21
- @region = region
22
- @copy_regions = copy_regions
23
- @account_ids = account_ids
24
- end
25
-
26
- def package(image, type: nil, ami: nil)
27
- @image = image
28
- @source = File.basename(@image)
29
- @name = "#{@source}-#{SecureRandom.hex(4)}"
30
- @type = type
31
- setup_image
32
- prepare
33
- execute("tar -C #{@mountpoint} -cpf - . | sudo tar -C #{@root} -xpf -")
34
- finalize
35
- snapshot
36
- cleanup
37
- create_ami if ami
38
- end
39
-
40
- private
41
-
42
- def prepare
43
- @mountpoint = Dir.mktmpdir
44
- execute("mkdir -p #{@mountpoint}")
45
- execute("mount #{@image} #{@mountpoint}")
46
- create_volume
47
- format_and_mount
48
- end
49
-
50
- def finalize
51
- execute("echo \"UUID=\\\"$(blkid -o value -s UUID #{@rootdev})\\\" / ext4 defaults 1 2\" > /tmp/fstab")
52
- execute("mv /tmp/fstab #{@root}/etc/fstab")
53
- chroot_exec('apt-get update')
54
- chroot_exec('apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y --force-yes --no-upgrade install grub-pc grub-legacy-ec2')
55
- chroot_exec('update-grub')
56
- execute("grub-install --root-directory=#{@root} $(echo #{@rootdev} | sed \"s/[0-9]*//g\")") if @hvm
57
- end
58
-
59
- def cleanup
60
- execute("umount #{@image}")
61
- execute("rm -f #{@image}")
62
- execute("rmdir #{@mountpoint}")
63
- end
64
-
65
- def chroot_exec(command)
66
- execute("mount -o bind /dev #{@root}/dev")
67
- execute("mount -o bind /sys #{@root}/sys")
68
- execute("mount -t proc none #{@root}/proc")
69
- execute("cp /etc/resolv.conf #{@root}/etc")
70
- execute("chroot #{@root} #{command}")
71
- execute("umount #{@root}/dev")
72
- execute("umount #{@root}/sys")
73
- execute("umount #{@root}/proc")
74
- end
75
-
76
- def partition
77
- execute("parted -s #{@rootdev} mklabel msdos")
78
- execute("parted -s #{@rootdev} mkpart primary ext2 0% 100%")
79
- @rootdev = "#{@rootdev}1"
80
- end
81
-
82
- def format_and_mount
83
- partition if @hvm
84
- execute("mkfs.ext4 #{@rootdev}")
85
- @root = Dir.mktmpdir
86
- execute("mkdir -p #{@root}")
87
- execute("mount #{@rootdev} #{@root}")
88
- end
89
-
90
- def instance_id
91
- @instance_id ||= metadata('instance-id')
92
- end
93
-
94
- def availability_zone
95
- @availability_zone ||= metadata('placement/availability-zone')
96
- end
97
-
98
- def client(region: nil)
99
- @client = nil if region
100
- @client ||= begin
101
- region ||= @region
102
- puts "Using AWS region #{region} for following request"
103
- credentials = Aws::Credentials.new(Linecook.config[:aws][:access_key], Linecook.config[:aws][:secret_key])
104
- Aws::EC2::Client.new(region: region, credentials: credentials)
105
- end
106
- end
107
-
108
- def create_volume
109
- resp = client.create_volume({
110
- size: @size,
111
- availability_zone: availability_zone, # required
112
- volume_type: "standard", # accepts standard, io1, gp2
113
- })
114
-
115
- @volume_id = resp.volume_id
116
- rootdev = free_device
117
-
118
- puts "Waiting for volume to become available"
119
- wait_for_state('available', 120) do
120
- client.describe_volumes(volume_ids: [@volume_id]).volumes.first.state
121
- end
122
-
123
- resp = client.attach_volume({
124
- volume_id: @volume_id,
125
- instance_id: instance_id,
126
- device: rootdev,
127
- })
128
-
129
- puts "Waiting for volume to attach"
130
- wait_for_state('attached', 120) do
131
- client.describe_volumes(volume_ids: [@volume_id]).volumes.first.attachments.first.state
132
- end
133
- @rootdev = "/dev/#{rootdev}"
134
- end
135
-
136
- def snapshot
137
- execute("umount #{@root}")
138
- puts 'Creating snapshot'
139
- resp = client.detach_volume(volume_id: @volume_id)
140
- wait_for_state('available', 120) do
141
- client.describe_volumes(volume_ids: [@volume_id]).volumes.first.state
142
- end
143
- resp = client.create_snapshot(volume_id: @volume_id, description: "Snapshot of #{@name}")
144
- @snapshot_id = resp.snapshot_id
145
- tag(@snapshot_id, Name: "Linecook snapshot for #{@source}", type: @type, image: @name, hvm: @hvm.to_s)
146
- client.delete_volume(volume_id: @volume_id)
147
- end
148
-
149
- def create_ami
150
-
151
- puts "Waiting for snapshot #{@snapshot_id} to be completed"
152
- wait_for_state('completed', 900) do
153
- client.describe_snapshots(snapshot_ids: [@snapshot_id]).snapshots.first.state
154
- end
155
-
156
- puts "Registering an AMI with for #{@name}"
157
- options = {
158
- name: @name,
159
- virtualization_type: @hvm ? 'hvm' : 'paravirtual',
160
- architecture: 'x86_64', # fixme
161
- root_device_name: '/dev/sda1', # fixme when does this need to be sda?
162
- block_device_mappings: [{
163
- device_name: '/dev/sda1',
164
- ebs: {
165
- snapshot_id: @snapshot_id,
166
- volume_size: @size,
167
- volume_type: @hvm ? 'gp2' : 'standard', # fixme: support iops?
168
- delete_on_termination: true,
169
- }
170
- }]
171
- }
172
- options.merge!({sriov_net_support: 'simple'}) if @hvm
173
-
174
- resp = client.register_image(**options)
175
- @ami_id = resp.image_id
176
-
177
- puts "Waiting for #{@ami_id} to become available"
178
-
179
- wait_for_state('available', 300) do
180
- client.describe_images(image_ids: [@ami_id]).images.first.state
181
- end
182
-
183
- amis = {
184
- @region => @ami_id
185
- }
186
-
187
- @copy_regions.each do |region|
188
- puts "Copying #{@ami_id} to #{region}"
189
- resp = client(region: region).copy_image({
190
- source_region: @region,
191
- source_image_id: @ami_id,
192
- name: @name,
193
- description: "Copy of #{@name}",
194
- })
195
- ami = resp.image_id
196
- puts "Waiting for #{ami} to become available in #{region}"
197
- wait_for_state('available', 1800) do
198
- client.describe_images(image_ids: [ami]).images.first.state
199
- end
200
- amis[region] = ami
201
- end
202
-
203
- unless @account_ids.empty?
204
- amis.each do |region, ami|
205
- puts "Updating account permissions for #{ami} in #{region}"
206
- client(region: region).modify_image_attribute({
207
- operation_type: 'add',
208
- attribute: 'launchPermission',
209
- image_id: ami,
210
- user_ids: @account_ids
211
- })
212
- end
213
- end
214
-
215
- amis.each do |region, ami|
216
- tag(ami, region: region, source: @source, name: @name, type: @type, hvm: @hvm.to_s)
217
- end
218
- amis
219
- end
220
-
221
- def free_device
222
- prefix = device_prefix
223
- ('f'..'zzz').to_a.each do |suffix|
224
- device = "#{prefix}#{suffix}"
225
- if free_device?(device)
226
- lock_device(device)
227
- return device
228
- end
229
- end
230
- return nil
231
- end
232
-
233
- def free_device?(device)
234
- test("[ ! -e /dev/#{device} ]") && test("[ ! -e /run/lock/linecook-#{device} ]")
235
- end
236
-
237
- def lock_device(device)
238
- execute("echo #{Process.pid} > /run/lock/linecook-#{device}")
239
- end
240
-
241
- def unlock_device(device)
242
- execute("rm /run/lock/linecook-#{device}")
243
- end
244
-
245
- def device_prefix
246
- prefixes = ['xvd', 'sd']
247
- capture('ls -1 /sys/block').lines.each do |dev|
248
- prefixes.each do |prefix|
249
- return prefix if dev =~ /^#{prefix}/
250
- end
251
- end
252
- return prefixes.first
253
- end
254
-
255
- def setup_image
256
- path = "/tmp/#{File.basename(@image)}"
257
- if instance_id
258
- return if File.exists?(@image)
259
- Linecook::Downloader.download(image_url, path, encrypted: true)
260
- @image = path
261
- else
262
- start_node
263
- @remote.run("wget '#{image_url}' -nv -O #{path}")
264
- @image = Linecook::Crypto.new(remote: @remote).decrypt_file(path)
265
- end
266
- end
267
-
268
- def image_url
269
- @url ||= Linecook::ImageManager.url(File.basename(@image), type: @type)
270
- end
271
-
272
- def start_node
273
- resp = client.run_instances(
274
- image_id: find_ami,
275
- min_count: 1,
276
- max_count: 1,
277
- instance_type: 'c4.large',
278
- instance_initiated_shutdown_behavior: 'terminate',
279
- security_groups: [security_group],
280
- key_name: key_pair
281
- )
282
- @instance_id = resp.instances.first.instance_id
283
- @availability_zone = resp.instances.first.placement.availability_zone
284
-
285
- puts 'Waiting for temporary instance to come online'
286
- wait_for_state('running', 300) do
287
- client.describe_instances(instance_ids: [@instance_id]).reservations.first.instances.first.state.name
288
- end
289
- tag(@instance_id, Name: 'linecook-temporary-installer-node')
290
- @remote = Linecook::SSH.new(instance_ip, username: 'ubuntu', keyfile: Linecook::SSH.private_key)
291
- @remote.upload("exec shutdown -h 60 'Delayed shutdown started'", '/tmp/delay-shutdown')
292
- execute('mv /tmp/delay-shutdown /etc/init/delay-shutdown.conf') # ubuntism is ok, since the temporary host can always be ubuntu
293
- execute('start delay-shutdown')
294
- # Install crypto deps
295
- execute('apt-get install -y --force-yes build-essential ruby ruby-dev')
296
- execute('gem install rbnacl rbnacl-libsodium')
297
- end
298
-
299
- def find_ami
300
- url = "http://uec-images.ubuntu.com/query/trusty/server/released.current.txt"
301
- type = @hvm ? 'hvm' : 'paravirtual'
302
- data = open(url).read.split("\n").map{|l| l.split}.detect do |ary|
303
- ary[4] == 'ebs' &&
304
- ary[5] == 'amd64' &&
305
- ary[6] == @region &&
306
- ary.last == type
307
- end
308
- data[7]
309
- end
310
-
311
- def instance_ip
312
- client.describe_instances(instance_ids: [@instance_id]).reservations.first.instances.first.public_ip_address
313
- end
314
-
315
- def security_group
316
- group_name = 'linecook-global-ssh'
317
- resp = client.describe_security_groups(filters: [{name: 'group-name', values: [group_name]}])
318
- if resp.security_groups.length < 1
319
- resp = client.create_security_group({
320
- group_name: group_name,
321
- description: "Allow global ssh for linecook temporary builder instances",
322
- })
323
-
324
- resp = client.authorize_security_group_ingress({
325
- group_name: group_name,
326
- ip_protocol: "tcp",
327
- from_port: 22,
328
- to_port: 22,
329
- cidr_ip: "0.0.0.0/0",
330
- })
331
- end
332
- group_name
333
- end
334
-
335
- def tag(id, **kwargs)
336
- puts "Will tag #{id} with #{kwargs}"
337
- resp = client(region: kwargs[:region]).create_tags(resources: [id], tags: kwargs.map{ |k,v| {key: k.to_s, value: v.to_s } })
338
- end
339
-
340
- def key_pair
341
- pubkey = Linecook::SSH.public_key
342
- resp = client.describe_key_pairs({
343
- filters: [ { name: 'fingerprint', values: [Linecook::SSH.sshv2_fingerprint(pubkey)] } ]
344
- })
345
-
346
- if resp.key_pairs.length >= 1
347
- return resp.key_pairs.first.key_name
348
- else
349
- keyname = "linecook-#{SecureRandom.uuid}"
350
- resp = client.import_key_pair({
351
- key_name: keyname,
352
- public_key_material: pubkey,
353
- })
354
- return keyname
355
- end
356
- end
357
-
358
- def metadata(key)
359
- (Timeout::timeout(1) { Net::HTTP.get(URI(File.join("http://169.254.169.254/latest/meta-data", key))) } rescue nil)
360
- end
361
-
362
- def wait_for_state(desired, timeout)
363
- attempts = 0
364
- state = nil
365
- while attempts < timeout && state != desired
366
- state = yield
367
- attempts += 1
368
- sleep(1)
369
- end
370
- end
371
- end
372
- end
373
- end
@@ -1,23 +0,0 @@
1
- require 'linecook-gem/packager/ebs'
2
-
3
- module Linecook
4
- module Packager
5
- extend self
6
-
7
- def package(image, type: type, **args)
8
- provider.package(image, type: type, **args)
9
- end
10
-
11
- private
12
- def provider
13
- name = Linecook.config[:packager][:provider]
14
- config = Linecook.config[:packager][name]
15
- case name
16
- when :ebs
17
- Linecook::Packager::EBS.new(**config)
18
- else
19
- fail "No packager implemented for for #{name}"
20
- end
21
- end
22
- end
23
- end
@@ -1,149 +0,0 @@
1
- require 'tmpdir'
2
- require 'fileutils'
3
-
4
- require 'chef-provisioner'
5
- require 'chefdepartie'
6
-
7
- module Linecook
8
- module Chef
9
- extend self
10
-
11
- def provision(build, role)
12
- chef_config = setup
13
- role_config = Linecook.config[:roles][role.to_sym]
14
- script = ChefProvisioner::Bootstrap.generate(
15
- profile: role_config[:profile] || false,
16
- node_name: chef_config[:node_name],
17
- chef_version: chef_config[:version] || nil,
18
- first_boot: {
19
- run_list: role_config[:run_list]
20
- },
21
- audit: Linecook.config[:provisioner][:chefzero][:audit]
22
- )
23
-
24
- puts "Establishing connection to build..."
25
- build.start
26
- build.ssh.forward(chef_port)
27
- build.ssh.upload(script, '/tmp/chef_bootstrap')
28
- build.ssh.run('[ -f /var/chef/cache/chef-client-running.pid ] && sudo rm -f /var/chef/cache/chef-client-running.pid || true')
29
- build.ssh.run("sudo hostname #{chef_config[:node_name]}")
30
- build.ssh.run('sudo bash /tmp/chef_bootstrap')
31
- build.ssh.run('sudo rm -rf /etc/chef')
32
- build.ssh.stop_forwarding
33
- Chefdepartie.stop
34
- FileUtils.rm_rf(Cache.path)
35
- end
36
-
37
- def chef_port
38
- ChefProvisioner::Config.server.split(':')[-1].to_i
39
- end
40
-
41
- private
42
-
43
- def setup
44
- ChefProvisioner::Config.setup(client: 'linecook', listen: 'localhost')
45
- config = Linecook.config
46
-
47
- chef_config = config[:chef]
48
- chef_config.merge!(node_name: "linecook-#{SecureRandom.hex(4)}",
49
- chef_server_url: ChefProvisioner::Config.server)
50
- Chefdepartie.run(background: true, config: chef_config, cache: Cache.path)
51
- chef_config
52
- end
53
-
54
- # Required in order to have multiple builds run on different refs
55
- module Cache
56
- CACHE_PATH = File.join(Linecook::Config::LINECOOK_HOME, 'chefcache').freeze
57
- PIDFILE = File.join(CACHE_PATH, 'pid')
58
- STAMPFILE = File.join(CACHE_PATH, 'stamp')
59
- STALE_THRESHOLD = 86400 # one day in seconds
60
- WAIT_TIMEOUT = 60 # time to wait for port to become available again
61
-
62
- extend self
63
-
64
- def path
65
- @cache_path ||= begin
66
- FileUtils.mkdir_p(CACHE_PATH)
67
- cache_path = Dir.mktmpdir('linecook-chef-cache')
68
- build
69
- copy(cache_path)
70
- wait_for_close
71
- cache_path
72
- end
73
- end
74
-
75
- private
76
-
77
- def wait_for_close
78
- attempts = 0
79
- while attempts < WAIT_TIMEOUT
80
- begin
81
- Timeout::timeout(1) do
82
- begin
83
- s = TCPSocket.new('127.0.0.1', Linecook::Chef.chef_port)
84
- s.close
85
- return true
86
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
87
- return false
88
- end
89
- end
90
- rescue Timeout::Error
91
- puts "Port #{Linecook::Chef.chef_port} is still in use"
92
- sleep(1)
93
- end
94
- attempts += 0
95
- end
96
- end
97
-
98
- def copy(cache_path)
99
- FileUtils.copy_entry(CACHE_PATH, cache_path, preserve: true)
100
- end
101
-
102
- def build
103
- if stale
104
- puts 'Regenerating cookbook cache'
105
- begin
106
- Chefdepartie.run(background: true, config: Linecook.config[:chef], cache: CACHE_PATH)
107
- rescue => e
108
- puts e.message
109
- puts 'Cache tainted, rebuilding completely'
110
- Chefdepartie.stop
111
- FileUtils.rm_rf(CACHE_PATH)
112
- Chefdepartie.run(background: true, config: Linecook.config[:chef], cache: CACHE_PATH)
113
- ensure
114
- Chefdepartie.stop
115
- end
116
- write_stamp
117
- unlock
118
- end
119
- end
120
-
121
- def stale
122
- return false if locked?
123
- lock
124
- old?
125
- end
126
-
127
- def locked?
128
- File.exists?(PIDFILE) && (true if Process.kill(0, File.read(PIDFILE)) rescue false)
129
- end
130
-
131
- def lock
132
- File.write(PIDFILE, Process.pid)
133
- end
134
-
135
- def unlock
136
- FileUtils.rm_f(PIDFILE)
137
- end
138
-
139
- def old?
140
- return true unless File.exists?(STAMPFILE)
141
- (Time.now.to_i - File.read(STAMPFILE).to_i) > STALE_THRESHOLD
142
- end
143
-
144
- def write_stamp
145
- File.write(STAMPFILE, Time.now.to_i)
146
- end
147
- end
148
- end
149
- end