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 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
@@ -0,0 +1,7 @@
1
+ SYNOPSIS
2
+
3
+ qemu-toolkit is a small set of scripts to control QEMU kvm virtualised
4
+ machines on a set of illumos hosts.
5
+
6
+ Have a look at 'storadm' and 'vmadm' - inline help will show you the
7
+ possibilities.
data/bin/storadm ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def root(*args)
4
+ File.expand_path(
5
+ File.join(File.dirname(__FILE__), '..',
6
+ *args))
7
+ end
8
+
9
+ $:.unshift root('lib')
10
+
11
+ require 'qemu-toolkit'
12
+
13
+ require 'qemu-toolkit/storadm'
14
+ QemuToolkit::Storadm.run
data/bin/vmadm ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def root(*args)
4
+ File.expand_path(
5
+ File.join(File.dirname(__FILE__), '..',
6
+ *args))
7
+ end
8
+
9
+ $:.unshift root('lib')
10
+
11
+ require 'qemu-toolkit'
12
+
13
+ require 'qemu-toolkit/vmadm'
14
+ QemuToolkit::Vmadm.run
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
@@ -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: []