summaryrefslogtreecommitdiffstats
path: root/features/support/helpers/storage_helper.rb
blob: 0e452ed4d4b32f3ca3de3a431d54536ed799a8e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# Helper class for manipulating VM storage *volumes*, i.e. it deals
# only with creation of images and keeps a name => volume path lookup
# table (plugging drives or getting info of plugged devices is done in
# the VM class). We'd like better coupling, but given the ridiculous
# disconnect between Libvirt::StoragePool and Libvirt::Domain (hint:
# they have nothing with each other to do whatsoever) it's what makes
# sense.

require 'libvirt'
require 'guestfs'
require 'rexml/document'
require 'etc'

class VMStorage

  def initialize(virt, xml_path)
    @virt = virt
    @xml_path = xml_path
    pool_xml = REXML::Document.new(File.read("#{@xml_path}/storage_pool.xml"))
    pool_name = pool_xml.elements['pool/name'].text
    @pool_path = "#{$config["TMPDIR"]}/#{pool_name}"
    begin
      @pool = @virt.lookup_storage_pool_by_name(pool_name)
    rescue Libvirt::RetrieveError
      @pool = nil
    end
    if @pool and (not(KEEP_SNAPSHOTS) or
                  (KEEP_SNAPSHOTS and not(Dir.exists?(@pool_path))))
      VMStorage.clear_storage_pool(@pool)
      @pool = nil
    end
    unless @pool
      pool_xml.elements['pool/target/path'].text = @pool_path
      @pool = @virt.define_storage_pool_xml(pool_xml.to_s)
      if not(Dir.exists?(@pool_path))
        # We'd like to use @pool.build, which will just create the
        # @pool_path directory, but it does so with root:root as owner
        # (at least with libvirt 1.2.21-2). libvirt itself can handle
        # that situation, but guestfs (at least with <=
        # 1:1.28.12-1+b3) cannot when invoked by a non-root user,
        # which we want to support.
        FileUtils.mkdir(@pool_path)
        FileUtils.chown(nil, 'libvirt-qemu', @pool_path)
        FileUtils.chmod("ug+wrx", @pool_path)
      end
    end
    @pool.create unless @pool.active?
    @pool.refresh
  end

  def VMStorage.clear_storage_pool_volumes(pool)
    was_not_active = !pool.active?
    if was_not_active
      pool.create
    end
    pool.list_volumes.each do |vol_name|
      vol = pool.lookup_volume_by_name(vol_name)
      vol.delete
    end
    if was_not_active
      pool.destroy
    end
  rescue
    # Some of the above operations can fail if the pool's path was
    # deleted by external means; let's ignore that.
  end

  def VMStorage.clear_storage_pool(pool)
    VMStorage.clear_storage_pool_volumes(pool)
    pool.destroy if pool.active?
    pool.undefine
  end

  def clear_pool
    VMStorage.clear_storage_pool(@pool)
  end

  def clear_volumes
    VMStorage.clear_storage_pool_volumes(@pool)
  end

  def delete_volume(name)
    @pool.lookup_volume_by_name(name).delete
  end

  def create_new_disk(name, options = {})
    options[:size] ||= 2
    options[:unit] ||= "GiB"
    options[:type] ||= "qcow2"
    # Require 'slightly' more space to be available to give a bit more leeway
    # with rounding, temp file creation, etc.
    reserved = 500
    needed = convert_to_MiB(options[:size].to_i, options[:unit])
    avail = convert_to_MiB(get_free_space('host', @pool_path), "KiB")
    assert(avail - reserved >= needed,
           "Error creating disk \"#{name}\" in \"#{@pool_path}\". " \
           "Need #{needed} MiB but only #{avail} MiB is available of " \
           "which #{reserved} MiB is reserved for other temporary files.")
    begin
      old_vol = @pool.lookup_volume_by_name(name)
    rescue Libvirt::RetrieveError
      # noop
    else
      old_vol.delete
    end
    uid = Etc::getpwnam("libvirt-qemu").uid
    gid = Etc::getgrnam("libvirt-qemu").gid
    vol_xml = REXML::Document.new(File.read("#{@xml_path}/volume.xml"))
    vol_xml.elements['volume/name'].text = name
    size_b = convert_to_bytes(options[:size].to_f, options[:unit])
    vol_xml.elements['volume/capacity'].text = size_b.to_s
    vol_xml.elements['volume/target/format'].attributes["type"] = options[:type]
    vol_xml.elements['volume/target/path'].text = "#{@pool_path}/#{name}"
    vol_xml.elements['volume/target/permissions/owner'].text = uid.to_s
    vol_xml.elements['volume/target/permissions/group'].text = gid.to_s
    vol = @pool.create_volume_xml(vol_xml.to_s)
    @pool.refresh
  end

  def clone_to_new_disk(from, to)
    begin
      old_to_vol = @pool.lookup_volume_by_name(to)
    rescue Libvirt::RetrieveError
      # noop
    else
      old_to_vol.delete
    end
    from_vol = @pool.lookup_volume_by_name(from)
    xml = REXML::Document.new(from_vol.xml_desc)
    pool_path = REXML::Document.new(@pool.xml_desc).elements['pool/target/path'].text
    xml.elements['volume/name'].text = to
    xml.elements['volume/target/path'].text = "#{pool_path}/#{to}"
    @pool.create_volume_xml_from(xml.to_s, from_vol)
  end

  def disk_format(name)
    vol = @pool.lookup_volume_by_name(name)
    vol_xml = REXML::Document.new(vol.xml_desc)
    return vol_xml.elements['volume/target/format'].attributes["type"]
  end

  def disk_path(name)
    @pool.lookup_volume_by_name(name).path
  end

  def disk_mklabel(name, parttype)
    disk = {
      :path => disk_path(name),
      :opts => {
        :format => disk_format(name)
      }
    }
    guestfs_disk_helper(disk) do |g, disk_handle|
      g.part_init(disk_handle, parttype)
    end
  end

  def disk_mkpartfs(name, parttype, fstype, opts = {})
    opts[:label] ||= nil
    opts[:luks_password] ||= nil
    disk = {
      :path => disk_path(name),
      :opts => {
        :format => disk_format(name)
      }
    }
    guestfs_disk_helper(disk) do |g, disk_handle|
      g.part_disk(disk_handle, parttype)
      g.part_set_name(disk_handle, 1, opts[:label]) if opts[:label]
      primary_partition = g.list_partitions()[0]
      if opts[:luks_password]
        g.luks_format(primary_partition, opts[:luks_password], 0)
        luks_mapping = File.basename(primary_partition) + "_unlocked"
        g.luks_open(primary_partition, opts[:luks_password], luks_mapping)
        luks_dev = "/dev/mapper/#{luks_mapping}"
        g.mkfs(fstype, luks_dev)
        g.luks_close(luks_dev)
      else
        g.mkfs(fstype, primary_partition)
      end
    end
  end

  def disk_mkswap(name, parttype)
    disk = {
      :path => disk_path(name),
      :opts => {
        :format => disk_format(name)
      }
    }
    guestfs_disk_helper(disk) do |g, disk_handle|
      g.part_disk(disk_handle, parttype)
      primary_partition = g.list_partitions()[0]
      g.mkswap(primary_partition)
    end
  end

  def guestfs_disk_helper(*disks)
    assert(block_given?)
    g = Guestfs::Guestfs.new()
    g.set_trace(1)
    message_callback = Proc.new do |event, _, message, _|
      debug_log("libguestfs: #{Guestfs.event_to_string(event)}: #{message}")
    end
    g.set_event_callback(message_callback,
                         Guestfs::EVENT_TRACE)
    g.set_autosync(1)
    disks.each do |disk|
      g.add_drive_opts(disk[:path], disk[:opts])
    end
    g.launch()
    yield(g, *g.list_devices())
  ensure
    g.close
  end

end