lxdev 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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/lxdev +77 -0
  3. data/lib/lxdev.rb +290 -0
  4. metadata +75 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b44f874a451b8fcbf87c6015993879e1d587f703
4
+ data.tar.gz: 31fbead89ddd558dc00126c21d3705ba759a720d
5
+ SHA512:
6
+ metadata.gz: dee6338ab4a878bcd51552da860f80db522ca855160ae774c9d75a85a0f4888947d1ec1af002f8e65b19d94c76d79ded6f60e99789db07a1edaa1ef04279a511
7
+ data.tar.gz: abe2b8ff716ae61d65dc9db7f43fac06886fa48b8129c96f015616b8217091234a2bb92b2343dada06f4d018d63236975eb99d43da381f0658d6e264a2e9363d
data/bin/lxdev ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ require 'lxdev'
3
+ require 'optparse'
4
+
5
+ def option_parser
6
+ opt_parser = OptionParser.new do |opts|
7
+ opts.banner = "Usage: lxdev [options] command"
8
+
9
+ opts.on("-h", "--help", "Prints this help") do
10
+ puts opts
11
+ exit
12
+ end
13
+
14
+ opts.on("-v", "--version", "Prints the version") do
15
+ puts "Version #{LxDev::VERSION}"
16
+ exit
17
+ end
18
+
19
+ end
20
+ opt_parser.separator ""
21
+ opt_parser.separator <<-EOS
22
+ Commands:
23
+ up Bring container up
24
+ status Show status of the container
25
+ ssh Log into the container
26
+ halt Stop the container
27
+ destroy Destroy the container
28
+ provision Run provisioning commands from YAML file
29
+ exec Run command as root in container
30
+ sudoers Create a sudoers file, allowing you to use lxdev without entering the sudo password
31
+ EOS
32
+ opt_parser.parse!()
33
+
34
+ end
35
+
36
+
37
+ def execute_main_command(lxdev)
38
+ if ARGV.empty?
39
+ puts "No arguments.\nRun \"lxdev --help\" for info"
40
+ exit 1
41
+ end
42
+
43
+ case ARGV.first
44
+ when 'up'
45
+ lxdev.up()
46
+ when 'status'
47
+ lxdev.status()
48
+ when 'ssh'
49
+ lxdev.ssh(ARGV[1..-1])
50
+ when 'halt'
51
+ lxdev.halt()
52
+ when 'destroy'
53
+ lxdev.destroy()
54
+ when 'provision'
55
+ lxdev.provision()
56
+ when 'exec'
57
+ command = ARGV[1..-1].join(" ")
58
+ if LxDev::SHELLS.include?(command)
59
+ lxdev.execute(command, interactive: true)
60
+ else
61
+ lxdev.execute(command)
62
+ end
63
+ when 'sudoers'
64
+ LxDev.create_sudoers_file()
65
+ else
66
+ puts "Unknown command.\nRun \"lxdev --help\" for info"
67
+ end
68
+ end
69
+
70
+
71
+
72
+ option_parser
73
+ lxdev = LxDev.setup()
74
+ if lxdev
75
+ execute_main_command(lxdev)
76
+ lxdev.save_state
77
+ end
data/lib/lxdev.rb ADDED
@@ -0,0 +1,290 @@
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.0'
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? ? 'bash --noprofile' : "'#{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{sudo 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{sudo 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{printf "#{user} ALL=(ALL) NOPASSWD: ALL\n" | sudo lxc exec #{@name} -- tee -a /etc/sudoers}
215
+ %x{sudo lxc exec #{@name} -- chmod 0440 /etc/sudoers}
216
+ end
217
+
218
+ def wait_for_boot
219
+ BOOT_TIMEOUT.times do |t|
220
+ @status = nil # reset status for each iteration to refresh IP
221
+ break if get_container_ip
222
+ abort_boot if t == (BOOT_TIMEOUT-1)
223
+ sleep 1
224
+ end
225
+ end
226
+
227
+ def forward_ports(ports)
228
+ redir_pids = []
229
+ ports.each do |guest, host|
230
+ puts "Forwarding #{get_container_ip}:#{guest} to local port #{host}"
231
+ pid = spawn %{sudo redir --caddr=#{get_container_ip} --cport=#{guest} --lport=#{host}}
232
+ redir_pids << pid
233
+ Process.detach(pid)
234
+ end
235
+ @state['redir_pids'] = redir_pids
236
+ end
237
+
238
+ def cleanup_forwarded_ports
239
+ if @state.empty?
240
+ return
241
+ end
242
+ @state['redir_pids'].each do |pid|
243
+ %x{sudo kill #{pid}}
244
+ end
245
+ end
246
+
247
+ def map_folders(folders)
248
+ counter = 0
249
+ folders.each do |host, guest|
250
+ counter = counter + 1
251
+ puts "Mounting #{host} in #{guest}"
252
+ absolute_path = %x{readlink -f #{host}}.chomp
253
+ %x{sudo lxc config device add #{@name} shared_folder_#{counter} disk source=#{absolute_path} path=#{guest}}
254
+ end
255
+ end
256
+
257
+ def abort_boot
258
+ puts "Timeout waiting for container to boot"
259
+ exit 1
260
+ end
261
+
262
+ def self.create_sudoers_file
263
+ user=%x{whoami}.chomp
264
+ puts <<-EOS
265
+ !! WARNING !!
266
+ This will create a file, /etc/sudoers.d/lxdev,
267
+ which will give your user #{user} access to running
268
+ the following commands :
269
+ #{WHITELISTED_SUDO_COMMANDS.join(" ")}
270
+ with superuser privileges. If you do not know what you're
271
+ doing, this can be dangerous and insecure.
272
+
273
+ If you want to do this, type 'yesplease'
274
+ EOS
275
+ action = STDIN.gets.chomp
276
+ unless action == 'yesplease'
277
+ puts "Not creating sudoers file"
278
+ return
279
+ end
280
+ content = []
281
+ content << "# Created by lxdev #{Time.now}"
282
+ WHITELISTED_SUDO_COMMANDS.each do |cmd|
283
+ cmd_with_path=%x{which #{cmd}}.chomp
284
+ content << "#{user} ALL=(root) NOPASSWD: #{cmd_with_path}"
285
+ end
286
+ %x{printf '#{content.join("\n")}\n' | sudo tee /etc/sudoers.d/lxdev}
287
+ %x{sudo chmod 0440 /etc/sudoers.d/lxdev}
288
+ puts "Created sudoers file."
289
+ end
290
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lxdev
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christian Lønaas
8
+ - Eivind Mork
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-06-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.1'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.1'
28
+ - !ruby/object:Gem::Dependency
29
+ name: terminal-table
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.8'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.8'
42
+ description: Lightweight vagrant-like system using LXD
43
+ email: christian.lonaas@gyldendal.no
44
+ executables:
45
+ - lxdev
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - bin/lxdev
50
+ - lib/lxdev.rb
51
+ homepage: https://github.com/GyldendalDigital/lxdev
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.5.1
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Automagic development environment with LXD
75
+ test_files: []