vmit 0.0.3 → 0.0.3.99

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.
@@ -18,17 +18,53 @@
18
18
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
- require 'vmit'
22
21
  require 'erb'
22
+ require 'vmit/unattended_install'
23
+ require 'vmit/vfs'
23
24
 
24
25
  module Vmit
25
26
 
26
- class Kickstart
27
+ class Kickstart < UnattendedInstall
27
28
 
28
- attr_accessor :install
29
+ def initialize(location)
30
+ super(location)
29
31
 
30
- # ks=floppy
31
- def initialize
32
+ media = Vmit::VFS.from(location)
33
+ case media
34
+ when Vmit::VFS::URI
35
+ @install = location
36
+ when Vmit::VFS::ISO
37
+ @install = :cdrom
38
+ vm.config.configure(:cdrom => location.to_s)
39
+ else raise ArgumentError.new("Unsupported autoinstallation: #{location}")
40
+ end
41
+ end
42
+
43
+ def execute_autoinstall(vm, args)
44
+ vm.config.push!
45
+ begin
46
+ vm.config.configure(args)
47
+ if @install == :cdrom
48
+ vm.config.configure(:cdrom => location.to_s)
49
+ end
50
+
51
+ Dir.mktmpdir do |floppy_dir|
52
+ FileUtils.chmod_R 0755, floppy_dir
53
+ vm.config.floppy = floppy_dir
54
+ vm.config.add_kernel_cmdline!('ks=floppy')
55
+ vm.config.add_kernel_cmdline!("repo=#{@install}")
56
+ vm.config.reboot = false
57
+
58
+ File.write(File.join(floppy_dir, 'ks.cfg'), to_ks_script)
59
+ Vmit.logger.info "Kickstart: 1st stage."
60
+ vm.up
61
+ vm.wait_until_shutdown! do
62
+ vm.vnc
63
+ end
64
+ end
65
+ ensure
66
+ vm.config.pop!
67
+ end
32
68
  end
33
69
 
34
70
  def to_ks_script
@@ -41,10 +77,10 @@ keyboard us
41
77
  timezone --utc America/New_York
42
78
  bootloader --location=mbr --driveorder=sda --append="rhgb quiet"
43
79
  install
44
- <% if install.is_a?(String) || install.is_a?(::URI)%>
45
- url --url=<%= install.to_s.strip %>
80
+ <% if @install.is_a?(String) || @install.is_a?(::URI)%>
81
+ url --url=<%= @install.to_s.strip %>
46
82
  <% else %>
47
- <%= install %>
83
+ <%= @install %>
48
84
  <% end %>
49
85
  network --device eth0 --bootproto dhcp
50
86
  zerombr yes
