PageRenderTime 70ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://bitbucket.org/dilos/userland-gate
Python | 4830 lines | 4402 code | 149 blank | 279 comment | 216 complexity | 25104b04cf6add9d01b73ed8ea618321 MD5 | raw file
Possible License(s): CPL-1.0, AGPL-3.0, IPL-1.0, Apache-2.0, MPL-2.0-no-copyleft-exception, Unlicense, CC-BY-SA-4.0, AGPL-1.0, GPL-3.0, LGPL-2.0, LGPL-2.1, CC-BY-3.0, MIT, ISC, EPL-1.0, GPL-2.0, BSD-3-Clause-No-Nuclear-License-2014, BSD-3-Clause, LGPL-3.0, BSD-2-Clause, 0BSD, JSON

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

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