vmit 0.0.3

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.
@@ -0,0 +1,23 @@
1
+ #
2
+ # Copyright (C) 2013 Duncan Mac-Vicar P. <dmacvicar@suse.de>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in
6
+ # the Software without restriction, including without limitation the rights to
7
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ # the Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ #
21
+ module Vmit
22
+ VERSION = "0.0.3"
23
+ end
data/lib/vmit/vfs.rb ADDED
@@ -0,0 +1,210 @@
1
+ #
2
+ # Copyright (C) 2013 Duncan Mac-Vicar P. <dmacvicar@suse.de>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in
6
+ # the Software without restriction, including without limitation the rights to
7
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ # the Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ #
21
+ require 'abstract_method'
22
+ require 'cheetah'
23
+ require 'uri'
24
+ require 'open-uri'
25
+ require 'progressbar'
26
+ require 'tempfile'
27
+
28
+ module Vmit
29
+
30
+ module VFS
31
+
32
+ # Opens a location
33
+ def self.from(location, *rest, &block)
34
+ [ISO, URI, Local].each do |handler|
35
+ if handler.accept?(location)
36
+ return handler.from(location)
37
+ end
38
+ end
39
+ raise ArgumentError.new("#{location} not supported")
40
+ end
41
+
42
+ class Handler
43
+ # alias for new
44
+ def self.from(*args)
45
+ self.new(*args)
46
+ end
47
+ end
48
+
49
+ class URI < Handler
50
+
51
+ # Whether this handler accepts the
52
+ # given location
53
+ #
54
+ # @param uri [URI,String] location
55
+ def self.accept?(location)
56
+ uri = case location
57
+ when ::URI then location
58
+ else ::URI.parse(location.to_s)
59
+ end
60
+ ['http', 'ftp'].include?(uri.scheme)
61
+ end
62
+
63
+ # @param [String] base_url Base location
64
+ def initialize(location)
65
+ @base_uri = case location
66
+ when ::URI then location
67
+ else ::URI.parse(location.to_s)
68
+ end
69
+ end
70
+
71
+ def self.open(loc, *rest, &block)
72
+ uri = case loc
73
+ when ::URI then loc
74
+ else ::URI.parse(loc.to_s)
75
+ end
76
+ unless accept?(uri)
77
+ raise ArgumentError.new('Only HTTP/FTP supported')
78
+ end
79
+ @pbar = nil
80
+ @filename = File.basename(uri.path)
81
+ ret = OpenURI.open_uri(uri.to_s,
82
+ :content_length_proc => lambda { |t|
83
+ if t && 0 < t
84
+ @pbar = ProgressBar.new(@filename, t)
85
+ @pbar.file_transfer_mode
86
+ end
87
+ },
88
+ :progress_proc => lambda { |s|
89
+ @pbar.set s if @pbar
90
+ }, &block)
91
+ @pbar = nil
92
+ # So that the progress bar line get overwriten
93
+ STDOUT.print "\r"
94
+ STDOUT.flush
95
+ ret
96
+ end
97
+ # Open a filename relative to the
98
+ # base location.
99
+ #
100
+ # @param [String] loc Location to open.
101
+ # If a base URI was given for HTTP then
102
+ # the path will be relative to that
103
+ def open(loc, *rest, &block)
104
+ uri = @base_uri.clone
105
+ uri.path = File.join(uri.path, loc.to_s)
106
+
107
+ Vmit::VFS::URI.open(uri, *rest, &block)
108
+ end
109
+ end
110
+
111
+ class ISO < Handler
112
+
113
+ attr_reader :iso_file
114
+
115
+ # Whether this handler accepts the
116
+ # given location.
117
+ #
118
+ # @param uri [URI,String] location
119
+ def self.accept?(location)
120
+ uri = case location
121
+ when ::URI then location
122
+ else ::URI.parse(location)
123
+ end
124
+
125
+ # either an iso:// url or a local file
126
+ unless (uri.scheme == 'iso' || uri.scheme.nil?)
127
+ return false
128
+ end
129
+ return false unless File.exist?(uri.path)
130
+ File.open(uri.path) do |f|
131
+ f.seek(0x8001)
132
+ return true if f.read(5) == 'CD001'
133
+ end
134
+ false
135
+ end
136
+
137
+ # Creates a ISO handler for +iso+
138
+ # @param [URI, String] iso ISO file
139
+ def initialize(location, *rest)
140
+ raise ArgumentError.new(location) unless self.class.accept?(location)
141
+ path = case location
142
+ when ::URI then location.path
143
+ else ::URI.parse(location).path
144
+ end
145
+ @iso_file = path
146
+ end
147
+
148
+ # Takes a iso URI on the form
149
+ # iso:///path/tothe/file.iso?path=/file/to/get
150
+ #
151
+ # @param [URI, String] uri ISO file and path as query string
152
+ def self.open(location)
153
+ uri = case location
154
+ when ::URI then location
155
+ else ::URI.parse(location)
156
+ end
157
+ handler = self.new(uri)
158
+ query = Hash[*uri.query.split('&').map {|p| p.split('=')}.flatten]
159
+ unless query.has_key?("path")
160
+ raise ArgumentError.new("#{uri}: missing path in query string")
161
+ end
162
+ handler.open(query["path"])
163
+ end
164
+
165
+ # Takes a path relative to +iso_file+
166
+ # @see iso_file
167
+ def open(name, *rest)
168
+ index = Cheetah.run('isoinfo', '-f', '-R', '-i', iso_file, :stdout => :capture)
169
+ files = index.each_line.to_a.map(&:strip)
170
+ raise Errno::ENOENT.new(name) if not files.include?(name)
171
+ tmp = Tempfile.new('vmit-vfs-iso-')
172
+ Cheetah.run('isoinfo', '-R', '-i', iso_file, '-x', name, :stdout => tmp)
173
+ tmp.close
174
+ tmp.open
175
+ if block_given?
176
+ yield tmp
177
+ end
178
+ tmp
179
+ end
180
+ end
181
+
182
+ class Local < Handler
183
+
184
+ # Whether this handler accepts the
185
+ # given location
186
+ #
187
+ # @param uri [URI,String] location
188
+ def self.accept?(location)
189
+ File.directory?(location.to_s)
190
+ end
191
+
192
+ def initialize(base_path=nil)
193
+ @base_path = base_path
194
+ @base_path ||= '/'
195
+ unless File.exist?(@base_path)
196
+ raise Errno::ENOENT.new(@base_path)
197
+ end
198
+ end
199
+
200
+ def self.open(dir, *rest, &block)
201
+ self.new(dir).open(name, *rest, &block)
202
+ end
203
+
204
+ def open(name, *rest, &block)
205
+ Kernel.open(File.join(@base_path, name), *rest, &block)
206
+ end
207
+ end
208
+
209
+ end
210
+ end
@@ -0,0 +1,299 @@
1
+ #
2
+ # Copyright (C) 2013 Duncan Mac-Vicar P. <dmacvicar@suse.de>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in
6
+ # the Software without restriction, including without limitation the rights to
7
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ # the Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ #
21
+ require 'cheetah'
22
+ require 'drb'
23
+ require 'fileutils'
24
+ require 'stringio'
25
+ require 'yaml'
26
+
27
+ require 'vmit/utils'
28
+
29
+ module Vmit
30
+
31
+ class VirtualMachine
32
+
33
+ attr_accessor :work_dir
34
+
35
+ VM_DEFAULTS = {
36
+ :memory => '1G',
37
+ }
38
+ SWITCH = 'br0'
39
+
40
+ # Accessor to current options
41
+ def [](key)
42
+ @opts[key]
43
+ end
44
+
45
+ def config_file
46
+ File.join(work_dir, 'config.yml')
47
+ end
48
+
49
+ def initialize(work_dir)
50
+ @pidfile = PidFile.new(:piddir => work_dir, :pidfile => "vmit.pid")
51
+ @work_dir = work_dir
52
+
53
+ @opts = {}
54
+ @opts.merge!(VM_DEFAULTS)
55
+
56
+ if File.exist?(config_file)
57
+ @opts.merge!(YAML::load(File.open(config_file)))
58
+ end
59
+
60
+ # By default the following keys are useful to be
61
+ # generated if they don't exist and then use the
62
+ # same in the future UNLESS they are
63
+ # overriden with vmit run
64
+ if not @opts.has_key?(:mac_address)
65
+ @opts[:mac_address] = Vmit::Utils.random_mac_address
66
+ end
67
+
68
+ if not @opts.has_key?(:uuid)
69
+ @opts[:uuid] = File.read("/proc/sys/kernel/random/uuid").strip
70
+ end
71
+
72
+ @network = if @opts.has_key?(:network)
73
+ Network.create(@opts[:network])
74
+ else
75
+ Vmit.logger.info 'No network selected. Using default.'
76
+ Network.default
77
+ end
78
+ Vmit.logger.info "Network: #{@network}"
79
+ end
80
+
81
+ # @return [Array,<String>] sorted list of snapshots
82
+ def disk_images
83
+ Dir.glob(File.join(work_dir, '*.qcow2')).sort do |a,b|
84
+ File.ctime(a) <=> File.ctime(b)
85
+ end
86
+ end
87
+
88
+ # Takes a disk snapshot
89
+ def disk_snapshot!
90
+ disk_image_shift!
91
+ end
92
+
93
+ def disk_image_init!(opts={})
94
+ disk_image_shift!(opts)
95
+ end
96
+
97
+ DISK_INIT_DEFAULTS = {:disk_size => '10G'}
98
+
99
+ # Shifts an image, adding a new one using the
100
+ # previous newest one as backing file
101
+ #
102
+ # @param [Hash] opts options for the disk shift
103
+ # @option opts [String] :disk_size Disk size. Only used for image creation
104
+ def disk_image_shift!(opts={})
105
+ runtime_opts = DISK_INIT_DEFAULTS.merge(opts)
106
+
107
+ file_name = File.join(work_dir, "sda-#{Time.now.to_i}.qcow2")
108
+ images = disk_images
109
+
110
+ file_name = 'base.qcow2' if images.size == 0
111
+
112
+ args = ['/usr/bin/qemu-img', 'create',
113
+ '-f', "qcow2"]
114
+
115
+ if not images.empty?
116
+ args << '-b'
117
+ args << images.last
118
+ end
119
+ args << file_name
120
+ if images.empty?
121
+ args << runtime_opts[:disk_size]
122
+ end
123
+
124
+ Vmit.logger.info "Shifted image. Current is '#{file_name}'."
125
+ Cheetah.run(*args)
126
+ end
127
+
128
+ # Rolls back to the previous snapshot
129
+ def disk_rollback!
130
+ images = disk_images
131
+
132
+ return if images.empty?
133
+
134
+ if images.size == 1
135
+ Vmit.logger.fatal "Only the base snapshot left!"
136
+ return
137
+ end
138
+ Vmit.logger.info "Removing #{images.last}"
139
+ FileUtils.rm(images.last)
140
+ end
141
+
142
+ # @returns [String] The latest COW snapshot
143
+ def current_image
144
+ curr = disk_images.last
145
+ raise "No hard disk image available" if curr.nil?
146
+ curr
147
+ end
148
+
149
+ def options
150
+ @opts
151
+ end
152
+
153
+ # @return [Hash] Config of the virtual machine
154
+ # This is all options plus the defaults
155
+ def config
156
+ VM_DEFAULTS.merge(@opts)
157
+ end
158
+
159
+ # @return [Hash] config that differs from default
160
+ # and therefore relevant to be persisted in config.yml
161
+ def relevant_config
162
+ config.diff(VM_DEFAULTS)
163
+ end
164
+
165
+ # Saves the configuration in config.yml
166
+ def save_config!
167
+ if not relevant_config.empty?
168
+ Vmit.logger.info "Writing config.yml..."
169
+ File.open(config_file, 'w') do |f|
170
+ f.write(relevant_config.to_yaml)
171
+ end
172
+ end
173
+ end
174
+
175
+ def to_s
176
+ config.to_s
177
+ end
178
+
179
+ BINDIR = File.join(File.dirname(__FILE__), '../../bin')
180
+
181
+ # Starts the virtual machine
182
+ #
183
+ # @param [Hash] runtime_opts Runtime options
184
+ # @option runtime_opts [String] :cdrom CDROM image
185
+ # @option runtime_opts [String] :kernel Kernel image
186
+ # @option runtime_opts [String] :initrd initrd image
187
+ # @option runtime_opts [String] :append Kernel command line
188
+ # @option runtime_opts [String] :floppy Floppy (image or directory)
189
+ def run(runtime_opts)
190
+ Vmit.logger.info "Starting VM..."
191
+ # Don't overwrite @opts so that
192
+ # run can be called various times
193
+ opts = {}
194
+ opts.merge!(@opts)
195
+ opts.merge!(runtime_opts)
196
+
197
+ config.each do |k,v|
198
+ Vmit.logger.info " => #{k} : #{v}"
199
+ end
200
+
201
+ begin
202
+ # HACK, will be replaced by a better config system
203
+ use_virtio = ! File.exist?(File.join(work_dir, '.disable-virtio'))
204
+
205
+ ifup = File.expand_path(File.join(BINDIR, 'vmit-ifup'))
206
+ ifdown = File.expand_path(File.join(BINDIR, 'vmit-ifdown'))
207
+
208
+ args = ['/usr/bin/qemu-kvm', '-boot', 'c',
209
+ '-m', "#{opts[:memory]}",
210
+ '-pidfile', File.join(work_dir, 'qemu.pid')]
211
+
212
+ if use_virtio
213
+ args << '-drive'
214
+ args << "file=#{current_image},if=virtio"
215
+
216
+ args << '-netdev'
217
+ args << "type=tap,script=#{ifup},downscript=#{ifdown},id=vnet0"
218
+ args << '-device'
219
+ args << "virtio-net-pci,netdev=vnet0,mac=#{opts[:mac_address]}"
220
+ else
221
+ args << '-drive'
222
+ args << "file=#{current_image}"
223
+
224
+ args << '-net'
225
+ args << "nic,macaddr=#{opts[:mac_address]}"
226
+ args << '-net'
227
+ args << "tap,script=#{ifup},downscript=#{ifdown}"
228
+ end
229
+
230
+ # advanced options, mostly to be used by plugins
231
+ [:cdrom, :kernel, :initrd, :append].each do |key|
232
+ if opts.has_key?(key)
233
+ args << "-#{key}"
234
+ args << case opts[key]
235
+ # append is multple
236
+ when Array then opts[key].join(' ')
237
+ else opts[key]
238
+ end
239
+ end
240
+ end
241
+
242
+ if opts.has_key?(:floppy)
243
+ if File.directory?(opts[:floppy])
244
+ args << '-fda'
245
+ args << "fat:floppy:#{opts[:floppy]}"
246
+ else
247
+ Vmit.logger.warn "#{opts[:floppy]} : only directories supported"
248
+ end
249
+ end
250
+
251
+ # options that translate to
252
+ # -no-something if :something => false
253
+ [:reboot].each do |key|
254
+ if opts.has_key?(key)
255
+ # default is true
256
+ args << "-no-#{key}" if not opts[key]
257
+ end
258
+ end
259
+
260
+ unless ENV['DISABLE_UUID']
261
+ args << '-uuid'
262
+ args << "#{opts[:uuid]}"
263
+ end
264
+
265
+ DRb.start_service nil, self
266
+ ENV['VMIT_SERVER'] = DRb.uri
267
+
268
+ ENV['VMIT_SWITCH'] = SWITCH
269
+ Vmit.logger.debug "Vmit server listening at #{DRb.uri}"
270
+
271
+ @network.auto do
272
+ begin
273
+ Cheetah.run(*args)
274
+ ensure
275
+ FileUtils.rm_f File.join(work_dir, 'qemu.pid')
276
+ end
277
+ end
278
+ rescue PidFile::DuplicateProcessError => e
279
+ Vmit.logger.fatal "VM in '#{work_dir}'' is already running (#{e})"
280
+ raise
281
+ end
282
+ end
283
+
284
+ # Called by vmit-ifup
285
+ def ifup(device)
286
+ Vmit.logger.info " Bringing interface #{device} up"
287
+ Cheetah.run '/sbin/ifconfig', device, '0.0.0.0', 'up'
288
+ @network.connect_interface(device)
289
+ end
290
+
291
+ # Called by vmit-ifdown
292
+ def ifdown(device)
293
+ Vmit.logger.info " Bringing down interface #{device}"
294
+ Cheetah.run '/sbin/ifconfig', device, '0.0.0.0', 'down'
295
+ @network.disconnect_interface(device)
296
+ end
297
+ end
298
+
299
+ end