@@ -0,0 +1,274 @@
1
+ require 'confstruct'
2
+ require 'libvirt'
3
+ require 'nokogiri'
4
+ require 'vmit/workspace'
5
+
6
+ module Vmit
7
+
8
+ class LibvirtVM
9
+
10
+ attr_reader :workspace
11
+ attr_reader :config
12
+ attr_reader :conn
13
+
14
+ def self.from_pwd(opts={})
15
+ workspace = Vmit::Workspace.from_pwd
16
+ LibvirtVM.new(workspace, opts)
17
+ end
18
+
19
+ # @param [Vmit::Workspace] workspace to run
20
+ # @param [Hash] runtime options that override
21
+ # the virtual machine options
22
+ def initialize(workspace, opts={})
23
+ @workspace = workspace
24
+ @config = Confstruct::Configuration.new(@workspace.config)
25
+ @config.configure(opts)
26
+
27
+ @conn = ::Libvirt::open("qemu:///system")
28
+ if not @conn
29
+ raise 'Can\'t initialize hypervisor'
30
+ end
31
+ end
32
+
33
+ # @return [Libvirt::Domain] returns the libvirt domain
34
+ # or creates one for the workspace if it does not exist
35
+ def domain
36
+ conn.lookup_domain_by_name(workspace.name) rescue nil
37
+ end
38
+
39
+ def up
40
+ unless down?
41
+ Vmit.logger.error "#{workspace.name} is already up. Run 'vmit ssh' or 'vmit vnc' to access it."
42
+ return
43
+ end
44
+
45
+ Vmit.logger.debug "\n#{conn.capabilities}"
46
+ Vmit.logger.info "Starting VM..."
47
+
48
+ network = conn.lookup_network_by_name('default')
49
+ Vmit.logger.debug "\n#{network.xml_desc}"
50
+ if not network.active?
51
+ network.create
52
+ end
53
+ Vmit.logger.debug "\n#{self.to_libvirt_xml}"
54
+
55
+ puts domain.inspect
56
+ domain.destroy if domain
57
+ if domain.nil?
58
+ conn.create_domain_xml(self.to_libvirt_xml)
59
+ end
60
+ end
61
+
62
+ def state
63
+ assert_up
64
+ st, reason = domain.state
65
+ st_sym = case st
66
+ when Libvirt::Domain::NOSTATE then :unknown
67
+ when Libvirt::Domain::RUNNING then :running
68
+ when Libvirt::Domain::BLOCKED then :blocked
69
+ when Libvirt::Domain::PAUSED then :paused
70
+ when Libvirt::Domain::SHUTDOWN then :shutdown
71
+ when Libvirt::Domain::SHUTOFF then :shutoff
72
+ when Libvirt::Domain::CRASHED then :crashed
73
+ when Libvirt::Domain::PMSUSPENDED then :pmsuspended
74
+ end
75
+ return st_sym, reason
76
+ end
77
+
78
+ def up?
79
+ !domain.nil? && domain.active?
80
+ end
81
+
82
+ def assert_up
83
+ unless up?
84
+ raise "VM is not running. Try 'vmit up'..."
85
+ end
86
+ end
87
+
88
+ def assert_down
89
+ if up?
90
+ raise "VM is running. Try 'vmit down'..."
91
+ end
92
+ end
93
+
94
+ def down?
95
+ !up?
96
+ end
97
+
98
+ def reboot
99
+ assert_up
100
+ domain.reboot
101
+ end
102
+
103
+ def shutdown
104
+ assert_up
105
+ domain.shutdown
106
+ end
107
+
108
+ def destroy
109
+ if domain
110
+ domain.destroy
111
+ end
112
+ end
113
+
114
+ # Waits until the machine is shutdown
115
+ # executing the passed block.
116
+ #
117
+ # If the machine is shutdown, the
118
+ # block will be killed. If the block
119
+ # exits, the machine will be stopped
120
+ # immediately (domain destroyed)
121
+ #
122
+ # @example
123
+ # vm.wait_until_shutdown! do
124
+ # vm.vnc
125
+ # end
126
+ #
127
+ def wait_until_shutdown!(&block)
128
+ chars = %w{ | / - \\ }
129
+ thread = Thread.new(&block)
130
+ thread.abort_on_exception = true
131
+
132
+ Vmit.logger.info "Waiting for machine..."
133
+ while true
134
+ print chars[0]
135
+
136
+ if down?
137
+ Thread.kill(thread)
138
+ return
139
+ end
140
+ if not thread.alive?
141
+ domain.destroy
142
+ end
143
+ sleep(1)
144
+ print "\b"
145
+ chars.push chars.shift
146
+ end
147
+
148
+ end
149
+
150
+ def ip_address
151
+ File.open('/var/lib/libvirt/dnsmasq/default.leases') do |f|
152
+ f.each_line do |line|
153
+ parts = line.split(' ')
154
+ if parts[1] == config.mac_address
155
+ return parts[2]
156
+ end
157
+ end
158
+ end
159
+ nil
160
+ end
161
+
162
+ def spice_address
163
+ assert_up
164
+ doc = Nokogiri::XML(domain.xml_desc)
165
+ port = doc.xpath("//graphics[@type='spice']/@port")
166
+ listen = doc.xpath("//graphics[@type='spice']/listen[@type='address']/@address")
167
+ return listen, port
168
+ end
169
+
170
+ def vnc_address
171
+ assert_up
172
+ doc = Nokogiri::XML(domain.xml_desc)
173
+ port = doc.xpath("//graphics[@type='vnc']/@port")
174
+ listen = doc.xpath("//graphics[@type='vnc']/listen[@type='address']/@address")
175
+ "#{listen}:#{port}"
176
+ end
177
+
178
+ # synchronus spice viewer
179
+ def spice
180
+ assert_up
181
+ addr, port = spice_address
182
+ raise "Can't get the SPICE information from the VM" unless addr
183
+ system("spicec --host #{addr} --port #{port}")
184
+ end
185
+
186
+ # synchronus vnc viewer
187
+ def vnc
188
+ assert_up
189
+ addr = vnc_address
190
+ raise "Can't get the VNC information from the VM" unless addr
191
+ system("vncviewer #{addr}")
192
+ end
193
+
194
+ def [](key)
195
+ if @runtime_opts.has_key?(key)
196
+ @runtime_opts[key]
197
+ else
198
+ workspace[key]
199
+ end
200
+ end
201
+
202
+ def to_libvirt_xml
203
+ builder = Nokogiri::XML::Builder.new do |xml|
204
+ xml.domain(:type => 'kvm') {
205
+ xml.name workspace.name
206
+ xml.uuid config.uuid
207
+ match = /([0-9+])([^0-9+])/.match config.memory
208
+ xml.memory(match[1], :unit => match[2])
209
+ xml.vcpu 1
210
+ xml.os {
211
+ xml.type('hvm', :arch => 'x86_64')
212
+ if config.lookup!('kernel')
213
+ xml.kernel config.kernel
214
+ if config.lookup!('kernel_cmdline')
215
+ xml.cmdline config.kernel_cmdline.join(' ')
216
+ end
217
+ end
218
+ xml.initrd config.initrd if config.lookup!('initrd')
219
+ xml.boot(:dev => 'cdrom') if config.lookup!('cdrom')
220
+ }
221
+ # for shutdown to work
222
+ xml.features {
223
+ xml.acpi
224
+ }
225
+ #xml.on_poweroff 'destroy'
226
+ unless config.lookup!('reboot').nil? || config.lookup!('reboot')
227
+ xml.on_reboot 'destroy'
228
+ end
229
+ #xml.on_crash 'destroy'
230
+ #xml.on_lockfailure 'poweroff'
231
+
232
+ xml.devices {
233
+ xml.emulator '/usr/bin/qemu-kvm'
234
+ xml.channel(:type => 'spicevmc') do
235
+ xml.target(:type => 'virtio', :name => 'com.redhat.spice.0')
236
+ end
237
+
238
+ xml.disk(:type => 'file', :device => 'disk') {
239
+ xml.driver(:name => 'qemu', :type => 'qcow2')
240
+ xml.source(:file => workspace.current_image)
241
+ if config.virtio
242
+ xml.target(:dev => 'sda', :bus => 'virtio')
243
+ else
244
+ xml.target(:dev => 'sda', :bus => 'ide')
245
+ end
246
+ }
247
+ if config.lookup!('cdrom')
248
+ xml.disk(:type => 'file', :device => 'cdrom') {
249
+ xml.source(:file => config.cdrom)
250
+ xml.target(:dev => 'hdc')
251
+ xml.readonly
252
+ }
253
+ end
254
+ if config.lookup!('floppy')
255
+ xml.disk(:type => 'dir', :device => 'floppy') {
256
+ xml.source(:dir => config.floppy)
257
+ xml.target(:dev => 'fda')
258
+ xml.readonly
259
+ }
260
+ end
261
+ xml.graphics(:type => 'vnc', :autoport => 'yes')
262
+ xml.graphics(:type => 'spice', :autoport => 'yes')
263
+ xml.interface(:type => 'network') {
264
+ xml.source(:network => 'default')
265
+ xml.mac(:address => config.mac_address)
266
+ }
267
+ }
268
+ }
269
+ end
270
+ builder.to_xml
271
+ end
272
+
273
+ end
274
+ end
@@ -18,8 +18,6 @@
18
18
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
19
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  #
21
- require 'vmit/autoyast'
22
- require 'vmit/kickstart'
23
21
  require 'abstract_method'
24
22
  require 'clamp'
25
23
  require 'net/http'
@@ -46,34 +44,52 @@ module Vmit
46
44
  disk_size
47
45
  end
48
46
 
49
- parameter "LOCATION", "Repository URL or ISO image to bootstrap from"
47
+ option ['-F', '--packages'], "PACKAGES",
48
+ "Add packages. Either a file with one package name per line or a
49
+ comma separated list", :default => [] do |pkgs|
50
+ case
51
+ when File.exist?(pkgs)
52
+ begin
53
+ File.read(pkgs).each_line.to_a.map(&:strip)
54
+ rescue
55
+ raise ArgumentError, "Can't read package list from #{pkgs}"
56
+ end
57
+ else
58
+ list = pkgs.split(',')
59
+ if list.empty?
60
+ raise ArgumentError, "Not a valid comma separated list of packages"
61
+ end
62
+ list
63
+ end
64
+ end
65
+
66
+ parameter "LOCATION ...", "Repository URL, ISO image or distribution name"
50
67
 
51
68
  def execute
52
69
  Vmit.logger.info 'Starting bootstrap'
53
- curr_dir = File.expand_path(Dir.pwd)
54
- vm = Vmit::VirtualMachine.new(curr_dir)
70
+ workspace = Vmit::Workspace.from_pwd
55
71
 
