lxdev 0.1.1 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2120a1f663ad212a874571fd8bf7e8a9c786de30
4
- data.tar.gz: dc6fd44130b3fa99542d48e181222ee4706b80d1
3
+ metadata.gz: 0b31393a23f8385606e4eac0f3bce19b0d9c8d24
4
+ data.tar.gz: 36d736278c01df9698f937504d759ae0c8605773
5
5
  SHA512:
6
- metadata.gz: 1b0bfc74622c66e21f19b339ea2d3f6274ae674dfffd28acaa5156634d57097c5ea2b95da84ffab3e2620183498e8cbb844fe89ef9c4a39d702a992907304d6a
7
- data.tar.gz: 87079f38bd82eecc97c2ca3c40e28639fc4447c6556d87b57b9e619c81efaebb34523315ecf954eba8039e8b4a5b92dbecf470756256006a4927a552a806834c
6
+ metadata.gz: e36ccf0712ada1a0c1ac016405607d60ed1c8674a20425b4513a0ea07f46c46673531d51aeb5b2b5885f469fbb09ab2610795c37cf68b83d79820823aeeb176a
7
+ data.tar.gz: d364c34c12388a837b5700f8e6eb7f3947ffb714a0d6b1021114b08b33220078ce61d1aabf45565a42a0f41ad4f7caecd0b3c00da14e9891e4f99c1003d840e6
data/bin/lxdev CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- require 'lxdev'
2
+ require 'lxdev/main'
3
3
  require 'optparse'
4
4
 
5
5
  def option_parser
@@ -12,7 +12,7 @@ def option_parser
12
12
  end
13
13
 
14
14
  opts.on("-v", "--version", "Prints the version") do
15
- puts "Version #{LxDev::VERSION}"
15
+ puts "Version #{LxDev::Main::VERSION}"
16
16
  exit
17
17
  end
18
18
 
@@ -27,6 +27,10 @@ halt Stop the container
27
27
  destroy Destroy the container
28
28
  provision Run provisioning commands from YAML file
29
29
  exec Run command as root in container
30
+ snapshot Snapshots the container. Takes a snapshot name as parameter.
31
+ rmsnapshot Deletes a snapshot. Takes a snapshot name as parameter.
32
+ restore Restore container to a snapshot with a previous state. Takes a snapshot name as parameter.
33
+ revert Restore container to the last snapshot taken.
30
34
  sudoers Create a sudoers file, allowing you to use lxdev without entering the sudo password
31
35
  EOS
32
36
  opt_parser.parse!()
@@ -55,13 +59,36 @@ def execute_main_command(lxdev)
55
59
  lxdev.provision()
56
60
  when 'exec'
57
61
  command = ARGV[1..-1].join(" ")
58
- if LxDev::SHELLS.include?(command)
62
+ if LxDev::Main::SHELLS.include?(command)
59
63
  lxdev.execute(command, interactive: true)
60
64
  else
61
65
  lxdev.execute(command)
62
66
  end
63
67
  when 'sudoers'
64
- LxDev.create_sudoers_file()
68
+ LxDev::Main.create_sudoers_file()
69
+ when 'snapshot'
70
+ snapshot_name = ARGV[1]
71
+ if snapshot_name.nil?
72
+ puts "Needs a snapshot name!"
73
+ exit 1
74
+ end
75
+ lxdev.snapshot(snapshot_name)
76
+ when 'restore'
77
+ snapshot_name = ARGV[1]
78
+ if snapshot_name.nil?
79
+ puts "Needs a snapshot name!"
80
+ exit 1
81
+ end
82
+ lxdev.restore(snapshot_name)
83
+ when 'revert'
84
+ lxdev.revert()
85
+ when 'rmsnapshot'
86
+ snapshot_name = ARGV[1]
87
+ if snapshot_name.nil?
88
+ puts "Needs a snapshot name!"
89
+ exit 1
90
+ end
91
+ lxdev.rmsnapshot(snapshot_name)
65
92
  else
