susi-qemu 0.0.1
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 +7 -0
- data/bin/susi +128 -0
- data/lib/disk.rb +45 -0
- data/lib/qmp.rb +56 -0
- data/lib/ssh.rb +19 -0
- data/lib/susi.rb +164 -0
- data/lib/vm.rb +171 -0
- data/lib/vnc.rb +46 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1dea990fdc410fb49ca174ec0a224c6a9561b557c334d8878ce09e044bafe810
|
4
|
+
data.tar.gz: 65fc4f31010e7a2da46e68bb1b9704c6a44209ab993e167e850322c317a15799
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91b8c72497b3b8c88cb8508bcb409d65850f0c489507c0f4e3f74c0a25da40cb0a23fc1928f408fbbb63fa25787e878474269f8d9942299f5b43a63d794d2523
|
7
|
+
data.tar.gz: 6d9d80553774e38b5a812c7c58f04d120b62f12e3a06ad5a150b53d5003a59466e0883948f61ff9028a3ed12ec1faec07ec3e603810681e3ca3a39b9698c3f18
|
data/bin/susi
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'susi'
|
4
|
+
|
5
|
+
if ARGV[0] == 'init'
|
6
|
+
Susi::init
|
7
|
+
|
8
|
+
elsif ARGV[0] == 'start'
|
9
|
+
Susi::start
|
10
|
+
|
11
|
+
elsif ARGV[0] == 'rm'
|
12
|
+
# quit vm and remove disk
|
13
|
+
Susi::VM.quit(Susi::current_vm_name)
|
14
|
+
Susi::rm
|
15
|
+
|
16
|
+
# create a disk
|
17
|
+
elsif ARGV[0] == 'disk' && ARGV[1] == 'create' &&
|
18
|
+
ARGV[2].is_a?(String) && ARGV[3].to_i > 0
|
19
|
+
disk_name = ARGV[2]
|
20
|
+
disk_size = ARGV[3].to_i
|
21
|
+
Susi::Disk.create(disk_name, disk_size)
|
22
|
+
|
23
|
+
# clone a disk
|
24
|
+
elsif ARGV[0] == 'disk' && ARGV[1] == 'clone' &&
|
25
|
+
ARGV[2].is_a?(String) && ARGV[3].is_a?(String) &&
|
26
|
+
File.exist?("#{ARGV[2]}.qcow2") &&
|
27
|
+
!File.exist?("#{ARGV[3]}.qcow2")
|
28
|
+
disk_name = ARGV[2]
|
29
|
+
new_disk_name = ARGV[3]
|
30
|
+
Susi::Disk.clone(disk_name, new_disk_name)
|
31
|
+
|
32
|
+
# start VM
|
33
|
+
elsif ARGV[0] == 'vm' && ARGV[1] == 'start' &&
|
34
|
+
ARGV[2].is_a?(String) && File.exist?("#{ARGV[2]}.qcow2")
|
35
|
+
vm_name = ARGV[2]
|
36
|
+
Susi::VM.start(vm_name, vm_name)
|
37
|
+
|
38
|
+
# install VM
|
39
|
+
elsif ARGV[0] == 'vm' && ARGV[1] == 'install' &&
|
40
|
+
ARGV[2].is_a?(String) && File.exist?("#{ARGV[2]}.qcow2") &&
|
41
|
+
ARGV[3].is_a?(String) && File.exist?(ARGV[3])
|
42
|
+
vm_name = ARGV[2]
|
43
|
+
iso = ARGV[3]
|
44
|
+
Susi::VM.install(vm_name, vm_name, iso)
|
45
|
+
|
46
|
+
# quit VM
|
47
|
+
elsif ARGV[0] == 'vm' && ARGV[1] == 'quit' &&
|
48
|
+
ARGV[2].is_a?(String)
|
49
|
+
vm_name = ARGV[2]
|
50
|
+
Susi::VM.quit(vm_name)
|
51
|
+
|
52
|
+
# list VMs
|
53
|
+
elsif ARGV[0] == 'vm' && ARGV[1] == 'ls'
|
54
|
+
Susi::VM.ls
|
55
|
+
elsif ARGV[0] == 'ls'
|
56
|
+
Susi::VM.ls
|
57
|
+
|
58
|
+
# open VNC
|
59
|
+
elsif ARGV[0] == 'vnc' &&
|
60
|
+
ARGV[1].is_a?(String)
|
61
|
+
vm_name = ARGV[2]
|
62
|
+
Susi::VNC.open(vm_name)
|
63
|
+
|
64
|
+
# open SSH
|
65
|
+
elsif ARGV[0] == 'ssh'
|
66
|
+
if ARGV[1].is_a?(String)
|
67
|
+
vm_name = ARGV[1]
|
68
|
+
Susi::SSH.open(vm_name)
|
69
|
+
else
|
70
|
+
Susi::SSH.open(Susi::current_vm_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
# download Debian netinstall ISO
|
74
|
+
elsif ARGV[0] == 'iso' && ARGV[1] == 'download'
|
75
|
+
Susi::Disk.download_debian_netinstall
|
76
|
+
|
77
|
+
else
|
78
|
+
puts <<-EOF
|
79
|
+
Invalid command
|
80
|
+
|
81
|
+
Usage:
|
82
|
+
Init susi setting
|
83
|
+
susi init
|
84
|
+
|
85
|
+
Start VM from current folder
|
86
|
+
susi start
|
87
|
+
|
88
|
+
Clean up disks
|
89
|
+
susi rm
|
90
|
+
|
91
|
+
Create a VM disk
|
92
|
+
susi disk create <disk_name> <disk_size>
|
93
|
+
|
94
|
+
Clone a VM disk
|
95
|
+
susi disk clone <disk_name> <new_disk_name>
|
96
|
+
|
97
|
+
Start a VM
|
98
|
+
susi vm start <vm_name>
|
99
|
+
|
100
|
+
Install a VM
|
101
|
+
susi vm install <vm_name> <iso>
|
102
|
+
|
103
|
+
Quit a VM
|
104
|
+
susi vm quit <vm_name>
|
105
|
+
|
106
|
+
List VMs
|
107
|
+
susi ls
|
108
|
+
susi vm ls
|
109
|
+
|
110
|
+
Open VNC
|
111
|
+
susi vnc <vm_name>
|
112
|
+
|
113
|
+
Open SSH
|
114
|
+
susi ssh <vm_name>
|
115
|
+
|
116
|
+
Download Debian netinstall ISO
|
117
|
+
susi iso download
|
118
|
+
|
119
|
+
susi (v#{Susi::VERSION}) - Simple User System Interface
|
120
|
+
|
121
|
+
author: Daniel Bovensiepen
|
122
|
+
contact: oss@bovi.li
|
123
|
+
www: https://github.com/bovi/susi
|
124
|
+
EOF
|
125
|
+
|
126
|
+
exit 1
|
127
|
+
|
128
|
+
end
|
data/lib/disk.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Susi
|
2
|
+
class Disk
|
3
|
+
DEFAULT_IMG = "debian-12.7.0-amd64-netinst.iso"
|
4
|
+
|
5
|
+
def self.download_debian_netinstall
|
6
|
+
# Check if ~/.susi directory exists, if not create it
|
7
|
+
susi_dir = File.expand_path("~/.susi")
|
8
|
+
Dir.mkdir(susi_dir) unless Dir.exist?(susi_dir)
|
9
|
+
|
10
|
+
# Check if ~/.susi/iso directory exists, if not create it
|
11
|
+
iso_dir = File.join(susi_dir, "iso")
|
12
|
+
Dir.mkdir(iso_dir) unless Dir.exist?(iso_dir)
|
13
|
+
|
14
|
+
# Set the output path to be in the iso directory
|
15
|
+
output_path = File.join(iso_dir, DEFAULT_IMG)
|
16
|
+
|
17
|
+
url = "https://mirrors.tuna.tsinghua.edu.cn/debian-cd/current/amd64/iso-cd/#{DEFAULT_IMG}"
|
18
|
+
|
19
|
+
puts "Downloading Debian netinstall ISO..."
|
20
|
+
|
21
|
+
begin
|
22
|
+
URI.open(url) do |remote_file|
|
23
|
+
File.open(output_path, "wb") do |local_file|
|
24
|
+
local_file.write(remote_file.read)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
puts "Download completed successfully."
|
28
|
+
rescue OpenURI::HTTPError => e
|
29
|
+
puts "Error downloading the ISO: #{e.message}"
|
30
|
+
rescue SocketError => e
|
31
|
+
puts "Network error: #{e.message}"
|
32
|
+
rescue => e
|
33
|
+
puts "An unexpected error occurred: #{e.message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.create(name, size)
|
38
|
+
system("qemu-img create -f qcow2 #{name}.qcow2 #{size}G > /dev/null 2>&1")
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.clone(name, clone)
|
42
|
+
system("qemu-img create -f qcow2 -F qcow2 -b #{name}.qcow2 #{clone}.qcow2 > /dev/null 2>&1")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/qmp.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Susi
|
5
|
+
class QMP
|
6
|
+
def initialize(port)
|
7
|
+
@port = port
|
8
|
+
@server = TCPSocket.new('localhost', @port)
|
9
|
+
|
10
|
+
resp = JSON.parse(@server.gets)
|
11
|
+
unless resp["QMP"]["version"]["qemu"]["major"] == 9
|
12
|
+
server.close
|
13
|
+
raise "QEMU version not supported"
|
14
|
+
end
|
15
|
+
|
16
|
+
# handshake to initialize QMP
|
17
|
+
@server.puts('{"execute":"qmp_capabilities"}')
|
18
|
+
resp = JSON.parse(@server.gets)
|
19
|
+
unless resp["return"] == {}
|
20
|
+
server.close
|
21
|
+
raise "Failed to connect to QMP socket"
|
22
|
+
end
|
23
|
+
|
24
|
+
if block_given?
|
25
|
+
yield(self)
|
26
|
+
self.close
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute(cmd)
|
31
|
+
@server.puts(JSON.dump({"execute" => cmd}))
|
32
|
+
resp = JSON.parse(@server.gets)
|
33
|
+
resp["return"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute_with_args(cmd, args)
|
37
|
+
@server.puts(JSON.dump({"execute" => cmd, "arguments" => args}))
|
38
|
+
resp = JSON.parse(@server.gets)
|
39
|
+
resp["return"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def name
|
43
|
+
@server.puts('{"execute":"query-name"}')
|
44
|
+
resp = JSON.parse(@server.gets)
|
45
|
+
resp["return"]["name"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def quit
|
49
|
+
@server.puts('{"execute":"quit"}')
|
50
|
+
end
|
51
|
+
|
52
|
+
def close
|
53
|
+
@server.close
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/ssh.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'net/ssh'
|
3
|
+
|
4
|
+
module Susi
|
5
|
+
class SSH
|
6
|
+
def self.open(name)
|
7
|
+
vm = VM.new(name)
|
8
|
+
exec "ssh -p #{vm.ssh_port} dabo@#{vm.ip}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.set_hostname(name)
|
12
|
+
vm = VM.new(name)
|
13
|
+
Net::SSH.start(vm.ip, 'dabo', port: vm.ssh_port, keys: [File.expand_path('~/.ssh/id_ed25519')]) do |ssh|
|
14
|
+
puts "setting up VM..."
|
15
|
+
ssh.exec!("sudo hostnamectl set-hostname #{name}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/susi.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'qmp'
|
4
|
+
require_relative 'vm'
|
5
|
+
require_relative 'ssh'
|
6
|
+
require_relative 'vnc'
|
7
|
+
require_relative 'disk'
|
8
|
+
|
9
|
+
require 'yaml'
|
10
|
+
require 'securerandom'
|
11
|
+
|
12
|
+
module Susi
|
13
|
+
VERSION = '0.0.1'
|
14
|
+
CONFIG_FILE = ".susi.yml"
|
15
|
+
SUSI_DIR = "#{ENV['HOME']}/.susi"
|
16
|
+
TEMPLATE_DIR = "#{SUSI_DIR}/templates"
|
17
|
+
DISK_DIR = "#{SUSI_DIR}/disks"
|
18
|
+
|
19
|
+
def self.current_vm_name
|
20
|
+
if !File.exist?(CONFIG_FILE)
|
21
|
+
raise "susi is not initialized"
|
22
|
+
end
|
23
|
+
|
24
|
+
# load config
|
25
|
+
config = YAML.load_file(CONFIG_FILE)
|
26
|
+
config['name'] || Dir.pwd.split('/').last
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.init
|
30
|
+
# check if .susi.yml exist, if not create it
|
31
|
+
if !File.exist?(CONFIG_FILE)
|
32
|
+
# create unique ID
|
33
|
+
id = SecureRandom.uuid
|
34
|
+
|
35
|
+
File.open(CONFIG_FILE, "w") do |file|
|
36
|
+
file.puts <<-YAML
|
37
|
+
id: #{id}
|
38
|
+
template: DEFAULT
|
39
|
+
YAML
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.start
|
45
|
+
if !File.exist?(CONFIG_FILE)
|
46
|
+
raise "susi is not initialized"
|
47
|
+
end
|
48
|
+
|
49
|
+
# load config
|
50
|
+
config = YAML.load_file(CONFIG_FILE)
|
51
|
+
id = config['id']
|
52
|
+
name = config['name'] || Dir.pwd.split('/').last
|
53
|
+
disk_template = "#{TEMPLATE_DIR}/#{config['template']}"
|
54
|
+
|
55
|
+
# check if vm is already running
|
56
|
+
if VM.running?(name)
|
57
|
+
raise "VM #{name} is already running"
|
58
|
+
end
|
59
|
+
|
60
|
+
# check if disk exist, if not clone it
|
61
|
+
disk_name = "#{DISK_DIR}/#{id}"
|
62
|
+
if !File.exist?("#{disk_name}.qcow2")
|
63
|
+
puts "Disk not found, cloning..."
|
64
|
+
Susi::Disk.clone(disk_template, disk_name)
|
65
|
+
|
66
|
+
Susi::VM.start(name, disk_name)
|
67
|
+
|
68
|
+
# SSH into VM and set hostname
|
69
|
+
Susi::SSH.set_hostname(name)
|
70
|
+
else
|
71
|
+
Susi::VM.start(name, disk_name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.rm
|
76
|
+
if !File.exist?(CONFIG_FILE)
|
77
|
+
raise "susi is not initialized"
|
78
|
+
end
|
79
|
+
|
80
|
+
# load config
|
81
|
+
config = YAML.load_file(CONFIG_FILE)
|
82
|
+
id = config['id']
|
83
|
+
disk_name = "#{DISK_DIR}/#{id}.qcow2"
|
84
|
+
|
85
|
+
if File.exist?(disk_name)
|
86
|
+
# verbose remove file
|
87
|
+
puts "removing #{disk_name}"
|
88
|
+
File.delete(disk_name)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# perform a test when the script is run
|
94
|
+
if __FILE__ == $0
|
95
|
+
require 'test/unit'
|
96
|
+
|
97
|
+
class SusiTest < Test::Unit::TestCase
|
98
|
+
def run_command(cmd)
|
99
|
+
system("#{cmd} > /dev/null 2>&1")
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_cli_empty
|
103
|
+
assert_equal(false, run_command("ruby ./susi.rb"))
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_create_img
|
107
|
+
Susi::Disk.create("test", "1")
|
108
|
+
|
109
|
+
# check created file
|
110
|
+
assert(File.exist?("test.qcow2"))
|
111
|
+
info = `qemu-img info test.qcow2`
|
112
|
+
assert(info.include?("virtual size: 1 GiB"))
|
113
|
+
assert(info.include?("format: qcow2"))
|
114
|
+
|
115
|
+
run_command("rm test.qcow2")
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_cli_create_img
|
119
|
+
assert_equal(true, run_command("ruby ./susi.rb disk create test 1"))
|
120
|
+
assert(File.exist?("test.qcow2"))
|
121
|
+
run_command("rm test.qcow2")
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_cli_create_img_fail
|
125
|
+
assert_equal(false, run_command("ruby ./susi.rb disk create test"))
|
126
|
+
assert_equal(false, run_command("ruby ./susi.rb disk create test pest"))
|
127
|
+
assert_equal(false, run_command("ruby ./susi.rb disk create 1"))
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_clone_img
|
131
|
+
Susi::Disk.create("test", "1")
|
132
|
+
Susi::Disk.clone("test", "clone")
|
133
|
+
|
134
|
+
assert(File.exist?("clone.qcow2"))
|
135
|
+
info = `qemu-img info clone.qcow2`
|
136
|
+
assert(info.include?("virtual size: 1 GiB"))
|
137
|
+
assert(info.include?("format: qcow2"))
|
138
|
+
assert(info.include?("backing file: test.qcow2"))
|
139
|
+
assert(info.include?("backing file format: qcow2"))
|
140
|
+
|
141
|
+
run_command("rm clone.qcow2")
|
142
|
+
run_command("rm test.qcow2")
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_cli_clone_img
|
146
|
+
assert_equal(true, run_command("ruby ./susi.rb disk create test 1"))
|
147
|
+
assert_equal(true, run_command("ruby ./susi.rb disk clone test clone"))
|
148
|
+
assert(File.exist?("clone.qcow2"))
|
149
|
+
run_command("rm clone.qcow2")
|
150
|
+
run_command("rm test.qcow2")
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_cli_clone_img_fail
|
154
|
+
assert_equal(false, run_command("ruby ./susi.rb disk clone test clone"))
|
155
|
+
assert_equal(true, run_command("ruby ./susi.rb disk create test 1"))
|
156
|
+
assert_equal(false, run_command("ruby ./susi.rb disk clone test"))
|
157
|
+
assert_equal(true, run_command("ruby ./susi.rb disk clone test clone"))
|
158
|
+
assert_equal(false, run_command("ruby ./susi.rb disk clone test clone"))
|
159
|
+
|
160
|
+
run_command("rm clone.qcow2")
|
161
|
+
run_command("rm test.qcow2")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/lib/vm.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
module Susi
|
2
|
+
class VM
|
3
|
+
def self.running?(name)
|
4
|
+
ps = `ps aux | grep qemu-system-x86_64`
|
5
|
+
ps.split("\n").each do |line|
|
6
|
+
next unless line.match /\-name/
|
7
|
+
m = line.match(/-name (\w+)/)
|
8
|
+
n = m[1]
|
9
|
+
return true if n == name
|
10
|
+
end
|
11
|
+
return false
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(name)
|
15
|
+
@name = name
|
16
|
+
@qmp_port = nil
|
17
|
+
@vnc_port = nil
|
18
|
+
@ssh_port = nil
|
19
|
+
|
20
|
+
ps = `ps aux | grep qemu-system-x86_64`
|
21
|
+
ps.split("\n").each do |line|
|
22
|
+
next unless line.match /\-name/
|
23
|
+
m = line.match(/-name (\w+)/)
|
24
|
+
n = m[1]
|
25
|
+
next unless n == @name
|
26
|
+
|
27
|
+
m = line.match(/-qmp tcp:localhost:(\d+)/)
|
28
|
+
@qmp_port = m[1].to_i
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def quit
|
33
|
+
QMP.new(@qmp_port) { |q| q.quit }
|
34
|
+
end
|
35
|
+
|
36
|
+
def disk
|
37
|
+
d = ''
|
38
|
+
QMP.new(@qmp_port) { |q| d = q.execute("query-block")[0]['inserted']['file'] }
|
39
|
+
d
|
40
|
+
end
|
41
|
+
|
42
|
+
def vnc_port
|
43
|
+
@vnc_port ||= begin
|
44
|
+
port = -1
|
45
|
+
QMP.new(@qmp_port) { |q| port = q.execute("query-vnc")['service'] }
|
46
|
+
port.to_i
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def vnc_www_port
|
51
|
+
vnc_port - 5900 + 5800
|
52
|
+
end
|
53
|
+
|
54
|
+
def vnc_websocket_port
|
55
|
+
vnc_port - 5900 + 5700
|
56
|
+
end
|
57
|
+
|
58
|
+
def ssh_port
|
59
|
+
@ssh_port ||= begin
|
60
|
+
port = -1
|
61
|
+
QMP.new(@qmp_port) do |q|
|
62
|
+
resp = q.execute_with_args("human-monitor-command", {"command-line" => "info usernet"})
|
63
|
+
resp = resp.split("\r\n")[2].split
|
64
|
+
src_port = resp[3].to_i
|
65
|
+
dst_port = resp[5].to_i
|
66
|
+
if dst_port == 22
|
67
|
+
port = src_port
|
68
|
+
end
|
69
|
+
end
|
70
|
+
port.to_i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def ip
|
75
|
+
ip = Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }
|
76
|
+
ip.ip_address
|
77
|
+
end
|
78
|
+
|
79
|
+
def qmp_port
|
80
|
+
@qmp_port
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.quit(name)
|
84
|
+
vm = VM.new(name)
|
85
|
+
vm.quit
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.install(name, disk, iso)
|
89
|
+
self.start(name, disk, cdrom: iso)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.get_free_port(start_port, end_port)
|
93
|
+
(start_port..end_port).each do |port|
|
94
|
+
begin
|
95
|
+
server = TCPServer.new('127.0.0.1', port)
|
96
|
+
server.close
|
97
|
+
return port
|
98
|
+
rescue Errno::EADDRINUSE
|
99
|
+
next
|
100
|
+
end
|
101
|
+
end
|
102
|
+
raise "No free port found in range #{start_port}..#{end_port}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.start(name, disk, cdrom: nil)
|
106
|
+
qmp_port = self.get_free_port(4000, 4099)
|
107
|
+
ssh_port = self.get_free_port(2000, 2099)
|
108
|
+
vnc_port = self.get_free_port(5900, 5999)
|
109
|
+
vnc_www_port = 5800 + vnc_port - 5900
|
110
|
+
vnc_websocket_port = 5700 + vnc_port - 5900
|
111
|
+
|
112
|
+
puts "QMP Port: #{qmp_port}"
|
113
|
+
puts "SSH Port: #{ssh_port}"
|
114
|
+
puts "VNC Port: #{vnc_port}"
|
115
|
+
puts "VNC WWW Port: #{vnc_www_port}"
|
116
|
+
puts "VNC WebSocket Port: #{vnc_websocket_port}"
|
117
|
+
|
118
|
+
cmd = []
|
119
|
+
|
120
|
+
cmd << "~/susi/docs/qemu/build/qemu-system-x86_64"
|
121
|
+
|
122
|
+
# general setup
|
123
|
+
cmd << "-name #{name}"
|
124
|
+
cmd << "-m 2048"
|
125
|
+
cmd << "-hda #{disk}.qcow2"
|
126
|
+
cmd << "-daemonize"
|
127
|
+
cmd << "-enable-kvm"
|
128
|
+
|
129
|
+
# control interfaces
|
130
|
+
cmd << "-qmp tcp:localhost:#{qmp_port},server,nowait"
|
131
|
+
cmd << "-vnc :#{vnc_port-5900},websocket=on"
|
132
|
+
|
133
|
+
# Network capabilities
|
134
|
+
cmd << "-nic user,model=virtio-net-pci,hostfwd=tcp::#{ssh_port}-:22"
|
135
|
+
|
136
|
+
# cdrom for installation
|
137
|
+
if cdrom
|
138
|
+
cmd << "-cdrom #{cdrom}"
|
139
|
+
end
|
140
|
+
|
141
|
+
puts cmd.join(" ")
|
142
|
+
system(cmd.join(" "))
|
143
|
+
|
144
|
+
QMP.new(qmp_port).close
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.ls
|
148
|
+
ps = `ps aux | grep qemu-system-x86_64`
|
149
|
+
ps.split("\n").each do |line|
|
150
|
+
next unless line.match /\-name/
|
151
|
+
|
152
|
+
# get VM access
|
153
|
+
m = line.match(/-name (\w+)/)
|
154
|
+
n = m[1]
|
155
|
+
vm = VM.new(n)
|
156
|
+
|
157
|
+
# get local device name or IP
|
158
|
+
ip = Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }
|
159
|
+
|
160
|
+
puts <<-EOF
|
161
|
+
VM: #{n}
|
162
|
+
- Disk: #{vm.disk}
|
163
|
+
- QMP: #{vm.qmp_port}
|
164
|
+
- VNC: vnc://#{ip.ip_address}:#{vm.vnc_port}
|
165
|
+
- Screen: http://#{ip.ip_address}:#{vm.vnc_www_port}/
|
166
|
+
- SSH: ssh -p #{vm.ssh_port} dabo@#{ip.ip_address}
|
167
|
+
EOF
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/lib/vnc.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'webrick'
|
3
|
+
|
4
|
+
module Susi
|
5
|
+
class VNC
|
6
|
+
def self.open(name)
|
7
|
+
vm = VM.new(name)
|
8
|
+
port = vm.vnc_www_port
|
9
|
+
ip = Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }
|
10
|
+
token = (0...8).map { (65 + rand(26)).chr }.join
|
11
|
+
url = "http://#{ip.ip_address}:#{port}/#{token}.html"
|
12
|
+
|
13
|
+
# prepare paths and files
|
14
|
+
path = File.expand_path(File.dirname(__FILE__))
|
15
|
+
novnc_path = File.join(path, "novnc")
|
16
|
+
html = File.read(File.join(novnc_path, "screen.html"))
|
17
|
+
html.gsub!("###TOKEN###", token)
|
18
|
+
html.gsub!("###HOST###", ip.ip_address)
|
19
|
+
html.gsub!("###PORT###", vm.vnc_websocket_port.to_s)
|
20
|
+
|
21
|
+
server = WEBrick::HTTPServer.new(:Port => port,
|
22
|
+
:Logger => WEBrick::Log.new("/dev/null"),
|
23
|
+
:AccessLog => [])
|
24
|
+
server.mount_proc "/#{token}.html" do |req, res|
|
25
|
+
# print access by someone on command line
|
26
|
+
puts "[#{Time.now}] Access by: #{req.peeraddr[3]}"
|
27
|
+
res.body = html
|
28
|
+
end
|
29
|
+
server.mount_proc "/#{token}.close.html" do |req, res|
|
30
|
+
puts "[#{Time.now}] Closing VNC..."
|
31
|
+
res.body = "Closing VNC..."
|
32
|
+
server.shutdown
|
33
|
+
end
|
34
|
+
server.mount("/core",
|
35
|
+
WEBrick::HTTPServlet::FileHandler,
|
36
|
+
File.join(novnc_path, "core"))
|
37
|
+
server.mount("/vendor",
|
38
|
+
WEBrick::HTTPServlet::FileHandler,
|
39
|
+
File.join(novnc_path, "vendor"))
|
40
|
+
|
41
|
+
puts "[#{Time.now}] VNC screen: #{url}"
|
42
|
+
|
43
|
+
server.start
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: susi-qemu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Bovensiepen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-09-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ssh
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.2.0
|
27
|
+
description: Config, manage, and maintain multiple QEMU instances for your projects
|
28
|
+
email: oss@bovi.li
|
29
|
+
executables:
|
30
|
+
- susi
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- bin/susi
|
35
|
+
- lib/disk.rb
|
36
|
+
- lib/qmp.rb
|
37
|
+
- lib/ssh.rb
|
38
|
+
- lib/susi.rb
|
39
|
+
- lib/vm.rb
|
40
|
+
- lib/vnc.rb
|
41
|
+
homepage: https://github.com/bovi/susi
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata:
|
45
|
+
source_code_uri: https://github.com/bovi/susi
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.3.15
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Manage project QEMU instances
|
65
|
+
test_files: []
|