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
|
@ -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/
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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`
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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()
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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')
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
setup_requires=['pbr'],
|
||||||
|
pbr=True)
|
|
@ -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
|
|
@ -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…
Reference in New Issue