351 lines
12 KiB
Python
351 lines
12 KiB
Python
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# All Rights Reserved.
|
|
# Copyright (c) 2010 Citrix Systems, Inc.
|
|
# Copyright (c) 2011 Piston Cloud Computing, Inc
|
|
# Copyright (c) 2012 University Of Minho
|
|
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
# Copyright (c) 2015 Red Hat, Inc
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""
|
|
Manages information about the guest.
|
|
|
|
This class encapsulates libvirt domain provides certain
|
|
higher level APIs around the raw libvirt API. These APIs are
|
|
then used by all the other libvirt related classes
|
|
"""
|
|
|
|
from lxml import etree
|
|
from oslo_log import log as logging
|
|
from oslo_utils import encodeutils
|
|
from oslo_utils import excutils
|
|
from oslo_utils import importutils
|
|
|
|
from nova.i18n import _LE
|
|
from nova import utils
|
|
from nova.virt.libvirt import config as vconfig
|
|
|
|
libvirt = None
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class Guest(object):
|
|
|
|
def __init__(self, domain):
|
|
|
|
global libvirt
|
|
if libvirt is None:
|
|
libvirt = importutils.import_module('libvirt')
|
|
|
|
self._domain = domain
|
|
|
|
def __repr__(self):
|
|
return "<Guest %(id)d %(name)s %(uuid)s>" % {
|
|
'id': self.id,
|
|
'name': self.name,
|
|
'uuid': self.uuid
|
|
}
|
|
|
|
@property
|
|
def id(self):
|
|
return self._domain.ID()
|
|
|
|
@property
|
|
def uuid(self):
|
|
return self._domain.UUIDString()
|
|
|
|
@property
|
|
def name(self):
|
|
return self._domain.name()
|
|
|
|
@property
|
|
def _encoded_xml(self):
|
|
return encodeutils.safe_decode(self._domain.XMLDesc(0))
|
|
|
|
@classmethod
|
|
def create(cls, xml, host):
|
|
"""Create a new Guest
|
|
|
|
:param xml: XML definition of the domain to create
|
|
:param host: host.Host connection to define the guest on
|
|
|
|
:returns guest.Guest: Guest ready to be launched
|
|
"""
|
|
try:
|
|
# TODO(sahid): Host.write_instance_config should return
|
|
# an instance of Guest
|
|
domain = host.write_instance_config(xml)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Error defining a domain with XML: %s') %
|
|
encodeutils.safe_decode(xml))
|
|
return cls(domain)
|
|
|
|
def launch(self, pause=False):
|
|
"""Starts a created guest.
|
|
|
|
:param pause: Indicates whether to start and pause the guest
|
|
"""
|
|
flags = pause and libvirt.VIR_DOMAIN_START_PAUSED or 0
|
|
try:
|
|
return self._domain.createWithFlags(flags)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Error launching a defined domain '
|
|
'with XML: %s') %
|
|
self._encoded_xml, errors='ignore')
|
|
|
|
def poweroff(self):
|
|
"""Stops a running guest."""
|
|
self._domain.destroy()
|
|
|
|
def resume(self):
|
|
"""Resumes a suspended guest."""
|
|
self._domain.resume()
|
|
|
|
def enable_hairpin(self):
|
|
"""Enables hairpin mode for this guest."""
|
|
interfaces = self.get_interfaces()
|
|
try:
|
|
for interface in interfaces:
|
|
utils.execute(
|
|
'tee',
|
|
'/sys/class/net/%s/brport/hairpin_mode' % interface,
|
|
process_input='1',
|
|
run_as_root=True,
|
|
check_exit_code=[0, 1])
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Error enabling hairpin mode with XML: %s') %
|
|
self._encoded_xml, errors='ignore')
|
|
|
|
def get_interfaces(self):
|
|
"""Returns a list of all network interfaces for this domain."""
|
|
doc = None
|
|
|
|
try:
|
|
doc = etree.fromstring(self._encoded_xml)
|
|
except Exception:
|
|
return []
|
|
|
|
interfaces = []
|
|
|
|
nodes = doc.findall('./devices/interface/target')
|
|
for target in nodes:
|
|
interfaces.append(target.get('dev'))
|
|
|
|
return interfaces
|
|
|
|
def get_vcpus_info(self):
|
|
"""Returns virtual cpus information of guest.
|
|
|
|
:returns: guest.VCPUInfo
|
|
"""
|
|
vcpus = self._domain.vcpus()
|
|
if vcpus is not None:
|
|
for vcpu in vcpus[0]:
|
|
yield VCPUInfo(
|
|
id=vcpu[0], cpu=vcpu[3], state=vcpu[1], time=vcpu[2])
|
|
|
|
def delete_configuration(self):
|
|
"""Undefines a domain from hypervisor."""
|
|
try:
|
|
self._domain.undefineFlags(
|
|
libvirt.VIR_DOMAIN_UNDEFINE_MANAGED_SAVE)
|
|
except libvirt.libvirtError:
|
|
LOG.debug("Error from libvirt during undefineFlags. %d"
|
|
"Retrying with undefine", self.id)
|
|
self._domain.undefine()
|
|
except AttributeError:
|
|
# Older versions of libvirt don't support undefine flags,
|
|
# trying to remove managed image
|
|
try:
|
|
if self._domain.hasManagedSaveImage(0):
|
|
self._domain.managedSaveRemove(0)
|
|
except AttributeError:
|
|
pass
|
|
self._domain.undefine()
|
|
|
|
def has_persistent_configuration(self):
|
|
"""Whether domain config is persistently stored on the host."""
|
|
return self._domain.isPersistent()
|
|
|
|
def attach_device(self, conf, persistent=False, live=False):
|
|
"""Attaches device to the guest.
|
|
|
|
:param conf: A LibvirtConfigObject of the device to attach
|
|
:param persistent: A bool to indicate whether the change is
|
|
persistent or not
|
|
:param live: A bool to indicate whether it affect the guest
|
|
in running state
|
|
"""
|
|
flags = persistent and libvirt.VIR_DOMAIN_AFFECT_CONFIG or 0
|
|
flags |= live and libvirt.VIR_DOMAIN_AFFECT_LIVE or 0
|
|
self._domain.attachDeviceFlags(conf.to_xml(), flags=flags)
|
|
|
|
def get_disk(self, device):
|
|
"""Returns the disk mounted at device
|
|
|
|
:returns LivirtConfigGuestDisk: mounted at device or None
|
|
"""
|
|
try:
|
|
doc = etree.fromstring(self._domain.XMLDesc(0))
|
|
except Exception:
|
|
return None
|
|
node = doc.find("./devices/disk/target[@dev='%s'].." % device)
|
|
if node is not None:
|
|
conf = vconfig.LibvirtConfigGuestDisk()
|
|
conf.parse_dom(node)
|
|
return conf
|
|
|
|
def detach_device(self, conf, persistent=False, live=False):
|
|
"""Detaches device to the guest.
|
|
|
|
:param conf: A LibvirtConfigObject of the device to detach
|
|
:param persistent: A bool to indicate whether the change is
|
|
persistent or not
|
|
:param live: A bool to indicate whether it affect the guest
|
|
in running state
|
|
"""
|
|
flags = persistent and libvirt.VIR_DOMAIN_AFFECT_CONFIG or 0
|
|
flags |= live and libvirt.VIR_DOMAIN_AFFECT_LIVE or 0
|
|
self._domain.detachDeviceFlags(conf.to_xml(), flags=flags)
|
|
|
|
def get_xml_desc(self, dump_inactive=False, dump_sensitive=False,
|
|
dump_migratable=False):
|
|
"""Returns xml description of guest.
|
|
|
|
:param dump_inactive: Dump inactive domain information
|
|
:param dump_sensitive: Dump security sensitive information
|
|
:param dump_migratable: Dump XML suitable for migration
|
|
|
|
:returns string: XML description of the guest
|
|
"""
|
|
flags = dump_inactive and libvirt.VIR_DOMAIN_XML_INACTIVE or 0
|
|
flags |= dump_sensitive and libvirt.VIR_DOMAIN_XML_SECURE or 0
|
|
flags |= dump_migratable and libvirt.VIR_DOMAIN_XML_MIGRATABLE or 0
|
|
return self._domain.XMLDesc(flags=flags)
|
|
|
|
def save_memory_state(self):
|
|
"""Saves the domain's memory state. Requires running domain.
|
|
|
|
raises: raises libvirtError on error
|
|
"""
|
|
self._domain.managedSave(0)
|
|
|
|
def get_block_device(self, disk):
|
|
"""Returns a block device wrapper for disk."""
|
|
return BlockDevice(self, disk)
|
|
|
|
|
|
class BlockDevice(object):
|
|
"""Wrapper around block device API"""
|
|
|
|
REBASE_DEFAULT_BANDWIDTH = 0 # in MiB/s - 0 unlimited
|
|
COMMIT_DEFAULT_BANDWIDTH = 0 # in MiB/s - 0 unlimited
|
|
|
|
def __init__(self, guest, disk):
|
|
self._guest = guest
|
|
self._disk = disk
|
|
|
|
def abort_job(self, async=False, pivot=False):
|
|
"""Request to cancel any job currently running on the block.
|
|
|
|
:param async: Request only, do not wait for completion
|
|
:param pivot: Pivot to new file when ending a copy or
|
|
active commit job
|
|
"""
|
|
flags = async and libvirt.VIR_DOMAIN_BLOCK_JOB_ABORT_ASYNC or 0
|
|
flags |= pivot and libvirt.VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT or 0
|
|
self._guest._domain.blockJobAbort(self._disk, flags=flags)
|
|
|
|
def get_job_info(self):
|
|
"""Returns information about job currently running
|
|
|
|
:returns: BlockDeviceJobInfo or None
|
|
"""
|
|
status = self._guest._domain.blockJobInfo(self._disk, flags=0)
|
|
if status != -1:
|
|
return BlockDeviceJobInfo(
|
|
job=status.get("type", 0),
|
|
bandwidth=status.get("bandwidth", 0),
|
|
cur=status.get("cur", 0),
|
|
end=status.get("end", 0))
|
|
|
|
def rebase(self, base, shallow=False, reuse_ext=False,
|
|
copy=False, relative=False):
|
|
"""Rebases block to new base
|
|
|
|
:param shallow: Limit copy to top of source backing chain
|
|
:param reuse_ext: Reuse existing external file of a copy
|
|
:param copy: Start a copy job
|
|
:param relative: Keep backing chain referenced using relative names
|
|
"""
|
|
flags = shallow and libvirt.VIR_DOMAIN_BLOCK_REBASE_SHALLOW or 0
|
|
flags |= reuse_ext and libvirt.VIR_DOMAIN_BLOCK_REBASE_REUSE_EXT or 0
|
|
flags |= copy and libvirt.VIR_DOMAIN_BLOCK_REBASE_COPY or 0
|
|
flags |= relative and libvirt.VIR_DOMAIN_BLOCK_REBASE_RELATIVE or 0
|
|
return self._guest._domain.blockRebase(
|
|
self._disk, base, self.REBASE_DEFAULT_BANDWIDTH, flags=flags)
|
|
|
|
def commit(self, base, top, relative=False):
|
|
"""Commit on block device
|
|
|
|
For performance during live snapshot it will reduces the disk chain
|
|
to a single disk.
|
|
|
|
:param relative: Keep backing chain referenced using relative names
|
|
"""
|
|
flags = relative and libvirt.VIR_DOMAIN_BLOCK_COMMIT_RELATIVE or 0
|
|
return self._guest._domain.blockCommit(
|
|
self._disk, base, top, self.COMMIT_DEFAULT_BANDWIDTH, flags=flags)
|
|
|
|
def resize(self, size_kb):
|
|
"""Resizes block device to Kib size."""
|
|
self._guest._domain.blockResize(self._disk, size_kb)
|
|
|
|
|
|
class VCPUInfo(object):
|
|
def __init__(self, id, cpu, state, time):
|
|
"""Structure for information about guest vcpus.
|
|
|
|
:param id: The virtual cpu number
|
|
:param cpu: The host cpu currently associated
|
|
:param state: The running state of the vcpu (0 offline, 1 running, 2
|
|
blocked on resource)
|
|
:param time: The cpu time used in nanoseconds
|
|
"""
|
|
self.id = id
|
|
self.cpu = cpu
|
|
self.state = state
|
|
self.time = time
|
|
|
|
|
|
class BlockDeviceJobInfo(object):
|
|
def __init__(self, job, bandwidth, cur, end):
|
|
"""Structure for information about running job.
|
|
|
|
:param job: The running job (0 placeholder, 1 pull,
|
|
2 copy, 3 commit, 4 active commit)
|
|
:param bandwidth: Used in MiB/s
|
|
:param cur: Indicates the position between 0 and 'end'
|
|
:param end: Indicates the position for this operation
|
|
"""
|
|
self.job = job
|
|
self.bandwidth = bandwidth
|
|
self.cur = cur
|
|
self.end = end
|