The init commit of stackforge/nova-zvm-virt-driver

Including all python modules that implements Nova virt driver for
zVM, and configuration files that make the project work with
OpenStack development infrastructure.

Change-Id: I7efe0d1b6ef42268438d4141860fb4c51c09d30c
This commit is contained in:
Huang Rui 2015-03-30 04:35:28 -04:00
parent 50f62fc6f9
commit 588d68108a
24 changed files with 9356 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
*.log
*.pyc
.autogenerated
.coverage
.project
.pydevproject
.settings
.testrepository
.tox
.venv
AUTHORS
build/
ChangeLog
coverage.xml
cover/*
covhtml
dist/
doc/build/
nova_zvm_virt_driver.egg-info/

7
.testr.conf Normal file
View File

@ -0,0 +1,7 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

5
README.rst Normal file
View File

@ -0,0 +1,5 @@
This project implements Nova virtulization driver for z/VM, which
enables OpenStack manage z/VM hypervisor and virtual machines
running in the z/VM system.
Wiki page: https://wiki.openstack.org/wiki/ZVMDriver

285
doc/source/conf.py Normal file
View File

@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
#
# nova-zvm-virt-driver documentation build configuration file, created by
# sphinx-quickstart on Sun Mar 29 22:34:14 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
import shlex
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['oslosphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['.templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'nova-zvm-virt-driver'
copyright = u'2015, IBM'
author = u'IBM'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '2015.1'
# The full version, including alpha/beta/rc tags.
release = '2015.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#html_theme = 'alabaster'
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {'incubating': True}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['.static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'nova-zvm-virt-driverdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'nova-zvm-virt-driver.tex', u'nova-zvm-virt-driver Documentation',
u'IBM', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'nova-zvm-virt-driver', u'nova-zvm-virt-driver Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'nova-zvm-virt-driver', u'nova-zvm-virt-driver Documentation',
author, 'nova-zvm-virt-driver', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

21
doc/source/index.rst Normal file
View File

@ -0,0 +1,21 @@
Welcome to nova-zvm-virt-driver's documentation!
================================================
This project implements Nova virtulization driver for z/VM, which
enables OpenStack manage z/VM hypervisor and virtual machines
running in the z/VM system.
Contents:
.. toctree::
:maxdepth: 2
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

0
nova_zvm/__init__.py Normal file
View File

View File

3227
nova_zvm/tests/test_zvm.py Normal file

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,31 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""A connection to an IBM z/VM Virtualization system.
Generally, OpenStack z/VM virt driver will call xCat REST API to operate
to z/VM hypervisors.xCat has a control point(a virtual machine) in z/VM
system, which enables xCat management node to control the z/VM system.
OpenStack z/VM driver will communicate with xCat management node through
xCat REST API. Thus OpenStack can operate to z/VM system indirectly.
"""
from nova_zvm.virt.zvm import driver
ZVMDriver = driver.ZVMDriver

View File

@ -0,0 +1,64 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import tarfile
from nova import exception
from nova import utils
from nova.virt import configdrive
from oslo_config import cfg
CONF = cfg.CONF
class ZVMConfigDriveBuilder(configdrive.ConfigDriveBuilder):
"""Enable ConfigDrive to make tgz package."""
def __init__(self, instance_md):
super(ZVMConfigDriveBuilder, self).__init__(instance_md)
def make_drive(self, path):
"""Make the config drive.
:param path: the path to place the config drive image at
:raises ProcessExecuteError if a helper process has failed.
"""
if CONF.config_drive_format == 'tgz':
self._make_tgz(path)
else:
raise exception.ConfigDriveUnknownFormat(
format=CONF.config_drive_format)
def _make_tgz(self, path):
try:
olddir = os.getcwd()
except OSError:
olddir = CONF.state_path
with utils.tempdir() as tmpdir:
self._write_md_files(tmpdir)
tar = tarfile.open(path, "w:gz")
os.chdir(tmpdir)
tar.add("openstack")
tar.add("ec2")
try:
os.chdir(olddir)
except Exception:
pass
tar.close()

View File

@ -0,0 +1,69 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nova.compute import power_state
HYPERVISOR_TYPE = 'zvm'
ARCHITECTURE = 's390x'
ALLOWED_VM_TYPE = 'zLinux'
XCAT_MGT = 'zvm'
XCAT_RINV_HOST_KEYWORDS = {
"zvm_host": "z/VM Host:",
"zhcp": "zHCP:",
"cec_vendor": "CEC Vendor:",
"cec_model": "CEC Model:",
"hypervisor_os": "Hypervisor OS:",
"hypervisor_name": "Hypervisor Name:",
"architecture": "Architecture:",
"lpar_cpu_total": "LPAR CPU Total:",
"lpar_cpu_used": "LPAR CPU Used:",
"lpar_memory_total": "LPAR Memory Total:",
"lpar_memory_used": "LPAR Memory Used:",
"lpar_memory_offline": "LPAR Memory Offline:",
"ipl_time": "IPL Time:",
}
XCAT_DISKPOOL_KEYWORDS = {
"disk_total": "Total:",
"disk_used": "Used:",
"disk_available": "Free:",
}
XCAT_RESPONSE_KEYS = ('info', 'data', 'node', 'errorcode', 'error')
ZVM_POWER_STAT = {
'on': power_state.RUNNING,
'off': power_state.SHUTDOWN,
}
ZVM_DEFAULT_ROOT_DISK = "dasda"
ZVM_DEFAULT_SECOND_DISK = "dasdb"
ZVM_DEFAULT_ROOT_VOLUME = "sda"
ZVM_DEFAULT_SECOND_VOLUME = "sdb"
ZVM_DEFAULT_THIRD_VOLUME = "sdc"
ZVM_DEFAULT_LAST_VOLUME = "sdz"
DEFAULT_EPH_DISK_FMT = "ext3"
DISK_FUNC_NAME = "setupDisk"
ZVM_DEFAULT_FCP_ID = 'auto'
ZVM_DEFAULT_NIC_VDEV = '1000'
ZVM_IMAGE_SIZE_MAX = 10

1997
nova_zvm/virt/zvm/driver.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nova import exception
from nova.i18n import _
class ZVMBaseException(exception.NovaException):
"""Base z/VM exception."""
pass
class ZVMDriverError(ZVMBaseException):
msg_fmt = _('z/VM driver error: %(msg)s')
class ZVMXCATRequestFailed(ZVMBaseException):
msg_fmt = _('Request to xCAT server %(xcatserver)s failed: %(msg)s')
class ZVMInvalidXCATResponseDataError(ZVMBaseException):
msg_fmt = _('Invalid data returned from xCAT: %(msg)s')
class ZVMXCATInternalError(ZVMBaseException):
msg_fmt = _('Error returned from xCAT: %(msg)s')
class ZVMVolumeError(ZVMBaseException):
msg_fmt = _('Volume error: %(msg)s')
class ZVMImageError(ZVMBaseException):
msg_fmt = _("Image error: %(msg)s")
class ZVMGetImageFromXCATFailed(ZVMBaseException):
msg_fmt = _('Get image from xCAT failed: %(msg)s')
class ZVMNetworkError(ZVMBaseException):
msg_fmt = _("z/VM network error: %(msg)s")
class ZVMXCATXdshFailed(ZVMBaseException):
msg_fmt = _('Execute xCAT xdsh command failed: %(msg)s')
class ZVMXCATCreateNodeFailed(ZVMBaseException):
msg_fmt = _('Create xCAT node %(node)s failed: %(msg)s')
class ZVMXCATCreateUserIdFailed(ZVMBaseException):
msg_fmt = _('Create xCAT user id %(instance)s failed: %(msg)s')
class ZVMXCATUpdateNodeFailed(ZVMBaseException):
msg_fmt = _('Update node %(node)s info failed: %(msg)s')
class ZVMXCATDeployNodeFailed(ZVMBaseException):
msg_fmt = _('Deploy image on node %(node)s failed: %(msg)s')
class ZVMConfigDriveError(ZVMBaseException):
msg_fmt = _('Create configure drive failed: %(msg)s')

View File

@ -0,0 +1,822 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import os
import re
import shutil
import tarfile
import xml.dom.minidom as Dom
from nova import exception as nova_exception
from nova.i18n import _
from nova.image import glance
from nova import utils
from nova.virt import images
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from nova_zvm.virt.zvm import const
from nova_zvm.virt.zvm import exception
from nova_zvm.virt.zvm import utils as zvmutils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
QUEUE_BUFFER_SIZE = 10
class ZVMImages(object):
def __init__(self):
self._xcat_url = zvmutils.XCATUrl()
self._pathutils = zvmutils.PathUtils()
def create_zvm_image(self, instance, image_name, image_href):
"""Create z/VM image from z/VM instance by invoking xCAT REST API
imgcapture.
"""
nodename = instance['name']
profile = image_name + "_" + image_href.replace('-', '_')
body = ['nodename=' + nodename,
'profile=' + profile]
if CONF.zvm_image_compression_level:
if CONF.zvm_image_compression_level.isdigit() and (
int(CONF.zvm_image_compression_level) in range(0, 10)):
body.append('compress=%s' % CONF.zvm_image_compression_level)
else:
msg = _("Invalid zvm_image_compression_level detected, please"
"specify it with a integer between 0 and 9 in your nova.conf")
raise exception.ZVMImageError(msg=msg)
url = self._xcat_url.imgcapture()
LOG.debug('Capturing %s start' % instance['name'])
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMImageError):
res = zvmutils.xcat_request("POST", url, body)
os_image = self._get_os_image(res)
return os_image
def _get_os_image(self, response):
"""Return the image_name by parsing the imgcapture rest api
response.
"""
image_name_xcat = ""
if len(response['info']) > 0:
for info in response['info']:
for info_element in info:
if "Completed capturing the image" in info_element:
start_index = info_element.find('(')
end_index = info_element.find(')')
image_name_xcat = info_element[start_index + 1:
end_index]
return image_name_xcat
if len(image_name_xcat) == 0:
msg = _("Capture image failed.")
LOG.error(msg)
raise exception.ZVMImageError(msg=msg)
else:
msg = _("Capture image returns bad response.")
LOG.error(msg)
raise exception.ZVMImageError(msg=msg)
def get_snapshot_time_path(self):
return self._pathutils.get_snapshot_time_path()
def get_image_from_xcat(self, image_name_xcat, image_name,
snapshot_time_path):
"""Import image from xCAT to nova, by invoking the imgexport
REST API.
"""
LOG.debug("Getting image from xCAT")
destination = os.path.join(snapshot_time_path, image_name + '.tgz')
host = zvmutils.get_host()
body = ['osimage=' + image_name_xcat,
'destination=' + destination,
'remotehost=' + host]
url = self._xcat_url.imgexport()
try:
zvmutils.xcat_request("POST", url, body)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError) as err:
msg = (_("Transfer image to compute node failed: %s") %
err.format_message())
raise exception.ZVMImageError(msg=msg)
return destination
def delete_image_glance(self, image_service, context, image_href):
"""Delete the image from glance database and image repository.
Can be used for a rollback step if operations to image fail.
"""
image_service.delete(context, image_href)
def clean_up_snapshot_time_path(self, snapshot_time_path):
"""Clean up the time_path and its contents under "snapshot_tmp" after
image uploaded to glance.
Also be used for a rollback step if operations to the image file fail.
"""
if os.path.exists(snapshot_time_path):
LOG.debug("Cleaning up nova local image file")
shutil.rmtree(snapshot_time_path)
def _delete_image_file_from_xcat(self, image_name_xcat):
"""Delete image file from xCAT MN.
When capturing, image in the xCAT MN's repository will be removed after
it is imported to nova compute node.
"""
LOG.debug("Removing image files from xCAT MN image repository")
url = self._xcat_url.rmimage('/' + image_name_xcat)
try:
zvmutils.xcat_request("DELETE", url)
except (exception.ZVMXCATInternalError,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATRequestFailed):
LOG.warn(_("Failed to delete image file %s from xCAT") %
image_name_xcat)
def _delete_image_object_from_xcat(self, image_name_xcat):
"""Delete image object from xCAT MN.
After capturing, image definition in the xCAT MN's table osimage and
linuximage will be removed after it is imported to nova compute node.
"""
LOG.debug("Deleting the image object")
url = self._xcat_url.rmobject('/' + image_name_xcat)
try:
zvmutils.xcat_request("DELETE", url)
except (exception.ZVMXCATInternalError,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATRequestFailed):
LOG.warn(_("Failed to delete image definition %s from xCAT") %
image_name_xcat)
def get_image_file_path_from_image_name(self, image_name_xcat):
return zvmutils.xcat_cmd_gettab("linuximage", "imagename",
image_name_xcat, "rootimgdir")
def delete_image_from_xcat(self, image_name_xcat):
self._delete_image_file_from_xcat(image_name_xcat)
self._delete_image_object_from_xcat(image_name_xcat)
def _getxmlnode(self, node, name):
return node.getElementsByTagName(name)[0] if node else []
def _getnode(self, node_root, tagname):
"""For parse manifest."""
nodename = node_root.getElementsByTagName(tagname)[0]
nodevalue = nodename.childNodes[0].data
return nodevalue
def parse_manifest_xml(self, image_package_path):
"""Return the image properties from manifest.xml."""
LOG.debug("Parsing the manifest.xml")
manifest_xml = os.path.join(image_package_path, "manifest.xml")
manifest = {}
if os.path.exists(manifest_xml):
xml_file = Dom.parse(manifest_xml)
else:
LOG.warn(_('manifest.xml does not exist'))
manifest['imagename'] = ''
manifest['imagetype'] = ''
manifest['osarch'] = ''
manifest['osname'] = ''
manifest['osvers'] = ''
manifest['profile'] = ''
manifest['provmethod'] = ''
return manifest
node_root = xml_file.documentElement
node_root = self._getxmlnode(node_root, 'osimage')
manifest['imagename'] = self._getnode(node_root, "imagename")
manifest['imagetype'] = self._getnode(node_root, "imagetype")
manifest['osarch'] = self._getnode(node_root, "osarch")
manifest['osname'] = self._getnode(node_root, "osname")
manifest['osvers'] = self._getnode(node_root, "osvers")
manifest['profile'] = self._getnode(node_root, "profile")
manifest['provmethod'] = self._getnode(node_root, "provmethod")
return manifest
def untar_image_bundle(self, snapshot_time_path, image_bundle):
"""Untar the image bundle *.tgz from xCAT and remove the *.tgz."""
if os.path.exists(image_bundle):
LOG.debug("Untarring the image bundle ... ")
tarobj = tarfile.open(image_bundle, "r:gz")
for tarinfo in tarobj:
tarobj.extract(tarinfo.name, path=snapshot_time_path)
tarobj.close()
os.remove(image_bundle)
else:
self.clean_up_snapshot_time_path(snapshot_time_path)
msg = _("Image bundle does not exist")
raise exception.ZVMImageError(msg=msg)
def get_image_file_name(self, image_package_path):
if os.path.exists(image_package_path):
file_contents = os.listdir(image_package_path)
for f in file_contents:
if f.endswith('.img'):
return f
msg = _("Can not find image file")
else:
msg = _("Image path %s not exist") % image_package_path
raise exception.ZVMImageError(msg=msg)
def image_exist_xcat(self, image_id):
"""To see if the specific image exist in xCAT MN's image
repository.
"""
LOG.debug("Checking if the image %s exists or not in xCAT "
"MN's image repository " % image_id)
image_uuid = image_id.replace('-', '_')
parm = '&criteria=profile=~' + image_uuid
url = self._xcat_url.lsdef_image(addp=parm)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMImageError):
res = zvmutils.xcat_request("GET", url)
res_image = res['info']
if '_' in str(res_image):
return True
else:
return False
def fetch_image(self, context, image_id, target, user, project):
LOG.debug("Downloading image %s from glance image server" %
image_id)
try:
images.fetch(context, image_id, target, user, project)
except Exception as err:
emsg = zvmutils.format_exception_msg(err)
msg = _("Download image file of image %(id)s failed with reason:"
" %(err)s") % {'id': image_id, 'err': emsg}
raise exception.ZVMImageError(msg=msg)
def generate_manifest_file(self, image_meta, image_name, disk_file,
manifest_path):
"""Generate the manifest.xml file from glance's image metadata
as a part of the image bundle.
"""
image_id = image_meta['id']
image_type = image_meta['properties']['image_type_xcat']
os_version = image_meta['properties']['os_version']
os_name = image_meta['properties']['os_name']
os_arch = image_meta['properties']['architecture']
prov_method = image_meta['properties']['provisioning_method']
image_profile = '_'.join((image_name, image_id.replace('-', '_')))
image_name_xcat = '-'.join((os_version, os_arch,
prov_method, image_profile))
rootimgdir_str = ('/install', prov_method, os_version,
os_arch, image_profile)
rootimgdir = '/'.join(rootimgdir_str)
today_date = datetime.date.today()
last_use_date_string = today_date.strftime("%Y-%m-%d")
is_deletable = "auto:last_use_date:" + last_use_date_string
doc = Dom.Document()
xcatimage = doc.createElement('xcatimage')
doc.appendChild(xcatimage)
# Add linuximage section
imagename = doc.createElement('imagename')
imagename_value = doc.createTextNode(image_name_xcat)
imagename.appendChild(imagename_value)
rootimagedir = doc.createElement('rootimgdir')
rootimagedir_value = doc.createTextNode(rootimgdir)
rootimagedir.appendChild(rootimagedir_value)
linuximage = doc.createElement('linuximage')
linuximage.appendChild(imagename)
linuximage.appendChild(rootimagedir)
xcatimage.appendChild(linuximage)
# Add osimage section
osimage = doc.createElement('osimage')
manifest = {'imagename': image_name_xcat,
'imagetype': image_type,
'isdeletable': is_deletable,
'osarch': os_arch,
'osname': os_name,
'osvers': os_version,
'profile': image_profile,
'provmethod': prov_method}
for item in manifest.keys():
itemkey = doc.createElement(item)
itemvalue = doc.createTextNode(manifest[item])
itemkey.appendChild(itemvalue)
osimage.appendChild(itemkey)
xcatimage.appendChild(osimage)
f = open(manifest_path + '/manifest.xml', 'w')
f.write(doc.toprettyxml(indent=''))
f.close()
# Add the rawimagefiles section
rawimagefiles = doc.createElement('rawimagefiles')
xcatimage.appendChild(rawimagefiles)
files = doc.createElement('files')
files_value = doc.createTextNode(rootimgdir + '/' + disk_file)
files.appendChild(files_value)
rawimagefiles.appendChild(files)
f = open(manifest_path + '/manifest.xml', 'w')
f.write(doc.toprettyxml(indent=' '))
f.close()
self._rewr(manifest_path)
return manifest_path + '/manifest.xml'
def _rewr(self, manifest_path):
f = open(manifest_path + '/manifest.xml', 'r')
lines = f.read()
f.close()
lines = lines.replace('\n', '')
lines = re.sub(r'>(\s*)<', r'>\n\1<', lines)
lines = re.sub(r'>[ \t]*(\S+)[ \t]*<', r'>\1<', lines)
f = open(manifest_path + '/manifest.xml', 'w')
f.write(lines)
f.close()
def generate_image_bundle(self, spawn_path, tmp_file_fn, image_name):
"""Generate the image bundle which is used to import to xCAT MN's
image repository.
"""
image_bundle_name = image_name + '.tgz'
tar_file = spawn_path + '/' + tmp_file_fn + '_' + image_bundle_name
LOG.debug("The generate the image bundle file is %s" % tar_file)
os.chdir(spawn_path)
tarFile = tarfile.open(tar_file, mode='w:gz')
try:
tarFile.add(tmp_file_fn)
tarFile.close()
except Exception as err:
msg = (_("Generate image bundle failed: %s") % err)
LOG.error(msg)
if os.path.isfile(tar_file):
os.remove(tar_file)
raise exception.ZVMImageError(msg=msg)
finally:
self._pathutils.clean_temp_folder(tmp_file_fn)
return tar_file
def check_space_imgimport_xcat(self, context, instance, tar_file,
xcat_free_space_threshold, zvm_xcat_master):
image_href = instance['image_ref']
try:
free_space_xcat = self.get_free_space_xcat(
xcat_free_space_threshold, zvm_xcat_master)
img_transfer_needed = self._get_transfer_needed_space_xcat(context,
image_href, tar_file)
larger = max(xcat_free_space_threshold, img_transfer_needed)
if img_transfer_needed > free_space_xcat:
larger = max(xcat_free_space_threshold, img_transfer_needed)
size_needed = float(larger - free_space_xcat)
self.prune_image_xcat(context, size_needed,
img_transfer_needed)
else:
LOG.debug("Image transfer needed space satisfied in xCAT")
except exception.ZVMImageError:
with excutils.save_and_reraise_exception():
os.remove(tar_file)
def put_image_to_xcat(self, image_bundle_package, image_profile):
"""Import the image bundle from compute node to xCAT MN's image
repository.
"""
remote_host_info = zvmutils.get_host()
body = ['osimage=%s' % image_bundle_package,
'profile=%s' % image_profile,
'remotehost=%s' % remote_host_info]
url = self._xcat_url.imgimport()
try:
zvmutils.xcat_request("POST", url, body)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError) as err:
msg = (_("Import the image bundle to xCAT MN failed: %s") %
err.format_message())
raise exception.ZVMImageError(msg=msg)
finally:
os.remove(image_bundle_package)
def get_imgname_xcat(self, image_id):
"""Get the xCAT deployable image name by image id."""
image_uuid = image_id.replace('-', '_')
parm = '&criteria=profile=~' + image_uuid
url = self._xcat_url.lsdef_image(addp=parm)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMImageError):
res = zvmutils.xcat_request("GET", url)
with zvmutils.expect_invalid_xcat_resp_data():
res_image = res['info'][0][0]
res_img_name = res_image.strip().split(" ")[0]
if res_img_name:
return res_img_name
else:
LOG.error(_("Fail to find the right image to deploy"))
def _get_image_list_xcat(self):
"""Get an image list from xcat osimage table.
criteria: osarch=s390x and provmethod=netboot|raw|sysclone and
isdeletable field
"""
display_field = '&field=isdeletable'
isdeletable_criteria = '&criteria=isdeletable=~^auto:last_use_date:'
osarch_criteria = '&criteria=osarch=s390x'
provmethod_criteria = '&criteria=provmethod=~netboot|raw|sysclone'
addp = ''.join([isdeletable_criteria, osarch_criteria,
provmethod_criteria, display_field])
url = self._xcat_url.lsdef_image(addp=addp)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMImageError):
output = zvmutils.xcat_request("GET", url)
image_list = []
if len(output['info']) <= 0:
return image_list
if len(output['info'][0]) > 0:
i = 0
while i < len(output['info'][0]):
if "Object name:" in output['info'][0][i]:
sub_list = []
len_objectname = len("Object name: ")
is_deletable = output['info'][0][i + 1]
last_use_date = self._validate_last_use_date(
output['info'][0][i][len_objectname:],
is_deletable)
if last_use_date is None:
i += 2
continue
sub_list.append(output['info'][0][i][len_objectname:])
sub_list.append(last_use_date)
image_list.append(sub_list)
i += 2
return image_list
def update_last_use_date(self, image_name_xcat):
"""Update the last_use_date in xCAT osimage table after a
successful deploy.
"""
LOG.debug("Update the last_use_date in xCAT osimage table "
"after a successful deploy")
today_date = datetime.date.today()
last_use_date_string = today_date.strftime("%Y-%m-%d")
url = self._xcat_url.tabch('/osimage')
is_deletable = "auto:last_use_date:" + last_use_date_string
body = ["imagename=" + image_name_xcat,
"osimage.isdeletable=" + is_deletable]
try:
zvmutils.xcat_request("PUT", url, body)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError) as err:
LOG.warn(_("Illegal date for last_use_date %s") %
err.format_message())
return last_use_date_string
def _validate_last_use_date(self, image_name, is_deletable):
"""Validate the isdeletable date format."""
last_use_date_string = is_deletable.split(":")[2]
timere = ("^\d{4}[-]((0([1-9]{1}))|"
"(1[0|1|2]))[-](([0-2]([0-9]{1}))|(3[0|1]))$")
if (len(last_use_date_string) == 10) and (
re.match(timere, last_use_date_string)):
LOG.debug("The format for last_use_date is valid ")
else:
LOG.warn(_("The format for image %s record in xcat table osimage's"
" last_used_date is not valid. The correct format is "
"auto:last_use_date:yyyy-mm-dd") % image_name)
return
try:
last_use_date_datetime = datetime.datetime.strptime(
last_use_date_string, '%Y-%m-%d')
except Exception as err:
LOG.warn(_("Illegal date for last_use_date %(msg)s") % err)
return
return last_use_date_datetime.date()
def _verify_is_deletable_periodic(self, last_use_date, clean_period):
"""Check the last_use_date of an image to determine if the image
need to be cleaned.
"""
now = datetime.date.today()
delta = (now - last_use_date).days
if (delta - clean_period) >= 0:
return True
else:
return False
def clean_image_cache_xcat(self, clean_period):
"""Clean the old image."""
image_list = self._get_image_list_xcat()
if len(image_list) <= 0:
return
else:
i = 0
while i < len(image_list):
image_name_xcat = image_list[i][0]
last_use_date = image_list[i][1]
if self._verify_is_deletable_periodic(last_use_date,
clean_period):
LOG.debug('Delete the image %s' % image_name_xcat)
self.delete_image_from_xcat(image_name_xcat)
else:
LOG.debug("Keep the image")
i += 1
def _get_image_bundle_size(self, tar_file):
size_byte = os.path.getsize(tar_file)
return float(size_byte) / 1024 / 1024 / 1024
def _get_image_size_glance(self, context, image_href):
(image_service, image_id) = glance.get_remote_image_service(
context, image_href)
try:
image_meta = image_service.show(context, image_href)
except nova_exception.ImageNotFound:
image_meta = {}
return 0
size_byte = image_meta['size']
return float(size_byte) / 1024 / 1024 / 1024
def get_free_space_xcat(self, xcat_free_space_threshold, zvm_xcat_master):
"""Get the free space in xCAT MN /install."""
LOG.debug("Get the xCAT MN /install free space")
addp = "&field=--freerepospace"
if isinstance(zvm_xcat_master, str):
url = self._xcat_url.rinv("/" + zvm_xcat_master, addp=addp)
else:
msg = _("zvm_xcat_master should be specified as a string")
LOG.error(msg)
raise exception.ZVMImageError(msg=msg)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMImageError):
result = zvmutils.xcat_request("GET", url)
with zvmutils.expect_invalid_xcat_resp_data():
if len(result['info']) == 0:
msg = _("'rinv <zvm_xcat_master> --freerepospace' returns "
"null, please check 'df -h /install', there may "
"be something wrong with the mount of /install")
raise exception.ZVMImageError(msg=msg)
free_space_line = result['info'][0][0]
free_space = free_space_line.split()[4]
if free_space.endswith("G"):
free_disk_value = free_space.rstrip("G")
return float(free_disk_value)
elif free_space.endswith("M"):
free_disk_value = free_space.rstrip("M")
return float(free_disk_value) / 1024
elif free_space == "0":
return 0
else:
return xcat_free_space_threshold
def get_imgcapture_needed(self, instance):
"""Get the space needed on xCAT MN for an image capture."""
LOG.debug("Getting image capture needed size for %s" %
instance['name'])
cmd = "df -h /"
result = None
result = zvmutils.xdsh(instance['name'], cmd)['data'][0]
imgcapture_needed_space = ""
try:
result_data = result[0].split()
if (CONF.zvm_image_compression_level and
int(CONF.zvm_image_compression_level) == 0):
imgcapture_needed_space = result_data[10]
else:
imgcapture_needed_space = result_data[11]
if imgcapture_needed_space.endswith("G"):
imgcapture_needed_space_value = imgcapture_needed_space.rstrip(
"G")
return float(imgcapture_needed_space_value) * 2
elif imgcapture_needed_space.endswith("M"):
imgcapture_needed_space_value = imgcapture_needed_space.rstrip(
"M")
return (float(imgcapture_needed_space_value) / 1024) * 2
else:
return const.ZVM_IMAGE_SIZE_MAX
except (IndexError, ValueError, TypeError) as err:
raise exception.ZVMImageError(msg=err)
def _get_transfer_needed_space_xcat(self, context, image_href, tar_file):
"""To transfer an image bundle from glance to xCAT, the needed size is
image_bundle_size + image_size.
"""
image_bundle_size = self._get_image_bundle_size(tar_file)
image_size = self._get_image_size_glance(context, image_href)
return image_bundle_size + image_size
def _get_image_href_by_osimage(self, image_name_xcat):
"""If we have the xCAT.osimage.imagename, we want to get the
image_href.
"""
try:
image_profile = image_name_xcat.split("-")
if len(image_profile) >= 4:
return image_profile[3]
else:
return image_name_xcat
except (TypeError, IndexError):
LOG.error(_("xCAT imagename format for %s is not as expected")
% image_name_xcat)
def _sort_image_by_use_date(self, image_list, left, right):
"""Sort the image_list by last_use_date from oldest image to latest."""
if (left < right):
i = left
j = right
x = image_list[left]
while (i < j):
while (i < j and image_list[j][1] >= x[1]):
j -= 1
if(i < j):
image_list[i] = image_list[j]
i += 1
while(i < j and image_list[i][1] < x[1]):
i += 1
if(i < j):
image_list[j] = image_list[i]
j -= 1
image_list[i] = x
self._sort_image_by_use_date(image_list, left, i - 1)
self._sort_image_by_use_date(image_list, i + 1, right)
def _get_to_be_deleted_images_xcat(self, context, size_needed,
current_needed):
"""To get a list of images which is to be removed from xCAT image
repository because it cannot provide enough space for image operations
from OpenStack.
"""
image_list = self._get_image_list_xcat()
size_sum = 0
to_be_deleted_image_profile = []
if len(image_list) <= 0:
msg = _("No image to be deleted, please create space manually "
"on xcat(%s).") % CONF.zvm_xcat_server
raise exception.ZVMImageError(msg=msg)
else:
self._sort_image_by_use_date(image_list, 0, len(image_list) - 1)
for img in image_list:
image_name_xcat = img[0]
image_profile = self._get_image_href_by_osimage(
image_name_xcat)
image_uuid = image_profile.partition('_')[2].replace("_", "-")
image_size = self._get_image_size_glance(context,
image_uuid)
if image_size > 0:
to_be_deleted_image_profile.append(image_profile)
size_sum += image_size
if size_sum >= size_needed:
return to_be_deleted_image_profile
if size_sum >= current_needed:
return to_be_deleted_image_profile
else:
msg = _("xCAT MN space not enough for the current image operation")
raise exception.ZVMImageError(msg=msg)
def prune_image_xcat(self, context, size_needed, current_needed):
"""Remove the images which meet remove criteria from xCAT."""
LOG.debug("Clear up space by clean images in xCAT")
to_be_deleted_image_profile = self._get_to_be_deleted_images_xcat(
context, size_needed, current_needed)
if len(to_be_deleted_image_profile) > 0:
for image_profile in to_be_deleted_image_profile:
image_name_xcat = self.get_imgname_xcat(image_profile)
self.delete_image_from_xcat(image_name_xcat)
def zimage_check(self, image_meta):
"""Do a brief check to see if the image is a valid zVM image."""
property_ = ['image_file_name', 'image_type_xcat', 'architecture',
'os_name', 'provisioning_method', 'os_version']
for prop in property_:
if prop not in image_meta['properties'].keys():
msg = (_("The image %s is not a valid zVM image,please check "
"if the image properties match the requirements.")
% image_meta['id'])
LOG.error(msg)
raise exception.ZVMImageError(msg=msg)
def cleanup_image_after_migration(self, inst_name):
"""Cleanup osimages in xCAT image repository while confirm migration
or revert migration at source compute.
"""
image_list = self._get_image_list_xcat()
matchee = ''.join(['rsz', inst_name])
for img in image_list:
img_name = img[0]
if matchee in img_name:
self.delete_image_from_xcat(img_name)
def get_root_disk_units(self, image_file_path):
"""use 'hexdump' to get the root_disk_units."""
cmd = "hexdump -n 48 -C %s" % image_file_path
try:
(output, _) = utils.execute(cmd, shell=True)
except processutils.ProcessExecutionError:
msg = (_("Get image property failed,"
" please check whether the image file exists!"))
raise exception.ZVMImageError(msg=msg)
LOG.debug("hexdump result is %s" % output)
try:
root_disk_units = int(output[144:156])
except ValueError:
msg = (_("Image file at %s is missing imbeded disk size "
"metadata, it was probably not captured with xCAT")
% image_file_path)
raise exception.ZVMImageError(msg=msg)
if 'FBA' not in output and 'CKD' not in output:
msg = (_("The image's disk type is not valid. Currently we only"
" support FBA and CKD disk"))
raise exception.ZVMImageError(msg=msg)
LOG.debug("The image's root_disk_units is %s" % root_disk_units)
return root_disk_units
def set_image_root_disk_units(self, context, image_meta, image_file_path):
"""Set the property 'root_disk_units'to image."""
new_image_meta = image_meta
root_disk_units = self.get_root_disk_units(image_file_path)
LOG.debug("The image's root_disk_units is %s" % root_disk_units)
(glance_image_service, image_id) = glance.get_remote_image_service(
context, image_meta['id'])
new_image_meta = glance_image_service.show(context, image_id)
new_image_meta['properties']['root_disk_units'] = str(root_disk_units)
try:
glance_image_service.update(context, image_id,
new_image_meta, None)
except nova_exception.ImageNotAuthorized:
msg = _('Not allowed to modify attributes for image %s') % image_id
LOG.error(msg)
return new_image_meta
def get_image_menifest(self, image_name_xcat):
"""Get image manifest info from xcat osimage table."""
attr_list = ['imagetype', 'osarch', 'imagename', 'osname',
'osvers', 'profile', 'provmethod']
menifest = zvmutils.xcat_cmd_gettab_multi_attr('osimage', 'imagename',
image_name_xcat, attr_list)
return menifest

View File

@ -0,0 +1,693 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import binascii
import datetime
from nova.compute import power_state
from nova import exception as nova_exception
from nova.i18n import _
from nova.openstack.common import loopingcall
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from nova_zvm.virt.zvm import const
from nova_zvm.virt.zvm import exception
from nova_zvm.virt.zvm import utils as zvmutils
from nova_zvm.virt.zvm import volumeop
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class ZVMInstance(object):
'''OpenStack instance that running on of z/VM hypervisor.'''
def __init__(self, instance={}):
"""Initialize instance attributes for database."""
self._xcat_url = zvmutils.XCATUrl()
self._xcat_conn = zvmutils.XCATConnection()
self._instance = instance
self._name = instance['name']
self._volumeop = volumeop.VolumeOperator()
def power_off(self):
"""Power off z/VM instance."""
try:
self._power_state("PUT", "off")
except exception.ZVMXCATInternalError as err:
err_str = err.format_message()
if ("Return Code: 200" in err_str and
"Reason Code: 12" in err_str):
# Instance already not active
LOG.warn(_("z/VM instance %s not active") % self._name)
return
else:
msg = _("Failed to power off instance: %s") % err_str
LOG.error(msg)
raise nova_exception.InstancePowerOffFailure(reason=msg)
def power_on(self):
""""Power on z/VM instance."""
try:
self._power_state("PUT", "on")
except exception.ZVMXCATInternalError as err:
err_str = err.format_message()
if ("Return Code: 200" in err_str and
"Reason Code: 8" in err_str):
# Instance already not active
LOG.warn(_("z/VM instance %s already active") % self._name)
return
self._wait_for_reachable()
if not self._reachable:
LOG.error(_("Failed to power on instance %s: timeout") %
self._name)
raise nova_exception.InstancePowerOnFailure(reason="timeout")
def reset(self):
"""Hard reboot z/VM instance."""
try:
self._power_state("PUT", "reset")
except exception.ZVMXCATInternalError as err:
err_str = err.format_message()
if ("Return Code: 200" in err_str and
"Reason Code: 12" in err_str):
# Be able to reset in power state of SHUTDOWN
LOG.warn(_("Reset z/VM instance %s from SHUTDOWN state") %
self._name)
return
else:
raise err
self._wait_for_reachable()
def reboot(self):
"""Soft reboot z/VM instance."""
self._power_state("PUT", "reboot")
self._wait_for_reachable()
def pause(self):
"""Pause the z/VM instance."""
self._power_state("PUT", "pause")
def unpause(self):
"""Unpause the z/VM instance."""
self._power_state("PUT", "unpause")
self._wait_for_reachable()
def attach_volume(self, volumeop, context, connection_info, instance,
mountpoint, is_active, rollback=True):
volumeop.attach_volume_to_instance(context, connection_info,
instance, mountpoint,
is_active, rollback)
def detach_volume(self, volumeop, connection_info, instance, mountpoint,
is_active, rollback=True):
volumeop.detach_volume_from_instance(connection_info,
instance, mountpoint,
is_active, rollback)
def get_info(self):
"""Get the current status of an z/VM instance.
Returns a dict containing:
:state: the running state, one of the power_state codes
:max_mem: (int) the maximum memory in KBytes allowed
:mem: (int) the memory in KBytes used by the domain
:num_cpu: (int) the number of virtual CPUs for the domain
:cpu_time: (int) the CPU time used in nanoseconds
"""
power_stat = self._get_power_stat()
is_reachable = self.is_reachable()
max_mem_kb = int(self._instance['memory_mb']) * 1024
if is_reachable:
try:
rec_list = self._get_rinv_info()
except exception.ZVMXCATInternalError:
raise nova_exception.InstanceNotFound(instance_id=self._name)
try:
mem = self._get_current_memory(rec_list)
num_cpu = self._get_cpu_count(rec_list)
cpu_time = self._get_cpu_used_time(rec_list)
_instance_info = {'state': power_stat,
'max_mem': max_mem_kb,
'mem': mem,
'num_cpu': num_cpu,
'cpu_time': cpu_time, }
except exception.ZVMInvalidXCATResponseDataError:
LOG.warn(_("Failed to get inventory info for %s") % self._name)
_instance_info = {'state': power_stat,
'max_mem': max_mem_kb,
'mem': max_mem_kb,
'num_cpu': self._instance['vcpus'],
'cpu_time': 0, }
else:
# Since xCAT rinv can't get info from a server that in power state
# of SHUTDOWN or PAUSED
if ((power_stat == power_state.RUNNING) and
(self._instance['power_state'] == power_state.PAUSED)):
# return paused state only previous power state is paused
_instance_info = {'state': power_state.PAUSED,
'max_mem': max_mem_kb,
'mem': max_mem_kb,
'num_cpu': self._instance['vcpus'],
'cpu_time': 0, }
else:
# otherwise return xcat returned state
_instance_info = {'state': power_stat,
'max_mem': max_mem_kb,
'mem': 0,
'num_cpu': self._instance['vcpus'],
'cpu_time': 0, }
return _instance_info
def create_xcat_node(self, zhcp, userid=None):
"""Create xCAT node for z/VM instance."""
LOG.debug("Creating xCAT node for %s" % self._name)
user_id = userid or self._name
body = ['userid=%s' % user_id,
'hcp=%s' % zhcp,
'mgt=zvm',
'groups=%s' % CONF.zvm_xcat_group]
url = self._xcat_url.mkdef('/' + self._name)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMXCATCreateNodeFailed, node=self._name):
zvmutils.xcat_request("POST", url, body)
def create_userid(self, block_device_info, image_meta):
"""Create z/VM userid into user directory for a z/VM instance."""
# We do not support boot from volume currently
LOG.debug("Creating the z/VM user entry for instance %s"
% self._name)
boot_from_volume = zvmutils.is_boot_from_volume(block_device_info)[1]
eph_disks = block_device_info.get('ephemerals', [])
kwprofile = 'profile=%s' % CONF.zvm_user_profile
body = [kwprofile,
'password=%s' % CONF.zvm_user_default_password,
'cpu=%i' % self._instance['vcpus'],
'memory=%im' % self._instance['memory_mb'],
'privilege=%s' % CONF.zvm_user_default_privilege]
url = self._xcat_url.mkvm('/' + self._name)
try:
zvmutils.xcat_request("POST", url, body)
if not boot_from_volume:
size = '%ig' % self._instance['root_gb']
# use a flavor the disk size is 0
if size == '0g':
size = image_meta['properties']['root_disk_units']
# Add root disk and set ipl
self.add_mdisk(CONF.zvm_diskpool,
CONF.zvm_user_root_vdev,
size)
self._set_ipl(CONF.zvm_user_root_vdev)
# Add additional ephemeral disk
if self._instance['ephemeral_gb'] != 0:
if eph_disks == []:
# Create ephemeral disk according to flavor
fmt = (CONF.default_ephemeral_format or
const.DEFAULT_EPH_DISK_FMT)
self.add_mdisk(CONF.zvm_diskpool,
CONF.zvm_user_adde_vdev,
'%ig' % self._instance['ephemeral_gb'],
fmt)
else:
# Create ephemeral disks according --ephemeral option
for idx, eph in enumerate(eph_disks):
vdev = (eph.get('vdev') or
zvmutils.generate_eph_vdev(idx))
size = eph['size']
size_in_units = eph.get('size_in_units', False)
if not size_in_units:
size = '%ig' % size
fmt = (eph.get('guest_format') or
CONF.default_ephemeral_format or
const.DEFAULT_EPH_DISK_FMT)
self.add_mdisk(CONF.zvm_diskpool, vdev, size, fmt)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError,
exception.ZVMDriverError) as err:
msg = _("Failed to create z/VM userid: %s") % err.format_message()
LOG.error(msg)
raise exception.ZVMXCATCreateUserIdFailed(instance=self._name,
msg=msg)
def prepare_volume_boot(self, context, instance, block_device_mapping,
root_device, volume_meta):
try:
connection_info = self._volumeop.get_root_volume_connection_info(
block_device_mapping, root_device)
(lun, wwpn, size,
fcp) = self._volumeop.extract_connection_info(context,
connection_info)
(kernel_parm_string, scpdata) = self._forge_hex_scpdata(fcp,
wwpn, lun, volume_meta)
loaddev_str = "%(wwpn)s %(lun)s 1 %(scpdata)s" % {'wwpn': wwpn,
'lun': lun, 'scpdata': scpdata}
self._volumeop.volume_boot_init(instance, fcp)
self._set_ipl(fcp)
self._set_loaddev(loaddev_str)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError,
exception.ZVMVolumeError,
exception.ZVMDriverError) as err:
msg = _("Failed to prepare volume to boot") % err.format_message()
LOG.error(msg)
raise exception.ZVMVolumeError(msg=msg)
return (lun, wwpn, size, fcp)
def clean_volume_boot(self, context, instance, block_device_mapping,
root_device):
try:
connection_info = self._volumeop.get_root_volume_connection_info(
block_device_mapping, root_device)
(lun, wwpn, size,
fcp) = self._volumeop.extract_connection_info(context,
connection_info)
self._volumeop.volume_boot_cleanup(instance, fcp)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError,
exception.ZVMVolumeError,
exception.ZVMDriverError) as err:
emsg = err.format_message()
msg = _("Failed to clean boot from volume preparations: %s") % emsg
LOG.warn(msg)
raise exception.ZVMVolumeError(msg=msg)
def _forge_hex_scpdata(self, fcp, wwpn, lun, volume_meta):
"""Forge scpdata in string form and HEX form."""
root = volume_meta['root']
os_type = volume_meta['os_type']
if os_type == 'rhel':
scpstring = ("=root=%(root)s selinux=0 "
"rd_ZFCP=0.0.%(fcp)s,0x%(wwpn)s,0x%(lun)s") % {
'root': root, 'fcp': fcp, 'wwpn': wwpn, 'lun': lun}
else: # sles
scpstring = ("=root=%(root)s "
"zfcp.device=0.0.%(fcp)s,0x%(wwpn)s,0x%(lun)s") % {
'root': root, 'fcp': fcp, 'wwpn': wwpn, 'lun': lun}
# Convert to HEX string
# Without Encode / Decode it will still work for python 2.6 but not for
# Python 3
try:
scpstring_b = scpstring.encode('ascii')
scpdata_b = binascii.hexlify(scpstring_b)
scpdata = scpdata_b.decode('ascii')
except Exception as err:
errmsg = _("Failed to forge hex scpdata: %s") % err
LOG.error(errmsg)
raise exception.ZVMDriverError(msg=errmsg)
return (scpstring, scpdata)
def _set_ipl(self, ipl_state):
body = ["--setipl %s" % ipl_state]
url = self._xcat_url.chvm('/' + self._name)
zvmutils.xcat_request("PUT", url, body)
def _set_loaddev(self, loaddev):
body = ["--setloaddev %s" % loaddev]
url = self._xcat_url.chvm('/' + self._name)
zvmutils.xcat_request("PUT", url, body)
def is_locked(self, zhcp_node):
cmd = "smcli Image_Lock_Query_DM -T %s" % self._name
resp = zvmutils.xdsh(zhcp_node, cmd)
return "is Unlocked..." not in str(resp)
def _wait_for_unlock(self, zhcp_node, interval=10, timeout=600):
LOG.debug("Waiting for unlock instance %s" % self._name)
def _wait_unlock(expiration):
if timeutils.utcnow() > expiration:
LOG.debug("Waiting for unlock instance %s timeout" %
self._name)
raise loopingcall.LoopingCallDone()
if not self.is_locked(zhcp_node):
LOG.debug("Instance %s is unlocked" %
self._name)
raise loopingcall.LoopingCallDone()
expiration = timeutils.utcnow() + datetime.timedelta(seconds=timeout)
timer = loopingcall.FixedIntervalLoopingCall(_wait_unlock,
expiration)
timer.start(interval=interval).wait()
def delete_userid(self, zhcp_node):
"""Delete z/VM userid for the instance.This will remove xCAT node
at same time.
"""
url = self._xcat_url.rmvm('/' + self._name)
try:
zvmutils.xcat_request("DELETE", url)
except exception.ZVMXCATInternalError as err:
emsg = err.format_message()
if (emsg.__contains__("Return Code: 400") and
emsg.__contains__("Reason Code: 4")):
# zVM user definition not found, delete xCAT node directly
self.delete_xcat_node()
elif (emsg.__contains__("Return Code: 400") and
(emsg.__contains__("Reason Code: 16") or
emsg.__contains__("Reason Code: 12"))):
# The vm or vm device was locked. Unlock before deleting
self._wait_for_unlock(zhcp_node)
zvmutils.xcat_request("DELETE", url)
else:
raise err
except exception.ZVMXCATRequestFailed as err:
emsg = err.format_message()
if (emsg.__contains__("Invalid nodes and/or groups") and
emsg.__contains__("Forbidden")):
# Assume neither zVM userid nor xCAT node exist in this case
return
else:
raise err
def delete_xcat_node(self):
"""Remove xCAT node for z/VM instance."""
url = self._xcat_url.rmdef('/' + self._name)
try:
zvmutils.xcat_request("DELETE", url)
except exception.ZVMXCATInternalError as err:
if err.format_message().__contains__("Could not find an object"):
# The xCAT node not exist
return
else:
raise err
def add_mdisk(self, diskpool, vdev, size, fmt=None):
"""Add a 3390 mdisk for a z/VM user.
NOTE: No read, write and multi password specified, and
access mode default as 'MR'.
"""
disk_type = CONF.zvm_diskpool_type
if (disk_type == 'ECKD'):
action = '--add3390'
elif (disk_type == 'FBA'):
action = '--add9336'
else:
errmsg = _("Disk type %s is not supported.") % disk_type
LOG.error(errmsg)
raise exception.ZVMDriverError(msg=errmsg)
if fmt:
body = [" ".join([action, diskpool, vdev, size, "MR", "''", "''",
"''", fmt])]
else:
body = [" ".join([action, diskpool, vdev, size])]
url = self._xcat_url.chvm('/' + self._name)
zvmutils.xcat_request("PUT", url, body)
def _power_state(self, method, state):
"""Invoke xCAT REST API to set/get power state for a instance."""
body = [state]
url = self._xcat_url.rpower('/' + self._name)
return zvmutils.xcat_request(method, url, body)
def _get_power_stat(self):
"""Get power status of a z/VM instance."""
LOG.debug('Query power stat of %s' % self._name)
res_dict = self._power_state("GET", "stat")
@zvmutils.wrap_invalid_xcat_resp_data_error
def _get_power_string(d):
tempstr = d['info'][0][0]
return tempstr[(tempstr.find(':') + 2):].strip()
power_stat = _get_power_string(res_dict)
return zvmutils.mapping_power_stat(power_stat)
def _get_rinv_info(self):
"""get rinv result and return in a list."""
url = self._xcat_url.rinv('/' + self._name, '&field=cpumem')
LOG.debug('Remote inventory of %s' % self._name)
res_info = zvmutils.xcat_request("GET", url)['info']
with zvmutils.expect_invalid_xcat_resp_data():
rinv_info = res_info[0][0].split('\n')
return rinv_info
@zvmutils.wrap_invalid_xcat_resp_data_error
def _modify_storage_format(self, mem):
"""modify storage from 'G' ' M' to 'K'."""
# Only special case for 0
if mem == '0':
return 0
new_mem = 0
if mem.endswith('G'):
new_mem = int(mem[:-1]) * 1024 * 1024
elif mem.endswith('M'):
new_mem = int(mem[:-1]) * 1024
elif mem.endswith('K'):
new_mem = int(mem[:-1])
else:
exp = "ending with a 'G', 'M' or 'K'"
errmsg = _("Invalid memory format: %(invalid)s; Expected: "
"%(exp)s") % {'invalid': mem, 'exp': exp}
LOG.error(errmsg)
raise exception.ZVMInvalidXCATResponseDataError(msg=errmsg)
return new_mem
@zvmutils.wrap_invalid_xcat_resp_data_error
def _get_current_memory(self, rec_list):
"""Return the max memory can be used."""
_mem = None
for rec in rec_list:
if rec.__contains__("Total Memory: "):
tmp_list = rec.split()
_mem = tmp_list[3]
_mem = self._modify_storage_format(_mem)
return _mem
@zvmutils.wrap_invalid_xcat_resp_data_error
def _get_cpu_count(self, rec_list):
"""Return the virtual cpu count."""
_cpu_flag = False
num_cpu = 0
for rec in rec_list:
if (_cpu_flag is True):
tmp_list = rec.split()
if (len(tmp_list) > 1):
if (tmp_list[1] == "CPU"):
num_cpu += 1
else:
_cpu_flag = False
if rec.__contains__("Processors: "):
_cpu_flag = True
return num_cpu
@zvmutils.wrap_invalid_xcat_resp_data_error
def _get_cpu_used_time(self, rec_list):
"""Return the cpu used time in."""
cpu_time = None
for rec in rec_list:
if rec.__contains__("CPU Used Time: "):
tmp_list = rec.split()
cpu_time = tmp_list[4]
return int(cpu_time)
def is_reachable(self):
"""Return True is the instance is reachable."""
url = self._xcat_url.nodestat('/' + self._name)
LOG.debug('Get instance status of %s' % self._name)
res_dict = zvmutils.xcat_request("GET", url)
with zvmutils.expect_invalid_xcat_resp_data():
status = res_dict['node'][0][0]['data'][0]
if status is not None:
if status.__contains__('sshd'):
return True
return False
def _wait_for_reachable(self):
"""Called at an interval until the instance is reachable."""
self._reachable = False
def _wait_reachable(expiration):
if (CONF.zvm_reachable_timeout and
timeutils.utcnow() > expiration):
raise loopingcall.LoopingCallDone()
if self.is_reachable():
self._reachable = True
LOG.debug("Instance %s reachable now" %
self._name)
raise loopingcall.LoopingCallDone()
expiration = timeutils.utcnow() + datetime.timedelta(
seconds=CONF.zvm_reachable_timeout)
timer = loopingcall.FixedIntervalLoopingCall(_wait_reachable,
expiration)
timer.start(interval=5).wait()
def update_node_info(self, image_meta):
LOG.debug("Update the node info for instance %s" % self._name)
image_name = image_meta['name']
image_id = image_meta['id']
os_type = image_meta['properties']['os_version']
os_arch = image_meta['properties']['architecture']
prov_method = image_meta['properties']['provisioning_method']
profile_name = '_'.join((image_name, image_id.replace('-', '_')))
body = ['noderes.netboot=%s' % const.HYPERVISOR_TYPE,
'nodetype.os=%s' % os_type,
'nodetype.arch=%s' % os_arch,
'nodetype.provmethod=%s' % prov_method,
'nodetype.profile=%s' % profile_name]
url = self._xcat_url.chtab('/' + self._name)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMXCATUpdateNodeFailed, node=self._name):
zvmutils.xcat_request("PUT", url, body)
def update_node_info_resize(self, image_name_xcat):
LOG.debug("Update the nodetype for instance %s" % self._name)
name_section = image_name_xcat.split("-")
os_type = name_section[0]
os_arch = name_section[1]
profile_name = name_section[3]
body = ['noderes.netboot=%s' % const.HYPERVISOR_TYPE,
'nodetype.os=%s' % os_type,
'nodetype.arch=%s' % os_arch,
'nodetype.provmethod=%s' % 'sysclone',
'nodetype.profile=%s' % profile_name]
url = self._xcat_url.chtab('/' + self._name)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMXCATUpdateNodeFailed, node=self._name):
zvmutils.xcat_request("PUT", url, body)
def get_provmethod(self):
addp = "&col=node=%s&attribute=provmethod" % self._name
url = self._xcat_url.gettab('/nodetype', addp)
res_info = zvmutils.xcat_request("GET", url)
return res_info['data'][0][0]
def update_node_provmethod(self, provmethod):
LOG.debug("Update the nodetype for instance %s" % self._name)
body = ['nodetype.provmethod=%s' % provmethod]
url = self._xcat_url.chtab('/' + self._name)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMXCATUpdateNodeFailed, node=self._name):
zvmutils.xcat_request("PUT", url, body)
def update_node_def(self, hcp, userid):
"""Update xCAT node definition."""
body = ['zvm.hcp=%s' % hcp,
'zvm.userid=%s' % userid]
url = self._xcat_url.chtab('/' + self._name)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMXCATUpdateNodeFailed, node=self._name):
zvmutils.xcat_request("PUT", url, body)
def deploy_node(self, image_name, transportfiles=None, vdev=None):
LOG.debug("Begin to deploy image on instance %s" % self._name)
vdev = vdev or CONF.zvm_user_root_vdev
remote_host_info = zvmutils.get_host()
body = ['netboot',
'device=%s' % vdev,
'osimage=%s' % image_name]
if transportfiles:
body.append('transport=%s' % transportfiles)
body.append('remotehost=%s' % remote_host_info)
url = self._xcat_url.nodeset('/' + self._name)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMXCATDeployNodeFailed, node=self._name):
zvmutils.xcat_request("PUT", url, body)
def copy_xcat_node(self, source_node_name):
"""Create xCAT node from an existing z/VM instance."""
LOG.debug("Creating xCAT node %s from existing node" % self._name)
url = self._xcat_url.lsdef_node('/' + source_node_name)
res_info = zvmutils.xcat_request("GET", url)['info'][0]
body = []
for info in res_info:
if ("=" in info and ("postbootscripts" not in info)
and ("postscripts" not in info)
and ("hostnames" not in info)):
body.append(info.lstrip())
url = self._xcat_url.mkdef('/' + self._name)
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMXCATCreateNodeFailed, node=self._name):
zvmutils.xcat_request("POST", url, body)
def get_console_log(self, logsize):
"""get console log."""
url = self._xcat_url.rinv('/' + self._name, '&field=console'
'&field=%s') % logsize
LOG.debug('Get console log of %s' % self._name)
res_info = zvmutils.xcat_request("GET", url)['info']
with zvmutils.expect_invalid_xcat_resp_data():
rinv_info = res_info[0][0]
return rinv_info

View File

@ -0,0 +1,189 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nova.i18n import _
from oslo_config import cfg
from oslo_log import log as logging
from nova_zvm.virt.zvm import exception
from nova_zvm.virt.zvm import utils as zvmutils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
NetworkUtils = zvmutils.NetworkUtils()
class NetworkOperator(object):
"""Configuration check and manage MAC address."""
def __init__(self):
self._xcat_url = zvmutils.XCATUrl()
def add_xcat_host(self, node, ip, host_name):
"""Add/Update hostname/ip bundle in xCAT MN nodes table."""
commands = "node=%s" % node + " hosts.ip=%s" % ip
commands += " hosts.hostnames=%s" % host_name
body = [commands]
url = self._xcat_url.tabch("/hosts")
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
result_data = zvmutils.xcat_request("PUT", url, body)['data']
return result_data
def _delete_xcat_host(self, node_name):
"""Remove xcat hosts table rows where node name is node_name."""
commands = "-d node=%s hosts" % node_name
body = [commands]
url = self._xcat_url.tabch("/hosts")
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url, body)['data']
def add_xcat_mac(self, node, interface, mac, zhcp=None):
"""Add node name, interface, mac address into xcat mac table."""
commands = "mac.node=%s" % node + " mac.mac=%s" % mac
commands += " mac.interface=%s" % interface
if zhcp is not None:
commands += " mac.comments=%s" % zhcp
url = self._xcat_url.tabch("/mac")
body = [commands]
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url, body)['data']
def add_xcat_switch(self, node, nic_name, interface, zhcp=None):
"""Add node name and nic name address into xcat switch table."""
commands = "switch.node=%s" % node
commands += " switch.port=%s" % nic_name
commands += " switch.interface=%s" % interface
if zhcp is not None:
commands += " switch.comments=%s" % zhcp
url = self._xcat_url.tabch("/switch")
body = [commands]
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url, body)['data']
def _delete_xcat_mac(self, node_name):
"""Remove node mac record from xcat mac table."""
commands = "-d node=%s mac" % node_name
url = self._xcat_url.tabch("/mac")
body = [commands]
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url, body)['data']
def _delete_xcat_switch(self, node_name):
"""Remove node switch record from xcat switch table."""
commands = "-d node=%s switch" % node_name
url = self._xcat_url.tabch("/switch")
body = [commands]
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url, body)['data']
def update_xcat_mac(self, node, interface, mac, zhcp=None):
"""Add node name, interface, mac address into xcat mac table."""
commands = "node=%s" % node + " interface=%s" % interface
commands += " mac.mac=%s" % mac
if zhcp is not None:
commands += " mac.comments=%s" % zhcp
url = self._xcat_url.tabch("/mac")
body = [commands]
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url, body)['data']
def update_xcat_switch(self, node, nic_name, interface, zhcp=None):
"""Add node name and nic name address into xcat switch table."""
commands = "node=%s" % node
commands += " interface=%s" % interface
commands += " switch.port=%s" % nic_name
if zhcp is not None:
commands += " switch.comments=%s" % zhcp
url = self._xcat_url.tabch("/switch")
body = [commands]
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url, body)['data']
def clean_mac_switch_host(self, node_name):
"""Clean node records in xCAT mac, host and switch table."""
self.clean_mac_switch(node_name)
self._delete_xcat_host(node_name)
def clean_mac_switch(self, node_name):
"""Clean node records in xCAT mac and switch table."""
self._delete_xcat_mac(node_name)
self._delete_xcat_switch(node_name)
def makehosts(self):
"""Update xCAT MN /etc/hosts file."""
url = self._xcat_url.network("/makehosts")
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url)['data']
def makeDNS(self):
"""Update xCAT MN DNS."""
url = self._xcat_url.network("/makedns")
with zvmutils.except_xcat_call_failed_and_reraise(
exception.ZVMNetworkError):
return zvmutils.xcat_request("PUT", url)['data']
def config_xcat_mac(self, instance_name):
"""Hook xCat to prevent assign MAC for instance."""
fake_mac_addr = "00:00:00:00:00:00"
nic_name = "fake"
self.add_xcat_mac(instance_name, nic_name, fake_mac_addr)
def create_nic(self, zhcpnode, inst_name, nic_name, mac_address, vdev,
userid=None):
"""Create network information in xCAT and zVM user direct."""
macid = mac_address.replace(':', '')[-6:]
self._add_instance_nic(zhcpnode, inst_name, vdev, macid, userid)
self._delete_xcat_mac(inst_name)
self.add_xcat_mac(inst_name, vdev, mac_address, zhcpnode)
self.add_xcat_switch(inst_name, nic_name, vdev, zhcpnode)
def _add_instance_nic(self, zhcpnode, inst_name, vdev, macid, userid=None):
"""Add NIC defination into user direct."""
if userid is None:
command = ("/opt/zhcp/bin/smcli Image_Definition_Update_DM -T %s" %
inst_name)
else:
command = ("/opt/zhcp/bin/smcli Image_Definition_Update_DM -T %s" %
userid)
command += " -k \'NICDEF=VDEV=%s TYPE=QDIO " % vdev
command += "MACID=%s\'" % macid
try:
zvmutils.xdsh(zhcpnode, command)
except exception.ZVMXCATXdshFailed as err:
msg = _("Adding nic error: %s") % err.format_message()
raise exception.ZVMNetworkError(msg=msg)

943
nova_zvm/virt/zvm/utils.py Normal file
View File

@ -0,0 +1,943 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import functools
import httplib
import os
import shutil
import socket
import time
from nova import block_device
from nova.compute import power_state
from nova import exception as nova_exception
from nova.i18n import _
from nova.i18n import _LE
from nova.virt import driver
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import excutils
from nova_zvm.virt.zvm import const
from nova_zvm.virt.zvm import exception
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.import_opt('instances_path', 'nova.compute.manager')
class XCATUrl(object):
"""To return xCAT url for invoking xCAT REST API."""
def __init__(self):
"""Set constant that used to form xCAT url."""
self.PREFIX = '/xcatws'
self.SUFFIX = ('?userName=' + CONF.zvm_xcat_username +
'&password=' + CONF.zvm_xcat_password +
'&format=json')
self.NODES = '/nodes'
self.VMS = '/vms'
self.IMAGES = '/images'
self.OBJECTS = '/objects/osimage'
self.OS = '/OS'
self.TABLES = '/tables'
self.HV = '/hypervisor'
self.NETWORK = '/networks'
self.POWER = '/power'
self.INVENTORY = '/inventory'
self.STATUS = '/status'
self.MIGRATE = '/migrate'
self.CAPTURE = '/capture'
self.EXPORT = '/export'
self.IMGIMPORT = '/import'
self.BOOTSTAT = '/bootstate'
self.XDSH = '/dsh'
def _nodes(self, arg=''):
return self.PREFIX + self.NODES + arg + self.SUFFIX
def _vms(self, arg=''):
return self.PREFIX + self.VMS + arg + self.SUFFIX
def _hv(self, arg=''):
return self.PREFIX + self.HV + arg + self.SUFFIX
def rpower(self, arg=''):
return self.PREFIX + self.NODES + arg + self.POWER + self.SUFFIX
def nodels(self, arg=''):
return self._nodes(arg)
def rinv(self, arg='', addp=None):
rurl = self.PREFIX + self.NODES + arg + self.INVENTORY + self.SUFFIX
return self._append_addp(rurl, addp)
def mkdef(self, arg=''):
return self._nodes(arg)
def rmdef(self, arg=''):
return self._nodes(arg)
def nodestat(self, arg=''):
return self.PREFIX + self.NODES + arg + self.STATUS + self.SUFFIX
def chvm(self, arg=''):
return self._vms(arg)
def lsvm(self, arg=''):
return self._vms(arg)
def chhv(self, arg=''):
return self._hv(arg)
def mkvm(self, arg=''):
return self._vms(arg)
def rmvm(self, arg=''):
return self._vms(arg)
def tabdump(self, arg='', addp=None):
rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX
return self._append_addp(rurl, addp)
def _append_addp(self, rurl, addp=None):
if addp is not None:
return rurl + addp
else:
return rurl
def imgcapture(self, arg=''):
return self.PREFIX + self.IMAGES + arg + self.CAPTURE + self.SUFFIX
def imgexport(self, arg=''):
return self.PREFIX + self.IMAGES + arg + self.EXPORT + self.SUFFIX
def rmimage(self, arg=''):
return self.PREFIX + self.IMAGES + arg + self.SUFFIX
def rmobject(self, arg=''):
return self.PREFIX + self.OBJECTS + arg + self.SUFFIX
def lsdef_node(self, arg='', addp=None):
rurl = self.PREFIX + self.NODES + arg + self.SUFFIX
return self._append_addp(rurl, addp)
def lsdef_image(self, arg='', addp=None):
rurl = self.PREFIX + self.IMAGES + arg + self.SUFFIX
return self._append_addp(rurl, addp)
def imgimport(self, arg=''):
return self.PREFIX + self.IMAGES + self.IMGIMPORT + arg + self.SUFFIX
def chtab(self, arg=''):
return self.PREFIX + self.NODES + arg + self.SUFFIX
def nodeset(self, arg=''):
return self.PREFIX + self.NODES + arg + self.BOOTSTAT + self.SUFFIX
def rmigrate(self, arg=''):
return self.PREFIX + self.NODES + arg + self.MIGRATE + self.SUFFIX
def gettab(self, arg='', addp=None):
rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX
return self._append_addp(rurl, addp)
def tabch(self, arg='', addp=None):
"""Add/update/delete row(s) in table arg, with attribute addp."""
rurl = self.PREFIX + self.TABLES + arg + self.SUFFIX
return self._append_addp(rurl, addp)
def xdsh(self, arg=''):
"""Run shell command."""
return self.PREFIX + self.NODES + arg + self.XDSH + self.SUFFIX
def network(self, arg='', addp=None):
rurl = self.PREFIX + self.NETWORK + arg + self.SUFFIX
if addp is not None:
return rurl + addp
else:
return rurl
class XCATConnection():
"""Https requests to xCAT web service."""
def __init__(self):
"""Initialize https connection to xCAT service."""
self.host = CONF.zvm_xcat_server
self.conn = httplib.HTTPSConnection(self.host,
timeout=CONF.zvm_xcat_connection_timeout)
def request(self, method, url, body=None, headers={}):
"""Send https request to xCAT server.
Will return a python dictionary including:
{'status': http return code,
'reason': http reason,
'message': response message}
"""
if body is not None:
body = jsonutils.dumps(body)
headers = {'content-type': 'text/plain',
'content-length': len(body)}
try:
self.conn.request(method, url, body, headers)
except socket.gaierror as err:
msg = _("Failed to find address: %s") % err
raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg)
except (socket.error, socket.timeout) as err:
msg = _("Communication error: %s") % err
raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg)
try:
res = self.conn.getresponse()
except Exception as err:
msg = _("Failed to get response from xCAT: %s") % err
raise exception.ZVMXCATRequestFailed(xcatserver=self.host, msg=msg)
msg = res.read()
resp = {
'status': res.status,
'reason': res.reason,
'message': msg}
# Only "200" or "201" returned from xCAT can be considered
# as good status
err = None
if method == "POST":
if res.status != 201:
err = str(resp)
else:
if res.status != 200:
err = str(resp)
if err is not None:
raise exception.ZVMXCATRequestFailed(xcatserver=self.host,
msg=err)
return resp
def xcat_request(method, url, body=None, headers={}):
conn = XCATConnection()
resp = conn.request(method, url, body, headers)
return load_xcat_resp(resp['message'])
def jsonloads(jsonstr):
try:
return jsonutils.loads(jsonstr)
except ValueError:
errmsg = _("xCAT response data is not in JSON format")
LOG.error(errmsg)
raise exception.ZVMDriverError(msg=errmsg)
@contextlib.contextmanager
def expect_invalid_xcat_resp_data():
"""Catch exceptions when using xCAT response data."""
try:
yield
except (ValueError, TypeError, IndexError, AttributeError,
KeyError) as err:
raise exception.ZVMInvalidXCATResponseDataError(msg=err)
def wrap_invalid_xcat_resp_data_error(function):
"""Catch exceptions when using xCAT response data."""
@functools.wraps(function)
def decorated_function(*arg, **kwargs):
try:
return function(*arg, **kwargs)
except (ValueError, TypeError, IndexError, AttributeError,
KeyError) as err:
raise exception.ZVMInvalidXCATResponseDataError(msg=err)
return decorated_function
@contextlib.contextmanager
def ignore_errors():
"""Only execute the clauses and ignore the results."""
try:
yield
except Exception as err:
emsg = format_exception_msg(err)
LOG.debug("Ignore an error: %s" % emsg)
pass
@contextlib.contextmanager
def except_xcat_call_failed_and_reraise(exc, **kwargs):
"""Catch all kinds of xCAT call failure and reraise.
exc: the exception that would be raised.
"""
try:
yield
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError) as err:
msg = err.format_message()
kwargs['msg'] = msg
LOG.error('XCAT response return error: %s', msg)
raise exc(**kwargs)
def convert_to_mb(s):
"""Convert memory size from GB to MB."""
s = s.upper()
try:
if s.endswith('G'):
return float(s[:-1].strip()) * 1024
else:
return float(s[:-1].strip())
except (IndexError, ValueError, KeyError, TypeError) as e:
errmsg = _("Invalid memory format: %s") % e
raise exception.ZVMDriverError(msg=errmsg)
@wrap_invalid_xcat_resp_data_error
def translate_xcat_resp(rawdata, dirt):
"""Translate xCAT response JSON stream to a python dictionary.
xCAT response example:
node: keyword1: value1\n
node: keyword2: value2\n
...
node: keywordn: valuen\n
Will return a python dictionary:
{keyword1: value1,
keyword2: value2,
...
keywordn: valuen,}
"""
data_list = rawdata.split("\n")
data = {}
for ls in data_list:
for k in dirt.keys():
if ls.__contains__(dirt[k]):
data[k] = ls[(ls.find(dirt[k]) + len(dirt[k])):].strip()
break
if data == {}:
msg = _("No value matched with keywords. Raw Data: %(raw)s; "
"Keywords: %(kws)s") % {'raw': rawdata, 'kws': str(dirt)}
raise exception.ZVMInvalidXCATResponseDataError(msg=msg)
return data
def mapping_power_stat(power_stat):
"""Translate power state to OpenStack defined constants."""
return const.ZVM_POWER_STAT.get(power_stat, power_state.NOSTATE)
@wrap_invalid_xcat_resp_data_error
def load_xcat_resp(message):
"""Abstract information from xCAT REST response body.
As default, xCAT response will in format of JSON and can be
converted to Python dictionary, would looks like:
{"data": [{"info": [info,]}, {"data": [data,]}, ..., {"error": [error,]}]}
Returns a Python dictionary, looks like:
{'info': [info,],
'data': [data,],
...
'error': [error,]}
"""
resp_list = jsonloads(message)['data']
keys = const.XCAT_RESPONSE_KEYS
resp = {}
for k in keys:
resp[k] = []
for d in resp_list:
for k in keys:
if d.get(k) is not None:
resp[k].append(d.get(k))
err = resp.get('error')
if err != []:
for e in err:
if _is_warning_or_recoverable_issue(str(e)):
# ignore known warnings or errors:
continue
else:
raise exception.ZVMXCATInternalError(msg=message)
_log_warnings(resp)
return resp
def _log_warnings(resp):
for msg in (resp['info'], resp['node'], resp['data']):
msgstr = str(msg)
if 'warn' in msgstr.lower():
LOG.info(_("Warning from xCAT: %s") % msgstr)
def _is_warning_or_recoverable_issue(err_str):
return _is_warning(err_str) or _is_recoverable_issue(err_str)
def _is_recoverable_issue(err_str):
dirmaint_request_counter_save = ['Return Code: 596', 'Reason Code: 1185']
recoverable_issues = [dirmaint_request_counter_save]
for issue in recoverable_issues:
# Search all matchs in the return value
# any mismatch leads to recoverable not empty
recoverable = [t for t in issue if t not in err_str]
if recoverable == []:
return True
return False
def _is_warning(err_str):
ignore_list = (
'Warning: the RSA host key for',
'Warning: Permanently added',
'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED',
)
for im in ignore_list:
if im in err_str:
return True
return False
def volume_in_mapping(mount_device, block_device_info):
block_device_list = [block_device.strip_dev(vol['mount_device'])
for vol in
driver.block_device_info_get_mapping(
block_device_info)]
swap = driver.block_device_info_get_swap(block_device_info)
if driver.swap_is_usable(swap):
block_device_list.append(
block_device.strip_dev(swap['device_name']))
block_device_list += [block_device.strip_dev(ephemeral['device_name'])
for ephemeral in
driver.block_device_info_get_ephemerals(
block_device_info)]
LOG.debug("block_device_list %s", block_device_list)
return block_device.strip_dev(mount_device) in block_device_list
def is_volume_root(root_device, mountpoint):
"""This judges if the moutpoint equals the root_device."""
return block_device.strip_dev(mountpoint) == block_device.strip_dev(
root_device)
def is_boot_from_volume(block_device_info):
root_mount_device = '/dev/' + const.ZVM_DEFAULT_ROOT_VOLUME
root_mount_device = root_mount_device.replace('/dev/s', '/dev/v')
boot_from_volume = volume_in_mapping(root_mount_device, block_device_info)
return root_mount_device, boot_from_volume
def get_host():
return ''.join([os.environ["USER"], '@', CONF.my_ip])
def get_userid(node_name):
"""Returns z/VM userid for the xCAT node."""
url = XCATUrl().lsdef_node(''.join(['/', node_name]))
info = xcat_request('GET', url)['info']
with expect_invalid_xcat_resp_data():
for s in info[0]:
if s.__contains__('userid='):
return s.strip().rpartition('=')[2]
def xdsh(node, commands):
""""Run command on xCAT node."""
LOG.debug('Run command %(cmd)s on xCAT node %(node)s' %
{'cmd': commands, 'node': node})
def xdsh_execute(node, commands):
"""Invoke xCAT REST API to execute command on node."""
xdsh_commands = 'command=%s' % commands
body = [xdsh_commands]
url = XCATUrl().xdsh('/' + node)
return xcat_request("PUT", url, body)
with except_xcat_call_failed_and_reraise(
exception.ZVMXCATXdshFailed):
res_dict = xdsh_execute(node, commands)
return res_dict
def punch_file(node, fn, fclass):
body = [" ".join(['--punchfile', fn, fclass, get_host()])]
url = XCATUrl().chvm('/' + node)
try:
xcat_request("PUT", url, body)
except Exception as err:
emsg = format_exception_msg(err)
with excutils.save_and_reraise_exception():
LOG.error(_('Punch file to %(node)s failed: %(msg)s') %
{'node': node, 'msg': emsg})
finally:
os.remove(fn)
def punch_adminpass_file(instance_path, instance_name, admin_password):
adminpass_fn = ''.join([instance_path, '/adminpwd.sh'])
_generate_adminpass_file(adminpass_fn, admin_password)
punch_file(instance_name, adminpass_fn, 'X')
def punch_xcat_auth_file(instance_path, instance_name):
"""Make xCAT MN authorized by virtual machines."""
mn_pub_key = get_mn_pub_key()
auth_fn = ''.join([instance_path, '/xcatauth.sh'])
_generate_auth_file(auth_fn, mn_pub_key)
punch_file(instance_name, auth_fn, 'X')
def process_eph_disk(instance_name, vdev=None, fmt=None, mntdir=None):
if not fmt:
fmt = CONF.default_ephemeral_format or const.DEFAULT_EPH_DISK_FMT
vdev = vdev or CONF.zvm_user_adde_vdev
mntdir = mntdir or CONF.zvm_default_ephemeral_mntdir
eph_parms = _generate_eph_parmline(vdev, fmt, mntdir)
aemod_handler(instance_name, const.DISK_FUNC_NAME, eph_parms)
def aemod_handler(instance_name, func_name, parms):
url = XCATUrl().chvm('/' + instance_name)
body = [" ".join(['--aemod', func_name, parms])]
try:
xcat_request("PUT", url, body)
except Exception as err:
emsg = format_exception_msg(err)
with excutils.save_and_reraise_exception():
LOG.error(_LE('Invoke AE method function: %(func)s on %(node)s '
'failed with reason: %(msg)s') %
{'func': func_name, 'node': instance_name, 'msg': emsg})
def punch_configdrive_file(transportfiles, instance_name):
punch_file(instance_name, transportfiles, 'X')
def punch_zipl_file(instance_path, instance_name, lun, wwpn, fcp, volume_meta):
zipl_fn = ''.join([instance_path, '/ziplset.sh'])
_generate_zipl_file(zipl_fn, lun, wwpn, fcp, volume_meta)
punch_file(instance_name, zipl_fn, 'X')
def generate_vdev(base, offset=1):
"""Generate virtual device number base on base vdev.
:param base: base virtual device number, string of 4 bit hex.
:param offset: offset to base, integer.
:output: virtual device number, string of 4 bit hex.
"""
vdev = hex(int(base, 16) + offset)[2:]
return vdev.rjust(4, '0')
def generate_eph_vdev(offset=1):
"""Generate virtual device number for ephemeral disks.
:parm offset: offset to zvm_user_adde_vdev.
:output: virtual device number, string of 4 bit hex.
"""
vdev = generate_vdev(CONF.zvm_user_adde_vdev, offset + 1)
if offset >= 0 and offset < 254:
return vdev
else:
msg = _("Invalid virtual device number for ephemeral disk: %s") % vdev
LOG.error(msg)
raise exception.ZVMDriverError(msg=msg)
def _generate_eph_parmline(vdev, fmt, mntdir):
parms = [
'action=addMdisk',
'vaddr=' + vdev,
'filesys=' + fmt,
'mntdir=' + mntdir
]
parmline = ' '.join(parms)
return parmline
def _generate_auth_file(fn, pub_key):
lines = ['#!/bin/bash\n',
'echo "%s" >> /root/.ssh/authorized_keys' % pub_key]
with open(fn, 'w') as f:
f.writelines(lines)
def _generate_adminpass_file(fn, admin_password):
lines = ['#! /bin/bash\n',
'echo %s|passwd --stdin root' % admin_password]
with open(fn, 'w') as f:
f.writelines(lines)
def _generate_zipl_file(fn, lun, wwpn, fcp, volume_meta):
image = volume_meta['image']
ramdisk = volume_meta['ramdisk']
root = volume_meta['root']
os_type = volume_meta['os_type']
if os_type == 'rhel':
lines = ['#!/bin/bash\n',
('echo -e "[defaultboot]\\n'
'timeout=5\\n'
'default=boot-from-volume\\n'
'target=/boot/\\n'
'[boot-from-volume]\\n'
'image=%(image)s\\n'
'ramdisk=%(ramdisk)s\\n'
'parameters=\\"root=%(root)s '
'rd_ZFCP=0.0.%(fcp)s,0x%(wwpn)s,0x%(lun)s selinux=0\\""'
'>/etc/zipl_volume.conf\n'
'zipl -c /etc/zipl_volume.conf')
% {'image': image, 'ramdisk': ramdisk, 'root': root, 'fcp': fcp,
'wwpn': wwpn, 'lun': lun}]
else: # sles
lines = ['#!/bin/bash\n',
('echo -e "[defaultboot]\\n'
'default=boot-from-volume\\n'
'[boot-from-volume]\\n'
'image=%(image)s\\n'
'target = /boot/zipl\\n'
'ramdisk=%(ramdisk)s\\n'
'parameters=\\"root=%(root)s '
'zfcp.device=0.0.%(fcp)s,0x%(wwpn)s,0x%(lun)s\\""'
'>/etc/zipl_volume.conf\n'
'mkinitrd\n'
'zipl -c /etc/zipl_volume.conf')
% {'image': image, 'ramdisk': ramdisk, 'root': root, 'fcp': fcp,
'wwpn': wwpn, 'lun': lun}]
with open(fn, 'w') as f:
f.writelines(lines)
@wrap_invalid_xcat_resp_data_error
def get_mn_pub_key():
cmd = 'cat /root/.ssh/id_rsa.pub'
resp = xdsh(CONF.zvm_xcat_master, cmd)
key = resp['data'][0][0]
start_idx = key.find('ssh-rsa')
key = key[start_idx:]
return key
def parse_os_version(os_version):
"""Separate os and version from os_version.
Possible return value are only:
('rhel', x.y) and ('sles', x.y) where x.y may not be digits
"""
supported = {'rhel': ['rhel', 'redhat', 'red hat'],
'sles': ['suse', 'sles']}
os_version = os_version.lower()
for distro, patterns in supported.items():
for i in patterns:
if os_version.startswith(i):
# Not guarrentee the version is digital
return distro, os_version.split(i, 2)[1]
else:
raise exception.ZVMImageError(msg='Unknown os_version property')
def xcat_cmd_gettab(table, col, col_value, attr):
addp = ("&col=%(col)s=%(col_value)s&attribute=%(attr)s" %
{'col': col, 'col_value': col_value, 'attr': attr})
url = XCATUrl().gettab('/%s' % table, addp)
res_info = xcat_request("GET", url)
with expect_invalid_xcat_resp_data():
return res_info['data'][0][0]
def xcat_cmd_gettab_multi_attr(table, col, col_value, attr_list):
attr_str = ''.join(["&attribute=%s" % attr for attr in attr_list])
addp = ("&col=%(col)s=%(col_value)s&%(attr)s" %
{'col': col, 'col_value': col_value, 'attr': attr_str})
url = XCATUrl().gettab('/%s' % table, addp)
res_data = xcat_request("GET", url)['data']
outp = {}
with expect_invalid_xcat_resp_data():
for attr in attr_list:
for data in res_data:
if attr in data[0]:
outp[attr] = data[0].rpartition(':')[2].strip()
res_data.remove(data)
break
return outp
def format_exception_msg(exc_obj):
"""Return message string from nova exceptions and common exceptions."""
if isinstance(exc_obj, nova_exception.NovaException):
return exc_obj.format_message()
else:
return str(exc_obj)
class PathUtils(object):
def open(self, path, mode):
"""Wrapper on __builin__.open used to simplify unit testing."""
import __builtin__
return __builtin__.open(path, mode)
def _get_image_tmp_path(self):
image_tmp_path = os.path.normpath(CONF.zvm_image_tmp_path)
if not os.path.exists(image_tmp_path):
LOG.debug('Creating folder %s for image temp files' %
image_tmp_path)
os.makedirs(image_tmp_path)
return image_tmp_path
def get_bundle_tmp_path(self, tmp_file_fn):
bundle_tmp_path = os.path.join(self._get_image_tmp_path(), "spawn_tmp",
tmp_file_fn)
if not os.path.exists(bundle_tmp_path):
LOG.debug('Creating folder %s for image bundle temp file' %
bundle_tmp_path)
os.makedirs(bundle_tmp_path)
return bundle_tmp_path
def get_img_path(self, bundle_file_path, image_name):
return os.path.join(bundle_file_path, image_name)
def _get_snapshot_path(self):
snapshot_folder = os.path.join(self._get_image_tmp_path(),
"snapshot_tmp")
if not os.path.exists(snapshot_folder):
LOG.debug("Creating the snapshot folder %s" % snapshot_folder)
os.makedirs(snapshot_folder)
return snapshot_folder
def _get_punch_path(self):
punch_folder = os.path.join(self._get_image_tmp_path(), "punch_tmp")
if not os.path.exists(punch_folder):
LOG.debug("Creating the punch folder %s" % punch_folder)
os.makedirs(punch_folder)
return punch_folder
def get_spawn_folder(self):
spawn_folder = os.path.join(self._get_image_tmp_path(), "spawn_tmp")
if not os.path.exists(spawn_folder):
LOG.debug("Creating the spawn folder %s" % spawn_folder)
os.makedirs(spawn_folder)
return spawn_folder
def make_time_stamp(self):
tmp_file_fn = time.strftime('%Y%m%d%H%M%S',
time.localtime(time.time()))
return tmp_file_fn
def get_snapshot_time_path(self):
snapshot_time_path = os.path.join(self._get_snapshot_path(),
self.make_time_stamp())
if not os.path.exists(snapshot_time_path):
LOG.debug('Creating folder %s for image bundle temp file' %
snapshot_time_path)
os.makedirs(snapshot_time_path)
return snapshot_time_path
def clean_temp_folder(self, tmp_file_fn):
if os.path.isdir(tmp_file_fn):
LOG.debug('Removing existing folder %s ', tmp_file_fn)
shutil.rmtree(tmp_file_fn)
def _get_instances_path(self):
return os.path.normpath(CONF.instances_path)
def get_instance_path(self, os_node, instance_name):
instance_folder = os.path.join(self._get_instances_path(), os_node,
instance_name)
if not os.path.exists(instance_folder):
LOG.debug("Creating the instance path %s" % instance_folder)
os.makedirs(instance_folder)
return instance_folder
def get_console_log_path(self, os_node, instance_name):
return os.path.join(self.get_instance_path(os_node, instance_name),
"console.log")
class NetworkUtils(object):
"""Utilities for z/VM network operator."""
def validate_ip_address(self, ip_address):
"""Check whether ip_address is valid."""
# TODO(Leon): check IP address format
pass
def validate_network_mask(self, mask):
"""Check whether mask is valid."""
# TODO(Leon): check network mask format
pass
def create_network_configuration_files(self, file_path, network_info,
base_vdev, os_type):
"""Generate network configuration files to instance."""
device_num = 0
cfg_files = []
cmd_strings = ''
udev_cfg_str = ''
dns_cfg_str = ''
route_cfg_str = ''
cmd_str = None
cfg_str = ''
# Red Hat
file_path_rhel = '/etc/sysconfig/network-scripts/'
# SuSE
file_path_sles = '/etc/sysconfig/network/'
file_name_route = file_path_sles + 'routes'
# Check the OS type
if (os_type == 'sles'):
file_path = file_path_sles
else:
file_path = file_path_rhel
file_name_dns = '/etc/resolv.conf'
for vif in network_info:
file_name = 'ifcfg-eth' + str(device_num)
network = vif['network']
(cfg_str, cmd_str, dns_str,
route_str) = self.generate_network_configration(network,
base_vdev, device_num, os_type)
LOG.debug('Network configure file content is: %s' % cfg_str)
target_net_conf_file_name = file_path + file_name
cfg_files.append((target_net_conf_file_name, cfg_str))
udev_cfg_str += self.generate_udev_configuration(device_num,
'0.0.' + str(base_vdev).zfill(4))
if cmd_str is not None:
cmd_strings += cmd_str
if len(dns_str) > 0:
dns_cfg_str += dns_str
if len(route_str) > 0:
route_cfg_str += route_str
base_vdev = str(hex(int(base_vdev, 16) + 3))[2:]
device_num += 1
if len(dns_cfg_str) > 0:
cfg_files.append((file_name_dns, dns_cfg_str))
if os_type == 'sles':
udev_file_name = '/etc/udev/rules.d/70-persistent-net.rules'
cfg_files.append((udev_file_name, udev_cfg_str))
if len(route_cfg_str) > 0:
cfg_files.append((file_name_route, route_cfg_str))
return cfg_files, cmd_strings
def generate_network_configration(self, network, vdev, device_num,
os_type):
"""Generate network configuration items."""
ip_v4 = netmask_v4 = gateway_v4 = broadcast_v4 = ''
subchannels = None
device = None
cidr_v4 = None
cmd_str = None
dns_str = ''
route_str = ''
subnets_v4 = [s for s in network['subnets'] if s['version'] == 4]
if len(subnets_v4[0]['ips']) > 0:
ip_v4 = subnets_v4[0]['ips'][0]['address']
if len(subnets_v4[0]['dns']) > 0:
for dns in subnets_v4[0]['dns']:
dns_str += 'nameserver ' + dns['address'] + '\n'
netmask_v4 = str(subnets_v4[0].as_netaddr().netmask)
gateway_v4 = subnets_v4[0]['gateway']['address'] or ''
broadcast_v4 = str(subnets_v4[0].as_netaddr().broadcast)
device = "eth" + str(device_num)
address_read = str(vdev).zfill(4)
address_write = str(hex(int(vdev, 16) + 1))[2:].zfill(4)
address_data = str(hex(int(vdev, 16) + 2))[2:].zfill(4)
subchannels = '0.0.%s' % address_read.lower()
subchannels += ',0.0.%s' % address_write.lower()
subchannels += ',0.0.%s' % address_data.lower()
cfg_str = 'DEVICE=\"' + device + '\"\n' + 'BOOTPROTO=\"static\"\n'
cfg_str += 'BROADCAST=\"' + broadcast_v4 + '\"\n'
cfg_str += 'GATEWAY=\"' + gateway_v4 + '\"\nIPADDR=\"' + ip_v4 + '\"\n'
cfg_str += 'NETMASK=\"' + netmask_v4 + '\"\n'
cfg_str += 'NETTYPE=\"qeth\"\nONBOOT=\"yes\"\n'
cfg_str += 'PORTNAME=\"PORT' + address_read + '\"\n'
cfg_str += 'OPTIONS=\"layer2=1\"\n'
cfg_str += 'SUBCHANNELS=\"' + subchannels + '\"\n'
if os_type == 'sles':
cidr_v4 = self._get_cidr_from_ip_netmask(ip_v4, netmask_v4)
cmd_str = 'qeth_configure -l 0.0.%s ' % address_read.lower()
cmd_str += '0.0.%(write)s 0.0.%(data)s 1\n' % {'write':
address_write.lower(), 'data': address_data.lower()}
cfg_str = "BOOTPROTO=\'static\'\nIPADDR=\'%s\'\n" % cidr_v4
cfg_str += "BROADCAST=\'%s\'\n" % broadcast_v4
cfg_str += "STARTMODE=\'onboot\'\n"
cfg_str += ("NAME=\'OSA Express Network card (%s)\'\n" %
address_read)
route_str += 'default %s - -\n' % gateway_v4
return cfg_str, cmd_str, dns_str, route_str
def generate_udev_configuration(self, device, dev_channel):
cfg_str = 'SUBSYSTEM==\"net\", ACTION==\"add\", DRIVERS==\"qeth\",'
cfg_str += ' KERNELS==\"%s\", ATTR{type}==\"1\",' % dev_channel
cfg_str += ' KERNEL==\"eth*\", NAME=\"eth%s\"\n' % device
return cfg_str
def _get_cidr_from_ip_netmask(self, ip, netmask):
netmask_fields = netmask.split('.')
bin_str = ''
for octet in netmask_fields:
bin_str += bin(int(octet))[2:].zfill(8)
mask = str(len(bin_str.rstrip('0')))
cidr_v4 = ip + '/' + mask
return cidr_v4

View File

@ -0,0 +1,801 @@
# Copyright 2013 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import re
import time
import nova.context
from nova.i18n import _
from nova.objects import block_device as block_device_obj
from nova.objects import instance as instance_obj
from nova import volume
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from nova_zvm.virt.zvm import const
from nova_zvm.virt.zvm import exception
from nova_zvm.virt.zvm import utils as zvmutils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class VolumeOperator(object):
"""Volume operator on IBM z/VM platform."""
def __init__(self):
self._svc_driver = SVCDriver()
def init_host(self, host_stats):
try:
self._svc_driver.init_host(host_stats)
except (exception.ZVMDriverError, exception.ZVMVolumeError) as err:
LOG.warning(_("Initialize zhcp failed. Reason: %s") %
err.format_message())
def attach_volume_to_instance(self, context, connection_info, instance,
mountpoint, is_active, rollback=True):
"""Attach a volume to an instance."""
if None in [connection_info, instance, is_active]:
errmsg = _("Missing required parameters.")
raise exception.ZVMDriverError(msg=errmsg)
LOG.debug("Attach a volume to an instance. conn_info: %(info)s; " +
"instance: %(name)s; mountpoint: %(point)s" %
{'info': connection_info, 'name': instance['name'],
'point': mountpoint})
if is_active:
self._svc_driver.attach_volume_active(context, connection_info,
instance, mountpoint,
rollback)
else:
self._svc_driver.attach_volume_inactive(context, connection_info,
instance, mountpoint,
rollback)
def detach_volume_from_instance(self, connection_info, instance,
mountpoint, is_active, rollback=True):
"""Detach a volume from an instance."""
if None in [connection_info, instance, is_active]:
errmsg = _("Missing required parameters.")
raise exception.ZVMDriverError(msg=errmsg)
LOG.debug("Detach a volume from an instance. conn_info: %(info)s; " +
"instance: %(name)s; mountpoint: %(point)s" %
{'info': connection_info, 'name': instance['name'],
'point': mountpoint})
if is_active:
self._svc_driver.detach_volume_active(connection_info, instance,
mountpoint, rollback)
else:
self._svc_driver.detach_volume_inactive(connection_info, instance,
mountpoint, rollback)
def get_volume_connector(self, instance):
if not instance:
errmsg = _("Instance must be provided.")
raise exception.ZVMDriverError(msg=errmsg)
return self._svc_driver.get_volume_connector(instance)
def has_persistent_volume(self, instance):
if not instance:
errmsg = _("Instance must be provided.")
raise exception.ZVMDriverError(msg=errmsg)
return self._svc_driver.has_persistent_volume(instance)
def extract_connection_info(self, context, connection_info):
return self._svc_driver._extract_connection_info(context,
connection_info)
def get_root_volume_connection_info(self, bdm, root_device):
for bd in bdm:
if zvmutils.is_volume_root(bd['mount_device'], root_device):
return bd['connection_info']
errmsg = _("Trying to extract volume connection info failed.")
raise exception.ZVMDriverError(msg=errmsg)
def volume_boot_init(self, instance, fcp):
self._svc_driver.volume_boot_init(instance, fcp)
def volume_boot_cleanup(self, instance, fcp):
self._svc_driver.volume_boot_cleanup(instance, fcp)
@contextlib.contextmanager
def wrap_internal_errors():
"""Wrap internal exceptions to ZVMVolumeError."""
try:
yield
except exception.ZVMBaseException:
raise
except Exception as err:
raise exception.ZVMVolumeError(msg=err)
class DriverAPI(object):
"""DriverAPI for implement volume_attach on IBM z/VM platform."""
def init_host(self, host_stats):
"""Initialize host environment."""
raise NotImplementedError
def get_volume_connector(self, instance):
"""Get volume connector for current driver."""
raise NotImplementedError
def attach_volume_active(self, context, connection_info, instance,
mountpoint, rollback):
"""Attach a volume to an running instance."""
raise NotImplementedError
def detach_volume_active(self, connection_info, instance, mountpoint,
rollback):
"""Detach a volume from an running instance."""
raise NotImplementedError
def attach_volume_inactive(self, context, connection_info, instance,
mountpoint, rollback):
"""Attach a volume to an shutdown instance."""
raise NotImplementedError
def detach_volume_inactive(self, connection_info, instance, mountpoint,
rollback):
"""Detach a volume from an shutdown instance."""
raise NotImplementedError
def has_persistent_volume(self, instance):
"""Decide if the specified instance has persistent volumes attached."""
raise NotImplementedError
class SVCDriver(DriverAPI):
"""SVC volume operator on IBM z/VM platform."""
def __init__(self):
self._xcat_url = zvmutils.XCATUrl()
self._path_utils = zvmutils.PathUtils()
self._host = CONF.zvm_host
self._pool_name = CONF.zvm_scsi_pool
self._fcp_pool = set()
self._instance_fcp_map = {}
self._is_instance_fcp_map_locked = False
self._volume_api = volume.API()
self._actions = {'attach_volume': 'addScsiVolume',
'detach_volume': 'removeScsiVolume',
'create_mountpoint': 'createfilesysnode',
'remove_mountpoint': 'removefilesysnode'}
self._RESERVE = 0
self._INCREASE = 1
self._DECREASE = 2
self._REMOVE = 3
def init_host(self, host_stats):
"""Initialize host environment."""
if not host_stats:
errmsg = _("Can not obtain host stats.")
raise exception.ZVMDriverError(msg=errmsg)
zhcp_fcp_list = CONF.zvm_zhcp_fcp_list
fcp_devices = self._expand_fcp_list(zhcp_fcp_list)
hcpnode = host_stats[0]['zhcp']['nodename']
for _fcp in fcp_devices:
with zvmutils.ignore_errors():
self._attach_device(hcpnode, _fcp)
with zvmutils.ignore_errors():
self._online_device(hcpnode, _fcp)
fcp_list = CONF.zvm_fcp_list
if (fcp_list is None):
errmsg = _("At least one fcp list should be given")
LOG.error(errmsg)
raise exception.ZVMVolumeError(msg=errmsg)
self._init_fcp_pool(fcp_list)
def _init_fcp_pool(self, fcp_list):
"""Map all instances and their fcp devices, and record all free fcps.
One instance should use only one fcp device so far.
"""
self._fcp_pool = self._expand_fcp_list(fcp_list)
self._instance_fcp_map = {}
# Any other functions should not modify _instance_fcp_map during
# FCP pool initialization
self._is_instance_fcp_map_locked = True
compute_host_bdms = self._get_host_volume_bdms()
for instance_bdms in compute_host_bdms:
instance_name = instance_bdms['instance']['name']
for _bdm in instance_bdms['instance_bdms']:
connection_info = self._build_connection_info(_bdm)
try:
_fcp = connection_info['data']['zvm_fcp']
if _fcp and _fcp in self._fcp_pool:
self._update_instance_fcp_map(instance_name, _fcp,
self._INCREASE)
if _fcp and _fcp not in self._fcp_pool:
errmsg = _("FCP device %(dev)s is not configured but "
"is used by %(inst_name)s.") % {'dev': _fcp,
'inst_name': instance_name}
LOG.warning(errmsg)
except (TypeError, KeyError):
pass
for _key in self._instance_fcp_map.keys():
fcp = self._instance_fcp_map.get(_key)['fcp']
self._fcp_pool.remove(fcp)
self._is_instance_fcp_map_locked = False
def _update_instance_fcp_map_if_unlocked(self, instance_name, fcp, action):
while self._is_instance_fcp_map_locked:
time.sleep(1)
self._update_instance_fcp_map(instance_name, fcp, action)
def _update_instance_fcp_map(self, instance_name, fcp, action):
fcp = fcp.lower()
if instance_name in self._instance_fcp_map:
# One instance should use only one fcp device so far
current_fcp = self._instance_fcp_map.get(instance_name)['fcp']
if fcp != current_fcp:
errmsg = _("Instance %(ins_name)s has multiple FCP devices "
"attached! FCP1: %(fcp1)s, FCP2: %(fcp2)s"
) % {'ins_name': instance_name, 'fcp1': fcp,
'fcp2': current_fcp}
LOG.warning(errmsg)
return
if action == self._RESERVE:
if instance_name in self._instance_fcp_map:
count = self._instance_fcp_map[instance_name]['count']
if count > 0:
errmsg = _("Try to reserve a fcp device which already "
"has volumes attached on: %(ins_name)s:%(fcp)s"
) % {'ins_name': instance_name, 'fcp': fcp}
LOG.warning(errmsg)
else:
new_item = {instance_name: {'fcp': fcp, 'count': 0}}
self._instance_fcp_map.update(new_item)
elif action == self._INCREASE:
if instance_name in self._instance_fcp_map:
count = self._instance_fcp_map[instance_name]['count']
new_item = {instance_name: {'fcp': fcp, 'count': count + 1}}
self._instance_fcp_map.update(new_item)
else:
new_item = {instance_name: {'fcp': fcp, 'count': 1}}
self._instance_fcp_map.update(new_item)
elif action == self._DECREASE:
if instance_name in self._instance_fcp_map:
count = self._instance_fcp_map[instance_name]['count']
if count > 0:
new_item = {instance_name: {'fcp': fcp,
'count': count - 1}}
self._instance_fcp_map.update(new_item)
else:
fcp = self._instance_fcp_map[instance_name]['fcp']
self._instance_fcp_map.pop(instance_name)
self._fcp_pool.add(fcp)
else:
errmsg = _("Try to decrease an inexistent map item: "
"%(ins_name)s:%(fcp)s"
) % {'ins_name': instance_name, 'fcp': fcp}
LOG.warning(errmsg)
elif action == self._REMOVE:
if instance_name in self._instance_fcp_map:
count = self._instance_fcp_map[instance_name]['count']
if count > 0:
errmsg = _("Try to remove a map item while some volumes "
"are still attached on: %(ins_name)s:%(fcp)s"
) % {'ins_name': instance_name, 'fcp': fcp}
LOG.warning(errmsg)
else:
fcp = self._instance_fcp_map[instance_name]['fcp']
self._instance_fcp_map.pop(instance_name)
self._fcp_pool.add(fcp)
else:
errmsg = _("Try to remove an inexistent map item: "
"%(ins_name)s:%(fcp)s"
) % {'ins_name': instance_name, 'fcp': fcp}
LOG.warning(errmsg)
else:
errmsg = _("Unrecognized option: %s") % action
LOG.warning(errmsg)
def _get_host_volume_bdms(self):
"""Return all block device mappings on a compute host."""
compute_host_bdms = []
instances = self._get_all_instances()
for instance in instances:
instance_bdms = self._get_instance_bdms(instance)
compute_host_bdms.append(dict(instance=instance,
instance_bdms=instance_bdms))
return compute_host_bdms
def _get_all_instances(self):
context = nova.context.get_admin_context()
return instance_obj.InstanceList.get_by_host(context, self._host)
def _get_instance_bdms(self, instance):
context = nova.context.get_admin_context()
instance_bdms = [bdm for bdm in
(block_device_obj.BlockDeviceMappingList.
get_by_instance_uuid(context, instance['uuid']))
if bdm.is_volume]
return instance_bdms
def has_persistent_volume(self, instance):
return bool(self._get_instance_bdms(instance))
def _build_connection_info(self, bdm):
try:
connection_info = jsonutils.loads(bdm['connection_info'])
return connection_info
except (TypeError, KeyError, ValueError):
return None
def get_volume_connector(self, instance):
empty_connector = {'zvm_fcp': None, 'wwpns': [], 'host': ''}
try:
fcp = self._instance_fcp_map.get(instance['name'])['fcp']
except Exception:
fcp = None
if not fcp:
fcp = self._get_fcp_from_pool()
if fcp:
self._update_instance_fcp_map_if_unlocked(instance['name'],
fcp, self._RESERVE)
if not fcp:
errmsg = _("No available FCP device found.")
LOG.warning(errmsg)
return empty_connector
fcp = fcp.lower()
wwpn = self._get_wwpn(fcp)
if not wwpn:
errmsg = _("FCP device %s has no available WWPN.") % fcp
LOG.warning(errmsg)
return empty_connector
wwpn = wwpn.lower()
return {'zvm_fcp': fcp, 'wwpns': [wwpn], 'host': CONF.zvm_host}
def _get_wwpn(self, fcp):
states = ['active', 'free']
for _state in states:
fcps_info = self._list_fcp_details(_state)
if not fcps_info:
continue
wwpn = self._extract_wwpn_from_fcp_info(fcp, fcps_info)
if wwpn:
return wwpn
def _list_fcp_details(self, state):
fields = '&field=--fcpdevices&field=' + state + '&field=details'
rsp = self._xcat_rinv(fields)
try:
fcp_details = rsp['info'][0][0].splitlines()
return fcp_details
except (TypeError, KeyError):
return None
def _extract_wwpn_from_fcp_info(self, fcp, fcps_info):
"""The FCP infomation would look like this:
host: FCP device number: xxxx
host: Status: Active
host: NPIV world wide port number: xxxxxxxx
host: Channel path ID: xx
host: Physical world wide port number: xxxxxxxx
......
host: FCP device number: xxxx
host: Status: Active
host: NPIV world wide port number: xxxxxxxx
host: Channel path ID: xx
host: Physical world wide port number: xxxxxxxx
"""
lines_per_item = 5
num_fcps = len(fcps_info) / lines_per_item
fcp = fcp.upper()
for _cur in range(0, num_fcps):
# Find target FCP device
if fcp not in fcps_info[_cur * lines_per_item]:
continue
# Try to get NPIV WWPN first
wwpn_info = fcps_info[(_cur + 1) * lines_per_item - 3]
wwpn = self._get_wwpn_from_line(wwpn_info)
if not wwpn:
# Get physical WWPN if NPIV WWPN is none
wwpn_info = fcps_info[(_cur + 1) * lines_per_item - 1]
wwpn = self._get_wwpn_from_line(wwpn_info)
return wwpn
def _get_wwpn_from_line(self, info_line):
wwpn = info_line.split(':')[-1].strip()
if wwpn and wwpn.upper() != 'NONE':
return wwpn
else:
return None
def _get_fcp_from_pool(self):
if self._fcp_pool:
return self._fcp_pool.pop()
self._init_fcp_pool(CONF.zvm_fcp_list)
if self._fcp_pool:
return self._fcp_pool.pop()
def _extract_connection_info(self, context, connection_info):
with wrap_internal_errors():
LOG.debug("Extract connection_info: %s" % connection_info)
lun = connection_info['data']['target_lun']
lun = "%04x000000000000" % int(lun)
wwpn = connection_info['data']['target_wwn']
size = '0G'
# There is no context in detach case
if context:
volume_id = connection_info['data']['volume_id']
volume = self._get_volume_by_id(context, volume_id)
size = str(volume['size']) + 'G'
fcp = connection_info['data']['zvm_fcp']
return (lun.lower(), self._format_wwpn(wwpn), size, fcp.lower())
def _format_wwpn(self, wwpn):
if isinstance(wwpn, basestring):
return wwpn.lower()
else:
new_wwpn = ';'.join(wwpn)
return new_wwpn.lower()
def _get_volume_by_id(self, context, volume_id):
volume = self._volume_api.get(context, volume_id)
return volume
def attach_volume_active(self, context, connection_info, instance,
mountpoint, rollback=True):
"""Attach a volume to an running instance."""
(lun, wwpn, size, fcp) = self._extract_connection_info(context,
connection_info)
try:
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._INCREASE)
self._add_zfcp_to_pool(fcp, wwpn, lun, size)
self._add_zfcp(instance, fcp, wwpn, lun, size)
if mountpoint:
self._create_mountpoint(instance, fcp, wwpn, lun, mountpoint)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError,
exception.ZVMVolumeError):
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._DECREASE)
do_detach = not self._is_fcp_in_use(instance, fcp)
if rollback:
with zvmutils.ignore_errors():
self._remove_mountpoint(instance, mountpoint)
with zvmutils.ignore_errors():
self._remove_zfcp(instance, fcp, wwpn, lun)
with zvmutils.ignore_errors():
self._remove_zfcp_from_pool(wwpn, lun)
with zvmutils.ignore_errors():
if do_detach:
self._detach_device(instance['name'], fcp)
raise
def detach_volume_active(self, connection_info, instance, mountpoint,
rollback=True):
"""Detach a volume from an running instance."""
(lun, wwpn, size, fcp) = self._extract_connection_info(None,
connection_info)
try:
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._DECREASE)
do_detach = not self._is_fcp_in_use(instance, fcp)
if mountpoint:
self._remove_mountpoint(instance, mountpoint)
self._remove_zfcp(instance, fcp, wwpn, lun)
self._remove_zfcp_from_pool(wwpn, lun)
if do_detach:
self._detach_device(instance['name'], fcp)
self._update_instance_fcp_map_if_unlocked(instance['name'],
fcp, self._REMOVE)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError,
exception.ZVMVolumeError):
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._INCREASE)
if rollback:
with zvmutils.ignore_errors():
self._add_zfcp_to_pool(fcp, wwpn, lun, size)
with zvmutils.ignore_errors():
self._add_zfcp(instance, fcp, wwpn, lun, size)
if mountpoint:
with zvmutils.ignore_errors():
self._create_mountpoint(instance, fcp, wwpn, lun,
mountpoint)
raise
def attach_volume_inactive(self, context, connection_info, instance,
mountpoint, rollback=True):
"""Attach a volume to an shutdown instance."""
(lun, wwpn, size, fcp) = self._extract_connection_info(context,
connection_info)
try:
do_attach = not self._is_fcp_in_use(instance, fcp)
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._INCREASE)
self._add_zfcp_to_pool(fcp, wwpn, lun, size)
self._allocate_zfcp(instance, fcp, size, wwpn, lun)
self._notice_attach(instance, fcp, wwpn, lun, mountpoint)
if do_attach:
self._attach_device(instance['name'], fcp)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError,
exception.ZVMVolumeError):
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._DECREASE)
do_detach = not self._is_fcp_in_use(instance, fcp)
if rollback:
with zvmutils.ignore_errors():
self._notice_detach(instance, fcp, wwpn, lun, mountpoint)
with zvmutils.ignore_errors():
self._remove_zfcp_from_pool(wwpn, lun)
with zvmutils.ignore_errors():
if do_detach:
self._detach_device(instance['name'], fcp)
self._update_instance_fcp_map_if_unlocked(
instance['name'], fcp, self._REMOVE)
raise
def detach_volume_inactive(self, connection_info, instance, mountpoint,
rollback=True):
"""Detach a volume from an shutdown instance."""
(lun, wwpn, size, fcp) = self._extract_connection_info(None,
connection_info)
try:
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._DECREASE)
do_detach = not self._is_fcp_in_use(instance, fcp)
self._remove_zfcp(instance, fcp, wwpn, lun)
self._remove_zfcp_from_pool(wwpn, lun)
self._notice_detach(instance, fcp, wwpn, lun, mountpoint)
if do_detach:
self._detach_device(instance['name'], fcp)
self._update_instance_fcp_map_if_unlocked(instance['name'],
fcp, self._REMOVE)
except (exception.ZVMXCATRequestFailed,
exception.ZVMInvalidXCATResponseDataError,
exception.ZVMXCATInternalError,
exception.ZVMVolumeError):
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._INCREASE)
if rollback:
with zvmutils.ignore_errors():
self._attach_device(instance['name'], fcp)
with zvmutils.ignore_errors():
self._notice_attach(instance, fcp, wwpn, lun, mountpoint)
with zvmutils.ignore_errors():
self._add_zfcp_to_pool(fcp, wwpn, lun, size)
with zvmutils.ignore_errors():
self._allocate_zfcp(instance, fcp, size, wwpn, lun)
raise
def volume_boot_init(self, instance, fcp):
self._update_instance_fcp_map_if_unlocked(instance['name'], fcp,
self._INCREASE)
self._attach_device(instance['name'], fcp)
def volume_boot_cleanup(self, instance, fcp):
self._update_instance_fcp_map_if_unlocked(instance['name'],
fcp, self._DECREASE)
self._detach_device(instance['name'], fcp)
def _expand_fcp_list(self, fcp_list):
"""Expand fcp list string into a python list object which contains
each fcp devices in the list string. A fcp list is composed of fcp
device addresses, range indicator '-', and split indicator ';'.
For example, if fcp_list is
"0011-0013;0015;0017-0018", expand_fcp_list(fcp_list) will return
[0011, 0012, 0013, 0015, 0017, 0018].
"""
LOG.debug("Expand FCP list %s" % fcp_list)
if not fcp_list:
return set()
range_pattern = '[0-9a-fA-F]{1,4}(-[0-9a-fA-F]{1,4})?'
match_pattern = "^(%(range)s)(;%(range)s)*$" % {'range': range_pattern}
if not re.match(match_pattern, fcp_list):
errmsg = _("Invalid FCP address %s") % fcp_list
raise exception.ZVMDriverError(msg=errmsg)
fcp_devices = set()
for _range in fcp_list.split(';'):
if '-' not in _range:
# single device
fcp_addr = int(_range, 16)
fcp_devices.add("%04x" % fcp_addr)
else:
# a range of address
(_min, _max) = _range.split('-')
_min = int(_min, 16)
_max = int(_max, 16)
for fcp_addr in range(_min, _max + 1):
fcp_devices.add("%04x" % fcp_addr)
# remove duplicate entries
return fcp_devices
def _attach_device(self, node, addr, mode='0'):
"""Attach a device to a node."""
body = [' '.join(['--dedicatedevice', addr, addr, mode])]
self._xcat_chvm(node, body)
def _detach_device(self, node, vdev):
"""Detach a device from a node."""
body = [' '.join(['--undedicatedevice', vdev])]
self._xcat_chvm(node, body)
def _online_device(self, node, dev):
"""After attaching a device to a node, the device should be made
online before it being in use.
"""
body = ["command=cio_ignore -r %s" % dev]
self._xcat_xdsh(node, body)
body = ["command=chccwdev -e %s" % dev]
self._xcat_xdsh(node, body)
def _is_fcp_in_use(self, instance, fcp):
if instance['name'] in self._instance_fcp_map:
count = self._instance_fcp_map.get(instance['name'])['count']
if count > 0:
return True
return False
def _notice_attach(self, instance, fcp, wwpn, lun, mountpoint):
# Create and send volume file
action = self._actions['attach_volume']
parms = self._get_volume_parms(action, fcp, wwpn, lun)
self._send_notice(instance, parms)
# Create and send mount point file
action = self._actions['create_mountpoint']
parms = self._get_mountpoint_parms(action, fcp, wwpn, lun, mountpoint)
self._send_notice(instance, parms)
def _notice_detach(self, instance, fcp, wwpn, lun, mountpoint):
# Create and send volume file
action = self._actions['detach_volume']
parms = self._get_volume_parms(action, fcp, wwpn, lun)
self._send_notice(instance, parms)
# Create and send mount point file
action = self._actions['remove_mountpoint']
parms = self._get_mountpoint_parms(action, fcp, wwpn, lun, mountpoint)
self._send_notice(instance, parms)
def _get_volume_parms(self, action, fcp, wwpn, lun):
action = "action=%s" % action
fcp = "fcpAddr=%s" % fcp
# Replace the ; in wwpn to be , in invoke script file since shell will
# treat ; as a new line
wwpn = wwpn.replace(';', ',')
wwpn = "wwpn=%s" % wwpn
lun = "lun=%s" % lun
parmline = ' '.join([action, fcp, wwpn, lun])
return parmline
def _get_mountpoint_parms(self, action, fcp, wwpn, lun, mountpoint):
action_parm = "action=%s" % action
mountpoint = "tgtFile=%s" % mountpoint
wwpn = wwpn.replace(';', ',')
if action == self._actions['create_mountpoint']:
path = self._get_zfcp_path_pattern()
srcdev = path % {'fcp': fcp, 'wwpn': wwpn, 'lun': lun}
srcfile = "srcFile=%s" % srcdev
parmline = ' '.join([action_parm, mountpoint, srcfile])
else:
parmline = ' '.join([action_parm, mountpoint])
return parmline
def _send_notice(self, instance, parms):
zvmutils.aemod_handler(instance['name'], const.DISK_FUNC_NAME, parms)
def _add_zfcp_to_pool(self, fcp, wwpn, lun, size):
body = [' '.join(['--addzfcp2pool', self._pool_name, 'free', wwpn,
lun, size, fcp])]
self._xcat_chhy(body)
def _remove_zfcp_from_pool(self, wwpn, lun):
body = [' '.join(['--removezfcpfrompool', CONF.zvm_scsi_pool, lun,
wwpn])]
self._xcat_chhy(body)
def _add_zfcp(self, instance, fcp, wwpn, lun, size):
body = [' '.join(['--addzfcp', CONF.zvm_scsi_pool, fcp, str(0), size,
str(0), wwpn, lun])]
self._xcat_chvm(instance['name'], body)
def _remove_zfcp(self, instance, fcp, wwpn, lun):
body = [' '.join(['--removezfcp', fcp, wwpn, lun, '1'])]
self._xcat_chvm(instance['name'], body)
def _create_mountpoint(self, instance, fcp, wwpn, lun, mountpoint):
path = self._get_zfcp_path_pattern()
srcdev = path % {'fcp': fcp, 'wwpn': wwpn, 'lun': lun}
body = [" ".join(['--createfilesysnode', srcdev, mountpoint])]
self._xcat_chvm(instance['name'], body)
def _remove_mountpoint(self, instance, mountpoint):
body = [' '.join(['--removefilesysnode', mountpoint])]
self._xcat_chvm(instance['name'], body)
def _allocate_zfcp(self, instance, fcp, size, wwpn, lun):
body = [" ".join(['--reservezfcp', CONF.zvm_scsi_pool, 'used',
instance['name'], fcp, size, wwpn, lun])]
self._xcat_chhy(body)
def _xcat_chvm(self, node, body):
url = self._xcat_url.chvm('/' + node)
zvmutils.xcat_request('PUT', url, body)
def _xcat_chhy(self, body):
url = self._xcat_url.chhv('/' + self._host)
zvmutils.xcat_request('PUT', url, body)
def _xcat_xdsh(self, node, body):
url = self._xcat_url.xdsh('/' + node)
zvmutils.xcat_request('PUT', url, body)
def _xcat_rinv(self, fields):
url = self._xcat_url.rinv('/' + self._host, fields)
return zvmutils.xcat_request('GET', url)
def _get_zfcp_path_pattern(self):
return '/dev/disk/by-path/ccw-0.0.%(fcp)s-zfcp-0x%(wwpn)s:0x%(lun)s'

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
pbr>=0.5.21,<1.0
oslo.config>=1.9.0 # Apache-2.0
oslo.log>=0.1.0 # Apache-2.0
oslo.serialization>=1.0.0 # Apache-2.0
oslo.utils>=1.0.0 # Apache-2.0

44
setup.cfg Normal file
View File

@ -0,0 +1,44 @@
[metadata]
name = nova-zvm-virt-driver
version = 2015.1
summary = zVM driver for OpenStack Nova.
description-file = README.rst
author = IBM
home-page = https://wiki.openstack.org/wiki/Nova-z/VM
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
[files]
packages =
nova_zvm
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
all_files = 1
[upload_sphinx]
upload-dir = doc/build/html
[compile_catalog]
directory = nova_zvm/locale
domain = nova-virt-zvm-driver
[update_catalog]
domain = nova-virt-zvm-drvier
output_dir = nova_zvm/locale
input_file = nova_zvm/locale/nova-zvm.pot
[extract_messages]
keywords = _ gettext ngettext l_ lazy_gettext
mapping_file = babel.cfg
output_file = nova_zvm/locale/nova-zvm.pot

9
setup.py Normal file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
import setuptools
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

13
test-requirements.txt Normal file
View File

@ -0,0 +1,13 @@
hacking>=0.9.2,<0.10
coverage>=3.6
discover
fixtures>=0.3.14
python-subunit
sphinx>=1.1.2
oslosphinx
oslotest>=1.2.0
testrepository>=0.0.17
testscenarios>=0.4,<0.5
testtools>=0.9.32
mock>=1.0
mox>=0.5.3

32
tox.ini Normal file
View File

@ -0,0 +1,32 @@
[tox]
minversion = 1.6
envlist = py27,pep8
skipsdist = True
[testenv]
usedevelop = True
install_command = pip install -U {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
-egit+https://github.com/openstack/nova#egg=nova
commands = python setup.py testr --slowest --testr-args='{posargs}'
[testenv:pep8]
deps = -r{toxinidir}/test-requirements.txt
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
[flake8]
ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E251,H405
exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools
[hacking]
import_exceptions = nova.i18n