Andy Bavier | ae1e493 | 2016-02-22 12:07:49 -0500 | [diff] [blame] | 1 | # Copyright (C) 2013 Canonical Ltd. |
| 2 | # Author: Robie Basak <robie.basak@canonical.com> |
| 3 | # |
| 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU Affero General Public License as published by |
| 6 | # the Free Software Foundation, either version 3 of the License, or |
| 7 | # (at your option) any later version. |
| 8 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU Affero General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU Affero General Public License |
| 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | |
| 17 | from __future__ import absolute_import |
| 18 | from __future__ import print_function |
| 19 | from __future__ import unicode_literals |
| 20 | |
| 21 | import codecs |
| 22 | import contextlib |
| 23 | import itertools |
| 24 | import os |
| 25 | import shutil |
| 26 | import subprocess |
| 27 | import tempfile |
| 28 | |
| 29 | import libvirt |
| 30 | from lxml import etree |
| 31 | from lxml.builder import E |
| 32 | |
| 33 | LIBVIRT_DNSMASQ_LEASE_FILE = '/var/lib/libvirt/dnsmasq/default.leases' |
| 34 | |
| 35 | |
| 36 | def get_libvirt_pool_object(libvirt_conn, pool_name): |
| 37 | try: |
| 38 | pool = libvirt_conn.storagePoolLookupByName(pool_name) |
| 39 | except libvirt.libvirtError: |
| 40 | raise RuntimeError("Cannot find pool %s." % repr(pool_name)) |
| 41 | return pool |
| 42 | |
| 43 | |
| 44 | def create_volume_from_fobj(new_volume_name, fobj, image_type='raw', |
| 45 | pool_name='default'): |
| 46 | """Create a new libvirt volume and populate it from a file-like object.""" |
| 47 | |
| 48 | compressed_fobj = tempfile.NamedTemporaryFile() |
| 49 | decompressed_fobj = tempfile.NamedTemporaryFile() |
| 50 | with contextlib.closing(compressed_fobj): |
| 51 | with contextlib.closing(decompressed_fobj): |
| 52 | shutil.copyfileobj(fobj, compressed_fobj) |
| 53 | compressed_fobj.flush() |
| 54 | compressed_fobj.seek(0) # is this necessary? |
| 55 | subprocess.check_call( |
| 56 | [ |
| 57 | 'qemu-img', 'convert', '-f', image_type, '-O', image_type, |
| 58 | compressed_fobj.name, decompressed_fobj.name |
| 59 | ], |
| 60 | shell=False, close_fds=False) |
| 61 | decompressed_fobj.seek(0) # is this necessary? |
| 62 | return _create_volume_from_fobj_with_size( |
| 63 | new_volume_name=new_volume_name, |
| 64 | fobj=decompressed_fobj, |
| 65 | fobj_size=os.fstat(decompressed_fobj.fileno()).st_size, |
| 66 | image_type=image_type, |
| 67 | pool_name=pool_name |
| 68 | ) |
| 69 | |
| 70 | |
| 71 | def _create_volume_from_fobj_with_size(new_volume_name, fobj, fobj_size, |
| 72 | image_type, pool_name): |
| 73 | conn = libvirt.open('qemu:///system') |
| 74 | pool = get_libvirt_pool_object(conn, pool_name) |
| 75 | |
| 76 | if image_type == 'raw': |
| 77 | extra = [E.allocation(str(fobj_size)), E.capacity(str(fobj_size))] |
| 78 | elif image_type == 'qcow2': |
| 79 | extra = [E.capacity('0')] |
| 80 | else: |
| 81 | raise NotImplementedError("Unknown image type %r." % image_type) |
| 82 | |
| 83 | new_vol = E.volume( |
| 84 | E.name(new_volume_name), |
| 85 | E.target(E.format(type=image_type)), |
| 86 | *extra |
| 87 | ) |
| 88 | vol = pool.createXML(etree.tostring(new_vol), 0) |
| 89 | |
| 90 | try: |
| 91 | stream = conn.newStream(0) |
| 92 | vol.upload(stream, 0, fobj_size, 0) |
| 93 | |
| 94 | def handler(stream_ignored, size, opaque_ignored): |
| 95 | return fobj.read(size) |
| 96 | |
| 97 | try: |
| 98 | stream.sendAll(handler, None) |
| 99 | except Exception as e: |
| 100 | try: |
| 101 | # This unexpectedly raises an exception even on a normal call, |
| 102 | # so ignore it. |
| 103 | stream.abort() |
| 104 | except: |
| 105 | pass |
| 106 | raise e |
| 107 | stream.finish() |
| 108 | except: |
| 109 | vol.delete(flags=0) |
| 110 | raise |
| 111 | |
| 112 | return vol |
| 113 | |
| 114 | |
| 115 | def volume_names_in_pool(pool_name='default'): |
| 116 | conn = libvirt.open('qemu:///system') |
| 117 | pool = get_libvirt_pool_object(conn, pool_name) |
| 118 | return pool.listVolumes() |
| 119 | |
| 120 | |
| 121 | def delete_volume_by_name(volume_name, pool_name='default'): |
| 122 | conn = libvirt.open('qemu:///system') |
| 123 | pool = get_libvirt_pool_object(conn, pool_name) |
| 124 | volume = pool.storageVolLookupByName(volume_name) |
| 125 | volume.delete(flags=0) |
| 126 | |
| 127 | |
| 128 | def have_volume_by_name(volume_name, pool_name='default'): |
| 129 | conn = libvirt.open('qemu:///system') |
| 130 | pool = get_libvirt_pool_object(conn, pool_name) |
| 131 | try: |
| 132 | volume = pool.storageVolLookupByName(volume_name) |
| 133 | except libvirt.libvirtError: |
| 134 | return False |
| 135 | else: |
| 136 | return True |
| 137 | |
| 138 | |
| 139 | def _get_all_domains(conn=None): |
| 140 | if conn is None: |
| 141 | conn = libvirt.open('qemu:///system') |
| 142 | |
| 143 | # libvirt in Precise doesn't seem to have a binding for |
| 144 | # virConnectListAllDomains, and it seems that we must enumerate |
| 145 | # defined-by-not-running and running instances separately and in different |
| 146 | # ways. |
| 147 | |
| 148 | for domain_id in conn.listDomainsID(): |
| 149 | yield conn.lookupByID(domain_id) |
| 150 | |
| 151 | for domain_name in conn.listDefinedDomains(): |
| 152 | yield conn.lookupByName(domain_name) |
| 153 | |
| 154 | |
| 155 | def _domain_element_to_volume_paths(element): |
| 156 | assert element.tag == 'domain' |
| 157 | return ( |
| 158 | source.get('file') |
| 159 | for source in element.xpath( |
| 160 | "/domain/devices/disk[@type='file']/source[@file]" |
| 161 | ) |
| 162 | ) |
| 163 | |
| 164 | |
| 165 | def _domain_volume_paths(domain): |
| 166 | volume_paths = set() |
| 167 | |
| 168 | for flags in [0, libvirt.VIR_DOMAIN_XML_INACTIVE]: |
| 169 | element = etree.fromstring(domain.XMLDesc(flags)) |
| 170 | volume_paths.update(_domain_element_to_volume_paths(element)) |
| 171 | |
| 172 | return frozenset(volume_paths) |
| 173 | |
| 174 | |
| 175 | def _volume_element_to_volume_paths(element): |
| 176 | assert element.tag == 'volume' |
| 177 | return itertools.chain( |
| 178 | (path.text for path in element.xpath('/volume/target/path')), |
| 179 | (path.text for path in element.xpath('/volume/backingStore/path')), |
| 180 | ) |
| 181 | |
| 182 | |
| 183 | def _volume_volume_paths(volume): |
| 184 | # Volumes can depend on other volumes ("backing stores"), so return all |
| 185 | # paths a volume needs to function, including the top level one. |
| 186 | volume_paths = set() |
| 187 | |
| 188 | element = etree.fromstring(volume.XMLDesc(0)) |
| 189 | volume_paths.update(_volume_element_to_volume_paths(element)) |
| 190 | |
| 191 | return frozenset(volume_paths) |
| 192 | |
| 193 | |
| 194 | def _get_all_domain_volume_paths(conn=None): |
| 195 | if conn is None: |
| 196 | conn = libvirt.open('qemu:///system') |
| 197 | |
| 198 | all_volume_paths = set() |
| 199 | for domain in _get_all_domains(conn): |
| 200 | for path in _domain_volume_paths(domain): |
| 201 | try: |
| 202 | volume = conn.storageVolLookupByKey(path) |
| 203 | except libvirt.libvirtError: |
| 204 | # ignore a lookup failure, since if a volume doesn't exist, |
| 205 | # it isn't reasonable to consider what backing volumes it may |
| 206 | # have |
| 207 | continue |
| 208 | all_volume_paths.update(_volume_volume_paths(volume)) |
| 209 | |
| 210 | return frozenset(all_volume_paths) |
| 211 | |
| 212 | |
| 213 | def get_all_domain_volume_names(conn=None, filter_by_dir=None): |
| 214 | # Limitation: filter_by_dir must currently end in a '/' and be the |
| 215 | # canonical path as libvirt returns it. Ideally I'd filter by pool instead, |
| 216 | # but the libvirt API appears to not provide any method to find what pool a |
| 217 | # volume is in when looked up by key. |
| 218 | if conn is None: |
| 219 | conn = libvirt.open('qemu:///system') |
| 220 | |
| 221 | for path in _get_all_domain_volume_paths(conn=conn): |
| 222 | volume = conn.storageVolLookupByKey(path) |
| 223 | if filter_by_dir and not volume.path().startswith(filter_by_dir): |
| 224 | continue |
| 225 | yield volume.name() |
| 226 | |
| 227 | |
| 228 | def get_domain_macs(domain_name, conn=None): |
| 229 | if conn is None: |
| 230 | conn = libvirt.open('qemu:///system') |
| 231 | |
| 232 | domain = conn.lookupByName(domain_name) |
| 233 | xml = etree.fromstring(domain.XMLDesc(0)) |
| 234 | for mac in xml.xpath( |
| 235 | "/domain/devices/interface[@type='network' or @type='bridge']/mac[@address]"): |
| 236 | yield mac.get('address') |
| 237 | |
| 238 | |
| 239 | def mac_to_ip(mac): |
| 240 | canonical_mac = mac.lower() |
| 241 | with codecs.open(LIBVIRT_DNSMASQ_LEASE_FILE, 'r') as f: |
| 242 | for line in f: |
| 243 | fields = line.split() |
| 244 | if len(fields) > 1 and fields[1].lower() == canonical_mac: |
| 245 | return fields[2] |
| 246 | return None |