vagrant-lxd 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.
@@ -0,0 +1,27 @@
1
+ #
2
+ # Copyright (c) 2017 Catalyst.net Ltd
3
+ #
4
+ # This file is part of vagrant-lxd.
5
+ #
6
+ # vagrant-lxd is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or (at
9
+ # your option) any later version.
10
+ #
11
+ # vagrant-lxd is distributed in the hope that it will be useful, but
12
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with vagrant-lxd. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+
20
+ module VagrantLXD
21
+ class Capability
22
+ def Capability.snapshot_list(machine)
23
+ env = machine.action(:snapshot_list)
24
+ env[:machine_snapshot_list] || []
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,145 @@
1
+ #
2
+ # Copyright (c) 2017 Catalyst.net Ltd
3
+ #
4
+ # This file is part of vagrant-lxd.
5
+ #
6
+ # vagrant-lxd is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or (at
9
+ # your option) any later version.
10
+ #
11
+ # vagrant-lxd is distributed in the hope that it will be useful, but
12
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with vagrant-lxd. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+
20
+ require 'vagrant/machine_state'
21
+
22
+ require 'vagrant-lxd/driver'
23
+ require 'vagrant-lxd/version'
24
+
25
+ module VagrantLXD
26
+ class Command < Vagrant.plugin('2', :command)
27
+ def Command.synopsis
28
+ 'manages the LXD provider'
29
+ end
30
+
31
+ def execute
32
+ main, subcommand, args = split_main_and_subcommand(@argv)
33
+
34
+ opts = OptionParser.new do |o|
35
+ o.banner = 'Usage: vagrant lxd <command>'
36
+ o.separator ''
37
+ o.separator 'Commands:'
38
+ o.separator ' attach associate machine with a running container'
39
+ o.separator ' detach disassociate machine from a running container'
40
+ o.separator ' version print current plugin version'
41
+ o.separator ''
42
+ o.separator 'For help on a specific command, run `vagrant lxd <command> -h`'
43
+ end
44
+
45
+ if main.include?('-h') or main.include?('--help')
46
+ @env.ui.info opts.help
47
+ exit 0
48
+ end
49
+
50
+ case subcommand
51
+ when 'attach'
52
+ attach(args)
53
+ when 'detach'
54
+ detach(args)
55
+ when 'version'
56
+ @env.ui.info 'Vagrant LXD Provider'
57
+ @env.ui.info 'Version ' << Version::VERSION
58
+ else
59
+ fail Vagrant::Errors::CLIInvalidUsage, help: opts.help
60
+ end
61
+ end
62
+
63
+ def attach(args)
64
+ opts = OptionParser.new do |o|
65
+ o.banner = 'Usage: vagrant lxd attach [machine ...] <container>'
66
+ o.separator ''
67
+ o.separator 'Associates a VM with a preexisting LXD container.'
68
+ o.separator ''
69
+ o.separator 'This command can be used to attach an inactive (not created) VM to a'
70
+ o.separator 'preexisting LXD container. Once it has been associated with a container,'
71
+ o.separator 'the machine can be used just like it had been created with `vagrant up`'
72
+ o.separator 'or detached from the container again with `vagrant lxd detach`.'
73
+ end
74
+
75
+ if args.include?('-h') or args.include?('--help')
76
+ @env.ui.info opts.help
77
+ exit 0
78
+ end
79
+
80
+ unless container = args.pop
81
+ fail Vagrant::Errors::CLIInvalidUsage, help: opts.help
82
+ end
83
+
84
+ with_target_machines(args) do |machine|
85
+ if machine.id == container
86
+ machine.ui.warn "Machine is already attached to container '#{container}', skipping..."
87
+ elsif machine.state.id == Vagrant::MachineState::NOT_CREATED_ID
88
+ machine.ui.info "Attaching to container '#{container}'..."
89
+ Driver.new(machine).attach(container)
90
+ else
91
+ machine.ui.error "Machine is already attached to container '#{machine.id}'"
92
+ fail Driver::DuplicateAttachmentFailure, machine_name: machine.name, container: container
93
+ end
94
+ end
95
+ end
96
+
97
+ def detach(args)
98
+ opts = OptionParser.new do |o|
99
+ o.banner = 'Usage: vagrant lxd detach [machine ...]'
100
+ o.separator ''
101
+ o.separator 'Disassociates a VM from its LXD container.'
102
+ o.separator ''
103
+ o.separator 'This command can be used to deactivate a VM without destroying the'
104
+ o.separator 'underlying container. Once detached, the machine can be recreated'
105
+ o.separator 'from scratch with `vagrant up` or associated to a different container'
106
+ o.separator 'by using `vagrant lxd attach`.'
107
+ end
108
+
109
+ if args.include?('-h') or args.include?('--help')
110
+ @env.ui.info opts.help
111
+ exit 0
112
+ end
113
+
114
+ with_target_machines(args) do |machine|
115
+ if machine.id.nil? or machine.state.id == Vagrant::MachineState::NOT_CREATED_ID
116
+ machine.ui.warn "Machine is not attached to a container, skipping..."
117
+ else
118
+ driver = Driver.new(machine)
119
+ machine.ui.info "Detaching from container '#{driver.machine_id}'..."
120
+ driver.detach
121
+ end
122
+ end
123
+ end
124
+
125
+ def with_target_machines(args)
126
+ machines = args.map(&:to_sym)
127
+
128
+ # NOTE We collect all vm names here in order to force Vagrant to
129
+ # load a full local environment, including provider configurations.
130
+ vms = with_target_vms { |_| }.map(&:name)
131
+
132
+ # When no machines are given, act on all of them.
133
+ machines = vms if machines.empty?
134
+
135
+ # Validate machine names.
136
+ unless vms | machines == vms
137
+ fail Vagrant::Errors::MachineNotFound, name: (machines - vms).first
138
+ end
139
+
140
+ machines.each do |name|
141
+ yield @env.machine(name, :lxd)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,92 @@
1
+ #
2
+ # Copyright (c) 2017 Catalyst.net Ltd
3
+ #
4
+ # This file is part of vagrant-lxd.
5
+ #
6
+ # vagrant-lxd is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or (at
9
+ # your option) any later version.
10
+ #
11
+ # vagrant-lxd is distributed in the hope that it will be useful, but
12
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with vagrant-lxd. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+
20
+ require 'uri'
21
+
22
+ module VagrantLXD
23
+ class Config < Vagrant.plugin('2', :config)
24
+ attr_accessor :api_endpoint
25
+ attr_accessor :name
26
+ attr_accessor :ephemeral
27
+ attr_accessor :timeout
28
+
29
+ def initialize
30
+ @name = UNSET_VALUE
31
+ @timeout = UNSET_VALUE
32
+ @ephemeral = UNSET_VALUE
33
+ @api_endpoint = UNSET_VALUE
34
+ end
35
+
36
+ def validate(machine)
37
+ errors = _detected_errors
38
+
39
+ unless [UNSET_VALUE, nil].include? name
40
+ if not name.is_a? String
41
+ errors << "Invalid `name' (value must be a string): #{name.inspect}"
42
+ elsif name.size >= 64
43
+ errors << "Invalid `name' (value must be less than 64 characters): #{name.inspect}"
44
+ elsif name =~ /[^a-zA-Z0-9-]/
45
+ errors << "Invalid `name' (value must contain only letters, numbers, and hyphens): #{name.inspect}"
46
+ end
47
+ end
48
+
49
+ unless timeout == UNSET_VALUE
50
+ if not timeout.is_a? Fixnum
51
+ errors << "Invalid `timeout' (value must be an integer): #{timeout.inspect}"
52
+ elsif timeout < 1
53
+ errors << "Invalid `timeout' (value must be positive): #{timeout.inspect}"
54
+ end
55
+ end
56
+
57
+ unless api_endpoint == UNSET_VALUE
58
+ begin
59
+ URI(api_endpoint).scheme == 'https' or raise URI::InvalidURIError
60
+ rescue URI::InvalidURIError
61
+ errors << "Invalid `api_endpoint' (value must be a valid HTTPS address): #{api_endpoint.inspect}"
62
+ end
63
+ end
64
+
65
+ unless [UNSET_VALUE, true, false].include? ephemeral
66
+ errors << "Invalid `ephemeral' (value must be true or false): #{ephemeral.inspect}"
67
+ end
68
+
69
+ { Version::NAME => errors }
70
+ end
71
+
72
+ def finalize!
73
+ if name == UNSET_VALUE
74
+ @name = nil
75
+ end
76
+
77
+ if ephemeral == UNSET_VALUE
78
+ @ephemeral = false
79
+ end
80
+
81
+ if timeout == UNSET_VALUE
82
+ @timeout = 10
83
+ end
84
+
85
+ if api_endpoint == UNSET_VALUE
86
+ @api_endpoint = URI('https://127.0.0.1:8443')
87
+ else
88
+ @api_endpoint = URI(api_endpoint)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,418 @@
1
+ #
2
+ # Copyright (c) 2017 Catalyst.net Ltd
3
+ #
4
+ # This file is part of vagrant-lxd.
5
+ #
6
+ # vagrant-lxd is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or (at
9
+ # your option) any later version.
10
+ #
11
+ # vagrant-lxd is distributed in the hope that it will be useful, but
12
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with vagrant-lxd. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+
20
+ require 'hyperkit'
21
+ require 'securerandom'
22
+ require 'tempfile'
23
+ require 'timeout'
24
+ require 'vagrant/machine_state'
25
+
26
+ module VagrantLXD
27
+ class Driver
28
+ include Vagrant::Util
29
+
30
+ VAGRANT_UID = 1000 # TODO Make this configurable.
31
+
32
+ class OperationTimeout < Vagrant::Errors::VagrantError
33
+ error_key 'lxd_operation_timeout'
34
+ end
35
+
36
+ class NetworkAddressAcquisitionTimeout < OperationTimeout
37
+ error_key 'lxd_network_address_acquisition_timeout'
38
+ end
39
+
40
+ class ConnectionFailure < Vagrant::Errors::ProviderNotUsable
41
+ error_key 'lxd_connection_failure'
42
+ end
43
+
44
+ class AuthenticationFailure < Vagrant::Errors::ProviderNotUsable
45
+ error_key 'lxd_authentication_failure'
46
+ end
47
+
48
+ class ContainerCreationFailure < Vagrant::Errors::VagrantError
49
+ error_key 'lxd_container_creation_failure'
50
+ end
51
+
52
+ class ImageCreationFailure < ContainerCreationFailure
53
+ error_key 'lxd_image_creation_failure'
54
+ end
55
+
56
+ class ContainerNotFound < Vagrant::Errors::VagrantError
57
+ error_key 'lxd_container_not_found'
58
+ end
59
+
60
+ class ContainerAlreadyExists < Vagrant::Errors::VagrantError
61
+ error_key 'lxd_container_already_exists'
62
+ end
63
+
64
+ class DuplicateAttachmentFailure < Vagrant::Errors::VagrantError
65
+ error_key 'lxd_duplicate_attachment_failure'
66
+ end
67
+
68
+ class Hyperkit::BadRequest
69
+ def reason
70
+ return unless data.is_a? Hash
71
+
72
+ if error = data[:error]
73
+ return error unless error.empty?
74
+ end
75
+
76
+ if metadata = data[:metadata] and err = metadata[:err]
77
+ return err unless err.empty?
78
+ end
79
+
80
+ 'No reason could be determined'
81
+ end
82
+ end
83
+
84
+ NOT_CREATED = Vagrant::MachineState::NOT_CREATED_ID
85
+
86
+ attr_reader :api_endpoint
87
+ attr_reader :ephemeral
88
+ attr_reader :name
89
+ attr_reader :timeout
90
+
91
+ def initialize(machine)
92
+ @machine = machine
93
+ @timeout = machine.provider_config.timeout
94
+ @api_endpoint = machine.provider_config.api_endpoint
95
+ @ephemeral = machine.provider_config.ephemeral
96
+ @name = machine.provider_config.name
97
+ @logger = Log4r::Logger.new('vagrant::lxd')
98
+ @lxd = Hyperkit::Client.new(api_endpoint: api_endpoint.to_s, verify_ssl: false)
99
+ end
100
+
101
+ def validate!
102
+ raise error(ConnectionFailure) unless connection_usable?
103
+ raise error(AuthenticationFailure) unless authentication_usable?
104
+ end
105
+
106
+ def synced_folders_usable?
107
+ # Check whether we've registered an idmap for the current user.
108
+ if map = container[:config][:'raw.idmap']
109
+ true if map =~ /^uid #{Process.uid} #{VAGRANT_UID}$/
110
+ end
111
+ rescue Vagrant::Errors::ProviderNotUsable
112
+ false
113
+ end
114
+
115
+ def mount(name, options)
116
+ container = @lxd.container(machine_id)
117
+ devices = container[:devices].to_hash
118
+ devices[name] = { type: 'disk', path: options[:guestpath], source: options[:hostpath] }
119
+ container[:devices] = devices
120
+ @lxd.update_container(machine_id, container)
121
+ end
122
+
123
+ def mounted?(name, options)
124
+ container = @lxd.container(machine_id)
125
+ devices = container[:devices].to_hash
126
+ name = name.to_sym
127
+ begin
128
+ devices[name] and
129
+ devices[name][:type] == 'disk' and
130
+ devices[name][:path] == options[:guestpath] and
131
+ devices[name][:source] == options[:hostpath]
132
+ end
133
+ end
134
+
135
+ def unmount(name, options)
136
+ container = @lxd.container(machine_id)
137
+ devices = container[:devices].to_hash
138
+ devices.delete(name.to_sym)
139
+ container[:devices] = devices
140
+ @lxd.update_container(machine_id, container)
141
+ end
142
+
143
+ def attach(container)
144
+ @lxd.container(container) # Query LXD to make sure the container exists.
145
+
146
+ if in_state? NOT_CREATED
147
+ @machine.id = container
148
+ else
149
+ fail DuplicateAttachmentFailure, machine_name: @machine.name, container: container
150
+ end
151
+ rescue Hyperkit::NotFound
152
+ @machine.ui.error "Container doesn't exist: #{container}"
153
+ fail ContainerNotFound, container: container
154
+ end
155
+
156
+ def detach
157
+ @machine.id = nil
158
+ end
159
+
160
+ #
161
+ # The following methods correspond directly to middleware actions.
162
+ #
163
+
164
+ def snapshot_list
165
+ @lxd.snapshots(machine_id)
166
+ end
167
+
168
+ def snapshot_save(name)
169
+ @lxd.create_snapshot(machine_id, name)
170
+ end
171
+
172
+ def snapshot_restore(name)
173
+ @lxd.restore_snapshot(machine_id, name)
174
+ end
175
+
176
+ def snapshot_delete(name)
177
+ @lxd.delete_snapshot(machine_id, name)
178
+ end
179
+
180
+ def state
181
+ return NOT_CREATED if machine_id.nil?
182
+ container_state = @lxd.container_state(machine_id)
183
+ container_state[:status].downcase.to_sym
184
+ rescue Hyperkit::NotFound
185
+ NOT_CREATED
186
+ end
187
+
188
+ def create
189
+ if in_state? NOT_CREATED
190
+ machine_id = generate_machine_id
191
+ file, fingerprint = prepare_image_file
192
+
193
+ begin
194
+ image = @lxd.image(fingerprint)
195
+ @logger.debug 'Using image: ' << image.inspect
196
+ rescue Hyperkit::NotFound
197
+ image = @lxd.create_image_from_file(file)
198
+ image_alias = @lxd.create_image_alias(fingerprint, machine_id)
199
+ @logger.debug 'Created image: ' << image.inspect
200
+ @logger.debug 'Created image alias: ' << image_alias.inspect
201
+ end
202
+
203
+ container = @lxd.create_container(machine_id, ephemeral: ephemeral, fingerprint: fingerprint, config: config)
204
+ @logger.debug 'Created container: ' << container.inspect
205
+
206
+ @machine.id = machine_id
207
+ end
208
+ rescue Hyperkit::Error => e
209
+ @lxd.delete_container(id) rescue nil unless container.nil?
210
+ @lxd.delete_image(image[:metadata][:fingerprint]) rescue nil unless image.nil?
211
+ if e.reason =~ /Container '([^']+)' already exists/
212
+ @machine.ui.error e.reason
213
+ fail ContainerAlreadyExists, machine_name: @machine.name, container: $1
214
+ else
215
+ @machine.ui.error "Failed to create container"
216
+ fail ContainerCreationFailure, machine_name: @machine.name, reason: e.reason
217
+ end
218
+ end
219
+
220
+ def resume
221
+ case state
222
+ when :stopped
223
+ @lxd.start_container(machine_id)
224
+ when :frozen
225
+ @lxd.unfreeze_container(machine_id, timeout: timeout)
226
+ end
227
+ rescue Hyperkit::BadRequest
228
+ @machine.ui.warn "Container failed to start within #{timeout} seconds"
229
+ fail OperationTimeout, time_limit: timeout, operation: 'start', machine_id: machine_id
230
+ end
231
+
232
+ def halt
233
+ if in_state? :running, :frozen
234
+ @lxd.stop_container(machine_id, timeout: timeout)
235
+ end
236
+ rescue Hyperkit::BadRequest
237
+ @machine.ui.warn "Container failed to stop within #{timeout} seconds, forcing shutdown..."
238
+ @lxd.stop_container(machine_id, timeout: timeout, force: true)
239
+ end
240
+
241
+ def suspend
242
+ if in_state? :running
243
+ @lxd.freeze_container(machine_id, timeout: timeout)
244
+ end
245
+ rescue Hyperkit::BadRequest
246
+ @machine.ui.warn "Container failed to suspend within #{timeout} seconds"
247
+ fail OperationTimeout, time_limit: timeout, operation: 'info', machine_id: machine_id
248
+ end
249
+
250
+ def destroy
251
+ if in_state? :stopped
252
+ delete_image
253
+ delete_container
254
+ else
255
+ @logger.debug "Skipped container destroy (#{machine_id} is not stopped)"
256
+ end
257
+ end
258
+
259
+ def info
260
+ if in_state? :running, :frozen
261
+ {
262
+ host: ipv4_address,
263
+ port: ipv4_port,
264
+ }
265
+ end
266
+ end
267
+
268
+ private
269
+
270
+ def delete_container
271
+ @lxd.delete_container(machine_id)
272
+ rescue Hyperkit::NotFound
273
+ @logger.warn "Container '#{machine_id}' not found, unable to destroy"
274
+ end
275
+
276
+ def delete_image
277
+ @lxd.delete_image(container[:config][:'volatile.base_image'])
278
+ rescue Hyperkit::NotFound
279
+ @logger.warn "Image for '#{machine_id}' not found, unable to destroy"
280
+ rescue Hyperkit::BadRequest
281
+ @logger.error "Unable to delete image for '#{machine_id}'"
282
+ end
283
+
284
+ def container
285
+ @lxd.container(machine_id)
286
+ end
287
+
288
+ def connection_usable?
289
+ @lxd.images
290
+ rescue Faraday::ConnectionFailed
291
+ false
292
+ else
293
+ true
294
+ end
295
+
296
+ def authentication_usable?
297
+ connection_usable? and @lxd.containers
298
+ rescue Hyperkit::Forbidden
299
+ false
300
+ else
301
+ true
302
+ end
303
+
304
+ def machine_id
305
+ @machine.id
306
+ end
307
+
308
+ def generate_machine_id
309
+ @name || begin
310
+ id = "vagrant-#{File.basename(Dir.pwd)}-#{@machine.name}-#{SecureRandom.hex(8)}"
311
+ id = id.slice(0...63).gsub(/[^a-zA-Z0-9]/, '-')
312
+ id
313
+ end
314
+ end
315
+
316
+ def in_state?(*any)
317
+ any.include?(state)
318
+ end
319
+
320
+ def ipv4_port
321
+ 22
322
+ end
323
+
324
+ def ipv4_address
325
+ @logger.debug "Looking up ipv4 address for #{machine_id}..."
326
+ Timeout.timeout(timeout) do
327
+ loop do
328
+ container_state = @lxd.container_state(machine_id)
329
+ if address = container_state[:network][:eth0][:addresses].find { |a| a[:family] == 'inet' }
330
+ return address[:address]
331
+ else
332
+ @logger.debug 'No ipv4 address found, sleeping 1s before trying again...'
333
+ sleep(1)
334
+ end
335
+ end
336
+ end
337
+ rescue Timeout::Error
338
+ @logger.warn "Failed to find ipv4 address for #{machine_id} within #{timeout} seconds!"
339
+ fail NetworkAddressAcquisitionTimeout, time_limit: timeout, lxd_bridge: 'lxdbr0' # FIXME Hardcoded bridge name
340
+ end
341
+
342
+ def config
343
+ config = {}
344
+
345
+ # Set "raw.idmap" if the host's sub{u,g}id configuration allows it.
346
+ # This allows sharing folders via LXD (see synced_folder.rb).
347
+ begin
348
+ %w(uid gid).each do |type|
349
+ value = Process.send(type)
350
+ if File.readlines("/etc/sub#{type}").grep(/^root:#{value}:[1-9]/).any?
351
+ config[:'raw.idmap'] ||= ''
352
+ config[:'raw.idmap'] << "#{type} #{value} #{VAGRANT_UID}\n"
353
+ end
354
+ end
355
+ rescue StandardError => e
356
+ @logger.warn "Cannot read subordinate permissions file: #{e.message}"
357
+ end
358
+
359
+ @logger.debug 'Resulting configuration: ' << config.inspect
360
+
361
+ config
362
+ end
363
+
364
+ def prepare_image_file
365
+ tmpdir = Dir.mktmpdir
366
+
367
+ lxc_dir = @machine.box.directory
368
+ lxc_rootfs = lxc_dir / 'rootfs.tar.gz'
369
+ lxc_fingerprint = Digest::SHA256.file(lxc_rootfs).hexdigest
370
+
371
+ lxd_dir = @machine.box.directory / '..' / 'lxd'
372
+ lxd_rootfs = lxd_dir / 'rootfs.tar.gz'
373
+ lxd_metadata = YAML.load(File.read(lxd_dir / 'metadata.yaml')) rescue nil
374
+
375
+ if lxd_rootfs.exist? and lxd_metadata.is_a? Hash and lxd_metadata['source_fingerprint'] == lxc_fingerprint
376
+ @machine.ui.info 'Importing LXC image...'
377
+ else
378
+ @machine.ui.info 'Converting LXC image to LXD format...'
379
+
380
+ SafeChdir.safe_chdir(tmpdir) do
381
+ FileUtils.cp(lxc_rootfs, tmpdir)
382
+
383
+ File.open('metadata.yaml', 'w') do |metadata|
384
+ metadata.puts 'architecture: ' << `uname -m`.strip
385
+ metadata.puts 'creation_date: ' << Time.now.strftime('%s')
386
+ metadata.puts 'source_fingerprint: ' << lxc_fingerprint
387
+ end
388
+
389
+ Subprocess.execute('gunzip', 'rootfs.tar.gz')
390
+ Subprocess.execute('tar', '-rf', 'rootfs.tar', 'metadata.yaml')
391
+ Subprocess.execute('gzip', 'rootfs.tar')
392
+
393
+ FileUtils.mkdir_p(lxd_dir)
394
+ FileUtils.mv('rootfs.tar.gz', lxd_dir)
395
+ FileUtils.mv('metadata.yaml', lxd_dir)
396
+ end
397
+ end
398
+
399
+ return lxd_rootfs, Digest::SHA256.file(lxd_rootfs).hexdigest
400
+ rescue Exception => e
401
+ @machine.ui.error 'Failed to create LXD image for container'
402
+ @logger.error 'Error preparing LXD image: ' << e.message << "\n" << e.backtrace.join("\n")
403
+ fail ImageCreationFailure, machine_name: @machine.name, error_message: e.message
404
+ ensure
405
+ FileUtils.rm_rf(tmpdir)
406
+ end
407
+
408
+ def error(klass)
409
+ klass.new(
410
+ provider: Version::NAME,
411
+ machine: @machine.name,
412
+ api_endpoint: @api_endpoint.to_s,
413
+ https_address: @api_endpoint.host,
414
+ client_cert: File.expand_path('.config/lxc/client.crt', ENV['HOME']),
415
+ )
416
+ end
417
+ end
418
+ end