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:
parent
50f62fc6f9
commit
588d68108a
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal 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
7
.testr.conf
Normal 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
5
README.rst
Normal 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
285
doc/source/conf.py
Normal 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
21
doc/source/index.rst
Normal 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
0
nova_zvm/__init__.py
Normal file
0
nova_zvm/tests/__init__.py
Normal file
0
nova_zvm/tests/__init__.py
Normal file
3227
nova_zvm/tests/test_zvm.py
Normal file
3227
nova_zvm/tests/test_zvm.py
Normal file
File diff suppressed because it is too large
Load Diff
0
nova_zvm/virt/__init__.py
Normal file
0
nova_zvm/virt/__init__.py
Normal file
31
nova_zvm/virt/zvm/__init__.py
Normal file
31
nova_zvm/virt/zvm/__init__.py
Normal 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
|
64
nova_zvm/virt/zvm/configdrive.py
Normal file
64
nova_zvm/virt/zvm/configdrive.py
Normal 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()
|
69
nova_zvm/virt/zvm/const.py
Normal file
69
nova_zvm/virt/zvm/const.py
Normal 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
1997
nova_zvm/virt/zvm/driver.py
Normal file
File diff suppressed because it is too large
Load Diff
80
nova_zvm/virt/zvm/exception.py
Normal file
80
nova_zvm/virt/zvm/exception.py
Normal 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')
|
822
nova_zvm/virt/zvm/imageop.py
Normal file
822
nova_zvm/virt/zvm/imageop.py
Normal 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
|
693
nova_zvm/virt/zvm/instance.py
Normal file
693
nova_zvm/virt/zvm/instance.py
Normal 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
|
189
nova_zvm/virt/zvm/networkop.py
Normal file
189
nova_zvm/virt/zvm/networkop.py
Normal 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
943
nova_zvm/virt/zvm/utils.py
Normal 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
|
801
nova_zvm/virt/zvm/volumeop.py
Normal file
801
nova_zvm/virt/zvm/volumeop.py
Normal 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
5
requirements.txt
Normal 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
44
setup.cfg
Normal 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
9
setup.py
Normal file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
import setuptools
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr'],
|
||||
pbr=True)
|
13
test-requirements.txt
Normal file
13
test-requirements.txt
Normal 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
32
tox.ini
Normal 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user