lxdev 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []