vagrant-kvm 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +14 -0
  5. data/CHANGELOG.md +29 -0
  6. data/DEVELOPMENT.md +87 -0
  7. data/Gemfile +1 -0
  8. data/INSTALL.md +229 -0
  9. data/LICENSE +2 -1
  10. data/README.md +154 -63
  11. data/Rakefile +24 -1
  12. data/example_box/README.md +8 -8
  13. data/lib/vagrant-kvm/action.rb +47 -5
  14. data/lib/vagrant-kvm/action/boot.rb +0 -4
  15. data/lib/vagrant-kvm/action/clear_forwarded_ports.rb +53 -0
  16. data/lib/vagrant-kvm/action/forward_ports.rb +104 -0
  17. data/lib/vagrant-kvm/action/import.rb +97 -18
  18. data/lib/vagrant-kvm/action/init_storage_pool.rb +3 -1
  19. data/lib/vagrant-kvm/action/message_not_running.rb +16 -0
  20. data/lib/vagrant-kvm/action/network.rb +12 -13
  21. data/lib/vagrant-kvm/action/package_vagrantfile.rb +3 -1
  22. data/lib/vagrant-kvm/action/prepare_gui.rb +20 -0
  23. data/lib/vagrant-kvm/action/prepare_nfs_settings.rb +8 -16
  24. data/lib/vagrant-kvm/action/prepare_nfs_valid_ids.rb +17 -0
  25. data/lib/vagrant-kvm/action/reset_image_permission.rb +23 -0
  26. data/lib/vagrant-kvm/action/resume_network.rb +28 -0
  27. data/lib/vagrant-kvm/action/share_folders.rb +6 -5
  28. data/lib/vagrant-kvm/action/suspend.rb +8 -1
  29. data/lib/vagrant-kvm/config.rb +103 -2
  30. data/lib/vagrant-kvm/driver/driver.rb +321 -99
  31. data/lib/vagrant-kvm/errors.rb +18 -0
  32. data/lib/vagrant-kvm/provider.rb +4 -1
  33. data/lib/vagrant-kvm/util.rb +3 -0
  34. data/lib/vagrant-kvm/util/commands.rb +23 -0
  35. data/lib/vagrant-kvm/util/definition_attributes.rb +33 -0
  36. data/lib/vagrant-kvm/util/disk_info.rb +48 -0
  37. data/lib/vagrant-kvm/util/network_definition.rb +44 -84
  38. data/lib/vagrant-kvm/util/vm_definition.rb +91 -103
  39. data/lib/vagrant-kvm/version.rb +1 -1
  40. data/locales/en.yml +8 -0
  41. data/locales/ja.yml +14 -0
  42. data/spec/acceptance/vagrant-kvm_spec.rb +80 -0
  43. data/spec/fedora/10.virt.rules +10 -0
  44. data/spec/fedora/50-vagrant-libvirt-access.pkla +6 -0
  45. data/spec/spec_helper.rb +30 -0
  46. data/spec/support/libvirt_helper.rb +37 -0
  47. data/spec/support/vagrant_kvm_helper.rb +39 -0
  48. data/spec/test_files/box.xml +74 -0
  49. data/spec/vagrant-kvm/config_spec.rb +56 -0
  50. data/spec/vagrant-kvm/driver/driver_spec.rb +36 -0
  51. data/spec/vagrant-kvm/errors_spec.rb +25 -0
  52. data/spec/vagrant-kvm/util/network_definition_spec.rb +60 -0
  53. data/spec/vagrant-kvm/util/vm_definition_spec.rb +76 -0
  54. data/templates/libvirt_domain.erb +34 -12
  55. data/templates/libvirt_network.erb +13 -0
  56. data/templates/package_Vagrantfile.erb +11 -0
  57. data/vagrant-kvm.gemspec +1 -2
  58. metadata +41 -42
@@ -6,12 +6,30 @@ module VagrantPlugins
6
6
  class VagrantKVMError < Vagrant::Errors::VagrantError
7
7
  error_namespace("vagrant_kvm.errors")
8
8
  end
9
+
9
10
  class KvmNoConnection < VagrantKVMError
10
11
  error_key(:kvm_no_connection)
11
12
  end
13
+
12
14
  class KvmInvalidVersion < VagrantKVMError
13
15
  error_key(:kvm_invalid_version)
14
16
  end
17
+
18
+ class KvmNoQEMUBinary < VagrantKVMError
19
+ error_key(:kvm_no_qemu_binary)
20
+ end
21
+
22
+ class KvmFailImageConversion < VagrantKVMError
23
+ error_key(:kvm_fail_image_conversion)
24
+ end
25
+
26
+ class KvmBadBoxFormat < VagrantKVMError
27
+ error_key(:kvm_bad_box_format)
28
+ end
29
+
30
+ class KvmFailedCommand < VagrantKVMError
31
+ error_key(:kvm_failed_command)
32
+ end
15
33
  end
16
34
  end
17
35
  end
@@ -66,7 +66,9 @@ module VagrantPlugins
66
66
  end
67
67
  end
68
68
 
69
- nil
69
+ # XXX duplicated with network.rb default
70
+ # If no private network configuration, return default ip
71
+ "192.168.123.10"
70
72
  end
71
73
 
72
74
  # Return the state of the VM
@@ -81,6 +83,7 @@ module VagrantPlugins
81
83
  state_id = :not_created if !@driver.uuid
82
84
  state_id = @driver.read_state if !state_id
83
85
  state_id = :unknown if !state_id
86
+ @logger.info("state is now:", state_id)
84
87
 
85
88
  # TODO Translate into short/long descriptions
86
89
  short = state_id
@@ -4,9 +4,12 @@ module VagrantPlugins
4
4
  module ProviderKvm
5
5
  module Util
6
6
  util_root = Pathname.new(File.expand_path("../util", __FILE__))
7
+ autoload :DefinitionAttributes, util_root.join("definition_attributes")
7
8
  autoload :VmDefinition, util_root.join("vm_definition")
8
9
  autoload :NetworkDefinition, util_root.join("network_definition")
9
10
  autoload :KvmTemplateRenderer, util_root.join("kvm_template_renderer")
11
+ autoload :Commands, util_root.join("commands")
12
+ autoload :DiskInfo, util_root.join("disk_info")
10
13
  end
11
14
  end
12
15
  end
@@ -0,0 +1,23 @@
1
+ module VagrantPlugins
2
+ module ProviderKvm
3
+ module Util
4
+ module Commands
5
+ def run_root_command(cmd)
6
+ # FIXME detect whether 'sudo' or 'su -c'
7
+ # for safety, we run cmd as single argument of sudo
8
+ unless res = system('sudo ' + cmd)
9
+ raise Errors::KvmFailedCommand, cmd: cmd, res: res
10
+ end
11
+ end
12
+
13
+ def run_command(cmd)
14
+ unless res = system(cmd)
15
+ raise Errors::KvmFailedCommand, cmd: cmd, res: res
16
+ end
17
+ res
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,33 @@
1
+ module VagrantPlugins
2
+ module ProviderKvm
3
+ module Util
4
+ module DefinitionAttributes
5
+ module InstanceMethods
6
+ def attributes
7
+ @attributes ||= {}
8
+ end
9
+
10
+ def attributes=(attrs)
11
+ @attributes = attrs
12
+ end
13
+
14
+ def get(key)
15
+ attributes[key]
16
+ end
17
+
18
+ def set(key, val)
19
+ attributes[key] = val
20
+ end
21
+
22
+ def update(args={})
23
+ attributes.merge!(args)
24
+ end
25
+ end
26
+
27
+ def self.included(receiver)
28
+ receiver.send :include, InstanceMethods
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,48 @@
1
+ module VagrantPlugins
2
+ module ProviderKvm
3
+ module Util
4
+ class DiskInfo
5
+ include Errors
6
+
7
+ attr_reader :backing, :capacity, :cluster, :size, :type
8
+
9
+ def initialize(vol_path)
10
+ logger = Log4r::Logger.new("vagrant::kvm::util::disk_info")
11
+ # default values
12
+ @capacity = {:size => 10, :unit => 'G'}
13
+ @backing = nil
14
+ @cluster = nil
15
+ begin
16
+ diskinfo = %x[qemu-img info #{vol_path}]
17
+ diskinfo.each_line do |line|
18
+ case line
19
+ when /^file format:/
20
+ result = line.match(%r{file format:\s+(?<format>(\w+))})
21
+ @type = result[:format]
22
+ when /virtual size:/
23
+ result = line.match(%r{virtual size:\s+(?<size>\d+(\.\d+)?)(?<unit>.)\s+\((?<bytesize>\d+)\sbytes\)})
24
+ # always take the size in bytes to avoid conversion
25
+ @capacity = {:size => result[:bytesize], :unit => "B"}
26
+ when /^disk size:/
27
+ result = line.match(%r{disk size:\s+(?<size>\d+(\.\d+)?)(?<unit>.)})
28
+ @size = {:size => result[:size], :unit => result[:unit]}
29
+ when /^backing file:/
30
+ result = line.match(%r{backing file:\s+(?<file>(\S+))})
31
+ @backing = result[:file]
32
+ when /^cluster[_ ]size:/
33
+ result = line.match(%r{cluster[_ ]size:\s+(?<size>(\d+))})
34
+ @cluster = result[:size]
35
+ end
36
+ end
37
+ rescue Errors::KvmFailedCommand => e
38
+ logger.error 'Failed to find volume size. Using defaults.'
39
+ logger.error e
40
+ end
41
+ end
42
+
43
+ # TBD backing chain 'qemu-img info --backing-chain'
44
+
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,105 +1,65 @@
1
1
  # Utility class to manage libvirt network definition
2
- require "nokogiri"
2
+ require "rexml/document"
3
3
 
4
4
  module VagrantPlugins
5
5
  module ProviderKvm
6
6
  module Util
7
7
  class NetworkDefinition
8
- # Attributes of the Network
9
- attr_reader :name
10
- attr_reader :domain_name
11
- attr_reader :base_ip
8
+ include DefinitionAttributes
12
9
 
13
10
  def initialize(name, definition=nil)
14
- @name = name
11
+ # create with defaults
12
+ # XXX defaults should move to config
13
+ self.attributes = {
14
+ :forward => "nat",
15
+ :domain_name => "vagrant.local",
16
+ :base_ip => "192.168.192.1",
17
+ :netmask => "255.255.255.0",
18
+ :range => {
19
+ :start => "192.168.192.100",
20
+ :end => "192.168.192.200",
21
+ },
22
+ :hosts => [],
23
+ name: name,
24
+ }
25
+
15
26
  if definition
16
- doc = Nokogiri::XML(definition)
17
- @forward = doc.at_css("network forward")["mode"] if doc.at_css("network forward")
18
- @domain_name = doc.at_css("network domain")["name"] if doc.at_css("network domain")
19
- @base_ip = doc.at_css("network ip")["address"]
20
- @netmask = doc.at_css("network ip")["netmask"]
21
- @range = {
22
- :start => doc.at_css("network ip dhcp range")["start"],
23
- :end => doc.at_css("network ip dhcp range")["end"]
24
- }
25
- @hosts = []
26
- doc.css("network ip dhcp host").each do |host|
27
- @hosts << {
28
- :mac => host["mac"],
29
- :name => host["name"],
30
- :ip => host["ip"]
27
+ doc = REXML::Document.new definition
28
+ if doc.elements["/network/forward"]
29
+ set(:forward, doc.elements["/network/forward"].attributes["mode"])
30
+ end
31
+
32
+ if doc.elements["/network/domain"]
33
+ set(:domain_name, doc.elements["/network/domain"].attributes["name"])
34
+ end
35
+ set(:base_ip, doc.elements["/network/ip"].attributes["address"])
36
+ set(:netmask, doc.elements["/network/ip"].attributes["netmask"])
37
+ set(:range, {
38
+ :start => doc.elements["/network/ip/dhcp/range"].attributes["start"],
39
+ :end => doc.elements["/network/ip/dhcp/range"].attributes["end"]
40
+ })
41
+ hosts = []
42
+ doc.elements.each("/network/ip/dhcp/host") do |host|
43
+ hosts << {
44
+ :mac => host.attributes["mac"],
45
+ :name => host.attributes["name"],
46
+ :ip => host.attributes["ip"]
31
47
  }
32
48
  end
33
- else
34
- # create with defaults
35
- # XXX defaults should move to config
36
- @forward = "nat"
37
- @domain_name = "vagrant.local"
38
- @base_ip = "192.168.192.1"
39
- @netmask = "255.255.255.0"
40
- @range = {
41
- :start => "192.168.192.100",
42
- :end => "192.168.192.200"}
43
- @hosts = []
49
+ set(:hosts, hosts)
44
50
  end
45
51
  end
46
52
 
47
- def configure(config)
48
- config = {
49
- :forward => @forward,
50
- :domain_name => @domain_name,
51
- :base_ip => @base_ip,
52
- :netmask => @netmask,
53
- :range => @range,
54
- :hosts => @hosts}.merge(config)
55
-
56
- @forward = config[:forward]
57
- @domain_name = config[:domain_name]
58
- @base_ip = config[:base_ip]
59
- @netmask = config[:netmask]
60
- @range = config[:range]
61
- @hosts = config[:hosts]
62
- end
63
-
64
- def as_xml
65
- xml = <<-EOXML
66
- <network>
67
- <name>#{@name}</name>
68
- <forward mode='#{@forward}'/>
69
- <domain name='#{@domain_name}'/>
70
- <ip address='#{@base_ip}' netmask='#{@netmask}'>
71
- <dhcp>
72
- <range start='#{@range[:start]}' end='#{@range[:end]}' />
73
- </dhcp>
74
- </ip>
75
- </network>
76
- EOXML
77
- xml = inject_hosts(xml) if @hosts.length > 0
78
- xml
79
- end
80
-
81
- def add_host(host)
82
- cur_host = @hosts.detect {|h| h[:mac] == host[:mac]}
83
- if cur_host
84
- cur_host[:ip] = host[:ip]
85
- cur_host[:name] = host[:name]
86
- else
87
- @hosts << {
88
- :mac => host[:mac],
89
- :name => host[:name],
90
- :ip => host[:ip]}
53
+ def ==(other)
54
+ # Don't compare the hosts
55
+ [:forward, :domain_name, :base_ip, :netmask, :range,].all? do |key|
56
+ get(key) == other.get(key)
91
57
  end
92
58
  end
93
59
 
94
- def inject_hosts(xml)
95
- doc = Nokogiri::XML(xml)
96
- entry_point = doc.at_css("network ip dhcp range")
97
- @hosts.each do |host|
98
- entry_point.add_next_sibling "<host mac='#{host[:mac]}' name='#{host[:name]}' ip='#{host[:ip]}' />"
99
- end
100
- doc.to_xml
60
+ def as_xml
61
+ KvmTemplateRenderer.render("libvirt_network", attributes.dup)
101
62
  end
102
-
103
63
  end
104
64
  end
105
65
  end
@@ -1,135 +1,119 @@
1
1
  # Utility class to translate ovf definition to libvirt XML
2
2
  # and manage XML formatting for libvirt interaction
3
3
  # Not a full OVF converter, only the minimal needed definition
4
- require "nokogiri"
4
+ require "rexml/document"
5
5
 
6
6
  module VagrantPlugins
7
7
  module ProviderKvm
8
8
  module Util
9
9
  class VmDefinition
10
- # Attributes of the VM
11
- attr_accessor :name
12
- attr_reader :cpus
13
- attr_accessor :disk
14
- attr_reader :mac
15
- attr_reader :arch
16
- attr_reader :network
17
- attr_accessor :image_type
10
+ include Errors
11
+ include DefinitionAttributes
18
12
 
19
13
  def self.list_interfaces(definition)
20
- nics = {}
21
- ifcount = 0
22
- doc = Nokogiri::XML(definition)
14
+ nics = []
15
+ doc = REXML::Document.new definition
23
16
  # look for user mode interfaces
24
- doc.css("devices interface[type='user']").each do |item|
25
- ifcount += 1
26
- adapter = ifcount
27
- nics[adapter] ||= {}
28
- nics[adapter][:type] = :user
17
+ doc.elements.each("//devices/interface[@type='user']") do |item|
18
+ nics << {:type => :user, :network => nil }
29
19
  end
30
20
  # look for interfaces on virtual network
31
- doc.css("devices interface[type='network']").each do |item|
32
- ifcount += 1
33
- adapter = ifcount
34
- nics[adapter] ||= {}
35
- nics[adapter][:network] = item.at_css("source")["network"]
36
- nics[adapter][:type] = :network
21
+ doc.elements.each("//devices/interface[@type='network']") do |item|
22
+ nics << {
23
+ :type => :network,
24
+ :network => item.elements["source"].attributes["network"].to_s
25
+ }
37
26
  end
38
27
  nics
39
28
  end
40
29
 
41
- def initialize(definition, source_type='libvirt')
42
- @uuid = nil
43
- @gui = nil
44
- @network = 'default'
45
- if source_type == 'ovf'
46
- create_from_ovf(definition)
47
- else
48
- create_from_libvirt(definition)
30
+ def initialize(definition)
31
+ self.attributes = {
32
+ :uuid => nil,
33
+ :gui => nil,
34
+ :vnc_autoport => false,
35
+ :vnc_password => nil,
36
+ :network => 'default',
37
+ :network_model => 'virtio',
38
+ :video_model => 'cirrus'
39
+ }
40
+ doc = REXML::Document.new definition
41
+ memory_unit = doc.elements["/domain/memory"].attributes["unit"]
42
+ update({
43
+ :name => doc.elements["/domain/name"].text,
44
+ :cpus => doc.elements["/domain/vcpu"].text,
45
+ :memory => size_in_bytes(doc.elements["/domain/memory"].text,
46
+ memory_unit), # always :memory is in bytes
47
+ :arch => doc.elements["/domain/os/type"].attributes["arch"],
48
+ :machine_type => doc.elements["/domain/os/type"].attributes["machine"],
49
+ :disk => doc.elements["//devices/disk/source"].attributes["file"],
50
+ :network => doc.elements["//devices/interface/source"].attributes["network"],
51
+ :network_model => :default,
52
+ :mac => format_mac(doc.elements["//devices/interface/mac"].attributes["address"]),
53
+ :image_type => doc.elements["//devices/disk/driver"].attributes["type"],
54
+ :qemu_bin => doc.elements["/domain/devices/emulator"].text,
55
+ :video_model => doc.elements["/domain/devices/video/model"].attributes["type"],
56
+ :disk_bus => doc.elements["//devices/disk/target"].attributes["bus"]
57
+ })
58
+ model_node = doc.elements["//devices/interface/model"]
59
+ if model_node
60
+ update({
61
+ :model_node => model_node,
62
+ :network_model => model_node.attributes["type"]
63
+ })
49
64
  end
50
- end
51
-
52
- def create_from_ovf(definition)
53
- doc = Nokogiri::XML(definition)
54
- # we don't need no namespace
55
- doc.remove_namespaces!
56
- @name = doc.at_css("VirtualSystemIdentifier").content
57
- devices = doc.css("VirtualHardwareSection Item")
58
- for device in devices
59
- case device.at_css("ResourceType").content
60
- # CPU
61
- when "3"
62
- @cpus = device.at_css("VirtualQuantity").content
63
- # Memory
64
- when "4"
65
- @memory = size_in_bytes(device.at_css("VirtualQuantity").content,
66
- device.at_css("AllocationUnits").content)
67
- end
65
+ if doc.elements["/domain/uuid"]
66
+ update({:uuid => doc.elements["/domain/uuid"].text})
67
+ end
68
+ if doc.elements["//devices/graphics"]
69
+ attrs = doc.elements["//devices/graphics"].attributes
70
+ update({
71
+ :gui => attrs["type"] == 'vnc',
72
+ :vnc_port => attrs['port'].to_i,
73
+ :vnc_autoport => attrs['autoport'] == 'yes',
74
+ :vnc_password => attrs['passwd']
75
+ })
68
76
  end
69
-
70
- # disk volume
71
- diskref = doc.at_css("DiskSection Disk")["fileRef"]
72
- @disk = doc.at_css("References File[id='#{diskref}']")["href"]
73
- @image_type = 'raw'
74
- # mac address
75
- # XXX we use only the first nic
76
- @mac = format_mac(doc.at_css("Machine Hardware Adapter[enabled='true']")['MACAddress'])
77
-
78
- # the architecture is not defined in the ovf file
79
- # we try to guess from OSType
80
- # see https://www.virtualbox.org/browser/vbox/trunk/src/VBox/Main/include/ovfreader.h
81
- @arch = doc.at_css("VirtualSystemIdentifier").
82
- content[-2..-1] == '64' ? "x86_64" : "i686"
83
77
  end
84
78
 
85
- def create_from_libvirt(definition)
86
- doc = Nokogiri::XML(definition)
87
- @name = doc.at_css("domain name").content
88
- @uuid = doc.at_css("domain uuid").content if doc.at_css("domain uuid")
89
- memory_unit = doc.at_css("domain memory")["unit"]
90
- @memory = size_in_bytes(doc.at_css("domain memory").content,
91
- memory_unit)
92
- @cpus = doc.at_css("domain vcpu").content
93
- @arch = doc.at_css("domain os type")["arch"]
94
- @disk = doc.at_css("devices disk source")["file"]
95
- @mac = doc.at_css("devices interface mac")["address"]
96
- @network = doc.at_css("devices interface source")["network"]
97
- @image_type = doc.at_css("devices disk driver")["type"]
98
- end
79
+ def as_xml
80
+ if attributes[:qemu_bin]
81
+ # user specified path of qemu binary
82
+ qemu_bin_list = [attributes[:qemu_bin]]
83
+ else
84
+ # RedHat and Debian-based systems have different executable names
85
+ # depending on version/architectures
86
+ qemu_bin_list = ['/usr/bin/qemu-system-x86_64'] if get(:arch).match(/64$/)
87
+ qemu_bin_list = ['/usr/bin/qemu-system-i386'] if get(:arch).match(/^i.86$/)
88
+ qemu_bin_list += [ '/usr/bin/qemu-kvm', '/usr/bin/kvm' ]
89
+ end
99
90
 
100
- def as_libvirt
101
- # RedHat and Debian-based systems have different executable names
102
- # depending on version/architectures
103
- qemu_bin = [ '/usr/bin/qemu-kvm', '/usr/bin/kvm' ]
104
- qemu_bin << '/usr/bin/qemu-system-x86_64' if @arch.match(/64$/)
105
- qemu_bin << '/usr/bin/qemu-system-i386' if @arch.match(/^i.86$/)
91
+ qemu_bin = qemu_bin_list.detect { |binary| File.exists? binary }
92
+ if not qemu_bin
93
+ raise Errors::KvmNoQEMUBinary,
94
+ :cause => attributes[:qemu_bin] ?
95
+ "Vagrantfile (specified binary: #{attributes[:qemu_bin]})" : "QEMU installation"
96
+ end
106
97
 
107
- xml = KvmTemplateRenderer.render("libvirt_domain", {
108
- :name => @name,
109
- :uuid => @uuid,
110
- :memory => size_from_bytes(@memory, "KiB"),
111
- :cpus => @cpus,
112
- :arch => @arch,
113
- :disk => @disk,
114
- :mac => format_mac(@mac),
115
- :network => @network,
116
- :gui => @gui,
117
- :image_type => @image_type,
118
- :qemu_bin => qemu_bin.detect { |binary| File.exists? binary }
119
- })
98
+ xml = KvmTemplateRenderer.render("libvirt_domain",
99
+ attributes.merge!(:memory_size => get_memory("KiB"),
100
+ :memory_unit => "KiB")
101
+ )
120
102
  xml
121
103
  end
122
104
 
123
105
  def get_memory(unit="bytes")
124
- size_from_bytes(@memory, unit)
106
+ size_from_bytes(get(:memory), unit)
125
107
  end
126
108
 
127
- def set_mac(mac)
128
- @mac = format_mac(mac)
129
- end
130
-
131
- def set_gui
132
- @gui = true
109
+ def update(args={})
110
+ args.each {|k,v|
111
+ case k
112
+ when :mac
113
+ args[:mac] = format_mac(args[:mac])
114
+ end
115
+ }
116
+ super(args)
133
117
  end
134
118
 
135
119
  # Takes a quantity and a unit
@@ -193,6 +177,10 @@ module VagrantPlugins
193
177
  end
194
178
  end
195
179
 
180
+ def format_bool(v)
181
+ v ? "yes" : "no"
182
+ end
183
+
196
184
  def format_mac(mac)
197
185
  if mac.length == 12
198
186
  mac = mac[0..1] + ":" + mac[2..3] + ":" +