qemu-toolkit 0.2.18
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.
- data/LICENSE +23 -0
- data/README +7 -0
- data/bin/storadm +14 -0
- data/bin/vmadm +14 -0
- data/bin/vmconnect +9 -0
- data/lib/qemu-toolkit/backend/illumos.rb +46 -0
- data/lib/qemu-toolkit/config.rb +48 -0
- data/lib/qemu-toolkit/dsl.rb +82 -0
- data/lib/qemu-toolkit/iscsi_target.rb +80 -0
- data/lib/qemu-toolkit/local_disk_set.rb +39 -0
- data/lib/qemu-toolkit/storadm.rb +110 -0
- data/lib/qemu-toolkit/vm.rb +331 -0
- data/lib/qemu-toolkit/vm_storage.rb +180 -0
- data/lib/qemu-toolkit/vmadm.rb +182 -0
- data/lib/qemu-toolkit/vnic.rb +61 -0
- data/lib/qemu-toolkit.rb +12 -0
- metadata +81 -0
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
Copyright (c) 2012 Kaspar Schiess
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person
|
5
|
+
obtaining a copy of this software and associated documentation
|
6
|
+
files (the "Software"), to deal in the Software without
|
7
|
+
restriction, including without limitation the rights to use,
|
8
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the
|
10
|
+
Software is furnished to do so, subject to the following
|
11
|
+
conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
18
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
20
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
21
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
22
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
23
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
data/bin/storadm
ADDED
data/bin/vmadm
ADDED
data/bin/vmconnect
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# A small helper script that allows to make a VNC connection through ssh
|
4
|
+
# securely. Note that on the VM host, you need 'socat' installed for this to
|
5
|
+
# work. Uses ssvnc (vncviewer).
|
6
|
+
|
7
|
+
host, machine = ARGV
|
8
|
+
exec "ssvnc exec='/usr/bin/ssh #{host} /usr/local/bin/socat "+
|
9
|
+
"STDIO UNIX-CONNECT:/var/run/qemu-toolkit/#{machine}/vm.vnc'"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module QemuToolkit
|
2
|
+
class Backend::Illumos
|
3
|
+
attr_accessor :verbose
|
4
|
+
|
5
|
+
def zfs *args
|
6
|
+
run_cmd :zfs, *args
|
7
|
+
end
|
8
|
+
def itadm(*args)
|
9
|
+
run_cmd :itadm, *args
|
10
|
+
end
|
11
|
+
def stmfadm *args
|
12
|
+
run_cmd :stmfadm, *args
|
13
|
+
end
|
14
|
+
def qemu name, args
|
15
|
+
exec_with_arg0 '/usr/bin/qemu-system-x86_64', name, *args
|
16
|
+
end
|
17
|
+
def iscsiadm *args
|
18
|
+
run_cmd :iscsiadm, *args
|
19
|
+
end
|
20
|
+
def dladm *args
|
21
|
+
run_cmd :dladm, *args
|
22
|
+
end
|
23
|
+
|
24
|
+
def disks(path)
|
25
|
+
output = zfs :list, '-H -o name -t volume', '-r', path
|
26
|
+
output.split("\n")
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_cmd(*args)
|
30
|
+
cmd = args.join(' ')
|
31
|
+
puts cmd if verbose
|
32
|
+
ret = %x(#{cmd} 2>&1)
|
33
|
+
|
34
|
+
raise "Execution error: #{cmd}." unless $?.success?
|
35
|
+
|
36
|
+
ret
|
37
|
+
end
|
38
|
+
def exec_with_arg0(command, name, *args)
|
39
|
+
# exec in this form only wants to be given raw arguments that contain
|
40
|
+
# no spaces. This is why we split and flatten args.
|
41
|
+
exec [command, name], *args.map { |a| a.split }.flatten
|
42
|
+
|
43
|
+
fail "exec failed."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module QemuToolkit
|
2
|
+
|
3
|
+
# A configuration class that acts as a singleton, but really isn't. This
|
4
|
+
# allows for a mixed style of coding, either using QemuToolkit::Config as
|
5
|
+
# an instance or as a class.
|
6
|
+
#
|
7
|
+
class Config
|
8
|
+
class << self # CLASS METHODS
|
9
|
+
def method_missing(sym, *args, &block)
|
10
|
+
return current.send(sym, *args, &block) if current.respond_to?(sym)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
def respond_to?(sym)
|
14
|
+
current.respond_to?(sym) || super
|
15
|
+
end
|
16
|
+
|
17
|
+
def current
|
18
|
+
@current ||= new
|
19
|
+
end
|
20
|
+
def reset
|
21
|
+
@current = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_writer :etc
|
26
|
+
attr_writer :var_run
|
27
|
+
|
28
|
+
def etc(*args)
|
29
|
+
expand_path(@etc, *args)
|
30
|
+
end
|
31
|
+
def var_run(*args)
|
32
|
+
expand_path(@var_run, *args)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The command runner backend for all commands. Actual execution happens
|
36
|
+
# through this instance. This has advantages for testing and for
|
37
|
+
# customizing behaviour.
|
38
|
+
#
|
39
|
+
def backend
|
40
|
+
@backend ||= Backend::Illumos.new
|
41
|
+
end
|
42
|
+
private
|
43
|
+
def expand_path(*args)
|
44
|
+
File.expand_path(
|
45
|
+
File.join(*args))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
|
2
|
+
module QemuToolkit
|
3
|
+
# A generic dsl class. You define a target keyword you want to associate with
|
4
|
+
# an object instance you also give during construction. The dsl will then
|
5
|
+
# react only to a call to TARGET, allowing to use a block to configure the
|
6
|
+
# object you give.
|
7
|
+
#
|
8
|
+
# Inside the block, the following delegations are made:
|
9
|
+
#
|
10
|
+
# foo 'some value'
|
11
|
+
# # delegated to obj.foo= 'some value' if possible
|
12
|
+
# # or then to obj.add_foo 'some_value'
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class Bar
|
16
|
+
# attr_accessor :name
|
17
|
+
# attr_accessor :test
|
18
|
+
# end
|
19
|
+
# bar = Bar.new
|
20
|
+
#
|
21
|
+
# dsl = QemuToolkit::DSL::File.new
|
22
|
+
# dsl.add_toplevel_target :foo, { |name| Bar.new(name) }
|
23
|
+
# dsl.load_file(some_file)
|
24
|
+
#
|
25
|
+
# # In this example, some_file would contain something like
|
26
|
+
# foo('name') do
|
27
|
+
# test 'something'
|
28
|
+
# end
|
29
|
+
# bar.name # => 'name'
|
30
|
+
# bar.test # => 'something'
|
31
|
+
#
|
32
|
+
class DSL
|
33
|
+
class File
|
34
|
+
attr_reader :objects
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@objects = []
|
38
|
+
end
|
39
|
+
def load_file(path)
|
40
|
+
eval(
|
41
|
+
::File.read(path),
|
42
|
+
binding,
|
43
|
+
path)
|
44
|
+
end
|
45
|
+
def add_toplevel_target target, producer
|
46
|
+
define_singleton_method(target) { |*args, &block|
|
47
|
+
object = producer.call(*args)
|
48
|
+
Unit.new(object, &block)
|
49
|
+
|
50
|
+
@objects << object
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
class Unit
|
55
|
+
def initialize(obj, &block)
|
56
|
+
@object = obj
|
57
|
+
instance_eval(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_missing(sym, *args, &block)
|
61
|
+
delegate = find_delegate_method(sym, @object)
|
62
|
+
return super unless delegate
|
63
|
+
|
64
|
+
if delegate.arity == args.size
|
65
|
+
delegate.call(*args, &block)
|
66
|
+
else
|
67
|
+
delegate.call(args, &block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
def respond_to?(sym)
|
71
|
+
find_delegate_method(sym, @object) || super
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def find_delegate_method sym, obj
|
76
|
+
["#{sym}=", "add_#{sym}"].each { |dm|
|
77
|
+
return obj.method(dm) if obj.respond_to? dm }
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module QemuToolkit
|
2
|
+
class ISCSITarget
|
3
|
+
def initialize(iqn, address, backend)
|
4
|
+
@iqn, @address = iqn, address
|
5
|
+
@backend = backend
|
6
|
+
|
7
|
+
# Keep track if disks has ever returned an array of size > 0. This would
|
8
|
+
# mean that the target is connected and will stay connected until we
|
9
|
+
# change that.
|
10
|
+
@mapped = false
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :iqn
|
14
|
+
attr_reader :address
|
15
|
+
|
16
|
+
def mapped?
|
17
|
+
disks.size > 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def ensure_exists
|
21
|
+
# If the target is mapped already, nothing to do.
|
22
|
+
return if mapped?
|
23
|
+
|
24
|
+
# Map the target
|
25
|
+
begin
|
26
|
+
@backend.iscsiadm :add, 'static-config', "#{iqn},#{address}", '2>/dev/null'
|
27
|
+
rescue
|
28
|
+
# Ignore already mapped targets
|
29
|
+
end
|
30
|
+
|
31
|
+
print "Waiting for iSCSI disks to come online..."
|
32
|
+
while !mapped?
|
33
|
+
print '.'
|
34
|
+
sleep 0.5 # takes a short while until login
|
35
|
+
end
|
36
|
+
puts 'OK.'
|
37
|
+
end
|
38
|
+
|
39
|
+
# A cached 'list target -vS'
|
40
|
+
#
|
41
|
+
def target_list
|
42
|
+
@backend.iscsiadm :list, :target, '-vS'
|
43
|
+
end
|
44
|
+
|
45
|
+
def disks
|
46
|
+
luns = []
|
47
|
+
|
48
|
+
state = 0
|
49
|
+
last_lun = nil
|
50
|
+
|
51
|
+
target_list.each_line do |line|
|
52
|
+
case state
|
53
|
+
when 0
|
54
|
+
state += 1 if line.match(/^Target: #{Regexp.escape(iqn)}\n/m)
|
55
|
+
lun = nil
|
56
|
+
when 1
|
57
|
+
if md=line.match(/^\s+LUN: (\d+)\n/m)
|
58
|
+
last_lun = md[1]
|
59
|
+
end
|
60
|
+
|
61
|
+
if last_lun && md=line.match(/^\s+OS Device Name: (\/dev\/rdsk\/.*)\n/m)
|
62
|
+
luns << [Integer(last_lun), md[1]]
|
63
|
+
end
|
64
|
+
|
65
|
+
state += 1 if line.match(/^Target: /)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
luns.sort_by { |no,_| no }.map { |_, dev| p0ify(dev) }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def p0ify(str)
|
74
|
+
# /dev/rdsk/c4t600144F0503CC9000000503F8B09000Ad0s2 to
|
75
|
+
# /dev/rdsk/c4t600144F0503CC9000000503F8B09000Ad0p0
|
76
|
+
|
77
|
+
str.sub(/s2$/, 'p0')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module QemuToolkit
|
2
|
+
|
3
|
+
class LocalDiskSet
|
4
|
+
def self.for(name, backend)
|
5
|
+
output = backend.zfs :list, "-oname,#{QemuToolkit::EXPORT_TAG} -H"
|
6
|
+
candidates = output.lines.map { |l| l.chomp.strip.split }
|
7
|
+
candidates.reject! { |n,_| ! n }
|
8
|
+
|
9
|
+
# Finds all qemu-toolkit storage spaces that end in '/name':
|
10
|
+
# Output from this step are only the spaces that match
|
11
|
+
storage_spaces = candidates.
|
12
|
+
select { |cand_name, flag|
|
13
|
+
%w(true false).include?(flag) && cand_name.end_with?('/'+name) }
|
14
|
+
|
15
|
+
# Finds all disks for each of the candidate exports:
|
16
|
+
# Output from this step should be <name, disks> tuples
|
17
|
+
storage_spaces.
|
18
|
+
map { |base_name, _| new(
|
19
|
+
base_name,
|
20
|
+
candidates.map(&:first).
|
21
|
+
select { |name|
|
22
|
+
|
23
|
+
name.match(/#{Regexp.escape(base_name)}\/disk\d+/) } ) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(name, disks)
|
27
|
+
@name = name
|
28
|
+
@disks = disks
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :name
|
32
|
+
|
33
|
+
def each_disk
|
34
|
+
@disks.sort.each do |path|
|
35
|
+
yield "/dev/zvol/dsk/#{path}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'clamp'
|
2
|
+
|
3
|
+
require 'qemu-toolkit'
|
4
|
+
|
5
|
+
module QemuToolkit
|
6
|
+
class Storadm < Clamp::Command
|
7
|
+
option ['-v', '--verbose'], :flag, 'be chatty'
|
8
|
+
option ['-n', '--dry-run'], :flag, "don't execute commands, instead just print them"
|
9
|
+
|
10
|
+
# Command backend to use during the processing of subcommands.
|
11
|
+
#
|
12
|
+
def backend
|
13
|
+
Config.backend
|
14
|
+
end
|
15
|
+
|
16
|
+
# A factory method for VM storage.
|
17
|
+
#
|
18
|
+
def storage(name)
|
19
|
+
VMStorage.new(name, backend)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Main execute method - delegates to _execute in the subcommands. This
|
23
|
+
# handles transforming Ruby errors into simple shell errors.
|
24
|
+
#
|
25
|
+
def execute
|
26
|
+
backend.verbose = verbose?
|
27
|
+
|
28
|
+
_execute
|
29
|
+
rescue => error
|
30
|
+
raise if verbose? || $rspec_executing
|
31
|
+
|
32
|
+
$stderr.puts error.to_s
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
|
36
|
+
subcommand('clone',
|
37
|
+
'Clones an existing VM storage as starting point for quickly creating new VMs.') do
|
38
|
+
|
39
|
+
parameter 'NAME', 'name of the new VM storage, ie: new_vm'
|
40
|
+
parameter 'TEMPLATE', 'VM storage to use as a template, ie: pool2/template'
|
41
|
+
parameter 'VERSION', 'template version to clone (aka zfs snapshot), ie: v1.2'
|
42
|
+
|
43
|
+
def _execute
|
44
|
+
storage(template).clone(name, version)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
subcommand('export',
|
49
|
+
'Creates an iSCSI target for the VM storage, with each disk mapped to a LUN.') do
|
50
|
+
parameter 'NAME', 'name of the VM storage, ie: pool1/new_vm'
|
51
|
+
|
52
|
+
def _execute
|
53
|
+
s = storage(name)
|
54
|
+
# Export the storage
|
55
|
+
s.export
|
56
|
+
# Print the IQN to be helpful.
|
57
|
+
puts "Created: #{s.iqn}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
subcommand('hide',
|
62
|
+
'Removes iSCSI target for the VM storage from the system.') do
|
63
|
+
parameter 'NAME', 'name of the VM storage, ie: pool1/new_vm'
|
64
|
+
|
65
|
+
def _execute
|
66
|
+
storage(name).hide
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
subcommand('create',
|
71
|
+
"Creates a VM storage space from scratch. Pass the size for at least one disk volume.") do
|
72
|
+
|
73
|
+
parameter 'NAME', 'name of the VM storage, ie: pool1/new_vm'
|
74
|
+
parameter 'SIZE ...', 'sizes of one or more disks, in ZFS format, ie: 10G'
|
75
|
+
|
76
|
+
def _execute
|
77
|
+
fail "Must create at least one disk." if size_list.empty?
|
78
|
+
|
79
|
+
s = storage(name)
|
80
|
+
|
81
|
+
fail "Must specify absulute storage path (pool/dataset) for creation." \
|
82
|
+
if s.relative_name?
|
83
|
+
|
84
|
+
s.create(size_list)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
subcommand('list',
|
89
|
+
"Lists all vm stores on this machine.") do
|
90
|
+
|
91
|
+
def _execute
|
92
|
+
re_splitter = /\s+/
|
93
|
+
store_list = backend.zfs :list, "-H -oname,#{QemuToolkit::EXPORT_TAG} -t filesystem"
|
94
|
+
store_list.each_line do |line|
|
95
|
+
clean_line = line.chomp.strip
|
96
|
+
last_space = clean_line.rindex(re_splitter)
|
97
|
+
next unless last_space
|
98
|
+
|
99
|
+
name, _, flag = clean_line.rpartition(re_splitter)
|
100
|
+
if flag != '-'
|
101
|
+
printf "%-20s", name
|
102
|
+
puts (flag == 'true') ?
|
103
|
+
storage(name).iqn :
|
104
|
+
'-'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,331 @@
|
|
1
|
+
require 'qemu-toolkit/config'
|
2
|
+
require 'qemu-toolkit/dsl'
|
3
|
+
require 'qemu-toolkit/iscsi_target'
|
4
|
+
|
5
|
+
require 'socket'
|
6
|
+
|
7
|
+
module QemuToolkit
|
8
|
+
# Abstracts a virtual machine on a vm host. This class provides all sorts
|
9
|
+
# of methods that execute administration actions.
|
10
|
+
#
|
11
|
+
class VM
|
12
|
+
class << self # CLASS METHODS
|
13
|
+
# Load all vm descriptions and provide an iterator for them.
|
14
|
+
#
|
15
|
+
def all(backend=nil)
|
16
|
+
Enumerator.new do |yielder|
|
17
|
+
Dir[Config.etc('*.rb')].each do |vm_file|
|
18
|
+
# Load all virtual machines from the given file
|
19
|
+
dsl = DSL::File.new
|
20
|
+
dsl.add_toplevel_target :virtual_machine, lambda { |name|
|
21
|
+
VM.new(backend).tap { |vm| vm.name = name } }
|
22
|
+
|
23
|
+
dsl.load_file(vm_file)
|
24
|
+
|
25
|
+
# Yield them all in turn
|
26
|
+
dsl.objects.each do |vm|
|
27
|
+
yielder << vm
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Access the definition of a single vm.
|
34
|
+
#
|
35
|
+
def [](name, backend=nil)
|
36
|
+
all(backend).find { |vm| vm.name === name }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# VM name
|
41
|
+
attr_accessor :name
|
42
|
+
# iSCSI target iqn and ip address to connect to
|
43
|
+
attr_accessor :iscsi_target
|
44
|
+
# A list of network cards that will be connected to vnics on the host.
|
45
|
+
attr_reader :nics
|
46
|
+
# A list of network configuration statements that will be passed through
|
47
|
+
# to qemu.
|
48
|
+
attr_reader :nets
|
49
|
+
# The number of cpus to configure, defaults to 2.
|
50
|
+
attr_accessor :cpus
|
51
|
+
# Ram in megabytes
|
52
|
+
attr_accessor :ram
|
53
|
+
# VNC display port
|
54
|
+
attr_accessor :vnc_display
|
55
|
+
|
56
|
+
def initialize(backend)
|
57
|
+
@disks = []
|
58
|
+
@drives = []
|
59
|
+
@nics = []
|
60
|
+
@nets = []
|
61
|
+
@cpus = 2
|
62
|
+
@ram = 1024
|
63
|
+
@backend = backend
|
64
|
+
@vnc_display = nil
|
65
|
+
@extra_args = []
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_drive(parameters)
|
69
|
+
@drives << parameters
|
70
|
+
end
|
71
|
+
def add_disk(path)
|
72
|
+
@disks << path
|
73
|
+
end
|
74
|
+
def add_nic(name, parameters)
|
75
|
+
@nics << [name, parameters]
|
76
|
+
end
|
77
|
+
def add_net(type, parameters)
|
78
|
+
@nets << [type, parameters]
|
79
|
+
end
|
80
|
+
def add_extra_arg(argument)
|
81
|
+
@extra_args << argument
|
82
|
+
end
|
83
|
+
|
84
|
+
# Runs the VM using qemu.
|
85
|
+
def start(dryrun, opts={})
|
86
|
+
if dryrun
|
87
|
+
puts command(opts)
|
88
|
+
else
|
89
|
+
# Make sure var/run/qemu-toolkit/VMNAME exists.
|
90
|
+
FileUtils.mkdir_p run_path
|
91
|
+
|
92
|
+
@backend.qemu("vm<#{name}>", command(opts))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns the command that is needed to run this virtual machine. Note
|
97
|
+
# that this also modifies system configuration and is not just a routine
|
98
|
+
# that returns a string.
|
99
|
+
#
|
100
|
+
# @return String command to run the machine
|
101
|
+
#
|
102
|
+
def command opts={}
|
103
|
+
cmd = []
|
104
|
+
cmd << "-name #{name}"
|
105
|
+
cmd << "-m #{ram}"
|
106
|
+
cmd << "-daemonize"
|
107
|
+
cmd << '-nographic'
|
108
|
+
cmd << "-cpu qemu64"
|
109
|
+
cmd << "-smp #{cpus}"
|
110
|
+
cmd << "-no-hpet"
|
111
|
+
cmd << "-enable-kvm"
|
112
|
+
cmd << "-vga cirrus"
|
113
|
+
cmd << "-k de-ch"
|
114
|
+
cmd << "-parallel none"
|
115
|
+
cmd << "-usb"
|
116
|
+
cmd << '-usbdevice tablet'
|
117
|
+
|
118
|
+
# Add disks
|
119
|
+
cmd += disk_options
|
120
|
+
|
121
|
+
# Was an iso image given to boot from?
|
122
|
+
if iso_path=opts[:bootiso]
|
123
|
+
cmd << "-cdrom #{iso_path}"
|
124
|
+
cmd << "-boot order=cd,once=d"
|
125
|
+
else
|
126
|
+
cmd << '-boot order=cd'
|
127
|
+
end
|
128
|
+
|
129
|
+
# Set paths for communication with vm
|
130
|
+
cmd << "-pidfile #{pid_path}"
|
131
|
+
|
132
|
+
cmd << socket_chardev(:monitor, monitor_path)
|
133
|
+
cmd << "-monitor chardev:monitor"
|
134
|
+
|
135
|
+
cmd << socket_chardev(:serial0, run_path('vm.console'))
|
136
|
+
cmd << "-serial chardev:serial0"
|
137
|
+
cmd << socket_chardev(:serial1, run_path('vm.ttyb'))
|
138
|
+
cmd << "-serial chardev:serial1"
|
139
|
+
|
140
|
+
# vnc socket
|
141
|
+
cmd << "-vnc unix:#{run_path('vm.vnc')}"
|
142
|
+
|
143
|
+
# If vnc_display is set, allow configuring a TCP based VNC port:
|
144
|
+
if vnc_display
|
145
|
+
cmd << "-vnc #{vnc_display}"
|
146
|
+
end
|
147
|
+
|
148
|
+
# networking: nic
|
149
|
+
vlan = 0
|
150
|
+
# Look up all existing vnics for this virtual machine
|
151
|
+
vnics = Vnic.for_prefix(name, @backend)
|
152
|
+
|
153
|
+
nics.each do |nic_name, parameters|
|
154
|
+
# All vnics that travel via the given interface (:via)
|
155
|
+
vnics_for_interface = vnics[parameters[:via]] || []
|
156
|
+
|
157
|
+
# Get a vnic that travels via the given interface.
|
158
|
+
vnic = vnics_for_interface.shift ||
|
159
|
+
Vnic.create(name, parameters[:via], @backend)
|
160
|
+
|
161
|
+
cmd << "-net vnic,"+
|
162
|
+
parameter_list(
|
163
|
+
vlan: vlan, name: nic_name,
|
164
|
+
ifname: vnic.vnic_name,
|
165
|
+
macaddr: parameters[:macaddr])
|
166
|
+
cmd << "-net nic,"+
|
167
|
+
parameter_list(
|
168
|
+
vlan: vlan, name: nic_name,
|
169
|
+
model: parameters[:model] || 'virtio',
|
170
|
+
macaddr: parameters[:macaddr])
|
171
|
+
|
172
|
+
vlan += 1
|
173
|
+
end
|
174
|
+
|
175
|
+
# networking: net
|
176
|
+
nets.each do |type, parameters|
|
177
|
+
cmd << "-net #{type},"+
|
178
|
+
parameter_list(parameters)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Extra arguments
|
182
|
+
cmd += @extra_args
|
183
|
+
|
184
|
+
return cmd
|
185
|
+
end
|
186
|
+
def disk_options
|
187
|
+
cmd = []
|
188
|
+
|
189
|
+
if @disks.empty? && !iscsi_target && @drives.empty?
|
190
|
+
raise "No disks defined, can't run."
|
191
|
+
end
|
192
|
+
|
193
|
+
disk_index = 0
|
194
|
+
if iscsi_target
|
195
|
+
target = produce_target(*iscsi_target)
|
196
|
+
target.ensure_exists
|
197
|
+
|
198
|
+
target.disks.each do |device|
|
199
|
+
params = {
|
200
|
+
file: device,
|
201
|
+
if: 'virtio',
|
202
|
+
index: disk_index,
|
203
|
+
media: 'disk'
|
204
|
+
}
|
205
|
+
params[:boot] = 'on' if disk_index == 0
|
206
|
+
cmd << "-drive " + parameter_list(params)
|
207
|
+
|
208
|
+
disk_index += 1
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
@disks.each do |path|
|
213
|
+
cmd << "-drive file=#{path},if=virtio,index=#{disk_index},"+
|
214
|
+
"media=disk,boot=on"
|
215
|
+
disk_index += 1
|
216
|
+
end
|
217
|
+
|
218
|
+
@drives.each do |drive_options|
|
219
|
+
cmd << "-drive " +
|
220
|
+
parameter_list(drive_options.merge(index: disk_index))
|
221
|
+
disk_index += 1
|
222
|
+
end
|
223
|
+
|
224
|
+
return cmd
|
225
|
+
end
|
226
|
+
|
227
|
+
# Connects the current terminal to the given socket. Available sockets
|
228
|
+
# include :monitor, :vnc, :console, :ttyb.
|
229
|
+
#
|
230
|
+
def connect(socket)
|
231
|
+
socket_path = run_path("vm.#{socket}")
|
232
|
+
cmd = "socat stdio unix-connect:#{socket_path}"
|
233
|
+
|
234
|
+
exec cmd
|
235
|
+
end
|
236
|
+
|
237
|
+
# Kills the vm the hard way.
|
238
|
+
#
|
239
|
+
def kill
|
240
|
+
run_cmd "kill #{pid}"
|
241
|
+
end
|
242
|
+
|
243
|
+
# Sends a shutdown command via the monitor socket of the virtual machine.
|
244
|
+
#
|
245
|
+
def shutdown
|
246
|
+
monitor_cmd 'system_powerdown'
|
247
|
+
end
|
248
|
+
|
249
|
+
# Returns an ISCSITarget for host and port.
|
250
|
+
#
|
251
|
+
def produce_target(host, port)
|
252
|
+
ISCSITarget.new(host, port, @backend)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns true if the virtual machine seems to be currently running.
|
256
|
+
#
|
257
|
+
def running?
|
258
|
+
if File.exist?(pid_path)
|
259
|
+
# Prod the process using kill. This will not actually kill the
|
260
|
+
# process!
|
261
|
+
begin
|
262
|
+
Process.kill(0, pid)
|
263
|
+
rescue Errno::ESRCH
|
264
|
+
# When this point is reached, the process doesn't exist.
|
265
|
+
return false
|
266
|
+
end
|
267
|
+
|
268
|
+
return true
|
269
|
+
end
|
270
|
+
|
271
|
+
return false
|
272
|
+
end
|
273
|
+
|
274
|
+
# Attempts to read and return the pid of the running VM process.
|
275
|
+
#
|
276
|
+
def pid
|
277
|
+
Integer(File.read(pid_path).lines.first.chomp)
|
278
|
+
end
|
279
|
+
|
280
|
+
private
|
281
|
+
def monitor_cmd(cmd)
|
282
|
+
socket = UNIXSocket.new(monitor_path)
|
283
|
+
socket.puts cmd
|
284
|
+
socket.close
|
285
|
+
end
|
286
|
+
|
287
|
+
def socket_chardev(name, path)
|
288
|
+
"-chardev socket,id=#{name},path=#{path},server,nowait"
|
289
|
+
end
|
290
|
+
|
291
|
+
# Formats a parameter list as key=value,key=value
|
292
|
+
#
|
293
|
+
def parameter_list(parameters)
|
294
|
+
parameters.
|
295
|
+
map { |k,v| "#{k}=#{v}" }.
|
296
|
+
join(',')
|
297
|
+
end
|
298
|
+
|
299
|
+
# Returns the path below /var/run (usually) that contains runtime files
|
300
|
+
# for the virtual machine.
|
301
|
+
#
|
302
|
+
def run_path(*args)
|
303
|
+
Config.var_run(name, *args)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns the file path of the vm pid file.
|
307
|
+
#
|
308
|
+
def pid_path
|
309
|
+
run_path 'vm.pid'
|
310
|
+
end
|
311
|
+
|
312
|
+
# Returns the file path of the monitor socket (unix socket below /var/run)
|
313
|
+
# usually. )
|
314
|
+
#
|
315
|
+
def monitor_path
|
316
|
+
run_path 'vm.monitor'
|
317
|
+
end
|
318
|
+
|
319
|
+
# Runs a command and returns its stdout. This raises an error if the
|
320
|
+
# command doesn't exit with a status of 0.
|
321
|
+
#
|
322
|
+
def run_cmd(*args)
|
323
|
+
cmd = args.join(' ')
|
324
|
+
ret = %x(#{cmd})
|
325
|
+
|
326
|
+
raise "Execution error: #{cmd}." unless $?.success?
|
327
|
+
|
328
|
+
ret
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module QemuToolkit
|
2
|
+
class VMStorage
|
3
|
+
def initialize(name, backend)
|
4
|
+
@name = name
|
5
|
+
@backend = backend
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :backend
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def create(sizes)
|
12
|
+
backend.zfs :create, "-o #{QemuToolkit::EXPORT_TAG}=false", name
|
13
|
+
sizes.each_with_index do |size, idx|
|
14
|
+
backend.zfs :create, "-V #{size}", name + "/disk#{idx+1}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Clones this vm storage to the given target name. If target_name is
|
19
|
+
# given without pool part, clone will live in the same pool as its parent
|
20
|
+
# vm storage. The version argument specifies which version should be
|
21
|
+
# cloned and must be a recursive snapshot on the parent vm.
|
22
|
+
#
|
23
|
+
def clone(target_name, version)
|
24
|
+
raise ArgumentError, "Must specify the dataset path for cloning." \
|
25
|
+
if relative_name?
|
26
|
+
|
27
|
+
path, vm_name = split
|
28
|
+
target_path = join path, target_name
|
29
|
+
backend.zfs :clone, "#@name@#{version}", target_path
|
30
|
+
|
31
|
+
backend.disks(name).each do |disk_path|
|
32
|
+
disk_name = subtract @name, disk_path
|
33
|
+
target_disk_name = join path, target_name, disk_name
|
34
|
+
backend.zfs :clone, "#{disk_path}@#{version}", target_disk_name
|
35
|
+
end
|
36
|
+
|
37
|
+
# Mark the dataset as hidden
|
38
|
+
backend.zfs :set, "#{QemuToolkit::EXPORT_TAG}=false", join(path, target_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Exports all disks of a virtual machine (called 'diskN' below the main
|
42
|
+
# dataset) as LUNs below a single iqn for the machine.
|
43
|
+
#
|
44
|
+
def export
|
45
|
+
fail "VM storage #{name} does not exist." unless exist?
|
46
|
+
fail "VM storage #{name} is already exported." if exported?
|
47
|
+
|
48
|
+
path, vm_name = split
|
49
|
+
|
50
|
+
backend.stmfadm 'create-tg', vm_name
|
51
|
+
|
52
|
+
backend.disks(name).each do |disk_path|
|
53
|
+
output = backend.stmfadm 'create-lu', "/dev/zvol/rdsk/"+disk_path
|
54
|
+
|
55
|
+
md=output.match /Logical unit created: ([0-9A-F]+)/
|
56
|
+
raise "Could not parse created LU. (#{output.inspect})" unless md
|
57
|
+
|
58
|
+
backend.stmfadm 'add-view', "-t #{vm_name}", md[1]
|
59
|
+
end
|
60
|
+
|
61
|
+
backend.stmfadm 'add-tg-member', "-g #{vm_name}", iqn
|
62
|
+
backend.itadm 'create-target', "-n #{iqn}", '-t frontend'
|
63
|
+
|
64
|
+
# Mark the dataset as exported
|
65
|
+
backend.zfs :set, "#{QemuToolkit::EXPORT_TAG}=true", name
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns true if the storage exists and is exported currently. Returns
|
69
|
+
# false if the storage exists and is not exported. In all other cases
|
70
|
+
# this method returns nil.
|
71
|
+
#
|
72
|
+
def exported?
|
73
|
+
case (export_tag || '').chomp
|
74
|
+
when 'true'
|
75
|
+
return true
|
76
|
+
when 'false'
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
|
80
|
+
return nil
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns true if the storage seems to exist and be a valid vm storage.
|
84
|
+
#
|
85
|
+
def exist?
|
86
|
+
! export_tag.nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
# Hides the vm storage from iSCSI.
|
90
|
+
#
|
91
|
+
def hide
|
92
|
+
fail "VM storage #{name} does not exist." unless exist?
|
93
|
+
fail "VM storage #{name} is already hidden." unless exported?
|
94
|
+
|
95
|
+
path, vm_name = split
|
96
|
+
|
97
|
+
raise "Cannot find an exported dataset named #{name}. " \
|
98
|
+
unless exported?
|
99
|
+
|
100
|
+
backend.stmfadm 'offline-target', iqn
|
101
|
+
backend.itadm 'delete-target', iqn
|
102
|
+
|
103
|
+
# Parse existing lus, look for vm_name/diskN
|
104
|
+
lus = backend.stmfadm 'list-lu', '-v'
|
105
|
+
last_lu = nil
|
106
|
+
lus.each_line do |line|
|
107
|
+
if md=line.match(/LU Name: ([0-9A-F]+)/)
|
108
|
+
last_lu = md[1]
|
109
|
+
end
|
110
|
+
if line.include?('Data File') &&
|
111
|
+
line.include?('/dev/zvol/rdsk') &&
|
112
|
+
line.match(%r(/#{Regexp.escape(vm_name)}/disk\d+))
|
113
|
+
|
114
|
+
backend.stmfadm 'delete-lu', last_lu
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
backend.stmfadm 'delete-tg', vm_name
|
119
|
+
|
120
|
+
# Mark the dataset as hidden
|
121
|
+
backend.zfs :set, "#{QemuToolkit::EXPORT_TAG}=false", name
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns whether the name used to construct this instance is relative
|
125
|
+
# or absolute. A relative name identifies a storage within its pool,
|
126
|
+
# an absolute name identifies it within the whole system.
|
127
|
+
#
|
128
|
+
# VMStorage.new('foo').relative_name? # => true
|
129
|
+
# VMStorage.new('b1/foo').relative_name? # => false
|
130
|
+
#
|
131
|
+
def relative_name?
|
132
|
+
@name.index('/') == nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def iqn
|
136
|
+
_, n = split
|
137
|
+
"iqn.2012-01.com.qemu-toolkit:#{n}"
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
# Returns the (cached) value of QemuToolkit::EXPORT_TAG of this storage.
|
143
|
+
# This does nothing more than execute
|
144
|
+
# zfs get -Ho value #{QemuToolkit::EXPORT_TAG} NAME
|
145
|
+
# and handle an exception by returning nil.
|
146
|
+
#
|
147
|
+
def export_tag
|
148
|
+
@export_tag ||= begin
|
149
|
+
backend.zfs :get, "-Ho value #{QemuToolkit::EXPORT_TAG}", name
|
150
|
+
rescue
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns a pair of dataset path and dataset name for the vm storage.
|
156
|
+
#
|
157
|
+
# @example
|
158
|
+
# VMStorage.new('foo/bar/baz').split
|
159
|
+
# # => ['foo/bar', 'baz']
|
160
|
+
#
|
161
|
+
def split
|
162
|
+
File.split(@name)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Joins parts of a vm storage name.
|
166
|
+
#
|
167
|
+
def join(*args)
|
168
|
+
File.join(*args)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Subtracts a prefix from a given string.
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# subtract 'foo', 'foo/bar' # => '/bar'
|
175
|
+
def subtract(prefix, string)
|
176
|
+
raise ArgumentError unless string.start_with?(prefix)
|
177
|
+
string[prefix.size..-1]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'clamp'
|
2
|
+
|
3
|
+
require 'qemu-toolkit/local_disk_set'
|
4
|
+
|
5
|
+
module QemuToolkit
|
6
|
+
class Vmadm < Clamp::Command
|
7
|
+
|
8
|
+
option ['-v', '--verbose'], :flag, 'be chatty'
|
9
|
+
option ['-n', '--dry-run'], :flag,
|
10
|
+
"don't execute commands, instead just print them"
|
11
|
+
|
12
|
+
option '--vmpath', "VMPATH",
|
13
|
+
"path to vm descriptions", default: '/etc/qemu-toolkit'
|
14
|
+
option '--varrun', 'VARRUN',
|
15
|
+
'path to runtime files', default: '/var/run/qemu-toolkit'
|
16
|
+
|
17
|
+
# Command backend to use during the processing of subcommands.
|
18
|
+
#
|
19
|
+
def backend
|
20
|
+
Config.backend
|
21
|
+
end
|
22
|
+
|
23
|
+
# Main execute method - delegates to _execute in the subcommands. This
|
24
|
+
# handles transforming Ruby errors into simple shell errors.
|
25
|
+
#
|
26
|
+
def execute
|
27
|
+
backend.verbose = verbose?
|
28
|
+
|
29
|
+
Config.etc = vmpath
|
30
|
+
Config.var_run = varrun
|
31
|
+
|
32
|
+
fail NotImplementedError, "Missing subcommand." unless respond_to?(:_execute)
|
33
|
+
_execute
|
34
|
+
rescue => error
|
35
|
+
raise if verbose? || $rspec_executing
|
36
|
+
|
37
|
+
$stderr.puts error.to_s
|
38
|
+
exit 1
|
39
|
+
end
|
40
|
+
|
41
|
+
subcommand('list',
|
42
|
+
'Lists all virtual machines on this system') do
|
43
|
+
def _execute
|
44
|
+
VM.all(backend).each do |vm|
|
45
|
+
printf "%-20s", vm.name
|
46
|
+
printf " %5s", vm.running? ? vm.pid : 'off'
|
47
|
+
puts
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# subcommand('random-mac',
|
53
|
+
# 'Generates a random MAC address') do
|
54
|
+
# def _execute
|
55
|
+
# puts random_mac_address
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
|
59
|
+
subcommand('create',
|
60
|
+
'Creates a configuration file for the VM from a template, filling in plausible values.') do
|
61
|
+
|
62
|
+
parameter 'NAME', 'name of the virtual machine'
|
63
|
+
|
64
|
+
def _execute
|
65
|
+
if VM[name]
|
66
|
+
puts "Machine already exists."
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
|
70
|
+
File.write(
|
71
|
+
Config.etc("#{name}.rb"),
|
72
|
+
vm_template(name))
|
73
|
+
|
74
|
+
FileUtils.mkdir(Config.var_run(name))
|
75
|
+
end
|
76
|
+
|
77
|
+
def vm_template(name)
|
78
|
+
local_disks = StringIO.new
|
79
|
+
|
80
|
+
disk_sets = LocalDiskSet.for(name, backend)
|
81
|
+
disk_sets.each do |set|
|
82
|
+
local_disks.puts " # Disks for storage at #{set.name}"
|
83
|
+
set.each_disk do |dev_path|
|
84
|
+
local_disks.puts " disk '#{dev_path}'"
|
85
|
+
end
|
86
|
+
local_disks.puts
|
87
|
+
end
|
88
|
+
|
89
|
+
%Q(virtual_machine "#{name}" do
|
90
|
+
# Block device setup
|
91
|
+
#
|
92
|
+
# Either configure a remote disk (via iSCSI):
|
93
|
+
# iscsi_target 'iqn.2012-01.com.qemu-toolkit:#{name}', "10.40.0.1"
|
94
|
+
#
|
95
|
+
# Or a local disk, like a zvol for example:
|
96
|
+
# disk /dev/zvol/dsk/pool/#{name}/disk1
|
97
|
+
#{local_disks.string}
|
98
|
+
|
99
|
+
# Network configuration
|
100
|
+
# nic 'eth0',
|
101
|
+
# macaddr: '#{random_mac_address}',
|
102
|
+
# via: 'igbX'
|
103
|
+
end
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
subcommand('start',
|
109
|
+
'Starts the virtual machine and daemonizes it.') do
|
110
|
+
|
111
|
+
parameter 'NAME', 'name of the virtual machine'
|
112
|
+
option '--bootiso', 'BOOTISO', 'boots this ISO instead of disk0'
|
113
|
+
|
114
|
+
def _execute
|
115
|
+
vm(name).start(dry_run?, bootiso: bootiso)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
subcommand('monitor',
|
120
|
+
'Enter QEMU monitor interactively for given VM.') do
|
121
|
+
parameter 'NAME', 'name of the virtual machine'
|
122
|
+
|
123
|
+
def _execute
|
124
|
+
vm(name).connect(:monitor)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
subcommand('shutdown',
|
128
|
+
'Shuts the VM down by issuing a system/powerdown event.') do
|
129
|
+
parameter 'NAME', 'name of the virtual machine'
|
130
|
+
|
131
|
+
def _execute
|
132
|
+
vm(name).shutdown
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
subcommand('kill',
|
137
|
+
'Tries to kill the VM using the kill command.') do
|
138
|
+
parameter 'NAME', 'name of the virtual machine'
|
139
|
+
|
140
|
+
def _execute
|
141
|
+
vm(name).kill
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
subcommand('vnc',
|
146
|
+
'Connect VM VNC server to standard IO. (use ssvnc to connect)') do
|
147
|
+
parameter 'NAME', 'name of the virtual machine'
|
148
|
+
|
149
|
+
def _execute
|
150
|
+
vm(name).connect(:vnc)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
subcommand('console',
|
155
|
+
'Opens serial console to VM. This only works if you configure your VM accordingly.') do
|
156
|
+
parameter 'NAME', 'name of the virtual machine'
|
157
|
+
|
158
|
+
def _execute
|
159
|
+
vm(name).connect(:console)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def random_mac_address
|
164
|
+
# Please see this discussion if improving this:
|
165
|
+
# http://stackoverflow.com/questions/8484877/mac-address-generator-in-python
|
166
|
+
mac = [ 0x00, 0x24, 0x81,
|
167
|
+
rand(0x7f),
|
168
|
+
rand(0xff),
|
169
|
+
rand(0xff) ]
|
170
|
+
mac.map { |e| e.to_s(16) }.join(':')
|
171
|
+
end
|
172
|
+
def vm(name)
|
173
|
+
vm = VM[name, backend]
|
174
|
+
unless vm
|
175
|
+
puts "No virtual machine by the name '#{name}' found."
|
176
|
+
exit 1
|
177
|
+
end
|
178
|
+
|
179
|
+
vm
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module QemuToolkit
|
2
|
+
class Vnic
|
3
|
+
class << self
|
4
|
+
def for_prefix(prefix, background)
|
5
|
+
vnics = Hash.new { |h,k| h[k] = Array.new; }
|
6
|
+
links = background.dladm 'show-vnic', '-po link,over,vid'
|
7
|
+
|
8
|
+
links.each_line do |line|
|
9
|
+
next unless line.start_with?(prefix)
|
10
|
+
link, over, vid = line.chomp.split(':')
|
11
|
+
|
12
|
+
# Assumes that vid 0 is always the 'no vlan' VLAN
|
13
|
+
over = "#{over}:#{vid}" if vid.to_i > 0
|
14
|
+
|
15
|
+
if md=link.match(/^(?<vm>.*)_(?<link_no>\d+)$/)
|
16
|
+
vnics[over] << Vnic.new(md[:vm], Integer(md[:link_no]), over)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
vnics
|
21
|
+
end
|
22
|
+
|
23
|
+
def create(prefix, over, backend)
|
24
|
+
# Retrieve links that exist for this prefix and this over interface
|
25
|
+
vnics = for_prefix(prefix, backend).values.flatten
|
26
|
+
next_vnic_number = (vnics.map(&:number).max || 0) + 1
|
27
|
+
|
28
|
+
new(prefix, next_vnic_number, over).tap { |o|
|
29
|
+
o.create(backend) }
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(prefix, number, over)
|
35
|
+
@prefix, @number, @over = prefix, number, over
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :prefix
|
39
|
+
attr_reader :number
|
40
|
+
attr_reader :over
|
41
|
+
|
42
|
+
def ==(other)
|
43
|
+
self.prefix == other.prefix &&
|
44
|
+
self.number == other.number &&
|
45
|
+
self.over == other.over
|
46
|
+
end
|
47
|
+
|
48
|
+
def create backend
|
49
|
+
if over.index(':')
|
50
|
+
iface, vlan = over.split(':')
|
51
|
+
backend.dladm 'create-vnic', "-l #{iface} -v #{vlan}", vnic_name
|
52
|
+
else
|
53
|
+
backend.dladm 'create-vnic', "-l #{over}", vnic_name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def vnic_name
|
58
|
+
"#{prefix}_#{number}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/qemu-toolkit.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
module QemuToolkit
|
3
|
+
EXPORT_TAG = 'qemu_toolkit:export'
|
4
|
+
|
5
|
+
module Backend; end
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'qemu-toolkit/config'
|
9
|
+
require 'qemu-toolkit/vm'
|
10
|
+
require 'qemu-toolkit/vm_storage'
|
11
|
+
require 'qemu-toolkit/backend/illumos'
|
12
|
+
require 'qemu-toolkit/vnic'
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: qemu-toolkit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.18
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kaspar Schiess
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: clamp
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description:
|
31
|
+
email: kaspar.schiess@technologyastronauts.ch
|
32
|
+
executables:
|
33
|
+
- vmadm
|
34
|
+
- storadm
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files:
|
37
|
+
- README
|
38
|
+
files:
|
39
|
+
- LICENSE
|
40
|
+
- README
|
41
|
+
- lib/qemu-toolkit/backend/illumos.rb
|
42
|
+
- lib/qemu-toolkit/config.rb
|
43
|
+
- lib/qemu-toolkit/dsl.rb
|
44
|
+
- lib/qemu-toolkit/iscsi_target.rb
|
45
|
+
- lib/qemu-toolkit/local_disk_set.rb
|
46
|
+
- lib/qemu-toolkit/storadm.rb
|
47
|
+
- lib/qemu-toolkit/vm.rb
|
48
|
+
- lib/qemu-toolkit/vm_storage.rb
|
49
|
+
- lib/qemu-toolkit/vmadm.rb
|
50
|
+
- lib/qemu-toolkit/vnic.rb
|
51
|
+
- lib/qemu-toolkit.rb
|
52
|
+
- bin/storadm
|
53
|
+
- bin/vmadm
|
54
|
+
- bin/vmconnect
|
55
|
+
homepage:
|
56
|
+
licenses: []
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options:
|
59
|
+
- --main
|
60
|
+
- README
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.8.24
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: Manages QEMU kvm virtual machines on Illumos hosts.
|
81
|
+
test_files: []
|