susi-qemu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. checksums.yaml +7 -0
  2. data/bin/susi +128 -0
  3. data/lib/disk.rb +45 -0
  4. data/lib/qmp.rb +56 -0
  5. data/lib/ssh.rb +19 -0
  6. data/lib/susi.rb +164 -0
  7. data/lib/vm.rb +171 -0
  8. data/lib/vnc.rb +46 -0
  9. 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: []