PageRenderTime 138ms CodeModel.GetById 46ms app.highlight 58ms RepoModel.GetById 1ms app.codeStats 2ms

/components/openstack/nova/files/solariszones/driver.py

https://bitbucket.org/dilos/userland-gate
Python | 4830 lines | 4402 code | 149 blank | 279 comment | 207 complexity | 25104b04cf6add9d01b73ed8ea618321 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1# Copyright 2011 Justin Santa Barbara
   2# All Rights Reserved.
   3#
   4# Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
   5#
   6#    Licensed under the Apache License, Version 2.0 (the "License"); you may
   7#    not use this file except in compliance with the License. You may obtain
   8#    a copy of the License at
   9#
  10#         http://www.apache.org/licenses/LICENSE-2.0
  11#
  12#    Unless required by applicable law or agreed to in writing, software
  13#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  14#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  15#    License for the specific language governing permissions and limitations
  16#    under the License.
  17
  18"""
  19Driver for Solaris Zones (nee Containers):
  20"""
  21
  22import base64
  23import glob
  24import os
  25import platform
  26import shutil
  27import tempfile
  28import uuid
  29
  30from collections import defaultdict
  31from openstack_common import get_ovsdb_info
  32import rad.bindings.com.oracle.solaris.rad.archivemgr_1 as archivemgr
  33import rad.bindings.com.oracle.solaris.rad.kstat_2 as kstat
  34import rad.bindings.com.oracle.solaris.rad.zonemgr_1 as zonemgr
  35import rad.client
  36import rad.connect
  37from solaris_install.target.size import Size
  38
  39from cinderclient import exceptions as cinder_exception
  40from cinderclient.v1 import client as v1_client
  41from eventlet import greenthread
  42from keystoneclient import exceptions as keystone_exception
  43from lxml import etree
  44from oslo_concurrency import lockutils, processutils
  45from oslo_config import cfg
  46from oslo_log import log as logging
  47from oslo_serialization import jsonutils
  48from oslo_utils import excutils
  49from oslo_utils import fileutils
  50from oslo_utils import strutils
  51from oslo_utils import versionutils
  52from passlib.hash import sha256_crypt
  53
  54from nova.api.metadata import password
  55from nova.compute import arch
  56from nova.compute import hv_type
  57from nova.compute import power_state
  58from nova.compute import task_states
  59from nova.compute import vm_mode
  60from nova.compute import vm_states
  61from nova import conductor
  62import nova.conf
  63from nova.console import type as ctype
  64from nova import context as nova_context
  65from nova import crypto
  66from nova import exception
  67from nova.i18n import _, _LE, _LI
  68from nova.image import API as glance_api
  69from nova.image import glance
  70from nova.network.neutronv2 import api as neutronv2_api
  71from nova import objects
  72from nova.objects import flavor as flavor_obj
  73from nova.objects import migrate_data as migrate_data_obj
  74from nova import utils
  75from nova.virt import driver
  76from nova.virt import event as virtevent
  77from nova.virt import hardware
  78from nova.virt import images
  79from nova.virt.solariszones import sysconfig
  80from nova.volume.cinder import API
  81from nova.volume.cinder import cinderclient
  82from nova.volume.cinder import translate_volume_exception
  83from nova.volume.cinder import _untranslate_volume_summary_view
  84
  85solariszones_opts = [
  86    cfg.StrOpt('boot_volume_type',
  87               default=None,
  88               help='Cinder volume type to use for boot volumes'),
  89    cfg.StrOpt('boot_volume_az',
  90               default=None,
  91               help='Cinder availability zone to use for boot volumes'),
  92    cfg.StrOpt('glancecache_dirname',
  93               default='/var/share/nova/images',
  94               help='Default path to Glance cache for Solaris Zones.'),
  95    cfg.StrOpt('live_migration_cipher',
  96               help='Cipher to use for encryption of memory traffic during '
  97                    'live migration. If not specified, a common encryption '
  98                    'algorithm will be negotiated. Options include: none or '
  99                    'the name of a supported OpenSSL cipher algorithm.'),
 100    cfg.StrOpt('solariszones_snapshots_directory',
 101               default='$instances_path/snapshots',
 102               help='Location to store snapshots before uploading them to the '
 103                    'Glance image service.'),
 104    cfg.StrOpt('zones_suspend_path',
 105               default='/var/share/zones/SYSsuspend',
 106               help='Default path for suspend images for Solaris Zones.'),
 107    cfg.BoolOpt('solariszones_boot_options',
 108                default=True,
 109                help='Allow kernel boot options to be set in instance '
 110                     'metadata.'),
 111]
 112
 113CONF = nova.conf.CONF
 114CONF.register_opts(solariszones_opts, 'solariszones')
 115LOG = logging.getLogger(__name__)
 116
 117# These should match the strings returned by the zone_state_str()
 118# function in the (private) libzonecfg library. These values are in turn
 119# returned in the 'state' string of the Solaris Zones' RAD interface by
 120# the zonemgr(3RAD) provider.
 121ZONE_STATE_CONFIGURED = 'configured'
 122ZONE_STATE_INCOMPLETE = 'incomplete'
 123ZONE_STATE_UNAVAILABLE = 'unavailable'
 124ZONE_STATE_INSTALLED = 'installed'
 125ZONE_STATE_READY = 'ready'
 126ZONE_STATE_RUNNING = 'running'
 127ZONE_STATE_SHUTTING_DOWN = 'shutting_down'
 128ZONE_STATE_DOWN = 'down'
 129ZONE_STATE_MOUNTED = 'mounted'
 130
 131# Mapping between zone state and Nova power_state.
 132SOLARISZONES_POWER_STATE = {
 133    ZONE_STATE_CONFIGURED:      power_state.NOSTATE,
 134    ZONE_STATE_INCOMPLETE:      power_state.NOSTATE,
 135    ZONE_STATE_UNAVAILABLE:     power_state.NOSTATE,
 136    ZONE_STATE_INSTALLED:       power_state.SHUTDOWN,
 137    ZONE_STATE_READY:           power_state.RUNNING,
 138    ZONE_STATE_RUNNING:         power_state.RUNNING,
 139    ZONE_STATE_SHUTTING_DOWN:   power_state.RUNNING,
 140    ZONE_STATE_DOWN:            power_state.RUNNING,
 141    ZONE_STATE_MOUNTED:         power_state.NOSTATE
 142}
 143
 144# Solaris Zones brands as defined in brands(5).
 145ZONE_BRAND_LABELED = 'labeled'
 146ZONE_BRAND_SOLARIS = 'solaris'
 147ZONE_BRAND_SOLARIS_KZ = 'solaris-kz'
 148ZONE_BRAND_SOLARIS10 = 'solaris10'
 149
 150# Mapping between supported zone brands and the name of the corresponding
 151# brand template.
 152ZONE_BRAND_TEMPLATE = {
 153    ZONE_BRAND_SOLARIS:         'SYSdefault',
 154    ZONE_BRAND_SOLARIS_KZ:      'SYSsolaris-kz',
 155}
 156
 157MAX_CONSOLE_BYTES = 102400
 158
 159VNC_CONSOLE_BASE_FMRI = 'svc:/application/openstack/nova/zone-vnc-console'
 160# Required in order to create a zone VNC console SMF service instance
 161VNC_SERVER_PATH = '/usr/bin/vncserver'
 162XTERM_PATH = '/usr/bin/xterm'
 163
 164ROOTZPOOL_RESOURCE = 'rootzpool'
 165
 166# The underlying Solaris Zones framework does not expose a specific
 167# version number, instead relying on feature tests to identify what is
 168# and what is not supported. A HYPERVISOR_VERSION is defined here for
 169# Nova's use but it generally should not be changed unless there is a
 170# incompatible change such as concerning kernel zone live migration.
 171HYPERVISOR_VERSION = '5.11'
 172
 173shared_storage = ['iscsi', 'fibre_channel']
 174
 175KSTAT_TYPE = {
 176    'NVVT_STR': 'string',
 177    'NVVT_STRS': 'strings',
 178    'NVVT_INT': 'integer',
 179    'NVVT_INTS': 'integers',
 180    'NVVT_KSTAT': 'kstat',
 181}
 182
 183
 184def lookup_resource(zone, resource):
 185    """Lookup specified resource from specified Solaris Zone."""
 186    try:
 187        val = zone.getResources(zonemgr.Resource(resource))
 188    except rad.client.ObjectError:
 189        return None
 190    except Exception:
 191        raise
 192    return val[0] if val else None
 193
 194
 195def lookup_resource_property(zone, resource, prop, filter=None):
 196    """Lookup specified property from specified Solaris Zone resource."""
 197    try:
 198        val = zone.getResourceProperties(zonemgr.Resource(resource, filter),
 199                                         [prop])
 200    except rad.client.ObjectError:
 201        return None
 202    except Exception:
 203        raise
 204    return val[0].value if val else None
 205
 206
 207def lookup_resource_property_value(zone, resource, prop, value):
 208    """Lookup specified property with value from specified Solaris Zone
 209    resource. Returns resource object if matching value is found, else None
 210    """
 211    try:
 212        resources = zone.getResources(zonemgr.Resource(resource))
 213        for resource in resources:
 214            for propertee in resource.properties:
 215                if propertee.name == prop and propertee.value == value:
 216                    return resource
 217        else:
 218            return None
 219    except rad.client.ObjectError:
 220        return None
 221    except Exception:
 222        raise
 223
 224
 225def zonemgr_strerror(ex):
 226    """Format the payload from a zonemgr(3RAD) rad.client.ObjectError
 227    exception into a sensible error string that can be logged. Newlines
 228    are converted to a colon-space string to create a single line.
 229
 230    If the exception was something other than rad.client.ObjectError,
 231    just return it as a string.
 232    """
 233    if not isinstance(ex, rad.client.ObjectError):
 234        return str(ex)
 235    payload = ex.get_payload()
 236    if payload.code == zonemgr.ErrorCode.NONE:
 237        return str(ex)
 238    error = [str(payload.code)]
 239    if payload.str is not None and payload.str != '':
 240        error.append(payload.str)
 241    if payload.stderr is not None and payload.stderr != '':
 242        stderr = payload.stderr.rstrip()
 243        error.append(stderr.replace('\n', ': '))
 244    result = ': '.join(error)
 245    return result
 246
 247
 248class MemoryAlignmentIncorrect(exception.FlavorMemoryTooSmall):
 249    msg_fmt = _("Requested flavor, %(flavor)s, memory size %(memsize)s does "
 250                "not align on %(align)s boundary.")
 251
 252
 253class SolarisVolumeAPI(API):
 254    """ Extending the volume api to support additional cinder sub-commands
 255    """
 256    @translate_volume_exception
 257    def create(self, context, size, name, description, snapshot=None,
 258               image_id=None, volume_type=None, metadata=None,
 259               availability_zone=None, source_volume=None):
 260        """Clone the source volume by calling the cinderclient version of
 261        create with a source_volid argument
 262
 263        :param context: the context for the clone
 264        :param size: size of the new volume, must be the same as the source
 265            volume
 266        :param name: display_name of the new volume
 267        :param description: display_description of the new volume
 268        :param snapshot: Snapshot object
 269        :param image_id: image_id to create the volume from
 270        :param volume_type: type of volume
 271        :param metadata: Additional metadata for the volume
 272        :param availability_zone: zone:host where the volume is to be created
 273        :param source_volume: Volume object
 274
 275        Returns a volume object
 276        """
 277        client = cinderclient(context)
 278
 279        if snapshot is not None:
 280            snapshot_id = snapshot['id']
 281        else:
 282            snapshot_id = None
 283
 284        if source_volume is not None:
 285            source_volid = source_volume['id']
 286        else:
 287            source_volid = None
 288
 289        kwargs = dict(snapshot_id=snapshot_id,
 290                      volume_type=volume_type,
 291                      user_id=context.user_id,
 292                      project_id=context.project_id,
 293                      availability_zone=availability_zone,
 294                      metadata=metadata,
 295                      imageRef=image_id,
 296                      source_volid=source_volid)
 297
 298        if isinstance(client, v1_client.Client):
 299            kwargs['display_name'] = name
 300            kwargs['display_description'] = description
 301        else:
 302            kwargs['name'] = name
 303            kwargs['description'] = description
 304
 305        try:
 306            item = cinderclient(context).volumes.create(size, **kwargs)
 307            return _untranslate_volume_summary_view(context, item)
 308        except cinder_exception.OverLimit:
 309            raise exception.OverQuota(overs='volumes')
 310        except (cinder_exception.BadRequest,
 311                keystone_exception.BadRequest) as reason:
 312            raise exception.InvalidInput(reason=reason)
 313
 314    @translate_volume_exception
 315    def update(self, context, volume_id, fields):
 316        """Update the fields of a volume for example used to rename a volume
 317        via a call to cinderclient
 318
 319        :param context: the context for the update
 320        :param volume_id: the id of the volume to update
 321        :param fields: a dictionary of of the name/value pairs to update
 322        """
 323        cinderclient(context).volumes.update(volume_id, **fields)
 324
 325    @translate_volume_exception
 326    def extend(self, context, volume, newsize):
 327        """Extend the size of a cinder volume by calling the cinderclient
 328
 329        :param context: the context for the extend
 330        :param volume: the volume object to extend
 331        :param newsize: the new size of the volume in GB
 332        """
 333        cinderclient(context).volumes.extend(volume, newsize)
 334
 335
 336class ZoneConfig(object):
 337    """ZoneConfig - context manager for access zone configurations.
 338    Automatically opens the configuration for a zone and commits any changes
 339    before exiting
 340    """
 341    def __init__(self, zone):
 342        """zone is a zonemgr object representing either a kernel zone or
 343        non-global zone.
 344        """
 345        self.zone = zone
 346        self.editing = False
 347
 348    def __enter__(self):
 349        """enables the editing of the zone."""
 350        try:
 351            self.zone.editConfig()
 352            self.editing = True
 353            return self
 354        except Exception as ex:
 355            reason = zonemgr_strerror(ex)
 356            LOG.exception(_("Unable to initialize editing of instance '%s' "
 357                            "via zonemgr(3RAD): %s")
 358                          % (self.zone.name, reason))
 359            raise
 360
 361    def __exit__(self, exc_type, exc_val, exc_tb):
 362        """looks for any kind of exception before exiting.  If one is found,
 363        cancel any configuration changes and reraise the exception.  If not,
 364        commit the new configuration.
 365        """
 366        if exc_type is not None and self.editing:
 367            # We received some kind of exception.  Cancel the config and raise.
 368            self.zone.cancelConfig()
 369            raise
 370        else:
 371            # commit the config
 372            try:
 373                self.zone.commitConfig()
 374            except Exception as ex:
 375                reason = zonemgr_strerror(ex)
 376                LOG.exception(_("Unable to commit the new configuration for "
 377                                "instance '%s' via zonemgr(3RAD): %s")
 378                              % (self.zone.name, reason))
 379
 380                # Last ditch effort to cleanup.
 381                self.zone.cancelConfig()
 382                raise
 383
 384    def setprop(self, resource, prop, value):
 385        """sets a property for an existing resource OR creates a new resource
 386        with the given property(s).
 387        """
 388        current = lookup_resource_property(self.zone, resource, prop)
 389        if current is not None and current == value:
 390            # the value is already set
 391            return
 392
 393        try:
 394            if current is None:
 395                self.zone.addResource(zonemgr.Resource(
 396                    resource, [zonemgr.Property(prop, value)]))
 397            else:
 398                self.zone.setResourceProperties(
 399                    zonemgr.Resource(resource),
 400                    [zonemgr.Property(prop, value)])
 401        except Exception as ex:
 402            reason = zonemgr_strerror(ex)
 403            LOG.exception(_("Unable to set '%s' property on '%s' resource for "
 404                            "instance '%s' via zonemgr(3RAD): %s")
 405                          % (prop, resource, self.zone.name, reason))
 406            raise
 407
 408    def addresource(self, resource, props=None, ignore_exists=False):
 409        """creates a new resource with an optional property list, or set the
 410        property if the resource exists and ignore_exists is true.
 411
 412        :param ignore_exists: If the resource exists, set the property for the
 413            resource.
 414        """
 415        if props is None:
 416            props = []
 417
 418        try:
 419            self.zone.addResource(zonemgr.Resource(resource, props))
 420        except Exception as ex:
 421            if isinstance(ex, rad.client.ObjectError):
 422                code = ex.get_payload().code
 423                if (ignore_exists and
 424                        code == zonemgr.ErrorCode.RESOURCE_ALREADY_EXISTS):
 425                    self.zone.setResourceProperties(
 426                        zonemgr.Resource(resource, None), props)
 427                    return
 428            reason = zonemgr_strerror(ex)
 429            LOG.exception(_("Unable to create new resource '%s' for instance "
 430                            "'%s' via zonemgr(3RAD): %s")
 431                          % (resource, self.zone.name, reason))
 432            raise
 433
 434    def removeresources(self, resource, props=None):
 435        """removes resources whose properties include the optional property
 436        list specified in props.
 437        """
 438        if props is None:
 439            props = []
 440
 441        try:
 442            self.zone.removeResources(zonemgr.Resource(resource, props))
 443        except Exception as ex:
 444            reason = zonemgr_strerror(ex)
 445            LOG.exception(_("Unable to remove resource '%s' for instance '%s' "
 446                            "via zonemgr(3RAD): %s")
 447                          % (resource, self.zone.name, reason))
 448            raise
 449
 450    def clear_resource_props(self, resource, props):
 451        """Clear property values of a given resource
 452        """
 453        try:
 454            self.zone.clearResourceProperties(zonemgr.Resource(resource, None),
 455                                              props)
 456        except rad.client.ObjectError as ex:
 457            reason = zonemgr_strerror(ex)
 458            LOG.exception(_("Unable to clear '%s' property on '%s' resource "
 459                            "for instance '%s' via zonemgr(3RAD): %s")
 460                          % (props, resource, self.zone.name, reason))
 461            raise
 462
 463
 464class SolarisZonesDriver(driver.ComputeDriver):
 465    """Solaris Zones Driver using the zonemgr(3RAD) and kstat(3RAD) providers.
 466
 467    The interface to this class talks in terms of 'instances' (Amazon EC2 and
 468    internal Nova terminology), by which we mean 'running virtual machine'
 469    (XenAPI terminology) or domain (Xen or libvirt terminology).
 470
 471    An instance has an ID, which is the identifier chosen by Nova to represent
 472    the instance further up the stack.  This is unfortunately also called a
 473    'name' elsewhere.  As far as this layer is concerned, 'instance ID' and
 474    'instance name' are synonyms.
 475
 476    Note that the instance ID or name is not human-readable or
 477    customer-controlled -- it's an internal ID chosen by Nova.  At the
 478    nova.virt layer, instances do not have human-readable names at all -- such
 479    things are only known higher up the stack.
 480
 481    Most virtualization platforms will also have their own identity schemes,
 482    to uniquely identify a VM or domain.  These IDs must stay internal to the
 483    platform-specific layer, and never escape the connection interface.  The
 484    platform-specific layer is responsible for keeping track of which instance
 485    ID maps to which platform-specific ID, and vice versa.
 486
 487    Some methods here take an instance of nova.compute.service.Instance.  This
 488    is the data structure used by nova.compute to store details regarding an
 489    instance, and pass them into this layer.  This layer is responsible for
 490    translating that generic data structure into terms that are specific to the
 491    virtualization platform.
 492
 493    """
 494
 495    capabilities = {
 496        "has_imagecache": False,
 497        "supports_recreate": True,
 498        "supports_migrate_to_same_host": False
 499    }
 500
 501    def __init__(self, virtapi):
 502        self.virtapi = virtapi
 503        self._archive_manager = None
 504        self._compute_event_callback = None
 505        self._conductor_api = conductor.API()
 506        self._fc_hbas = None
 507        self._fc_wwnns = None
 508        self._fc_wwpns = None
 509        self._host_stats = {}
 510        self._initiator = None
 511        self._install_engine = None
 512        self._kstat_control = None
 513        self._pagesize = os.sysconf('SC_PAGESIZE')
 514        self._rad_connection = None
 515        self._rootzpool_suffix = ROOTZPOOL_RESOURCE
 516        self._uname = os.uname()
 517        self._validated_archives = list()
 518        self._volume_api = SolarisVolumeAPI()
 519        self._zone_manager = None
 520
 521    @property
 522    def rad_connection(self):
 523        if self._rad_connection is None:
 524            self._rad_connection = rad.connect.connect_unix()
 525        else:
 526            # taken from rad.connect.RadConnection.__repr__ to look for a
 527            # closed connection
 528            if self._rad_connection._closed is not None:
 529                # the RAD connection has been lost.  Reconnect to RAD
 530                self._rad_connection = rad.connect.connect_unix()
 531
 532        return self._rad_connection
 533
 534    @property
 535    def zone_manager(self):
 536        try:
 537            if (self._zone_manager is None or
 538                    self._zone_manager._conn._closed is not None):
 539                self._zone_manager = self.rad_connection.get_object(
 540                    zonemgr.ZoneManager())
 541        except Exception as ex:
 542            reason = _("Unable to obtain RAD object: %s") % ex
 543            raise exception.NovaException(reason)
 544
 545        return self._zone_manager
 546
 547    @property
 548    def kstat_control(self):
 549        try:
 550            if (self._kstat_control is None or
 551                    self._kstat_control._conn._closed is not None):
 552                self._kstat_control = self.rad_connection.get_object(
 553                    kstat.Control())
 554        except Exception as ex:
 555            reason = _("Unable to obtain RAD object: %s") % ex
 556            raise exception.NovaException(reason)
 557
 558        return self._kstat_control
 559
 560    @property
 561    def archive_manager(self):
 562        try:
 563            if (self._archive_manager is None or
 564                    self._archive_manager._conn._closed is not None):
 565                self._archive_manager = self.rad_connection.get_object(
 566                    archivemgr.ArchiveManager())
 567        except Exception as ex:
 568            reason = _("Unable to obtain RAD object: %s") % ex
 569            raise exception.NovaException(reason)
 570
 571        return self._archive_manager
 572
 573    def init_host(self, host):
 574        """Initialize anything that is necessary for the driver to function,
 575        including catching up with currently running VM's on the given host.
 576        """
 577        # TODO(Vek): Need to pass context in for access to auth_token
 578        pass
 579
 580    def cleanup_host(self, host):
 581        """Clean up anything that is necessary for the driver gracefully stop,
 582        including ending remote sessions. This is optional.
 583        """
 584        pass
 585
 586    def _get_fc_hbas(self):
 587        """Get Fibre Channel HBA information."""
 588        if self._fc_hbas:
 589            return self._fc_hbas
 590
 591        out = None
 592        try:
 593            out, err = utils.execute('/usr/sbin/fcinfo', 'hba-port')
 594        except processutils.ProcessExecutionError:
 595            return []
 596
 597        if out is None:
 598            raise RuntimeError(_("Cannot find any Fibre Channel HBAs"))
 599
 600        hbas = []
 601        hba = {}
 602        for line in out.splitlines():
 603            line = line.strip()
 604            # Collect the following hba-port data:
 605            # 1: Port WWN
 606            # 2: State (online|offline)
 607            # 3: Node WWN
 608            if line.startswith("HBA Port WWN:"):
 609                # New HBA port entry
 610                hba = {}
 611                wwpn = line.split()[-1]
 612                hba['port_name'] = wwpn
 613                continue
 614            elif line.startswith("Port Mode:"):
 615                mode = line.split()[-1]
 616                # Skip Target mode ports
 617                if mode != 'Initiator':
 618                    break
 619            elif line.startswith("State:"):
 620                state = line.split()[-1]
 621                hba['port_state'] = state
 622                continue
 623            elif line.startswith("Node WWN:"):
 624                wwnn = line.split()[-1]
 625                hba['node_name'] = wwnn
 626                continue
 627            if len(hba) == 3:
 628                hbas.append(hba)
 629                hba = {}
 630        self._fc_hbas = hbas
 631        return self._fc_hbas
 632
 633    def _get_fc_wwnns(self):
 634        """Get Fibre Channel WWNNs from the system, if any."""
 635        hbas = self._get_fc_hbas()
 636
 637        wwnns = []
 638        for hba in hbas:
 639            if hba['port_state'] == 'online':
 640                wwnn = hba['node_name']
 641                wwnns.append(wwnn)
 642        return wwnns
 643
 644    def _get_fc_wwpns(self):
 645        """Get Fibre Channel WWPNs from the system, if any."""
 646        hbas = self._get_fc_hbas()
 647
 648        wwpns = []
 649        for hba in hbas:
 650            if hba['port_state'] == 'online':
 651                wwpn = hba['port_name']
 652                wwpns.append(wwpn)
 653        return wwpns
 654
 655    def _get_iscsi_initiator(self):
 656        """ Return the iSCSI initiator node name IQN for this host """
 657        try:
 658            out, err = utils.execute('/usr/sbin/iscsiadm', 'list',
 659                                     'initiator-node')
 660            # Sample first line of command output:
 661            # Initiator node name: iqn.1986-03.com.sun:01:e00000000000.4f757217
 662            initiator_name_line = out.splitlines()[0]
 663            initiator_iqn = initiator_name_line.rsplit(' ', 1)[1]
 664            return initiator_iqn
 665        except processutils.ProcessExecutionError as ex:
 666            LOG.info(_("Failed to get the initiator-node info: %s") % (ex))
 667            return None
 668
 669    def _get_zone_by_name(self, name):
 670        """Return a Solaris Zones object via RAD by name."""
 671        try:
 672            zone = self.rad_connection.get_object(
 673                zonemgr.Zone(), rad.client.ADRGlobPattern({'name': name}))
 674        except rad.client.NotFoundError:
 675            return None
 676        except Exception:
 677            raise
 678        return zone
 679
 680    def _get_state(self, zone):
 681        """Return the running state, one of the power_state codes."""
 682        return SOLARISZONES_POWER_STATE[zone.state]
 683
 684    def _pages_to_kb(self, pages):
 685        """Convert a number of pages of memory into a total size in KBytes."""
 686        return (pages * self._pagesize) / 1024
 687
 688    def _get_max_mem(self, zone):
 689        """Return the maximum memory in KBytes allowed."""
 690        if zone.brand == ZONE_BRAND_SOLARIS:
 691            mem_resource = 'swap'
 692        else:
 693            mem_resource = 'physical'
 694
 695        max_mem = lookup_resource_property(zone, 'capped-memory', mem_resource)
 696        if max_mem is not None:
 697            return strutils.string_to_bytes("%sB" % max_mem) / 1024
 698
 699        # If physical property in capped-memory doesn't exist, this may
 700        # represent a non-global zone so just return the system's total
 701        # memory.
 702        return self._pages_to_kb(os.sysconf('SC_PHYS_PAGES'))
 703
 704    def _get_mem(self, zone):
 705        """Return the memory in KBytes used by the domain."""
 706
 707        # There isn't any way of determining this from the hypervisor
 708        # perspective in Solaris, so just return the _get_max_mem() value
 709        # for now.
 710        return self._get_max_mem(zone)
 711
 712    def _get_num_cpu(self, zone):
 713        """Return the number of virtual CPUs for the domain.
 714
 715        In the case of kernel zones, the number of virtual CPUs a zone
 716        ends up with depends on whether or not there were 'virtual-cpu'
 717        or 'dedicated-cpu' resources in the configuration or whether
 718        there was an assigned pool in the configuration. This algorithm
 719        attempts to emulate what the virtual platform code does to
 720        determine a number of virtual CPUs to use.
 721        """
 722        # If a 'virtual-cpu' resource exists, use the minimum number of
 723        # CPUs defined there.
 724        ncpus = lookup_resource_property(zone, 'virtual-cpu', 'ncpus')
 725        if ncpus is not None:
 726            min = ncpus.split('-', 1)[0]
 727            if min.isdigit():
 728                return int(min)
 729
 730        # Otherwise if a 'dedicated-cpu' resource exists, use the maximum
 731        # number of CPUs defined there.
 732        ncpus = lookup_resource_property(zone, 'dedicated-cpu', 'ncpus')
 733        if ncpus is not None:
 734            max = ncpus.split('-', 1)[-1]
 735            if max.isdigit():
 736                return int(max)
 737
 738        # Finally if neither resource exists but the zone was assigned a
 739        # pool in the configuration, the number of CPUs would be the size
 740        # of the processor set. Currently there's no way of easily
 741        # determining this so use the system's notion of the total number
 742        # of online CPUs.
 743        return os.sysconf('SC_NPROCESSORS_ONLN')
 744
 745    def _kstat_data(self, uri):
 746        """Return Kstat snapshot data via RAD as a dictionary."""
 747        if not isinstance(uri, str):
 748            raise exception.NovaException("kstat URI must be string type: "
 749                                          "%s is %s" % (uri, type(uri)))
 750
 751        if not uri.startswith("kstat:/"):
 752            uri = "kstat:/" + uri
 753
 754        try:
 755            self.kstat_control.update()
 756            kstat_obj = self.rad_connection.get_object(
 757                kstat.Kstat(), rad.client.ADRGlobPattern({"uri": uri}))
 758
 759        except Exception as reason:
 760            LOG.info(_("Unable to retrieve kstat object '%s' via kstat(3RAD): "
 761                       "%s") % (uri, reason))
 762            return None
 763
 764        ks_data = {}
 765        for name, data in kstat_obj.getMap().items():
 766            ks_data[name] = getattr(data, KSTAT_TYPE[str(data.type)])
 767
 768        return ks_data
 769
 770    def _sum_kstat_statistic(self, kstat_data, statistic):
 771        total = 0
 772        for ks in kstat_data.values():
 773            data = ks.getMap()[statistic]
 774            value = getattr(data, KSTAT_TYPE[str(data.type)])
 775            try:
 776                total += value
 777            except TypeError:
 778                LOG.error(_("Unable to aggregate non-summable kstat %s;%s "
 779                            " of type %s") % (ks.getParent().uri, statistic,
 780                                              type(value)))
 781                return None
 782
 783        return total
 784
 785    def _get_kstat_statistic(self, ks, statistic):
 786        if not isinstance(ks, kstat.Kstat):
 787            reason = (_("Attempted to get a kstat from %s type.") % (type(ks)))
 788            raise TypeError(reason)
 789
 790        try:
 791            data = ks.getMap()[statistic]
 792            value = getattr(data, KSTAT_TYPE[str(data.type)])
 793        except TypeError:
 794            value = None
 795
 796        return value
 797
 798    def _get_cpu_time(self, zone):
 799        """Return the CPU time used in nanoseconds."""
 800        if zone.id == -1:
 801            return 0
 802
 803        # The retry value of 3 was determined by the "we shouldn't hit this
 804        # often, but if we do it should resolve quickly so try again"+1
 805        # algorithm.
 806        for _attempt in range(3):
 807            total = 0
 808
 809            accum_uri = "kstat:/zones/cpu/sys_zone_accum/%d" % zone.id
 810            uri = "kstat:/zones/cpu/sys_zone_%d" % zone.id
 811
 812            initial = self._kstat_data(accum_uri)
 813            cpus = self._kstat_data(uri)
 814
 815            total += self._sum_kstat_statistic(cpus, 'cpu_nsec_kernel_cur')
 816            total += self._sum_kstat_statistic(cpus, 'cpu_nsec_user_cur')
 817
 818            final = self._kstat_data(accum_uri)
 819
 820            if initial['gen_num'] == final['gen_num']:
 821                total += initial['cpu_nsec_user'] + initial['cpu_nsec_kernel']
 822                return total
 823
 824        LOG.error(_("Unable to get accurate cpu usage beacuse cpu list "
 825                    "keeps changing"))
 826        return 0
 827
 828    def get_info(self, instance):
 829        """Get the current status of an instance, by name (not ID!)
 830
 831        :param instance: nova.objects.instance.Instance object
 832
 833        Returns a InstanceInfo object
 834        """
 835        # TODO(Vek): Need to pass context in for access to auth_token
 836        name = instance['name']
 837        zone = self._get_zone_by_name(name)
 838        if zone is None:
 839            raise exception.InstanceNotFound(instance_id=name)
 840        return hardware.InstanceInfo(state=self._get_state(zone),
 841                                     max_mem_kb=self._get_max_mem(zone),
 842                                     mem_kb=self._get_mem(zone),
 843                                     num_cpu=self._get_num_cpu(zone),
 844                                     cpu_time_ns=self._get_cpu_time(zone))
 845
 846    def get_num_instances(self):
 847        """Return the total number of virtual machines.
 848
 849        Return the number of virtual machines that the hypervisor knows
 850        about.
 851
 852        .. note::
 853
 854            This implementation works for all drivers, but it is
 855            not particularly efficient. Maintainers of the virt drivers are
 856            encouraged to override this method with something more
 857            efficient.
 858        """
 859        return len(self.list_instances())
 860
 861    def instance_exists(self, instance):
 862        """Checks existence of an instance on the host.
 863
 864        :param instance: The instance to lookup
 865
 866        Returns True if an instance with the supplied ID exists on
 867        the host, False otherwise.
 868
 869        .. note::
 870
 871            This implementation works for all drivers, but it is
 872            not particularly efficient. Maintainers of the virt drivers are
 873            encouraged to override this method with something more
 874            efficient.
 875        """
 876        try:
 877            return instance.uuid in self.list_instance_uuids()
 878        except NotImplementedError:
 879            return instance.name in self.list_instances()
 880
 881    def estimate_instance_overhead(self, instance_info):
 882        """Estimate the virtualization overhead required to build an instance
 883        of the given flavor.
 884
 885        Defaults to zero, drivers should override if per-instance overhead
 886        calculations are desired.
 887
 888        :param instance_info: Instance/flavor to calculate overhead for.
 889        :returns: Dict of estimated overhead values.
 890        """
 891        return {'memory_mb': 0}
 892
 893    def _get_list_zone_object(self):
 894        """Return a list of all Solaris Zones objects via RAD."""
 895        return self.rad_connection.list_objects(zonemgr.Zone())
 896
 897    def list_instances(self):
 898        """Return the names of all the instances known to the virtualization
 899        layer, as a list.
 900        """
 901        # TODO(Vek): Need to pass context in for access to auth_token
 902        instances_list = []
 903        for zone in self._get_list_zone_object():
 904            instances_list.append(self.rad_connection.get_object(zone).name)
 905        return instances_list
 906
 907    def list_instance_uuids(self):
 908        """Return the UUIDS of all the instances known to the virtualization
 909        layer, as a list.
 910        """
 911        raise NotImplementedError()
 912
 913    def _rebuild_block_devices(self, context, instance, bdms, recreate):
 914        root_ci = None
 915        rootmp = instance['root_device_name']
 916        for entry in bdms:
 917            if entry['connection_info'] is None:
 918                continue
 919
 920            if entry['device_name'] == rootmp:
 921                root_ci = jsonutils.loads(entry['connection_info'])
 922                # Let's make sure this is a well formed connection_info, by
 923                # checking if it has a serial key that represents the
 924                # volume_id. If not check to see if the block device has a
 925                # volume_id, if so then assign this to the root_ci.serial.
 926                #
 927                # If we cannot repair the connection_info then simply do not
 928                # return a root_ci and let the caller decide if they want to
 929                # fail or not.
 930                if root_ci.get('serial') is None:
 931                    if entry.get('volume_id') is not None:
 932                        root_ci['serial'] = entry['volume_id']
 933                    else:
 934                        LOG.debug(_("Unable to determine the volume id for "
 935                                    "the connection info for the root device "
 936                                    "for instance '%s'") % instance['name'])
 937                        root_ci = None
 938
 939                continue
 940
 941            if not recreate:
 942                ci = jsonutils.loads(entry['connection_info'])
 943                self.detach_volume(ci, instance, entry['device_name'])
 944
 945        if root_ci is None and recreate:
 946            msg = (_("Unable to find the root device for instance '%s'.")
 947                   % instance['name'])
 948            raise exception.NovaException(msg)
 949
 950        return root_ci
 951
 952    def _set_instance_metahostid(self, instance):
 953        """Attempt to get the hostid from the current configured zone and
 954        return the hostid.  Otherwise return None, and do not set the hostid in
 955        the instance
 956        """
 957        hostid = instance.system_metadata.get('hostid')
 958        if hostid is not None:
 959            return hostid
 960
 961        zone = self._get_zone_by_name(instance['name'])
 962        if zone is None:
 963            return None
 964
 965        hostid = lookup_resource_property(zone, 'global', 'hostid')
 966        if hostid:
 967            instance.system_metadata['hostid'] = hostid
 968
 969        return hostid
 970
 971    def rebuild(self, context, instance, image_meta, injected_files,
 972                admin_password, bdms, detach_block_devices,
 973                attach_block_devices, network_info=None,
 974                recreate=False, block_device_info=None,
 975                preserve_ephemeral=False):
 976        """Destroy and re-make this instance.
 977
 978        A 'rebuild' effectively purges all existing data from the system and
 979        remakes the VM with given 'metadata' and 'personalities'.
 980
 981        This base class method shuts down the VM, detaches all block devices,
 982        then spins up the new VM afterwards. It may be overridden by
 983        hypervisors that need to - e.g. for optimisations, or when the 'VM'
 984        is actually proxied and needs to be held across the shutdown + spin
 985        up steps.
 986
 987        :param context: security context
 988        :param instance: nova.objects.instance.Instance
 989                         This function should use the data there to guide
 990                         the creation of the new instance.
 991        :param nova.objects.ImageMeta image_meta:
 992            The metadata of the image of the instance.
 993        :param injected_files: User files to inject into instance.
 994        :param admin_password: Administrator password to set in instance.
 995        :param bdms: block-device-mappings to use for rebuild
 996        :param detach_block_devices: function to detach block devices. See
 997            nova.compute.manager.ComputeManager:_rebuild_default_impl for
 998            usage.
 999        :param attach_block_devices: function to attach block devices. See
1000            nova.compute.manager.ComputeManager:_rebuild_default_impl for
1001            usage.
1002        :param network_info:
1003           :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
1004        :param recreate: True if the instance is being recreated on a new
1005            hypervisor - all the cleanup of old state is skipped.
1006        :param block_device_info: Information about block devices to be
1007                                  attached to the instance.
1008        :param preserve_ephemeral: True if the default ephemeral storage
1009                                   partition must be preserved on rebuild
1010        """
1011        if recreate:
1012            instance.system_metadata['evac_from'] = instance['launched_on']
1013            instance.save()
1014            extra_specs = self._get_flavor(instance)['extra_specs'].copy()
1015            brand = extra_specs.get('zonecfg:brand', ZONE_BRAND_SOLARIS)
1016            if brand == ZONE_BRAND_SOLARIS:
1017                msg = (_("'%s' branded zones do not currently support "
1018                         "evacuation.") % brand)
1019                raise exception.NovaException(msg)
1020        else:
1021            self._power_off(instance, "HALT")
1022
1023        instance.task_state = task_states.REBUILD_BLOCK_DEVICE_MAPPING
1024        instance.save(expected_task_state=[task_states.REBUILDING])
1025        root_ci = self._rebuild_block_devices(context, instance, bdms,
1026                                              recreate)
1027
1028        if recreate:
1029            if root_ci is not None:
1030                driver_type = root_ci['driver_volume_type']
1031            else:
1032                driver_type = 'local'
1033            if driver_type not in shared_storage:
1034                msg = (_("Root device is not on shared storage for instance "
1035                         "'%s'.") % instance['name'])
1036                raise exception.NovaException(msg)
1037
1038        if not recreate:
1039            self.destroy(context, instance, network_info, block_device_info)
1040            if root_ci is not None:
1041                self._volume_api.detach(context, root_ci['serial'])
1042                self._volume_api.delete(context, root_ci['serial'])
1043
1044                # Go ahead and remove the root bdm from the bdms so that we do
1045                # not trip up spawn either checking against the use of c1d0 or
1046                # attempting to re-attach the root device.
1047                bdms.objects.remove(bdms.root_bdm())
1048                rootdevname = block_device_info.get('root_device_name')
1049                if rootdevname is not None:
1050                    bdi_bdms = block_device_info.get('block_device_mapping')
1051                    for entry in bdi_bdms:
1052                        if entry['mount_device'] == rootdevname:
1053                            bdi_bdms.remove(entry)
1054                            break
1055
1056        instance.task_state = task_states.REBUILD_SPAWNING
1057        instance.save(
1058            expected_task_state=[task_states.REBUILD_BLOCK_DEVICE_MAPPING])
1059
1060        # Instead of using a boolean for 'rebuilding' scratch data, use a
1061        # string because the object will translate it to a string anyways.
1062        if recreate:
1063            extra_specs = self._get_flavor(instance)['extra_specs'].copy()
1064
1065            instance.system_metadata['rebuilding'] = 'false'
1066            self._create_config(context, instance, network_info, root_ci, None)
1067            del instance.system_metadata['evac_from']
1068            instance.save()
1069        else:
1070            instance.system_metadata['rebuilding'] = 'true'
1071            self.spawn(context, instance, image_meta, injected_files,
1072                       admin_password, network_info, block_device_info)
1073
1074        del instance.system_metadata['rebuilding']
1075        name = instance['name']
1076        zone = self._get_zone_by_name(name)
1077        if zone is None:
1078            raise exception.InstanceNotFound(instance_id=name)
1079
1080        if recreate:
1081            zone.attach(['-x', 'initialize-hostdata'])
1082
1083            rootmp = instance['root_device_name']
1084            for entry in bdms:
1085                if (entry['connection_info'] is None or
1086                        rootmp == entry['device_name']):
1087                    continue
1088
1089                connection_info = jsonutils.loads(entry['connection_info'])
1090                mount = entry['device_name']
1091                self.attach_volume(context, connection_info, instance, mount)
1092
1093            self._power_on(instance, network_info)
1094
1095        if admin_password is not None:
1096            # Because there is no way to make sure a zone is ready upon
1097            # returning from a boot request. We must give the zone a few
1098            # seconds to boot before attempting to set the admin password.
1099            greenthread.sleep(15)
1100            self.set_admin_password(instance, admin_password)
1101
1102    def _get_flavor(self, instance):
1103        """Retrieve the flavor object as specified in the instance object"""
1104        return flavor_obj.Flavor.get_by_id(
1105            nova_context.get_admin_context(read_deleted='yes'),
1106            instance['instance_type_id'])
1107
1108    def _fetch_image(self, context, instance):
1109        """Fetch an image using Glance given the instance's image_ref."""
1110        glancecache_dirname = CONF.solariszones.glancecache_dirname
1111        fileutils.ensure_tree(glancecache_dirname)
1112        iref = instance['image_ref']
1113        image = os.path.join(glancecache_dirname, iref)
1114        downloading = image + '.downloading'
1115
1116        with lockutils.lock('glance-image-%s' % iref):
1117            if os.path.isfile(downloading):
1118                LOG.debug(_('Cleaning partial download of %s' % iref))
1119                os.unlink(image)
1120                os.unlink(downloading)
1121
1122            elif os.path.exists(image):
1123                LOG.debug(_("Using existing, cached Glance image: id %s")
1124                          % iref)
1125                return image
1126
1127            LOG.debug(_("Fetching new Glance image: id %s") % iref)
1128            try:
1129                # touch the empty .downloading file
1130                with open(downloading, 'w'):
1131                    pass
1132                images.fetch(context, iref, image, instance['user_id'],
1133                             instance['project_id'])
1134                os.unlink(downloading)
1135                return image
1136            except Exception as reason:
1137                LOG.exception(_("Unable to fetch Glance image: id %s: %s")
1138                              % (iref, reason))
1139                raise
1140
1141    @lockutils.synchronized('validate_image')
1142    def _validate_image(self, context, image, instance):
1143        """Validate a glance image for compatibility with the instance."""
1144        # Skip if the image was already checked and confirmed as valid.
1145        if instance['image_ref'] in self._validated_archives:
1146            return
1147
1148        try:
1149            ua = self.archive_manager.getArchive(image)
1150        except Exception as ex:
1151            if isinstance(ex, rad.client.ObjectError):
1152                reason = ex.get_payload().info
1153            else:
1154                reason = str(ex)
1155            raise exception.ImageUnacceptable(image_id=instance['image_ref'],
1156                                              reason=reason)
1157
1158        # Validate the image at this point to ensure:
1159        # - contains one deployable system
1160        deployables = ua.getArchivedSystems()
1161        if len(deployables) != 1:
1162            reason = _("Image must contain only a single deployable system.")
1163            raise exception.ImageUnacceptable(image_id=instance['image_ref'],
1164                                              reason=reason)
1165        # - matching architecture
1166        deployable_arch = str(ua.isa)
1167        compute_arch = platform.processor()
1168        if deployable_arch.lower() != compute_arch:
1169            reason = (_("Unified Archive architecture '%s' is incompatible "
1170                      "with this compute host's architecture, '%s'.")
1171                      % (deployable_arch, compute_arch))
1172
1173            # For some reason we have gotten the wrong architecture image,
1174            # which should have been filtered by the scheduler. One reason this
1175            # could happen is because the images architecture type is
1176            # incorrectly set. Check for this and report a better reason.
1177            glanceapi = glance_api()
1178            image_meta = glanceapi.get(context, instance['image_ref'])
1179            image_properties = image_meta.get('properties')
1180            if image_properties.get('architecture') is None:
1181                reason = reason + (_(" The 'architecture' property is not set "
1182                                     "on the Glance image."))
1183
1184            raise exception.ImageUnacceptable(image_id=instance['image_ref'],
1185                                              reason=reason)
1186        # - single root pool only
1187        if not deployables[0].rootOnly:
1188            reason = _("Image contains more than one ZFS pool.")
1189            raise exception.ImageUnacceptable(image_id=instance['image_ref'],
1190                                              reason=reason)
1191        # - looks like it's OK
1192        self._validated_archives.append(instance['image_ref'])
1193
1194    def _validate_flavor(self, instance):
1195        """Validate the flavor for compatibility with zone brands"""
1196        flavor = self._get_flavor(instance)
1197        extra_specs = flavor['extra_specs'].copy()
1198        brand = extra_specs.get('zonecfg:brand', ZONE_BRAND_SOLARIS)
1199
1200        if brand == ZONE_BRAND_SOLARIS_KZ:
1201            # verify the memory is 256mb aligned
1202            test_size = Size('256MB')
1203            instance_size = Size('%sMB' % instance['memory_mb'])
1204
1205            if instance_size.byte_value % test_size.byte_value:
1206                # non-zero result so it doesn't align
1207                raise MemoryAlignmentIncorrect(
1208                    flavor=flavor['name'],
1209                    memsize=str(instance['memory_mb']),
1210                    align='256')
1211
1212    def _suri_from_volume_info(self, connection_info):
1213        """Returns a suri(5) formatted string based on connection_info.
1214        Currently supports local ZFS volume, NFS, Fibre Channel and iSCSI
1215        driver types.
1216        """
1217        driver_type = connection_info['driver_volume_type']
1218        if driver_type not in ['iscsi', 'fibre_channel', 'local', 'nfs']:
1219            raise exception.VolumeDriverNotFound(driver_type=driver_type)
1220        if driver_type == 'local':
1221            suri = 'dev:/dev/zvol/dsk/%s' % connection_info['volume_path']
1222        elif driver_type == 'iscsi':
1223            data = connection_info['data']
1224            # suri(5) format:
1225            #       iscsi://<host>[:<port>]/target.<IQN>,lun.<LUN>
1226            # luname-only URI format for the multipathing:
1227            #       iscsi://<host>[:<port>]/luname.naa.<ID>
1228            # Sample iSCSI connection data values:
1229            # target_portal: 192.168.1.244:3260
1230            # target_iqn: iqn.2010-10.org.openstack:volume-a89c.....
1231            # target_lun: 1
1232            suri = None
1233            if 'target_iqns' in data:
1234                target = data['target_iqns'][0]
1235                target_lun = data['target_luns'][0]
1236                try:
1237                    utils.execute('/usr/sbin/iscsiadm', 'list', 'target',
1238                                  '-vS', target)
1239                    out, err = utils.execute('/usr/sbin/suriadm', 'lookup-uri',
1240                                             '-t', 'iscsi',
1241                                             '-p', 'target=%s' % target,
1242                                             '-p', 'lun=%s' % target_lun)
1243                    for line in [l.strip() for l in out.splitlines()]:
1244                        if "luname.naa." in line:
1245                            LOG.debug(_("The found luname-only URI for the "
1246                         

Large files files are truncated, but you can click here to view the full file