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.
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: []