66
93
  puts "Unknown command.\nRun \"lxdev --help\" for info"
67
94
  end
@@ -70,7 +97,7 @@ end
70
97
 
71
98
 
72
99
  option_parser
73
- lxdev = LxDev.setup()
100
+ lxdev = LxDev::Main.setup()
74
101
  if lxdev
75
102
  execute_main_command(lxdev)
76
103
  lxdev.save_state
data/lib/lxdev/main.rb ADDED
@@ -0,0 +1,371 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'terminal-table'
4
+ require 'lxdev/system'
5
+
6
+ module LxDev
7
+ class Main
8
+ WHITELISTED_SUDO_COMMANDS = ["lxc", "redir", "kill"]
9
+ SHELLS = ["bash", "zsh", "sh", "csh", "tcsh", "ash"]
10
+ BOOT_TIMEOUT = 30
11
+ VERSION = '0.1.2'
12
+
13
+ def initialize
14
+ @uid = System.exec("id -u").output.chomp
15
+ @gid = System.exec("id -g").output.chomp
16
+ @config = YAML.load_file('lxdev.yml')
17
+ @name = @config['box']['name']
18
+ @image = @config['box']['image']
19
+ @user = @config['box']['user']
20
+ @ports = @config['box']['ports'] || {}
21
+ Dir.mkdir('.lxdev') unless File.directory?('.lxdev')
22
+ begin
23
+ @state = YAML.load_file('.lxdev/state')
24
+ rescue
25
+ @state = Hash.new
26
+ end
27
+ rescue Errno::ENOENT
28
+ puts "lxdev.yml not found"
29
+ exit 1
30
+ end
31
+
32
+ def self.setup
33
+ unless lxd_initialized?
34
+ puts "Please run 'lxd init' and configure LXD first"
35
+ return false
36
+ end
37
+ lxdev = Main.new
38
+ unless lxdev.set_ssh_keys
39
+ puts "No ssh keys detected. Make sure you have an ssh key, a running agent, and the key added to the agent, e.g. with ssh-add."
40
+ return false
41
+ end
42
+ return lxdev
43
+ end
44
+
45
+ def save_state
46
+ File.open('.lxdev/state', 'w') {|f| f.write @state.to_yaml} unless @state.empty?
47
+ end
48
+
49
+ def set_ssh_keys
50
+ ssh_keys = System.exec("ssh-add -L").output
51
+ if ssh_keys[0..3] == 'ssh-'
52
+ @ssh_keys = ssh_keys
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ def status
59
+ ensure_container_created
60
+
61
+ container_status = get_container_status
62
+ folders = container_status.first['devices'].map {|name, folders| [name, "#{folders['source']} => #{folders['path']}"] if folders['source']}.compact
63
+ table = Terminal::Table.new do |t|
64
+ t.add_row ['Name', container_status.first['name']]
65
+ t.add_row ['Status', container_status.first['status']]
66
+ t.add_row ['IP', get_container_ip]
67
+ t.add_row ['Image', @image]
68
+ t.add_separator
69
+ folders.each do |folder|
70
+ t.add_row folder
71
+ end
72
+ t.add_separator
73
+ @ports.each do |guest, host|
74
+ t.add_row ['Forwarded port', "guest: #{guest} host: #{host}"]
75
+ end
76
+ if container_status.first['snapshots'].any?
77
+ t.add_separator
78
+ t.add_row ['Snapshots', '']
79
+ end
80
+ container_status.first['snapshots'].each do |snapshot|
81
+ t.add_row [snapshot['name'].partition('/').last, snapshot['created_at']]
82
+ end
83
+ end
84
+ puts table
85
+ end
86
+
87
+ def up
88
+ do_provision = false
89
+ unless @state.empty?
90
+ puts "Container state .lxdev/state exists, is it running? If not it might have stopped unexpectedly. Please remove the file before starting."
91
+ exit 1
92
+ end
93
+ if get_container_status.empty?
94
+ create_container
95
+ do_provision = true
96
+ else
97
+ if get_container_status.first['status'] == 'Running'
98
+ puts "#{@name} is already running!"
99
+ exit 1
100
+ else
101
+ start_container
102
+ end
103
+ end
104
+ puts "Waiting for boot..."
105
+ wait_for_boot
106
+ @state['status'] = 'running'
107
+ puts "Forwarding ports..."
108
+ forward_ports(@ports)
109
+ provision if do_provision
110
+ end
111
+
112
+ def halt
113
+ ensure_container_created
114
+ System.exec("sudo lxc stop #{@name}")
115
+ cleanup_forwarded_ports
116
+ remove_state
117
+ end
118
+
119
+ def destroy
120
+ ensure_container_created
121
+ System.exec("sudo lxc delete #{@name}")
122
+ end
123
+
124
+ def ssh(args)
125
+ ensure_container_created
126
+ host = get_container_ip
127
+ if host.nil?
128
+ puts "#{@name} doesn't seem to be running."
129
+ exit 1
130
+ end
131
+ ssh_command = "ssh -o StrictHostKeyChecking=no -t #{@user}@#{get_container_ip} #{args.empty? ? '' : "'#{args.join(' ')}'"}"
132
+ exec ssh_command
133
+ end
134
+
135
+ def execute(command, interactive: false)
136
+ if interactive
137
+ exec("sudo lxc exec #{@name} #{command}") # execution stops here and gives control to exec
138
+ end
139
+ IO.popen("sudo lxc exec #{@name} -- /bin/sh -c '#{command}'", err: [:child, :out]) do |cmd_output|
140
+ cmd_output.each do |line|
141
+ puts line
142
+ end
143
+ end
144
+ end
145
+
146
+ def provision
147
+ ensure_container_created
148
+ if get_container_status.first['status'] != 'Running'
149
+ puts "#{@name} is not running!"
150
+ exit 1
151
+ end
152
+ provisioning = @config['box']['provisioning']
153
+ if provisioning.nil?
154
+ puts "Nothing to do"
155
+ return
156
+ end
157
+ if @config['box']['auto_snapshots']
158
+ snapshot_name = "provision_#{Time.now.to_i}"
159
+ snapshot(snapshot_name)
160
+ end
161
+ puts "Provisioning #{@name}..."
162
+ STDOUT.sync = true
163
+ provisioning.each do |cmd|
164
+ execute cmd
165
+ end
166
+ STDOUT.sync = false
167
+ end
168
+
169
+ def snapshot(snapshot_name)
170
+ puts "Creating snapshot #{snapshot_name}"
171
+ System.exec("sudo lxc snapshot #{@name} #{snapshot_name}")
172
+ end
173
+
174
+ def restore(snapshot_name)
175
+ puts "Restoring snapshot #{snapshot_name}"
176
+ exitstatus = System.exec("sudo lxc restore #{@name} #{snapshot_name}").exitstatus
177
+ exitstatus == 0
178
+ end
179
+
180
+ def rmsnapshot(snapshot_name)
181
+ puts "Deleting snapshot #{snapshot_name}"
182
+ exitstatus = System.exec("sudo lxc delete #{@name}/#{snapshot_name}").exitstatus
183
+ exitstatus == 0
184
+ end
185
+
186
+ def revert
187
+ snapshot = get_container_status.first['snapshots'].last
188
+ snapshot_name = snapshot['name'].partition('/').last
189
+ if restore(snapshot_name)
190
+ puts "Reverted to snapshot #{snapshot_name}"
191
+ puts "Deleting snapshot"
192
+ rmsnapshot(snapshot_name)
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ def self.lxd_initialized?
199
+ exitstatus = System.exec("sudo lxc info | grep 'lxd init'").exitstatus
200
+ exitstatus != 0
201
+ end
202
+
203
+ def ensure_container_created
204
+ container_status = get_container_status
205
+ unless container_status.size > 0
206
+ puts "Container not created yet. Run lxdev up"
207
+ exit(0)
208
+ end
209
+ end
210
+
211
+ def remove_state
212
+ File.delete('.lxdev/state') if File.exists?('.lxdev/state')
213
+ @state = {}
214
+ end
215
+
216
+ def create_container
217
+ add_subuid_and_subgid
218
+ puts "Launching #{@name}..."
219
+ System.exec("sudo lxc init #{@image} #{@name}")
220
+ System.exec(%{printf "uid #{@uid} 1001\ngid #{@gid} 1001"| sudo lxc config set #{@name} raw.idmap -})
221
+ System.exec("sudo lxc start #{@name}")
222
+ puts "Creating user #{@user}..."
223
+ create_container_user(@user)
224
+ puts "Mapping folders.."
225
+ map_folders(@config['box']['folders'])
226
+ end
227
+
228
+ def start_container
229
+ puts "Starting #{@name}..."
230
+ System.exec("sudo lxc start #{@name}")
231
+ end
232
+
233
+ def get_container_status
234
+ return @status unless @status.nil?
235
+ command_result = System.exec("sudo lxc list #{@name} --format=json")
236
+ @status = JSON.parse(command_result.output)
237
+ end
238
+
239
+ def get_container_ip
240
+ get_container_status.first['state']['network']['eth0']['addresses'].select {|addr| addr['family'] == 'inet'}.first['address']
241
+ rescue
242
+ nil
243
+ end
244
+
245
+ def add_subuid_and_subgid
246
+ need_restart = false
247
+ if System.exec("grep -q 'root:#{@uid}:1' /etc/subuid").exitstatus != 0
248
+ System.exec("echo 'root:#{@uid}:1' | sudo tee -a /etc/subuid")
249
+ need_restart = true
250
+ end
251
+ if System.exec("grep -q 'root:#{@gid}:1' /etc/subgid").exitstatus != 0
252
+ System.exec("echo 'root:#{@gid}:1' | sudo tee -a /etc/subgid")
253
+ need_restart = true
254
+ end
255
+ if need_restart
256
+ begin
257
+ System.exec("sudo systemctl restart #{lxd_service_name}")
258
+ rescue
259
+ puts "The LXD service needs to be restarted, but the service name cannot be detected. Please restart it manually."
260
+ end
261
+ end
262
+ end
263
+
264
+ def create_container_user(user)
265
+ System.exec("sudo lxc exec #{@name} -- groupadd --gid 1001 #{user}")
266
+ System.exec("sudo lxc exec #{@name} -- useradd --uid 1001 --gid 1001 -s /bin/bash -m #{user}")
267
+ System.exec("sudo lxc exec #{@name} -- mkdir /home/#{user}/.ssh")
268
+ System.exec("sudo lxc exec #{@name} -- chmod 0700 /home/#{user}/.ssh")
269
+ System.exec("printf '#{@ssh_keys}' | sudo lxc exec #{@name} tee /home/#{user}/.ssh/authorized_keys")
270
+ System.exec("sudo lxc exec #{@name} -- chown -R #{user} /home/#{user}/.ssh")
271
+ System.exec("sudo lxc exec #{@name} -- touch /home/#{@user}/.hushlogin")
272
+ System.exec("sudo lxc exec #{@name} -- chown #{user} /home/#{user}/.hushlogin")
273
+ System.exec(%{printf "#{user} ALL=(ALL) NOPASSWD: ALL\n" | sudo lxc exec #{@name} -- tee -a /etc/sudoers})
274
+ System.exec("sudo lxc exec #{@name} -- chmod 0440 /etc/sudoers")
275
+ end
276
+
277
+ def wait_for_boot
278
+ BOOT_TIMEOUT.times do |t|
279
+ @status = nil # reset status for each iteration to refresh IP
280
+ break if get_container_ip
281
+ abort_boot if t == (BOOT_TIMEOUT - 1)
282
+ sleep 1
283
+ end
284
+ end
285
+
286
+ def forward_ports(ports)
287
+ redir_pids = []
288
+ ports.each do |guest, host|
289
+ puts "Forwarding #{get_container_ip}:#{guest} to local port #{host}"
290
+ pid = System.spawn_exec("sudo redir --caddr=#{get_container_ip} --cport=#{guest} --lport=#{host}")
291
+ redir_pids << pid
292
+ Process.detach(pid)
293
+ end
294
+ @state['redir_pids'] = redir_pids
295
+ end
296
+
297
+ def cleanup_forwarded_ports
298
+ if @state.empty?
299
+ return
300
+ end
301
+ @state['redir_pids'].each do |pid|
302
+ System.exec("sudo kill #{pid}")
303
+ end
304
+ end
305
+
306
+ def map_folders(folders)
307
+ counter = 0
308
+ folders.each do |host, guest|
309
+ counter = counter + 1
310
+ puts "Mounting #{host} in #{guest}"
311
+ absolute_path = System.exec("readlink -f #{host}").output.chomp
312
+ System.exec("sudo lxc config device add #{@name} shared_folder_#{counter} disk source=#{absolute_path} path=#{guest}")
313
+ end
314
+ end
315
+
316
+ def get_snapshots
317
+ snapshots = []
318
+ get_container_status.first['snapshots'].each do |snapshot|
319
+ result = {}
320
+ result['name'] = snapshot['name']
321
+ result['date'] = snapshot['created_at']
322
+ snapshots << result
323
+ end
324
+ snapshots
325
+ end
326
+
327
+ def abort_boot
328
+ puts "Timeout waiting for container to boot"
329
+ exit 1
330
+ end
331
+
332
+ def lxd_service_name
333
+ if System.exec("sudo systemctl status lxd.service").exitstatus == 0
334
+ 'lxd.service'
335
+ elsif System.exec("sudo systemctl status snap.lxd.daemon.service").exitstatus == 0
336
+ 'snap.lxd.daemon.service'
337
+ else
338
+ raise 'There seems to be no LXD service on the system!'
339
+ end
340
+ end
341
+
342
+ def self.create_sudoers_file
343
+ user = System.exec("whoami").output.chomp
344
+ puts <<-EOS
345
+ !! WARNING !!
346
+ This will create a file, /etc/sudoers.d/lxdev,
347
+ which will give your user #{user} access to running
348
+ the following commands :
349
+ #{WHITELISTED_SUDO_COMMANDS.join(" ")}
350
+ with superuser privileges. If you do not know what you're
351
+ doing, this can be dangerous and insecure.
352
+
353
+ If you want to do this, type 'yesplease'
354
+ EOS
355
+ action = STDIN.gets.chomp
356
+ unless action == 'yesplease'
357
+ puts "Not creating sudoers file"
358
+ return
359
+ end
360
+ content = []
361
+ content << "# Created by lxdev #{Time.now}"
362
+ WHITELISTED_SUDO_COMMANDS.each do |cmd|
363
+ cmd_with_path = System.exec("which #{cmd}").output.chomp
364
+ content << "#{user} ALL=(root) NOPASSWD: #{cmd_with_path}"
365
+ end
366
+ System.exec(%{printf '#{content.join("\n")}\n' | sudo tee /etc/sudoers.d/lxdev})
367
+ System.exec("sudo chmod 0440 /etc/sudoers.d/lxdev")
368
+ puts "Created sudoers file."
369
+ end
370
+ end
371
+ end
@@ -0,0 +1,19 @@
1
+ module LxDev
2
+ class System
3
+ class Result
4
+ attr_accessor :output
5
+ attr_accessor :exitstatus
6
+ end
7
+
8
+ def self.exec(cmd)
9
+ return_object = Result.new
10
+ return_object.output = %x{#{cmd}}
11
+ return_object.exitstatus = $?.exitstatus
12
+ return_object
13
+ end
14
+
15
+ def self.spawn_exec(cmd)
16
+ spawn(cmd)
17
+ end
18
+ end
19
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lxdev
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Lønaas
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-06-22 00:00:00.000000000 Z
12
+ date: 2018-08-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
@@ -47,7 +47,8 @@ extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
49
  - bin/lxdev
50
- - lib/lxdev.rb
50
+ - lib/lxdev/main.rb
51
+ - lib/lxdev/system.rb
51
52
  homepage: https://github.com/GyldendalDigital/lxdev
52
53
  licenses:
53
54
  - MIT
@@ -68,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
69
  version: '0'
69
70
  requirements: []
70
71
  rubyforge_project:
71
- rubygems_version: 2.5.1
72
+ rubygems_version: 2.5.2.1
72
73
  signing_key:
73
74
  specification_version: 4
74
75
  summary: Automagic development environment with LXD
data/lib/lxdev.rb DELETED
@@ -1,292 +0,0 @@
1
- require 'yaml'
2
- require 'json'
3
- require 'terminal-table'
4
-
5
- class LxDev
6
- WHITELISTED_SUDO_COMMANDS = ["lxc", "redir", "kill"]
7
- SHELLS = ["bash", "zsh", "sh", "csh", "tcsh", "ash"]
8
- BOOT_TIMEOUT = 30
9
- VERSION = '0.1.1'
10
-
11
- def initialize
12
- @uid=%x{id -u}.chomp
13
- @gid=%x{id -g}.chomp
14
- @config = YAML.load_file('lxdev.yml')
15
- @name = @config['box']['name']
16
- @image = @config['box']['image']
17
- @user = @config['box']['user']
18
- @ports = @config['box']['ports'] || {}
19
- Dir.mkdir('.lxdev') unless File.directory?('.lxdev')
20
- begin
21
- @state = YAML.load_file('.lxdev/state')
22
- rescue
23
- @state = Hash.new
24
- end
25
- rescue Errno::ENOENT
26
- puts "lxdev.yml not found"
27
- exit 1
28
- end
29
-
30
- def self.setup
31
- unless lxd_initialized?
32
- puts "Please run 'lxd init' and configure LXD first"
33
- return false
34
- end
35
- return LxDev.new
36
- end
37
-
38
- def save_state
39
- File.open('.lxdev/state', 'w') {|f| f.write @state.to_yaml } unless @state.empty?
40
- end
41
-
42
- def status
43
- ensure_container_created
44
-
45
- container_status = get_container_status
46
- folders = container_status.first['devices'].map{|name, folders| [name,"#{folders['source']} => #{folders['path']}"] if folders['source']}.compact
47
- table = Terminal::Table.new do |t|
48
- t.add_row ['Name', container_status.first['name']]
49
- t.add_row ['Status', container_status.first['status']]
50
- t.add_row ['IP', get_container_ip]
51
- t.add_row ['Image', @image]
52
- t.add_separator
53
- folders.each do |folder|
54
- t.add_row folder
55
- end
56
- t.add_separator
57
- @ports.each do |guest,host|
58
- t.add_row ['Forwarded port', "guest: #{guest} host: #{host}"]
59
- end
60
- end
61
- puts table
62
- end
63
-
64
- def up
65
- do_provision = false
66
- unless @state.empty?
67
- puts "Container state .lxdev/state exists, is it running? If not it might have stopped unexpectedly. Please remove the file before starting."
68
- exit 1
69
- end
70
- if get_container_status.empty?
71
- create_container
72
- do_provision = true
73
- else
74
- if get_container_status.first['status'] == 'Running'
75
- puts "#{@name} is already running!"
76
- exit 1
77
- else
78
- start_container
79
- end
80
- end
81
- puts "Waiting for boot..."
82
- wait_for_boot
83
- @state['status'] = 'running'
84
- puts "Forwarding ports..."
85
- forward_ports(@ports)
86
- provision if do_provision
87
- end
88
-
89
- def halt
90
- ensure_container_created
91
- %x{sudo lxc stop #{@name}}
92
- cleanup_forwarded_ports
93
- remove_state
94
- end
95
-
96
- def destroy
97
- ensure_container_created
98
- %x{sudo lxc delete #{@name}}
99
- end
100
-
101
- def ssh(args)
102
- ensure_container_created
103
- host = get_container_ip
104
- if host.nil?
105
- puts "#{@name} doesn't seem to be running."
106
- exit 1
107
- end
108
- ssh_command = "ssh -o StrictHostKeyChecking=no -t #{@user}@#{get_container_ip} #{args.empty? ? '' : "'#{args.join(' ')}'"}"
109
- exec ssh_command
110
- end
111
-
112
- def execute(command, interactive: false)
113
- if interactive
114
- exec("sudo lxc exec #{@name} #{command}") # execution stops here and gives control to exec
115
- end
116
- IO.popen("sudo lxc exec #{@name} -- /bin/sh -c '#{command}'", err: [:child, :out]) do |cmd_output|
117
- cmd_output.each do |line|
118
- puts line
119
- end
120
- end
121
- end
122
-
123
- def provision
124
- ensure_container_created
125
- if get_container_status.first['status'] != 'Running'
126
- puts "#{@name} is not running!"
127
- exit 1
128
- end
129
- provisioning = @config['box']['provisioning']
130
- if provisioning.nil?
131
- puts "Nothing to do"
132
- return
133
- end
134
- puts "Provisioning #{@name}..."
135
- STDOUT.sync = true
136
- provisioning.each do |cmd|
137
- execute cmd
138
- end
139
- STDOUT.sync = false
140
- end
141
-
142
- private
143
- def self.lxd_initialized?
144
- %x{sudo lxc info | grep 'lxd init'}
145
- $?.exitstatus != 0
146
- end
147
-
148
- def ensure_container_created
149
- container_status = get_container_status
150
- unless container_status.size > 0
151
- puts "Container not created yet. Run lxdev up"
152
- exit(0)
153
- end
154
- end
155
-
156
- def remove_state
157
- File.delete('.lxdev/state') if File.exists?('.lxdev/state')
158
- @state = {}
159
- end
160
-
161
- def create_container
162
- add_subuid_and_subgid
163
- puts "Launching #{@name}..."
164
- %x{sudo lxc init #{@image} #{@name}}
165
- %x{printf "uid #{@uid} 1001\ngid #{@gid} 1001"| sudo lxc config set #{@name} raw.idmap -}
166
- %x{sudo lxc start #{@name}}
167
- puts "Creating user #{@user}..."
168
- create_container_user(@user)
169
- puts "Mapping folders.."
170
- map_folders(@config['box']['folders'])
171
- end
172
-
173
- def start_container
174
- puts "Starting #{@name}..."
175
- %x{sudo lxc start #{@name}}
176
- end
177
-
178
- def get_container_status
179
- return @status unless @status.nil?
180
- container_status = %x{sudo lxc list #{@name} --format=json}
181
- @status = JSON.parse(container_status)
182
- end
183
-
184
- def get_container_ip
185
- get_container_status.first['state']['network']['eth0']['addresses'].select{|addr| addr['family'] == 'inet'}.first['address']
186
- rescue
187
- nil
188
- end
189
-
190
- def add_subuid_and_subgid
191
- need_restart = false
192
- %x{grep -q 'root:#{@uid}:1' /etc/subuid}
193
- if $?.exitstatus != 0
194
- %x{echo 'root:#{@uid}:1' | sudo tee -a /etc/subuid}
195
- need_restart = true
196
- end
197
- %x{grep -q 'root:#{@gid}:1' /etc/subgid}
198
- if $?.exitstatus != 0
199
- %x{echo 'root:#{@gid}:1' | sudo tee -a /etc/subgid}
200
- need_restart = true
201
- end
202
- if need_restart
203
- %x{sudo systemctl restart lxd.service}
204
- end
205
- end
206
-
207
- def create_container_user(user)
208
- %x{sudo lxc exec #{@name} -- groupadd --gid 1001 #{user}}
209
- %x{sudo lxc exec #{@name} -- useradd --uid 1001 --gid 1001 -s /bin/bash -m #{user}}
210
- %x{sudo lxc exec #{@name} -- mkdir /home/#{user}/.ssh}
211
- %x{sudo lxc exec #{@name} -- chmod 0700 /home/#{user}/.ssh}
212
- %x{ssh-add -L | sudo lxc exec #{@name} tee /home/#{user}/.ssh/authorized_keys}
213
- %x{sudo lxc exec #{@name} -- chown -R #{user} /home/#{user}/.ssh}
214
- %x{sudo lxc exec #{@name} -- touch /home/#{@user}/.hushlogin}
215
- %x{sudo lxc exec #{@name} -- chown #{user} /home/#{user}/.hushlogin}
216
- %x{printf "#{user} ALL=(ALL) NOPASSWD: ALL\n" | sudo lxc exec #{@name} -- tee -a /etc/sudoers}
217
- %x{sudo lxc exec #{@name} -- chmod 0440 /etc/sudoers}
218
- end
219
-
220
- def wait_for_boot
221
- BOOT_TIMEOUT.times do |t|
222
- @status = nil # reset status for each iteration to refresh IP
223
- break if get_container_ip
224
- abort_boot if t == (BOOT_TIMEOUT-1)
225
- sleep 1
226
- end
227
- end
228
-
229
- def forward_ports(ports)
230
- redir_pids = []
231
- ports.each do |guest, host|
232
- puts "Forwarding #{get_container_ip}:#{guest} to local port #{host}"
233
- pid = spawn %{sudo redir --caddr=#{get_container_ip} --cport=#{guest} --lport=#{host}}
234
- redir_pids << pid
235
- Process.detach(pid)
236
- end
237
- @state['redir_pids'] = redir_pids
238
- end
239
-
240
- def cleanup_forwarded_ports
241
- if @state.empty?
242
- return
243
- end
244
- @state['redir_pids'].each do |pid|
245
- %x{sudo kill #{pid}}
246
- end
247
- end
248
-
249
- def map_folders(folders)
250
- counter = 0
251
- folders.each do |host, guest|
252
- counter = counter + 1
253
- puts "Mounting #{host} in #{guest}"
254
- absolute_path = %x{readlink -f #{host}}.chomp
255
- %x{sudo lxc config device add #{@name} shared_folder_#{counter} disk source=#{absolute_path} path=#{guest}}
256
- end
257
- end
258
-
259
- def abort_boot
260
- puts "Timeout waiting for container to boot"
261
- exit 1
262
- end
263
-
264
- def self.create_sudoers_file
265
- user=%x{whoami}.chomp
266
- puts <<-EOS
267
- !! WARNING !!
268
- This will create a file, /etc/sudoers.d/lxdev,
269
- which will give your user #{user} access to running
270
- the following commands :
271
- #{WHITELISTED_SUDO_COMMANDS.join(" ")}
272
- with superuser privileges. If you do not know what you're
273
- doing, this can be dangerous and insecure.
274
-
275
- If you want to do this, type 'yesplease'
276
- EOS
277
- action = STDIN.gets.chomp
278
- unless action == 'yesplease'
279
- puts "Not creating sudoers file"
280
- return
281
- end
282
- content = []
283
- content << "# Created by lxdev #{Time.now}"
284
- WHITELISTED_SUDO_COMMANDS.each do |cmd|
285
- cmd_with_path=%x{which #{cmd}}.chomp
286
- content << "#{user} ALL=(root) NOPASSWD: #{cmd_with_path}"
287
- end
288
- %x{printf '#{content.join("\n")}\n' | sudo tee /etc/sudoers.d/lxdev}
289
- %x{sudo chmod 0440 /etc/sudoers.d/lxdev}
290
- puts "Created sudoers file."
291
- end
292
- end