56
72
  Vmit.logger.info ' Deleting old images'
57
73
  FileUtils.rm_f(Dir.glob('*.qcow2'))
58
74
  opts = {}
59
75
  opts[:disk_size] = disk_size if disk_size
60
- vm.disk_image_init!(opts)
61
- vm.save_config!
76
+ workspace.disk_image_init!(opts)
77
+ workspace.save_config!
62
78
 
63
- uri = URI.parse(location)
64
- bootstrap = [Vmit::Bootstrap::FromImage,
65
- Vmit::Bootstrap::FromMedia].find do |method|
66
- method.accept?(uri)
67
- end
79
+ location = location_list.join(' ')
80
+ install_media = Vmit::InstallMedia.scan(location)
81
+
82
+ Vmit.logger.info "Install media: #{install_media}"
68
83
 
69
- if bootstrap
70
- bootstrap.new(vm, uri).execute
71
- else
72
- raise "Can't bootstrap from #{location}"
84
+ packages.each do |pkg|
85
+ install_media.unattended_install.config.add_packages!(pkg)
73
86
  end
74
87
 
88
+ vm = Vmit::LibvirtVM.from_pwd
89
+ install_media.autoinstall(vm)
90
+
75
91
  Vmit.logger.info 'Creating snapshot of fresh system.'
76
- vm.disk_snapshot!
92
+ workspace.disk_snapshot!
77
93
  Vmit.logger.info 'Bootstraping done. Call vmit run to start your system.'
78
94
  end
79
95
 
