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