@@ -0,0 +1,208 @@
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 'yaml'
22
+
23
+ module Vmit
24
+
25
+ class Registry
26
+ include Enumerable
27
+ end
28
+
29
+ # Wraps a registry providing buffered
30
+ # semantics. All writes are buffered
31
+ # until +save!+ is called.
32
+ #
33
+ # Changes in the wrapped registry for
34
+ # already read keys are not reflected
35
+ # until +reload!+ is called.
36
+ #
37
+ # reg = ufferedRegistry.new(backing_registry)
38
+ # reg[:key1] = "value" # backing_registry not changed
39
+ # reg.save! # backing_registry changed
40
+ #
41
+ class BufferedRegistry < Registry
42
+
43
+ def initialize(registry)
44
+ @buffer = Hash.new
45
+ @registry = registry
46
+ end
47
+
48
+ def [](key)
49
+ if not @buffer.has_key?(key)
50
+ @buffer[key] = @registry[key]
51
+ end
52
+ @buffer[key]
53
+ end
54
+
55
+ def []=(key, val)
56
+ @buffer[key] = val
57
+ end
58
+
59
+ def save!
60
+ @buffer.each do |key, val|
61
+ @registry[key] = val
62
+ end
63
+ end
64
+
65
+ def reload!
66
+ @buffer.keys.each do |key|
67
+ @buffer[key] = @registry[key]
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ # Add types to keys
74
+ # You need to inherit from this class
75
+ #
76
+ # @example
77
+ # class MyTypes < TypedRegistry
78
+ # type :key1, Fixnum
79
+ # type :key2, Float
80
+ # end
81
+ #
82
+ # reg = MyTypes.new(backing_registry)
83
+ #
84
+ class TypedRegistry < Registry
85
+
86
+ class << self
87
+ def type(key, t=nil)
88
+ @type_info ||= Hash.new
89
+ @type_info[key] = t unless t.nil?
90
+ @type_info[key]
91
+ end
92
+ end
93
+
94
+ def initialize(registry)
95
+ @registry = registry
96
+ end
97
+
98
+ def type(key)
99
+ self.class.type(key)
100
+ end
101
+
102
+ def [](key)
103
+ rawval = @registry[key]
104
+ case type(key).to_s
105
+ when 'String' then rawval.to_s
106
+ when 'Fixnum' then rawval.to_i
107
+ when 'Float' then rawval.to_f
108
+ else rawval
109
+ end
110
+ end
111
+
112
+ def []=(key, val)
113
+ if type(key)
114
+ unless val.is_a?(type(key))
115
+ raise TypeError.new("Expected #{type(key)} for #{key}")
116
+ end
117
+ end
118
+ @registry[key] = val
119
+ end
120
+ end
121
+
122
+ # Takes configuration options from a yml
123
+ # file.
124
+ class YamlRegistry < Registry
125
+ def initialize(file_path)
126
+ @file_path = file_path
127
+ reload!
128
+ end
129
+
130
+ def reload!
131
+ @data = YAML::load(File.read(@file_path))
132
+ end
133
+
134
+ def save!
135
+ File.write(@file_path, @data.to_yaml)
136
+ end
137
+
138
+ def [](key)
139
+ # YAML uses strings for keys
140
+ # we use symbols.
141
+ if @data.has_key?(key)
142
+ @data[key]
143
+ else
144
+ @data[key.to_s]
145
+ end
146
+ end
147
+
148
+ def []=(key, val)
149
+ @data[key.to_s] = val
150
+ save!
151
+ reload!
152
+ end
153
+
154
+ def each(&block)
155
+ Enumerator.new do |enum|
156
+ @data.each do |key, val|
157
+ enum.yield key.to_sym, val
158
+ end
159
+ end
160
+ end
161
+
162
+ def keys
163
+ each.to_a.map(&:first)
164
+ end
165
+ end
166
+
167
+ # Takes configuration options from a
168
+ # filesystem tree where the files are
169
+ # the keys and the content the values
170
+ class FilesystemRegistry < Registry
171
+ def initialize(base_path)
172
+ @base_path = base_path
173
+ end
174
+
175
+ def [](key)
176
+ begin
177
+ path = File.join(@base_path, key.to_s)
178
+ unless File.directory?(path)
179
+ File.read(path)
180
+ else
181
+ return FilesystemRegistry.new(File.join(@base_path, path))
182
+ end
183
+ rescue Errno::ENOENT
184
+ nil
185
+ end
186
+ end
187
+
188
+ def []=(key, val)
189
+ File.write(File.join(@base_path, key.to_s), val)
190
+ end
191
+
192
+ def each(&block)
193
+ Enumerator.new do |enum|
194
+ Dir.entries(@base_path).reject do |elem|
195
+ ['.', '..'].include?(elem)
196
+ end.each do |key|
197
+ enum.yield key.to_sym, self[key]
198
+ end
199
+ end
200
+ end
201
+
202
+ def keys
203
+ each.to_a.map(&:first)
204
+ end
205
+
206
+ end
207
+
208
+ end