Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: I6371190302f621f04138927539f3fa9a7197df02
This commit is contained in:
parent
13b0affae5
commit
79452f470c
|
@ -1,7 +0,0 @@
|
||||||
[run]
|
|
||||||
branch = True
|
|
||||||
source = os_brick
|
|
||||||
omit = os_brick/tests/*
|
|
||||||
|
|
||||||
[report]
|
|
||||||
ignore_errors = True
|
|
|
@ -1,65 +0,0 @@
|
||||||
*/.*
|
|
||||||
!.coveragerc
|
|
||||||
!.gitignore
|
|
||||||
!.mailmap
|
|
||||||
!.testr.conf
|
|
||||||
|
|
||||||
*.py[cod]
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Packages
|
|
||||||
*.egg
|
|
||||||
*.egg-info
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
eggs
|
|
||||||
parts
|
|
||||||
bin
|
|
||||||
var
|
|
||||||
sdist
|
|
||||||
develop-eggs
|
|
||||||
.installed.cfg
|
|
||||||
lib
|
|
||||||
lib64
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
.coverage
|
|
||||||
.tox
|
|
||||||
nosetests.xml
|
|
||||||
.testrepository
|
|
||||||
.venv
|
|
||||||
tools/lintstack.head.py
|
|
||||||
tools/pylint_exceptions
|
|
||||||
cover
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
|
|
||||||
# Mr Developer
|
|
||||||
.mr.developer.cfg
|
|
||||||
.project
|
|
||||||
.pydevproject
|
|
||||||
|
|
||||||
# Complexity
|
|
||||||
output/*.html
|
|
||||||
output/*/index.html
|
|
||||||
|
|
||||||
# Sphinx
|
|
||||||
doc/build
|
|
||||||
|
|
||||||
# Release notes
|
|
||||||
releasenotes/build/
|
|
||||||
|
|
||||||
# pbr generates these
|
|
||||||
AUTHORS
|
|
||||||
ChangeLog
|
|
||||||
|
|
||||||
# Editors
|
|
||||||
*~
|
|
||||||
.*.swp
|
|
||||||
.*sw?
|
|
|
@ -1,4 +0,0 @@
|
||||||
[gerrit]
|
|
||||||
host=review.openstack.org
|
|
||||||
port=29418
|
|
||||||
project=openstack/os-brick.git
|
|
3
.mailmap
3
.mailmap
|
@ -1,3 +0,0 @@
|
||||||
# Format is:
|
|
||||||
# <preferred e-mail> <other e-mail 1>
|
|
||||||
# <preferred e-mail> <other e-mail 2>
|
|
|
@ -1,7 +0,0 @@
|
||||||
[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
|
|
|
@ -1,16 +0,0 @@
|
||||||
If you would like to contribute to the development of OpenStack,
|
|
||||||
you must follow the steps in this page:
|
|
||||||
|
|
||||||
http://docs.openstack.org/infra/manual/developers.html
|
|
||||||
|
|
||||||
Once those steps have been completed, changes to OpenStack
|
|
||||||
should be submitted for review via the Gerrit tool, following
|
|
||||||
the workflow documented at:
|
|
||||||
|
|
||||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
|
||||||
|
|
||||||
Pull requests submitted through GitHub will be ignored.
|
|
||||||
|
|
||||||
Bugs should be filed on Launchpad, not GitHub:
|
|
||||||
|
|
||||||
https://bugs.launchpad.net/os-brick
|
|
|
@ -1,4 +0,0 @@
|
||||||
brick Style Commandments
|
|
||||||
===============================================
|
|
||||||
|
|
||||||
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
|
|
176
LICENSE
176
LICENSE
|
@ -1,176 +0,0 @@
|
||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
This project is no longer maintained.
|
||||||
|
|
||||||
|
The contents of this repository are still available in the Git
|
||||||
|
source code management system. To see the contents of this
|
||||||
|
repository before it reached its end of life, please check out the
|
||||||
|
previous commit with "git checkout HEAD^1".
|
||||||
|
|
||||||
|
For ongoing work on maintaining OpenStack packages in the Debian
|
||||||
|
distribution, please see the Debian OpenStack packaging team at
|
||||||
|
https://wiki.debian.org/OpenStack/.
|
||||||
|
|
||||||
|
For any further questions, please email
|
||||||
|
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||||
|
Freenode.
|
48
README.rst
48
README.rst
|
@ -1,48 +0,0 @@
|
||||||
========================
|
|
||||||
Team and repository tags
|
|
||||||
========================
|
|
||||||
|
|
||||||
.. image:: http://governance.openstack.org/badges/os-brick.svg
|
|
||||||
:target: http://governance.openstack.org/reference/tags/index.html
|
|
||||||
|
|
||||||
.. Change things from this point on
|
|
||||||
|
|
||||||
===============================
|
|
||||||
brick
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/os-brick.svg
|
|
||||||
:target: https://pypi.python.org/pypi/os-brick/
|
|
||||||
:alt: Latest Version
|
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/dm/os-brick.svg
|
|
||||||
:target: https://pypi.python.org/pypi/os-brick/
|
|
||||||
:alt: Downloads
|
|
||||||
|
|
||||||
OpenStack Cinder brick library for managing local volume attaches
|
|
||||||
|
|
||||||
|
|
||||||
Features
|
|
||||||
--------
|
|
||||||
|
|
||||||
* Discovery of volumes being attached to a host for many transport protocols.
|
|
||||||
* Removal of volumes from a host.
|
|
||||||
|
|
||||||
Hacking
|
|
||||||
-------
|
|
||||||
|
|
||||||
Hacking on brick requires python-gdbm (for Debian derived distributions),
|
|
||||||
Python 2.7 and Python 3.4. A recent tox is required, as is a recent virtualenv
|
|
||||||
(13.1.0 or newer).
|
|
||||||
|
|
||||||
If "tox -e py34" fails with the error "db type could not be determined", remove
|
|
||||||
the .testrepository/ directory and then run "tox -e py34".
|
|
||||||
|
|
||||||
For any other information, refer to the developer documents:
|
|
||||||
http://docs.openstack.org/developer/os-brick/index.html
|
|
||||||
OR refer to the parent project, Cinder:
|
|
||||||
http://docs.openstack.org/developer/cinder/
|
|
||||||
|
|
||||||
* License: Apache License, Version 2.0
|
|
||||||
* Source: http://git.openstack.org/cgit/openstack/os-brick
|
|
||||||
* Bugs: http://bugs.launchpad.net/os-brick
|
|
15
bindep.txt
15
bindep.txt
|
@ -1,15 +0,0 @@
|
||||||
# This is a cross-platform list tracking distribution packages needed for
|
|
||||||
# install and tests
|
|
||||||
# see http://docs.openstack.org/infra/bindep/ for additional information.
|
|
||||||
|
|
||||||
curl
|
|
||||||
multipath-utils [platform:dpkg rpm]
|
|
||||||
sg3-utils [platform:dpkg]
|
|
||||||
sg3_utils [platform:rpm]
|
|
||||||
libxml2-devel [platform:rpm]
|
|
||||||
libxml2-dev [platform:dpkg]
|
|
||||||
libxslt-devel [platform:rpm]
|
|
||||||
libxslt1-dev [platform:dpkg]
|
|
||||||
libssl-dev [platform:dpkg]
|
|
||||||
openssl-devel [platform:rpm !platform:suse]
|
|
||||||
libopenssl-devel [platform:suse !platform:rpm]
|
|
|
@ -1,10 +0,0 @@
|
||||||
API Documentation
|
|
||||||
=================
|
|
||||||
|
|
||||||
The **os-brick** package provides the ability to collect host initiator
|
|
||||||
information as well as discovery volumes and removal of volumes from a host.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
os_brick/index
|
|
|
@ -1,14 +0,0 @@
|
||||||
:mod:`exception` -- Exceptions
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: os_brick.exception
|
|
||||||
:synopsis: Exceptions generated by os-brick
|
|
||||||
|
|
||||||
.. autoclass:: os_brick.exception.BrickException
|
|
||||||
.. autoclass:: os_brick.exception.NotFound
|
|
||||||
.. autoclass:: os_brick.exception.Invalid
|
|
||||||
.. autoclass:: os_brick.exception.InvalidParameterValue
|
|
||||||
.. autoclass:: os_brick.exception.NoFibreChannelHostsFound
|
|
||||||
.. autoclass:: os_brick.exception.NoFibreChannelVolumeDeviceFound
|
|
||||||
.. autoclass:: os_brick.exception.VolumeDeviceNotFound
|
|
||||||
.. autoclass:: os_brick.exception.ProtocolNotSupported
|
|
|
@ -1,14 +0,0 @@
|
||||||
:mod:`os_brick` -- OpenStack Brick library
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
.. automodule:: os_brick
|
|
||||||
:synopsis: OpenStack Brick library
|
|
||||||
|
|
||||||
|
|
||||||
Sub-modules:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
initiator/index
|
|
||||||
exception
|
|
|
@ -1,34 +0,0 @@
|
||||||
:mod:`connector` -- Connector
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: os_brick.initiator.connector
|
|
||||||
:synopsis: Connector module for os-brick
|
|
||||||
|
|
||||||
.. autoclass:: os_brick.initiator.connector.InitiatorConnector
|
|
||||||
|
|
||||||
.. automethod:: factory
|
|
||||||
|
|
||||||
.. autoclass:: os_brick.initiator.connector.ISCSIConnector
|
|
||||||
|
|
||||||
.. automethod:: connect_volume
|
|
||||||
.. automethod:: disconnect_volume
|
|
||||||
|
|
||||||
.. autoclass:: os_brick.initiator.connector.FibreChannelConnector
|
|
||||||
|
|
||||||
.. automethod:: connect_volume
|
|
||||||
.. automethod:: disconnect_volume
|
|
||||||
|
|
||||||
.. autoclass:: os_brick.initiator.connector.AoEConnector
|
|
||||||
|
|
||||||
.. automethod:: connect_volume
|
|
||||||
.. automethod:: disconnect_volume
|
|
||||||
|
|
||||||
.. autoclass:: os_brick.initiator.connector.LocalConnector
|
|
||||||
|
|
||||||
.. automethod:: connect_volume
|
|
||||||
.. automethod:: disconnect_volume
|
|
||||||
|
|
||||||
.. autoclass:: os_brick.initiator.connector.HuaweiStorHyperConnector
|
|
||||||
|
|
||||||
.. automethod:: connect_volume
|
|
||||||
.. automethod:: disconnect_volume
|
|
|
@ -1,13 +0,0 @@
|
||||||
:mod:`initiator` -- Initiator
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: os_brick.initiator
|
|
||||||
:synopsis: Initiator module
|
|
||||||
|
|
||||||
|
|
||||||
Sub-modules:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
connector
|
|
|
@ -1 +0,0 @@
|
||||||
.. include:: ../../ChangeLog
|
|
|
@ -1,75 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# 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 sys
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
|
||||||
# -- General configuration ----------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
|
||||||
extensions = [
|
|
||||||
'sphinx.ext.autodoc',
|
|
||||||
'oslosphinx',
|
|
||||||
'reno.sphinxext',
|
|
||||||
]
|
|
||||||
|
|
||||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
|
||||||
# text edit cycles.
|
|
||||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'os-brick'
|
|
||||||
copyright = u'2015, OpenStack Foundation'
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# -- Options for HTML output --------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
|
||||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
|
||||||
# html_theme_path = ["."]
|
|
||||||
# html_theme = '_theme'
|
|
||||||
# html_static_path = ['static']
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = '%sdoc' % project
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author, documentclass
|
|
||||||
# [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index',
|
|
||||||
'%s.tex' % project,
|
|
||||||
u'%s Documentation' % project,
|
|
||||||
u'OpenStack Foundation', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
|
||||||
#intersphinx_mapping = {'http://docs.python.org/': None}
|
|
|
@ -1,4 +0,0 @@
|
||||||
============
|
|
||||||
Contributing
|
|
||||||
============
|
|
||||||
.. include:: ../../CONTRIBUTING.rst
|
|
|
@ -1,50 +0,0 @@
|
||||||
os-brick |release| Documenation
|
|
||||||
===============================
|
|
||||||
|
|
||||||
Overview
|
|
||||||
--------
|
|
||||||
**os-brick** is a Python package containing classes that help
|
|
||||||
with volume discovery and removal from a host.
|
|
||||||
|
|
||||||
:doc:`installation`
|
|
||||||
Instructions on how to get the distribution.
|
|
||||||
|
|
||||||
:doc:`tutorial`
|
|
||||||
Start here for a quick overview.
|
|
||||||
|
|
||||||
:doc:`api/index`
|
|
||||||
The complete API Documenation, organized by module.
|
|
||||||
|
|
||||||
|
|
||||||
Changes
|
|
||||||
-------
|
|
||||||
see the :doc:`changelog` for a full list of changes to **os-brick**.
|
|
||||||
|
|
||||||
About This Documentation
|
|
||||||
------------------------
|
|
||||||
This documentation is generated using the `Sphinx
|
|
||||||
<http://sphinx.pocoo.org/>`_ documentation generator. The source files
|
|
||||||
for the documentation are located in the *doc/* directory of the
|
|
||||||
**os-brick** distribution. To generate the docs locally run the
|
|
||||||
following command from the root directory of the **os-brick** source.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ python setup.py doc
|
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:hidden:
|
|
||||||
|
|
||||||
installation
|
|
||||||
tutorial
|
|
||||||
changelog
|
|
||||||
api/index
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
|
@ -1,18 +0,0 @@
|
||||||
============
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
At the command line::
|
|
||||||
|
|
||||||
$ pip install os-brick
|
|
||||||
|
|
||||||
Or, if you have virtualenvwrapper installed::
|
|
||||||
|
|
||||||
$ mkvirtualenv os-brick
|
|
||||||
$ pip install os-brick
|
|
||||||
|
|
||||||
Or, from source::
|
|
||||||
|
|
||||||
$ git clone https://github.com/openstack/os-brick
|
|
||||||
$ cd os-brick
|
|
||||||
$ python setup.py install
|
|
|
@ -1 +0,0 @@
|
||||||
.. include:: ../../README.rst
|
|
|
@ -1,36 +0,0 @@
|
||||||
Tutorial
|
|
||||||
========
|
|
||||||
|
|
||||||
This tutorial is intended as an introduction to working with
|
|
||||||
**os-brick**.
|
|
||||||
|
|
||||||
Prerequisites
|
|
||||||
-------------
|
|
||||||
Before we start, make sure that you have the **os-brick** distribution
|
|
||||||
:doc:`installed <installation>`. In the Python shell, the following
|
|
||||||
should run without raising an exception:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
>>> import os_brick
|
|
||||||
|
|
||||||
Fetch all of the initiator information from the host
|
|
||||||
----------------------------------------------------
|
|
||||||
An example of how to collect the initiator information that is needed
|
|
||||||
to export a volume to this host.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from os_brick.initiator import connector
|
|
||||||
|
|
||||||
# what helper do you want to use to get root access?
|
|
||||||
root_helper = "sudo"
|
|
||||||
# The ip address of the host you are running on
|
|
||||||
my_ip = "192.168.1.1"
|
|
||||||
# Do you want to support multipath connections?
|
|
||||||
multipath = True
|
|
||||||
# Do you want to enforce that multipath daemon is running?
|
|
||||||
enforce_multipath = False
|
|
||||||
initiator = connector.get_connector_properties(root_helper, my_ip,
|
|
||||||
multipath,
|
|
||||||
enforce_multipath)
|
|
|
@ -1,8 +0,0 @@
|
||||||
# os-brick command filters
|
|
||||||
# This file should be owned by (and only-writeable by) the root user
|
|
||||||
|
|
||||||
[Filters]
|
|
||||||
# privileged/__init__.py: priv_context.PrivContext(default)
|
|
||||||
# This line ties the superuser privs with the config files, context name,
|
|
||||||
# and (implicitly) the actual python code invoked.
|
|
||||||
privsep-rootwrap: RegExpFilter, privsep-helper, root, privsep-helper, --config-file, /etc/(?!\.\.).*, --privsep_context, os_brick.privileged.default, --privsep_sock_path, /tmp/.*
|
|
|
@ -1,18 +0,0 @@
|
||||||
# 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.
|
|
||||||
"""
|
|
||||||
:mod:`os_brick` -- OpenStack host based volume management
|
|
||||||
=========================================================
|
|
||||||
|
|
||||||
.. autmodule:: os_brick
|
|
||||||
:synopsis: OpenStack host based volume management.
|
|
||||||
"""
|
|
|
@ -1,127 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_brick.encryptors import nop
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import importutils
|
|
||||||
from oslo_utils import strutils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
LUKS = "luks"
|
|
||||||
PLAIN = "plain"
|
|
||||||
|
|
||||||
FORMAT_TO_FRONTEND_ENCRYPTOR_MAP = {
|
|
||||||
LUKS: 'os_brick.encryptors.luks.LuksEncryptor',
|
|
||||||
PLAIN: 'os_brick.encryptors.cryptsetup.CryptsetupEncryptor'
|
|
||||||
}
|
|
||||||
|
|
||||||
LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP = {
|
|
||||||
"nova.volume.encryptors.luks.LuksEncryptor": LUKS,
|
|
||||||
"nova.volume.encryptors.cryptsetup.CryptsetupEncryptor": PLAIN,
|
|
||||||
"nova.volume.encryptors.nop.NoopEncryptor": None,
|
|
||||||
"os_brick.encryptors.luks.LuksEncryptor": LUKS,
|
|
||||||
"os_brick.encryptors.cryptsetup.CryptsetupEncryptor": PLAIN,
|
|
||||||
"os_brick.encryptors.nop.NoopEncryptor": None,
|
|
||||||
"LuksEncryptor": LUKS,
|
|
||||||
"CryptsetupEncryptor": PLAIN,
|
|
||||||
"NoOpEncryptor": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_volume_encryptor(root_helper,
|
|
||||||
connection_info,
|
|
||||||
keymgr,
|
|
||||||
execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
"""Creates a VolumeEncryptor used to encrypt the specified volume.
|
|
||||||
|
|
||||||
:param: the connection information used to attach the volume
|
|
||||||
:returns VolumeEncryptor: the VolumeEncryptor for the volume
|
|
||||||
"""
|
|
||||||
encryptor = nop.NoOpEncryptor(root_helper=root_helper,
|
|
||||||
connection_info=connection_info,
|
|
||||||
keymgr=keymgr,
|
|
||||||
execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
location = kwargs.get('control_location', None)
|
|
||||||
if location and location.lower() == 'front-end': # case insensitive
|
|
||||||
provider = kwargs.get('provider')
|
|
||||||
|
|
||||||
# TODO(lyarwood): Remove the following in Queens and raise an
|
|
||||||
# ERROR if provider is not a key in SUPPORTED_ENCRYPTION_PROVIDERS.
|
|
||||||
# Until then continue to allow both the class name and path to be used.
|
|
||||||
if provider in LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP:
|
|
||||||
LOG.warning("Use of the in tree encryptor class %(provider)s"
|
|
||||||
" by directly referencing the implementation class"
|
|
||||||
" will be blocked in the Queens release of"
|
|
||||||
" os-brick.", {'provider': provider})
|
|
||||||
provider = LEGACY_PROVIDER_CLASS_TO_FORMAT_MAP[provider]
|
|
||||||
|
|
||||||
if provider in FORMAT_TO_FRONTEND_ENCRYPTOR_MAP:
|
|
||||||
provider = FORMAT_TO_FRONTEND_ENCRYPTOR_MAP[provider]
|
|
||||||
elif provider is None:
|
|
||||||
provider = "os_brick.encryptors.nop.NoOpEncryptor"
|
|
||||||
else:
|
|
||||||
LOG.warning("Use of the out of tree encryptor class "
|
|
||||||
"%(provider)s will be blocked with the Queens "
|
|
||||||
"release of os-brick.", {'provider': provider})
|
|
||||||
|
|
||||||
try:
|
|
||||||
encryptor = importutils.import_object(
|
|
||||||
provider,
|
|
||||||
root_helper,
|
|
||||||
connection_info,
|
|
||||||
keymgr,
|
|
||||||
execute,
|
|
||||||
**kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("Error instantiating %(provider)s: %(exception)s",
|
|
||||||
{'provider': provider, 'exception': e})
|
|
||||||
raise
|
|
||||||
|
|
||||||
msg = ("Using volume encryptor '%(encryptor)s' for connection: "
|
|
||||||
"%(connection_info)s" %
|
|
||||||
{'encryptor': encryptor, 'connection_info': connection_info})
|
|
||||||
LOG.debug(strutils.mask_password(msg))
|
|
||||||
|
|
||||||
return encryptor
|
|
||||||
|
|
||||||
|
|
||||||
def get_encryption_metadata(context, volume_api, volume_id, connection_info):
|
|
||||||
metadata = {}
|
|
||||||
if ('data' in connection_info and
|
|
||||||
connection_info['data'].get('encrypted', False)):
|
|
||||||
try:
|
|
||||||
metadata = volume_api.get_volume_encryption_metadata(context,
|
|
||||||
volume_id)
|
|
||||||
if not metadata:
|
|
||||||
LOG.warning('Volume %s should be encrypted but there is no '
|
|
||||||
'encryption metadata.', volume_id)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("Failed to retrieve encryption metadata for "
|
|
||||||
"volume %(volume_id)s: %(exception)s",
|
|
||||||
{'volume_id': volume_id, 'exception': e})
|
|
||||||
raise
|
|
||||||
|
|
||||||
if metadata:
|
|
||||||
msg = ("Using volume encryption metadata '%(metadata)s' for "
|
|
||||||
"connection: %(connection_info)s" %
|
|
||||||
{'metadata': metadata, 'connection_info': connection_info})
|
|
||||||
LOG.debug(strutils.mask_password(msg))
|
|
||||||
|
|
||||||
return metadata
|
|
|
@ -1,64 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 abc
|
|
||||||
|
|
||||||
from os_brick import executor
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class VolumeEncryptor(executor.Executor):
|
|
||||||
"""Base class to support encrypted volumes.
|
|
||||||
|
|
||||||
A VolumeEncryptor provides hooks for attaching and detaching volumes, which
|
|
||||||
are called immediately prior to attaching the volume to an instance and
|
|
||||||
immediately following detaching the volume from an instance. This class
|
|
||||||
performs no actions for either hook.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, root_helper,
|
|
||||||
connection_info,
|
|
||||||
keymgr,
|
|
||||||
execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(VolumeEncryptor, self).__init__(root_helper,
|
|
||||||
execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
self._key_manager = keymgr
|
|
||||||
|
|
||||||
self.encryption_key_id = kwargs.get('encryption_key_id')
|
|
||||||
|
|
||||||
def _get_key(self, context):
|
|
||||||
"""Retrieves the encryption key for the specified volume.
|
|
||||||
|
|
||||||
:param: the connection information used to attach the volume
|
|
||||||
"""
|
|
||||||
return self._key_manager.get(context, self.encryption_key_id)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def attach_volume(self, context, **kwargs):
|
|
||||||
"""Hook called immediately prior to attaching a volume to an instance.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def detach_volume(self, **kwargs):
|
|
||||||
"""Hook called immediately after detaching a volume from an instance.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass
|
|
|
@ -1,181 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 array
|
|
||||||
import binascii
|
|
||||||
import os
|
|
||||||
|
|
||||||
from os_brick.encryptors import base
|
|
||||||
from os_brick import exception
|
|
||||||
from oslo_concurrency import processutils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CryptsetupEncryptor(base.VolumeEncryptor):
|
|
||||||
"""A VolumeEncryptor based on dm-crypt.
|
|
||||||
|
|
||||||
This VolumeEncryptor uses dm-crypt to encrypt the specified volume.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, root_helper,
|
|
||||||
connection_info,
|
|
||||||
keymgr,
|
|
||||||
execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(CryptsetupEncryptor, self).__init__(
|
|
||||||
root_helper=root_helper,
|
|
||||||
connection_info=connection_info,
|
|
||||||
keymgr=keymgr,
|
|
||||||
execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
# Fail if no device_path was set when connecting the volume, e.g. in
|
|
||||||
# the case of libvirt network volume drivers.
|
|
||||||
data = connection_info['data']
|
|
||||||
if not data.get('device_path'):
|
|
||||||
volume_id = data.get('volume_id') or connection_info.get('serial')
|
|
||||||
raise exception.VolumeEncryptionNotSupported(
|
|
||||||
volume_id=volume_id,
|
|
||||||
volume_type=connection_info['driver_volume_type'])
|
|
||||||
|
|
||||||
# the device's path as given to libvirt -- e.g., /dev/disk/by-path/...
|
|
||||||
self.symlink_path = connection_info['data']['device_path']
|
|
||||||
|
|
||||||
# a unique name for the volume -- e.g., the iSCSI participant name
|
|
||||||
self.dev_name = 'crypt-%s' % os.path.basename(self.symlink_path)
|
|
||||||
|
|
||||||
# NOTE(lixiaoy1): This is to import fix for 1439869 from Nova.
|
|
||||||
# NOTE(tsekiyama): In older version of nova, dev_name was the same
|
|
||||||
# as the symlink name. Now it has 'crypt-' prefix to avoid conflict
|
|
||||||
# with multipath device symlink. To enable rolling update, we use the
|
|
||||||
# old name when the encrypted volume already exists.
|
|
||||||
old_dev_name = os.path.basename(self.symlink_path)
|
|
||||||
wwn = data.get('multipath_id')
|
|
||||||
if self._is_crypt_device_available(old_dev_name):
|
|
||||||
self.dev_name = old_dev_name
|
|
||||||
LOG.debug("Using old encrypted volume name: %s", self.dev_name)
|
|
||||||
elif wwn and wwn != old_dev_name:
|
|
||||||
# FibreChannel device could be named '/dev/mapper/<WWN>'.
|
|
||||||
if self._is_crypt_device_available(wwn):
|
|
||||||
self.dev_name = wwn
|
|
||||||
LOG.debug("Using encrypted volume name from wwn: %s",
|
|
||||||
self.dev_name)
|
|
||||||
|
|
||||||
# the device's actual path on the compute host -- e.g., /dev/sd_
|
|
||||||
self.dev_path = os.path.realpath(self.symlink_path)
|
|
||||||
|
|
||||||
def _is_crypt_device_available(self, dev_name):
|
|
||||||
if not os.path.exists('/dev/mapper/%s' % dev_name):
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._execute('cryptsetup', 'status', dev_name, run_as_root=True)
|
|
||||||
except processutils.ProcessExecutionError as e:
|
|
||||||
# If /dev/mapper/<dev_name> is a non-crypt block device (such as a
|
|
||||||
# normal disk or multipath device), exit_code will be 1. In the
|
|
||||||
# case, we will omit the warning message.
|
|
||||||
if e.exit_code != 1:
|
|
||||||
LOG.warning('cryptsetup status %(dev_name)s exited '
|
|
||||||
'abnormally (status %(exit_code)s): %(err)s',
|
|
||||||
{"dev_name": dev_name, "exit_code": e.exit_code,
|
|
||||||
"err": e.stderr})
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_passphrase(self, key):
|
|
||||||
"""Convert raw key to string."""
|
|
||||||
return binascii.hexlify(key).decode('utf-8')
|
|
||||||
|
|
||||||
def _open_volume(self, passphrase, **kwargs):
|
|
||||||
"""Open the LUKS partition on the volume using passphrase.
|
|
||||||
|
|
||||||
:param passphrase: the passphrase used to access the volume
|
|
||||||
"""
|
|
||||||
LOG.debug("opening encrypted volume %s", self.dev_path)
|
|
||||||
|
|
||||||
# NOTE(joel-coffman): cryptsetup will strip trailing newlines from
|
|
||||||
# input specified on stdin unless --key-file=- is specified.
|
|
||||||
cmd = ["cryptsetup", "create", "--key-file=-"]
|
|
||||||
|
|
||||||
cipher = kwargs.get("cipher", None)
|
|
||||||
if cipher is not None:
|
|
||||||
cmd.extend(["--cipher", cipher])
|
|
||||||
|
|
||||||
key_size = kwargs.get("key_size", None)
|
|
||||||
if key_size is not None:
|
|
||||||
cmd.extend(["--key-size", key_size])
|
|
||||||
|
|
||||||
cmd.extend([self.dev_name, self.dev_path])
|
|
||||||
|
|
||||||
self._execute(*cmd, process_input=passphrase,
|
|
||||||
check_exit_code=True, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
def _get_mangled_passphrase(self, key):
|
|
||||||
"""Convert the raw key into a list of unsigned int's and then a string
|
|
||||||
|
|
||||||
"""
|
|
||||||
# NOTE(lyarwood): This replicates the methods used prior to Newton to
|
|
||||||
# first encode the passphrase as a list of unsigned int's before
|
|
||||||
# decoding back into a string. This method strips any leading 0's
|
|
||||||
# of the resulting hex digit pairs, resulting in a different
|
|
||||||
# passphrase being returned.
|
|
||||||
encoded_key = array.array('B', key).tolist()
|
|
||||||
return ''.join(hex(x).replace('0x', '') for x in encoded_key)
|
|
||||||
|
|
||||||
def attach_volume(self, context, **kwargs):
|
|
||||||
"""Shadow the device and pass an unencrypted version to the instance.
|
|
||||||
|
|
||||||
Transparent disk encryption is achieved by mounting the volume via
|
|
||||||
dm-crypt and passing the resulting device to the instance. The
|
|
||||||
instance is unaware of the underlying encryption due to modifying the
|
|
||||||
original symbolic link to refer to the device mounted by dm-crypt.
|
|
||||||
"""
|
|
||||||
key = self._get_key(context).get_encoded()
|
|
||||||
passphrase = self._get_passphrase(key)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._open_volume(passphrase, **kwargs)
|
|
||||||
except processutils.ProcessExecutionError as e:
|
|
||||||
if e.exit_code == 2:
|
|
||||||
# NOTE(lyarwood): Workaround bug#1633518 by attempting to use
|
|
||||||
# a mangled passphrase to open the device..
|
|
||||||
LOG.info("Unable to open %s with the current passphrase, "
|
|
||||||
"attempting to use a mangled passphrase to open "
|
|
||||||
"the volume.", self.dev_path)
|
|
||||||
self._open_volume(self._get_mangled_passphrase(key), **kwargs)
|
|
||||||
|
|
||||||
# modify the original symbolic link to refer to the decrypted device
|
|
||||||
self._execute('ln', '--symbolic', '--force',
|
|
||||||
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True)
|
|
||||||
|
|
||||||
def _close_volume(self, **kwargs):
|
|
||||||
"""Closes the device (effectively removes the dm-crypt mapping)."""
|
|
||||||
LOG.debug("closing encrypted volume %s", self.dev_path)
|
|
||||||
# cryptsetup returns 4 when attempting to destroy a non-active
|
|
||||||
# dm-crypt device. We are going to ignore this error code to make
|
|
||||||
# nova deleting that instance successfully.
|
|
||||||
self._execute('cryptsetup', 'remove', self.dev_name,
|
|
||||||
run_as_root=True, check_exit_code=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
def detach_volume(self, **kwargs):
|
|
||||||
"""Removes the dm-crypt mapping for the device."""
|
|
||||||
self._close_volume(**kwargs)
|
|
|
@ -1,187 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_brick.encryptors import cryptsetup
|
|
||||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def is_luks(root_helper, device, execute=None):
|
|
||||||
"""Checks if the specified device uses LUKS for encryption.
|
|
||||||
|
|
||||||
:param device: the device to check
|
|
||||||
:returns: true if the specified device uses LUKS; false otherwise
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# check to see if the device uses LUKS: exit status is 0
|
|
||||||
# if the device is a LUKS partition and non-zero if not
|
|
||||||
if execute is None:
|
|
||||||
execute = priv_rootwrap.execute
|
|
||||||
execute('cryptsetup', 'isLuks', '--verbose', device,
|
|
||||||
run_as_root=True, root_helper=root_helper,
|
|
||||||
check_exit_code=True)
|
|
||||||
return True
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
LOG.warning("isLuks exited abnormally (status %(exit_code)s): "
|
|
||||||
"%(stderr)s",
|
|
||||||
{"exit_code": e.exit_code, "stderr": e.stderr})
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class LuksEncryptor(cryptsetup.CryptsetupEncryptor):
|
|
||||||
"""A VolumeEncryptor based on LUKS.
|
|
||||||
|
|
||||||
This VolumeEncryptor uses dm-crypt to encrypt the specified volume.
|
|
||||||
"""
|
|
||||||
def __init__(self, root_helper,
|
|
||||||
connection_info,
|
|
||||||
keymgr,
|
|
||||||
execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(LuksEncryptor, self).__init__(
|
|
||||||
root_helper=root_helper,
|
|
||||||
connection_info=connection_info,
|
|
||||||
keymgr=keymgr,
|
|
||||||
execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
def _format_volume(self, passphrase, **kwargs):
|
|
||||||
"""Creates a LUKS header on the volume.
|
|
||||||
|
|
||||||
:param passphrase: the passphrase used to access the volume
|
|
||||||
"""
|
|
||||||
LOG.debug("formatting encrypted volume %s", self.dev_path)
|
|
||||||
|
|
||||||
# NOTE(joel-coffman): cryptsetup will strip trailing newlines from
|
|
||||||
# input specified on stdin unless --key-file=- is specified.
|
|
||||||
cmd = ["cryptsetup", "--batch-mode", "luksFormat", "--key-file=-"]
|
|
||||||
|
|
||||||
cipher = kwargs.get("cipher", None)
|
|
||||||
if cipher is not None:
|
|
||||||
cmd.extend(["--cipher", cipher])
|
|
||||||
|
|
||||||
key_size = kwargs.get("key_size", None)
|
|
||||||
if key_size is not None:
|
|
||||||
cmd.extend(["--key-size", key_size])
|
|
||||||
|
|
||||||
cmd.extend([self.dev_path])
|
|
||||||
|
|
||||||
self._execute(*cmd, process_input=passphrase,
|
|
||||||
check_exit_code=True, run_as_root=True,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
attempts=3)
|
|
||||||
|
|
||||||
def _open_volume(self, passphrase, **kwargs):
|
|
||||||
"""Opens the LUKS partition on the volume using passphrase.
|
|
||||||
|
|
||||||
:param passphrase: the passphrase used to access the volume
|
|
||||||
"""
|
|
||||||
LOG.debug("opening encrypted volume %s", self.dev_path)
|
|
||||||
self._execute('cryptsetup', 'luksOpen', '--key-file=-',
|
|
||||||
self.dev_path, self.dev_name, process_input=passphrase,
|
|
||||||
run_as_root=True, check_exit_code=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
def _unmangle_volume(self, key, passphrase, **kwargs):
|
|
||||||
"""Workaround for bug#1633518
|
|
||||||
|
|
||||||
First identify if a mangled passphrase is used and if found then
|
|
||||||
replace with the correct unmangled version of the passphrase.
|
|
||||||
"""
|
|
||||||
mangled_passphrase = self._get_mangled_passphrase(key)
|
|
||||||
self._open_volume(mangled_passphrase, **kwargs)
|
|
||||||
self._close_volume(**kwargs)
|
|
||||||
LOG.debug("%s correctly opened with a mangled passphrase, replacing "
|
|
||||||
"this with the original passphrase", self.dev_path)
|
|
||||||
|
|
||||||
# NOTE(lyarwood): Now that we are sure that the mangled passphrase is
|
|
||||||
# used attempt to add the correct passphrase before removing the
|
|
||||||
# mangled version from the volume.
|
|
||||||
|
|
||||||
# luksAddKey currently prompts for the following input :
|
|
||||||
# Enter any existing passphrase:
|
|
||||||
# Enter new passphrase for key slot:
|
|
||||||
# Verify passphrase:
|
|
||||||
self._execute('cryptsetup', 'luksAddKey', self.dev_path,
|
|
||||||
process_input=''.join([mangled_passphrase, '\n',
|
|
||||||
passphrase, '\n', passphrase]),
|
|
||||||
run_as_root=True, check_exit_code=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
# Verify that we can open the volume with the current passphrase
|
|
||||||
# before removing the mangled passphrase.
|
|
||||||
self._open_volume(passphrase, **kwargs)
|
|
||||||
self._close_volume(**kwargs)
|
|
||||||
|
|
||||||
# luksRemoveKey only prompts for the key to remove.
|
|
||||||
self._execute('cryptsetup', 'luksRemoveKey', self.dev_path,
|
|
||||||
process_input=mangled_passphrase,
|
|
||||||
run_as_root=True, check_exit_code=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
LOG.debug("%s mangled passphrase successfully replaced", self.dev_path)
|
|
||||||
|
|
||||||
def attach_volume(self, context, **kwargs):
|
|
||||||
"""Shadow the device and pass an unencrypted version to the instance.
|
|
||||||
|
|
||||||
Transparent disk encryption is achieved by mounting the volume via
|
|
||||||
dm-crypt and passing the resulting device to the instance. The
|
|
||||||
instance is unaware of the underlying encryption due to modifying the
|
|
||||||
original symbolic link to refer to the device mounted by dm-crypt.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = self._get_key(context).get_encoded()
|
|
||||||
passphrase = self._get_passphrase(key)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._open_volume(passphrase, **kwargs)
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
if e.exit_code == 1 and not is_luks(self._root_helper,
|
|
||||||
self.dev_path,
|
|
||||||
execute=self._execute):
|
|
||||||
# the device has never been formatted; format it and try again
|
|
||||||
LOG.info("%s is not a valid LUKS device;"
|
|
||||||
" formatting device for first use",
|
|
||||||
self.dev_path)
|
|
||||||
self._format_volume(passphrase, **kwargs)
|
|
||||||
self._open_volume(passphrase, **kwargs)
|
|
||||||
elif e.exit_code == 2:
|
|
||||||
# NOTE(lyarwood): Workaround bug#1633518 by replacing any
|
|
||||||
# mangled passphrases that are found on the volume.
|
|
||||||
# TODO(lyarwood): Remove workaround during R.
|
|
||||||
LOG.warning("%s is not usable with the current "
|
|
||||||
"passphrase, attempting to use a mangled "
|
|
||||||
"passphrase to open the volume.",
|
|
||||||
self.dev_path)
|
|
||||||
self._unmangle_volume(key, passphrase, **kwargs)
|
|
||||||
self._open_volume(passphrase, **kwargs)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# modify the original symbolic link to refer to the decrypted device
|
|
||||||
self._execute('ln', '--symbolic', '--force',
|
|
||||||
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True)
|
|
||||||
|
|
||||||
def _close_volume(self, **kwargs):
|
|
||||||
"""Closes the device (effectively removes the dm-crypt mapping)."""
|
|
||||||
LOG.debug("closing encrypted volume %s", self.dev_path)
|
|
||||||
self._execute('cryptsetup', 'luksClose', self.dev_name,
|
|
||||||
run_as_root=True, check_exit_code=True,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
attempts=3)
|
|
|
@ -1,43 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_brick.encryptors import base
|
|
||||||
|
|
||||||
|
|
||||||
class NoOpEncryptor(base.VolumeEncryptor):
|
|
||||||
"""A VolumeEncryptor that does nothing.
|
|
||||||
|
|
||||||
This class exists solely to wrap regular (i.e., unencrypted) volumes so
|
|
||||||
that they do not require special handling with respect to an encrypted
|
|
||||||
volume. This implementation performs no action when a volume is attached
|
|
||||||
or detached.
|
|
||||||
"""
|
|
||||||
def __init__(self, root_helper,
|
|
||||||
connection_info,
|
|
||||||
keymgr,
|
|
||||||
execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(NoOpEncryptor, self).__init__(
|
|
||||||
root_helper=root_helper,
|
|
||||||
connection_info=connection_info,
|
|
||||||
keymgr=keymgr,
|
|
||||||
execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
def attach_volume(self, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def detach_volume(self):
|
|
||||||
pass
|
|
|
@ -1,230 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Exceptions for the Brick library."""
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
import six
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BrickException(Exception):
|
|
||||||
"""Base Brick Exception
|
|
||||||
|
|
||||||
To correctly use this class, inherit from it and define
|
|
||||||
a 'msg_fmt' property. That msg_fmt will get printf'd
|
|
||||||
with the keyword arguments provided to the constructor.
|
|
||||||
"""
|
|
||||||
message = _("An unknown exception occurred.")
|
|
||||||
code = 500
|
|
||||||
headers = {}
|
|
||||||
safe = False
|
|
||||||
|
|
||||||
def __init__(self, message=None, **kwargs):
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
if 'code' not in self.kwargs:
|
|
||||||
try:
|
|
||||||
self.kwargs['code'] = self.code
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not message:
|
|
||||||
try:
|
|
||||||
message = self.message % kwargs
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# kwargs doesn't match a variable in the message
|
|
||||||
# log the issue and the kwargs
|
|
||||||
LOG.exception("Exception in string format operation. "
|
|
||||||
"msg='%s'", self.message)
|
|
||||||
for name, value in kwargs.items():
|
|
||||||
LOG.error("%(name)s: %(value)s", {'name': name,
|
|
||||||
'value': value})
|
|
||||||
|
|
||||||
# at least get the core message out if something happened
|
|
||||||
message = self.message
|
|
||||||
|
|
||||||
# Put the message in 'msg' so that we can access it. If we have it in
|
|
||||||
# message it will be overshadowed by the class' message attribute
|
|
||||||
self.msg = message
|
|
||||||
super(BrickException, self).__init__(message)
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return six.text_type(self.msg)
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(BrickException):
|
|
||||||
message = _("Resource could not be found.")
|
|
||||||
code = 404
|
|
||||||
safe = True
|
|
||||||
|
|
||||||
|
|
||||||
class Invalid(BrickException):
|
|
||||||
message = _("Unacceptable parameters.")
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
# Cannot be templated as the error syntax varies.
|
|
||||||
# msg needs to be constructed when raised.
|
|
||||||
class InvalidParameterValue(Invalid):
|
|
||||||
message = _("%(err)s")
|
|
||||||
|
|
||||||
|
|
||||||
class NoFibreChannelHostsFound(BrickException):
|
|
||||||
message = _("We are unable to locate any Fibre Channel devices.")
|
|
||||||
|
|
||||||
|
|
||||||
class NoFibreChannelVolumeDeviceFound(BrickException):
|
|
||||||
message = _("Unable to find a Fibre Channel volume device.")
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeNotDeactivated(BrickException):
|
|
||||||
message = _('Volume %(name)s was not deactivated in time.')
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeDeviceNotFound(BrickException):
|
|
||||||
message = _("Volume device not found at %(device)s.")
|
|
||||||
|
|
||||||
|
|
||||||
class VolumePathsNotFound(BrickException):
|
|
||||||
message = _("Could not find any paths for the volume.")
|
|
||||||
|
|
||||||
|
|
||||||
class VolumePathNotRemoved(BrickException):
|
|
||||||
message = _("Volume path %(volume_path)s was not removed in time.")
|
|
||||||
|
|
||||||
|
|
||||||
class ProtocolNotSupported(BrickException):
|
|
||||||
message = _("Connect to volume via protocol %(protocol)s not supported.")
|
|
||||||
|
|
||||||
|
|
||||||
class TargetPortalNotFound(BrickException):
|
|
||||||
message = _("Unable to find target portal %(target_portal)s.")
|
|
||||||
|
|
||||||
|
|
||||||
class TargetPortalsNotFound(BrickException):
|
|
||||||
message = _("Unable to find target portal in %(target_portals)s.")
|
|
||||||
|
|
||||||
|
|
||||||
class FailedISCSITargetPortalLogin(BrickException):
|
|
||||||
message = _("Unable to login to iSCSI Target Portal")
|
|
||||||
|
|
||||||
|
|
||||||
class BlockDeviceReadOnly(BrickException):
|
|
||||||
message = _("Block device %(device)s is Read-Only.")
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeGroupNotFound(BrickException):
|
|
||||||
message = _("Unable to find Volume Group: %(vg_name)s")
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeGroupCreationFailed(BrickException):
|
|
||||||
message = _("Failed to create Volume Group: %(vg_name)s")
|
|
||||||
|
|
||||||
|
|
||||||
class CommandExecutionFailed(BrickException):
|
|
||||||
message = _("Failed to execute command %(cmd)s")
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeDriverException(BrickException):
|
|
||||||
message = _('An error occurred while IO to volume %(name)s.')
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidIOHandleObject(BrickException):
|
|
||||||
message = _('IO handle of %(protocol)s has wrong object '
|
|
||||||
'type %(actual_type)s.')
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeEncryptionNotSupported(Invalid):
|
|
||||||
message = _("Volume encryption is not supported for %(volume_type)s "
|
|
||||||
"volume %(volume_id)s.")
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE(mriedem): This extends ValueError to maintain backward compatibility.
|
|
||||||
class InvalidConnectorProtocol(ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ExceptionChainer(BrickException):
|
|
||||||
"""A Exception that can contain a group of exceptions.
|
|
||||||
|
|
||||||
This exception serves as a container for exceptions, useful when we want to
|
|
||||||
store all exceptions that happened during a series of steps and then raise
|
|
||||||
them all together as one.
|
|
||||||
|
|
||||||
The representation of the exception will include all exceptions and their
|
|
||||||
tracebacks.
|
|
||||||
|
|
||||||
This class also includes a context manager for convenience, one that will
|
|
||||||
support both swallowing the exception as if nothing had happened and
|
|
||||||
raising the exception. In both cases the exception will be stored.
|
|
||||||
|
|
||||||
If a message is provided to the context manager it will be formatted and
|
|
||||||
logged with warning level.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._exceptions = []
|
|
||||||
self._repr = None
|
|
||||||
super(ExceptionChainer, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
# Since generating the representation can be slow we cache it
|
|
||||||
if not self._repr:
|
|
||||||
tracebacks = (
|
|
||||||
''.join(traceback.format_exception(*e)).replace('\n', '\n\t')
|
|
||||||
for e in self._exceptions)
|
|
||||||
self._repr = '\n'.join('\nChained Exception #%s\n\t%s' % (i + 1, t)
|
|
||||||
for i, t in enumerate(tracebacks))
|
|
||||||
return self._repr
|
|
||||||
|
|
||||||
__str__ = __unicode__ = __repr__
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
# We want to be able to do boolean checks on the exception
|
|
||||||
return bool(self._exceptions)
|
|
||||||
|
|
||||||
__bool__ = __nonzero__ # For Python 3
|
|
||||||
|
|
||||||
def add_exception(self, exc_type, exc_val, exc_tb):
|
|
||||||
# Clear the representation cache
|
|
||||||
self._repr = None
|
|
||||||
self._exceptions.append((exc_type, exc_val, exc_tb))
|
|
||||||
|
|
||||||
def context(self, catch_exception, msg='', *msg_args):
|
|
||||||
self._catch_exception = catch_exception
|
|
||||||
self._exc_msg = msg
|
|
||||||
self._exc_msg_args = msg_args
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
if exc_type:
|
|
||||||
self.add_exception(exc_type, exc_val, exc_tb)
|
|
||||||
if self._exc_msg:
|
|
||||||
LOG.warning(self._exc_msg, *self._exc_msg_args)
|
|
||||||
if self._catch_exception:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ExecutionTimeout(putils.ProcessExecutionError):
|
|
||||||
pass
|
|
|
@ -1,84 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Generic exec utility that allows us to set the
|
|
||||||
execute and root_helper attributes for putils.
|
|
||||||
Some projects need their own execute wrapper
|
|
||||||
and root_helper settings, so this provides that hook.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_context import context as context_utils
|
|
||||||
from oslo_utils import encodeutils
|
|
||||||
|
|
||||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
|
||||||
|
|
||||||
|
|
||||||
class Executor(object):
|
|
||||||
def __init__(self, root_helper, execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
if execute is None:
|
|
||||||
execute = priv_rootwrap.execute
|
|
||||||
self.set_execute(execute)
|
|
||||||
self.set_root_helper(root_helper)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def safe_decode(string):
|
|
||||||
return string and encodeutils.safe_decode(string, errors='ignore')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def make_putils_error_safe(cls, exc):
|
|
||||||
"""Converts ProcessExecutionError string attributes to unicode."""
|
|
||||||
for field in ('stderr', 'stdout', 'cmd', 'description'):
|
|
||||||
value = getattr(exc, field, None)
|
|
||||||
if value:
|
|
||||||
setattr(exc, field, cls.safe_decode(value))
|
|
||||||
|
|
||||||
def _execute(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
result = self.__execute(*args, **kwargs)
|
|
||||||
if result:
|
|
||||||
result = (self.safe_decode(result[0]),
|
|
||||||
self.safe_decode(result[1]))
|
|
||||||
return result
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
self.make_putils_error_safe(e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def set_execute(self, execute):
|
|
||||||
self.__execute = execute
|
|
||||||
|
|
||||||
def set_root_helper(self, helper):
|
|
||||||
self._root_helper = helper
|
|
||||||
|
|
||||||
|
|
||||||
class Thread(threading.Thread):
|
|
||||||
"""Thread class that inherits the parent's context.
|
|
||||||
|
|
||||||
This is useful when you are spawning a thread and want LOG entries to
|
|
||||||
display the right context information, such as the request.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# Store the caller's context as a private variable shared among threads
|
|
||||||
self.__context__ = context_utils.get_current()
|
|
||||||
super(Thread, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# Store the context in the current thread's request store
|
|
||||||
if self.__context__:
|
|
||||||
self.__context__.update_store()
|
|
||||||
super(Thread, self).run()
|
|
|
@ -1,28 +0,0 @@
|
||||||
# Copyright 2014 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.
|
|
||||||
|
|
||||||
"""oslo.i18n integration module.
|
|
||||||
|
|
||||||
See http://docs.openstack.org/developer/oslo.i18n/usage.html .
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import oslo_i18n as i18n
|
|
||||||
|
|
||||||
DOMAIN = 'os-brick'
|
|
||||||
|
|
||||||
_translators = i18n.TranslatorFactory(domain=DOMAIN)
|
|
||||||
|
|
||||||
# The primary translation function using the well-known name "_"
|
|
||||||
_ = _translators.primary
|
|
|
@ -1,63 +0,0 @@
|
||||||
# Copyright 2015 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
"""
|
|
||||||
Brick's Initiator module.
|
|
||||||
|
|
||||||
The initator module contains the capabilities for discovering the initiator
|
|
||||||
information as well as discovering and removing volumes from a host.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
|
|
||||||
MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$")
|
|
||||||
MULTIPATH_PATH_CHECK_REGEX = re.compile("\s+\d+:\d+:\d+:\d+\s+")
|
|
||||||
|
|
||||||
PLATFORM_ALL = 'ALL'
|
|
||||||
PLATFORM_x86 = 'X86'
|
|
||||||
PLATFORM_S390 = 'S390'
|
|
||||||
PLATFORM_PPC64 = 'PPC64'
|
|
||||||
OS_TYPE_ALL = 'ALL'
|
|
||||||
OS_TYPE_LINUX = 'LINUX'
|
|
||||||
OS_TYPE_WINDOWS = 'WIN'
|
|
||||||
|
|
||||||
S390X = "s390x"
|
|
||||||
S390 = "s390"
|
|
||||||
PPC64 = "ppc64"
|
|
||||||
PPC64LE = "ppc64le"
|
|
||||||
|
|
||||||
ISCSI = "ISCSI"
|
|
||||||
ISER = "ISER"
|
|
||||||
FIBRE_CHANNEL = "FIBRE_CHANNEL"
|
|
||||||
AOE = "AOE"
|
|
||||||
DRBD = "DRBD"
|
|
||||||
NFS = "NFS"
|
|
||||||
SMBFS = 'SMBFS'
|
|
||||||
GLUSTERFS = "GLUSTERFS"
|
|
||||||
LOCAL = "LOCAL"
|
|
||||||
HUAWEISDSHYPERVISOR = "HUAWEISDSHYPERVISOR"
|
|
||||||
HGST = "HGST"
|
|
||||||
RBD = "RBD"
|
|
||||||
SCALEIO = "SCALEIO"
|
|
||||||
SCALITY = "SCALITY"
|
|
||||||
QUOBYTE = "QUOBYTE"
|
|
||||||
DISCO = "DISCO"
|
|
||||||
VZSTORAGE = "VZSTORAGE"
|
|
||||||
SHEEPDOG = "SHEEPDOG"
|
|
||||||
VMDK = "VMDK"
|
|
||||||
GPFS = "GPFS"
|
|
||||||
VERITAS_HYPERSCALE = "VERITAS_HYPERSCALE"
|
|
|
@ -1,317 +0,0 @@
|
||||||
# Copyright 2013 OpenStack Foundation.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
"""Brick Connector objects for each supported transport protocol.
|
|
||||||
|
|
||||||
.. module: connector
|
|
||||||
|
|
||||||
The connectors here are responsible for discovering and removing volumes for
|
|
||||||
each of the supported transport protocols.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import platform
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import importutils
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
|
||||||
|
|
||||||
# These constants are being deprecated and moving to the init file.
|
|
||||||
# Please use the constants there instead.
|
|
||||||
|
|
||||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
|
|
||||||
MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$")
|
|
||||||
MULTIPATH_PATH_CHECK_REGEX = re.compile("\s+\d+:\d+:\d+:\d+\s+")
|
|
||||||
|
|
||||||
PLATFORM_ALL = 'ALL'
|
|
||||||
PLATFORM_x86 = 'X86'
|
|
||||||
PLATFORM_S390 = 'S390'
|
|
||||||
PLATFORM_PPC64 = 'PPC64'
|
|
||||||
OS_TYPE_ALL = 'ALL'
|
|
||||||
OS_TYPE_LINUX = 'LINUX'
|
|
||||||
OS_TYPE_WINDOWS = 'WIN'
|
|
||||||
|
|
||||||
S390X = "s390x"
|
|
||||||
S390 = "s390"
|
|
||||||
PPC64 = "ppc64"
|
|
||||||
PPC64LE = "ppc64le"
|
|
||||||
|
|
||||||
ISCSI = "ISCSI"
|
|
||||||
ISER = "ISER"
|
|
||||||
FIBRE_CHANNEL = "FIBRE_CHANNEL"
|
|
||||||
AOE = "AOE"
|
|
||||||
DRBD = "DRBD"
|
|
||||||
NFS = "NFS"
|
|
||||||
GLUSTERFS = "GLUSTERFS"
|
|
||||||
LOCAL = "LOCAL"
|
|
||||||
GPFS = "GPFS"
|
|
||||||
HUAWEISDSHYPERVISOR = "HUAWEISDSHYPERVISOR"
|
|
||||||
HGST = "HGST"
|
|
||||||
RBD = "RBD"
|
|
||||||
SCALEIO = "SCALEIO"
|
|
||||||
SCALITY = "SCALITY"
|
|
||||||
QUOBYTE = "QUOBYTE"
|
|
||||||
DISCO = "DISCO"
|
|
||||||
VZSTORAGE = "VZSTORAGE"
|
|
||||||
SHEEPDOG = "SHEEPDOG"
|
|
||||||
|
|
||||||
# List of connectors to call when getting
|
|
||||||
# the connector properties for a host
|
|
||||||
connector_list = [
|
|
||||||
'os_brick.initiator.connectors.base.BaseLinuxConnector',
|
|
||||||
'os_brick.initiator.connectors.iscsi.ISCSIConnector',
|
|
||||||
'os_brick.initiator.connectors.fibre_channel.FibreChannelConnector',
|
|
||||||
('os_brick.initiator.connectors.fibre_channel_s390x.'
|
|
||||||
'FibreChannelConnectorS390X'),
|
|
||||||
('os_brick.initiator.connectors.fibre_channel_ppc64.'
|
|
||||||
'FibreChannelConnectorPPC64'),
|
|
||||||
'os_brick.initiator.connectors.aoe.AoEConnector',
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
'os_brick.initiator.connectors.rbd.RBDConnector',
|
|
||||||
'os_brick.initiator.connectors.local.LocalConnector',
|
|
||||||
'os_brick.initiator.connectors.gpfs.GPFSConnector',
|
|
||||||
'os_brick.initiator.connectors.drbd.DRBDConnector',
|
|
||||||
'os_brick.initiator.connectors.huawei.HuaweiStorHyperConnector',
|
|
||||||
'os_brick.initiator.connectors.hgst.HGSTConnector',
|
|
||||||
'os_brick.initiator.connectors.scaleio.ScaleIOConnector',
|
|
||||||
'os_brick.initiator.connectors.disco.DISCOConnector',
|
|
||||||
'os_brick.initiator.connectors.vmware.VmdkConnector',
|
|
||||||
'os_brick.initiator.windows.base.BaseWindowsConnector',
|
|
||||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
|
||||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
|
||||||
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
|
|
||||||
'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Mappings used to determine who to contruct in the factory
|
|
||||||
_connector_mapping_linux = {
|
|
||||||
initiator.AOE:
|
|
||||||
'os_brick.initiator.connectors.aoe.AoEConnector',
|
|
||||||
initiator.DRBD:
|
|
||||||
'os_brick.initiator.connectors.drbd.DRBDConnector',
|
|
||||||
|
|
||||||
initiator.GLUSTERFS:
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
initiator.NFS:
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
initiator.SCALITY:
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
initiator.QUOBYTE:
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
initiator.VZSTORAGE:
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
|
|
||||||
initiator.ISCSI:
|
|
||||||
'os_brick.initiator.connectors.iscsi.ISCSIConnector',
|
|
||||||
initiator.ISER:
|
|
||||||
'os_brick.initiator.connectors.iscsi.ISCSIConnector',
|
|
||||||
initiator.FIBRE_CHANNEL:
|
|
||||||
'os_brick.initiator.connectors.fibre_channel.FibreChannelConnector',
|
|
||||||
|
|
||||||
initiator.LOCAL:
|
|
||||||
'os_brick.initiator.connectors.local.LocalConnector',
|
|
||||||
initiator.HUAWEISDSHYPERVISOR:
|
|
||||||
'os_brick.initiator.connectors.huawei.HuaweiStorHyperConnector',
|
|
||||||
initiator.HGST:
|
|
||||||
'os_brick.initiator.connectors.hgst.HGSTConnector',
|
|
||||||
initiator.RBD:
|
|
||||||
'os_brick.initiator.connectors.rbd.RBDConnector',
|
|
||||||
initiator.SCALEIO:
|
|
||||||
'os_brick.initiator.connectors.scaleio.ScaleIOConnector',
|
|
||||||
initiator.DISCO:
|
|
||||||
'os_brick.initiator.connectors.disco.DISCOConnector',
|
|
||||||
initiator.SHEEPDOG:
|
|
||||||
'os_brick.initiator.connectors.sheepdog.SheepdogConnector',
|
|
||||||
initiator.VMDK:
|
|
||||||
'os_brick.initiator.connectors.vmware.VmdkConnector',
|
|
||||||
initiator.GPFS:
|
|
||||||
'os_brick.initiator.connectors.gpfs.GPFSConnector',
|
|
||||||
initiator.VERITAS_HYPERSCALE:
|
|
||||||
'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mapping for the S390X platform
|
|
||||||
_connector_mapping_linux_s390x = {
|
|
||||||
initiator.FIBRE_CHANNEL:
|
|
||||||
'os_brick.initiator.connectors.fibre_channel_s390x.'
|
|
||||||
'FibreChannelConnectorS390X',
|
|
||||||
initiator.DRBD:
|
|
||||||
'os_brick.initiator.connectors.drbd.DRBDConnector',
|
|
||||||
initiator.NFS:
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
initiator.ISCSI:
|
|
||||||
'os_brick.initiator.connectors.iscsi.ISCSIConnector',
|
|
||||||
initiator.LOCAL:
|
|
||||||
'os_brick.initiator.connectors.local.LocalConnector',
|
|
||||||
initiator.RBD:
|
|
||||||
'os_brick.initiator.connectors.rbd.RBDConnector',
|
|
||||||
initiator.GPFS:
|
|
||||||
'os_brick.initiator.connectors.gpfs.GPFSConnector',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mapping for the PPC64 platform
|
|
||||||
_connector_mapping_linux_ppc64 = {
|
|
||||||
initiator.FIBRE_CHANNEL:
|
|
||||||
('os_brick.initiator.connectors.fibre_channel_ppc64.'
|
|
||||||
'FibreChannelConnectorPPC64'),
|
|
||||||
initiator.DRBD:
|
|
||||||
'os_brick.initiator.connectors.drbd.DRBDConnector',
|
|
||||||
initiator.NFS:
|
|
||||||
'os_brick.initiator.connectors.remotefs.RemoteFsConnector',
|
|
||||||
initiator.ISCSI:
|
|
||||||
'os_brick.initiator.connectors.iscsi.ISCSIConnector',
|
|
||||||
initiator.LOCAL:
|
|
||||||
'os_brick.initiator.connectors.local.LocalConnector',
|
|
||||||
initiator.RBD:
|
|
||||||
'os_brick.initiator.connectors.rbd.RBDConnector',
|
|
||||||
initiator.GPFS:
|
|
||||||
'os_brick.initiator.connectors.gpfs.GPFSConnector',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mapping for the windows connectors
|
|
||||||
_connector_mapping_windows = {
|
|
||||||
initiator.ISCSI:
|
|
||||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
|
||||||
initiator.FIBRE_CHANNEL:
|
|
||||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
|
||||||
initiator.SMBFS:
|
|
||||||
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Create aliases to the old names until 2.0.0
|
|
||||||
# TODO(smcginnis) Remove this lookup once unit test code is updated to
|
|
||||||
# point to the correct location
|
|
||||||
for item in connector_list:
|
|
||||||
_name = item.split('.')[-1]
|
|
||||||
globals()[_name] = importutils.import_class(item)
|
|
||||||
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath,
|
|
||||||
host=None, execute=None):
|
|
||||||
"""Get the connection properties for all protocols.
|
|
||||||
|
|
||||||
When the connector wants to use multipath, multipath=True should be
|
|
||||||
specified. If enforce_multipath=True is specified too, an exception is
|
|
||||||
thrown when multipathd is not running. Otherwise, it falls back to
|
|
||||||
multipath=False and only the first path shown up is used.
|
|
||||||
For the compatibility reason, even if multipath=False is specified,
|
|
||||||
some cinder storage drivers may export the target for multipath, which
|
|
||||||
can be found via sendtargets discovery.
|
|
||||||
|
|
||||||
:param root_helper: The command prefix for executing as root.
|
|
||||||
:type root_helper: str
|
|
||||||
:param my_ip: The IP address of the local host.
|
|
||||||
:type my_ip: str
|
|
||||||
:param multipath: Enable multipath?
|
|
||||||
:type multipath: bool
|
|
||||||
:param enforce_multipath: Should we enforce that the multipath daemon is
|
|
||||||
running? If the daemon isn't running then the
|
|
||||||
return dict will have multipath as False.
|
|
||||||
:type enforce_multipath: bool
|
|
||||||
:param host: hostname.
|
|
||||||
:param execute: execute helper.
|
|
||||||
:returns: dict containing all of the collected initiator values.
|
|
||||||
"""
|
|
||||||
props = {}
|
|
||||||
props['platform'] = platform.machine()
|
|
||||||
props['os_type'] = sys.platform
|
|
||||||
props['ip'] = my_ip
|
|
||||||
props['host'] = host if host else socket.gethostname()
|
|
||||||
|
|
||||||
for item in connector_list:
|
|
||||||
connector = importutils.import_class(item)
|
|
||||||
|
|
||||||
if (utils.platform_matches(props['platform'], connector.platform) and
|
|
||||||
utils.os_matches(props['os_type'], connector.os_type)):
|
|
||||||
props = utils.merge_dict(props,
|
|
||||||
connector.get_connector_properties(
|
|
||||||
root_helper,
|
|
||||||
host=host,
|
|
||||||
multipath=multipath,
|
|
||||||
enforce_multipath=enforce_multipath,
|
|
||||||
execute=execute))
|
|
||||||
|
|
||||||
return props
|
|
||||||
|
|
||||||
|
|
||||||
# TODO(walter-boring) We have to keep this class defined here
|
|
||||||
# so we don't break backwards compatibility
|
|
||||||
class InitiatorConnector(object):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def factory(protocol, root_helper, driver=None,
|
|
||||||
use_multipath=False,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
arch=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
"""Build a Connector object based upon protocol and architecture."""
|
|
||||||
|
|
||||||
# We do this instead of assigning it in the definition
|
|
||||||
# to help mocking for unit tests
|
|
||||||
if arch is None:
|
|
||||||
arch = platform.machine()
|
|
||||||
|
|
||||||
# Set the correct mapping for imports
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
_mapping = _connector_mapping_windows
|
|
||||||
elif arch in (initiator.S390, initiator.S390X):
|
|
||||||
_mapping = _connector_mapping_linux_s390x
|
|
||||||
elif arch in (initiator.PPC64, initiator.PPC64LE):
|
|
||||||
_mapping = _connector_mapping_linux_ppc64
|
|
||||||
|
|
||||||
else:
|
|
||||||
_mapping = _connector_mapping_linux
|
|
||||||
|
|
||||||
LOG.debug("Factory for %(protocol)s on %(arch)s",
|
|
||||||
{'protocol': protocol, 'arch': arch})
|
|
||||||
protocol = protocol.upper()
|
|
||||||
|
|
||||||
# set any special kwargs needed by connectors
|
|
||||||
if protocol in (initiator.NFS, initiator.GLUSTERFS,
|
|
||||||
initiator.SCALITY, initiator.QUOBYTE,
|
|
||||||
initiator.VZSTORAGE):
|
|
||||||
kwargs.update({'mount_type': protocol.lower()})
|
|
||||||
elif protocol == initiator.ISER:
|
|
||||||
kwargs.update({'transport': 'iser'})
|
|
||||||
|
|
||||||
# now set all the default kwargs
|
|
||||||
kwargs.update(
|
|
||||||
{'root_helper': root_helper,
|
|
||||||
'driver': driver,
|
|
||||||
'use_multipath': use_multipath,
|
|
||||||
'device_scan_attempts': device_scan_attempts,
|
|
||||||
})
|
|
||||||
|
|
||||||
connector = _mapping.get(protocol)
|
|
||||||
if not connector:
|
|
||||||
msg = (_("Invalid InitiatorConnector protocol "
|
|
||||||
"specified %(protocol)s") %
|
|
||||||
dict(protocol=protocol))
|
|
||||||
raise exception.InvalidConnectorProtocol(msg)
|
|
||||||
|
|
||||||
conn_cls = importutils.import_class(connector)
|
|
||||||
return conn_cls(*args, **kwargs)
|
|
|
@ -1,176 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_service import loopingcall
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import initiator
|
|
||||||
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AoEConnector(base.BaseLinuxConnector):
|
|
||||||
"""Connector class to attach/detach AoE volumes."""
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(AoEConnector, self).__init__(
|
|
||||||
root_helper,
|
|
||||||
driver=driver,
|
|
||||||
device_scan_attempts=device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The AoE connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return '/dev/etherd'
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
aoe_device, aoe_path = self._get_aoe_info(connection_properties)
|
|
||||||
volume_paths = []
|
|
||||||
if os.path.exists(aoe_path):
|
|
||||||
volume_paths.append(aoe_path)
|
|
||||||
|
|
||||||
return volume_paths
|
|
||||||
|
|
||||||
def _get_aoe_info(self, connection_properties):
|
|
||||||
shelf = connection_properties['target_shelf']
|
|
||||||
lun = connection_properties['target_lun']
|
|
||||||
aoe_device = 'e%(shelf)s.%(lun)s' % {'shelf': shelf,
|
|
||||||
'lun': lun}
|
|
||||||
path = self.get_search_path()
|
|
||||||
aoe_path = '%(path)s/%(device)s' % {'path': path,
|
|
||||||
'device': aoe_device}
|
|
||||||
return aoe_device, aoe_path
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@lockutils.synchronized('aoe_control', 'aoe-')
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Discover and attach the volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
|
|
||||||
connection_properties for AoE must include:
|
|
||||||
target_shelf - shelf id of volume
|
|
||||||
target_lun - lun id of volume
|
|
||||||
"""
|
|
||||||
aoe_device, aoe_path = self._get_aoe_info(connection_properties)
|
|
||||||
|
|
||||||
device_info = {
|
|
||||||
'type': 'block',
|
|
||||||
'device': aoe_device,
|
|
||||||
'path': aoe_path,
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.path.exists(aoe_path):
|
|
||||||
self._aoe_revalidate(aoe_device)
|
|
||||||
else:
|
|
||||||
self._aoe_discover()
|
|
||||||
|
|
||||||
waiting_status = {'tries': 0}
|
|
||||||
|
|
||||||
# NOTE(jbr_): Device path is not always present immediately
|
|
||||||
def _wait_for_discovery(aoe_path):
|
|
||||||
if os.path.exists(aoe_path):
|
|
||||||
raise loopingcall.LoopingCallDone
|
|
||||||
|
|
||||||
if waiting_status['tries'] >= self.device_scan_attempts:
|
|
||||||
raise exception.VolumeDeviceNotFound(device=aoe_path)
|
|
||||||
|
|
||||||
LOG.info("AoE volume not yet found at: %(path)s. "
|
|
||||||
"Try number: %(tries)s",
|
|
||||||
{'path': aoe_device, 'tries': waiting_status['tries']})
|
|
||||||
|
|
||||||
self._aoe_discover()
|
|
||||||
waiting_status['tries'] += 1
|
|
||||||
|
|
||||||
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_discovery,
|
|
||||||
aoe_path)
|
|
||||||
timer.start(interval=2).wait()
|
|
||||||
|
|
||||||
if waiting_status['tries']:
|
|
||||||
LOG.debug("Found AoE device %(path)s "
|
|
||||||
"(after %(tries)s rediscover)",
|
|
||||||
{'path': aoe_path,
|
|
||||||
'tries': waiting_status['tries']})
|
|
||||||
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@lockutils.synchronized('aoe_control', 'aoe-')
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Detach and flush the volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
|
|
||||||
connection_properties for AoE must include:
|
|
||||||
target_shelf - shelf id of volume
|
|
||||||
target_lun - lun id of volume
|
|
||||||
"""
|
|
||||||
aoe_device, aoe_path = self._get_aoe_info(connection_properties)
|
|
||||||
|
|
||||||
if os.path.exists(aoe_path):
|
|
||||||
self._aoe_flush(aoe_device)
|
|
||||||
|
|
||||||
def _aoe_discover(self):
|
|
||||||
(out, err) = self._execute('aoe-discover',
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
check_exit_code=0)
|
|
||||||
|
|
||||||
LOG.debug('aoe-discover: stdout=%(out)s stderr%(err)s',
|
|
||||||
{'out': out, 'err': err})
|
|
||||||
|
|
||||||
def _aoe_revalidate(self, aoe_device):
|
|
||||||
(out, err) = self._execute('aoe-revalidate',
|
|
||||||
aoe_device,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
check_exit_code=0)
|
|
||||||
|
|
||||||
LOG.debug('aoe-revalidate %(dev)s: stdout=%(out)s stderr%(err)s',
|
|
||||||
{'dev': aoe_device, 'out': out, 'err': err})
|
|
||||||
|
|
||||||
def _aoe_flush(self, aoe_device):
|
|
||||||
(out, err) = self._execute('aoe-flush',
|
|
||||||
aoe_device,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
check_exit_code=0)
|
|
||||||
LOG.debug('aoe-flush %(dev)s: stdout=%(out)s stderr%(err)s',
|
|
||||||
{'dev': aoe_device, 'out': out, 'err': err})
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,128 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 glob
|
|
||||||
import os
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import initiator
|
|
||||||
|
|
||||||
from os_brick.initiator import host_driver
|
|
||||||
from os_brick.initiator import initiator_connector
|
|
||||||
from os_brick.initiator import linuxscsi
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLinuxConnector(initiator_connector.InitiatorConnector):
|
|
||||||
os_type = initiator.OS_TYPE_LINUX
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None, execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
self._linuxscsi = linuxscsi.LinuxSCSI(root_helper, execute=execute)
|
|
||||||
|
|
||||||
if not driver:
|
|
||||||
driver = host_driver.HostDriver()
|
|
||||||
self.set_driver(driver)
|
|
||||||
|
|
||||||
super(BaseLinuxConnector, self).__init__(root_helper, execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The generic connector properties."""
|
|
||||||
multipath = kwargs['multipath']
|
|
||||||
enforce_multipath = kwargs['enforce_multipath']
|
|
||||||
props = {}
|
|
||||||
|
|
||||||
props['multipath'] = (multipath and
|
|
||||||
linuxscsi.LinuxSCSI.is_multipath_running(
|
|
||||||
enforce_multipath, root_helper,
|
|
||||||
execute=kwargs.get('execute')))
|
|
||||||
|
|
||||||
return props
|
|
||||||
|
|
||||||
def check_valid_device(self, path, run_as_root=True):
|
|
||||||
cmd = ('dd', 'if=%(path)s' % {"path": path},
|
|
||||||
'of=/dev/null', 'count=1')
|
|
||||||
out, info = None, None
|
|
||||||
try:
|
|
||||||
out, info = self._execute(*cmd, run_as_root=run_as_root,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
LOG.error("Failed to access the device on the path "
|
|
||||||
"%(path)s: %(error)s.",
|
|
||||||
{"path": path, "error": e.stderr})
|
|
||||||
return False
|
|
||||||
# If the info is none, the path does not exist.
|
|
||||||
if info is None:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
volumes = []
|
|
||||||
path = self.get_search_path()
|
|
||||||
if path:
|
|
||||||
# now find all entries in the search path
|
|
||||||
if os.path.isdir(path):
|
|
||||||
path_items = [path, '/*']
|
|
||||||
file_filter = ''.join(path_items)
|
|
||||||
volumes = glob.glob(file_filter)
|
|
||||||
|
|
||||||
return volumes
|
|
||||||
|
|
||||||
def _discover_mpath_device(self, device_wwn, connection_properties,
|
|
||||||
device_name):
|
|
||||||
"""This method discovers a multipath device.
|
|
||||||
|
|
||||||
Discover a multipath device based on a defined connection_property
|
|
||||||
and a device_wwn and return the multipath_id and path of the multipath
|
|
||||||
enabled device if there is one.
|
|
||||||
"""
|
|
||||||
|
|
||||||
path = self._linuxscsi.find_multipath_device_path(device_wwn)
|
|
||||||
device_path = None
|
|
||||||
multipath_id = None
|
|
||||||
|
|
||||||
if path is None:
|
|
||||||
# find_multipath_device only accept realpath not symbolic path
|
|
||||||
device_realpath = os.path.realpath(device_name)
|
|
||||||
mpath_info = self._linuxscsi.find_multipath_device(
|
|
||||||
device_realpath)
|
|
||||||
if mpath_info:
|
|
||||||
device_path = mpath_info['device']
|
|
||||||
multipath_id = device_wwn
|
|
||||||
else:
|
|
||||||
# we didn't find a multipath device.
|
|
||||||
# so we assume the kernel only sees 1 device
|
|
||||||
device_path = device_name
|
|
||||||
LOG.debug("Unable to find multipath device name for "
|
|
||||||
"volume. Using path %(device)s for volume.",
|
|
||||||
{'device': device_path})
|
|
||||||
else:
|
|
||||||
device_path = path
|
|
||||||
multipath_id = device_wwn
|
|
||||||
if connection_properties.get('access_mode', '') != 'ro':
|
|
||||||
try:
|
|
||||||
# Sometimes the multipath devices will show up as read only
|
|
||||||
# initially and need additional time/rescans to get to RW.
|
|
||||||
self._linuxscsi.wait_for_rw(device_wwn, device_path)
|
|
||||||
except exception.BlockDeviceReadOnly:
|
|
||||||
LOG.warning('Block device %s is still read-only. '
|
|
||||||
'Continuing anyway.', device_path)
|
|
||||||
return device_path, multipath_id
|
|
|
@ -1,42 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 copy
|
|
||||||
|
|
||||||
from os_brick.initiator import initiator_connector
|
|
||||||
|
|
||||||
|
|
||||||
class BaseISCSIConnector(initiator_connector.InitiatorConnector):
|
|
||||||
def _iterate_all_targets(self, connection_properties):
|
|
||||||
for portal, iqn, lun in self._get_all_targets(connection_properties):
|
|
||||||
props = copy.deepcopy(connection_properties)
|
|
||||||
props['target_portal'] = portal
|
|
||||||
props['target_iqn'] = iqn
|
|
||||||
props['target_lun'] = lun
|
|
||||||
for key in ('target_portals', 'target_iqns', 'target_luns'):
|
|
||||||
props.pop(key, None)
|
|
||||||
yield props
|
|
||||||
|
|
||||||
def _get_all_targets(self, connection_properties):
|
|
||||||
if all([key in connection_properties for key in ('target_portals',
|
|
||||||
'target_iqns',
|
|
||||||
'target_luns')]):
|
|
||||||
return zip(connection_properties['target_portals'],
|
|
||||||
connection_properties['target_iqns'],
|
|
||||||
connection_properties['target_luns'])
|
|
||||||
|
|
||||||
return [(connection_properties['target_portal'],
|
|
||||||
connection_properties['target_iqn'],
|
|
||||||
connection_properties.get('target_lun', 0))]
|
|
|
@ -1,208 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 glob
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import six
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
|
|
||||||
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
|
||||||
|
|
||||||
|
|
||||||
class DISCOConnector(base.BaseLinuxConnector):
|
|
||||||
"""Class implements the connector driver for DISCO."""
|
|
||||||
|
|
||||||
DISCO_PREFIX = 'dms'
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
"""Init DISCO connector."""
|
|
||||||
super(DISCOConnector, self).__init__(
|
|
||||||
root_helper,
|
|
||||||
driver=driver,
|
|
||||||
device_scan_attempts=device_scan_attempts,
|
|
||||||
*args, **kwargs
|
|
||||||
)
|
|
||||||
LOG.debug("Init DISCO connector")
|
|
||||||
|
|
||||||
self.server_port = None
|
|
||||||
self.server_ip = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The DISCO connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
"""Get directory path where to get DISCO volumes."""
|
|
||||||
return "/dev"
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
"""Get config for DISCO volume driver."""
|
|
||||||
self.get_config(connection_properties)
|
|
||||||
volume_paths = []
|
|
||||||
disco_id = connection_properties['disco_id']
|
|
||||||
disco_dev = '/dev/dms%s' % (disco_id)
|
|
||||||
device_paths = [disco_dev]
|
|
||||||
for path in device_paths:
|
|
||||||
if os.path.exists(path):
|
|
||||||
volume_paths.append(path)
|
|
||||||
return volume_paths
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
"""Return all DISCO volumes that exist in the search directory."""
|
|
||||||
path = self.get_search_path()
|
|
||||||
|
|
||||||
if os.path.isdir(path):
|
|
||||||
path_items = [path, '/', self.DISCO_PREFIX, '*']
|
|
||||||
file_filter = ''.join(path_items)
|
|
||||||
return glob.glob(file_filter)
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_config(self, connection_properties):
|
|
||||||
"""Get config for DISCO volume driver."""
|
|
||||||
self.server_port = (
|
|
||||||
six.text_type(connection_properties['conf']['server_port']))
|
|
||||||
self.server_ip = (
|
|
||||||
six.text_type(connection_properties['conf']['server_ip']))
|
|
||||||
|
|
||||||
disco_id = connection_properties['disco_id']
|
|
||||||
disco_dev = '/dev/dms%s' % (disco_id)
|
|
||||||
device_info = {'type': 'block',
|
|
||||||
'path': disco_dev}
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect the volume. Returns xml for libvirt."""
|
|
||||||
LOG.debug("Enter in DISCO connect_volume")
|
|
||||||
device_info = self.get_config(connection_properties)
|
|
||||||
LOG.debug("Device info : %s.", device_info)
|
|
||||||
disco_id = connection_properties['disco_id']
|
|
||||||
disco_dev = '/dev/dms%s' % (disco_id)
|
|
||||||
LOG.debug("Attaching %s", disco_dev)
|
|
||||||
|
|
||||||
self._mount_disco_volume(disco_dev, disco_id)
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Detach the volume from instance."""
|
|
||||||
disco_id = connection_properties['disco_id']
|
|
||||||
disco_dev = '/dev/dms%s' % (disco_id)
|
|
||||||
LOG.debug("detaching %s", disco_dev)
|
|
||||||
|
|
||||||
if os.path.exists(disco_dev):
|
|
||||||
ret = self._send_disco_vol_cmd(self.server_ip,
|
|
||||||
self.server_port,
|
|
||||||
2,
|
|
||||||
disco_id)
|
|
||||||
if ret is not None:
|
|
||||||
msg = _("Detach volume failed")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
else:
|
|
||||||
LOG.info("Volume already detached from host")
|
|
||||||
|
|
||||||
def _mount_disco_volume(self, path, volume_id):
|
|
||||||
"""Send request to mount volume on physical host."""
|
|
||||||
LOG.debug("Enter in mount disco volume %(port)s "
|
|
||||||
"and %(ip)s.",
|
|
||||||
{'port': self.server_port,
|
|
||||||
'ip': self.server_ip})
|
|
||||||
|
|
||||||
if not os.path.exists(path):
|
|
||||||
ret = self._send_disco_vol_cmd(self.server_ip,
|
|
||||||
self.server_port,
|
|
||||||
1,
|
|
||||||
volume_id)
|
|
||||||
if ret is not None:
|
|
||||||
msg = _("Attach volume failed")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
else:
|
|
||||||
LOG.info("Volume already attached to host")
|
|
||||||
|
|
||||||
def _connect_tcp_socket(self, client_ip, client_port):
|
|
||||||
"""Connect to TCP socket."""
|
|
||||||
sock = None
|
|
||||||
|
|
||||||
for res in socket.getaddrinfo(client_ip,
|
|
||||||
client_port,
|
|
||||||
socket.AF_UNSPEC,
|
|
||||||
socket.SOCK_STREAM):
|
|
||||||
aff, socktype, proto, canonname, saa = res
|
|
||||||
try:
|
|
||||||
sock = socket.socket(aff, socktype, proto)
|
|
||||||
except socket.error:
|
|
||||||
sock = None
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
sock.connect(saa)
|
|
||||||
except socket.error:
|
|
||||||
sock.close()
|
|
||||||
sock = None
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
if sock is None:
|
|
||||||
LOG.error("Cannot connect TCP socket")
|
|
||||||
return sock
|
|
||||||
|
|
||||||
def _send_disco_vol_cmd(self, client_ip, client_port, op_code, vol_id):
|
|
||||||
"""Send DISCO client socket command."""
|
|
||||||
s = self._connect_tcp_socket(client_ip, int(client_port))
|
|
||||||
|
|
||||||
if s is not None:
|
|
||||||
inst_id = 'DEFAULT-INSTID'
|
|
||||||
pktlen = 2 + 8 + len(inst_id)
|
|
||||||
LOG.debug("pktlen=%(plen)s op=%(op)s "
|
|
||||||
"vol_id=%(vol_id)s, inst_id=%(inst_id)s",
|
|
||||||
{'plen': pktlen, 'op': op_code,
|
|
||||||
'vol_id': vol_id, 'inst_id': inst_id})
|
|
||||||
data = struct.pack("!HHQ14s",
|
|
||||||
pktlen,
|
|
||||||
op_code,
|
|
||||||
int(vol_id),
|
|
||||||
inst_id)
|
|
||||||
s.sendall(data)
|
|
||||||
ret = s.recv(4)
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
LOG.debug("Received ret len=%(lenR)d, ret=%(ret)s",
|
|
||||||
{'lenR': len(repr(ret)), 'ret': repr(ret)})
|
|
||||||
|
|
||||||
ret_val = "".join("%02x" % ord(c) for c in ret)
|
|
||||||
|
|
||||||
if ret_val != '00000000':
|
|
||||||
return 'ERROR'
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,110 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 tempfile
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
|
|
||||||
class DRBDConnector(base.BaseLinuxConnector):
|
|
||||||
""""Connector class to attach/detach DRBD resources."""
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
execute=putils.execute, *args, **kwargs):
|
|
||||||
|
|
||||||
super(DRBDConnector, self).__init__(root_helper, driver=driver,
|
|
||||||
execute=execute, *args, **kwargs)
|
|
||||||
|
|
||||||
self._execute = execute
|
|
||||||
self._root_helper = root_helper
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The DRBD connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def check_valid_device(self, path, run_as_root=True):
|
|
||||||
"""Verify an existing volume."""
|
|
||||||
# TODO(linbit): check via drbdsetup first, to avoid blocking/hanging
|
|
||||||
# in case of network problems?
|
|
||||||
|
|
||||||
return super(DRBDConnector, self).check_valid_device(path, run_as_root)
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
|
|
||||||
base = "/dev/"
|
|
||||||
blkdev_list = []
|
|
||||||
|
|
||||||
for e in os.listdir(base):
|
|
||||||
path = base + e
|
|
||||||
if os.path.isblk(path):
|
|
||||||
blkdev_list.append(path)
|
|
||||||
|
|
||||||
return blkdev_list
|
|
||||||
|
|
||||||
def _drbdadm_command(self, cmd, data_dict, sh_secret):
|
|
||||||
# TODO(linbit): Write that resource file to a permanent location?
|
|
||||||
tmp = tempfile.NamedTemporaryFile(suffix="res", delete=False, mode="w")
|
|
||||||
try:
|
|
||||||
kv = {'shared-secret': sh_secret}
|
|
||||||
tmp.write(data_dict['config'] % kv)
|
|
||||||
tmp.close()
|
|
||||||
|
|
||||||
(out, err) = self._execute('drbdadm', cmd,
|
|
||||||
"-c", tmp.name,
|
|
||||||
data_dict['name'],
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
finally:
|
|
||||||
os.unlink(tmp.name)
|
|
||||||
|
|
||||||
return (out, err)
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Attach the volume."""
|
|
||||||
|
|
||||||
self._drbdadm_command("adjust", connection_properties,
|
|
||||||
connection_properties['provider_auth'])
|
|
||||||
|
|
||||||
device_info = {
|
|
||||||
'type': 'block',
|
|
||||||
'path': connection_properties['device'],
|
|
||||||
}
|
|
||||||
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Detach the volume."""
|
|
||||||
|
|
||||||
self._drbdadm_command("down", connection_properties,
|
|
||||||
connection_properties['provider_auth'])
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
path = connection_properties['device']
|
|
||||||
return [path]
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
# TODO(linbit): is it allowed to return "/dev", or is that too broad?
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,49 +0,0 @@
|
||||||
# Copyright 2013 OpenStack Foundation.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_brick.initiator.connectors import base
|
|
||||||
from os_brick.initiator.connectors import base_iscsi
|
|
||||||
|
|
||||||
|
|
||||||
class FakeConnector(base.BaseLinuxConnector):
|
|
||||||
|
|
||||||
fake_path = '/dev/vdFAKE'
|
|
||||||
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
fake_device_info = {'type': 'fake',
|
|
||||||
'path': self.fake_path}
|
|
||||||
return fake_device_info
|
|
||||||
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
return [self.fake_path]
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return '/dev/disk/by-path'
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
return ['/dev/disk/by-path/fake-volume-1',
|
|
||||||
'/dev/disk/by-path/fake-volume-X']
|
|
||||||
|
|
||||||
|
|
||||||
class FakeBaseISCSIConnector(FakeConnector, base_iscsi.BaseISCSIConnector):
|
|
||||||
pass
|
|
|
@ -1,298 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_service import loopingcall
|
|
||||||
import six
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick.initiator import linuxfc
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FibreChannelConnector(base.BaseLinuxConnector):
|
|
||||||
"""Connector class to attach/detach Fibre Channel volumes."""
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
execute=None, use_multipath=False,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
self._linuxfc = linuxfc.LinuxFibreChannel(root_helper, execute)
|
|
||||||
super(FibreChannelConnector, self).__init__(
|
|
||||||
root_helper, driver=driver,
|
|
||||||
execute=execute,
|
|
||||||
device_scan_attempts=device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
self.use_multipath = use_multipath
|
|
||||||
|
|
||||||
def set_execute(self, execute):
|
|
||||||
super(FibreChannelConnector, self).set_execute(execute)
|
|
||||||
self._linuxscsi.set_execute(execute)
|
|
||||||
self._linuxfc.set_execute(execute)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The Fibre Channel connector properties."""
|
|
||||||
props = {}
|
|
||||||
fc = linuxfc.LinuxFibreChannel(root_helper,
|
|
||||||
execute=kwargs.get('execute'))
|
|
||||||
|
|
||||||
wwpns = fc.get_fc_wwpns()
|
|
||||||
if wwpns:
|
|
||||||
props['wwpns'] = wwpns
|
|
||||||
wwnns = fc.get_fc_wwnns()
|
|
||||||
if wwnns:
|
|
||||||
props['wwnns'] = wwnns
|
|
||||||
|
|
||||||
return props
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
"""Where do we look for FC based volumes."""
|
|
||||||
return '/dev/disk/by-path'
|
|
||||||
|
|
||||||
def _get_possible_volume_paths(self, connection_properties, hbas):
|
|
||||||
ports = connection_properties['target_wwn']
|
|
||||||
possible_devs = self._get_possible_devices(hbas, ports)
|
|
||||||
|
|
||||||
lun = connection_properties.get('target_lun', 0)
|
|
||||||
host_paths = self._get_host_devices(possible_devs, lun)
|
|
||||||
return host_paths
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
volume_paths = []
|
|
||||||
# first fetch all of the potential paths that might exist
|
|
||||||
# how the FC fabric is zoned may alter the actual list
|
|
||||||
# that shows up on the system. So, we verify each path.
|
|
||||||
hbas = self._linuxfc.get_fc_hbas_info()
|
|
||||||
device_paths = self._get_possible_volume_paths(
|
|
||||||
connection_properties, hbas)
|
|
||||||
for path in device_paths:
|
|
||||||
if os.path.exists(path):
|
|
||||||
volume_paths.append(path)
|
|
||||||
|
|
||||||
return volume_paths
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('extend_volume')
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
"""Update the local kernel's size information.
|
|
||||||
|
|
||||||
Try and update the local kernel's size information
|
|
||||||
for an FC volume.
|
|
||||||
"""
|
|
||||||
volume_paths = self.get_volume_paths(connection_properties)
|
|
||||||
if volume_paths:
|
|
||||||
return self._linuxscsi.extend_volume(volume_paths)
|
|
||||||
else:
|
|
||||||
LOG.warning("Couldn't find any volume paths on the host to "
|
|
||||||
"extend volume for %(props)s",
|
|
||||||
{'props': connection_properties})
|
|
||||||
raise exception.VolumePathsNotFound()
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Attach the volume to instance_name.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
|
|
||||||
connection_properties for Fibre Channel must include:
|
|
||||||
target_wwn - World Wide Name
|
|
||||||
target_lun - LUN id of the volume
|
|
||||||
"""
|
|
||||||
LOG.debug("execute = %s", self._execute)
|
|
||||||
device_info = {'type': 'block'}
|
|
||||||
|
|
||||||
hbas = self._linuxfc.get_fc_hbas_info()
|
|
||||||
host_devices = self._get_possible_volume_paths(
|
|
||||||
connection_properties, hbas)
|
|
||||||
|
|
||||||
if len(host_devices) == 0:
|
|
||||||
# this is empty because we don't have any FC HBAs
|
|
||||||
LOG.warning("We are unable to locate any Fibre Channel devices")
|
|
||||||
raise exception.NoFibreChannelHostsFound()
|
|
||||||
|
|
||||||
# The /dev/disk/by-path/... node is not always present immediately
|
|
||||||
# We only need to find the first device. Once we see the first device
|
|
||||||
# multipath will have any others.
|
|
||||||
def _wait_for_device_discovery(host_devices):
|
|
||||||
tries = self.tries
|
|
||||||
for device in host_devices:
|
|
||||||
LOG.debug("Looking for Fibre Channel dev %(device)s",
|
|
||||||
{'device': device})
|
|
||||||
if os.path.exists(device) and self.check_valid_device(device):
|
|
||||||
self.host_device = device
|
|
||||||
# get the /dev/sdX device. This is used
|
|
||||||
# to find the multipath device.
|
|
||||||
self.device_name = os.path.realpath(device)
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
|
|
||||||
if self.tries >= self.device_scan_attempts:
|
|
||||||
LOG.error("Fibre Channel volume device not found.")
|
|
||||||
raise exception.NoFibreChannelVolumeDeviceFound()
|
|
||||||
|
|
||||||
LOG.info("Fibre Channel volume device not yet found. "
|
|
||||||
"Will rescan & retry. Try number: %(tries)s.",
|
|
||||||
{'tries': tries})
|
|
||||||
|
|
||||||
self._linuxfc.rescan_hosts(hbas,
|
|
||||||
connection_properties['target_lun'])
|
|
||||||
self.tries = self.tries + 1
|
|
||||||
|
|
||||||
self.host_device = None
|
|
||||||
self.device_name = None
|
|
||||||
self.tries = 0
|
|
||||||
timer = loopingcall.FixedIntervalLoopingCall(
|
|
||||||
_wait_for_device_discovery, host_devices)
|
|
||||||
timer.start(interval=2).wait()
|
|
||||||
|
|
||||||
tries = self.tries
|
|
||||||
if self.host_device is not None and self.device_name is not None:
|
|
||||||
LOG.debug("Found Fibre Channel volume %(name)s "
|
|
||||||
"(after %(tries)s rescans)",
|
|
||||||
{'name': self.device_name, 'tries': tries})
|
|
||||||
|
|
||||||
# find out the WWN of the device
|
|
||||||
device_wwn = self._linuxscsi.get_scsi_wwn(self.host_device)
|
|
||||||
LOG.debug("Device WWN = '%(wwn)s'", {'wwn': device_wwn})
|
|
||||||
device_info['scsi_wwn'] = device_wwn
|
|
||||||
|
|
||||||
# see if the new drive is part of a multipath
|
|
||||||
# device. If so, we'll use the multipath device.
|
|
||||||
if self.use_multipath:
|
|
||||||
(device_path, multipath_id) = (super(
|
|
||||||
FibreChannelConnector, self)._discover_mpath_device(
|
|
||||||
device_wwn, connection_properties, self.device_name))
|
|
||||||
if multipath_id:
|
|
||||||
# only set the multipath_id if we found one
|
|
||||||
device_info['multipath_id'] = multipath_id
|
|
||||||
|
|
||||||
else:
|
|
||||||
device_path = self.host_device
|
|
||||||
|
|
||||||
device_info['path'] = device_path
|
|
||||||
LOG.debug("connect_volume returning %s", device_info)
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
def _get_host_devices(self, possible_devs, lun):
|
|
||||||
host_devices = []
|
|
||||||
for pci_num, target_wwn in possible_devs:
|
|
||||||
host_device = "/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % (
|
|
||||||
pci_num,
|
|
||||||
target_wwn,
|
|
||||||
self._linuxscsi.process_lun_id(lun))
|
|
||||||
host_devices.append(host_device)
|
|
||||||
return host_devices
|
|
||||||
|
|
||||||
def _get_possible_devices(self, hbas, wwnports):
|
|
||||||
"""Compute the possible fibre channel device options.
|
|
||||||
|
|
||||||
:param hbas: available hba devices.
|
|
||||||
:param wwnports: possible wwn addresses. Can either be string
|
|
||||||
or list of strings.
|
|
||||||
|
|
||||||
:returns: list of (pci_id, wwn) tuples
|
|
||||||
|
|
||||||
Given one or more wwn (mac addresses for fibre channel) ports
|
|
||||||
do the matrix math to figure out a set of pci device, wwn
|
|
||||||
tuples that are potentially valid (they won't all be). This
|
|
||||||
provides a search space for the device connection.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# the wwn (think mac addresses for fiber channel devices) can
|
|
||||||
# either be a single value or a list. Normalize it to a list
|
|
||||||
# for further operations.
|
|
||||||
wwns = []
|
|
||||||
if isinstance(wwnports, list):
|
|
||||||
for wwn in wwnports:
|
|
||||||
wwns.append(str(wwn))
|
|
||||||
elif isinstance(wwnports, six.string_types):
|
|
||||||
wwns.append(str(wwnports))
|
|
||||||
|
|
||||||
raw_devices = []
|
|
||||||
for hba in hbas:
|
|
||||||
pci_num = self._get_pci_num(hba)
|
|
||||||
if pci_num is not None:
|
|
||||||
for wwn in wwns:
|
|
||||||
target_wwn = "0x%s" % wwn.lower()
|
|
||||||
raw_devices.append((pci_num, target_wwn))
|
|
||||||
return raw_devices
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Detach the volume from instance_name.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
|
|
||||||
connection_properties for Fibre Channel must include:
|
|
||||||
target_wwn - World Wide Name
|
|
||||||
target_lun - LUN id of the volume
|
|
||||||
"""
|
|
||||||
|
|
||||||
devices = []
|
|
||||||
wwn = None
|
|
||||||
volume_paths = self.get_volume_paths(connection_properties)
|
|
||||||
mpath_path = None
|
|
||||||
for path in volume_paths:
|
|
||||||
real_path = self._linuxscsi.get_name_from_path(path)
|
|
||||||
if self.use_multipath and not mpath_path:
|
|
||||||
wwn = self._linuxscsi.get_scsi_wwn(path)
|
|
||||||
mpath_path = self._linuxscsi.find_multipath_device_path(wwn)
|
|
||||||
if mpath_path:
|
|
||||||
self._linuxscsi.flush_multipath_device(mpath_path)
|
|
||||||
device_info = self._linuxscsi.get_device_info(real_path)
|
|
||||||
devices.append(device_info)
|
|
||||||
|
|
||||||
LOG.debug("devices to remove = %s", devices)
|
|
||||||
self._remove_devices(connection_properties, devices)
|
|
||||||
|
|
||||||
def _remove_devices(self, connection_properties, devices):
|
|
||||||
# There may have been more than 1 device mounted
|
|
||||||
# by the kernel for this volume. We have to remove
|
|
||||||
# all of them
|
|
||||||
for device in devices:
|
|
||||||
self._linuxscsi.remove_scsi_device(device["device"])
|
|
||||||
|
|
||||||
def _get_pci_num(self, hba):
|
|
||||||
# NOTE(walter-boring)
|
|
||||||
# device path is in format of (FC and FCoE) :
|
|
||||||
# /sys/devices/pci0000:00/0000:00:03.0/0000:05:00.3/host2/fc_host/host2
|
|
||||||
# /sys/devices/pci0000:20/0000:20:03.0/0000:21:00.2/net/ens2f2/ctlr_2
|
|
||||||
# /host3/fc_host/host3
|
|
||||||
# we always want the value prior to the host or net value
|
|
||||||
if hba is not None:
|
|
||||||
if "device_path" in hba:
|
|
||||||
device_path = hba['device_path'].split('/')
|
|
||||||
for index, value in enumerate(device_path):
|
|
||||||
if value.startswith('net') or value.startswith('host'):
|
|
||||||
return device_path[index - 1]
|
|
||||||
return None
|
|
|
@ -1,65 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import fibre_channel
|
|
||||||
from os_brick.initiator import linuxfc
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FibreChannelConnectorPPC64(fibre_channel.FibreChannelConnector):
|
|
||||||
"""Connector class to attach/detach Fibre Channel volumes on PPC64 arch."""
|
|
||||||
|
|
||||||
platform = initiator.PLATFORM_PPC64
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
execute=None, use_multipath=False,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(FibreChannelConnectorPPC64, self).__init__(
|
|
||||||
root_helper,
|
|
||||||
driver=driver,
|
|
||||||
execute=execute,
|
|
||||||
device_scan_attempts=device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
self._linuxfc = linuxfc.LinuxFibreChannelPPC64(root_helper, execute)
|
|
||||||
self.use_multipath = use_multipath
|
|
||||||
|
|
||||||
def set_execute(self, execute):
|
|
||||||
super(FibreChannelConnectorPPC64, self).set_execute(execute)
|
|
||||||
self._linuxscsi.set_execute(execute)
|
|
||||||
self._linuxfc.set_execute(execute)
|
|
||||||
|
|
||||||
def _get_host_devices(self, possible_devs, lun):
|
|
||||||
host_devices = []
|
|
||||||
for pci_num, target_wwn in possible_devs:
|
|
||||||
host_device = "/dev/disk/by-path/fc-%s-lun-%s" % (
|
|
||||||
target_wwn,
|
|
||||||
self._linuxscsi.process_lun_id(lun))
|
|
||||||
host_devices.append(host_device)
|
|
||||||
return host_devices
|
|
||||||
|
|
||||||
def _get_possible_volume_paths(self, connection_properties, hbas):
|
|
||||||
ports = connection_properties['target_wwn']
|
|
||||||
it_map = connection_properties['initiator_target_map']
|
|
||||||
for hba in hbas:
|
|
||||||
if hba['node_name'] in it_map.keys():
|
|
||||||
hba['target_wwn'] = it_map.get(hba['node_name'])
|
|
||||||
possible_devs = self._get_possible_devices(hbas, ports)
|
|
||||||
lun = connection_properties.get('target_lun', 0)
|
|
||||||
host_paths = self._get_host_devices(possible_devs, lun)
|
|
||||||
return host_paths
|
|
|
@ -1,94 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import initiator
|
|
||||||
|
|
||||||
from os_brick.initiator.connectors import fibre_channel
|
|
||||||
from os_brick.initiator import linuxfc
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FibreChannelConnectorS390X(fibre_channel.FibreChannelConnector):
|
|
||||||
"""Connector class to attach/detach Fibre Channel volumes on S390X arch."""
|
|
||||||
|
|
||||||
platform = initiator.PLATFORM_S390
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
execute=None, use_multipath=False,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(FibreChannelConnectorS390X, self).__init__(
|
|
||||||
root_helper,
|
|
||||||
driver=driver,
|
|
||||||
execute=execute,
|
|
||||||
device_scan_attempts=device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
LOG.debug("Initializing Fibre Channel connector for S390")
|
|
||||||
self._linuxfc = linuxfc.LinuxFibreChannelS390X(root_helper, execute)
|
|
||||||
self.use_multipath = use_multipath
|
|
||||||
|
|
||||||
def set_execute(self, execute):
|
|
||||||
super(FibreChannelConnectorS390X, self).set_execute(execute)
|
|
||||||
self._linuxscsi.set_execute(execute)
|
|
||||||
self._linuxfc.set_execute(execute)
|
|
||||||
|
|
||||||
def _get_host_devices(self, possible_devs, lun):
|
|
||||||
host_devices = []
|
|
||||||
for pci_num, target_wwn in possible_devs:
|
|
||||||
host_device = self._get_device_file_path(
|
|
||||||
pci_num,
|
|
||||||
target_wwn,
|
|
||||||
lun)
|
|
||||||
# NOTE(arne_r)
|
|
||||||
# LUN driver path is the same on all distros, so no need to have
|
|
||||||
# multiple calls here
|
|
||||||
self._linuxfc.configure_scsi_device(pci_num, target_wwn,
|
|
||||||
self._get_lun_string(lun))
|
|
||||||
host_devices.extend(host_device)
|
|
||||||
return host_devices
|
|
||||||
|
|
||||||
def _get_lun_string(self, lun):
|
|
||||||
target_lun = 0
|
|
||||||
if lun <= 0xffff:
|
|
||||||
target_lun = "0x%04x000000000000" % lun
|
|
||||||
elif lun <= 0xffffffff:
|
|
||||||
target_lun = "0x%08x00000000" % lun
|
|
||||||
return target_lun
|
|
||||||
|
|
||||||
def _get_device_file_path(self, pci_num, target_wwn, lun):
|
|
||||||
# NOTE(arne_r)
|
|
||||||
# Need to add two possible ways to resolve device paths,
|
|
||||||
# depending on OS. Since it gets passed to '_get_possible_volume_paths'
|
|
||||||
# having a mismatch is not a problem
|
|
||||||
host_device = [
|
|
||||||
"/dev/disk/by-path/ccw-%s-zfcp-%s:%s" % (
|
|
||||||
pci_num, target_wwn, self._get_lun_string(lun)),
|
|
||||||
"/dev/disk/by-path/ccw-%s-fc-%s-lun-%s" % (
|
|
||||||
pci_num, target_wwn, lun),
|
|
||||||
]
|
|
||||||
return host_device
|
|
||||||
|
|
||||||
def _remove_devices(self, connection_properties, devices):
|
|
||||||
hbas = self._linuxfc.get_fc_hbas_info()
|
|
||||||
ports = connection_properties['target_wwn']
|
|
||||||
possible_devs = self._get_possible_devices(hbas, ports)
|
|
||||||
lun = connection_properties.get('target_lun', 0)
|
|
||||||
target_lun = self._get_lun_string(lun)
|
|
||||||
for pci_num, target_wwn in possible_devs:
|
|
||||||
self._linuxfc.deconfigure_scsi_device(pci_num,
|
|
||||||
target_wwn,
|
|
||||||
target_lun)
|
|
|
@ -1,41 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_brick.i18n import _
|
|
||||||
from os_brick.initiator.connectors import local
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
|
|
||||||
class GPFSConnector(local.LocalConnector):
|
|
||||||
""""Connector class to attach/detach File System backed volumes."""
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect to a volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
connection_properties must include:
|
|
||||||
device_path - path to the volume to be connected
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
if 'device_path' not in connection_properties:
|
|
||||||
msg = (_("Invalid connection_properties specified "
|
|
||||||
"no device_path attribute."))
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
device_info = {'type': 'gpfs',
|
|
||||||
'path': connection_properties['device_path']}
|
|
||||||
return device_info
|
|
|
@ -1,183 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 socket
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HGSTConnector(base.BaseLinuxConnector):
|
|
||||||
"""Connector class to attach/detach HGST volumes."""
|
|
||||||
|
|
||||||
VGCCLUSTER = 'vgc-cluster'
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(HGSTConnector, self).__init__(root_helper, driver=driver,
|
|
||||||
device_scan_attempts=
|
|
||||||
device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
self._vgc_host = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The HGST connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _log_cli_err(self, err):
|
|
||||||
"""Dumps the full command output to a logfile in error cases."""
|
|
||||||
LOG.error("CLI fail: '%(cmd)s' = %(code)s\nout: %(stdout)s\n"
|
|
||||||
"err: %(stderr)s",
|
|
||||||
{'cmd': err.cmd, 'code': err.exit_code,
|
|
||||||
'stdout': err.stdout, 'stderr': err.stderr})
|
|
||||||
|
|
||||||
def _find_vgc_host(self):
|
|
||||||
"""Finds vgc-cluster hostname for this box."""
|
|
||||||
params = [self.VGCCLUSTER, "domain-list", "-1"]
|
|
||||||
try:
|
|
||||||
out, unused = self._execute(*params, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
self._log_cli_err(err)
|
|
||||||
msg = _("Unable to get list of domain members, check that "
|
|
||||||
"the cluster is running.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
domain = out.splitlines()
|
|
||||||
params = ["ip", "addr", "list"]
|
|
||||||
try:
|
|
||||||
out, unused = self._execute(*params, run_as_root=False)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
self._log_cli_err(err)
|
|
||||||
msg = _("Unable to get list of IP addresses on this host, "
|
|
||||||
"check permissions and networking.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
nets = out.splitlines()
|
|
||||||
for host in domain:
|
|
||||||
try:
|
|
||||||
ip = socket.gethostbyname(host)
|
|
||||||
for l in nets:
|
|
||||||
x = l.strip()
|
|
||||||
if x.startswith("inet %s/" % ip):
|
|
||||||
return host
|
|
||||||
except socket.error:
|
|
||||||
pass
|
|
||||||
msg = _("Current host isn't part of HGST domain.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
def _hostname(self):
|
|
||||||
"""Returns hostname to use for cluster operations on this box."""
|
|
||||||
if self._vgc_host is None:
|
|
||||||
self._vgc_host = self._find_vgc_host()
|
|
||||||
return self._vgc_host
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return "/dev"
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
path = ("%(path)s/%(name)s" %
|
|
||||||
{'path': self.get_search_path(),
|
|
||||||
'name': connection_properties['name']})
|
|
||||||
volume_path = None
|
|
||||||
if os.path.exists(path):
|
|
||||||
volume_path = path
|
|
||||||
return [volume_path]
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Attach a Space volume to running host.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
connection_properties for HGST must include:
|
|
||||||
name - Name of space to attach
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
if connection_properties is None:
|
|
||||||
msg = _("Connection properties passed in as None.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
if 'name' not in connection_properties:
|
|
||||||
msg = _("Connection properties missing 'name' field.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
device_info = {
|
|
||||||
'type': 'block',
|
|
||||||
'device': connection_properties['name'],
|
|
||||||
'path': '/dev/' + connection_properties['name']
|
|
||||||
}
|
|
||||||
volname = device_info['device']
|
|
||||||
params = [self.VGCCLUSTER, 'space-set-apphosts']
|
|
||||||
params += ['-n', volname]
|
|
||||||
params += ['-A', self._hostname()]
|
|
||||||
params += ['--action', 'ADD']
|
|
||||||
try:
|
|
||||||
self._execute(*params, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
self._log_cli_err(err)
|
|
||||||
msg = (_("Unable to set apphost for space %s") % volname)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Detach and flush the volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
For HGST must include:
|
|
||||||
name - Name of space to detach
|
|
||||||
noremovehost - Host which should never be removed
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
"""
|
|
||||||
if connection_properties is None:
|
|
||||||
msg = _("Connection properties passed in as None.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
if 'name' not in connection_properties:
|
|
||||||
msg = _("Connection properties missing 'name' field.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
if 'noremovehost' not in connection_properties:
|
|
||||||
msg = _("Connection properties missing 'noremovehost' field.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
if connection_properties['noremovehost'] != self._hostname():
|
|
||||||
params = [self.VGCCLUSTER, 'space-set-apphosts']
|
|
||||||
params += ['-n', connection_properties['name']]
|
|
||||||
params += ['-A', self._hostname()]
|
|
||||||
params += ['--action', 'DELETE']
|
|
||||||
try:
|
|
||||||
self._execute(*params, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
self._log_cli_err(err)
|
|
||||||
msg = (_("Unable to set apphost for space %s") %
|
|
||||||
connection_properties['name'])
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,193 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
|
||||||
|
|
||||||
|
|
||||||
class HuaweiStorHyperConnector(base.BaseLinuxConnector):
|
|
||||||
""""Connector class to attach/detach SDSHypervisor volumes."""
|
|
||||||
|
|
||||||
attached_success_code = 0
|
|
||||||
has_been_attached_code = 50151401
|
|
||||||
attach_mnid_done_code = 50151405
|
|
||||||
vbs_unnormal_code = 50151209
|
|
||||||
not_mount_node_code = 50155007
|
|
||||||
iscliexist = True
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
self.cli_path = os.getenv('HUAWEISDSHYPERVISORCLI_PATH')
|
|
||||||
if not self.cli_path:
|
|
||||||
self.cli_path = '/usr/local/bin/sds/sds_cli'
|
|
||||||
LOG.debug("CLI path is not configured, using default %s.",
|
|
||||||
self.cli_path)
|
|
||||||
if not os.path.isfile(self.cli_path):
|
|
||||||
self.iscliexist = False
|
|
||||||
LOG.error('SDS CLI file not found, '
|
|
||||||
'HuaweiStorHyperConnector init failed.')
|
|
||||||
super(HuaweiStorHyperConnector, self).__init__(root_helper,
|
|
||||||
driver=driver,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The HuaweiStor connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
# TODO(walter-boring): Where is the location on the filesystem to
|
|
||||||
# look for Huawei volumes to show up?
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
# TODO(walter-boring): what to return here for all Huawei volumes ?
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
volume_path = None
|
|
||||||
try:
|
|
||||||
volume_path = self._get_volume_path(connection_properties)
|
|
||||||
except Exception:
|
|
||||||
msg = _("Couldn't find a volume.")
|
|
||||||
LOG.warning(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
return [volume_path]
|
|
||||||
|
|
||||||
def _get_volume_path(self, connection_properties):
|
|
||||||
out = self._query_attached_volume(
|
|
||||||
connection_properties['volume_id'])
|
|
||||||
if not out or int(out['ret_code']) != 0:
|
|
||||||
msg = _("Couldn't find attached volume.")
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
return out['dev_addr']
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect to a volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
LOG.debug("Connect_volume connection properties: %s.",
|
|
||||||
connection_properties)
|
|
||||||
out = self._attach_volume(connection_properties['volume_id'])
|
|
||||||
if not out or int(out['ret_code']) not in (self.attached_success_code,
|
|
||||||
self.has_been_attached_code,
|
|
||||||
self.attach_mnid_done_code):
|
|
||||||
msg = (_("Attach volume failed, "
|
|
||||||
"error code is %s") % out['ret_code'])
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
volume_path = self._get_volume_path(connection_properties)
|
|
||||||
except Exception:
|
|
||||||
msg = _("query attached volume failed or volume not attached.")
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
device_info = {'type': 'block',
|
|
||||||
'path': volume_path}
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Disconnect a volume from the local host.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
"""
|
|
||||||
LOG.debug("Disconnect_volume: %s.", connection_properties)
|
|
||||||
out = self._detach_volume(connection_properties['volume_id'])
|
|
||||||
if not out or int(out['ret_code']) not in (self.attached_success_code,
|
|
||||||
self.vbs_unnormal_code,
|
|
||||||
self.not_mount_node_code):
|
|
||||||
msg = (_("Disconnect_volume failed, "
|
|
||||||
"error code is %s") % out['ret_code'])
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
def is_volume_connected(self, volume_name):
|
|
||||||
"""Check if volume already connected to host"""
|
|
||||||
LOG.debug('Check if volume %s already connected to a host.',
|
|
||||||
volume_name)
|
|
||||||
out = self._query_attached_volume(volume_name)
|
|
||||||
if out:
|
|
||||||
return int(out['ret_code']) == 0
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _attach_volume(self, volume_name):
|
|
||||||
return self._cli_cmd('attach', volume_name)
|
|
||||||
|
|
||||||
def _detach_volume(self, volume_name):
|
|
||||||
return self._cli_cmd('detach', volume_name)
|
|
||||||
|
|
||||||
def _query_attached_volume(self, volume_name):
|
|
||||||
return self._cli_cmd('querydev', volume_name)
|
|
||||||
|
|
||||||
def _cli_cmd(self, method, volume_name):
|
|
||||||
LOG.debug("Enter into _cli_cmd.")
|
|
||||||
if not self.iscliexist:
|
|
||||||
msg = _("SDS command line doesn't exist, "
|
|
||||||
"can't execute SDS command.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
if not method or volume_name is None:
|
|
||||||
return
|
|
||||||
cmd = [self.cli_path, '-c', method, '-v', volume_name]
|
|
||||||
out, clilog = self._execute(*cmd, run_as_root=False,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
analyse_result = self._analyze_output(out)
|
|
||||||
LOG.debug('%(method)s volume returns %(analyse_result)s.',
|
|
||||||
{'method': method, 'analyse_result': analyse_result})
|
|
||||||
if clilog:
|
|
||||||
LOG.error("SDS CLI output some log: %s.", clilog)
|
|
||||||
return analyse_result
|
|
||||||
|
|
||||||
def _analyze_output(self, out):
|
|
||||||
LOG.debug("Enter into _analyze_output.")
|
|
||||||
if out:
|
|
||||||
analyse_result = {}
|
|
||||||
out_temp = out.split('\n')
|
|
||||||
for line in out_temp:
|
|
||||||
LOG.debug("Line is %s.", line)
|
|
||||||
if line.find('=') != -1:
|
|
||||||
key, val = line.split('=', 1)
|
|
||||||
LOG.debug("%(key)s = %(val)s", {'key': key, 'val': val})
|
|
||||||
if key in ['ret_code', 'ret_desc', 'dev_addr']:
|
|
||||||
analyse_result[key] = val
|
|
||||||
return analyse_result
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,79 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_brick.i18n import _
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
|
|
||||||
class LocalConnector(base.BaseLinuxConnector):
|
|
||||||
""""Connector class to attach/detach File System backed volumes."""
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(LocalConnector, self).__init__(root_helper, driver=driver,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The Local connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
path = connection_properties['device_path']
|
|
||||||
return [path]
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
# TODO(walter-boring): not sure what to return here.
|
|
||||||
return []
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect to a volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
connection_properties must include:
|
|
||||||
device_path - path to the volume to be connected
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
if 'device_path' not in connection_properties:
|
|
||||||
msg = (_("Invalid connection_properties specified "
|
|
||||||
"no device_path attribute"))
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
device_info = {'type': 'local',
|
|
||||||
'path': connection_properties['device_path']}
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Disconnect a volume from the local host.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,252 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 tempfile
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import fileutils
|
|
||||||
from oslo_utils import netutils
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick.initiator import linuxrbd
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RBDConnector(base.BaseLinuxConnector):
|
|
||||||
""""Connector class to attach/detach RBD volumes."""
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None, use_multipath=False,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
|
|
||||||
super(RBDConnector, self).__init__(root_helper, driver=driver,
|
|
||||||
device_scan_attempts=
|
|
||||||
device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
self.do_local_attach = kwargs.get('do_local_attach', False)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The RBD connector properties."""
|
|
||||||
return {'do_local_attach': kwargs.get('do_local_attach', False)}
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
# TODO(e0ne): Implement this for local volume.
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
# TODO(walter-boring): don't know where the connector
|
|
||||||
# looks for RBD volumes.
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
# TODO(e0ne): Implement this for local volume.
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _sanitize_mon_hosts(self, hosts):
|
|
||||||
def _sanitize_host(host):
|
|
||||||
if netutils.is_valid_ipv6(host):
|
|
||||||
host = '[%s]' % host
|
|
||||||
return host
|
|
||||||
return list(map(_sanitize_host, hosts))
|
|
||||||
|
|
||||||
def _check_or_get_keyring_contents(self, keyring, cluster_name, user):
|
|
||||||
try:
|
|
||||||
if keyring is None:
|
|
||||||
keyring_path = ("/etc/ceph/%s.client.%s.keyring" %
|
|
||||||
(cluster_name, user))
|
|
||||||
with open(keyring_path, 'r') as keyring_file:
|
|
||||||
keyring = keyring_file.read()
|
|
||||||
return keyring
|
|
||||||
except IOError:
|
|
||||||
msg = (_("Keyring path %s is not readable.") % (keyring_path))
|
|
||||||
raise exception.BrickException(msg=msg)
|
|
||||||
|
|
||||||
def _create_ceph_conf(self, monitor_ips, monitor_ports,
|
|
||||||
cluster_name, user, keyring):
|
|
||||||
monitors = ["%s:%s" % (ip, port) for ip, port in
|
|
||||||
zip(self._sanitize_mon_hosts(monitor_ips), monitor_ports)]
|
|
||||||
mon_hosts = "mon_host = %s" % (','.join(monitors))
|
|
||||||
|
|
||||||
keyring = self._check_or_get_keyring_contents(keyring, cluster_name,
|
|
||||||
user)
|
|
||||||
|
|
||||||
try:
|
|
||||||
fd, ceph_conf_path = tempfile.mkstemp(prefix="brickrbd_")
|
|
||||||
with os.fdopen(fd, 'w') as conf_file:
|
|
||||||
conf_file.writelines([mon_hosts, "\n", keyring, "\n"])
|
|
||||||
return ceph_conf_path
|
|
||||||
except IOError:
|
|
||||||
msg = (_("Failed to write data to %s.") % (ceph_conf_path))
|
|
||||||
raise exception.BrickException(msg=msg)
|
|
||||||
|
|
||||||
def _get_rbd_handle(self, connection_properties):
|
|
||||||
try:
|
|
||||||
user = connection_properties['auth_username']
|
|
||||||
pool, volume = connection_properties['name'].split('/')
|
|
||||||
cluster_name = connection_properties.get('cluster_name')
|
|
||||||
monitor_ips = connection_properties.get('hosts')
|
|
||||||
monitor_ports = connection_properties.get('ports')
|
|
||||||
keyring = connection_properties.get('keyring')
|
|
||||||
except IndexError:
|
|
||||||
msg = _("Connect volume failed, malformed connection properties")
|
|
||||||
raise exception.BrickException(msg=msg)
|
|
||||||
|
|
||||||
conf = self._create_ceph_conf(monitor_ips, monitor_ports,
|
|
||||||
str(cluster_name), user,
|
|
||||||
keyring)
|
|
||||||
try:
|
|
||||||
rbd_client = linuxrbd.RBDClient(user, pool, conffile=conf,
|
|
||||||
rbd_cluster_name=str(cluster_name))
|
|
||||||
rbd_volume = linuxrbd.RBDVolume(rbd_client, volume)
|
|
||||||
rbd_handle = linuxrbd.RBDVolumeIOWrapper(
|
|
||||||
linuxrbd.RBDImageMetadata(rbd_volume, pool, user, conf))
|
|
||||||
except Exception:
|
|
||||||
fileutils.delete_if_exists(conf)
|
|
||||||
raise
|
|
||||||
|
|
||||||
return rbd_handle
|
|
||||||
|
|
||||||
def _get_rbd_args(self, connection_properties):
|
|
||||||
try:
|
|
||||||
user = connection_properties['auth_username']
|
|
||||||
monitor_ips = connection_properties.get('hosts')
|
|
||||||
monitor_ports = connection_properties.get('ports')
|
|
||||||
except KeyError:
|
|
||||||
msg = _("Connect volume failed, malformed connection properties")
|
|
||||||
raise exception.BrickException(msg=msg)
|
|
||||||
|
|
||||||
args = ['--id', user]
|
|
||||||
if monitor_ips and monitor_ports:
|
|
||||||
monitors = ["%s:%s" % (ip, port) for ip, port in
|
|
||||||
zip(
|
|
||||||
self._sanitize_mon_hosts(monitor_ips),
|
|
||||||
monitor_ports)]
|
|
||||||
for monitor in monitors:
|
|
||||||
args += ['--mon_host', monitor]
|
|
||||||
return args
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_rbd_device_name(pool, volume):
|
|
||||||
"""Return device name which will be generated by RBD kernel module.
|
|
||||||
|
|
||||||
:param pool: RBD pool name.
|
|
||||||
:type pool: string
|
|
||||||
:param volume: RBD image name.
|
|
||||||
:type volume: string
|
|
||||||
"""
|
|
||||||
return '/dev/rbd/{pool}/{volume}'.format(pool=pool, volume=volume)
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect to a volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
do_local_attach = connection_properties.get('do_local_attach',
|
|
||||||
self.do_local_attach)
|
|
||||||
|
|
||||||
if do_local_attach:
|
|
||||||
# NOTE(e0ne): sanity check if ceph-common is installed.
|
|
||||||
cmd = ['which', 'rbd']
|
|
||||||
try:
|
|
||||||
self._execute(*cmd)
|
|
||||||
except putils.ProcessExecutionError:
|
|
||||||
msg = _("ceph-common package is not installed.")
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
# NOTE(e0ne): map volume to a block device
|
|
||||||
# via the rbd kernel module.
|
|
||||||
pool, volume = connection_properties['name'].split('/')
|
|
||||||
rbd_dev_path = RBDConnector.get_rbd_device_name(pool, volume)
|
|
||||||
if (not os.path.islink(rbd_dev_path) or
|
|
||||||
not os.path.exists(os.path.realpath(rbd_dev_path))):
|
|
||||||
cmd = ['rbd', 'map', volume, '--pool', pool]
|
|
||||||
cmd += self._get_rbd_args(connection_properties)
|
|
||||||
self._execute(*cmd, root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
else:
|
|
||||||
LOG.debug('volume %(vol)s is already mapped to local'
|
|
||||||
' device %(dev)s',
|
|
||||||
{'vol': volume,
|
|
||||||
'dev': os.path.realpath(rbd_dev_path)})
|
|
||||||
|
|
||||||
return {'path': rbd_dev_path,
|
|
||||||
'type': 'block'}
|
|
||||||
|
|
||||||
rbd_handle = self._get_rbd_handle(connection_properties)
|
|
||||||
return {'path': rbd_handle}
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Disconnect a volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
"""
|
|
||||||
do_local_attach = connection_properties.get('do_local_attach',
|
|
||||||
self.do_local_attach)
|
|
||||||
if do_local_attach:
|
|
||||||
pool, volume = connection_properties['name'].split('/')
|
|
||||||
dev_name = RBDConnector.get_rbd_device_name(pool, volume)
|
|
||||||
cmd = ['rbd', 'unmap', dev_name]
|
|
||||||
cmd += self._get_rbd_args(connection_properties)
|
|
||||||
self._execute(*cmd, root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
else:
|
|
||||||
if device_info:
|
|
||||||
rbd_handle = device_info.get('path', None)
|
|
||||||
if rbd_handle is not None:
|
|
||||||
fileutils.delete_if_exists(rbd_handle.rbd_conf)
|
|
||||||
rbd_handle.close()
|
|
||||||
|
|
||||||
def check_valid_device(self, path, run_as_root=True):
|
|
||||||
"""Verify an existing RBD handle is connected and valid."""
|
|
||||||
rbd_handle = path
|
|
||||||
|
|
||||||
if rbd_handle is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
original_offset = rbd_handle.tell()
|
|
||||||
|
|
||||||
try:
|
|
||||||
rbd_handle.read(4096)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("Failed to access RBD device handle: %(error)s",
|
|
||||||
{"error": e})
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
rbd_handle.seek(original_offset, 0)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,121 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick.remotefs import remotefs
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteFsConnector(base.BaseLinuxConnector):
|
|
||||||
"""Connector class to attach/detach NFS and GlusterFS volumes."""
|
|
||||||
|
|
||||||
def __init__(self, mount_type, root_helper, driver=None,
|
|
||||||
execute=None,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
kwargs = kwargs or {}
|
|
||||||
conn = kwargs.get('conn')
|
|
||||||
mount_type_lower = mount_type.lower()
|
|
||||||
if conn:
|
|
||||||
mount_point_base = conn.get('mount_point_base')
|
|
||||||
if mount_type_lower in ('nfs', 'glusterfs', 'scality',
|
|
||||||
'quobyte', 'vzstorage'):
|
|
||||||
kwargs[mount_type_lower + '_mount_point_base'] = (
|
|
||||||
kwargs.get(mount_type_lower + '_mount_point_base') or
|
|
||||||
mount_point_base)
|
|
||||||
else:
|
|
||||||
LOG.warning("Connection details not present."
|
|
||||||
" RemoteFsClient may not initialize properly.")
|
|
||||||
|
|
||||||
if mount_type_lower == 'scality':
|
|
||||||
cls = remotefs.ScalityRemoteFsClient
|
|
||||||
elif mount_type_lower == 'vzstorage':
|
|
||||||
cls = remotefs.VZStorageRemoteFSClient
|
|
||||||
else:
|
|
||||||
cls = remotefs.RemoteFsClient
|
|
||||||
self._remotefsclient = cls(mount_type, root_helper, execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
super(RemoteFsConnector, self).__init__(
|
|
||||||
root_helper, driver=driver,
|
|
||||||
execute=execute,
|
|
||||||
device_scan_attempts=device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The RemoteFS connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def set_execute(self, execute):
|
|
||||||
super(RemoteFsConnector, self).set_execute(execute)
|
|
||||||
self._remotefsclient.set_execute(execute)
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return self._remotefsclient.get_mount_base()
|
|
||||||
|
|
||||||
def _get_volume_path(self, connection_properties):
|
|
||||||
mnt_flags = []
|
|
||||||
if connection_properties.get('options'):
|
|
||||||
mnt_flags = connection_properties['options'].split()
|
|
||||||
|
|
||||||
nfs_share = connection_properties['export']
|
|
||||||
self._remotefsclient.mount(nfs_share, mnt_flags)
|
|
||||||
mount_point = self._remotefsclient.get_mount_point(nfs_share)
|
|
||||||
path = mount_point + '/' + connection_properties['name']
|
|
||||||
return path
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
path = self._get_volume_path(connection_properties)
|
|
||||||
return [path]
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Ensure that the filesystem containing the volume is mounted.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
connection_properties must include:
|
|
||||||
export - remote filesystem device (e.g. '172.18.194.100:/var/nfs')
|
|
||||||
name - file name within the filesystem
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
|
|
||||||
|
|
||||||
connection_properties may optionally include:
|
|
||||||
options - options to pass to mount
|
|
||||||
"""
|
|
||||||
path = self._get_volume_path(connection_properties)
|
|
||||||
return {'path': path}
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""No need to do anything to disconnect a volume in a filesystem.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,492 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 json
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
|
|
||||||
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
|
||||||
|
|
||||||
|
|
||||||
class ScaleIOConnector(base.BaseLinuxConnector):
|
|
||||||
"""Class implements the connector driver for ScaleIO."""
|
|
||||||
|
|
||||||
OK_STATUS_CODE = 200
|
|
||||||
VOLUME_NOT_MAPPED_ERROR = 84
|
|
||||||
VOLUME_ALREADY_MAPPED_ERROR = 81
|
|
||||||
GET_GUID_CMD = ['/opt/emc/scaleio/sdc/bin/drv_cfg', '--query_guid']
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(ScaleIOConnector, self).__init__(
|
|
||||||
root_helper,
|
|
||||||
driver=driver,
|
|
||||||
device_scan_attempts=device_scan_attempts,
|
|
||||||
*args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
self.local_sdc_ip = None
|
|
||||||
self.server_ip = None
|
|
||||||
self.server_port = None
|
|
||||||
self.server_username = None
|
|
||||||
self.server_password = None
|
|
||||||
self.server_token = None
|
|
||||||
self.volume_id = None
|
|
||||||
self.volume_name = None
|
|
||||||
self.volume_path = None
|
|
||||||
self.iops_limit = None
|
|
||||||
self.bandwidth_limit = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The ScaleIO connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return "/dev/disk/by-id"
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
self.get_config(connection_properties)
|
|
||||||
volume_paths = []
|
|
||||||
device_paths = [self._find_volume_path()]
|
|
||||||
for path in device_paths:
|
|
||||||
if os.path.exists(path):
|
|
||||||
volume_paths.append(path)
|
|
||||||
return volume_paths
|
|
||||||
|
|
||||||
def _find_volume_path(self):
|
|
||||||
LOG.info(
|
|
||||||
"Looking for volume %(volume_id)s, maximum tries: %(tries)s",
|
|
||||||
{'volume_id': self.volume_id, 'tries': self.device_scan_attempts}
|
|
||||||
)
|
|
||||||
|
|
||||||
# look for the volume in /dev/disk/by-id directory
|
|
||||||
by_id_path = self.get_search_path()
|
|
||||||
|
|
||||||
disk_filename = self._wait_for_volume_path(by_id_path)
|
|
||||||
full_disk_name = ("%(path)s/%(filename)s" %
|
|
||||||
{'path': by_id_path, 'filename': disk_filename})
|
|
||||||
LOG.info("Full disk name is %(full_path)s",
|
|
||||||
{'full_path': full_disk_name})
|
|
||||||
return full_disk_name
|
|
||||||
|
|
||||||
# NOTE: Usually 3 retries is enough to find the volume.
|
|
||||||
# If there are network issues, it could take much longer. Set
|
|
||||||
# the max retries to 15 to make sure we can find the volume.
|
|
||||||
@utils.retry(exceptions=exception.BrickException,
|
|
||||||
retries=15,
|
|
||||||
backoff_rate=1)
|
|
||||||
def _wait_for_volume_path(self, path):
|
|
||||||
if not os.path.isdir(path):
|
|
||||||
msg = (
|
|
||||||
_("ScaleIO volume %(volume_id)s not found at "
|
|
||||||
"expected path.") % {'volume_id': self.volume_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.debug(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
disk_filename = None
|
|
||||||
filenames = os.listdir(path)
|
|
||||||
LOG.info(
|
|
||||||
"Files found in %(path)s path: %(files)s ",
|
|
||||||
{'path': path, 'files': filenames}
|
|
||||||
)
|
|
||||||
|
|
||||||
for filename in filenames:
|
|
||||||
if (filename.startswith("emc-vol") and
|
|
||||||
filename.endswith(self.volume_id)):
|
|
||||||
disk_filename = filename
|
|
||||||
break
|
|
||||||
|
|
||||||
if not disk_filename:
|
|
||||||
msg = (_("ScaleIO volume %(volume_id)s not found.") %
|
|
||||||
{'volume_id': self.volume_id})
|
|
||||||
LOG.debug(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
return disk_filename
|
|
||||||
|
|
||||||
def _get_client_id(self):
|
|
||||||
request = (
|
|
||||||
"https://%(server_ip)s:%(server_port)s/"
|
|
||||||
"api/types/Client/instances/getByIp::%(sdc_ip)s/" %
|
|
||||||
{
|
|
||||||
'server_ip': self.server_ip,
|
|
||||||
'server_port': self.server_port,
|
|
||||||
'sdc_ip': self.local_sdc_ip
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info("ScaleIO get client id by ip request: %(request)s",
|
|
||||||
{'request': request})
|
|
||||||
|
|
||||||
r = requests.get(
|
|
||||||
request,
|
|
||||||
auth=(self.server_username, self.server_token),
|
|
||||||
verify=False
|
|
||||||
)
|
|
||||||
|
|
||||||
r = self._check_response(r, request)
|
|
||||||
sdc_id = r.json()
|
|
||||||
if not sdc_id:
|
|
||||||
msg = (_("Client with ip %(sdc_ip)s was not found.") %
|
|
||||||
{'sdc_ip': self.local_sdc_ip})
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
if r.status_code != 200 and "errorCode" in sdc_id:
|
|
||||||
msg = (_("Error getting sdc id from ip %(sdc_ip)s: %(err)s") %
|
|
||||||
{'sdc_ip': self.local_sdc_ip, 'err': sdc_id['message']})
|
|
||||||
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
LOG.info("ScaleIO sdc id is %(sdc_id)s.",
|
|
||||||
{'sdc_id': sdc_id})
|
|
||||||
return sdc_id
|
|
||||||
|
|
||||||
def _get_volume_id(self):
|
|
||||||
volname_encoded = urllib.parse.quote(self.volume_name, '')
|
|
||||||
volname_double_encoded = urllib.parse.quote(volname_encoded, '')
|
|
||||||
LOG.debug(_(
|
|
||||||
"Volume name after double encoding is %(volume_name)s."),
|
|
||||||
{'volume_name': volname_double_encoded}
|
|
||||||
)
|
|
||||||
|
|
||||||
request = (
|
|
||||||
"https://%(server_ip)s:%(server_port)s/api/types/Volume/instances"
|
|
||||||
"/getByName::%(encoded_volume_name)s" %
|
|
||||||
{
|
|
||||||
'server_ip': self.server_ip,
|
|
||||||
'server_port': self.server_port,
|
|
||||||
'encoded_volume_name': volname_double_encoded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info(
|
|
||||||
"ScaleIO get volume id by name request: %(request)s",
|
|
||||||
{'request': request}
|
|
||||||
)
|
|
||||||
|
|
||||||
r = requests.get(request,
|
|
||||||
auth=(self.server_username, self.server_token),
|
|
||||||
verify=False)
|
|
||||||
|
|
||||||
r = self._check_response(r, request)
|
|
||||||
|
|
||||||
volume_id = r.json()
|
|
||||||
if not volume_id:
|
|
||||||
msg = (_("Volume with name %(volume_name)s wasn't found.") %
|
|
||||||
{'volume_name': self.volume_name})
|
|
||||||
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
if r.status_code != self.OK_STATUS_CODE and "errorCode" in volume_id:
|
|
||||||
msg = (
|
|
||||||
_("Error getting volume id from name %(volume_name)s: "
|
|
||||||
"%(err)s") %
|
|
||||||
{'volume_name': self.volume_name, 'err': volume_id['message']}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
LOG.info("ScaleIO volume id is %(volume_id)s.",
|
|
||||||
{'volume_id': volume_id})
|
|
||||||
return volume_id
|
|
||||||
|
|
||||||
def _check_response(self, response, request, is_get_request=True,
|
|
||||||
params=None):
|
|
||||||
if response.status_code == 401 or response.status_code == 403:
|
|
||||||
LOG.info("Token is invalid, "
|
|
||||||
"going to re-login to get a new one")
|
|
||||||
|
|
||||||
login_request = (
|
|
||||||
"https://%(server_ip)s:%(server_port)s/api/login" %
|
|
||||||
{'server_ip': self.server_ip, 'server_port': self.server_port}
|
|
||||||
)
|
|
||||||
|
|
||||||
r = requests.get(
|
|
||||||
login_request,
|
|
||||||
auth=(self.server_username, self.server_password),
|
|
||||||
verify=False
|
|
||||||
)
|
|
||||||
|
|
||||||
token = r.json()
|
|
||||||
# repeat request with valid token
|
|
||||||
LOG.debug(_("Going to perform request %(request)s again "
|
|
||||||
"with valid token"), {'request': request})
|
|
||||||
|
|
||||||
if is_get_request:
|
|
||||||
res = requests.get(request,
|
|
||||||
auth=(self.server_username, token),
|
|
||||||
verify=False)
|
|
||||||
else:
|
|
||||||
headers = {'content-type': 'application/json'}
|
|
||||||
res = requests.post(
|
|
||||||
request,
|
|
||||||
data=json.dumps(params),
|
|
||||||
headers=headers,
|
|
||||||
auth=(self.server_username, token),
|
|
||||||
verify=False
|
|
||||||
)
|
|
||||||
|
|
||||||
self.server_token = token
|
|
||||||
return res
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get_config(self, connection_properties):
|
|
||||||
self.local_sdc_ip = connection_properties['hostIP']
|
|
||||||
self.volume_name = connection_properties['scaleIO_volname']
|
|
||||||
self.volume_id = connection_properties['scaleIO_volume_id']
|
|
||||||
self.server_ip = connection_properties['serverIP']
|
|
||||||
self.server_port = connection_properties['serverPort']
|
|
||||||
self.server_username = connection_properties['serverUsername']
|
|
||||||
self.server_password = connection_properties['serverPassword']
|
|
||||||
self.server_token = connection_properties['serverToken']
|
|
||||||
self.iops_limit = connection_properties['iopsLimit']
|
|
||||||
self.bandwidth_limit = connection_properties['bandwidthLimit']
|
|
||||||
device_info = {'type': 'block',
|
|
||||||
'path': self.volume_path}
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@lockutils.synchronized('scaleio', 'scaleio-')
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect the volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
device_info = self.get_config(connection_properties)
|
|
||||||
LOG.debug(
|
|
||||||
_(
|
|
||||||
"scaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, "
|
|
||||||
"REST Server IP: %(server_ip)s, "
|
|
||||||
"REST Server username: %(username)s, "
|
|
||||||
"iops limit:%(iops_limit)s, "
|
|
||||||
"bandwidth limit: %(bandwidth_limit)s."
|
|
||||||
), {
|
|
||||||
'volume_name': self.volume_name,
|
|
||||||
'volume_id': self.volume_id,
|
|
||||||
'sdc_ip': self.local_sdc_ip,
|
|
||||||
'server_ip': self.server_ip,
|
|
||||||
'username': self.server_username,
|
|
||||||
'iops_limit': self.iops_limit,
|
|
||||||
'bandwidth_limit': self.bandwidth_limit
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info("ScaleIO sdc query guid command: %(cmd)s",
|
|
||||||
{'cmd': self.GET_GUID_CMD})
|
|
||||||
|
|
||||||
try:
|
|
||||||
(out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
LOG.info("Map volume %(cmd)s: stdout=%(out)s "
|
|
||||||
"stderr=%(err)s",
|
|
||||||
{'cmd': self.GET_GUID_CMD, 'out': out, 'err': err})
|
|
||||||
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
msg = (_("Error querying sdc guid: %(err)s") % {'err': e.stderr})
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
guid = out
|
|
||||||
LOG.info("Current sdc guid: %(guid)s", {'guid': guid})
|
|
||||||
params = {'guid': guid, 'allowMultipleMappings': 'TRUE'}
|
|
||||||
self.volume_id = self.volume_id or self._get_volume_id()
|
|
||||||
|
|
||||||
headers = {'content-type': 'application/json'}
|
|
||||||
request = (
|
|
||||||
"https://%(server_ip)s:%(server_port)s/api/instances/"
|
|
||||||
"Volume::%(volume_id)s/action/addMappedSdc" %
|
|
||||||
{'server_ip': self.server_ip, 'server_port': self.server_port,
|
|
||||||
'volume_id': self.volume_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info("map volume request: %(request)s", {'request': request})
|
|
||||||
r = requests.post(
|
|
||||||
request,
|
|
||||||
data=json.dumps(params),
|
|
||||||
headers=headers,
|
|
||||||
auth=(self.server_username, self.server_token),
|
|
||||||
verify=False
|
|
||||||
)
|
|
||||||
|
|
||||||
r = self._check_response(r, request, False, params)
|
|
||||||
if r.status_code != self.OK_STATUS_CODE:
|
|
||||||
response = r.json()
|
|
||||||
error_code = response['errorCode']
|
|
||||||
if error_code == self.VOLUME_ALREADY_MAPPED_ERROR:
|
|
||||||
LOG.warning(
|
|
||||||
"Ignoring error mapping volume %(volume_name)s: "
|
|
||||||
"volume already mapped.",
|
|
||||||
{'volume_name': self.volume_name}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
msg = (
|
|
||||||
_("Error mapping volume %(volume_name)s: %(err)s") %
|
|
||||||
{'volume_name': self.volume_name,
|
|
||||||
'err': response['message']}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
self.volume_path = self._find_volume_path()
|
|
||||||
device_info['path'] = self.volume_path
|
|
||||||
|
|
||||||
# Set QoS settings after map was performed
|
|
||||||
if self.iops_limit is not None or self.bandwidth_limit is not None:
|
|
||||||
params = {'guid': guid}
|
|
||||||
if self.bandwidth_limit is not None:
|
|
||||||
params['bandwidthLimitInKbps'] = self.bandwidth_limit
|
|
||||||
if self.iops_limit is not None:
|
|
||||||
params['iopsLimit'] = self.iops_limit
|
|
||||||
|
|
||||||
request = (
|
|
||||||
"https://%(server_ip)s:%(server_port)s/api/instances/"
|
|
||||||
"Volume::%(volume_id)s/action/setMappedSdcLimits" %
|
|
||||||
{'server_ip': self.server_ip, 'server_port': self.server_port,
|
|
||||||
'volume_id': self.volume_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info("Set client limit request: %(request)s",
|
|
||||||
{'request': request})
|
|
||||||
|
|
||||||
r = requests.post(
|
|
||||||
request,
|
|
||||||
data=json.dumps(params),
|
|
||||||
headers=headers,
|
|
||||||
auth=(self.server_username, self.server_token),
|
|
||||||
verify=False
|
|
||||||
)
|
|
||||||
r = self._check_response(r, request, False, params)
|
|
||||||
if r.status_code != self.OK_STATUS_CODE:
|
|
||||||
response = r.json()
|
|
||||||
LOG.info("Set client limit response: %(response)s",
|
|
||||||
{'response': response})
|
|
||||||
msg = (
|
|
||||||
_("Error setting client limits for volume "
|
|
||||||
"%(volume_name)s: %(err)s") %
|
|
||||||
{'volume_name': self.volume_name,
|
|
||||||
'err': response['message']}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.error(msg)
|
|
||||||
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@lockutils.synchronized('scaleio', 'scaleio-')
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Disconnect the ScaleIO volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
"""
|
|
||||||
self.get_config(connection_properties)
|
|
||||||
self.volume_id = self.volume_id or self._get_volume_id()
|
|
||||||
LOG.info(
|
|
||||||
"ScaleIO disconnect volume in ScaleIO brick volume driver."
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.debug(
|
|
||||||
_("ScaleIO Volume name: %(volume_name)s, SDC IP: %(sdc_ip)s, "
|
|
||||||
"REST Server IP: %(server_ip)s"),
|
|
||||||
{'volume_name': self.volume_name, 'sdc_ip': self.local_sdc_ip,
|
|
||||||
'server_ip': self.server_ip}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info("ScaleIO sdc query guid command: %(cmd)s",
|
|
||||||
{'cmd': self.GET_GUID_CMD})
|
|
||||||
|
|
||||||
try:
|
|
||||||
(out, err) = self._execute(*self.GET_GUID_CMD, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
LOG.info(
|
|
||||||
"Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s",
|
|
||||||
{'cmd': self.GET_GUID_CMD, 'out': out, 'err': err}
|
|
||||||
)
|
|
||||||
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
msg = _("Error querying sdc guid: %(err)s") % {'err': e.stderr}
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
guid = out
|
|
||||||
LOG.info("Current sdc guid: %(guid)s", {'guid': guid})
|
|
||||||
|
|
||||||
params = {'guid': guid}
|
|
||||||
headers = {'content-type': 'application/json'}
|
|
||||||
request = (
|
|
||||||
"https://%(server_ip)s:%(server_port)s/api/instances/"
|
|
||||||
"Volume::%(volume_id)s/action/removeMappedSdc" %
|
|
||||||
{'server_ip': self.server_ip, 'server_port': self.server_port,
|
|
||||||
'volume_id': self.volume_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info("Unmap volume request: %(request)s",
|
|
||||||
{'request': request})
|
|
||||||
r = requests.post(
|
|
||||||
request,
|
|
||||||
data=json.dumps(params),
|
|
||||||
headers=headers,
|
|
||||||
auth=(self.server_username, self.server_token),
|
|
||||||
verify=False
|
|
||||||
)
|
|
||||||
|
|
||||||
r = self._check_response(r, request, False, params)
|
|
||||||
if r.status_code != self.OK_STATUS_CODE:
|
|
||||||
response = r.json()
|
|
||||||
error_code = response['errorCode']
|
|
||||||
if error_code == self.VOLUME_NOT_MAPPED_ERROR:
|
|
||||||
LOG.warning(
|
|
||||||
"Ignoring error unmapping volume %(volume_id)s: "
|
|
||||||
"volume not mapped.", {'volume_id': self.volume_name}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
msg = (_("Error unmapping volume %(volume_id)s: %(err)s") %
|
|
||||||
{'volume_id': self.volume_name,
|
|
||||||
'err': response['message']})
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(walter-boring): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,127 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick.initiator import linuxsheepdog
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SheepdogConnector(base.BaseLinuxConnector):
|
|
||||||
""""Connector class to attach/detach sheepdog volumes."""
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None, use_multipath=False,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
|
|
||||||
super(SheepdogConnector, self).__init__(root_helper, driver=driver,
|
|
||||||
device_scan_attempts=
|
|
||||||
device_scan_attempts,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The Sheepdog connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
# TODO(lixiaoy1): don't know where the connector
|
|
||||||
# looks for sheepdog volumes.
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
# TODO(lixiaoy1): don't know where the connector
|
|
||||||
# looks for sheepdog volumes.
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
# TODO(lixiaoy1): not sure what to return here for sheepdog
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _get_sheepdog_handle(self, connection_properties):
|
|
||||||
try:
|
|
||||||
host = connection_properties['hosts'][0]
|
|
||||||
name = connection_properties['name']
|
|
||||||
port = connection_properties['ports'][0]
|
|
||||||
except IndexError:
|
|
||||||
msg = _("Connect volume failed, malformed connection properties")
|
|
||||||
raise exception.BrickException(msg=msg)
|
|
||||||
|
|
||||||
sheepdog_handle = linuxsheepdog.SheepdogVolumeIOWrapper(
|
|
||||||
host, port, name)
|
|
||||||
return sheepdog_handle
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect to a volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
sheepdog_handle = self._get_sheepdog_handle(connection_properties)
|
|
||||||
return {'path': sheepdog_handle}
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Disconnect a volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
"""
|
|
||||||
if device_info:
|
|
||||||
sheepdog_handle = device_info.get('path', None)
|
|
||||||
self.check_IO_handle_valid(sheepdog_handle,
|
|
||||||
linuxsheepdog.SheepdogVolumeIOWrapper,
|
|
||||||
'Sheepdog')
|
|
||||||
if sheepdog_handle is not None:
|
|
||||||
sheepdog_handle.close()
|
|
||||||
|
|
||||||
def check_valid_device(self, path, run_as_root=True):
|
|
||||||
"""Verify an existing sheepdog handle is connected and valid."""
|
|
||||||
sheepdog_handle = path
|
|
||||||
|
|
||||||
if sheepdog_handle is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
original_offset = sheepdog_handle.tell()
|
|
||||||
|
|
||||||
try:
|
|
||||||
sheepdog_handle.read(4096)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error("Failed to access sheepdog device "
|
|
||||||
"handle: %(error)s",
|
|
||||||
{"error": e})
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
sheepdog_handle.seek(original_offset, 0)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
# TODO(lixiaoy1): is this possible?
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,277 +0,0 @@
|
||||||
# Copyright (c) 2016 VMware, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 tempfile
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import fileutils
|
|
||||||
try:
|
|
||||||
from oslo_vmware import api
|
|
||||||
from oslo_vmware import exceptions as oslo_vmw_exceptions
|
|
||||||
from oslo_vmware import image_transfer
|
|
||||||
from oslo_vmware.objects import datastore
|
|
||||||
from oslo_vmware import rw_handles
|
|
||||||
from oslo_vmware import vim_util
|
|
||||||
except ImportError:
|
|
||||||
vim_util = None
|
|
||||||
import six
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick.initiator import initiator_connector
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class VmdkConnector(initiator_connector.InitiatorConnector):
|
|
||||||
"""Connector for volumes created by the VMDK driver.
|
|
||||||
|
|
||||||
This connector is only used for backup and restore of Cinder volumes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
TMP_IMAGES_DATASTORE_FOLDER_PATH = "cinder_temp"
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# Check if oslo.vmware library is available.
|
|
||||||
if vim_util is None:
|
|
||||||
message = _("Missing oslo_vmware python module, ensure oslo.vmware"
|
|
||||||
" library is installed and available.")
|
|
||||||
raise exception.BrickException(message=message)
|
|
||||||
|
|
||||||
super(VmdkConnector, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self._ip = None
|
|
||||||
self._port = None
|
|
||||||
self._username = None
|
|
||||||
self._password = None
|
|
||||||
self._api_retry_count = None
|
|
||||||
self._task_poll_interval = None
|
|
||||||
self._ca_file = None
|
|
||||||
self._insecure = None
|
|
||||||
self._tmp_dir = None
|
|
||||||
self._timeout = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def check_valid_device(self, path, run_as_root=True):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _load_config(self, connection_properties):
|
|
||||||
config = connection_properties['config']
|
|
||||||
self._ip = config['vmware_host_ip']
|
|
||||||
self._port = config['vmware_host_port']
|
|
||||||
self._username = config['vmware_host_username']
|
|
||||||
self._password = config['vmware_host_password']
|
|
||||||
self._api_retry_count = config['vmware_api_retry_count']
|
|
||||||
self._task_poll_interval = config['vmware_task_poll_interval']
|
|
||||||
self._ca_file = config['vmware_ca_file']
|
|
||||||
self._insecure = config['vmware_insecure']
|
|
||||||
self._tmp_dir = config['vmware_tmp_dir']
|
|
||||||
self._timeout = config['vmware_image_transfer_timeout_secs']
|
|
||||||
|
|
||||||
def _create_session(self):
|
|
||||||
return api.VMwareAPISession(self._ip,
|
|
||||||
self._username,
|
|
||||||
self._password,
|
|
||||||
self._api_retry_count,
|
|
||||||
self._task_poll_interval,
|
|
||||||
port=self._port,
|
|
||||||
cacert=self._ca_file,
|
|
||||||
insecure=self._insecure)
|
|
||||||
|
|
||||||
def _create_temp_file(self, *args, **kwargs):
|
|
||||||
fileutils.ensure_tree(self._tmp_dir)
|
|
||||||
fd, tmp = tempfile.mkstemp(dir=self._tmp_dir, *args, **kwargs)
|
|
||||||
os.close(fd)
|
|
||||||
return tmp
|
|
||||||
|
|
||||||
def _download_vmdk(
|
|
||||||
self, tmp_file_path, session, backing, vmdk_path, vmdk_size):
|
|
||||||
with open(tmp_file_path, "wb") as tmp_file:
|
|
||||||
image_transfer.copy_stream_optimized_disk(
|
|
||||||
None,
|
|
||||||
self._timeout,
|
|
||||||
tmp_file,
|
|
||||||
session=session,
|
|
||||||
host=self._ip,
|
|
||||||
port=self._port,
|
|
||||||
vm=backing,
|
|
||||||
vmdk_file_path=vmdk_path,
|
|
||||||
vmdk_size=vmdk_size)
|
|
||||||
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
# Download the volume vmdk from vCenter server to a temporary file
|
|
||||||
# and return its path.
|
|
||||||
self._load_config(connection_properties)
|
|
||||||
session = self._create_session()
|
|
||||||
|
|
||||||
tmp_file_path = self._create_temp_file(
|
|
||||||
suffix=".vmdk", prefix=connection_properties['volume_id'])
|
|
||||||
backing = vim_util.get_moref(connection_properties['volume'],
|
|
||||||
"VirtualMachine")
|
|
||||||
vmdk_path = connection_properties['vmdk_path']
|
|
||||||
vmdk_size = connection_properties['vmdk_size']
|
|
||||||
try:
|
|
||||||
self._download_vmdk(
|
|
||||||
tmp_file_path, session, backing, vmdk_path, vmdk_size)
|
|
||||||
finally:
|
|
||||||
session.logout()
|
|
||||||
|
|
||||||
# Save the last modified time of the temporary so that we can decide
|
|
||||||
# whether to upload the file back to vCenter server during disconnect.
|
|
||||||
last_modified = os.path.getmtime(tmp_file_path)
|
|
||||||
return {'path': tmp_file_path, 'last_modified': last_modified}
|
|
||||||
|
|
||||||
def _snapshot_exists(self, session, backing):
|
|
||||||
snapshot = session.invoke_api(vim_util,
|
|
||||||
'get_object_property',
|
|
||||||
session.vim,
|
|
||||||
backing,
|
|
||||||
'snapshot')
|
|
||||||
if snapshot is None or snapshot.rootSnapshotList is None:
|
|
||||||
return False
|
|
||||||
return len(snapshot.rootSnapshotList) != 0
|
|
||||||
|
|
||||||
def _create_temp_ds_folder(self, session, ds_folder_path, dc_ref):
|
|
||||||
fileManager = session.vim.service_content.fileManager
|
|
||||||
try:
|
|
||||||
session.invoke_api(session.vim,
|
|
||||||
'MakeDirectory',
|
|
||||||
fileManager,
|
|
||||||
name=ds_folder_path,
|
|
||||||
datacenter=dc_ref)
|
|
||||||
except oslo_vmw_exceptions.FileAlreadyExistsException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Note(vbala) remove this method when we implement it in oslo.vmware
|
|
||||||
def _upload_vmdk(
|
|
||||||
self, read_handle, host, port, dc_name, ds_name, cookies,
|
|
||||||
upload_file_path, file_size, cacerts, timeout_secs):
|
|
||||||
write_handle = rw_handles.FileWriteHandle(host,
|
|
||||||
port,
|
|
||||||
dc_name,
|
|
||||||
ds_name,
|
|
||||||
cookies,
|
|
||||||
upload_file_path,
|
|
||||||
file_size,
|
|
||||||
cacerts=cacerts)
|
|
||||||
image_transfer._start_transfer(read_handle, write_handle, timeout_secs)
|
|
||||||
|
|
||||||
def _disconnect(self, tmp_file_path, session, ds_ref, dc_ref, vmdk_path):
|
|
||||||
# The restored volume is in compressed (streamOptimized) format.
|
|
||||||
# So we upload it to a temporary location in vCenter datastore and copy
|
|
||||||
# the compressed vmdk to the volume vmdk. The copy operation
|
|
||||||
# decompresses the disk to a format suitable for attaching to Nova
|
|
||||||
# instances in vCenter.
|
|
||||||
dstore = datastore.get_datastore_by_ref(session, ds_ref)
|
|
||||||
ds_path = dstore.build_path(
|
|
||||||
VmdkConnector.TMP_IMAGES_DATASTORE_FOLDER_PATH,
|
|
||||||
os.path.basename(tmp_file_path))
|
|
||||||
self._create_temp_ds_folder(
|
|
||||||
session, six.text_type(ds_path.parent), dc_ref)
|
|
||||||
|
|
||||||
with open(tmp_file_path, "rb") as tmp_file:
|
|
||||||
dc_name = session.invoke_api(
|
|
||||||
vim_util, 'get_object_property', session.vim, dc_ref, 'name')
|
|
||||||
cookies = session.vim.client.options.transport.cookiejar
|
|
||||||
cacerts = self._ca_file if self._ca_file else not self._insecure
|
|
||||||
self._upload_vmdk(
|
|
||||||
tmp_file, self._ip, self._port, dc_name, dstore.name, cookies,
|
|
||||||
ds_path.rel_path, os.path.getsize(tmp_file_path), cacerts,
|
|
||||||
self._timeout)
|
|
||||||
|
|
||||||
# Delete the current volume vmdk because the copy operation does not
|
|
||||||
# overwrite.
|
|
||||||
LOG.debug("Deleting %s", vmdk_path)
|
|
||||||
disk_mgr = session.vim.service_content.virtualDiskManager
|
|
||||||
task = session.invoke_api(session.vim,
|
|
||||||
'DeleteVirtualDisk_Task',
|
|
||||||
disk_mgr,
|
|
||||||
name=vmdk_path,
|
|
||||||
datacenter=dc_ref)
|
|
||||||
session.wait_for_task(task)
|
|
||||||
|
|
||||||
src = six.text_type(ds_path)
|
|
||||||
LOG.debug("Copying %(src)s to %(dest)s", {'src': src,
|
|
||||||
'dest': vmdk_path})
|
|
||||||
task = session.invoke_api(session.vim,
|
|
||||||
'CopyVirtualDisk_Task',
|
|
||||||
disk_mgr,
|
|
||||||
sourceName=src,
|
|
||||||
sourceDatacenter=dc_ref,
|
|
||||||
destName=vmdk_path,
|
|
||||||
destDatacenter=dc_ref)
|
|
||||||
session.wait_for_task(task)
|
|
||||||
|
|
||||||
# Delete the compressed vmdk at the temporary location.
|
|
||||||
LOG.debug("Deleting %s", src)
|
|
||||||
file_mgr = session.vim.service_content.fileManager
|
|
||||||
task = session.invoke_api(session.vim,
|
|
||||||
'DeleteDatastoreFile_Task',
|
|
||||||
file_mgr,
|
|
||||||
name=src,
|
|
||||||
datacenter=dc_ref)
|
|
||||||
session.wait_for_task(task)
|
|
||||||
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
tmp_file_path = device_info['path']
|
|
||||||
if not os.path.exists(tmp_file_path):
|
|
||||||
msg = _("Vmdk: %s not found.") % tmp_file_path
|
|
||||||
raise exception.NotFound(message=msg)
|
|
||||||
|
|
||||||
session = None
|
|
||||||
try:
|
|
||||||
# We upload the temporary file to vCenter server only if it is
|
|
||||||
# modified after connect_volume.
|
|
||||||
if os.path.getmtime(tmp_file_path) > device_info['last_modified']:
|
|
||||||
self._load_config(connection_properties)
|
|
||||||
session = self._create_session()
|
|
||||||
backing = vim_util.get_moref(connection_properties['volume'],
|
|
||||||
"VirtualMachine")
|
|
||||||
# Currently there is no way we can restore the volume if it
|
|
||||||
# contains redo-log based snapshots (bug 1599026).
|
|
||||||
if self._snapshot_exists(session, backing):
|
|
||||||
msg = (_("Backing of volume: %s contains one or more "
|
|
||||||
"snapshots; cannot disconnect.") %
|
|
||||||
connection_properties['volume_id'])
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
ds_ref = vim_util.get_moref(
|
|
||||||
connection_properties['datastore'], "Datastore")
|
|
||||||
dc_ref = vim_util.get_moref(
|
|
||||||
connection_properties['datacenter'], "Datacenter")
|
|
||||||
vmdk_path = connection_properties['vmdk_path']
|
|
||||||
self._disconnect(
|
|
||||||
tmp_file_path, session, ds_ref, dc_ref, vmdk_path)
|
|
||||||
finally:
|
|
||||||
os.remove(tmp_file_path)
|
|
||||||
if session:
|
|
||||||
session.logout()
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,160 +0,0 @@
|
||||||
# Copyright (c) 2017 Veritas Technologies LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 json
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
synchronized = lockutils.synchronized_with_prefix('os-brick-vrts-hyperscale-')
|
|
||||||
|
|
||||||
|
|
||||||
class HyperScaleConnector(base.BaseLinuxConnector):
|
|
||||||
"""Class implements the os-brick connector for HyperScale volumes."""
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None,
|
|
||||||
execute=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
|
|
||||||
super(HyperScaleConnector, self).__init__(
|
|
||||||
root_helper, driver=driver,
|
|
||||||
execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The HyperScale connector properties."""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect a volume to an instance."""
|
|
||||||
|
|
||||||
out = None
|
|
||||||
err = None
|
|
||||||
device_info = {}
|
|
||||||
volume_name = None
|
|
||||||
|
|
||||||
if 'name' in connection_properties.keys():
|
|
||||||
volume_name = connection_properties['name']
|
|
||||||
|
|
||||||
if volume_name is None:
|
|
||||||
msg = _("Failed to connect volume: invalid volume name.")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
cmd_arg = {'operation': 'connect_volume'}
|
|
||||||
cmd_arg['volume_guid'] = volume_name
|
|
||||||
cmdarg_json = json.dumps(cmd_arg)
|
|
||||||
|
|
||||||
LOG.debug("HyperScale command hscli: %(cmd_arg)s",
|
|
||||||
{'cmd_arg': cmdarg_json})
|
|
||||||
try:
|
|
||||||
(out, err) = self._execute('hscli', cmdarg_json,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
msg = (_("Error executing hscli: %(err)s") % {'err': e.stderr})
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
LOG.debug("Result of hscli: stdout=%(out)s "
|
|
||||||
"stderr=%(err)s",
|
|
||||||
{'out': out, 'err': err})
|
|
||||||
|
|
||||||
if err or out is None or len(out) == 0:
|
|
||||||
msg = (_("Failed to connect volume with stdout=%(out)s "
|
|
||||||
"stderr=%(err)s") % {'out': out, 'err': err})
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
output = json.loads(out)
|
|
||||||
payload = output.get('payload')
|
|
||||||
if payload is None:
|
|
||||||
msg = _("Failed to connect volume: "
|
|
||||||
"hscli returned invalid payload")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
if ('vsa_ip' not in payload.keys() or
|
|
||||||
'refl_factor' not in payload.keys()):
|
|
||||||
msg = _("Failed to connect volume: "
|
|
||||||
"hscli returned invalid results")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
device_info['vsa_ip'] = payload.get('vsa_ip')
|
|
||||||
device_info['path'] = (
|
|
||||||
'/dev/' + connection_properties['name'][1:32])
|
|
||||||
refl_factor = int(payload.get('refl_factor'))
|
|
||||||
device_info['refl_factor'] = str(refl_factor)
|
|
||||||
|
|
||||||
if refl_factor > 0:
|
|
||||||
if 'refl_targets' not in payload.keys():
|
|
||||||
msg = _("Failed to connect volume: "
|
|
||||||
"hscli returned inconsistent results")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
device_info['refl_targets'] = (
|
|
||||||
payload.get('refl_targets'))
|
|
||||||
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
@synchronized('connect_volume')
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Disconnect a volume from an instance."""
|
|
||||||
volume_name = None
|
|
||||||
|
|
||||||
if 'name' in connection_properties.keys():
|
|
||||||
volume_name = connection_properties['name']
|
|
||||||
|
|
||||||
if volume_name is None:
|
|
||||||
msg = _("Failed to disconnect volume: invalid volume name")
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
cmd_arg = {'operation': 'disconnect_volume'}
|
|
||||||
cmd_arg['volume_guid'] = volume_name
|
|
||||||
cmdarg_json = json.dumps(cmd_arg)
|
|
||||||
|
|
||||||
LOG.debug("HyperScale command hscli: %(cmd_arg)s",
|
|
||||||
{'cmd_arg': cmdarg_json})
|
|
||||||
try:
|
|
||||||
(out, err) = self._execute('hscli', cmdarg_json,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
msg = (_("Error executing hscli: %(err)s") % {'err': e.stderr})
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
if err:
|
|
||||||
msg = (_("Failed to connect volume: stdout=%(out)s "
|
|
||||||
"stderr=%(err)s") % {'out': out, 'err': err})
|
|
||||||
raise exception.BrickException(message=msg)
|
|
|
@ -1,36 +0,0 @@
|
||||||
# Copyright 2013 OpenStack Foundation.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 errno
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class HostDriver(object):
|
|
||||||
|
|
||||||
def get_all_block_devices(self):
|
|
||||||
"""Get the list of all block devices seen in /dev/disk/by-path/."""
|
|
||||||
dir = "/dev/disk/by-path/"
|
|
||||||
try:
|
|
||||||
files = os.listdir(dir)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno == errno.ENOENT:
|
|
||||||
files = []
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
devices = []
|
|
||||||
for file in files:
|
|
||||||
devices.append(dir + file)
|
|
||||||
return devices
|
|
|
@ -1,197 +0,0 @@
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 abc
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import executor
|
|
||||||
from os_brick import initiator
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class InitiatorConnector(executor.Executor):
|
|
||||||
|
|
||||||
# This object can be used on any platform (x86, S390)
|
|
||||||
platform = initiator.PLATFORM_ALL
|
|
||||||
|
|
||||||
# This object can be used on any os type (linux, windows)
|
|
||||||
os_type = initiator.OS_TYPE_ALL
|
|
||||||
|
|
||||||
def __init__(self, root_helper, driver=None, execute=None,
|
|
||||||
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
|
||||||
*args, **kwargs):
|
|
||||||
super(InitiatorConnector, self).__init__(root_helper, execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
self.device_scan_attempts = device_scan_attempts
|
|
||||||
|
|
||||||
def set_driver(self, driver):
|
|
||||||
"""The driver is used to find used LUNs."""
|
|
||||||
self.driver = driver
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_connector_properties(root_helper, *args, **kwargs):
|
|
||||||
"""The generic connector properties."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def check_valid_device(self, path, run_as_root=True):
|
|
||||||
"""Test to see if the device path is a real device.
|
|
||||||
|
|
||||||
:param path: The file system path for the device.
|
|
||||||
:type path: str
|
|
||||||
:param run_as_root: run the tests as root user?
|
|
||||||
:type run_as_root: bool
|
|
||||||
:returns: bool
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
"""Connect to a volume.
|
|
||||||
|
|
||||||
The connection_properties describes the information needed by
|
|
||||||
the specific protocol to use to make the connection.
|
|
||||||
|
|
||||||
The connection_properties is a dictionary that describes the target
|
|
||||||
volume. It varies slightly by protocol type (iscsi, fibre_channel),
|
|
||||||
but the structure is usually the same.
|
|
||||||
|
|
||||||
|
|
||||||
An example for iSCSI:
|
|
||||||
|
|
||||||
{'driver_volume_type': 'iscsi',
|
|
||||||
'data': {
|
|
||||||
'target_luns': [0, 2],
|
|
||||||
'target_iqns': ['iqn.2000-05.com.3pardata:20810002ac00383d',
|
|
||||||
'iqn.2000-05.com.3pardata:21810002ac00383d'],
|
|
||||||
'target_discovered': True,
|
|
||||||
'encrypted': False,
|
|
||||||
'qos_specs': None,
|
|
||||||
'target_portals': ['10.52.1.11:3260', '10.52.2.11:3260'],
|
|
||||||
'access_mode': 'rw',
|
|
||||||
}}
|
|
||||||
|
|
||||||
An example for fibre_channel:
|
|
||||||
|
|
||||||
{'driver_volume_type': 'fibre_channel',
|
|
||||||
'data': {
|
|
||||||
'initiator_target_map': {'100010604b010459': ['21230002AC00383D'],
|
|
||||||
'100010604b01045d': ['21230002AC00383D']
|
|
||||||
},
|
|
||||||
'target_discovered': True,
|
|
||||||
'encrypted': False,
|
|
||||||
'qos_specs': None,
|
|
||||||
'target_lun': 1,
|
|
||||||
'access_mode': 'rw',
|
|
||||||
'target_wwn': [
|
|
||||||
'20210002AC00383D',
|
|
||||||
'20220002AC00383D',
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:returns: dict
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def disconnect_volume(self, connection_properties, device_info,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
"""Disconnect a volume from the local host.
|
|
||||||
|
|
||||||
The connection_properties are the same as from connect_volume.
|
|
||||||
The device_info is returned from connect_volume.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
:param device_info: historical difference, but same as connection_props
|
|
||||||
:type device_info: dict
|
|
||||||
:param force: Whether to forcefully disconnect even if flush fails.
|
|
||||||
:type force: bool
|
|
||||||
:param ignore_errors: When force is True, this will decide whether to
|
|
||||||
ignore errors or raise an exception once finished
|
|
||||||
the operation. Default is False.
|
|
||||||
:type ignore_errors: bool
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
"""Return the list of existing paths for a volume.
|
|
||||||
|
|
||||||
The job of this method is to find out what paths in
|
|
||||||
the system are associated with a volume as described
|
|
||||||
by the connection_properties.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_search_path(self):
|
|
||||||
"""Return the directory where a Connector looks for volumes.
|
|
||||||
|
|
||||||
Some Connectors need the information in the
|
|
||||||
connection_properties to determine the search path.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
"""Update the attached volume's size.
|
|
||||||
|
|
||||||
This method will attempt to update the local hosts's
|
|
||||||
volume after the volume has been extended on the remote
|
|
||||||
system. The new volume size in bytes will be returned.
|
|
||||||
If there is a failure to update, then None will be returned.
|
|
||||||
|
|
||||||
:param connection_properties: The volume connection properties.
|
|
||||||
:returns: new size of the volume.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_all_available_volumes(self, connection_properties=None):
|
|
||||||
"""Return all volumes that exist in the search directory.
|
|
||||||
|
|
||||||
At connect_volume time, a Connector looks in a specific
|
|
||||||
directory to discover a volume's paths showing up.
|
|
||||||
This method's job is to return all paths in the directory
|
|
||||||
that connect_volume uses to find a volume.
|
|
||||||
|
|
||||||
This method is used in coordination with get_volume_paths()
|
|
||||||
to verify that volumes have gone away after disconnect_volume
|
|
||||||
has been called.
|
|
||||||
|
|
||||||
:param connection_properties: The dictionary that describes all
|
|
||||||
of the target volume attributes.
|
|
||||||
:type connection_properties: dict
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def check_IO_handle_valid(self, handle, data_type, protocol):
|
|
||||||
"""Check IO handle has correct data type."""
|
|
||||||
if (handle and not isinstance(handle, data_type)):
|
|
||||||
raise exception.InvalidIOHandleObject(
|
|
||||||
protocol=protocol,
|
|
||||||
actual_type=type(handle))
|
|
|
@ -1,314 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Generic linux Fibre Channel utilities."""
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick.initiator import linuxscsi
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxFibreChannel(linuxscsi.LinuxSCSI):
|
|
||||||
|
|
||||||
def has_fc_support(self):
|
|
||||||
FC_HOST_SYSFS_PATH = '/sys/class/fc_host'
|
|
||||||
if os.path.isdir(FC_HOST_SYSFS_PATH):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_hba_channel_scsi_target(self, hba):
|
|
||||||
"""Try to get the HBA channel and SCSI target for an HBA.
|
|
||||||
|
|
||||||
This method only works for Fibre Channel targets that implement a
|
|
||||||
single WWNN for all ports, so caller should expect us to return either
|
|
||||||
None or an empty list.
|
|
||||||
|
|
||||||
:returns: List or None
|
|
||||||
"""
|
|
||||||
# Leave only the number from the host_device field (ie: host6)
|
|
||||||
host_device = hba['host_device']
|
|
||||||
if host_device and len(host_device) > 4:
|
|
||||||
host_device = host_device[4:]
|
|
||||||
|
|
||||||
path = '/sys/class/fc_transport/target%s:' % host_device
|
|
||||||
cmd = 'grep %(wwnn)s %(path)s*/node_name' % {'wwnn': hba['node_name'],
|
|
||||||
'path': path}
|
|
||||||
try:
|
|
||||||
out, _err = self._execute(cmd)
|
|
||||||
return [line.split('/')[4].split(':')[1:]
|
|
||||||
for line in out.split('\n') if line.startswith(path)]
|
|
||||||
except Exception as exc:
|
|
||||||
LOG.debug('Could not get HBA channel and SCSI target ID, path: '
|
|
||||||
'%(path)s, reason: %(reason)s', {'path': path,
|
|
||||||
'reason': exc})
|
|
||||||
return None
|
|
||||||
|
|
||||||
def rescan_hosts(self, hbas, target_lun):
|
|
||||||
for hba in hbas:
|
|
||||||
# Try to get HBA channel and SCSI target to use as filters
|
|
||||||
cts = self._get_hba_channel_scsi_target(hba)
|
|
||||||
# If we couldn't get the channel and target use wildcards
|
|
||||||
if not cts:
|
|
||||||
cts = [('-', '-')]
|
|
||||||
for hba_channel, target_id in cts:
|
|
||||||
LOG.debug('Scanning host %(host)s (wwnn: %(wwnn)s, c: '
|
|
||||||
'%(channel)s, t: %(target)s, l: %(lun)s)',
|
|
||||||
{'host': hba['host_device'],
|
|
||||||
'wwnn': hba['node_name'], 'channel': hba_channel,
|
|
||||||
'target': target_id, 'lun': target_lun})
|
|
||||||
self.echo_scsi_command(
|
|
||||||
"/sys/class/scsi_host/%s/scan" % hba['host_device'],
|
|
||||||
"%(c)s %(t)s %(l)s" % {'c': hba_channel,
|
|
||||||
't': target_id,
|
|
||||||
'l': target_lun})
|
|
||||||
|
|
||||||
def get_fc_hbas(self):
|
|
||||||
"""Get the Fibre Channel HBA information."""
|
|
||||||
|
|
||||||
if not self.has_fc_support():
|
|
||||||
# there is no FC support in the kernel loaded
|
|
||||||
# so there is no need to even try to run systool
|
|
||||||
LOG.debug("No Fibre Channel support detected on system.")
|
|
||||||
return []
|
|
||||||
|
|
||||||
out = None
|
|
||||||
try:
|
|
||||||
out, _err = self._execute('systool', '-c', 'fc_host', '-v',
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
except putils.ProcessExecutionError as exc:
|
|
||||||
# This handles the case where rootwrap is used
|
|
||||||
# and systool is not installed
|
|
||||||
# 96 = nova.cmd.rootwrap.RC_NOEXECFOUND:
|
|
||||||
if exc.exit_code == 96:
|
|
||||||
LOG.warning("systool is not installed")
|
|
||||||
return []
|
|
||||||
except OSError as exc:
|
|
||||||
# This handles the case where rootwrap is NOT used
|
|
||||||
# and systool is not installed
|
|
||||||
if exc.errno == errno.ENOENT:
|
|
||||||
LOG.warning("systool is not installed")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# No FC HBAs were found
|
|
||||||
if out is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
lines = out.split('\n')
|
|
||||||
# ignore the first 2 lines
|
|
||||||
lines = lines[2:]
|
|
||||||
hbas = []
|
|
||||||
hba = {}
|
|
||||||
lastline = None
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
# 2 newlines denotes a new hba port
|
|
||||||
if line == '' and lastline == '':
|
|
||||||
if len(hba) > 0:
|
|
||||||
hbas.append(hba)
|
|
||||||
hba = {}
|
|
||||||
else:
|
|
||||||
val = line.split('=')
|
|
||||||
if len(val) == 2:
|
|
||||||
key = val[0].strip().replace(" ", "")
|
|
||||||
value = val[1].strip()
|
|
||||||
hba[key] = value.replace('"', '')
|
|
||||||
lastline = line
|
|
||||||
|
|
||||||
return hbas
|
|
||||||
|
|
||||||
def get_fc_hbas_info(self):
|
|
||||||
"""Get Fibre Channel WWNs and device paths from the system, if any."""
|
|
||||||
|
|
||||||
# Note(walter-boring) modern Linux kernels contain the FC HBA's in /sys
|
|
||||||
# and are obtainable via the systool app
|
|
||||||
hbas = self.get_fc_hbas()
|
|
||||||
|
|
||||||
hbas_info = []
|
|
||||||
for hba in hbas:
|
|
||||||
wwpn = hba['port_name'].replace('0x', '')
|
|
||||||
wwnn = hba['node_name'].replace('0x', '')
|
|
||||||
device_path = hba['ClassDevicepath']
|
|
||||||
device = hba['ClassDevice']
|
|
||||||
hbas_info.append({'port_name': wwpn,
|
|
||||||
'node_name': wwnn,
|
|
||||||
'host_device': device,
|
|
||||||
'device_path': device_path})
|
|
||||||
return hbas_info
|
|
||||||
|
|
||||||
def get_fc_wwpns(self):
|
|
||||||
"""Get Fibre Channel WWPNs from the system, if any."""
|
|
||||||
|
|
||||||
# Note(walter-boring) modern Linux kernels contain the FC HBA's in /sys
|
|
||||||
# and are obtainable via the systool app
|
|
||||||
hbas = self.get_fc_hbas()
|
|
||||||
|
|
||||||
wwpns = []
|
|
||||||
for hba in hbas:
|
|
||||||
if hba['port_state'] == 'Online':
|
|
||||||
wwpn = hba['port_name'].replace('0x', '')
|
|
||||||
wwpns.append(wwpn)
|
|
||||||
|
|
||||||
return wwpns
|
|
||||||
|
|
||||||
def get_fc_wwnns(self):
|
|
||||||
"""Get Fibre Channel WWNNs from the system, if any."""
|
|
||||||
|
|
||||||
# Note(walter-boring) modern Linux kernels contain the FC HBA's in /sys
|
|
||||||
# and are obtainable via the systool app
|
|
||||||
hbas = self.get_fc_hbas()
|
|
||||||
|
|
||||||
wwnns = []
|
|
||||||
for hba in hbas:
|
|
||||||
if hba['port_state'] == 'Online':
|
|
||||||
wwnn = hba['node_name'].replace('0x', '')
|
|
||||||
wwnns.append(wwnn)
|
|
||||||
|
|
||||||
return wwnns
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxFibreChannelS390X(LinuxFibreChannel):
|
|
||||||
def get_fc_hbas_info(self):
|
|
||||||
"""Get Fibre Channel WWNs and device paths from the system, if any."""
|
|
||||||
|
|
||||||
hbas = self.get_fc_hbas()
|
|
||||||
|
|
||||||
hbas_info = []
|
|
||||||
for hba in hbas:
|
|
||||||
if hba['port_state'] == 'Online':
|
|
||||||
wwpn = hba['port_name'].replace('0x', '')
|
|
||||||
wwnn = hba['node_name'].replace('0x', '')
|
|
||||||
device_path = hba['ClassDevicepath']
|
|
||||||
device = hba['ClassDevice']
|
|
||||||
hbas_info.append({'port_name': wwpn,
|
|
||||||
'node_name': wwnn,
|
|
||||||
'host_device': device,
|
|
||||||
'device_path': device_path})
|
|
||||||
return hbas_info
|
|
||||||
|
|
||||||
def configure_scsi_device(self, device_number, target_wwn, lun):
|
|
||||||
"""Write the LUN to the port's unit_add attribute.
|
|
||||||
|
|
||||||
If auto-discovery of Fibre-Channel target ports is
|
|
||||||
disabled on s390 platforms, ports need to be added to
|
|
||||||
the configuration.
|
|
||||||
If auto-discovery of LUNs is disabled on s390 platforms
|
|
||||||
luns need to be added to the configuration through the
|
|
||||||
unit_add interface
|
|
||||||
"""
|
|
||||||
LOG.debug("Configure lun for s390: device_number=%(device_num)s "
|
|
||||||
"target_wwn=%(target_wwn)s target_lun=%(target_lun)s",
|
|
||||||
{'device_num': device_number,
|
|
||||||
'target_wwn': target_wwn,
|
|
||||||
'target_lun': lun})
|
|
||||||
filepath = ("/sys/bus/ccw/drivers/zfcp/%s/%s" %
|
|
||||||
(device_number, target_wwn))
|
|
||||||
if not (os.path.exists(filepath)):
|
|
||||||
zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/port_rescan" %
|
|
||||||
(device_number))
|
|
||||||
LOG.debug("port_rescan call for s390: %s", zfcp_device_command)
|
|
||||||
try:
|
|
||||||
self.echo_scsi_command(zfcp_device_command, "1")
|
|
||||||
except putils.ProcessExecutionError as exc:
|
|
||||||
LOG.warning("port_rescan call for s390 failed exit"
|
|
||||||
" %(code)s, stderr %(stderr)s",
|
|
||||||
{'code': exc.exit_code, 'stderr': exc.stderr})
|
|
||||||
|
|
||||||
zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/%s/unit_add" %
|
|
||||||
(device_number, target_wwn))
|
|
||||||
LOG.debug("unit_add call for s390 execute: %s", zfcp_device_command)
|
|
||||||
try:
|
|
||||||
self.echo_scsi_command(zfcp_device_command, lun)
|
|
||||||
except putils.ProcessExecutionError as exc:
|
|
||||||
LOG.warning("unit_add call for s390 failed exit %(code)s, "
|
|
||||||
"stderr %(stderr)s",
|
|
||||||
{'code': exc.exit_code, 'stderr': exc.stderr})
|
|
||||||
|
|
||||||
def deconfigure_scsi_device(self, device_number, target_wwn, lun):
|
|
||||||
"""Write the LUN to the port's unit_remove attribute.
|
|
||||||
|
|
||||||
If auto-discovery of LUNs is disabled on s390 platforms
|
|
||||||
luns need to be removed from the configuration through the
|
|
||||||
unit_remove interface
|
|
||||||
"""
|
|
||||||
LOG.debug("Deconfigure lun for s390: "
|
|
||||||
"device_number=%(device_num)s "
|
|
||||||
"target_wwn=%(target_wwn)s target_lun=%(target_lun)s",
|
|
||||||
{'device_num': device_number,
|
|
||||||
'target_wwn': target_wwn,
|
|
||||||
'target_lun': lun})
|
|
||||||
zfcp_device_command = ("/sys/bus/ccw/drivers/zfcp/%s/%s/unit_remove" %
|
|
||||||
(device_number, target_wwn))
|
|
||||||
LOG.debug("unit_remove call for s390 execute: %s", zfcp_device_command)
|
|
||||||
try:
|
|
||||||
self.echo_scsi_command(zfcp_device_command, lun)
|
|
||||||
except putils.ProcessExecutionError as exc:
|
|
||||||
LOG.warning("unit_remove call for s390 failed exit %(code)s, "
|
|
||||||
"stderr %(stderr)s",
|
|
||||||
{'code': exc.exit_code, 'stderr': exc.stderr})
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxFibreChannelPPC64(LinuxFibreChannel):
|
|
||||||
|
|
||||||
def _get_hba_channel_scsi_target(self, hba, wwpn):
|
|
||||||
"""Try to get the HBA channel and SCSI target for an HBA.
|
|
||||||
|
|
||||||
This method works for Fibre Channel targets iterating over all the
|
|
||||||
target wwpn port and finding the c, t, l. so caller should expect us to
|
|
||||||
return either None or an empty list.
|
|
||||||
"""
|
|
||||||
# Leave only the number from the host_device field (ie: host6)
|
|
||||||
host_device = hba['host_device']
|
|
||||||
if host_device and len(host_device) > 4:
|
|
||||||
host_device = host_device[4:]
|
|
||||||
path = '/sys/class/fc_transport/target%s:' % host_device
|
|
||||||
cmd = 'grep -l %(wwpn)s %(path)s*/port_name' % {'wwpn': wwpn,
|
|
||||||
'path': path}
|
|
||||||
try:
|
|
||||||
out, _err = self._execute(cmd, shell=True)
|
|
||||||
return [line.split('/')[4].split(':')[1:]
|
|
||||||
for line in out.split('\n') if line.startswith(path)]
|
|
||||||
except Exception as exc:
|
|
||||||
LOG.error("Could not get HBA channel and SCSI target ID, "
|
|
||||||
"reason: %s", exc)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def rescan_hosts(self, hbas, target_lun):
|
|
||||||
for hba in hbas:
|
|
||||||
# Try to get HBA channel and SCSI target to use as filters
|
|
||||||
for wwpn in hba['target_wwn']:
|
|
||||||
cts = self._get_hba_channel_scsi_target(hba, wwpn)
|
|
||||||
# If we couldn't get the channel and target use wildcards
|
|
||||||
if not cts:
|
|
||||||
cts = [('-', '-')]
|
|
||||||
for hba_channel, target_id in cts:
|
|
||||||
LOG.debug('Scanning host %(host)s (wwpn: %(wwpn)s, c: '
|
|
||||||
'%(channel)s, t: %(target)s, l: %(lun)s)',
|
|
||||||
{'host': hba['host_device'],
|
|
||||||
'wwpn': hba['target_wwn'],
|
|
||||||
'channel': hba_channel,
|
|
||||||
'target': target_id,
|
|
||||||
'lun': target_lun})
|
|
||||||
self.echo_scsi_command(
|
|
||||||
"/sys/class/scsi_host/%s/scan" % hba['host_device'],
|
|
||||||
"%(c)s %(t)s %(l)s" % {'c': hba_channel,
|
|
||||||
't': target_id,
|
|
||||||
'l': target_lun})
|
|
|
@ -1,231 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Generic RBD connection utilities."""
|
|
||||||
|
|
||||||
import io
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
try:
|
|
||||||
import rados
|
|
||||||
import rbd
|
|
||||||
except ImportError:
|
|
||||||
rados = None
|
|
||||||
rbd = None
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RBDClient(object):
|
|
||||||
|
|
||||||
def __init__(self, user, pool, *args, **kwargs):
|
|
||||||
|
|
||||||
self.rbd_user = user
|
|
||||||
self.rbd_pool = pool
|
|
||||||
|
|
||||||
for attr in ['rbd_user', 'rbd_pool']:
|
|
||||||
val = getattr(self, attr)
|
|
||||||
if val is not None:
|
|
||||||
setattr(self, attr, utils.convert_str(val))
|
|
||||||
|
|
||||||
# allow these to be overridden for testing
|
|
||||||
self.rados = kwargs.get('rados', rados)
|
|
||||||
self.rbd = kwargs.get('rbd', rbd)
|
|
||||||
|
|
||||||
if self.rados is None:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
err=_('rados module required'))
|
|
||||||
if self.rbd is None:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
err=_('rbd module required'))
|
|
||||||
|
|
||||||
self.rbd_conf = kwargs.get('conffile', '/etc/ceph/ceph.conf')
|
|
||||||
self.rbd_cluster_name = kwargs.get('rbd_cluster_name', 'ceph')
|
|
||||||
self.rados_connect_timeout = kwargs.get('rados_connect_timeout', -1)
|
|
||||||
|
|
||||||
self.client, self.ioctx = self.connect()
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, type_, value, traceback):
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
LOG.debug("opening connection to ceph cluster (timeout=%s).",
|
|
||||||
self.rados_connect_timeout)
|
|
||||||
client = self.rados.Rados(rados_id=self.rbd_user,
|
|
||||||
clustername=self.rbd_cluster_name,
|
|
||||||
conffile=self.rbd_conf)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.rados_connect_timeout >= 0:
|
|
||||||
client.connect(
|
|
||||||
timeout=self.rados_connect_timeout)
|
|
||||||
else:
|
|
||||||
client.connect()
|
|
||||||
ioctx = client.open_ioctx(self.rbd_pool)
|
|
||||||
return client, ioctx
|
|
||||||
except self.rados.Error:
|
|
||||||
msg = _("Error connecting to ceph cluster.")
|
|
||||||
LOG.exception(msg)
|
|
||||||
# shutdown cannot raise an exception
|
|
||||||
client.shutdown()
|
|
||||||
raise exception.BrickException(message=msg)
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
# closing an ioctx cannot raise an exception
|
|
||||||
self.ioctx.close()
|
|
||||||
self.client.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
class RBDVolume(object):
|
|
||||||
"""Context manager for dealing with an existing rbd volume."""
|
|
||||||
|
|
||||||
def __init__(self, client, name, snapshot=None, read_only=False):
|
|
||||||
if snapshot is not None:
|
|
||||||
snapshot = utils.convert_str(snapshot)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.image = client.rbd.Image(client.ioctx,
|
|
||||||
utils.convert_str(name),
|
|
||||||
snapshot=snapshot,
|
|
||||||
read_only=read_only)
|
|
||||||
except client.rbd.Error:
|
|
||||||
LOG.exception("error opening rbd image %s", name)
|
|
||||||
client.disconnect()
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.client = client
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
try:
|
|
||||||
self.image.close()
|
|
||||||
finally:
|
|
||||||
self.client.disconnect()
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, type_, value, traceback):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def __getattr__(self, attrib):
|
|
||||||
return getattr(self.image, attrib)
|
|
||||||
|
|
||||||
|
|
||||||
class RBDImageMetadata(object):
|
|
||||||
"""RBD image metadata to be used with RBDVolumeIOWrapper."""
|
|
||||||
def __init__(self, image, pool, user, conf):
|
|
||||||
self.image = image
|
|
||||||
self.pool = utils.convert_str(pool or '')
|
|
||||||
self.user = utils.convert_str(user or '')
|
|
||||||
self.conf = utils.convert_str(conf or '')
|
|
||||||
|
|
||||||
|
|
||||||
class RBDVolumeIOWrapper(io.RawIOBase):
|
|
||||||
"""Enables LibRBD.Image objects to be treated as Python IO objects.
|
|
||||||
|
|
||||||
Calling unimplemented interfaces will raise IOError.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, rbd_volume):
|
|
||||||
super(RBDVolumeIOWrapper, self).__init__()
|
|
||||||
self._rbd_volume = rbd_volume
|
|
||||||
self._offset = 0
|
|
||||||
|
|
||||||
def _inc_offset(self, length):
|
|
||||||
self._offset += length
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rbd_image(self):
|
|
||||||
return self._rbd_volume.image
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rbd_user(self):
|
|
||||||
return self._rbd_volume.user
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rbd_pool(self):
|
|
||||||
return self._rbd_volume.pool
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rbd_conf(self):
|
|
||||||
return self._rbd_volume.conf
|
|
||||||
|
|
||||||
def read(self, length=None):
|
|
||||||
offset = self._offset
|
|
||||||
total = self._rbd_volume.image.size()
|
|
||||||
|
|
||||||
# NOTE(dosaboy): posix files do not barf if you read beyond their
|
|
||||||
# length (they just return nothing) but rbd images do so we need to
|
|
||||||
# return empty string if we have reached the end of the image.
|
|
||||||
if (offset >= total):
|
|
||||||
return b''
|
|
||||||
|
|
||||||
if length is None:
|
|
||||||
length = total
|
|
||||||
|
|
||||||
if (offset + length) > total:
|
|
||||||
length = total - offset
|
|
||||||
|
|
||||||
self._inc_offset(length)
|
|
||||||
return self._rbd_volume.image.read(int(offset), int(length))
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
self._rbd_volume.image.write(data, self._offset)
|
|
||||||
self._inc_offset(len(data))
|
|
||||||
|
|
||||||
def seekable(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def seek(self, offset, whence=0):
|
|
||||||
if whence == 0:
|
|
||||||
new_offset = offset
|
|
||||||
elif whence == 1:
|
|
||||||
new_offset = self._offset + offset
|
|
||||||
elif whence == 2:
|
|
||||||
new_offset = self._rbd_volume.image.size()
|
|
||||||
new_offset += offset
|
|
||||||
else:
|
|
||||||
raise IOError(_("Invalid argument - whence=%s not supported") %
|
|
||||||
(whence))
|
|
||||||
|
|
||||||
if (new_offset < 0):
|
|
||||||
raise IOError(_("Invalid argument"))
|
|
||||||
|
|
||||||
self._offset = new_offset
|
|
||||||
|
|
||||||
def tell(self):
|
|
||||||
return self._offset
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
try:
|
|
||||||
self._rbd_volume.image.flush()
|
|
||||||
except AttributeError:
|
|
||||||
LOG.warning("flush() not supported in this version of librbd")
|
|
||||||
|
|
||||||
def fileno(self):
|
|
||||||
"""RBD does not have support for fileno() so we raise IOError.
|
|
||||||
|
|
||||||
Raising IOError is recommended way to notify caller that interface is
|
|
||||||
not supported - see http://docs.python.org/2/library/io.html#io.IOBase
|
|
||||||
"""
|
|
||||||
raise IOError(_("fileno() not supported by RBD()"))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.rbd_image.close()
|
|
|
@ -1,617 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Generic linux scsi subsystem and Multipath utilities.
|
|
||||||
|
|
||||||
Note, this is not iSCSI.
|
|
||||||
"""
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import six
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import executor
|
|
||||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
MULTIPATH_ERROR_REGEX = re.compile("\w{3} \d+ \d\d:\d\d:\d\d \|.*$")
|
|
||||||
MULTIPATH_WWID_REGEX = re.compile("\((?P<wwid>.+)\)")
|
|
||||||
MULTIPATH_DEVICE_ACTIONS = ['unchanged:', 'reject:', 'reload:',
|
|
||||||
'switchpg:', 'rename:', 'create:',
|
|
||||||
'resize:']
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxSCSI(executor.Executor):
|
|
||||||
# As found in drivers/scsi/scsi_lib.c
|
|
||||||
WWN_TYPES = {'t10.': '1', 'eui.': '2', 'naa.': '3'}
|
|
||||||
|
|
||||||
def echo_scsi_command(self, path, content):
|
|
||||||
"""Used to echo strings to scsi subsystem."""
|
|
||||||
|
|
||||||
args = ["-a", path]
|
|
||||||
kwargs = dict(process_input=content,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
self._execute('tee', *args, **kwargs)
|
|
||||||
|
|
||||||
def get_name_from_path(self, path):
|
|
||||||
"""Translates /dev/disk/by-path/ entry to /dev/sdX."""
|
|
||||||
|
|
||||||
name = os.path.realpath(path)
|
|
||||||
if name.startswith("/dev/"):
|
|
||||||
return name
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def remove_scsi_device(self, device, force=False, exc=None):
|
|
||||||
"""Removes a scsi device based upon /dev/sdX name."""
|
|
||||||
path = "/sys/block/%s/device/delete" % device.replace("/dev/", "")
|
|
||||||
if os.path.exists(path):
|
|
||||||
exc = exception.ExceptionChainer() if exc is None else exc
|
|
||||||
# flush any outstanding IO first
|
|
||||||
with exc.context(force, 'Flushing %s failed', device):
|
|
||||||
self.flush_device_io(device)
|
|
||||||
|
|
||||||
LOG.debug("Remove SCSI device %(device)s with %(path)s",
|
|
||||||
{'device': device, 'path': path})
|
|
||||||
with exc.context(force, 'Removing %s failed', device):
|
|
||||||
self.echo_scsi_command(path, "1")
|
|
||||||
|
|
||||||
@utils.retry(exceptions=exception.VolumePathNotRemoved)
|
|
||||||
def wait_for_volumes_removal(self, volumes_names):
|
|
||||||
"""Wait for device paths to be removed from the system."""
|
|
||||||
str_names = ', '.join(volumes_names)
|
|
||||||
LOG.debug('Checking to see if SCSI volumes %s have been removed.',
|
|
||||||
str_names)
|
|
||||||
exist = [volume_name for volume_name in volumes_names
|
|
||||||
if os.path.exists('/dev/' + volume_name)]
|
|
||||||
if exist:
|
|
||||||
LOG.debug('%s still exist.', ', '.join(exist))
|
|
||||||
raise exception.VolumePathNotRemoved(volume_path=exist)
|
|
||||||
LOG.debug("SCSI volumes %s have been removed.", str_names)
|
|
||||||
|
|
||||||
def get_device_info(self, device):
|
|
||||||
(out, _err) = self._execute('sg_scan', device, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
dev_info = {'device': device, 'host': None,
|
|
||||||
'channel': None, 'id': None, 'lun': None}
|
|
||||||
if out:
|
|
||||||
line = out.strip()
|
|
||||||
line = line.replace(device + ": ", "")
|
|
||||||
info = line.split(" ")
|
|
||||||
|
|
||||||
for item in info:
|
|
||||||
if '=' in item:
|
|
||||||
pair = item.split('=')
|
|
||||||
dev_info[pair[0]] = pair[1]
|
|
||||||
elif 'scsi' in item:
|
|
||||||
dev_info['host'] = item.replace('scsi', '')
|
|
||||||
|
|
||||||
return dev_info
|
|
||||||
|
|
||||||
def get_sysfs_wwn(self, device_names):
|
|
||||||
"""Return the wwid from sysfs in any of devices in udev format."""
|
|
||||||
wwid = self.get_sysfs_wwid(device_names)
|
|
||||||
glob_str = '/dev/disk/by-id/scsi-'
|
|
||||||
wwn_paths = glob.glob(glob_str + '*')
|
|
||||||
# If we don't have multiple designators on page 0x83
|
|
||||||
if wwid and glob_str + wwid in wwn_paths:
|
|
||||||
return wwid
|
|
||||||
|
|
||||||
# If we have multiple designators follow the symlinks
|
|
||||||
for wwn_path in wwn_paths:
|
|
||||||
try:
|
|
||||||
if os.path.islink(wwn_path) and os.stat(wwn_path):
|
|
||||||
path = os.path.realpath(wwn_path)
|
|
||||||
if path.startswith('/dev/') and path[5:] in device_names:
|
|
||||||
return wwn_path[len(glob_str):]
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def get_sysfs_wwid(self, device_names):
|
|
||||||
"""Return the wwid from sysfs in any of devices in udev format."""
|
|
||||||
for device_name in device_names:
|
|
||||||
try:
|
|
||||||
with open('/sys/block/%s/device/wwid' % device_name) as f:
|
|
||||||
wwid = f.read().strip()
|
|
||||||
except IOError:
|
|
||||||
continue
|
|
||||||
# The sysfs wwid has the wwn type in string format as a prefix,
|
|
||||||
# but udev uses its numerical representation as returned by
|
|
||||||
# scsi_id's page 0x83, so we need to map it
|
|
||||||
udev_wwid = self.WWN_TYPES.get(wwid[:4], '8') + wwid[4:]
|
|
||||||
return udev_wwid
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def get_scsi_wwn(self, path):
|
|
||||||
"""Read the WWN from page 0x83 value for a SCSI device."""
|
|
||||||
|
|
||||||
(out, _err) = self._execute('/lib/udev/scsi_id', '--page', '0x83',
|
|
||||||
'--whitelisted', path,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
return out.strip()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_multipath_running(enforce_multipath, root_helper, execute=None):
|
|
||||||
try:
|
|
||||||
if execute is None:
|
|
||||||
execute = priv_rootwrap.execute
|
|
||||||
execute('multipathd', 'show', 'status',
|
|
||||||
run_as_root=True, root_helper=root_helper)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.error('multipathd is not running: exit code %(err)s',
|
|
||||||
{'err': err.exit_code})
|
|
||||||
if enforce_multipath:
|
|
||||||
raise
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_dm_name(self, dm):
|
|
||||||
"""Get the Device map name given the device name of the dm on sysfs.
|
|
||||||
|
|
||||||
:param dm: Device map name as seen in sysfs. ie: 'dm-0'
|
|
||||||
:returns: String with the name, or empty string if not available.
|
|
||||||
ie: '36e843b658476b7ed5bc1d4d10d9b1fde'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open('/sys/block/' + dm + '/dm/name') as f:
|
|
||||||
return f.read().strip()
|
|
||||||
except IOError:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def find_sysfs_multipath_dm(self, device_names):
|
|
||||||
"""Find the dm device name given a list of device names
|
|
||||||
|
|
||||||
:param device_names: Iterable with device names, not paths. ie: ['sda']
|
|
||||||
:returns: String with the dm name or None if not found. ie: 'dm-0'
|
|
||||||
"""
|
|
||||||
glob_str = '/sys/block/%s/holders/dm-*'
|
|
||||||
for dev_name in device_names:
|
|
||||||
dms = glob.glob(glob_str % dev_name)
|
|
||||||
if dms:
|
|
||||||
__, device_name, __, dm = dms[0].rsplit('/', 3)
|
|
||||||
return dm
|
|
||||||
return None
|
|
||||||
|
|
||||||
def remove_connection(self, devices_names, is_multipath, force=False,
|
|
||||||
exc=None):
|
|
||||||
"""Remove LUNs and multipath associated with devices names.
|
|
||||||
|
|
||||||
:param devices_names: Iterable with real device names ('sda', 'sdb')
|
|
||||||
:param is_multipath: Whether this is a multipath connection or not
|
|
||||||
:param force: Whether to forcefully disconnect even if flush fails.
|
|
||||||
:param exc: ExceptionChainer where to add exceptions if forcing
|
|
||||||
:returns: Multipath device map name if found and not flushed
|
|
||||||
"""
|
|
||||||
if not devices_names:
|
|
||||||
return
|
|
||||||
multipath_name = None
|
|
||||||
exc = exception.ExceptionChainer() if exc is None else exc
|
|
||||||
LOG.debug('Removing %(type)s devices %(devices)s',
|
|
||||||
{'type': 'multipathed' if is_multipath else 'single pathed',
|
|
||||||
'devices': ', '.join(devices_names)})
|
|
||||||
|
|
||||||
if is_multipath:
|
|
||||||
multipath_dm = self.find_sysfs_multipath_dm(devices_names)
|
|
||||||
multipath_name = multipath_dm and self.get_dm_name(multipath_dm)
|
|
||||||
if multipath_name:
|
|
||||||
with exc.context(force, 'Flushing %s failed', multipath_name):
|
|
||||||
self.flush_multipath_device(multipath_name)
|
|
||||||
multipath_name = None
|
|
||||||
|
|
||||||
for device_name in devices_names:
|
|
||||||
self.remove_scsi_device('/dev/' + device_name, force, exc)
|
|
||||||
|
|
||||||
# Wait until the symlinks are removed
|
|
||||||
with exc.context(force, 'Some devices remain from %s', devices_names):
|
|
||||||
try:
|
|
||||||
self.wait_for_volumes_removal(devices_names)
|
|
||||||
finally:
|
|
||||||
# Since we use /dev/disk/by-id/scsi- links to get the wwn we
|
|
||||||
# must ensure they are always removed.
|
|
||||||
self._remove_scsi_symlinks(devices_names)
|
|
||||||
return multipath_name
|
|
||||||
|
|
||||||
def _remove_scsi_symlinks(self, devices_names):
|
|
||||||
devices = ['/dev/' + dev for dev in devices_names]
|
|
||||||
links = glob.glob('/dev/disk/by-id/scsi-*')
|
|
||||||
unlink = [link for link in links
|
|
||||||
if os.path.realpath(link) in devices]
|
|
||||||
if unlink:
|
|
||||||
priv_rootwrap.unlink_root(no_errors=True, *unlink)
|
|
||||||
|
|
||||||
def flush_device_io(self, device):
|
|
||||||
"""This is used to flush any remaining IO in the buffers."""
|
|
||||||
if os.path.exists(device):
|
|
||||||
try:
|
|
||||||
# NOTE(geguileo): With 30% connection error rates flush can get
|
|
||||||
# stuck, set timeout to prevent it from hanging here forever.
|
|
||||||
# Retry twice after 20 and 40 seconds.
|
|
||||||
LOG.debug("Flushing IO for device %s", device)
|
|
||||||
self._execute('blockdev', '--flushbufs', device,
|
|
||||||
run_as_root=True, attempts=3, timeout=300,
|
|
||||||
interval=10, root_helper=self._root_helper)
|
|
||||||
except putils.ProcessExecutionError as exc:
|
|
||||||
LOG.warning("Failed to flush IO buffers prior to removing "
|
|
||||||
"device: %(code)s", {'code': exc.exit_code})
|
|
||||||
raise
|
|
||||||
|
|
||||||
def flush_multipath_device(self, device_map_name):
|
|
||||||
LOG.debug("Flush multipath device %s", device_map_name)
|
|
||||||
# NOTE(geguileo): With 30% connection error rates flush can get stuck,
|
|
||||||
# set timeout to prevent it from hanging here forever. Retry twice
|
|
||||||
# after 20 and 40 seconds.
|
|
||||||
self._execute('multipath', '-f', device_map_name, run_as_root=True,
|
|
||||||
attempts=3, timeout=300, interval=10,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
|
|
||||||
@utils.retry(exceptions=exception.VolumeDeviceNotFound)
|
|
||||||
def wait_for_path(self, volume_path):
|
|
||||||
"""Wait for a path to show up."""
|
|
||||||
LOG.debug("Checking to see if %s exists yet.",
|
|
||||||
volume_path)
|
|
||||||
if not os.path.exists(volume_path):
|
|
||||||
LOG.debug("%(path)s doesn't exists yet.", {'path': volume_path})
|
|
||||||
raise exception.VolumeDeviceNotFound(
|
|
||||||
device=volume_path)
|
|
||||||
else:
|
|
||||||
LOG.debug("%s has shown up.", volume_path)
|
|
||||||
|
|
||||||
@utils.retry(exceptions=exception.BlockDeviceReadOnly, retries=5)
|
|
||||||
def wait_for_rw(self, wwn, device_path):
|
|
||||||
"""Wait for block device to be Read-Write."""
|
|
||||||
LOG.debug("Checking to see if %s is read-only.",
|
|
||||||
device_path)
|
|
||||||
out, info = self._execute('lsblk', '-o', 'NAME,RO', '-l', '-n')
|
|
||||||
LOG.debug("lsblk output: %s", out)
|
|
||||||
blkdevs = out.splitlines()
|
|
||||||
for blkdev in blkdevs:
|
|
||||||
# Entries might look like:
|
|
||||||
#
|
|
||||||
# "3624a93709a738ed78583fd120013902b (dm-1) 1"
|
|
||||||
#
|
|
||||||
# or
|
|
||||||
#
|
|
||||||
# "sdd 0"
|
|
||||||
#
|
|
||||||
# We are looking for the first and last part of them. For FC
|
|
||||||
# multipath devices the name is in the format of '<WWN> (dm-<ID>)'
|
|
||||||
blkdev_parts = blkdev.split(' ')
|
|
||||||
ro = blkdev_parts[-1]
|
|
||||||
name = blkdev_parts[0]
|
|
||||||
|
|
||||||
# We must validate that all pieces of the dm-# device are rw,
|
|
||||||
# if some are still ro it can cause problems.
|
|
||||||
if wwn in name and int(ro) == 1:
|
|
||||||
LOG.debug("Block device %s is read-only", device_path)
|
|
||||||
self._execute('multipath', '-r', check_exit_code=[0, 1, 21],
|
|
||||||
run_as_root=True, root_helper=self._root_helper)
|
|
||||||
raise exception.BlockDeviceReadOnly(
|
|
||||||
device=device_path)
|
|
||||||
else:
|
|
||||||
LOG.debug("Block device %s is not read-only.", device_path)
|
|
||||||
|
|
||||||
def find_multipath_device_path(self, wwn):
|
|
||||||
"""Look for the multipath device file for a volume WWN.
|
|
||||||
|
|
||||||
Multipath devices can show up in several places on
|
|
||||||
a linux system.
|
|
||||||
|
|
||||||
1) When multipath friendly names are ON:
|
|
||||||
a device file will show up in
|
|
||||||
/dev/disk/by-id/dm-uuid-mpath-<WWN>
|
|
||||||
/dev/disk/by-id/dm-name-mpath<N>
|
|
||||||
/dev/disk/by-id/scsi-mpath<N>
|
|
||||||
/dev/mapper/mpath<N>
|
|
||||||
|
|
||||||
2) When multipath friendly names are OFF:
|
|
||||||
/dev/disk/by-id/dm-uuid-mpath-<WWN>
|
|
||||||
/dev/disk/by-id/scsi-<WWN>
|
|
||||||
/dev/mapper/<WWN>
|
|
||||||
|
|
||||||
"""
|
|
||||||
LOG.info("Find Multipath device file for volume WWN %(wwn)s",
|
|
||||||
{'wwn': wwn})
|
|
||||||
# First look for the common path
|
|
||||||
wwn_dict = {'wwn': wwn}
|
|
||||||
path = "/dev/disk/by-id/dm-uuid-mpath-%(wwn)s" % wwn_dict
|
|
||||||
try:
|
|
||||||
self.wait_for_path(path)
|
|
||||||
return path
|
|
||||||
except exception.VolumeDeviceNotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# for some reason the common path wasn't found
|
|
||||||
# lets try the dev mapper path
|
|
||||||
path = "/dev/mapper/%(wwn)s" % wwn_dict
|
|
||||||
try:
|
|
||||||
self.wait_for_path(path)
|
|
||||||
return path
|
|
||||||
except exception.VolumeDeviceNotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# couldn't find a path
|
|
||||||
LOG.warning("couldn't find a valid multipath device path for "
|
|
||||||
"%(wwn)s", wwn_dict)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def find_multipath_device(self, device):
|
|
||||||
"""Discover multipath devices for a mpath device.
|
|
||||||
|
|
||||||
This uses the slow multipath -l command to find a
|
|
||||||
multipath device description, then screen scrapes
|
|
||||||
the output to discover the multipath device name
|
|
||||||
and it's devices.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
mdev = None
|
|
||||||
devices = []
|
|
||||||
out = None
|
|
||||||
try:
|
|
||||||
(out, _err) = self._execute('multipath', '-l', device,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
except putils.ProcessExecutionError as exc:
|
|
||||||
LOG.warning("multipath call failed exit %(code)s",
|
|
||||||
{'code': exc.exit_code})
|
|
||||||
raise exception.CommandExecutionFailed(
|
|
||||||
cmd='multipath -l %s' % device)
|
|
||||||
|
|
||||||
if out:
|
|
||||||
lines = out.strip()
|
|
||||||
lines = lines.split("\n")
|
|
||||||
lines = [line for line in lines
|
|
||||||
if not re.match(MULTIPATH_ERROR_REGEX, line)]
|
|
||||||
if lines:
|
|
||||||
|
|
||||||
mdev_name = lines[0].split(" ")[0]
|
|
||||||
|
|
||||||
if mdev_name in MULTIPATH_DEVICE_ACTIONS:
|
|
||||||
mdev_name = lines[0].split(" ")[1]
|
|
||||||
|
|
||||||
mdev = '/dev/mapper/%s' % mdev_name
|
|
||||||
|
|
||||||
# Confirm that the device is present.
|
|
||||||
try:
|
|
||||||
os.stat(mdev)
|
|
||||||
except OSError:
|
|
||||||
LOG.warning("Couldn't find multipath device %s",
|
|
||||||
mdev)
|
|
||||||
return None
|
|
||||||
|
|
||||||
wwid_search = MULTIPATH_WWID_REGEX.search(lines[0])
|
|
||||||
if wwid_search is not None:
|
|
||||||
mdev_id = wwid_search.group('wwid')
|
|
||||||
else:
|
|
||||||
mdev_id = mdev_name
|
|
||||||
|
|
||||||
LOG.debug("Found multipath device = %(mdev)s",
|
|
||||||
{'mdev': mdev})
|
|
||||||
device_lines = lines[3:]
|
|
||||||
for dev_line in device_lines:
|
|
||||||
if dev_line.find("policy") != -1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
dev_line = dev_line.lstrip(' |-`')
|
|
||||||
dev_info = dev_line.split()
|
|
||||||
address = dev_info[0].split(":")
|
|
||||||
|
|
||||||
dev = {'device': '/dev/%s' % dev_info[1],
|
|
||||||
'host': address[0], 'channel': address[1],
|
|
||||||
'id': address[2], 'lun': address[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.append(dev)
|
|
||||||
|
|
||||||
if mdev is not None:
|
|
||||||
info = {"device": mdev,
|
|
||||||
"id": mdev_id,
|
|
||||||
"name": mdev_name,
|
|
||||||
"devices": devices}
|
|
||||||
return info
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_device_size(self, device):
|
|
||||||
"""Get the size in bytes of a volume."""
|
|
||||||
(out, _err) = self._execute('blockdev', '--getsize64',
|
|
||||||
device, run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
var = six.text_type(out.strip())
|
|
||||||
if var.isnumeric():
|
|
||||||
return int(var)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def multipath_reconfigure(self):
|
|
||||||
"""Issue a multipathd reconfigure.
|
|
||||||
|
|
||||||
When attachments come and go, the multipathd seems
|
|
||||||
to get lost and not see the maps. This causes
|
|
||||||
resize map to fail 100%. To overcome this we have
|
|
||||||
to issue a reconfigure prior to resize map.
|
|
||||||
"""
|
|
||||||
(out, _err) = self._execute('multipathd', 'reconfigure',
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def multipath_resize_map(self, mpath_id):
|
|
||||||
"""Issue a multipath resize map on device.
|
|
||||||
|
|
||||||
This forces the multipath daemon to update it's
|
|
||||||
size information a particular multipath device.
|
|
||||||
"""
|
|
||||||
(out, _err) = self._execute('multipathd', 'resize', 'map', mpath_id,
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def extend_volume(self, volume_paths):
|
|
||||||
"""Signal the SCSI subsystem to test for volume resize.
|
|
||||||
|
|
||||||
This function tries to signal the local system's kernel
|
|
||||||
that an already attached volume might have been resized.
|
|
||||||
"""
|
|
||||||
LOG.debug("extend volume %s", volume_paths)
|
|
||||||
|
|
||||||
for volume_path in volume_paths:
|
|
||||||
device = self.get_device_info(volume_path)
|
|
||||||
LOG.debug("Volume device info = %s", device)
|
|
||||||
device_id = ("%(host)s:%(channel)s:%(id)s:%(lun)s" %
|
|
||||||
{'host': device['host'],
|
|
||||||
'channel': device['channel'],
|
|
||||||
'id': device['id'],
|
|
||||||
'lun': device['lun']})
|
|
||||||
|
|
||||||
scsi_path = ("/sys/bus/scsi/drivers/sd/%(device_id)s" %
|
|
||||||
{'device_id': device_id})
|
|
||||||
|
|
||||||
size = self.get_device_size(volume_path)
|
|
||||||
LOG.debug("Starting size: %s", size)
|
|
||||||
|
|
||||||
# now issue the device rescan
|
|
||||||
rescan_path = "%(scsi_path)s/rescan" % {'scsi_path': scsi_path}
|
|
||||||
self.echo_scsi_command(rescan_path, "1")
|
|
||||||
new_size = self.get_device_size(volume_path)
|
|
||||||
LOG.debug("volume size after scsi device rescan %s", new_size)
|
|
||||||
|
|
||||||
scsi_wwn = self.get_scsi_wwn(volume_paths[0])
|
|
||||||
mpath_device = self.find_multipath_device_path(scsi_wwn)
|
|
||||||
if mpath_device:
|
|
||||||
# Force a reconfigure so that resize works
|
|
||||||
self.multipath_reconfigure()
|
|
||||||
|
|
||||||
size = self.get_device_size(mpath_device)
|
|
||||||
LOG.info("mpath(%(device)s) current size %(size)s",
|
|
||||||
{'device': mpath_device, 'size': size})
|
|
||||||
result = self.multipath_resize_map(scsi_wwn)
|
|
||||||
if 'fail' in result:
|
|
||||||
LOG.error("Multipathd failed to update the size mapping of "
|
|
||||||
"multipath device %(scsi_wwn)s volume %(volume)s",
|
|
||||||
{'scsi_wwn': scsi_wwn, 'volume': volume_paths})
|
|
||||||
return None
|
|
||||||
|
|
||||||
new_size = self.get_device_size(mpath_device)
|
|
||||||
LOG.info("mpath(%(device)s) new size %(size)s",
|
|
||||||
{'device': mpath_device, 'size': new_size})
|
|
||||||
|
|
||||||
return new_size
|
|
||||||
|
|
||||||
def process_lun_id(self, lun_ids):
|
|
||||||
if isinstance(lun_ids, list):
|
|
||||||
processed = []
|
|
||||||
for x in lun_ids:
|
|
||||||
x = self._format_lun_id(x)
|
|
||||||
processed.append(x)
|
|
||||||
else:
|
|
||||||
processed = self._format_lun_id(lun_ids)
|
|
||||||
return processed
|
|
||||||
|
|
||||||
def _format_lun_id(self, lun_id):
|
|
||||||
# make sure lun_id is an int
|
|
||||||
lun_id = int(lun_id)
|
|
||||||
if lun_id < 256:
|
|
||||||
return lun_id
|
|
||||||
else:
|
|
||||||
return ("0x%04x%04x00000000" %
|
|
||||||
(lun_id & 0xffff, lun_id >> 16 & 0xffff))
|
|
||||||
|
|
||||||
def get_hctl(self, session, lun):
|
|
||||||
"""Given an iSCSI session return the host, channel, target, and lun."""
|
|
||||||
glob_str = '/sys/class/iscsi_host/host*/device/session' + session
|
|
||||||
paths = glob.glob(glob_str + '/target*')
|
|
||||||
if paths:
|
|
||||||
__, channel, target = os.path.split(paths[0])[1].split(':')
|
|
||||||
# Check if we can get the host
|
|
||||||
else:
|
|
||||||
target = channel = '-'
|
|
||||||
paths = glob.glob(glob_str)
|
|
||||||
|
|
||||||
if not paths:
|
|
||||||
LOG.debug('No hctl found on session %s with lun %s', session, lun)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Extract the host number from the path
|
|
||||||
host = paths[0][26:paths[0].index('/', 26)]
|
|
||||||
res = (host, channel, target, lun)
|
|
||||||
LOG.debug('HCTL %s found on session %s with lun %s', res, session, lun)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def device_name_by_hctl(self, session, hctl):
|
|
||||||
"""Find the device name given a session and the hctl.
|
|
||||||
|
|
||||||
:param session: A string with the session number
|
|
||||||
"param hctl: An iterable with the host, channel, target, and lun as
|
|
||||||
passed to scan. ie: ('5', '-', '-', '0')
|
|
||||||
"""
|
|
||||||
if '-' in hctl:
|
|
||||||
hctl = ['*' if x == '-' else x for x in hctl]
|
|
||||||
path = ('/sys/class/scsi_host/host%(h)s/device/session%(s)s/target'
|
|
||||||
'%(h)s:%(c)s:%(t)s/%(h)s:%(c)s:%(t)s:%(l)s/block/*' %
|
|
||||||
{'h': hctl[0], 'c': hctl[1], 't': hctl[2], 'l': hctl[3],
|
|
||||||
's': session})
|
|
||||||
# Sort devices and return the first so we don't return a partition
|
|
||||||
devices = sorted(glob.glob(path))
|
|
||||||
device = os.path.split(devices[0])[1] if devices else None
|
|
||||||
LOG.debug('Searching for a device in session %s and hctl %s yield: %s',
|
|
||||||
session, hctl, device)
|
|
||||||
return device
|
|
||||||
|
|
||||||
def scan_iscsi(self, host, channel='-', target='-', lun='-'):
|
|
||||||
"""Send an iSCSI scan request given the host and optionally the ctl."""
|
|
||||||
LOG.debug('Scanning host %(host)s c: %(channel)s, '
|
|
||||||
't: %(target)s, l: %(lun)s)',
|
|
||||||
{'host': host, 'channel': channel,
|
|
||||||
'target': target, 'lun': lun})
|
|
||||||
self.echo_scsi_command('/sys/class/scsi_host/host%s/scan' % host,
|
|
||||||
'%(c)s %(t)s %(l)s' % {'c': channel,
|
|
||||||
't': target,
|
|
||||||
'l': lun})
|
|
||||||
|
|
||||||
def multipath_add_wwid(self, wwid):
|
|
||||||
"""Add a wwid to the list of know multipath wwids.
|
|
||||||
|
|
||||||
This has the effect of multipathd being willing to create a dm for a
|
|
||||||
multipath even when there's only 1 device.
|
|
||||||
"""
|
|
||||||
out, err = self._execute('multipath', '-a', wwid,
|
|
||||||
run_as_root=True,
|
|
||||||
check_exit_code=False,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
return out.strip() == "wwid '" + wwid + "' added"
|
|
||||||
|
|
||||||
def multipath_add_path(self, realpath):
|
|
||||||
"""Add a path to multipathd for monitoring.
|
|
||||||
|
|
||||||
This has the effect of multipathd checking an already checked device
|
|
||||||
for multipath.
|
|
||||||
|
|
||||||
Together with `multipath_add_wwid` we can create a multipath when
|
|
||||||
there's only 1 path.
|
|
||||||
"""
|
|
||||||
stdout, stderr = self._execute('multipathd', 'add', 'path', realpath,
|
|
||||||
run_as_root=True, timeout=5,
|
|
||||||
check_exit_code=False,
|
|
||||||
root_helper=self._root_helper)
|
|
||||||
return stdout.strip() == 'ok'
|
|
|
@ -1,114 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Generic SheepDog Connection Utilities.
|
|
||||||
|
|
||||||
"""
|
|
||||||
import eventlet
|
|
||||||
import io
|
|
||||||
from oslo_concurrency import processutils
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
class SheepdogVolumeIOWrapper(io.RawIOBase):
|
|
||||||
"""File-like object with Sheepdog backend."""
|
|
||||||
|
|
||||||
def __init__(self, addr, port, volume, snapshot_name=None):
|
|
||||||
self._addr = addr
|
|
||||||
self._port = port
|
|
||||||
self._vdiname = volume
|
|
||||||
self._snapshot_name = snapshot_name
|
|
||||||
self._offset = 0
|
|
||||||
# SheepdogVolumeIOWrapper instance becomes invalid
|
|
||||||
# if a write error occurs.
|
|
||||||
self._valid = True
|
|
||||||
|
|
||||||
def _execute(self, cmd, data=None):
|
|
||||||
try:
|
|
||||||
# NOTE(yamada-h): processutils.execute causes busy waiting
|
|
||||||
# under eventlet.
|
|
||||||
# To avoid wasting CPU resources, it should not be used for
|
|
||||||
# the command which takes long time to execute.
|
|
||||||
# For workaround, we replace a subprocess module with
|
|
||||||
# the original one while only executing a read/write command.
|
|
||||||
_processutils_subprocess = processutils.subprocess
|
|
||||||
processutils.subprocess = eventlet.patcher.original('subprocess')
|
|
||||||
return processutils.execute(*cmd, process_input=data)[0]
|
|
||||||
except (processutils.ProcessExecutionError, OSError):
|
|
||||||
self._valid = False
|
|
||||||
raise exception.VolumeDriverException(name=self._vdiname)
|
|
||||||
finally:
|
|
||||||
processutils.subprocess = _processutils_subprocess
|
|
||||||
|
|
||||||
def read(self, length=None):
|
|
||||||
if not self._valid:
|
|
||||||
raise exception.VolumeDriverException(name=self._vdiname)
|
|
||||||
|
|
||||||
cmd = ['dog', 'vdi', 'read', '-a', self._addr, '-p', self._port]
|
|
||||||
if self._snapshot_name:
|
|
||||||
cmd.extend(('-s', self._snapshot_name))
|
|
||||||
cmd.extend((self._vdiname, self._offset))
|
|
||||||
if length:
|
|
||||||
cmd.append(length)
|
|
||||||
data = self._execute(cmd)
|
|
||||||
self._offset += len(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
if not self._valid:
|
|
||||||
raise exception.VolumeDriverException(name=self._vdiname)
|
|
||||||
|
|
||||||
length = len(data)
|
|
||||||
cmd = ('dog', 'vdi', 'write', '-a', self._addr, '-p', self._port,
|
|
||||||
self._vdiname, self._offset, length)
|
|
||||||
self._execute(cmd, data)
|
|
||||||
self._offset += length
|
|
||||||
return length
|
|
||||||
|
|
||||||
def seek(self, offset, whence=0):
|
|
||||||
if not self._valid:
|
|
||||||
raise exception.VolumeDriverException(name=self._vdiname)
|
|
||||||
|
|
||||||
if whence == 0:
|
|
||||||
# SEEK_SET or 0 - start of the stream (the default);
|
|
||||||
# offset should be zero or positive
|
|
||||||
new_offset = offset
|
|
||||||
elif whence == 1:
|
|
||||||
# SEEK_CUR or 1 - current stream position; offset may be negative
|
|
||||||
new_offset = self._offset + offset
|
|
||||||
else:
|
|
||||||
# SEEK_END or 2 - end of the stream; offset is usually negative
|
|
||||||
# TODO(yamada-h): Support SEEK_END
|
|
||||||
raise IOError(_("Invalid argument - whence=%s not supported.") %
|
|
||||||
whence)
|
|
||||||
|
|
||||||
if new_offset < 0:
|
|
||||||
raise IOError(_("Invalid argument - negative seek offset."))
|
|
||||||
|
|
||||||
self._offset = new_offset
|
|
||||||
|
|
||||||
def tell(self):
|
|
||||||
return self._offset
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def fileno(self):
|
|
||||||
"""Sheepdog does not have support for fileno so we raise IOError.
|
|
||||||
|
|
||||||
Raising IOError is recommended way to notify caller that interface is
|
|
||||||
not supported - see http://docs.python.org/2/library/io.html#io.IOBase
|
|
||||||
"""
|
|
||||||
raise IOError(_("fileno is not supported by SheepdogVolumeIOWrapper"))
|
|
|
@ -1,116 +0,0 @@
|
||||||
# Copyright 2016 Cloudbase Solutions Srl
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_win import utilsfactory
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick import initiator
|
|
||||||
from os_brick.initiator import initiator_connector
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseWindowsConnector(initiator_connector.InitiatorConnector):
|
|
||||||
platform = initiator.PLATFORM_ALL
|
|
||||||
os_type = initiator.OS_TYPE_WINDOWS
|
|
||||||
|
|
||||||
DEFAULT_DEVICE_SCAN_INTERVAL = 2
|
|
||||||
|
|
||||||
def __init__(self, root_helper=None, *args, **kwargs):
|
|
||||||
super(BaseWindowsConnector, self).__init__(root_helper,
|
|
||||||
*args, **kwargs)
|
|
||||||
self.device_scan_interval = kwargs.pop(
|
|
||||||
'device_scan_interval', self.DEFAULT_DEVICE_SCAN_INTERVAL)
|
|
||||||
|
|
||||||
self._diskutils = utilsfactory.get_diskutils()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_multipath_support(enforce_multipath):
|
|
||||||
hostutils = utilsfactory.get_hostutils()
|
|
||||||
mpio_enabled = hostutils.check_server_feature(
|
|
||||||
hostutils.FEATURE_MPIO)
|
|
||||||
if not mpio_enabled:
|
|
||||||
err_msg = _("Using multipath connections for iSCSI and FC disks "
|
|
||||||
"requires the Multipath IO Windows feature to be "
|
|
||||||
"enabled. MPIO must be configured to claim such "
|
|
||||||
"devices.")
|
|
||||||
LOG.error(err_msg)
|
|
||||||
if enforce_multipath:
|
|
||||||
raise exception.BrickException(err_msg)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(*args, **kwargs):
|
|
||||||
multipath = kwargs['multipath']
|
|
||||||
enforce_multipath = kwargs['enforce_multipath']
|
|
||||||
|
|
||||||
props = {}
|
|
||||||
props['multipath'] = (
|
|
||||||
multipath and
|
|
||||||
BaseWindowsConnector.check_multipath_support(enforce_multipath))
|
|
||||||
return props
|
|
||||||
|
|
||||||
def _get_scsi_wwn(self, device_number):
|
|
||||||
# NOTE(lpetrut): The Linux connectors use scsi_id to retrieve the
|
|
||||||
# disk unique id, which prepends the identifier type to the unique id
|
|
||||||
# retrieved from the page 83 SCSI inquiry data. We'll do the same
|
|
||||||
# to remain consistent.
|
|
||||||
disk_uid, uid_type = self._diskutils.get_disk_uid_and_uid_type(
|
|
||||||
device_number)
|
|
||||||
scsi_wwn = '%s%s' % (uid_type, disk_uid)
|
|
||||||
return scsi_wwn
|
|
||||||
|
|
||||||
def check_valid_device(self, path, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
with open(path, 'r') as dev:
|
|
||||||
dev.read(1)
|
|
||||||
except IOError:
|
|
||||||
LOG.exception(
|
|
||||||
"Failed to access the device on the path "
|
|
||||||
"%(path)s", {"path": path})
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_all_available_volumes(self):
|
|
||||||
# TODO(lpetrut): query for disks based on the protocol used.
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _check_device_paths(self, device_paths):
|
|
||||||
if len(device_paths) > 1:
|
|
||||||
err_msg = _("Multiple volume paths were found: %s. This can "
|
|
||||||
"occur if multipath is used and MPIO is not "
|
|
||||||
"properly configured, thus not claiming the device "
|
|
||||||
"paths. This issue must be addressed urgently as "
|
|
||||||
"it can lead to data corruption.")
|
|
||||||
raise exception.BrickException(err_msg % device_paths)
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
volume_paths = self.get_volume_paths(connection_properties)
|
|
||||||
if not volume_paths:
|
|
||||||
err_msg = _("Could not find the disk. Extend failed.")
|
|
||||||
raise exception.NotFound(err_msg)
|
|
||||||
|
|
||||||
device_path = volume_paths[0]
|
|
||||||
device_number = self._diskutils.get_device_number_from_device_name(
|
|
||||||
device_path)
|
|
||||||
self._diskutils.refresh_disk(device_number)
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return None
|
|
|
@ -1,131 +0,0 @@
|
||||||
# Copyright 2016 Cloudbase Solutions Srl
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 collections
|
|
||||||
import time
|
|
||||||
|
|
||||||
from os_win import utilsfactory
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.windows import base as win_conn_base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WindowsFCConnector(win_conn_base.BaseWindowsConnector):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(WindowsFCConnector, self).__init__(*args, **kwargs)
|
|
||||||
self._fc_utils = utilsfactory.get_fc_utils()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(*args, **kwargs):
|
|
||||||
props = {}
|
|
||||||
|
|
||||||
fc_utils = utilsfactory.get_fc_utils()
|
|
||||||
fc_utils.refresh_hba_configuration()
|
|
||||||
fc_hba_ports = fc_utils.get_fc_hba_ports()
|
|
||||||
|
|
||||||
if fc_hba_ports:
|
|
||||||
wwnns = []
|
|
||||||
wwpns = []
|
|
||||||
for port in fc_hba_ports:
|
|
||||||
wwnns.append(port['node_name'])
|
|
||||||
wwpns.append(port['port_name'])
|
|
||||||
props['wwpns'] = wwpns
|
|
||||||
props['wwnns'] = list(set(wwnns))
|
|
||||||
return props
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
volume_paths = self.get_volume_paths(connection_properties)
|
|
||||||
if not volume_paths:
|
|
||||||
raise exception.NoFibreChannelVolumeDeviceFound()
|
|
||||||
|
|
||||||
device_path = volume_paths[0]
|
|
||||||
device_number = self._diskutils.get_device_number_from_device_name(
|
|
||||||
device_path)
|
|
||||||
scsi_wwn = self._get_scsi_wwn(device_number)
|
|
||||||
device_info = {'type': 'block',
|
|
||||||
'path': device_path,
|
|
||||||
'number': device_number,
|
|
||||||
'scsi_wwn': scsi_wwn}
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
# Returns a list containing at most one disk path such as
|
|
||||||
# \\.\PhysicalDrive4.
|
|
||||||
#
|
|
||||||
# If multipath is used and the MPIO service is properly configured
|
|
||||||
# to claim the disks, we'll still get a single device path, having
|
|
||||||
# the same format, which will be used for all the IO operations.
|
|
||||||
disk_paths = set()
|
|
||||||
|
|
||||||
for attempt in range(self.device_scan_attempts):
|
|
||||||
self._diskutils.rescan_disks()
|
|
||||||
volume_mappings = self._get_fc_volume_mappings(
|
|
||||||
connection_properties)
|
|
||||||
LOG.debug("Retrieved volume mappings %(vol_mappings)s "
|
|
||||||
"for volume %(conn_props)s",
|
|
||||||
dict(vol_mappings=volume_mappings,
|
|
||||||
conn_props=connection_properties))
|
|
||||||
|
|
||||||
# Because of MPIO, we may not be able to get the device name
|
|
||||||
# from a specific mapping if the disk was accessed through
|
|
||||||
# an other HBA at that moment. In that case, the device name
|
|
||||||
# will show up as an empty string.
|
|
||||||
for mapping in volume_mappings:
|
|
||||||
device_name = mapping['device_name']
|
|
||||||
if device_name:
|
|
||||||
disk_paths.add(device_name)
|
|
||||||
|
|
||||||
if disk_paths:
|
|
||||||
break
|
|
||||||
|
|
||||||
time.sleep(self.device_scan_interval)
|
|
||||||
|
|
||||||
self._check_device_paths(disk_paths)
|
|
||||||
return list(disk_paths)
|
|
||||||
|
|
||||||
def _get_fc_volume_mappings(self, connection_properties):
|
|
||||||
# Note(lpetrut): All the WWNs returned by os-win are upper case.
|
|
||||||
target_wwpns = [wwpn.upper()
|
|
||||||
for wwpn in connection_properties['target_wwn']]
|
|
||||||
target_lun = connection_properties['target_lun']
|
|
||||||
|
|
||||||
volume_mappings = []
|
|
||||||
hba_mappings = self._get_fc_hba_mappings()
|
|
||||||
for node_name in hba_mappings:
|
|
||||||
target_mappings = self._fc_utils.get_fc_target_mappings(node_name)
|
|
||||||
for mapping in target_mappings:
|
|
||||||
if (mapping['port_name'] in target_wwpns
|
|
||||||
and mapping['lun'] == target_lun):
|
|
||||||
volume_mappings.append(mapping)
|
|
||||||
|
|
||||||
return volume_mappings
|
|
||||||
|
|
||||||
def _get_fc_hba_mappings(self):
|
|
||||||
mappings = collections.defaultdict(list)
|
|
||||||
fc_hba_ports = self._fc_utils.get_fc_hba_ports()
|
|
||||||
for port in fc_hba_ports:
|
|
||||||
mappings[port['node_name']].append(port['port_name'])
|
|
||||||
return mappings
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
pass
|
|
|
@ -1,166 +0,0 @@
|
||||||
# Copyright 2016 Cloudbase Solutions Srl
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_win import exceptions as os_win_exc
|
|
||||||
from os_win import utilsfactory
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick.initiator.connectors import base_iscsi
|
|
||||||
from os_brick.initiator.windows import base as win_conn_base
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WindowsISCSIConnector(win_conn_base.BaseWindowsConnector,
|
|
||||||
base_iscsi.BaseISCSIConnector):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(WindowsISCSIConnector, self).__init__(*args, **kwargs)
|
|
||||||
self.use_multipath = kwargs.pop('use_multipath', False)
|
|
||||||
self.initiator_list = kwargs.pop('initiator_list', [])
|
|
||||||
|
|
||||||
self._iscsi_utils = utilsfactory.get_iscsi_initiator_utils()
|
|
||||||
|
|
||||||
self.validate_initiators()
|
|
||||||
|
|
||||||
def validate_initiators(self):
|
|
||||||
"""Validates the list of requested initiator HBAs
|
|
||||||
|
|
||||||
Validates the list of requested initiator HBAs to be used
|
|
||||||
when establishing iSCSI sessions.
|
|
||||||
"""
|
|
||||||
valid_initiator_list = True
|
|
||||||
if not self.initiator_list:
|
|
||||||
LOG.info("No iSCSI initiator was explicitly requested. "
|
|
||||||
"The Microsoft iSCSI initiator will choose the "
|
|
||||||
"initiator when establishing sessions.")
|
|
||||||
else:
|
|
||||||
available_initiators = self._iscsi_utils.get_iscsi_initiators()
|
|
||||||
for initiator in self.initiator_list:
|
|
||||||
if initiator not in available_initiators:
|
|
||||||
LOG.warning("The requested initiator %(req_initiator)s "
|
|
||||||
"is not in the list of available initiators: "
|
|
||||||
"%(avail_initiators)s.",
|
|
||||||
dict(req_initiator=initiator,
|
|
||||||
avail_initiators=available_initiators))
|
|
||||||
valid_initiator_list = False
|
|
||||||
return valid_initiator_list
|
|
||||||
|
|
||||||
def get_initiator(self):
|
|
||||||
"""Returns the iSCSI initiator node name."""
|
|
||||||
return self._iscsi_utils.get_iscsi_initiator()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(*args, **kwargs):
|
|
||||||
iscsi_utils = utilsfactory.get_iscsi_initiator_utils()
|
|
||||||
initiator = iscsi_utils.get_iscsi_initiator()
|
|
||||||
return dict(initiator=initiator)
|
|
||||||
|
|
||||||
def _get_all_paths(self, connection_properties):
|
|
||||||
initiator_list = self.initiator_list or [None]
|
|
||||||
all_targets = self._get_all_targets(connection_properties)
|
|
||||||
paths = [(initiator_name, target_portal, target_iqn, target_lun)
|
|
||||||
for target_portal, target_iqn, target_lun in all_targets
|
|
||||||
for initiator_name in initiator_list]
|
|
||||||
return paths
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
volume_connected = False
|
|
||||||
for (initiator_name,
|
|
||||||
target_portal,
|
|
||||||
target_iqn,
|
|
||||||
target_lun) in self._get_all_paths(connection_properties):
|
|
||||||
try:
|
|
||||||
LOG.info("Attempting to establish an iSCSI session to "
|
|
||||||
"target %(target_iqn)s on portal %(target_portal)s "
|
|
||||||
"accessing LUN %(target_lun)s using initiator "
|
|
||||||
"%(initiator_name)s.",
|
|
||||||
dict(target_portal=target_portal,
|
|
||||||
target_iqn=target_iqn,
|
|
||||||
target_lun=target_lun,
|
|
||||||
initiator_name=initiator_name))
|
|
||||||
self._iscsi_utils.login_storage_target(
|
|
||||||
target_lun=target_lun,
|
|
||||||
target_iqn=target_iqn,
|
|
||||||
target_portal=target_portal,
|
|
||||||
auth_username=connection_properties.get('auth_username'),
|
|
||||||
auth_password=connection_properties.get('auth_password'),
|
|
||||||
mpio_enabled=self.use_multipath,
|
|
||||||
initiator_name=initiator_name,
|
|
||||||
ensure_lun_available=False)
|
|
||||||
self._iscsi_utils.ensure_lun_available(
|
|
||||||
target_iqn=target_iqn,
|
|
||||||
target_lun=target_lun,
|
|
||||||
rescan_attempts=self.device_scan_attempts,
|
|
||||||
retry_interval=self.device_scan_interval)
|
|
||||||
|
|
||||||
if not volume_connected:
|
|
||||||
(device_number,
|
|
||||||
device_path) = (
|
|
||||||
self._iscsi_utils.get_device_number_and_path(
|
|
||||||
target_iqn, target_lun))
|
|
||||||
volume_connected = True
|
|
||||||
|
|
||||||
if not self.use_multipath:
|
|
||||||
break
|
|
||||||
except os_win_exc.OSWinException:
|
|
||||||
LOG.exception("Could not establish the iSCSI session.")
|
|
||||||
|
|
||||||
if not volume_connected:
|
|
||||||
raise exception.BrickException(
|
|
||||||
_("Could not connect volume %s.") % connection_properties)
|
|
||||||
|
|
||||||
scsi_wwn = self._get_scsi_wwn(device_number)
|
|
||||||
|
|
||||||
device_info = {'type': 'block',
|
|
||||||
'path': device_path,
|
|
||||||
'number': device_number,
|
|
||||||
'scsi_wwn': scsi_wwn}
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
# We want to refresh the cached information first.
|
|
||||||
self._diskutils.rescan_disks()
|
|
||||||
for (target_portal,
|
|
||||||
target_iqn,
|
|
||||||
target_lun) in self._get_all_targets(connection_properties):
|
|
||||||
|
|
||||||
luns = self._iscsi_utils.get_target_luns(target_iqn)
|
|
||||||
# We disconnect the target only if it does not expose other
|
|
||||||
# luns which may be in use.
|
|
||||||
if not luns or luns == [target_lun]:
|
|
||||||
self._iscsi_utils.logout_storage_target(target_iqn)
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
device_paths = set()
|
|
||||||
|
|
||||||
for (target_portal,
|
|
||||||
target_iqn,
|
|
||||||
target_lun) in self._get_all_targets(connection_properties):
|
|
||||||
|
|
||||||
(device_number,
|
|
||||||
device_path) = self._iscsi_utils.get_device_number_and_path(
|
|
||||||
target_iqn, target_lun)
|
|
||||||
if device_path:
|
|
||||||
device_paths.add(device_path)
|
|
||||||
|
|
||||||
self._check_device_paths(device_paths)
|
|
||||||
return list(device_paths)
|
|
|
@ -1,95 +0,0 @@
|
||||||
# Copyright 2016 Cloudbase Solutions Srl
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
from os_win import utilsfactory
|
|
||||||
|
|
||||||
from os_brick.initiator.windows import base as win_conn_base
|
|
||||||
from os_brick.remotefs import windows_remotefs as remotefs
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
|
|
||||||
class WindowsSMBFSConnector(win_conn_base.BaseWindowsConnector):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(WindowsSMBFSConnector, self).__init__(*args, **kwargs)
|
|
||||||
# If this flag is set, we use the local paths in case of local
|
|
||||||
# shares. This is in fact mandatory in some cases, for example
|
|
||||||
# for the Hyper-C scenario.
|
|
||||||
self._local_path_for_loopback = kwargs.get('local_path_for_loopback',
|
|
||||||
False)
|
|
||||||
self._remotefsclient = remotefs.WindowsRemoteFsClient(
|
|
||||||
mount_type='smbfs',
|
|
||||||
*args, **kwargs)
|
|
||||||
self._smbutils = utilsfactory.get_smbutils()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connector_properties(*args, **kwargs):
|
|
||||||
# No connector properties updates in this case.
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def connect_volume(self, connection_properties):
|
|
||||||
self.ensure_share_mounted(connection_properties)
|
|
||||||
disk_path = self._get_disk_path(connection_properties)
|
|
||||||
device_info = {'type': 'file',
|
|
||||||
'path': disk_path}
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def disconnect_volume(self, connection_properties,
|
|
||||||
force=False, ignore_errors=False):
|
|
||||||
export_path = self._get_export_path(connection_properties)
|
|
||||||
self._remotefsclient.unmount(export_path)
|
|
||||||
|
|
||||||
def _get_export_path(self, connection_properties):
|
|
||||||
return connection_properties['export'].replace('/', '\\')
|
|
||||||
|
|
||||||
def _get_disk_path(self, connection_properties):
|
|
||||||
# This is expected to be the share address, as an UNC path.
|
|
||||||
export_path = self._get_export_path(connection_properties)
|
|
||||||
mount_base = self._remotefsclient.get_mount_base()
|
|
||||||
use_local_path = (self._local_path_for_loopback and
|
|
||||||
self._smbutils.is_local_share(export_path))
|
|
||||||
|
|
||||||
disk_dir = export_path
|
|
||||||
if mount_base:
|
|
||||||
# This will be a symlink pointing to either the share
|
|
||||||
# path directly or to the local share path, if requested
|
|
||||||
# and available.
|
|
||||||
disk_dir = self._remotefsclient.get_mount_point(
|
|
||||||
export_path)
|
|
||||||
elif use_local_path:
|
|
||||||
share_name = self._remotefsclient.get_share_name(export_path)
|
|
||||||
disk_dir = self._remotefsclient.get_local_share_path(share_name)
|
|
||||||
|
|
||||||
disk_name = connection_properties['name']
|
|
||||||
disk_path = os.path.join(disk_dir, disk_name)
|
|
||||||
return disk_path
|
|
||||||
|
|
||||||
def get_search_path(self):
|
|
||||||
return self._remotefsclient.get_mount_base()
|
|
||||||
|
|
||||||
@utils.trace
|
|
||||||
def get_volume_paths(self, connection_properties):
|
|
||||||
return [self._get_disk_path(connection_properties)]
|
|
||||||
|
|
||||||
def ensure_share_mounted(self, connection_properties):
|
|
||||||
export_path = self._get_export_path(connection_properties)
|
|
||||||
mount_options = connection_properties.get('options')
|
|
||||||
self._remotefsclient.mount(export_path, mount_options)
|
|
||||||
|
|
||||||
def extend_volume(self, connection_properties):
|
|
||||||
raise NotImplementedError
|
|
|
@ -1,829 +0,0 @@
|
||||||
# Copyright 2013 OpenStack Foundation.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
LVM class for performing LVM operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import executor
|
|
||||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
|
||||||
from os_brick import utils
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import excutils
|
|
||||||
from six import moves
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LVM(executor.Executor):
|
|
||||||
"""LVM object to enable various LVM related operations."""
|
|
||||||
|
|
||||||
LVM_CMD_PREFIX = ['env', 'LC_ALL=C']
|
|
||||||
|
|
||||||
def __init__(self, vg_name, root_helper, create_vg=False,
|
|
||||||
physical_volumes=None, lvm_type='default',
|
|
||||||
executor=None, lvm_conf=None):
|
|
||||||
|
|
||||||
"""Initialize the LVM object.
|
|
||||||
|
|
||||||
The LVM object is based on an LVM VolumeGroup, one instantiation
|
|
||||||
for each VolumeGroup you have/use.
|
|
||||||
|
|
||||||
:param vg_name: Name of existing VG or VG to create
|
|
||||||
:param root_helper: Execution root_helper method to use
|
|
||||||
:param create_vg: Indicates the VG doesn't exist
|
|
||||||
and we want to create it
|
|
||||||
:param physical_volumes: List of PVs to build VG on
|
|
||||||
:param lvm_type: VG and Volume type (default, or thin)
|
|
||||||
:param executor: Execute method to use, None uses
|
|
||||||
oslo_concurrency.processutils
|
|
||||||
|
|
||||||
"""
|
|
||||||
super(LVM, self).__init__(execute=executor, root_helper=root_helper)
|
|
||||||
self.vg_name = vg_name
|
|
||||||
self.pv_list = []
|
|
||||||
self.vg_size = 0.0
|
|
||||||
self.vg_free_space = 0.0
|
|
||||||
self.vg_lv_count = 0
|
|
||||||
self.vg_uuid = None
|
|
||||||
self.vg_thin_pool = None
|
|
||||||
self.vg_thin_pool_size = 0.0
|
|
||||||
self.vg_thin_pool_free_space = 0.0
|
|
||||||
self._supports_snapshot_lv_activation = None
|
|
||||||
self._supports_lvchange_ignoreskipactivation = None
|
|
||||||
self.vg_provisioned_capacity = 0.0
|
|
||||||
|
|
||||||
# Ensure LVM_SYSTEM_DIR has been added to LVM.LVM_CMD_PREFIX
|
|
||||||
# before the first LVM command is executed, and use the directory
|
|
||||||
# where the specified lvm_conf file is located as the value.
|
|
||||||
if lvm_conf and os.path.isfile(lvm_conf):
|
|
||||||
lvm_sys_dir = os.path.dirname(lvm_conf)
|
|
||||||
LVM.LVM_CMD_PREFIX = ['env',
|
|
||||||
'LC_ALL=C',
|
|
||||||
'LVM_SYSTEM_DIR=' + lvm_sys_dir]
|
|
||||||
|
|
||||||
if create_vg and physical_volumes is not None:
|
|
||||||
self.pv_list = physical_volumes
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._create_vg(physical_volumes)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error creating Volume Group')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
raise exception.VolumeGroupCreationFailed(vg_name=self.vg_name)
|
|
||||||
|
|
||||||
if self._vg_exists() is False:
|
|
||||||
LOG.error('Unable to locate Volume Group %s', vg_name)
|
|
||||||
raise exception.VolumeGroupNotFound(vg_name=vg_name)
|
|
||||||
|
|
||||||
# NOTE: we assume that the VG has been activated outside of Cinder
|
|
||||||
|
|
||||||
if lvm_type == 'thin':
|
|
||||||
pool_name = "%s-pool" % self.vg_name
|
|
||||||
if self.get_volume(pool_name) is None:
|
|
||||||
try:
|
|
||||||
self.create_thin_pool(pool_name)
|
|
||||||
except putils.ProcessExecutionError:
|
|
||||||
# Maybe we just lost the race against another copy of
|
|
||||||
# this driver being in init in parallel - e.g.
|
|
||||||
# cinder-volume and cinder-backup starting in parallel
|
|
||||||
if self.get_volume(pool_name) is None:
|
|
||||||
raise
|
|
||||||
|
|
||||||
self.vg_thin_pool = pool_name
|
|
||||||
self.activate_lv(self.vg_thin_pool)
|
|
||||||
self.pv_list = self.get_all_physical_volumes(root_helper, vg_name)
|
|
||||||
|
|
||||||
def _vg_exists(self):
|
|
||||||
"""Simple check to see if VG exists.
|
|
||||||
|
|
||||||
:returns: True if vg specified in object exists, else False
|
|
||||||
|
|
||||||
"""
|
|
||||||
exists = False
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--noheadings',
|
|
||||||
'-o', 'name', self.vg_name]
|
|
||||||
(out, _err) = self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
|
|
||||||
if out is not None:
|
|
||||||
volume_groups = out.split()
|
|
||||||
if self.vg_name in volume_groups:
|
|
||||||
exists = True
|
|
||||||
|
|
||||||
return exists
|
|
||||||
|
|
||||||
def _create_vg(self, pv_list):
|
|
||||||
cmd = ['vgcreate', self.vg_name, ','.join(pv_list)]
|
|
||||||
self._execute(*cmd, root_helper=self._root_helper, run_as_root=True)
|
|
||||||
|
|
||||||
def _get_vg_uuid(self):
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--noheadings',
|
|
||||||
'-o', 'uuid', self.vg_name]
|
|
||||||
(out, _err) = self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
if out is not None:
|
|
||||||
return out.split()
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _get_thin_pool_free_space(self, vg_name, thin_pool_name):
|
|
||||||
"""Returns available thin pool free space.
|
|
||||||
|
|
||||||
:param vg_name: the vg where the pool is placed
|
|
||||||
:param thin_pool_name: the thin pool to gather info for
|
|
||||||
:returns: Free space in GB (float), calculated using data_percent
|
|
||||||
|
|
||||||
"""
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvs', '--noheadings', '--unit=g',
|
|
||||||
'-o', 'size,data_percent', '--separator',
|
|
||||||
':', '--nosuffix']
|
|
||||||
# NOTE(gfidente): data_percent only applies to some types of LV so we
|
|
||||||
# make sure to append the actual thin pool name
|
|
||||||
cmd.append("/dev/%s/%s" % (vg_name, thin_pool_name))
|
|
||||||
|
|
||||||
free_space = 0.0
|
|
||||||
|
|
||||||
try:
|
|
||||||
(out, err) = self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
if out is not None:
|
|
||||||
out = out.strip()
|
|
||||||
data = out.split(':')
|
|
||||||
pool_size = float(data[0])
|
|
||||||
data_percent = float(data[1])
|
|
||||||
consumed_space = pool_size / 100 * data_percent
|
|
||||||
free_space = pool_size - consumed_space
|
|
||||||
free_space = round(free_space, 2)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error querying thin pool about data_percent')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
|
|
||||||
return free_space
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_lvm_version(root_helper):
|
|
||||||
"""Static method to get LVM version from system.
|
|
||||||
|
|
||||||
:param root_helper: root_helper to use for execute
|
|
||||||
:returns: version 3-tuple
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--version']
|
|
||||||
(out, _err) = priv_rootwrap.execute(*cmd,
|
|
||||||
root_helper=root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
lines = out.split('\n')
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if 'LVM version' in line:
|
|
||||||
version_list = line.split()
|
|
||||||
# NOTE(gfidente): version is formatted as follows:
|
|
||||||
# major.minor.patchlevel(library API version)[-customisation]
|
|
||||||
version = version_list[2]
|
|
||||||
version_filter = r"(\d+)\.(\d+)\.(\d+).*"
|
|
||||||
r = re.search(version_filter, version)
|
|
||||||
version_tuple = tuple(map(int, r.group(1, 2, 3)))
|
|
||||||
return version_tuple
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def supports_thin_provisioning(root_helper):
|
|
||||||
"""Static method to check for thin LVM support on a system.
|
|
||||||
|
|
||||||
:param root_helper: root_helper to use for execute
|
|
||||||
:returns: True if supported, False otherwise
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
return LVM.get_lvm_version(root_helper) >= (2, 2, 95)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def supports_snapshot_lv_activation(self):
|
|
||||||
"""Property indicating whether snap activation changes are supported.
|
|
||||||
|
|
||||||
Check for LVM version >= 2.02.91.
|
|
||||||
(LVM2 git: e8a40f6 Allow to activate snapshot)
|
|
||||||
|
|
||||||
:returns: True/False indicating support
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._supports_snapshot_lv_activation is not None:
|
|
||||||
return self._supports_snapshot_lv_activation
|
|
||||||
|
|
||||||
self._supports_snapshot_lv_activation = (
|
|
||||||
self.get_lvm_version(self._root_helper) >= (2, 2, 91))
|
|
||||||
|
|
||||||
return self._supports_snapshot_lv_activation
|
|
||||||
|
|
||||||
@property
|
|
||||||
def supports_lvchange_ignoreskipactivation(self):
|
|
||||||
"""Property indicating whether lvchange can ignore skip activation.
|
|
||||||
|
|
||||||
Check for LVM version >= 2.02.99.
|
|
||||||
(LVM2 git: ab789c1bc add --ignoreactivationskip to lvchange)
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._supports_lvchange_ignoreskipactivation is not None:
|
|
||||||
return self._supports_lvchange_ignoreskipactivation
|
|
||||||
|
|
||||||
self._supports_lvchange_ignoreskipactivation = (
|
|
||||||
self.get_lvm_version(self._root_helper) >= (2, 2, 99))
|
|
||||||
|
|
||||||
return self._supports_lvchange_ignoreskipactivation
|
|
||||||
|
|
||||||
@property
|
|
||||||
def supports_full_pool_create(self):
|
|
||||||
"""Property indicating whether 100% pool creation is supported.
|
|
||||||
|
|
||||||
Check for LVM version >= 2.02.115.
|
|
||||||
Ref: https://bugzilla.redhat.com/show_bug.cgi?id=998347
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.get_lvm_version(self._root_helper) >= (2, 2, 115):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_lv_info(root_helper, vg_name=None, lv_name=None):
|
|
||||||
"""Retrieve info about LVs (all, in a VG, or a single LV).
|
|
||||||
|
|
||||||
:param root_helper: root_helper to use for execute
|
|
||||||
:param vg_name: optional, gathers info for only the specified VG
|
|
||||||
:param lv_name: optional, gathers info for only the specified LV
|
|
||||||
:returns: List of Dictionaries with LV info
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvs', '--noheadings', '--unit=g',
|
|
||||||
'-o', 'vg_name,name,size', '--nosuffix']
|
|
||||||
if lv_name is not None and vg_name is not None:
|
|
||||||
cmd.append("%s/%s" % (vg_name, lv_name))
|
|
||||||
elif vg_name is not None:
|
|
||||||
cmd.append(vg_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
(out, _err) = priv_rootwrap.execute(*cmd,
|
|
||||||
root_helper=root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
with excutils.save_and_reraise_exception(reraise=True) as ctx:
|
|
||||||
if "not found" in err.stderr or "Failed to find" in err.stderr:
|
|
||||||
ctx.reraise = False
|
|
||||||
LOG.info("Logical Volume not found when querying "
|
|
||||||
"LVM info. (vg_name=%(vg)s, lv_name=%(lv)s",
|
|
||||||
{'vg': vg_name, 'lv': lv_name})
|
|
||||||
out = None
|
|
||||||
|
|
||||||
lv_list = []
|
|
||||||
if out is not None:
|
|
||||||
volumes = out.split()
|
|
||||||
iterator = moves.zip(*[iter(volumes)] * 3) # pylint: disable=E1101
|
|
||||||
for vg, name, size in iterator:
|
|
||||||
lv_list.append({"vg": vg, "name": name, "size": size})
|
|
||||||
|
|
||||||
return lv_list
|
|
||||||
|
|
||||||
def get_volumes(self, lv_name=None):
|
|
||||||
"""Get all LV's associated with this instantiation (VG).
|
|
||||||
|
|
||||||
:returns: List of Dictionaries with LV info
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.get_lv_info(self._root_helper,
|
|
||||||
self.vg_name,
|
|
||||||
lv_name)
|
|
||||||
|
|
||||||
def get_volume(self, name):
|
|
||||||
"""Get reference object of volume specified by name.
|
|
||||||
|
|
||||||
:returns: dict representation of Logical Volume if exists
|
|
||||||
|
|
||||||
"""
|
|
||||||
ref_list = self.get_volumes(name)
|
|
||||||
for r in ref_list:
|
|
||||||
if r['name'] == name:
|
|
||||||
return r
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all_physical_volumes(root_helper, vg_name=None):
|
|
||||||
"""Static method to get all PVs on a system.
|
|
||||||
|
|
||||||
:param root_helper: root_helper to use for execute
|
|
||||||
:param vg_name: optional, gathers info for only the specified VG
|
|
||||||
:returns: List of Dictionaries with PV info
|
|
||||||
|
|
||||||
"""
|
|
||||||
field_sep = '|'
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['pvs', '--noheadings',
|
|
||||||
'--unit=g',
|
|
||||||
'-o', 'vg_name,name,size,free',
|
|
||||||
'--separator', field_sep,
|
|
||||||
'--nosuffix']
|
|
||||||
(out, _err) = priv_rootwrap.execute(*cmd,
|
|
||||||
root_helper=root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
|
|
||||||
pvs = out.split()
|
|
||||||
if vg_name is not None:
|
|
||||||
pvs = [pv for pv in pvs if vg_name == pv.split(field_sep)[0]]
|
|
||||||
|
|
||||||
pv_list = []
|
|
||||||
for pv in pvs:
|
|
||||||
fields = pv.split(field_sep)
|
|
||||||
pv_list.append({'vg': fields[0],
|
|
||||||
'name': fields[1],
|
|
||||||
'size': float(fields[2]),
|
|
||||||
'available': float(fields[3])})
|
|
||||||
return pv_list
|
|
||||||
|
|
||||||
def get_physical_volumes(self):
|
|
||||||
"""Get all PVs associated with this instantiation (VG).
|
|
||||||
|
|
||||||
:returns: List of Dictionaries with PV info
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.pv_list = self.get_all_physical_volumes(self._root_helper,
|
|
||||||
self.vg_name)
|
|
||||||
return self.pv_list
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_all_volume_groups(root_helper, vg_name=None):
|
|
||||||
"""Static method to get all VGs on a system.
|
|
||||||
|
|
||||||
:param root_helper: root_helper to use for execute
|
|
||||||
:param vg_name: optional, gathers info for only the specified VG
|
|
||||||
:returns: List of Dictionaries with VG info
|
|
||||||
|
|
||||||
"""
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['vgs', '--noheadings',
|
|
||||||
'--unit=g', '-o',
|
|
||||||
'name,size,free,lv_count,uuid',
|
|
||||||
'--separator', ':',
|
|
||||||
'--nosuffix']
|
|
||||||
if vg_name is not None:
|
|
||||||
cmd.append(vg_name)
|
|
||||||
|
|
||||||
(out, _err) = priv_rootwrap.execute(*cmd,
|
|
||||||
root_helper=root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
vg_list = []
|
|
||||||
if out is not None:
|
|
||||||
vgs = out.split()
|
|
||||||
for vg in vgs:
|
|
||||||
fields = vg.split(':')
|
|
||||||
vg_list.append({'name': fields[0],
|
|
||||||
'size': float(fields[1]),
|
|
||||||
'available': float(fields[2]),
|
|
||||||
'lv_count': int(fields[3]),
|
|
||||||
'uuid': fields[4]})
|
|
||||||
|
|
||||||
return vg_list
|
|
||||||
|
|
||||||
def update_volume_group_info(self):
|
|
||||||
"""Update VG info for this instantiation.
|
|
||||||
|
|
||||||
Used to update member fields of object and
|
|
||||||
provide a dict of info for caller.
|
|
||||||
|
|
||||||
:returns: Dictionaries of VG info
|
|
||||||
|
|
||||||
"""
|
|
||||||
vg_list = self.get_all_volume_groups(self._root_helper, self.vg_name)
|
|
||||||
|
|
||||||
if len(vg_list) != 1:
|
|
||||||
LOG.error('Unable to find VG: %s', self.vg_name)
|
|
||||||
raise exception.VolumeGroupNotFound(vg_name=self.vg_name)
|
|
||||||
|
|
||||||
self.vg_size = float(vg_list[0]['size'])
|
|
||||||
self.vg_free_space = float(vg_list[0]['available'])
|
|
||||||
self.vg_lv_count = int(vg_list[0]['lv_count'])
|
|
||||||
self.vg_uuid = vg_list[0]['uuid']
|
|
||||||
|
|
||||||
total_vols_size = 0.0
|
|
||||||
if self.vg_thin_pool is not None:
|
|
||||||
# NOTE(xyang): If providing only self.vg_name,
|
|
||||||
# get_lv_info will output info on the thin pool and all
|
|
||||||
# individual volumes.
|
|
||||||
# get_lv_info(self._root_helper, 'stack-vg')
|
|
||||||
# sudo lvs --noheadings --unit=g -o vg_name,name,size
|
|
||||||
# --nosuffix stack-vg
|
|
||||||
# stack-vg stack-pool 9.51
|
|
||||||
# stack-vg volume-13380d16-54c3-4979-9d22-172082dbc1a1 1.00
|
|
||||||
# stack-vg volume-629e13ab-7759-46a5-b155-ee1eb20ca892 1.00
|
|
||||||
# stack-vg volume-e3e6281c-51ee-464c-b1a7-db6c0854622c 1.00
|
|
||||||
#
|
|
||||||
# If providing both self.vg_name and self.vg_thin_pool,
|
|
||||||
# get_lv_info will output only info on the thin pool, but not
|
|
||||||
# individual volumes.
|
|
||||||
# get_lv_info(self._root_helper, 'stack-vg', 'stack-pool')
|
|
||||||
# sudo lvs --noheadings --unit=g -o vg_name,name,size
|
|
||||||
# --nosuffix stack-vg/stack-pool
|
|
||||||
# stack-vg stack-pool 9.51
|
|
||||||
#
|
|
||||||
# We need info on both the thin pool and the volumes,
|
|
||||||
# therefore we should provide only self.vg_name, but not
|
|
||||||
# self.vg_thin_pool here.
|
|
||||||
for lv in self.get_lv_info(self._root_helper,
|
|
||||||
self.vg_name):
|
|
||||||
lvsize = lv['size']
|
|
||||||
# get_lv_info runs "lvs" command with "--nosuffix".
|
|
||||||
# This removes "g" from "1.00g" and only outputs "1.00".
|
|
||||||
# Running "lvs" command without "--nosuffix" will output
|
|
||||||
# "1.00g" if "g" is the unit.
|
|
||||||
# Remove the unit if it is in lv['size'].
|
|
||||||
if not lv['size'][-1].isdigit():
|
|
||||||
lvsize = lvsize[:-1]
|
|
||||||
if lv['name'] == self.vg_thin_pool:
|
|
||||||
self.vg_thin_pool_size = lvsize
|
|
||||||
tpfs = self._get_thin_pool_free_space(self.vg_name,
|
|
||||||
self.vg_thin_pool)
|
|
||||||
self.vg_thin_pool_free_space = tpfs
|
|
||||||
else:
|
|
||||||
total_vols_size = total_vols_size + float(lvsize)
|
|
||||||
total_vols_size = round(total_vols_size, 2)
|
|
||||||
|
|
||||||
self.vg_provisioned_capacity = total_vols_size
|
|
||||||
|
|
||||||
def _calculate_thin_pool_size(self):
|
|
||||||
"""Calculates the correct size for a thin pool.
|
|
||||||
|
|
||||||
Ideally we would use 100% of the containing volume group and be done.
|
|
||||||
But the 100%VG notation to lvcreate is not implemented and thus cannot
|
|
||||||
be used. See https://bugzilla.redhat.com/show_bug.cgi?id=998347
|
|
||||||
|
|
||||||
Further, some amount of free space must remain in the volume group for
|
|
||||||
metadata for the contained logical volumes. The exact amount depends
|
|
||||||
on how much volume sharing you expect.
|
|
||||||
|
|
||||||
:returns: An lvcreate-ready string for the number of calculated bytes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# make sure volume group information is current
|
|
||||||
self.update_volume_group_info()
|
|
||||||
|
|
||||||
if LVM.supports_full_pool_create:
|
|
||||||
return ["-l", "100%FREE"]
|
|
||||||
|
|
||||||
# leave 5% free for metadata
|
|
||||||
return ["-L", "%sg" % (self.vg_free_space * 0.95)]
|
|
||||||
|
|
||||||
def create_thin_pool(self, name=None):
|
|
||||||
"""Creates a thin provisioning pool for this VG.
|
|
||||||
|
|
||||||
The syntax here is slightly different than the default
|
|
||||||
lvcreate -T, so we'll just write a custom cmd here
|
|
||||||
and do it.
|
|
||||||
|
|
||||||
:param name: Name to use for pool, default is "<vg-name>-pool"
|
|
||||||
:returns: The size string passed to the lvcreate command
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not LVM.supports_thin_provisioning(self._root_helper):
|
|
||||||
LOG.error('Requested to setup thin provisioning, '
|
|
||||||
'however current LVM version does not '
|
|
||||||
'support it.')
|
|
||||||
return None
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
name = '%s-pool' % self.vg_name
|
|
||||||
|
|
||||||
vg_pool_name = '%s/%s' % (self.vg_name, name)
|
|
||||||
|
|
||||||
size_args = self._calculate_thin_pool_size()
|
|
||||||
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-T']
|
|
||||||
cmd.extend(size_args)
|
|
||||||
cmd.append(vg_pool_name)
|
|
||||||
|
|
||||||
LOG.debug("Creating thin pool '%(pool)s' with size %(size)s of "
|
|
||||||
"total %(free)sg", {'pool': vg_pool_name,
|
|
||||||
'size': size_args,
|
|
||||||
'free': self.vg_free_space})
|
|
||||||
|
|
||||||
self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
|
|
||||||
self.vg_thin_pool = name
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def create_volume(self, name, size_str, lv_type='default', mirror_count=0):
|
|
||||||
"""Creates a logical volume on the object's VG.
|
|
||||||
|
|
||||||
:param name: Name to use when creating Logical Volume
|
|
||||||
:param size_str: Size to use when creating Logical Volume
|
|
||||||
:param lv_type: Type of Volume (default or thin)
|
|
||||||
:param mirror_count: Use LVM mirroring with specified count
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if lv_type == 'thin':
|
|
||||||
pool_path = '%s/%s' % (self.vg_name, self.vg_thin_pool)
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-T', '-V', size_str, '-n',
|
|
||||||
name, pool_path]
|
|
||||||
else:
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-n', name, self.vg_name,
|
|
||||||
'-L', size_str]
|
|
||||||
|
|
||||||
if mirror_count > 0:
|
|
||||||
cmd.extend(['-m', mirror_count, '--nosync',
|
|
||||||
'--mirrorlog', 'mirrored'])
|
|
||||||
terras = int(size_str[:-1]) / 1024.0
|
|
||||||
if terras >= 1.5:
|
|
||||||
rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
|
|
||||||
# NOTE(vish): Next power of two for region size. See:
|
|
||||||
# http://red.ht/U2BPOD
|
|
||||||
cmd.extend(['-R', str(rsize)])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error creating Volume')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
@utils.retry(putils.ProcessExecutionError)
|
|
||||||
def create_lv_snapshot(self, name, source_lv_name, lv_type='default'):
|
|
||||||
"""Creates a snapshot of a logical volume.
|
|
||||||
|
|
||||||
:param name: Name to assign to new snapshot
|
|
||||||
:param source_lv_name: Name of Logical Volume to snapshot
|
|
||||||
:param lv_type: Type of LV (default or thin)
|
|
||||||
|
|
||||||
"""
|
|
||||||
source_lvref = self.get_volume(source_lv_name)
|
|
||||||
if source_lvref is None:
|
|
||||||
LOG.error("Trying to create snapshot by non-existent LV: %s",
|
|
||||||
source_lv_name)
|
|
||||||
raise exception.VolumeDeviceNotFound(device=source_lv_name)
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '--name', name, '--snapshot',
|
|
||||||
'%s/%s' % (self.vg_name, source_lv_name)]
|
|
||||||
if lv_type != 'thin':
|
|
||||||
size = source_lvref['size']
|
|
||||||
cmd.extend(['-L', '%sg' % (size)])
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error creating snapshot')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _mangle_lv_name(self, name):
|
|
||||||
# Linux LVM reserves name that starts with snapshot, so that
|
|
||||||
# such volume name can't be created. Mangle it.
|
|
||||||
if not name.startswith('snapshot'):
|
|
||||||
return name
|
|
||||||
return '_' + name
|
|
||||||
|
|
||||||
def _lv_is_active(self, name):
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvdisplay', '--noheading', '-C', '-o',
|
|
||||||
'Attr', '%s/%s' % (self.vg_name, name)]
|
|
||||||
out, _err = self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
if out:
|
|
||||||
out = out.strip()
|
|
||||||
# An example output might be '-wi-a----'; the 4th index specifies
|
|
||||||
# the status of the volume. 'a' for active, '-' for inactive.
|
|
||||||
if (out[4] == 'a'):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def deactivate_lv(self, name):
|
|
||||||
lv_path = self.vg_name + '/' + self._mangle_lv_name(name)
|
|
||||||
cmd = ['lvchange', '-a', 'n']
|
|
||||||
cmd.append(lv_path)
|
|
||||||
try:
|
|
||||||
self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error deactivating LV')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Wait until lv is deactivated to return in
|
|
||||||
# order to prevent a race condition.
|
|
||||||
self._wait_for_volume_deactivation(name)
|
|
||||||
|
|
||||||
@utils.retry(exceptions=exception.VolumeNotDeactivated, retries=3,
|
|
||||||
backoff_rate=1)
|
|
||||||
def _wait_for_volume_deactivation(self, name):
|
|
||||||
LOG.debug("Checking to see if volume %s has been deactivated.",
|
|
||||||
name)
|
|
||||||
if self._lv_is_active(name):
|
|
||||||
LOG.debug("Volume %s is still active.", name)
|
|
||||||
raise exception.VolumeNotDeactivated(name=name)
|
|
||||||
else:
|
|
||||||
LOG.debug("Volume %s has been deactivated.", name)
|
|
||||||
|
|
||||||
def activate_lv(self, name, is_snapshot=False, permanent=False):
|
|
||||||
"""Ensure that logical volume/snapshot logical volume is activated.
|
|
||||||
|
|
||||||
:param name: Name of LV to activate
|
|
||||||
:param is_snapshot: whether LV is a snapshot
|
|
||||||
:param permanent: whether we should drop skipactivation flag
|
|
||||||
:raises: putils.ProcessExecutionError
|
|
||||||
"""
|
|
||||||
|
|
||||||
# This is a no-op if requested for a snapshot on a version
|
|
||||||
# of LVM that doesn't support snapshot activation.
|
|
||||||
# (Assume snapshot LV is always active.)
|
|
||||||
if is_snapshot and not self.supports_snapshot_lv_activation:
|
|
||||||
return
|
|
||||||
|
|
||||||
lv_path = self.vg_name + '/' + self._mangle_lv_name(name)
|
|
||||||
|
|
||||||
# Must pass --yes to activate both the snap LV and its origin LV.
|
|
||||||
# Otherwise lvchange asks if you would like to do this interactively,
|
|
||||||
# and fails.
|
|
||||||
cmd = ['lvchange', '-a', 'y', '--yes']
|
|
||||||
|
|
||||||
if self.supports_lvchange_ignoreskipactivation:
|
|
||||||
cmd.append('-K')
|
|
||||||
# If permanent=True is specified, drop the skipactivation flag in
|
|
||||||
# order to make this LV automatically activated after next reboot.
|
|
||||||
if permanent:
|
|
||||||
cmd += ['-k', 'n']
|
|
||||||
|
|
||||||
cmd.append(lv_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error activating LV')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
@utils.retry(putils.ProcessExecutionError)
|
|
||||||
def delete(self, name):
|
|
||||||
"""Delete logical volume or snapshot.
|
|
||||||
|
|
||||||
:param name: Name of LV to delete
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def run_udevadm_settle():
|
|
||||||
self._execute('udevadm', 'settle',
|
|
||||||
root_helper=self._root_helper, run_as_root=True,
|
|
||||||
check_exit_code=False)
|
|
||||||
|
|
||||||
# LV removal seems to be a race with other writers or udev in
|
|
||||||
# some cases (see LP #1270192), so we enable retry deactivation
|
|
||||||
LVM_CONFIG = 'activation { retry_deactivation = 1} '
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._execute(
|
|
||||||
'lvremove',
|
|
||||||
'--config', LVM_CONFIG,
|
|
||||||
'-f',
|
|
||||||
'%s/%s' % (self.vg_name, name),
|
|
||||||
root_helper=self._root_helper, run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.debug('Error reported running lvremove: CMD: %(command)s, '
|
|
||||||
'RESPONSE: %(response)s',
|
|
||||||
{'command': err.cmd, 'response': err.stderr})
|
|
||||||
|
|
||||||
LOG.debug('Attempting udev settle and retry of lvremove...')
|
|
||||||
run_udevadm_settle()
|
|
||||||
|
|
||||||
# The previous failing lvremove -f might leave behind
|
|
||||||
# suspended devices; when lvmetad is not available, any
|
|
||||||
# further lvm command will block forever.
|
|
||||||
# Therefore we need to skip suspended devices on retry.
|
|
||||||
LVM_CONFIG += 'devices { ignore_suspended_devices = 1}'
|
|
||||||
|
|
||||||
self._execute(
|
|
||||||
'lvremove',
|
|
||||||
'--config', LVM_CONFIG,
|
|
||||||
'-f',
|
|
||||||
'%s/%s' % (self.vg_name, name),
|
|
||||||
root_helper=self._root_helper, run_as_root=True)
|
|
||||||
LOG.debug('Successfully deleted volume: %s after '
|
|
||||||
'udev settle.', name)
|
|
||||||
|
|
||||||
def revert(self, snapshot_name):
|
|
||||||
"""Revert an LV from snapshot.
|
|
||||||
|
|
||||||
:param snapshot_name: Name of snapshot to revert
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._execute('lvconvert', '--merge',
|
|
||||||
snapshot_name, root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
|
|
||||||
def lv_has_snapshot(self, name):
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvdisplay', '--noheading', '-C', '-o',
|
|
||||||
'Attr', '%s/%s' % (self.vg_name, name)]
|
|
||||||
out, _err = self._execute(*cmd,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
if out:
|
|
||||||
out = out.strip()
|
|
||||||
if (out[0] == 'o') or (out[0] == 'O'):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def extend_volume(self, lv_name, new_size):
|
|
||||||
"""Extend the size of an existing volume."""
|
|
||||||
# Volumes with snaps have attributes 'o' or 'O' and will be
|
|
||||||
# deactivated, but Thin Volumes with snaps have attribute 'V'
|
|
||||||
# and won't be deactivated because the lv_has_snapshot method looks
|
|
||||||
# for 'o' or 'O'
|
|
||||||
if self.lv_has_snapshot(lv_name):
|
|
||||||
self.deactivate_lv(lv_name)
|
|
||||||
try:
|
|
||||||
cmd = LVM.LVM_CMD_PREFIX + ['lvextend', '-L', new_size,
|
|
||||||
'%s/%s' % (self.vg_name, lv_name)]
|
|
||||||
self._execute(*cmd, root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error extending Volume')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def vg_mirror_free_space(self, mirror_count):
|
|
||||||
free_capacity = 0.0
|
|
||||||
|
|
||||||
disks = []
|
|
||||||
for pv in self.pv_list:
|
|
||||||
disks.append(float(pv['available']))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
disks = sorted([a for a in disks if a > 0.0], reverse=True)
|
|
||||||
if len(disks) <= mirror_count:
|
|
||||||
break
|
|
||||||
# consume the smallest disk
|
|
||||||
disk = disks[-1]
|
|
||||||
disks = disks[:-1]
|
|
||||||
# match extents for each mirror on the largest disks
|
|
||||||
for index in list(range(mirror_count)):
|
|
||||||
disks[index] -= disk
|
|
||||||
free_capacity += disk
|
|
||||||
|
|
||||||
return free_capacity
|
|
||||||
|
|
||||||
def vg_mirror_size(self, mirror_count):
|
|
||||||
return (self.vg_free_space / (mirror_count + 1))
|
|
||||||
|
|
||||||
def rename_volume(self, lv_name, new_name):
|
|
||||||
"""Change the name of an existing volume."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._execute('lvrename', self.vg_name, lv_name, new_name,
|
|
||||||
root_helper=self._root_helper,
|
|
||||||
run_as_root=True)
|
|
||||||
except putils.ProcessExecutionError as err:
|
|
||||||
LOG.exception('Error renaming logical volume')
|
|
||||||
LOG.error('Cmd :%s', err.cmd)
|
|
||||||
LOG.error('StdOut :%s', err.stdout)
|
|
||||||
LOG.error('StdErr :%s', err.stderr)
|
|
||||||
raise
|
|
|
@ -1,23 +0,0 @@
|
||||||
# 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 oslo_privsep import capabilities as c
|
|
||||||
from oslo_privsep import priv_context
|
|
||||||
|
|
||||||
# It is expected that most (if not all) os-brick operations can be
|
|
||||||
# executed with these privileges.
|
|
||||||
default = priv_context.PrivContext(
|
|
||||||
__name__,
|
|
||||||
cfg_section='privsep_osbrick',
|
|
||||||
pypath=__name__ + '.default',
|
|
||||||
capabilities=[c.CAP_SYS_ADMIN],
|
|
||||||
)
|
|
|
@ -1,220 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Just in case it wasn't clear, this is a massive security back-door.
|
|
||||||
|
|
||||||
`execute_root()` (or the same via `execute(run_as_root=True)`) allows
|
|
||||||
any command to be run as the privileged user (default "root"). This
|
|
||||||
is intended only as an expedient transition and should be removed
|
|
||||||
ASAP.
|
|
||||||
|
|
||||||
This is not completely unreasonable because:
|
|
||||||
|
|
||||||
1. We have no tool/workflow for merging changes to rootwrap filter
|
|
||||||
configs from os-brick into nova/cinder, which makes it difficult
|
|
||||||
to evolve these loosely coupled projects.
|
|
||||||
|
|
||||||
2. Let's not pretend the earlier situation was any better. The
|
|
||||||
rootwrap filters config contained several entries like "allow cp as
|
|
||||||
root with any arguments", etc, and would have posed only a mild
|
|
||||||
inconvenience to an attacker. At least with privsep we can (in
|
|
||||||
principle) run the "root" commands as a non-root uid, with
|
|
||||||
restricted Linux capabilities.
|
|
||||||
|
|
||||||
The plan is to switch os-brick to privsep using this module (removing
|
|
||||||
the urgency of (1)), then work on the larger refactor that addresses
|
|
||||||
(2) in followup changes.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import six
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import strutils
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import privileged
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def custom_execute(*cmd, **kwargs):
|
|
||||||
"""Custom execute with additional functionality on top of Oslo's.
|
|
||||||
|
|
||||||
Additional features are timeouts and exponential backoff retries.
|
|
||||||
|
|
||||||
The exponential backoff retries replaces standard Oslo random sleep times
|
|
||||||
that range from 200ms to 2seconds when attempts is greater than 1, but it
|
|
||||||
is disabled if delay_on_retry is passed as a parameter.
|
|
||||||
|
|
||||||
Exponential backoff is controlled via interval and backoff_rate parameters,
|
|
||||||
just like the os_brick.utils.retry decorator.
|
|
||||||
|
|
||||||
To use the timeout mechanism to stop the subprocess with a specific signal
|
|
||||||
after a number of seconds we must pass a non-zero timeout value in the
|
|
||||||
call.
|
|
||||||
|
|
||||||
When using multiple attempts and timeout at the same time the method will
|
|
||||||
only raise the timeout exception to the caller if the last try timeouts.
|
|
||||||
|
|
||||||
Timeout mechanism is controlled with timeout, signal, and raise_timeout
|
|
||||||
parameters.
|
|
||||||
|
|
||||||
:param interval: The multiplier
|
|
||||||
:param backoff_rate: Base used for the exponential backoff
|
|
||||||
:param timeout: Timeout defined in seconds
|
|
||||||
:param signal: Signal to use to stop the process on timeout
|
|
||||||
:param raise_timeout: Raise and exception on timeout or return error as
|
|
||||||
stderr. Defaults to raising if check_exit_code is
|
|
||||||
not False.
|
|
||||||
:returns: Tuple with stdout and stderr
|
|
||||||
"""
|
|
||||||
# Since python 2 doesn't have nonlocal we use a mutable variable to store
|
|
||||||
# the previous attempt number, the timeout handler, and the process that
|
|
||||||
# timed out
|
|
||||||
shared_data = [0, None, None]
|
|
||||||
|
|
||||||
def on_timeout(proc):
|
|
||||||
sanitized_cmd = strutils.mask_password(' '.join(cmd))
|
|
||||||
LOG.warning('Stopping %(cmd)s with signal %(signal)s after %(time)ss.',
|
|
||||||
{'signal': sig_end, 'cmd': sanitized_cmd, 'time': timeout})
|
|
||||||
shared_data[2] = proc
|
|
||||||
proc.send_signal(sig_end)
|
|
||||||
|
|
||||||
def on_execute(proc):
|
|
||||||
# Call user's on_execute method
|
|
||||||
if on_execute_call:
|
|
||||||
on_execute_call(proc)
|
|
||||||
# Sleep if this is not the first try and we have a timeout interval
|
|
||||||
if shared_data[0] and interval:
|
|
||||||
exp = backoff_rate ** shared_data[0]
|
|
||||||
wait_for = max(0, interval * exp)
|
|
||||||
LOG.debug('Sleeping for %s seconds', wait_for)
|
|
||||||
time.sleep(wait_for)
|
|
||||||
# Increase the number of tries and start the timeout timer
|
|
||||||
shared_data[0] += 1
|
|
||||||
if timeout:
|
|
||||||
shared_data[2] = None
|
|
||||||
shared_data[1] = threading.Timer(timeout, on_timeout, (proc,))
|
|
||||||
shared_data[1].start()
|
|
||||||
|
|
||||||
def on_completion(proc):
|
|
||||||
# This is always called regardless of success or failure
|
|
||||||
# Cancel the timeout timer
|
|
||||||
if shared_data[1]:
|
|
||||||
shared_data[1].cancel()
|
|
||||||
# Call user's on_completion method
|
|
||||||
if on_completion_call:
|
|
||||||
on_completion_call(proc)
|
|
||||||
|
|
||||||
# We will be doing the wait ourselves in on_execute
|
|
||||||
if 'delay_on_retry' in kwargs:
|
|
||||||
interval = None
|
|
||||||
else:
|
|
||||||
kwargs['delay_on_retry'] = False
|
|
||||||
interval = kwargs.pop('interval', 1)
|
|
||||||
backoff_rate = kwargs.pop('backoff_rate', 2)
|
|
||||||
|
|
||||||
timeout = kwargs.pop('timeout', None)
|
|
||||||
sig_end = kwargs.pop('signal', signal.SIGTERM)
|
|
||||||
default_raise_timeout = kwargs.get('check_exit_code', True)
|
|
||||||
raise_timeout = kwargs.pop('raise_timeout', default_raise_timeout)
|
|
||||||
|
|
||||||
on_execute_call = kwargs.pop('on_execute', None)
|
|
||||||
on_completion_call = kwargs.pop('on_completion', None)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return putils.execute(on_execute=on_execute,
|
|
||||||
on_completion=on_completion, *cmd, **kwargs)
|
|
||||||
except putils.ProcessExecutionError:
|
|
||||||
# proc is only stored if a timeout happened
|
|
||||||
proc = shared_data[2]
|
|
||||||
if proc:
|
|
||||||
sanitized_cmd = strutils.mask_password(' '.join(cmd))
|
|
||||||
msg = ('Time out on proc %(pid)s after waiting %(time)s seconds '
|
|
||||||
'when running %(cmd)s' %
|
|
||||||
{'pid': proc.pid, 'time': timeout, 'cmd': sanitized_cmd})
|
|
||||||
LOG.debug(msg)
|
|
||||||
if raise_timeout:
|
|
||||||
raise exception.ExecutionTimeout(stdout='', stderr=msg,
|
|
||||||
cmd=sanitized_cmd)
|
|
||||||
return '', msg
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
# Entrypoint used for rootwrap.py transition code. Don't use this for
|
|
||||||
# other purposes, since it will be removed when we think the
|
|
||||||
# transition is finished.
|
|
||||||
def execute(*cmd, **kwargs):
|
|
||||||
"""NB: Raises processutils.ProcessExecutionError on failure."""
|
|
||||||
run_as_root = kwargs.pop('run_as_root', False)
|
|
||||||
kwargs.pop('root_helper', None)
|
|
||||||
try:
|
|
||||||
if run_as_root:
|
|
||||||
return execute_root(*cmd, **kwargs)
|
|
||||||
else:
|
|
||||||
return custom_execute(*cmd, **kwargs)
|
|
||||||
except OSError as e:
|
|
||||||
# Note:
|
|
||||||
# putils.execute('bogus', run_as_root=True)
|
|
||||||
# raises ProcessExecutionError(exit_code=1) (because there's a
|
|
||||||
# "sh -c bogus" involved in there somewhere, but:
|
|
||||||
# putils.execute('bogus', run_as_root=False)
|
|
||||||
# raises OSError(not found).
|
|
||||||
#
|
|
||||||
# Lots of code in os-brick catches only ProcessExecutionError
|
|
||||||
# and never encountered the latter when using rootwrap.
|
|
||||||
# Rather than fix all the callers, we just always raise
|
|
||||||
# ProcessExecutionError here :(
|
|
||||||
|
|
||||||
sanitized_cmd = strutils.mask_password(' '.join(cmd))
|
|
||||||
raise putils.ProcessExecutionError(
|
|
||||||
cmd=sanitized_cmd, description=six.text_type(e))
|
|
||||||
|
|
||||||
|
|
||||||
# See comment on `execute`
|
|
||||||
@privileged.default.entrypoint
|
|
||||||
def execute_root(*cmd, **kwargs):
|
|
||||||
"""NB: Raises processutils.ProcessExecutionError/OSError on failure."""
|
|
||||||
return custom_execute(*cmd, shell=False, run_as_root=False, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@privileged.default.entrypoint
|
|
||||||
def unlink_root(*links, **kwargs):
|
|
||||||
"""Unlink system links with sys admin privileges.
|
|
||||||
|
|
||||||
By default it will raise an exception if a link does not exist and stop
|
|
||||||
unlinking remaining links.
|
|
||||||
|
|
||||||
This behavior can be modified passing optional parameters `no_errors` and
|
|
||||||
`raise_at_end`.
|
|
||||||
|
|
||||||
:param no_errors: Don't raise an exception on error
|
|
||||||
"param raise_at_end: Don't raise an exception on first error, try to
|
|
||||||
unlink all links and then raise a ChainedException
|
|
||||||
with all the errors that where found.
|
|
||||||
"""
|
|
||||||
no_errors = kwargs.get('no_errors', False)
|
|
||||||
raise_at_end = kwargs.get('raise_at_end', False)
|
|
||||||
exc = exception.ExceptionChainer()
|
|
||||||
catch_exception = no_errors or raise_at_end
|
|
||||||
for link in links:
|
|
||||||
with exc.context(catch_exception, 'Unlink failed for %s', link):
|
|
||||||
os.unlink(link)
|
|
||||||
if not no_errors and raise_at_end and exc:
|
|
||||||
raise exc
|
|
|
@ -1,261 +0,0 @@
|
||||||
# Copyright (c) 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Remote filesystem client utilities."""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import six
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick import executor
|
|
||||||
from os_brick.i18n import _
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteFsClient(executor.Executor):
|
|
||||||
|
|
||||||
def __init__(self, mount_type, root_helper,
|
|
||||||
execute=None, *args, **kwargs):
|
|
||||||
super(RemoteFsClient, self).__init__(root_helper, execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
mount_type_to_option_prefix = {
|
|
||||||
'nfs': 'nfs',
|
|
||||||
'cifs': 'smbfs',
|
|
||||||
'glusterfs': 'glusterfs',
|
|
||||||
'vzstorage': 'vzstorage',
|
|
||||||
'quobyte': 'quobyte',
|
|
||||||
'scality': 'scality'
|
|
||||||
}
|
|
||||||
|
|
||||||
if mount_type not in mount_type_to_option_prefix:
|
|
||||||
raise exception.ProtocolNotSupported(protocol=mount_type)
|
|
||||||
|
|
||||||
self._mount_type = mount_type
|
|
||||||
option_prefix = mount_type_to_option_prefix[mount_type]
|
|
||||||
|
|
||||||
self._mount_base = kwargs.get(option_prefix + '_mount_point_base')
|
|
||||||
if not self._mount_base:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
err=_('%s_mount_point_base required') % option_prefix)
|
|
||||||
|
|
||||||
self._mount_options = kwargs.get(option_prefix + '_mount_options')
|
|
||||||
|
|
||||||
if mount_type == "nfs":
|
|
||||||
self._check_nfs_options()
|
|
||||||
|
|
||||||
def get_mount_base(self):
|
|
||||||
return self._mount_base
|
|
||||||
|
|
||||||
def _get_hash_str(self, base_str):
|
|
||||||
"""Return a string that represents hash of base_str (hex format)."""
|
|
||||||
if isinstance(base_str, six.text_type):
|
|
||||||
base_str = base_str.encode('utf-8')
|
|
||||||
return hashlib.md5(base_str).hexdigest()
|
|
||||||
|
|
||||||
def get_mount_point(self, device_name):
|
|
||||||
"""Get Mount Point.
|
|
||||||
|
|
||||||
:param device_name: example 172.18.194.100:/var/nfs
|
|
||||||
"""
|
|
||||||
return os.path.join(self._mount_base,
|
|
||||||
self._get_hash_str(device_name))
|
|
||||||
|
|
||||||
def _read_mounts(self):
|
|
||||||
(out, _err) = self._execute('mount', check_exit_code=0)
|
|
||||||
lines = out.split('\n')
|
|
||||||
mounts = {}
|
|
||||||
for line in lines:
|
|
||||||
tokens = line.split()
|
|
||||||
if 2 < len(tokens):
|
|
||||||
device = tokens[0]
|
|
||||||
mnt_point = tokens[2]
|
|
||||||
mounts[mnt_point] = device
|
|
||||||
return mounts
|
|
||||||
|
|
||||||
def mount(self, share, flags=None):
|
|
||||||
"""Mount given share."""
|
|
||||||
mount_path = self.get_mount_point(share)
|
|
||||||
|
|
||||||
if mount_path in self._read_mounts():
|
|
||||||
LOG.info('Already mounted: %s', mount_path)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._execute('mkdir', '-p', mount_path, check_exit_code=0)
|
|
||||||
if self._mount_type == 'nfs':
|
|
||||||
self._mount_nfs(share, mount_path, flags)
|
|
||||||
else:
|
|
||||||
self._do_mount(self._mount_type, share, mount_path,
|
|
||||||
self._mount_options, flags)
|
|
||||||
|
|
||||||
def _do_mount(self, mount_type, share, mount_path, mount_options=None,
|
|
||||||
flags=None):
|
|
||||||
"""Mounts share based on the specified params."""
|
|
||||||
mnt_cmd = ['mount', '-t', mount_type]
|
|
||||||
if mount_options is not None:
|
|
||||||
mnt_cmd.extend(['-o', mount_options])
|
|
||||||
if flags is not None:
|
|
||||||
mnt_cmd.extend(flags)
|
|
||||||
mnt_cmd.extend([share, mount_path])
|
|
||||||
|
|
||||||
self._execute(*mnt_cmd, root_helper=self._root_helper,
|
|
||||||
run_as_root=True, check_exit_code=0)
|
|
||||||
|
|
||||||
def _mount_nfs(self, nfs_share, mount_path, flags=None):
|
|
||||||
"""Mount nfs share using present mount types."""
|
|
||||||
mnt_errors = {}
|
|
||||||
|
|
||||||
# This loop allows us to first try to mount with NFS 4.1 for pNFS
|
|
||||||
# support but falls back to mount NFS 4 or NFS 3 if either the client
|
|
||||||
# or server do not support it.
|
|
||||||
for mnt_type in sorted(self._nfs_mount_type_opts.keys(), reverse=True):
|
|
||||||
options = self._nfs_mount_type_opts[mnt_type]
|
|
||||||
try:
|
|
||||||
self._do_mount('nfs', nfs_share, mount_path, options, flags)
|
|
||||||
LOG.debug('Mounted %(sh)s using %(mnt_type)s.',
|
|
||||||
{'sh': nfs_share, 'mnt_type': mnt_type})
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
mnt_errors[mnt_type] = six.text_type(e)
|
|
||||||
LOG.debug('Failed to do %s mount.', mnt_type)
|
|
||||||
raise exception.BrickException(_("NFS mount failed for share %(sh)s. "
|
|
||||||
"Error - %(error)s")
|
|
||||||
% {'sh': nfs_share,
|
|
||||||
'error': mnt_errors})
|
|
||||||
|
|
||||||
def _check_nfs_options(self):
|
|
||||||
"""Checks and prepares nfs mount type options."""
|
|
||||||
self._nfs_mount_type_opts = {'nfs': self._mount_options}
|
|
||||||
nfs_vers_opt_patterns = ['^nfsvers', '^vers', '^v[\d]']
|
|
||||||
for opt in nfs_vers_opt_patterns:
|
|
||||||
if self._option_exists(self._mount_options, opt):
|
|
||||||
return
|
|
||||||
|
|
||||||
# pNFS requires NFS 4.1. The mount.nfs4 utility does not automatically
|
|
||||||
# negotiate 4.1 support, we have to ask for it by specifying two
|
|
||||||
# options: vers=4 and minorversion=1.
|
|
||||||
pnfs_opts = self._update_option(self._mount_options, 'vers', '4')
|
|
||||||
pnfs_opts = self._update_option(pnfs_opts, 'minorversion', '1')
|
|
||||||
self._nfs_mount_type_opts['pnfs'] = pnfs_opts
|
|
||||||
|
|
||||||
def _option_exists(self, options, opt_pattern):
|
|
||||||
"""Checks if the option exists in nfs options and returns position."""
|
|
||||||
options = [x.strip() for x in options.split(',')] if options else []
|
|
||||||
pos = 0
|
|
||||||
for opt in options:
|
|
||||||
pos = pos + 1
|
|
||||||
if re.match(opt_pattern, opt, flags=0):
|
|
||||||
return pos
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _update_option(self, options, option, value=None):
|
|
||||||
"""Update option if exists else adds it and returns new options."""
|
|
||||||
opts = [x.strip() for x in options.split(',')] if options else []
|
|
||||||
pos = self._option_exists(options, option)
|
|
||||||
if pos:
|
|
||||||
opts.pop(pos - 1)
|
|
||||||
opt = '%s=%s' % (option, value) if value else option
|
|
||||||
opts.append(opt)
|
|
||||||
return ",".join(opts) if len(opts) > 1 else opts[0]
|
|
||||||
|
|
||||||
|
|
||||||
class ScalityRemoteFsClient(RemoteFsClient):
|
|
||||||
def __init__(self, mount_type, root_helper,
|
|
||||||
execute=None, *args, **kwargs):
|
|
||||||
super(ScalityRemoteFsClient, self).__init__(mount_type, root_helper,
|
|
||||||
execute=execute,
|
|
||||||
*args, **kwargs)
|
|
||||||
self._mount_type = mount_type
|
|
||||||
self._mount_base = kwargs.get(
|
|
||||||
'scality_mount_point_base', "").rstrip('/')
|
|
||||||
if not self._mount_base:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
err=_('scality_mount_point_base required'))
|
|
||||||
self._mount_options = None
|
|
||||||
|
|
||||||
def get_mount_point(self, device_name):
|
|
||||||
return os.path.join(self._mount_base,
|
|
||||||
device_name,
|
|
||||||
"00")
|
|
||||||
|
|
||||||
def mount(self, share, flags=None):
|
|
||||||
"""Mount the Scality ScaleOut FS.
|
|
||||||
|
|
||||||
The `share` argument is ignored because you can't mount several
|
|
||||||
SOFS at the same type on a single server. But we want to keep the
|
|
||||||
same method signature for class inheritance purpose.
|
|
||||||
"""
|
|
||||||
if self._mount_base in self._read_mounts():
|
|
||||||
LOG.info('Already mounted: %s', self._mount_base)
|
|
||||||
return
|
|
||||||
self._execute('mkdir', '-p', self._mount_base, check_exit_code=0)
|
|
||||||
super(ScalityRemoteFsClient, self)._do_mount(
|
|
||||||
'sofs', '/etc/sfused.conf', self._mount_base)
|
|
||||||
|
|
||||||
|
|
||||||
class VZStorageRemoteFSClient(RemoteFsClient):
|
|
||||||
def _vzstorage_write_mds_list(self, cluster_name, mdss):
|
|
||||||
tmp_dir = tempfile.mkdtemp(prefix='vzstorage-')
|
|
||||||
tmp_bs_path = os.path.join(tmp_dir, 'bs_list')
|
|
||||||
with open(tmp_bs_path, 'w') as f:
|
|
||||||
for mds in mdss:
|
|
||||||
f.write(mds + "\n")
|
|
||||||
|
|
||||||
conf_dir = os.path.join('/etc/pstorage/clusters', cluster_name)
|
|
||||||
if os.path.exists(conf_dir):
|
|
||||||
bs_path = os.path.join(conf_dir, 'bs_list')
|
|
||||||
self._execute('cp', '-f', tmp_bs_path, bs_path,
|
|
||||||
root_helper=self._root_helper, run_as_root=True)
|
|
||||||
else:
|
|
||||||
self._execute('cp', '-rf', tmp_dir, conf_dir,
|
|
||||||
root_helper=self._root_helper, run_as_root=True)
|
|
||||||
self._execute('chown', '-R', 'root:root', conf_dir,
|
|
||||||
root_helper=self._root_helper, run_as_root=True)
|
|
||||||
|
|
||||||
def _do_mount(self, mount_type, vz_share, mount_path,
|
|
||||||
mount_options=None, flags=None):
|
|
||||||
m = re.search("(?:(\S+):\/)?([a-zA-Z0-9_-]+)(?::(\S+))?", vz_share)
|
|
||||||
if not m:
|
|
||||||
msg = (_("Invalid Virtuozzo Storage share specification: %r."
|
|
||||||
"Must be: [MDS1[,MDS2],...:/]<CLUSTER NAME>[:PASSWORD].")
|
|
||||||
% vz_share)
|
|
||||||
raise exception.BrickException(msg)
|
|
||||||
|
|
||||||
mdss = m.group(1)
|
|
||||||
cluster_name = m.group(2)
|
|
||||||
passwd = m.group(3)
|
|
||||||
|
|
||||||
if mdss:
|
|
||||||
mdss = mdss.split(',')
|
|
||||||
self._vzstorage_write_mds_list(cluster_name, mdss)
|
|
||||||
|
|
||||||
if passwd:
|
|
||||||
self._execute('pstorage', '-c', cluster_name, 'auth-node', '-P',
|
|
||||||
process_input=passwd,
|
|
||||||
root_helper=self._root_helper, run_as_root=True)
|
|
||||||
|
|
||||||
mnt_cmd = ['pstorage-mount', '-c', cluster_name]
|
|
||||||
if flags:
|
|
||||||
mnt_cmd.extend(flags)
|
|
||||||
mnt_cmd.extend([mount_path])
|
|
||||||
|
|
||||||
self._execute(*mnt_cmd, root_helper=self._root_helper,
|
|
||||||
run_as_root=True, check_exit_code=0)
|
|
|
@ -1,129 +0,0 @@
|
||||||
# Copyright 2016 Cloudbase Solutions Srl
|
|
||||||
# All Rights Reserved
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""Windows remote filesystem client utilities."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from os_win import utilsfactory
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.i18n import _
|
|
||||||
from os_brick.remotefs import remotefs
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WindowsRemoteFsClient(remotefs.RemoteFsClient):
|
|
||||||
_username_regex = re.compile(r'user(?:name)?=([^, ]+)')
|
|
||||||
_password_regex = re.compile(r'pass(?:word)?=([^, ]+)')
|
|
||||||
_loopback_share_map = {}
|
|
||||||
|
|
||||||
def __init__(self, mount_type, root_helper=None,
|
|
||||||
execute=None, *args, **kwargs):
|
|
||||||
mount_type_to_option_prefix = {
|
|
||||||
'cifs': 'smbfs',
|
|
||||||
'smbfs': 'smbfs',
|
|
||||||
}
|
|
||||||
|
|
||||||
self._local_path_for_loopback = kwargs.get('local_path_for_loopback',
|
|
||||||
False)
|
|
||||||
|
|
||||||
if mount_type not in mount_type_to_option_prefix:
|
|
||||||
raise exception.ProtocolNotSupported(protocol=mount_type)
|
|
||||||
|
|
||||||
self._mount_type = mount_type
|
|
||||||
option_prefix = mount_type_to_option_prefix[mount_type]
|
|
||||||
|
|
||||||
self._mount_base = kwargs.get(option_prefix + '_mount_point_base')
|
|
||||||
self._mount_options = kwargs.get(option_prefix + '_mount_options')
|
|
||||||
|
|
||||||
self._smbutils = utilsfactory.get_smbutils()
|
|
||||||
self._pathutils = utilsfactory.get_pathutils()
|
|
||||||
|
|
||||||
def get_local_share_path(self, share, expect_existing=True):
|
|
||||||
local_share_path = self._smbutils.get_smb_share_path(share)
|
|
||||||
if not local_share_path and expect_existing:
|
|
||||||
err_msg = _("Could not find the local "
|
|
||||||
"share path for %(share)s.")
|
|
||||||
raise exception.VolumePathsNotFound(err_msg % dict(share=share))
|
|
||||||
|
|
||||||
return local_share_path
|
|
||||||
|
|
||||||
def _get_share_norm_path(self, share):
|
|
||||||
return share.replace('/', '\\')
|
|
||||||
|
|
||||||
def get_share_name(self, share):
|
|
||||||
return self._get_share_norm_path(share).lstrip('\\').split('\\', 1)[1]
|
|
||||||
|
|
||||||
def mount(self, share, flags=None):
|
|
||||||
share_norm_path = self._get_share_norm_path(share)
|
|
||||||
use_local_path = (self._local_path_for_loopback and
|
|
||||||
self._smbutils.is_local_share(share_norm_path))
|
|
||||||
|
|
||||||
if use_local_path:
|
|
||||||
LOG.info("Skipping mounting local share %(share_path)s.",
|
|
||||||
dict(share_path=share_norm_path))
|
|
||||||
else:
|
|
||||||
mount_options = " ".join(
|
|
||||||
[self._mount_options or '', flags or ''])
|
|
||||||
username, password = self._parse_credentials(mount_options)
|
|
||||||
|
|
||||||
if not self._smbutils.check_smb_mapping(
|
|
||||||
share_norm_path):
|
|
||||||
self._smbutils.mount_smb_share(share_norm_path,
|
|
||||||
username=username,
|
|
||||||
password=password)
|
|
||||||
|
|
||||||
if self._mount_base:
|
|
||||||
self._create_mount_point(share, use_local_path)
|
|
||||||
|
|
||||||
def unmount(self, share):
|
|
||||||
self._smbutils.unmount_smb_share(self._get_share_norm_path(share))
|
|
||||||
|
|
||||||
def _create_mount_point(self, share, use_local_path):
|
|
||||||
# The mount point will contain a hash of the share so we're
|
|
||||||
# intentionally preserving the original share path as this is
|
|
||||||
# what the caller will expect.
|
|
||||||
mnt_point = self.get_mount_point(share)
|
|
||||||
share_norm_path = self._get_share_norm_path(share)
|
|
||||||
share_name = self.get_share_name(share)
|
|
||||||
symlink_dest = (share_norm_path if not use_local_path
|
|
||||||
else self.get_local_share_path(share_name))
|
|
||||||
|
|
||||||
if not os.path.isdir(self._mount_base):
|
|
||||||
os.makedirs(self._mount_base)
|
|
||||||
|
|
||||||
if os.path.exists(mnt_point):
|
|
||||||
if not self._pathutils.is_symlink(mnt_point):
|
|
||||||
raise exception.BrickException(_("Link path already exists "
|
|
||||||
"and it's not a symlink"))
|
|
||||||
else:
|
|
||||||
self._pathutils.create_sym_link(mnt_point, symlink_dest)
|
|
||||||
|
|
||||||
def _parse_credentials(self, opts_str):
|
|
||||||
if not opts_str:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
match = self._username_regex.findall(opts_str)
|
|
||||||
username = match[0] if match and match[0] != 'guest' else None
|
|
||||||
|
|
||||||
match = self._password_regex.findall(opts_str)
|
|
||||||
password = match[0] if match else None
|
|
||||||
|
|
||||||
return username, password
|
|
|
@ -1,103 +0,0 @@
|
||||||
# Copyright 2010-2011 OpenStack Foundation
|
|
||||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 logging
|
|
||||||
import os
|
|
||||||
import testtools
|
|
||||||
|
|
||||||
import fixtures
|
|
||||||
import mock
|
|
||||||
from oslo_utils import strutils
|
|
||||||
|
|
||||||
|
|
||||||
class TestCase(testtools.TestCase):
|
|
||||||
"""Test case base class for all unit tests."""
|
|
||||||
|
|
||||||
SENTINEL = object()
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Run before each test method to initialize test environment."""
|
|
||||||
super(TestCase, self).setUp()
|
|
||||||
|
|
||||||
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
|
|
||||||
try:
|
|
||||||
test_timeout = int(test_timeout)
|
|
||||||
except ValueError:
|
|
||||||
# If timeout value is invalid do not set a timeout.
|
|
||||||
test_timeout = 0
|
|
||||||
if test_timeout > 0:
|
|
||||||
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
|
|
||||||
self.useFixture(fixtures.NestedTempfile())
|
|
||||||
self.useFixture(fixtures.TempHomeDir())
|
|
||||||
|
|
||||||
environ_enabled = (lambda var_name:
|
|
||||||
strutils.bool_from_string(os.environ.get(var_name)))
|
|
||||||
if environ_enabled('OS_STDOUT_CAPTURE'):
|
|
||||||
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
|
|
||||||
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
|
|
||||||
if environ_enabled('OS_STDERR_CAPTURE'):
|
|
||||||
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
|
|
||||||
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
|
|
||||||
if environ_enabled('OS_LOG_CAPTURE'):
|
|
||||||
log_format = '%(levelname)s [%(name)s] %(message)s'
|
|
||||||
if environ_enabled('OS_DEBUG'):
|
|
||||||
level = logging.DEBUG
|
|
||||||
else:
|
|
||||||
level = logging.INFO
|
|
||||||
self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
|
|
||||||
format=log_format,
|
|
||||||
level=level))
|
|
||||||
|
|
||||||
def _common_cleanup(self):
|
|
||||||
"""Runs after each test method to tear down test environment."""
|
|
||||||
|
|
||||||
# Stop any timers
|
|
||||||
for x in self.injected:
|
|
||||||
try:
|
|
||||||
x.stop()
|
|
||||||
except AssertionError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Delete attributes that don't start with _ so they don't pin
|
|
||||||
# memory around unnecessarily for the duration of the test
|
|
||||||
# suite
|
|
||||||
for key in [k for k in self.__dict__.keys() if k[0] != '_']:
|
|
||||||
del self.__dict__[key]
|
|
||||||
|
|
||||||
def log_level(self, level):
|
|
||||||
"""Set logging level to the specified value."""
|
|
||||||
log_root = logging.getLogger(None).logger
|
|
||||||
log_root.setLevel(level)
|
|
||||||
|
|
||||||
def mock_object(self, obj, attr_name, new_attr=SENTINEL, **kwargs):
|
|
||||||
"""Use python mock to mock an object attribute
|
|
||||||
|
|
||||||
Mocks the specified objects attribute with the given value.
|
|
||||||
Automatically performs 'addCleanup' for the mock.
|
|
||||||
"""
|
|
||||||
args = [obj, attr_name]
|
|
||||||
if new_attr is not self.SENTINEL:
|
|
||||||
args.append(new_attr)
|
|
||||||
patcher = mock.patch.object(*args, **kwargs)
|
|
||||||
mocked = patcher.start()
|
|
||||||
self.addCleanup(patcher.stop)
|
|
||||||
return mocked
|
|
||||||
|
|
||||||
def patch(self, path, *args, **kwargs):
|
|
||||||
"""Use python mock to mock a path with automatic cleanup."""
|
|
||||||
patcher = mock.patch(path, *args, **kwargs)
|
|
||||||
result = patcher.start()
|
|
||||||
self.addCleanup(patcher.stop)
|
|
||||||
return result
|
|
|
@ -1,178 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 castellan.tests.unit.key_manager import fake
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from os_brick import encryptors
|
|
||||||
from os_brick.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeEncryptorTestCase(base.TestCase):
|
|
||||||
def _create(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(VolumeEncryptorTestCase, self).setUp()
|
|
||||||
self.connection_info = {
|
|
||||||
"data": {
|
|
||||||
"device_path": "/dev/disk/by-path/"
|
|
||||||
"ip-192.0.2.0:3260-iscsi-iqn.2010-10.org.openstack"
|
|
||||||
":volume-fake_uuid-lun-1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
self.root_helper = None
|
|
||||||
self.keymgr = fake.fake_api()
|
|
||||||
self.encryptor = self._create()
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEncryptorTestCase(VolumeEncryptorTestCase):
|
|
||||||
|
|
||||||
def _test_get_encryptor(self, provider, expected_provider_class):
|
|
||||||
encryption = {'control_location': 'front-end',
|
|
||||||
'provider': provider}
|
|
||||||
encryptor = encryptors.get_volume_encryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr,
|
|
||||||
**encryption)
|
|
||||||
self.assertIsInstance(encryptor, expected_provider_class)
|
|
||||||
|
|
||||||
def test_get_encryptors(self):
|
|
||||||
|
|
||||||
self._test_get_encryptor('luks',
|
|
||||||
encryptors.luks.LuksEncryptor)
|
|
||||||
# TODO(lyarwood): Remove the following in Pike
|
|
||||||
self._test_get_encryptor('LuksEncryptor',
|
|
||||||
encryptors.luks.LuksEncryptor)
|
|
||||||
self._test_get_encryptor('os_brick.encryptors.luks.LuksEncryptor',
|
|
||||||
encryptors.luks.LuksEncryptor)
|
|
||||||
self._test_get_encryptor('nova.volume.encryptors.luks.LuksEncryptor',
|
|
||||||
encryptors.luks.LuksEncryptor)
|
|
||||||
|
|
||||||
self._test_get_encryptor('plain',
|
|
||||||
encryptors.cryptsetup.CryptsetupEncryptor)
|
|
||||||
# TODO(lyarwood): Remove the following in Pike
|
|
||||||
self._test_get_encryptor('CryptsetupEncryptor',
|
|
||||||
encryptors.cryptsetup.CryptsetupEncryptor)
|
|
||||||
self._test_get_encryptor(
|
|
||||||
'os_brick.encryptors.cryptsetup.CryptsetupEncryptor',
|
|
||||||
encryptors.cryptsetup.CryptsetupEncryptor)
|
|
||||||
self._test_get_encryptor(
|
|
||||||
'nova.volume.encryptors.cryptsetup.CryptsetupEncryptor',
|
|
||||||
encryptors.cryptsetup.CryptsetupEncryptor)
|
|
||||||
|
|
||||||
self._test_get_encryptor(None,
|
|
||||||
encryptors.nop.NoOpEncryptor)
|
|
||||||
# TODO(lyarwood): Remove the following in Pike
|
|
||||||
self._test_get_encryptor('NoOpEncryptor',
|
|
||||||
encryptors.nop.NoOpEncryptor)
|
|
||||||
self._test_get_encryptor('os_brick.encryptors.nop.NoOpEncryptor',
|
|
||||||
encryptors.nop.NoOpEncryptor)
|
|
||||||
self._test_get_encryptor('nova.volume.encryptors.nop.NoopEncryptor',
|
|
||||||
encryptors.nop.NoOpEncryptor)
|
|
||||||
|
|
||||||
def test_get_error_encryptors(self):
|
|
||||||
encryption = {'control_location': 'front-end',
|
|
||||||
'provider': 'ErrorEncryptor'}
|
|
||||||
self.assertRaises(ValueError,
|
|
||||||
encryptors.get_volume_encryptor,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr,
|
|
||||||
**encryption)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.encryptors.LOG')
|
|
||||||
def test_error_log(self, log):
|
|
||||||
encryption = {'control_location': 'front-end',
|
|
||||||
'provider': 'TestEncryptor'}
|
|
||||||
provider = 'TestEncryptor'
|
|
||||||
try:
|
|
||||||
encryptors.get_volume_encryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr,
|
|
||||||
**encryption)
|
|
||||||
except Exception as e:
|
|
||||||
log.error.assert_called_once_with("Error instantiating "
|
|
||||||
"%(provider)s: "
|
|
||||||
"%(exception)s",
|
|
||||||
{'provider': provider,
|
|
||||||
'exception': e})
|
|
||||||
|
|
||||||
@mock.patch('os_brick.encryptors.LOG')
|
|
||||||
def test_get_missing_out_of_tree_encryptor_log(self, log):
|
|
||||||
provider = 'TestEncryptor'
|
|
||||||
encryption = {'control_location': 'front-end',
|
|
||||||
'provider': provider}
|
|
||||||
try:
|
|
||||||
encryptors.get_volume_encryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr,
|
|
||||||
**encryption)
|
|
||||||
except Exception as e:
|
|
||||||
log.error.assert_called_once_with("Error instantiating "
|
|
||||||
"%(provider)s: "
|
|
||||||
"%(exception)s",
|
|
||||||
{'provider': provider,
|
|
||||||
'exception': e})
|
|
||||||
log.warning.assert_called_once_with("Use of the out of tree "
|
|
||||||
"encryptor class %(provider)s "
|
|
||||||
"will be blocked with the "
|
|
||||||
"Queens release of os-brick.",
|
|
||||||
{'provider': provider})
|
|
||||||
|
|
||||||
@mock.patch('os_brick.encryptors.LOG')
|
|
||||||
def test_get_direct_encryptor_log(self, log):
|
|
||||||
encryption = {'control_location': 'front-end',
|
|
||||||
'provider': 'LuksEncryptor'}
|
|
||||||
encryptors.get_volume_encryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr,
|
|
||||||
**encryption)
|
|
||||||
|
|
||||||
encryption = {'control_location': 'front-end',
|
|
||||||
'provider': 'os_brick.encryptors.luks.LuksEncryptor'}
|
|
||||||
encryptors.get_volume_encryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr,
|
|
||||||
**encryption)
|
|
||||||
|
|
||||||
encryption = {'control_location': 'front-end',
|
|
||||||
'provider': 'nova.volume.encryptors.luks.LuksEncryptor'}
|
|
||||||
encryptors.get_volume_encryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr,
|
|
||||||
**encryption)
|
|
||||||
|
|
||||||
log.warning.assert_has_calls([
|
|
||||||
mock.call("Use of the in tree encryptor class %(provider)s by "
|
|
||||||
"directly referencing the implementation class will be "
|
|
||||||
"blocked in the Queens release of os-brick.",
|
|
||||||
{'provider': 'LuksEncryptor'}),
|
|
||||||
mock.call("Use of the in tree encryptor class %(provider)s by "
|
|
||||||
"directly referencing the implementation class will be "
|
|
||||||
"blocked in the Queens release of os-brick.",
|
|
||||||
{'provider':
|
|
||||||
'os_brick.encryptors.luks.LuksEncryptor'}),
|
|
||||||
mock.call("Use of the in tree encryptor class %(provider)s by "
|
|
||||||
"directly referencing the implementation class will be "
|
|
||||||
"blocked in the Queens release of os-brick.",
|
|
||||||
{'provider':
|
|
||||||
'nova.volume.encryptors.luks.LuksEncryptor'})])
|
|
|
@ -1,187 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 copy
|
|
||||||
import mock
|
|
||||||
import six
|
|
||||||
|
|
||||||
from castellan.common.objects import symmetric_key as key
|
|
||||||
from castellan.tests.unit.key_manager import fake
|
|
||||||
from os_brick.encryptors import cryptsetup
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.tests.encryptors import test_base
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
|
|
||||||
|
|
||||||
def fake__get_key(context, passphrase):
|
|
||||||
raw = bytes(binascii.unhexlify(passphrase))
|
|
||||||
symmetric_key = key.SymmetricKey('AES', len(raw) * 8, raw)
|
|
||||||
return symmetric_key
|
|
||||||
|
|
||||||
|
|
||||||
class CryptsetupEncryptorTestCase(test_base.VolumeEncryptorTestCase):
|
|
||||||
|
|
||||||
@mock.patch('os.path.exists', return_value=False)
|
|
||||||
def _create(self, mock_exists):
|
|
||||||
return cryptsetup.CryptsetupEncryptor(
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
keymgr=self.keymgr)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(CryptsetupEncryptorTestCase, self).setUp()
|
|
||||||
|
|
||||||
self.dev_path = self.connection_info['data']['device_path']
|
|
||||||
self.dev_name = 'crypt-%s' % self.dev_path.split('/')[-1]
|
|
||||||
|
|
||||||
self.symlink_path = self.dev_path
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test__open_volume(self, mock_execute):
|
|
||||||
self.encryptor._open_volume("passphrase")
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
|
|
||||||
self.dev_path, process_input='passphrase',
|
|
||||||
run_as_root=True,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_attach_volume(self, mock_execute):
|
|
||||||
fake_key = 'e8b76872e3b04c18b3b6656bbf6f5089'
|
|
||||||
self.encryptor._get_key = mock.MagicMock()
|
|
||||||
self.encryptor._get_key.return_value = fake__get_key(None, fake_key)
|
|
||||||
|
|
||||||
self.encryptor.attach_volume(None)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
|
|
||||||
self.dev_path, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
mock.call('ln', '--symbolic', '--force',
|
|
||||||
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test__close_volume(self, mock_execute):
|
|
||||||
self.encryptor.detach_volume()
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'remove', self.dev_name,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_detach_volume(self, mock_execute):
|
|
||||||
self.encryptor.detach_volume()
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'remove', self.dev_name,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_init_volume_encryption_not_supported(self):
|
|
||||||
# Tests that creating a CryptsetupEncryptor fails if there is no
|
|
||||||
# device_path key.
|
|
||||||
type = 'unencryptable'
|
|
||||||
data = dict(volume_id='a194699b-aa07-4433-a945-a5d23802043e')
|
|
||||||
connection_info = dict(driver_volume_type=type, data=data)
|
|
||||||
exc = self.assertRaises(exception.VolumeEncryptionNotSupported,
|
|
||||||
cryptsetup.CryptsetupEncryptor,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=connection_info,
|
|
||||||
keymgr=fake.fake_api())
|
|
||||||
self.assertIn(type, six.text_type(exc))
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
@mock.patch('os.path.exists', return_value=True)
|
|
||||||
def test_init_volume_encryption_with_old_name(self, mock_exists,
|
|
||||||
mock_execute):
|
|
||||||
# If an old name crypt device exists, dev_path should be the old name.
|
|
||||||
old_dev_name = self.dev_path.split('/')[-1]
|
|
||||||
encryptor = cryptsetup.CryptsetupEncryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr)
|
|
||||||
self.assertFalse(encryptor.dev_name.startswith('crypt-'))
|
|
||||||
self.assertEqual(old_dev_name, encryptor.dev_name)
|
|
||||||
self.assertEqual(self.dev_path, encryptor.dev_path)
|
|
||||||
self.assertEqual(self.symlink_path, encryptor.symlink_path)
|
|
||||||
mock_exists.assert_called_once_with('/dev/mapper/%s' % old_dev_name)
|
|
||||||
mock_execute.assert_called_once_with(
|
|
||||||
'cryptsetup', 'status', old_dev_name, run_as_root=True)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
@mock.patch('os.path.exists', side_effect=[False, True])
|
|
||||||
def test_init_volume_encryption_with_wwn(self, mock_exists, mock_execute):
|
|
||||||
# If an wwn name crypt device exists, dev_path should be based on wwn.
|
|
||||||
old_dev_name = self.dev_path.split('/')[-1]
|
|
||||||
wwn = 'fake_wwn'
|
|
||||||
connection_info = copy.deepcopy(self.connection_info)
|
|
||||||
connection_info['data']['multipath_id'] = wwn
|
|
||||||
encryptor = cryptsetup.CryptsetupEncryptor(
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
connection_info=connection_info,
|
|
||||||
keymgr=fake.fake_api(),
|
|
||||||
execute=mock_execute)
|
|
||||||
self.assertFalse(encryptor.dev_name.startswith('crypt-'))
|
|
||||||
self.assertEqual(wwn, encryptor.dev_name)
|
|
||||||
self.assertEqual(self.dev_path, encryptor.dev_path)
|
|
||||||
self.assertEqual(self.symlink_path, encryptor.symlink_path)
|
|
||||||
mock_exists.assert_has_calls([
|
|
||||||
mock.call('/dev/mapper/%s' % old_dev_name),
|
|
||||||
mock.call('/dev/mapper/%s' % wwn)])
|
|
||||||
mock_execute.assert_called_once_with(
|
|
||||||
'cryptsetup', 'status', wwn, run_as_root=True)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_attach_volume_unmangle_passphrase(self, mock_execute):
|
|
||||||
fake_key = '0725230b'
|
|
||||||
fake_key_mangled = '72523b'
|
|
||||||
self.encryptor._get_key = mock.MagicMock()
|
|
||||||
self.encryptor._get_key.return_value = fake__get_key(None, fake_key)
|
|
||||||
|
|
||||||
mock_execute.side_effect = [
|
|
||||||
putils.ProcessExecutionError(exit_code=2), # luksOpen
|
|
||||||
mock.DEFAULT,
|
|
||||||
mock.DEFAULT,
|
|
||||||
]
|
|
||||||
|
|
||||||
self.encryptor.attach_volume(None)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
|
|
||||||
self.dev_path, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'create', '--key-file=-', self.dev_name,
|
|
||||||
self.dev_path, process_input=fake_key_mangled,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('ln', '--symbolic', '--force',
|
|
||||||
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
])
|
|
||||||
self.assertEqual(3, mock_execute.call_count)
|
|
|
@ -1,254 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
|
|
||||||
from castellan.common.objects import symmetric_key as key
|
|
||||||
from os_brick.encryptors import luks
|
|
||||||
from os_brick.tests.encryptors import test_cryptsetup
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
|
|
||||||
|
|
||||||
class LuksEncryptorTestCase(test_cryptsetup.CryptsetupEncryptorTestCase):
|
|
||||||
def _create(self):
|
|
||||||
return luks.LuksEncryptor(root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_is_luks(self, mock_execute):
|
|
||||||
luks.is_luks(self.root_helper, self.dev_path, execute=mock_execute)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
|
|
||||||
run_as_root=True, root_helper=self.root_helper,
|
|
||||||
check_exit_code=True),
|
|
||||||
], any_order=False)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
@mock.patch('os_brick.encryptors.luks.LOG')
|
|
||||||
def test_is_luks_with_error(self, mock_log, mock_execute):
|
|
||||||
error_msg = "Device %s is not a valid LUKS device." % self.dev_path
|
|
||||||
mock_execute.side_effect = putils.ProcessExecutionError(
|
|
||||||
exit_code=1, stderr=error_msg)
|
|
||||||
|
|
||||||
luks.is_luks(self.root_helper, self.dev_path, execute=mock_execute)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
|
|
||||||
run_as_root=True, root_helper=self.root_helper,
|
|
||||||
check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
self.assertEqual(1, mock_log.warning.call_count) # warning logged
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test__format_volume(self, mock_execute):
|
|
||||||
self.encryptor._format_volume("passphrase")
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', '--batch-mode', 'luksFormat',
|
|
||||||
'--key-file=-', self.dev_path,
|
|
||||||
process_input='passphrase',
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True, attempts=3),
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test__open_volume(self, mock_execute):
|
|
||||||
self.encryptor._open_volume("passphrase")
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input='passphrase',
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_attach_volume(self, mock_execute):
|
|
||||||
fake_key = '0c84146034e747639b698368807286df'
|
|
||||||
self.encryptor._get_key = mock.MagicMock()
|
|
||||||
self.encryptor._get_key.return_value = (
|
|
||||||
test_cryptsetup.fake__get_key(None, fake_key))
|
|
||||||
|
|
||||||
self.encryptor.attach_volume(None)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
mock.call('ln', '--symbolic', '--force',
|
|
||||||
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_attach_volume_not_formatted(self, mock_execute):
|
|
||||||
fake_key = 'bc37c5eccebe403f9cc2d0dd20dac2bc'
|
|
||||||
self.encryptor._get_key = mock.MagicMock()
|
|
||||||
self.encryptor._get_key.return_value = (
|
|
||||||
test_cryptsetup.fake__get_key(None, fake_key))
|
|
||||||
|
|
||||||
mock_execute.side_effect = [
|
|
||||||
putils.ProcessExecutionError(exit_code=1), # luksOpen
|
|
||||||
putils.ProcessExecutionError(exit_code=1), # isLuks
|
|
||||||
mock.DEFAULT, # luksFormat
|
|
||||||
mock.DEFAULT, # luksOpen
|
|
||||||
mock.DEFAULT, # ln
|
|
||||||
]
|
|
||||||
|
|
||||||
self.encryptor.attach_volume(None)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', '--batch-mode', 'luksFormat',
|
|
||||||
'--key-file=-', self.dev_path, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True, attempts=3),
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
mock.call('ln', '--symbolic', '--force',
|
|
||||||
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
], any_order=False)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_attach_volume_fail(self, mock_execute):
|
|
||||||
fake_key = 'ea6c2e1b8f7f4f84ae3560116d659ba2'
|
|
||||||
self.encryptor._get_key = mock.MagicMock()
|
|
||||||
self.encryptor._get_key.return_value = (
|
|
||||||
test_cryptsetup.fake__get_key(None, fake_key))
|
|
||||||
|
|
||||||
mock_execute.side_effect = [
|
|
||||||
putils.ProcessExecutionError(exit_code=1), # luksOpen
|
|
||||||
mock.DEFAULT, # isLuks
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertRaises(putils.ProcessExecutionError,
|
|
||||||
self.encryptor.attach_volume, None)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'isLuks', '--verbose', self.dev_path,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
run_as_root=True, check_exit_code=True),
|
|
||||||
], any_order=False)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test__close_volume(self, mock_execute):
|
|
||||||
self.encryptor.detach_volume()
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'luksClose', self.dev_name,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
attempts=3, run_as_root=True, check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_detach_volume(self, mock_execute):
|
|
||||||
self.encryptor.detach_volume()
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'luksClose', self.dev_name,
|
|
||||||
root_helper=self.root_helper,
|
|
||||||
attempts=3, run_as_root=True, check_exit_code=True),
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_get_mangled_passphrase(self):
|
|
||||||
# Confirm that a mangled passphrase is provided as per bug#1633518
|
|
||||||
unmangled_raw_key = bytes(binascii.unhexlify('0725230b'))
|
|
||||||
symmetric_key = key.SymmetricKey('AES', len(unmangled_raw_key) * 8,
|
|
||||||
unmangled_raw_key)
|
|
||||||
unmangled_encoded_key = symmetric_key.get_encoded()
|
|
||||||
self.assertEqual(self.encryptor._get_mangled_passphrase(
|
|
||||||
unmangled_encoded_key), '72523b')
|
|
||||||
|
|
||||||
@mock.patch('os_brick.executor.Executor._execute')
|
|
||||||
def test_attach_volume_unmangle_passphrase(self, mock_execute):
|
|
||||||
fake_key = '0725230b'
|
|
||||||
fake_key_mangled = '72523b'
|
|
||||||
self.encryptor._get_key = mock.MagicMock()
|
|
||||||
self.encryptor._get_key.return_value = \
|
|
||||||
test_cryptsetup.fake__get_key(None, fake_key)
|
|
||||||
|
|
||||||
mock_execute.side_effect = [
|
|
||||||
putils.ProcessExecutionError(exit_code=2), # luksOpen
|
|
||||||
mock.DEFAULT, # luksOpen
|
|
||||||
mock.DEFAULT, # luksClose
|
|
||||||
mock.DEFAULT, # luksAddKey
|
|
||||||
mock.DEFAULT, # luksOpen
|
|
||||||
mock.DEFAULT, # luksClose
|
|
||||||
mock.DEFAULT, # luksRemoveKey
|
|
||||||
mock.DEFAULT, # luksOpen
|
|
||||||
mock.DEFAULT, # ln
|
|
||||||
]
|
|
||||||
|
|
||||||
self.encryptor.attach_volume(None)
|
|
||||||
|
|
||||||
mock_execute.assert_has_calls([
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key_mangled,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'luksClose', self.dev_name,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True, attempts=3),
|
|
||||||
mock.call('cryptsetup', 'luksAddKey', self.dev_path,
|
|
||||||
process_input=''.join([fake_key_mangled,
|
|
||||||
'\n', fake_key,
|
|
||||||
'\n', fake_key]),
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'luksClose', self.dev_name,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True, attempts=3),
|
|
||||||
mock.call('cryptsetup', 'luksRemoveKey', self.dev_path,
|
|
||||||
process_input=fake_key_mangled,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('cryptsetup', 'luksOpen', '--key-file=-', self.dev_path,
|
|
||||||
self.dev_name, process_input=fake_key,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
mock.call('ln', '--symbolic', '--force',
|
|
||||||
'/dev/mapper/%s' % self.dev_name, self.symlink_path,
|
|
||||||
root_helper=self.root_helper, run_as_root=True,
|
|
||||||
check_exit_code=True),
|
|
||||||
], any_order=False)
|
|
||||||
self.assertEqual(9, mock_execute.call_count)
|
|
|
@ -1,30 +0,0 @@
|
||||||
# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# 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 os_brick.encryptors import nop
|
|
||||||
from os_brick.tests.encryptors import test_base
|
|
||||||
|
|
||||||
|
|
||||||
class NoOpEncryptorTestCase(test_base.VolumeEncryptorTestCase):
|
|
||||||
def _create(self):
|
|
||||||
return nop.NoOpEncryptor(root_helper=self.root_helper,
|
|
||||||
connection_info=self.connection_info,
|
|
||||||
keymgr=self.keymgr)
|
|
||||||
|
|
||||||
def test_attach_volume(self):
|
|
||||||
self.encryptor.attach_volume(None)
|
|
||||||
|
|
||||||
def test_detach_volume(self):
|
|
||||||
self.encryptor.detach_volume()
|
|
|
@ -1,128 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
import os
|
|
||||||
|
|
||||||
from oslo_service import loopingcall
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.connectors import aoe
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class FakeFixedIntervalLoopingCall(object):
|
|
||||||
def __init__(self, f=None, *args, **kw):
|
|
||||||
self.args = args
|
|
||||||
self.kw = kw
|
|
||||||
self.f = f
|
|
||||||
self._stop = False
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._stop = True
|
|
||||||
|
|
||||||
def wait(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def start(self, interval, initial_delay=None):
|
|
||||||
while not self._stop:
|
|
||||||
try:
|
|
||||||
self.f(*self.args, **self.kw)
|
|
||||||
except loopingcall.LoopingCallDone:
|
|
||||||
return self
|
|
||||||
except Exception:
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
class AoEConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
"""Test cases for AoE initiator class."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(AoEConnectorTestCase, self).setUp()
|
|
||||||
self.connector = aoe.AoEConnector('sudo')
|
|
||||||
self.connection_properties = {'target_shelf': 'fake_shelf',
|
|
||||||
'target_lun': 'fake_lun'}
|
|
||||||
self.mock_object(loopingcall, 'FixedIntervalLoopingCall',
|
|
||||||
FakeFixedIntervalLoopingCall)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
expected = "/dev/etherd"
|
|
||||||
actual_path = self.connector.get_search_path()
|
|
||||||
self.assertEqual(expected, actual_path)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
def test_get_volume_paths(self, mock_exists):
|
|
||||||
expected = ["/dev/etherd/efake_shelf.fake_lun"]
|
|
||||||
paths = self.connector.get_volume_paths(self.connection_properties)
|
|
||||||
self.assertEqual(expected, paths)
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = aoe.AoEConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', side_effect=[True, True])
|
|
||||||
def test_connect_volume(self, exists_mock):
|
|
||||||
"""Ensure that if path exist aoe-revalidate was called."""
|
|
||||||
aoe_device, aoe_path = self.connector._get_aoe_info(
|
|
||||||
self.connection_properties)
|
|
||||||
with mock.patch.object(self.connector, '_execute',
|
|
||||||
return_value=["", ""]):
|
|
||||||
self.connector.connect_volume(self.connection_properties)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', side_effect=[False, True])
|
|
||||||
def test_connect_volume_without_path(self, exists_mock):
|
|
||||||
"""Ensure that if path doesn't exist aoe-discovery was called."""
|
|
||||||
|
|
||||||
aoe_device, aoe_path = self.connector._get_aoe_info(
|
|
||||||
self.connection_properties)
|
|
||||||
expected_info = {
|
|
||||||
'type': 'block',
|
|
||||||
'device': aoe_device,
|
|
||||||
'path': aoe_path,
|
|
||||||
}
|
|
||||||
|
|
||||||
with mock.patch.object(self.connector, '_execute',
|
|
||||||
return_value=["", ""]):
|
|
||||||
volume_info = self.connector.connect_volume(
|
|
||||||
self.connection_properties)
|
|
||||||
|
|
||||||
self.assertDictEqual(volume_info, expected_info)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=False)
|
|
||||||
def test_connect_volume_could_not_discover_path(self, exists_mock):
|
|
||||||
_aoe_device, aoe_path = self.connector._get_aoe_info(
|
|
||||||
self.connection_properties)
|
|
||||||
|
|
||||||
with mock.patch.object(self.connector, '_execute',
|
|
||||||
return_value=["", ""]):
|
|
||||||
self.assertRaises(exception.VolumeDeviceNotFound,
|
|
||||||
self.connector.connect_volume,
|
|
||||||
self.connection_properties)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
def test_disconnect_volume(self, mock_exists):
|
|
||||||
"""Ensure that if path exist aoe-revaliadte was called."""
|
|
||||||
aoe_device, aoe_path = self.connector._get_aoe_info(
|
|
||||||
self.connection_properties)
|
|
||||||
|
|
||||||
with mock.patch.object(self.connector, '_execute',
|
|
||||||
return_value=["", ""]):
|
|
||||||
self.connector.disconnect_volume(self.connection_properties, {})
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
self.connection_properties)
|
|
|
@ -1,77 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
|
|
||||||
from os_brick.initiator.connectors import base_iscsi
|
|
||||||
from os_brick.initiator.connectors import fake
|
|
||||||
from os_brick.tests import base as test_base
|
|
||||||
|
|
||||||
|
|
||||||
class BaseISCSIConnectorTestCase(test_base.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(BaseISCSIConnectorTestCase, self).setUp()
|
|
||||||
self.connector = fake.FakeBaseISCSIConnector(None)
|
|
||||||
|
|
||||||
@mock.patch.object(base_iscsi.BaseISCSIConnector, '_get_all_targets')
|
|
||||||
def test_iterate_all_targets(self, mock_get_all_targets):
|
|
||||||
# extra_property cannot be a sentinel, a copied sentinel will not
|
|
||||||
# identical to the original one.
|
|
||||||
connection_properties = {
|
|
||||||
'target_portals': mock.sentinel.target_portals,
|
|
||||||
'target_iqns': mock.sentinel.target_iqns,
|
|
||||||
'target_luns': mock.sentinel.target_luns,
|
|
||||||
'extra_property': 'extra_property'}
|
|
||||||
mock_get_all_targets.return_value = [(
|
|
||||||
mock.sentinel.portal, mock.sentinel.iqn, mock.sentinel.lun)]
|
|
||||||
|
|
||||||
# method is a generator, and it yields dictionaries. list() will
|
|
||||||
# iterate over all of the method's items.
|
|
||||||
list_props = list(
|
|
||||||
self.connector._iterate_all_targets(connection_properties))
|
|
||||||
|
|
||||||
mock_get_all_targets.assert_called_once_with(connection_properties)
|
|
||||||
self.assertEqual(1, len(list_props))
|
|
||||||
|
|
||||||
expected_props = {'target_portal': mock.sentinel.portal,
|
|
||||||
'target_iqn': mock.sentinel.iqn,
|
|
||||||
'target_lun': mock.sentinel.lun,
|
|
||||||
'extra_property': 'extra_property'}
|
|
||||||
self.assertEqual(expected_props, list_props[0])
|
|
||||||
|
|
||||||
def test_get_all_targets(self):
|
|
||||||
connection_properties = {
|
|
||||||
'target_portals': [mock.sentinel.target_portals],
|
|
||||||
'target_iqns': [mock.sentinel.target_iqns],
|
|
||||||
'target_luns': [mock.sentinel.target_luns]}
|
|
||||||
|
|
||||||
all_targets = self.connector._get_all_targets(connection_properties)
|
|
||||||
|
|
||||||
expected_targets = zip([mock.sentinel.target_portals],
|
|
||||||
[mock.sentinel.target_iqns],
|
|
||||||
[mock.sentinel.target_luns])
|
|
||||||
self.assertEqual(list(expected_targets), list(all_targets))
|
|
||||||
|
|
||||||
def test_get_all_targets_single_target(self):
|
|
||||||
connection_properties = {
|
|
||||||
'target_portal': mock.sentinel.target_portal,
|
|
||||||
'target_iqn': mock.sentinel.target_iqn,
|
|
||||||
'target_lun': mock.sentinel.target_lun}
|
|
||||||
|
|
||||||
all_targets = self.connector._get_all_targets(connection_properties)
|
|
||||||
|
|
||||||
expected_target = (mock.sentinel.target_portal,
|
|
||||||
mock.sentinel.target_iqn,
|
|
||||||
mock.sentinel.target_lun)
|
|
||||||
self.assertEqual([expected_target], all_targets)
|
|
|
@ -1,152 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 glob
|
|
||||||
import os
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.connectors import disco
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class DISCOConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
"""Test cases for DISCO connector."""
|
|
||||||
|
|
||||||
# Fake volume information
|
|
||||||
volume = {
|
|
||||||
'name': 'a-disco-volume',
|
|
||||||
'disco_id': '1234567'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Conf for test
|
|
||||||
conf = {
|
|
||||||
'ip': test_connector.MY_IP,
|
|
||||||
'port': 9898
|
|
||||||
}
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(DISCOConnectorTestCase, self).setUp()
|
|
||||||
|
|
||||||
self.fake_connection_properties = {
|
|
||||||
'name': self.volume['name'],
|
|
||||||
'disco_id': self.volume['disco_id'],
|
|
||||||
'conf': {
|
|
||||||
'server_ip': self.conf['ip'],
|
|
||||||
'server_port': self.conf['port']}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.fake_volume_status = {'attached': True,
|
|
||||||
'detached': False}
|
|
||||||
self.fake_request_status = {'success': None,
|
|
||||||
'fail': 'ERROR'}
|
|
||||||
self.volume_status = 'detached'
|
|
||||||
self.request_status = 'success'
|
|
||||||
|
|
||||||
# Patch the request and os calls to fake versions
|
|
||||||
self.mock_object(disco.DISCOConnector,
|
|
||||||
'_send_disco_vol_cmd',
|
|
||||||
self.perform_disco_request)
|
|
||||||
self.mock_object(os.path, 'exists', self.is_volume_attached)
|
|
||||||
self.mock_object(glob, 'glob', self.list_disco_volume)
|
|
||||||
|
|
||||||
# The actual DISCO connector
|
|
||||||
self.connector = disco.DISCOConnector(
|
|
||||||
'sudo', execute=self.fake_execute)
|
|
||||||
|
|
||||||
def perform_disco_request(self, *cmd, **kwargs):
|
|
||||||
"""Fake the socket call."""
|
|
||||||
return self.fake_request_status[self.request_status]
|
|
||||||
|
|
||||||
def is_volume_attached(self, *cmd, **kwargs):
|
|
||||||
"""Fake volume detection check."""
|
|
||||||
return self.fake_volume_status[self.volume_status]
|
|
||||||
|
|
||||||
def list_disco_volume(self, *cmd, **kwargs):
|
|
||||||
"""Fake the glob call."""
|
|
||||||
path_dir = self.connector.get_search_path()
|
|
||||||
volume_id = self.volume['disco_id']
|
|
||||||
volume_items = [path_dir, '/', self.connector.DISCO_PREFIX, volume_id]
|
|
||||||
volume_path = ''.join(volume_items)
|
|
||||||
return [volume_path]
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = disco.DISCOConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
"""DISCO volumes should be under /dev."""
|
|
||||||
expected = "/dev"
|
|
||||||
actual = self.connector.get_search_path()
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_get_volume_paths(self):
|
|
||||||
"""Test to get all the path for a specific volume."""
|
|
||||||
expected = ['/dev/dms1234567']
|
|
||||||
self.volume_status = 'attached'
|
|
||||||
actual = self.connector.get_volume_paths(
|
|
||||||
self.fake_connection_properties)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
"""Attach a volume."""
|
|
||||||
self.connector.connect_volume(self.fake_connection_properties)
|
|
||||||
|
|
||||||
def test_connect_volume_already_attached(self):
|
|
||||||
"""Make sure that we don't issue the request."""
|
|
||||||
self.request_status = 'fail'
|
|
||||||
self.volume_status = 'attached'
|
|
||||||
self.test_connect_volume()
|
|
||||||
|
|
||||||
def test_connect_volume_request_fail(self):
|
|
||||||
"""Fail the attach request."""
|
|
||||||
self.volume_status = 'detached'
|
|
||||||
self.request_status = 'fail'
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.test_connect_volume)
|
|
||||||
|
|
||||||
def test_disconnect_volume(self):
|
|
||||||
"""Detach a volume."""
|
|
||||||
self.connector.disconnect_volume(self.fake_connection_properties, None)
|
|
||||||
|
|
||||||
def test_disconnect_volume_attached(self):
|
|
||||||
"""Detach a volume attached."""
|
|
||||||
self.request_status = 'success'
|
|
||||||
self.volume_status = 'attached'
|
|
||||||
self.test_disconnect_volume()
|
|
||||||
|
|
||||||
def test_disconnect_volume_already_detached(self):
|
|
||||||
"""Ensure that we don't issue the request."""
|
|
||||||
self.request_status = 'fail'
|
|
||||||
self.volume_status = 'detached'
|
|
||||||
self.test_disconnect_volume()
|
|
||||||
|
|
||||||
def test_disconnect_volume_request_fail(self):
|
|
||||||
"""Fail the detach request."""
|
|
||||||
self.volume_status = 'attached'
|
|
||||||
self.request_status = 'fail'
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.test_disconnect_volume)
|
|
||||||
|
|
||||||
def test_get_all_available_volumes(self):
|
|
||||||
"""Test to get all the available DISCO volumes."""
|
|
||||||
expected = ['/dev/dms1234567']
|
|
||||||
actual = self.connector.get_all_available_volumes(None)
|
|
||||||
self.assertItemsEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
self.fake_connection_properties)
|
|
|
@ -1,89 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 os_brick.initiator.connectors import drbd
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class DRBDConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
|
|
||||||
RESOURCE_TEMPLATE = '''
|
|
||||||
resource r0 {
|
|
||||||
on host1 {
|
|
||||||
}
|
|
||||||
net {
|
|
||||||
shared-secret "%(shared-secret)s";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(DRBDConnectorTestCase, self).setUp()
|
|
||||||
|
|
||||||
self.connector = drbd.DRBDConnector(
|
|
||||||
None, execute=self._fake_exec)
|
|
||||||
|
|
||||||
self.execs = []
|
|
||||||
|
|
||||||
def _fake_exec(self, *cmd, **kwargs):
|
|
||||||
self.execs.append(cmd)
|
|
||||||
|
|
||||||
# out, err
|
|
||||||
return ('', '')
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = drbd.DRBDConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
"""Test connect_volume."""
|
|
||||||
|
|
||||||
cprop = {
|
|
||||||
'provider_auth': 'my-secret',
|
|
||||||
'config': self.RESOURCE_TEMPLATE,
|
|
||||||
'name': 'my-precious',
|
|
||||||
'device': '/dev/drbd951722',
|
|
||||||
'data': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
res = self.connector.connect_volume(cprop)
|
|
||||||
|
|
||||||
self.assertEqual(cprop['device'], res['path'])
|
|
||||||
self.assertEqual('adjust', self.execs[0][1])
|
|
||||||
self.assertEqual(cprop['name'], self.execs[0][4])
|
|
||||||
|
|
||||||
def test_disconnect_volume(self):
|
|
||||||
"""Test the disconnect volume case."""
|
|
||||||
|
|
||||||
cprop = {
|
|
||||||
'provider_auth': 'my-secret',
|
|
||||||
'config': self.RESOURCE_TEMPLATE,
|
|
||||||
'name': 'my-precious',
|
|
||||||
'device': '/dev/drbd951722',
|
|
||||||
'data': {},
|
|
||||||
}
|
|
||||||
dev_info = {}
|
|
||||||
|
|
||||||
self.connector.disconnect_volume(cprop, dev_info)
|
|
||||||
|
|
||||||
self.assertEqual('down', self.execs[0][1])
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
cprop = {'name': 'something'}
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
cprop)
|
|
|
@ -1,452 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
import os
|
|
||||||
import six
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.connectors import base
|
|
||||||
from os_brick.initiator.connectors import fibre_channel
|
|
||||||
from os_brick.initiator import linuxfc
|
|
||||||
from os_brick.initiator import linuxscsi
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(FibreChannelConnectorTestCase, self).setUp()
|
|
||||||
self.connector = fibre_channel.FibreChannelConnector(
|
|
||||||
None, execute=self.fake_execute, use_multipath=False)
|
|
||||||
self.assertIsNotNone(self.connector)
|
|
||||||
self.assertIsNotNone(self.connector._linuxfc)
|
|
||||||
self.assertIsNotNone(self.connector._linuxscsi)
|
|
||||||
|
|
||||||
def fake_get_fc_hbas(self):
|
|
||||||
return [{'ClassDevice': 'host1',
|
|
||||||
'ClassDevicePath': '/sys/devices/pci0000:00/0000:00:03.0'
|
|
||||||
'/0000:05:00.2/host1/fc_host/host1',
|
|
||||||
'dev_loss_tmo': '30',
|
|
||||||
'fabric_name': '0x1000000533f55566',
|
|
||||||
'issue_lip': '<store method only>',
|
|
||||||
'max_npiv_vports': '255',
|
|
||||||
'maxframe_size': '2048 bytes',
|
|
||||||
'node_name': '0x200010604b019419',
|
|
||||||
'npiv_vports_inuse': '0',
|
|
||||||
'port_id': '0x680409',
|
|
||||||
'port_name': '0x100010604b019419',
|
|
||||||
'port_state': 'Online',
|
|
||||||
'port_type': 'NPort (fabric via point-to-point)',
|
|
||||||
'speed': '10 Gbit',
|
|
||||||
'supported_classes': 'Class 3',
|
|
||||||
'supported_speeds': '10 Gbit',
|
|
||||||
'symbolic_name': 'Emulex 554M FV4.0.493.0 DV8.3.27',
|
|
||||||
'tgtid_bind_type': 'wwpn (World Wide Port Name)',
|
|
||||||
'uevent': None,
|
|
||||||
'vport_create': '<store method only>',
|
|
||||||
'vport_delete': '<store method only>'}]
|
|
||||||
|
|
||||||
def fake_get_fc_hbas_info(self):
|
|
||||||
hbas = self.fake_get_fc_hbas()
|
|
||||||
info = [{'port_name': hbas[0]['port_name'].replace('0x', ''),
|
|
||||||
'node_name': hbas[0]['node_name'].replace('0x', ''),
|
|
||||||
'host_device': hbas[0]['ClassDevice'],
|
|
||||||
'device_path': hbas[0]['ClassDevicePath']}]
|
|
||||||
return info
|
|
||||||
|
|
||||||
def fibrechan_connection(self, volume, location, wwn):
|
|
||||||
return {'driver_volume_type': 'fibrechan',
|
|
||||||
'data': {
|
|
||||||
'volume_id': volume['id'],
|
|
||||||
'target_portal': location,
|
|
||||||
'target_wwn': wwn,
|
|
||||||
'target_lun': 1,
|
|
||||||
}}
|
|
||||||
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
def test_get_connector_properties(self, mock_hbas):
|
|
||||||
mock_hbas.return_value = self.fake_get_fc_hbas()
|
|
||||||
multipath = True
|
|
||||||
enforce_multipath = True
|
|
||||||
props = fibre_channel.FibreChannelConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=multipath,
|
|
||||||
enforce_multipath=enforce_multipath)
|
|
||||||
|
|
||||||
hbas = self.fake_get_fc_hbas()
|
|
||||||
expected_props = {'wwpns': [hbas[0]['port_name'].replace('0x', '')],
|
|
||||||
'wwnns': [hbas[0]['node_name'].replace('0x', '')]}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
search_path = self.connector.get_search_path()
|
|
||||||
expected = "/dev/disk/by-path"
|
|
||||||
self.assertEqual(expected, search_path)
|
|
||||||
|
|
||||||
def test_get_pci_num(self):
|
|
||||||
hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0"
|
|
||||||
"/0000:05:00.3/host2/fc_host/host2"}
|
|
||||||
pci_num = self.connector._get_pci_num(hba)
|
|
||||||
self.assertEqual("0000:05:00.3", pci_num)
|
|
||||||
|
|
||||||
hba = {'device_path': "/sys/devices/pci0000:00/0000:00:03.0"
|
|
||||||
"/0000:05:00.3/0000:06:00.6/host2/fc_host/host2"}
|
|
||||||
pci_num = self.connector._get_pci_num(hba)
|
|
||||||
self.assertEqual("0000:06:00.6", pci_num)
|
|
||||||
|
|
||||||
hba = {'device_path': "/sys/devices/pci0000:20/0000:20:03.0"
|
|
||||||
"/0000:21:00.2/net/ens2f2/ctlr_2/host3"
|
|
||||||
"/fc_host/host3"}
|
|
||||||
pci_num = self.connector._get_pci_num(hba)
|
|
||||||
self.assertEqual("0000:21:00.2", pci_num)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
|
||||||
def test_get_volume_paths(self, fake_fc_hbas_info,
|
|
||||||
fake_fc_hbas, fake_exists):
|
|
||||||
fake_fc_hbas.side_effect = self.fake_get_fc_hbas
|
|
||||||
fake_fc_hbas_info.side_effect = self.fake_get_fc_hbas_info
|
|
||||||
|
|
||||||
name = 'volume-00000001'
|
|
||||||
vol = {'id': 1, 'name': name}
|
|
||||||
location = '10.0.2.15:3260'
|
|
||||||
wwn = '1234567890123456'
|
|
||||||
connection_info = self.fibrechan_connection(vol, location, wwn)
|
|
||||||
volume_paths = self.connector.get_volume_paths(
|
|
||||||
connection_info['data'])
|
|
||||||
|
|
||||||
expected = ['/dev/disk/by-path/pci-0000:05:00.2'
|
|
||||||
'-fc-0x1234567890123456-lun-1']
|
|
||||||
self.assertEqual(expected, volume_paths)
|
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_scsi_device')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
|
||||||
@mock.patch.object(base.BaseLinuxConnector, 'check_valid_device')
|
|
||||||
def test_connect_volume(self, check_valid_device_mock,
|
|
||||||
get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
remove_device_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock):
|
|
||||||
check_valid_device_mock.return_value = True
|
|
||||||
get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas
|
|
||||||
get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info
|
|
||||||
|
|
||||||
wwn = '1234567890'
|
|
||||||
multipath_devname = '/dev/md-1'
|
|
||||||
devices = {"device": multipath_devname,
|
|
||||||
"id": wwn,
|
|
||||||
"devices": [{'device': '/dev/sdb',
|
|
||||||
'address': '1:0:0:1',
|
|
||||||
'host': 1, 'channel': 0,
|
|
||||||
'id': 0, 'lun': 1}]}
|
|
||||||
get_device_info_mock.return_value = devices['devices'][0]
|
|
||||||
get_scsi_wwn_mock.return_value = wwn
|
|
||||||
|
|
||||||
location = '10.0.2.15:3260'
|
|
||||||
name = 'volume-00000001'
|
|
||||||
vol = {'id': 1, 'name': name}
|
|
||||||
# Should work for string, unicode, and list
|
|
||||||
wwns = ['1234567890123456', six.text_type('1234567890123456'),
|
|
||||||
['1234567890123456', '1234567890123457']]
|
|
||||||
for wwn in wwns:
|
|
||||||
connection_info = self.fibrechan_connection(vol, location, wwn)
|
|
||||||
dev_info = self.connector.connect_volume(connection_info['data'])
|
|
||||||
exp_wwn = wwn[0] if isinstance(wwn, list) else wwn
|
|
||||||
dev_str = ('/dev/disk/by-path/pci-0000:05:00.2-fc-0x%s-lun-1' %
|
|
||||||
exp_wwn)
|
|
||||||
self.assertEqual(dev_info['type'], 'block')
|
|
||||||
self.assertEqual(dev_info['path'], dev_str)
|
|
||||||
self.assertNotIn('multipath_id', dev_info)
|
|
||||||
self.assertNotIn('devices', dev_info)
|
|
||||||
|
|
||||||
self.connector.disconnect_volume(connection_info['data'], dev_info)
|
|
||||||
expected_commands = []
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
|
|
||||||
# Should not work for anything other than string, unicode, and list
|
|
||||||
connection_info = self.fibrechan_connection(vol, location, 123)
|
|
||||||
self.assertRaises(exception.NoFibreChannelHostsFound,
|
|
||||||
self.connector.connect_volume,
|
|
||||||
connection_info['data'])
|
|
||||||
|
|
||||||
get_fc_hbas_mock.side_effect = [[]]
|
|
||||||
get_fc_hbas_info_mock.side_effect = [[]]
|
|
||||||
self.assertRaises(exception.NoFibreChannelHostsFound,
|
|
||||||
self.connector.connect_volume,
|
|
||||||
connection_info['data'])
|
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
|
|
||||||
def _test_connect_volume_multipath(self, get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock,
|
|
||||||
access_mode,
|
|
||||||
should_wait_for_rw,
|
|
||||||
find_mp_device_path_mock):
|
|
||||||
self.connector.use_multipath = True
|
|
||||||
get_fc_hbas_mock.side_effect = self.fake_get_fc_hbas
|
|
||||||
get_fc_hbas_info_mock.side_effect = self.fake_get_fc_hbas_info
|
|
||||||
|
|
||||||
wwn = '1234567890'
|
|
||||||
multipath_devname = '/dev/md-1'
|
|
||||||
devices = {"device": multipath_devname,
|
|
||||||
"id": wwn,
|
|
||||||
"devices": [{'device': '/dev/sdb',
|
|
||||||
'address': '1:0:0:1',
|
|
||||||
'host': 1, 'channel': 0,
|
|
||||||
'id': 0, 'lun': 1},
|
|
||||||
{'device': '/dev/sdc',
|
|
||||||
'address': '1:0:0:2',
|
|
||||||
'host': 1, 'channel': 0,
|
|
||||||
'id': 0, 'lun': 1}]}
|
|
||||||
get_device_info_mock.side_effect = devices['devices']
|
|
||||||
get_scsi_wwn_mock.return_value = wwn
|
|
||||||
|
|
||||||
location = '10.0.2.15:3260'
|
|
||||||
name = 'volume-00000001'
|
|
||||||
vol = {'id': 1, 'name': name}
|
|
||||||
initiator_wwn = ['1234567890123456', '1234567890123457']
|
|
||||||
|
|
||||||
find_mp_device_path_mock.return_value = '/dev/mapper/mpatha'
|
|
||||||
find_mp_dev_mock.return_value = {"device": "dm-3",
|
|
||||||
"id": wwn,
|
|
||||||
"name": "mpatha"}
|
|
||||||
|
|
||||||
connection_info = self.fibrechan_connection(vol, location,
|
|
||||||
initiator_wwn)
|
|
||||||
connection_info['data']['access_mode'] = access_mode
|
|
||||||
|
|
||||||
self.connector.connect_volume(connection_info['data'])
|
|
||||||
|
|
||||||
self.assertEqual(should_wait_for_rw, wait_for_rw_mock.called)
|
|
||||||
|
|
||||||
self.connector.disconnect_volume(connection_info['data'],
|
|
||||||
devices['devices'][0])
|
|
||||||
expected_commands = [
|
|
||||||
'multipath -f ' + find_mp_device_path_mock.return_value,
|
|
||||||
'blockdev --flushbufs /dev/sdb',
|
|
||||||
'tee -a /sys/block/sdb/device/delete',
|
|
||||||
'blockdev --flushbufs /dev/sdc',
|
|
||||||
'tee -a /sys/block/sdc/device/delete',
|
|
||||||
]
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
return connection_info
|
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
|
||||||
@mock.patch.object(base.BaseLinuxConnector, 'check_valid_device')
|
|
||||||
def test_connect_volume_multipath_rw(self, check_valid_device_mock,
|
|
||||||
get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock):
|
|
||||||
|
|
||||||
check_valid_device_mock.return_value = True
|
|
||||||
self._test_connect_volume_multipath(get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock,
|
|
||||||
'rw',
|
|
||||||
True)
|
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
|
||||||
@mock.patch.object(base.BaseLinuxConnector, 'check_valid_device')
|
|
||||||
def test_connect_volume_multipath_no_access_mode(self,
|
|
||||||
check_valid_device_mock,
|
|
||||||
get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock):
|
|
||||||
|
|
||||||
check_valid_device_mock.return_value = True
|
|
||||||
self._test_connect_volume_multipath(get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock,
|
|
||||||
None,
|
|
||||||
True)
|
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
|
||||||
@mock.patch.object(base.BaseLinuxConnector, 'check_valid_device')
|
|
||||||
def test_connect_volume_multipath_ro(self, check_valid_device_mock,
|
|
||||||
get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock):
|
|
||||||
|
|
||||||
check_valid_device_mock.return_value = True
|
|
||||||
self._test_connect_volume_multipath(get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock,
|
|
||||||
'ro',
|
|
||||||
False)
|
|
||||||
|
|
||||||
@mock.patch.object(base.BaseLinuxConnector, '_discover_mpath_device')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
|
||||||
@mock.patch.object(base.BaseLinuxConnector, 'check_valid_device')
|
|
||||||
def test_connect_volume_multipath_not_found(self,
|
|
||||||
check_valid_device_mock,
|
|
||||||
get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock,
|
|
||||||
discover_mp_dev_mock):
|
|
||||||
check_valid_device_mock.return_value = True
|
|
||||||
discover_mp_dev_mock.return_value = ("/dev/disk/by-path/something",
|
|
||||||
None)
|
|
||||||
|
|
||||||
connection_info = self._test_connect_volume_multipath(
|
|
||||||
get_device_info_mock, get_scsi_wwn_mock, get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock, realpath_mock, exists_mock, wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock, 'rw', False)
|
|
||||||
|
|
||||||
self.assertNotIn('multipathd_id', connection_info['data'])
|
|
||||||
|
|
||||||
@mock.patch.object(fibre_channel.FibreChannelConnector, 'get_volume_paths')
|
|
||||||
def test_extend_volume_no_path(self, mock_volume_paths):
|
|
||||||
mock_volume_paths.return_value = []
|
|
||||||
volume = {'id': 'fake_uuid'}
|
|
||||||
wwn = '1234567890123456'
|
|
||||||
connection_info = self.fibrechan_connection(volume,
|
|
||||||
"10.0.2.15:3260",
|
|
||||||
wwn)
|
|
||||||
|
|
||||||
self.assertRaises(exception.VolumePathsNotFound,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
connection_info['data'])
|
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
|
|
||||||
@mock.patch.object(fibre_channel.FibreChannelConnector, 'get_volume_paths')
|
|
||||||
def test_extend_volume(self, mock_volume_paths, mock_scsi_extend):
|
|
||||||
fake_new_size = 1024
|
|
||||||
mock_volume_paths.return_value = ['/dev/vdx']
|
|
||||||
mock_scsi_extend.return_value = fake_new_size
|
|
||||||
volume = {'id': 'fake_uuid'}
|
|
||||||
wwn = '1234567890123456'
|
|
||||||
connection_info = self.fibrechan_connection(volume,
|
|
||||||
"10.0.2.15:3260",
|
|
||||||
wwn)
|
|
||||||
new_size = self.connector.extend_volume(connection_info['data'])
|
|
||||||
self.assertEqual(fake_new_size, new_size)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'isdir')
|
|
||||||
def test_get_all_available_volumes_path_not_dir(self, mock_isdir):
|
|
||||||
mock_isdir.return_value = False
|
|
||||||
expected = []
|
|
||||||
actual = self.connector.get_all_available_volumes()
|
|
||||||
self.assertItemsEqual(expected, actual)
|
|
||||||
|
|
||||||
@mock.patch('eventlet.greenthread.sleep', mock.Mock())
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'wait_for_rw')
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(os.path, 'realpath', return_value='/dev/sdb')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas_info')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_scsi_wwn')
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_device_info')
|
|
||||||
@mock.patch.object(base.BaseLinuxConnector, 'check_valid_device')
|
|
||||||
def test_connect_volume_device_not_valid(self, check_valid_device_mock,
|
|
||||||
get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock):
|
|
||||||
|
|
||||||
check_valid_device_mock.return_value = False
|
|
||||||
self.assertRaises(exception.NoFibreChannelVolumeDeviceFound,
|
|
||||||
self._test_connect_volume_multipath,
|
|
||||||
get_device_info_mock,
|
|
||||||
get_scsi_wwn_mock,
|
|
||||||
get_fc_hbas_info_mock,
|
|
||||||
get_fc_hbas_mock,
|
|
||||||
realpath_mock,
|
|
||||||
exists_mock,
|
|
||||||
wait_for_rw_mock,
|
|
||||||
find_mp_dev_mock,
|
|
||||||
'rw',
|
|
||||||
True)
|
|
|
@ -1,43 +0,0 @@
|
||||||
# (c) Copyright 2013 IBM Company
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
|
|
||||||
from os_brick.initiator.connectors import fibre_channel_ppc64
|
|
||||||
from os_brick.initiator import linuxscsi
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class FibreChannelConnectorPPC64TestCase(test_connector.ConnectorTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(FibreChannelConnectorPPC64TestCase, self).setUp()
|
|
||||||
self.connector = fibre_channel_ppc64.FibreChannelConnectorPPC64(
|
|
||||||
None, execute=self.fake_execute, use_multipath=False)
|
|
||||||
self.assertIsNotNone(self.connector)
|
|
||||||
self.assertIsNotNone(self.connector._linuxfc)
|
|
||||||
self.assertEqual(self.connector._linuxfc.__class__.__name__,
|
|
||||||
"LinuxFibreChannelPPC64")
|
|
||||||
self.assertIsNotNone(self.connector._linuxscsi)
|
|
||||||
|
|
||||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'process_lun_id', return_value='2')
|
|
||||||
def test_get_host_devices(self, mock_process_lun_id):
|
|
||||||
lun = 2
|
|
||||||
possible_devs = [(3, "0x5005076802232ade"),
|
|
||||||
(3, "0x5005076802332ade"), ]
|
|
||||||
devices = self.connector._get_host_devices(possible_devs, lun)
|
|
||||||
self.assertEqual(2, len(devices))
|
|
||||||
device_path = "/dev/disk/by-path/fc-0x5005076802232ade-lun-2"
|
|
||||||
self.assertEqual(devices[0], device_path)
|
|
||||||
device_path = "/dev/disk/by-path/fc-0x5005076802332ade-lun-2"
|
|
||||||
self.assertEqual(devices[1], device_path)
|
|
|
@ -1,73 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
|
|
||||||
from os_brick.initiator.connectors import fibre_channel_s390x
|
|
||||||
from os_brick.initiator import linuxfc
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class FibreChannelConnectorS390XTestCase(test_connector.ConnectorTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(FibreChannelConnectorS390XTestCase, self).setUp()
|
|
||||||
self.connector = fibre_channel_s390x.FibreChannelConnectorS390X(
|
|
||||||
None, execute=self.fake_execute, use_multipath=False)
|
|
||||||
self.assertIsNotNone(self.connector)
|
|
||||||
self.assertIsNotNone(self.connector._linuxfc)
|
|
||||||
self.assertEqual(self.connector._linuxfc.__class__.__name__,
|
|
||||||
"LinuxFibreChannelS390X")
|
|
||||||
self.assertIsNotNone(self.connector._linuxscsi)
|
|
||||||
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'configure_scsi_device')
|
|
||||||
def test_get_host_devices(self, mock_configure_scsi_device):
|
|
||||||
lun = 2
|
|
||||||
possible_devs = [(3, 5), ]
|
|
||||||
devices = self.connector._get_host_devices(possible_devs, lun)
|
|
||||||
mock_configure_scsi_device.assert_called_with(3, 5,
|
|
||||||
"0x0002000000000000")
|
|
||||||
self.assertEqual(2, len(devices))
|
|
||||||
device_path = "/dev/disk/by-path/ccw-3-zfcp-5:0x0002000000000000"
|
|
||||||
self.assertEqual(devices[0], device_path)
|
|
||||||
device_path = "/dev/disk/by-path/ccw-3-fc-5-lun-2"
|
|
||||||
self.assertEqual(devices[1], device_path)
|
|
||||||
|
|
||||||
def test_get_lun_string(self):
|
|
||||||
lun = 1
|
|
||||||
lunstring = self.connector._get_lun_string(lun)
|
|
||||||
self.assertEqual(lunstring, "0x0001000000000000")
|
|
||||||
lun = 0xff
|
|
||||||
lunstring = self.connector._get_lun_string(lun)
|
|
||||||
self.assertEqual(lunstring, "0x00ff000000000000")
|
|
||||||
lun = 0x101
|
|
||||||
lunstring = self.connector._get_lun_string(lun)
|
|
||||||
self.assertEqual(lunstring, "0x0101000000000000")
|
|
||||||
lun = 0x4020400a
|
|
||||||
lunstring = self.connector._get_lun_string(lun)
|
|
||||||
self.assertEqual(lunstring, "0x4020400a00000000")
|
|
||||||
|
|
||||||
@mock.patch.object(fibre_channel_s390x.FibreChannelConnectorS390X,
|
|
||||||
'_get_possible_devices', return_value=[(3, 5), ])
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannelS390X, 'get_fc_hbas_info',
|
|
||||||
return_value=[])
|
|
||||||
@mock.patch.object(linuxfc.LinuxFibreChannelS390X,
|
|
||||||
'deconfigure_scsi_device')
|
|
||||||
def test_remove_devices(self, mock_deconfigure_scsi_device,
|
|
||||||
mock_get_fc_hbas_info, mock_get_possible_devices):
|
|
||||||
connection_properties = {'target_wwn': 5, 'target_lun': 2}
|
|
||||||
self.connector._remove_devices(connection_properties, devices=None)
|
|
||||||
mock_deconfigure_scsi_device.assert_called_with(3, 5,
|
|
||||||
"0x0002000000000000")
|
|
||||||
mock_get_fc_hbas_info.assert_called_once_with()
|
|
||||||
mock_get_possible_devices.assert_called_once_with([], 5)
|
|
|
@ -1,36 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 os_brick.initiator.connectors import gpfs
|
|
||||||
from os_brick.tests.initiator.connectors import test_local
|
|
||||||
|
|
||||||
|
|
||||||
class GPFSConnectorTestCase(test_local.LocalConnectorTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(GPFSConnectorTestCase, self).setUp()
|
|
||||||
self.connection_properties = {'name': 'foo',
|
|
||||||
'device_path': '/tmp/bar'}
|
|
||||||
self.connector = gpfs.GPFSConnector(None)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
cprops = self.connection_properties
|
|
||||||
dev_info = self.connector.connect_volume(cprops)
|
|
||||||
self.assertEqual(dev_info['type'], 'gpfs')
|
|
||||||
self.assertEqual(dev_info['path'], cprops['device_path'])
|
|
||||||
|
|
||||||
def test_connect_volume_with_invalid_connection_data(self):
|
|
||||||
cprops = {}
|
|
||||||
self.assertRaises(ValueError,
|
|
||||||
self.connector.connect_volume, cprops)
|
|
|
@ -1,219 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
import os
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator import connector
|
|
||||||
from os_brick.initiator.connectors import hgst
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class HGSTConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
"""Test cases for HGST initiator class."""
|
|
||||||
|
|
||||||
IP_OUTPUT = """
|
|
||||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
|
|
||||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
|
||||||
inet 127.0.0.1/8 scope host lo
|
|
||||||
valid_lft forever preferred_lft forever
|
|
||||||
inet 169.254.169.254/32 scope link lo
|
|
||||||
valid_lft forever preferred_lft forever
|
|
||||||
inet6 ::1/128 scope host
|
|
||||||
valid_lft forever preferred_lft forever
|
|
||||||
2: em1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master
|
|
||||||
link/ether 00:25:90:d9:18:08 brd ff:ff:ff:ff:ff:ff
|
|
||||||
inet6 fe80::225:90ff:fed9:1808/64 scope link
|
|
||||||
valid_lft forever preferred_lft forever
|
|
||||||
3: em2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state
|
|
||||||
link/ether 00:25:90:d9:18:09 brd ff:ff:ff:ff:ff:ff
|
|
||||||
inet 192.168.0.23/24 brd 192.168.0.255 scope global em2
|
|
||||||
valid_lft forever preferred_lft forever
|
|
||||||
inet6 fe80::225:90ff:fed9:1809/64 scope link
|
|
||||||
valid_lft forever preferred_lft forever
|
|
||||||
"""
|
|
||||||
|
|
||||||
DOMAIN_OUTPUT = """localhost"""
|
|
||||||
|
|
||||||
DOMAIN_FAILED = """this.better.not.resolve.to.a.name.or.else"""
|
|
||||||
|
|
||||||
SET_APPHOST_OUTPUT = """
|
|
||||||
VLVM_SET_APPHOSTS0000000395
|
|
||||||
Request Succeeded
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HGSTConnectorTestCase, self).setUp()
|
|
||||||
self.connector = hgst.HGSTConnector(
|
|
||||||
None, execute=self._fake_exec)
|
|
||||||
self._fail_set_apphosts = False
|
|
||||||
self._fail_ip = False
|
|
||||||
self._fail_domain_list = False
|
|
||||||
|
|
||||||
def _fake_exec_set_apphosts(self, *cmd):
|
|
||||||
if self._fail_set_apphosts:
|
|
||||||
raise putils.ProcessExecutionError(None, None, 1)
|
|
||||||
else:
|
|
||||||
return self.SET_APPHOST_OUTPUT, ''
|
|
||||||
|
|
||||||
def _fake_exec_ip(self, *cmd):
|
|
||||||
if self._fail_ip:
|
|
||||||
# Remove localhost so there is no IP match
|
|
||||||
return self.IP_OUTPUT.replace("127.0.0.1", "x.x.x.x"), ''
|
|
||||||
else:
|
|
||||||
return self.IP_OUTPUT, ''
|
|
||||||
|
|
||||||
def _fake_exec_domain_list(self, *cmd):
|
|
||||||
if self._fail_domain_list:
|
|
||||||
return self.DOMAIN_FAILED, ''
|
|
||||||
else:
|
|
||||||
return self.DOMAIN_OUTPUT, ''
|
|
||||||
|
|
||||||
def _fake_exec(self, *cmd, **kwargs):
|
|
||||||
self.cmdline = " ".join(cmd)
|
|
||||||
if cmd[0] == "ip":
|
|
||||||
return self._fake_exec_ip(*cmd)
|
|
||||||
elif cmd[0] == "vgc-cluster":
|
|
||||||
if cmd[1] == "domain-list":
|
|
||||||
return self._fake_exec_domain_list(*cmd)
|
|
||||||
elif cmd[1] == "space-set-apphosts":
|
|
||||||
return self._fake_exec_set_apphosts(*cmd)
|
|
||||||
else:
|
|
||||||
return '', ''
|
|
||||||
|
|
||||||
def test_factory(self):
|
|
||||||
"""Can we instantiate a HGSTConnector of the right kind?"""
|
|
||||||
obj = connector.InitiatorConnector.factory('HGST', None, arch='x86_64')
|
|
||||||
self.assertEqual("HGSTConnector", obj.__class__.__name__)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
expected = "/dev"
|
|
||||||
actual = self.connector.get_search_path()
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
def test_get_volume_paths(self, mock_exists):
|
|
||||||
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'stor1'}
|
|
||||||
path = "/dev/%s" % cprops['name']
|
|
||||||
expected = [path]
|
|
||||||
actual = self.connector.get_volume_paths(cprops)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
"""Tests that a simple connection succeeds"""
|
|
||||||
self._fail_set_apphosts = False
|
|
||||||
self._fail_ip = False
|
|
||||||
self._fail_domain_list = False
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'stor1'}
|
|
||||||
dev_info = self.connector.connect_volume(cprops)
|
|
||||||
self.assertEqual('block', dev_info['type'])
|
|
||||||
self.assertEqual('space', dev_info['device'])
|
|
||||||
self.assertEqual('/dev/space', dev_info['path'])
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = hgst.HGSTConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_connect_volume_nohost_fail(self):
|
|
||||||
"""This host should not be found, connect should fail."""
|
|
||||||
self._fail_set_apphosts = False
|
|
||||||
self._fail_ip = True
|
|
||||||
self._fail_domain_list = False
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'stor1'}
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.connect_volume,
|
|
||||||
cprops)
|
|
||||||
|
|
||||||
def test_connect_volume_nospace_fail(self):
|
|
||||||
"""The space command will fail, exception to be thrown"""
|
|
||||||
self._fail_set_apphosts = True
|
|
||||||
self._fail_ip = False
|
|
||||||
self._fail_domain_list = False
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'stor1'}
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.connect_volume,
|
|
||||||
cprops)
|
|
||||||
|
|
||||||
def test_disconnect_volume(self):
|
|
||||||
"""Simple disconnection should pass and disconnect me"""
|
|
||||||
self._fail_set_apphosts = False
|
|
||||||
self._fail_ip = False
|
|
||||||
self._fail_domain_list = False
|
|
||||||
self._cmdline = ""
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'stor1'}
|
|
||||||
self.connector.disconnect_volume(cprops, None)
|
|
||||||
exp_cli = ("vgc-cluster space-set-apphosts -n space "
|
|
||||||
"-A localhost --action DELETE")
|
|
||||||
self.assertEqual(exp_cli, self.cmdline)
|
|
||||||
|
|
||||||
def test_disconnect_volume_nohost(self):
|
|
||||||
"""Should not run a setapphosts because localhost will"""
|
|
||||||
"""be the noremotehost"""
|
|
||||||
self._fail_set_apphosts = False
|
|
||||||
self._fail_ip = False
|
|
||||||
self._fail_domain_list = False
|
|
||||||
self._cmdline = ""
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'localhost'}
|
|
||||||
self.connector.disconnect_volume(cprops, None)
|
|
||||||
# The last command should be the IP listing, not set apphosts
|
|
||||||
exp_cli = ("ip addr list")
|
|
||||||
self.assertEqual(exp_cli, self.cmdline)
|
|
||||||
|
|
||||||
def test_disconnect_volume_fails(self):
|
|
||||||
"""The set-apphosts should fail, exception to be thrown"""
|
|
||||||
self._fail_set_apphosts = True
|
|
||||||
self._fail_ip = False
|
|
||||||
self._fail_domain_list = False
|
|
||||||
self._cmdline = ""
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'stor1'}
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.disconnect_volume,
|
|
||||||
cprops, None)
|
|
||||||
|
|
||||||
def test_bad_connection_properties(self):
|
|
||||||
"""Send in connection_properties missing required fields"""
|
|
||||||
# Invalid connection_properties
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.connect_volume,
|
|
||||||
None)
|
|
||||||
# Name required for connect_volume
|
|
||||||
cprops = {'noremovehost': 'stor1'}
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.connect_volume,
|
|
||||||
cprops)
|
|
||||||
# Invalid connection_properties
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.disconnect_volume,
|
|
||||||
None, None)
|
|
||||||
# Name and noremovehost needed for disconnect_volume
|
|
||||||
cprops = {'noremovehost': 'stor1'}
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.disconnect_volume,
|
|
||||||
cprops, None)
|
|
||||||
cprops = {'name': 'space'}
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector.disconnect_volume,
|
|
||||||
cprops, None)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
cprops = {'name': 'space', 'noremovehost': 'stor1'}
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
cprops)
|
|
|
@ -1,230 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.connectors import huawei
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class HuaweiStorHyperConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
"""Test cases for StorHyper initiator class."""
|
|
||||||
|
|
||||||
attached = False
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HuaweiStorHyperConnectorTestCase, self).setUp()
|
|
||||||
self.fake_sdscli_file = tempfile.mktemp()
|
|
||||||
self.addCleanup(os.remove, self.fake_sdscli_file)
|
|
||||||
newefile = open(self.fake_sdscli_file, 'w')
|
|
||||||
newefile.write('test')
|
|
||||||
newefile.close()
|
|
||||||
|
|
||||||
self.connector = huawei.HuaweiStorHyperConnector(
|
|
||||||
None, execute=self.fake_execute)
|
|
||||||
self.connector.cli_path = self.fake_sdscli_file
|
|
||||||
self.connector.iscliexist = True
|
|
||||||
|
|
||||||
self.connector_fail = huawei.HuaweiStorHyperConnector(
|
|
||||||
None, execute=self.fake_execute_fail)
|
|
||||||
self.connector_fail.cli_path = self.fake_sdscli_file
|
|
||||||
self.connector_fail.iscliexist = True
|
|
||||||
|
|
||||||
self.connector_nocli = huawei.HuaweiStorHyperConnector(
|
|
||||||
None, execute=self.fake_execute_fail)
|
|
||||||
self.connector_nocli.cli_path = self.fake_sdscli_file
|
|
||||||
self.connector_nocli.iscliexist = False
|
|
||||||
|
|
||||||
self.connection_properties = {
|
|
||||||
'access_mode': 'rw',
|
|
||||||
'qos_specs': None,
|
|
||||||
'volume_id': 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f'
|
|
||||||
}
|
|
||||||
|
|
||||||
self.device_info = {'type': 'block',
|
|
||||||
'path': '/dev/vdxxx'}
|
|
||||||
HuaweiStorHyperConnectorTestCase.attached = False
|
|
||||||
|
|
||||||
def fake_execute(self, *cmd, **kwargs):
|
|
||||||
method = cmd[2]
|
|
||||||
self.cmds.append(" ".join(cmd))
|
|
||||||
if 'attach' == method:
|
|
||||||
HuaweiStorHyperConnectorTestCase.attached = True
|
|
||||||
return 'ret_code=0', None
|
|
||||||
if 'querydev' == method:
|
|
||||||
if HuaweiStorHyperConnectorTestCase.attached:
|
|
||||||
return 'ret_code=0\ndev_addr=/dev/vdxxx', None
|
|
||||||
else:
|
|
||||||
return 'ret_code=1\ndev_addr=/dev/vdxxx', None
|
|
||||||
if 'detach' == method:
|
|
||||||
HuaweiStorHyperConnectorTestCase.attached = False
|
|
||||||
return 'ret_code=0', None
|
|
||||||
|
|
||||||
def fake_execute_fail(self, *cmd, **kwargs):
|
|
||||||
method = cmd[2]
|
|
||||||
self.cmds.append(" ".join(cmd))
|
|
||||||
if 'attach' == method:
|
|
||||||
HuaweiStorHyperConnectorTestCase.attached = False
|
|
||||||
return 'ret_code=330151401', None
|
|
||||||
if 'querydev' == method:
|
|
||||||
if HuaweiStorHyperConnectorTestCase.attached:
|
|
||||||
return 'ret_code=0\ndev_addr=/dev/vdxxx', None
|
|
||||||
else:
|
|
||||||
return 'ret_code=1\ndev_addr=/dev/vdxxx', None
|
|
||||||
if 'detach' == method:
|
|
||||||
HuaweiStorHyperConnectorTestCase.attached = True
|
|
||||||
return 'ret_code=330155007', None
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = huawei.HuaweiStorHyperConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
actual = self.connector.get_search_path()
|
|
||||||
self.assertIsNone(actual)
|
|
||||||
|
|
||||||
@mock.patch.object(huawei.HuaweiStorHyperConnector,
|
|
||||||
'_query_attached_volume')
|
|
||||||
def test_get_volume_paths(self, mock_query_attached):
|
|
||||||
path = self.device_info['path']
|
|
||||||
mock_query_attached.return_value = {'ret_code': 0,
|
|
||||||
'dev_addr': path}
|
|
||||||
|
|
||||||
expected = [path]
|
|
||||||
actual = self.connector.get_volume_paths(self.connection_properties)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
"""Test the basic connect volume case."""
|
|
||||||
|
|
||||||
retval = self.connector.connect_volume(self.connection_properties)
|
|
||||||
self.assertEqual(self.device_info, retval)
|
|
||||||
|
|
||||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c querydev'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
|
||||||
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
|
|
||||||
def test_disconnect_volume(self):
|
|
||||||
"""Test the basic disconnect volume case."""
|
|
||||||
self.connector.connect_volume(self.connection_properties)
|
|
||||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
|
||||||
self.connector.disconnect_volume(self.connection_properties,
|
|
||||||
self.device_info)
|
|
||||||
self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached)
|
|
||||||
|
|
||||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c querydev'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c detach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
|
||||||
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
|
|
||||||
def test_is_volume_connected(self):
|
|
||||||
"""Test if volume connected to host case."""
|
|
||||||
self.connector.connect_volume(self.connection_properties)
|
|
||||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
|
||||||
is_connected = self.connector.is_volume_connected(
|
|
||||||
'volume-b2911673-863c-4380-a5f2-e1729eecfe3f')
|
|
||||||
self.assertEqual(HuaweiStorHyperConnectorTestCase.attached,
|
|
||||||
is_connected)
|
|
||||||
self.connector.disconnect_volume(self.connection_properties,
|
|
||||||
self.device_info)
|
|
||||||
self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached)
|
|
||||||
is_connected = self.connector.is_volume_connected(
|
|
||||||
'volume-b2911673-863c-4380-a5f2-e1729eecfe3f')
|
|
||||||
self.assertEqual(HuaweiStorHyperConnectorTestCase.attached,
|
|
||||||
is_connected)
|
|
||||||
|
|
||||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c querydev'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c querydev'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c detach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c querydev'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
|
||||||
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
|
|
||||||
def test__analyze_output(self):
|
|
||||||
cliout = 'ret_code=0\ndev_addr=/dev/vdxxx\nret_desc="success"'
|
|
||||||
analyze_result = {'dev_addr': '/dev/vdxxx',
|
|
||||||
'ret_desc': '"success"',
|
|
||||||
'ret_code': '0'}
|
|
||||||
result = self.connector._analyze_output(cliout)
|
|
||||||
self.assertEqual(analyze_result, result)
|
|
||||||
|
|
||||||
def test_connect_volume_fail(self):
|
|
||||||
"""Test the fail connect volume case."""
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector_fail.connect_volume,
|
|
||||||
self.connection_properties)
|
|
||||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
|
|
||||||
def test_disconnect_volume_fail(self):
|
|
||||||
"""Test the fail disconnect volume case."""
|
|
||||||
self.connector.connect_volume(self.connection_properties)
|
|
||||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector_fail.disconnect_volume,
|
|
||||||
self.connection_properties,
|
|
||||||
self.device_info)
|
|
||||||
|
|
||||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c querydev'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c detach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
|
||||||
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
|
|
||||||
def test_connect_volume_nocli(self):
|
|
||||||
"""Test the fail connect volume case."""
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector_nocli.connect_volume,
|
|
||||||
self.connection_properties)
|
|
||||||
|
|
||||||
def test_disconnect_volume_nocli(self):
|
|
||||||
"""Test the fail disconnect volume case."""
|
|
||||||
self.connector.connect_volume(self.connection_properties)
|
|
||||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.connector_nocli.disconnect_volume,
|
|
||||||
self.connection_properties,
|
|
||||||
self.device_info)
|
|
||||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
|
||||||
self.fake_sdscli_file + ' -c querydev'
|
|
||||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
|
||||||
self.assertEqual(expected_commands, self.cmds)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
self.connection_properties)
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,58 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 os_brick.initiator.connectors import local
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class LocalConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(LocalConnectorTestCase, self).setUp()
|
|
||||||
self.connection_properties = {'name': 'foo',
|
|
||||||
'device_path': '/tmp/bar'}
|
|
||||||
self.connector = local.LocalConnector(None)
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = local.LocalConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
actual = self.connector.get_search_path()
|
|
||||||
self.assertIsNone(actual)
|
|
||||||
|
|
||||||
def test_get_volume_paths(self):
|
|
||||||
expected = [self.connection_properties['device_path']]
|
|
||||||
actual = self.connector.get_volume_paths(
|
|
||||||
self.connection_properties)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
cprops = self.connection_properties
|
|
||||||
dev_info = self.connector.connect_volume(cprops)
|
|
||||||
self.assertEqual(dev_info['type'], 'local')
|
|
||||||
self.assertEqual(dev_info['path'], cprops['device_path'])
|
|
||||||
|
|
||||||
def test_connect_volume_with_invalid_connection_data(self):
|
|
||||||
cprops = {}
|
|
||||||
self.assertRaises(ValueError,
|
|
||||||
self.connector.connect_volume, cprops)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
self.connection_properties)
|
|
|
@ -1,269 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 ddt
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.connectors import rbd
|
|
||||||
from os_brick.initiator import linuxrbd
|
|
||||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
from os_brick import utils
|
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
|
||||||
class RBDConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(RBDConnectorTestCase, self).setUp()
|
|
||||||
|
|
||||||
self.user = 'fake_user'
|
|
||||||
self.pool = 'fake_pool'
|
|
||||||
self.volume = 'fake_volume'
|
|
||||||
self.clustername = 'fake_ceph'
|
|
||||||
self.hosts = ['192.168.10.2']
|
|
||||||
self.ports = ['6789']
|
|
||||||
self.keyring = "[client.cinder]\n key = test\n"
|
|
||||||
|
|
||||||
self.connection_properties = {
|
|
||||||
'auth_username': self.user,
|
|
||||||
'name': '%s/%s' % (self.pool, self.volume),
|
|
||||||
'cluster_name': self.clustername,
|
|
||||||
'hosts': self.hosts,
|
|
||||||
'ports': self.ports,
|
|
||||||
'keyring': self.keyring,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
rbd_connector = rbd.RBDConnector(None)
|
|
||||||
path = rbd_connector.get_search_path()
|
|
||||||
self.assertIsNone(path)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rbd')
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rados')
|
|
||||||
def test_get_volume_paths(self, mock_rados, mock_rbd):
|
|
||||||
rbd_connector = rbd.RBDConnector(None)
|
|
||||||
expected = []
|
|
||||||
actual = rbd_connector.get_volume_paths(self.connection_properties)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = rbd.RBDConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {'do_local_attach': False}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rbd')
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rados')
|
|
||||||
@mock.patch.object(rbd.RBDConnector, '_create_ceph_conf')
|
|
||||||
@mock.patch('os.path.exists')
|
|
||||||
def test_connect_volume(self, mock_path, mock_conf, mock_rados, mock_rbd):
|
|
||||||
"""Test the connect volume case."""
|
|
||||||
rbd_connector = rbd.RBDConnector(None)
|
|
||||||
mock_path.return_value = False
|
|
||||||
mock_conf.return_value = "/tmp/fake_dir/fake_ceph.conf"
|
|
||||||
device_info = rbd_connector.connect_volume(self.connection_properties)
|
|
||||||
|
|
||||||
# Ensure rados is instantiated correctly
|
|
||||||
mock_rados.Rados.assert_called_once_with(
|
|
||||||
clustername=self.clustername,
|
|
||||||
rados_id=utils.convert_str(self.user),
|
|
||||||
conffile='/tmp/fake_dir/fake_ceph.conf')
|
|
||||||
|
|
||||||
# Ensure correct calls to connect to cluster
|
|
||||||
self.assertEqual(1, mock_rados.Rados.return_value.connect.call_count)
|
|
||||||
mock_rados.Rados.return_value.open_ioctx.assert_called_once_with(
|
|
||||||
utils.convert_str(self.pool))
|
|
||||||
|
|
||||||
# Ensure rbd image is instantiated correctly
|
|
||||||
mock_rbd.Image.assert_called_once_with(
|
|
||||||
mock_rados.Rados.return_value.open_ioctx.return_value,
|
|
||||||
utils.convert_str(self.volume), read_only=False,
|
|
||||||
snapshot=None)
|
|
||||||
|
|
||||||
# Ensure expected object is returned correctly
|
|
||||||
self.assertIsInstance(device_info['path'],
|
|
||||||
linuxrbd.RBDVolumeIOWrapper)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rbd')
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rados')
|
|
||||||
@mock.patch.object(rbd.RBDConnector, '_create_ceph_conf')
|
|
||||||
@mock.patch('os.path.exists')
|
|
||||||
def test_provided_keyring(self, mock_path, mock_conf, mock_rados,
|
|
||||||
mock_rbd):
|
|
||||||
conn = rbd.RBDConnector(None)
|
|
||||||
mock_path.return_value = False
|
|
||||||
mock_conf.return_value = "/tmp/fake_dir/fake_ceph.conf"
|
|
||||||
self.connection_properties['keyring'] = self.keyring
|
|
||||||
conn.connect_volume(self.connection_properties)
|
|
||||||
mock_conf.assert_called_once_with(self.hosts, self.ports,
|
|
||||||
self.clustername, self.user,
|
|
||||||
self.keyring)
|
|
||||||
|
|
||||||
def test_keyring_is_none(self):
|
|
||||||
conn = rbd.RBDConnector(None)
|
|
||||||
keyring = None
|
|
||||||
keyring_data = "[client.cinder]\n key = test\n"
|
|
||||||
mockopen = mock.mock_open(read_data=keyring_data)
|
|
||||||
mockopen.return_value.__exit__ = mock.Mock()
|
|
||||||
with mock.patch('os_brick.initiator.connectors.rbd.open', mockopen,
|
|
||||||
create=True):
|
|
||||||
self.assertEqual(
|
|
||||||
conn._check_or_get_keyring_contents(keyring, 'cluster',
|
|
||||||
'user'), keyring_data)
|
|
||||||
|
|
||||||
def test_keyring_raise_error(self):
|
|
||||||
conn = rbd.RBDConnector(None)
|
|
||||||
keyring = None
|
|
||||||
mockopen = mock.mock_open()
|
|
||||||
mockopen.return_value = ""
|
|
||||||
with mock.patch('os_brick.initiator.connectors.rbd.open', mockopen,
|
|
||||||
create=True) as mock_keyring_file:
|
|
||||||
mock_keyring_file.side_effect = IOError
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
conn._check_or_get_keyring_contents, keyring,
|
|
||||||
'cluster', 'user')
|
|
||||||
|
|
||||||
@ddt.data((['192.168.1.1', '192.168.1.2'],
|
|
||||||
['192.168.1.1', '192.168.1.2']),
|
|
||||||
(['3ffe:1900:4545:3:200:f8ff:fe21:67cf',
|
|
||||||
'fe80:0:0:0:200:f8ff:fe21:67cf'],
|
|
||||||
['[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
|
|
||||||
'[fe80:0:0:0:200:f8ff:fe21:67cf]']),
|
|
||||||
(['foobar', 'fizzbuzz'], ['foobar', 'fizzbuzz']),
|
|
||||||
(['192.168.1.1',
|
|
||||||
'3ffe:1900:4545:3:200:f8ff:fe21:67cf',
|
|
||||||
'hello, world!'],
|
|
||||||
['192.168.1.1',
|
|
||||||
'[3ffe:1900:4545:3:200:f8ff:fe21:67cf]',
|
|
||||||
'hello, world!']))
|
|
||||||
@ddt.unpack
|
|
||||||
def test_sanitize_mon_host(self, hosts_in, hosts_out):
|
|
||||||
conn = rbd.RBDConnector(None)
|
|
||||||
self.assertEqual(hosts_out, conn._sanitize_mon_hosts(hosts_in))
|
|
||||||
|
|
||||||
@mock.patch('os_brick.initiator.connectors.rbd.tempfile.mkstemp')
|
|
||||||
def test_create_ceph_conf(self, mock_mkstemp):
|
|
||||||
mockopen = mock.mock_open()
|
|
||||||
fd = mock.sentinel.fd
|
|
||||||
tmpfile = mock.sentinel.tmpfile
|
|
||||||
mock_mkstemp.return_value = (fd, tmpfile)
|
|
||||||
|
|
||||||
with mock.patch('os.fdopen', mockopen, create=True):
|
|
||||||
rbd_connector = rbd.RBDConnector(None)
|
|
||||||
conf_path = rbd_connector._create_ceph_conf(
|
|
||||||
self.hosts, self.ports, self.clustername, self.user,
|
|
||||||
self.keyring)
|
|
||||||
self.assertEqual(conf_path, tmpfile)
|
|
||||||
mock_mkstemp.assert_called_once_with(prefix='brickrbd_')
|
|
||||||
|
|
||||||
@mock.patch.object(priv_rootwrap, 'execute', return_value=None)
|
|
||||||
def test_connect_local_volume(self, mock_execute):
|
|
||||||
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
|
|
||||||
conn = {'name': 'pool/image',
|
|
||||||
'auth_username': 'fake_user',
|
|
||||||
'hosts': ['192.168.10.2'],
|
|
||||||
'ports': ['6789']}
|
|
||||||
device_info = rbd_connector.connect_volume(conn)
|
|
||||||
execute_call1 = mock.call('which', 'rbd')
|
|
||||||
cmd = ['rbd', 'map', 'image', '--pool', 'pool', '--id', 'fake_user',
|
|
||||||
'--mon_host', '192.168.10.2:6789']
|
|
||||||
execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True)
|
|
||||||
mock_execute.assert_has_calls([execute_call1, execute_call2])
|
|
||||||
expected_info = {'path': '/dev/rbd/pool/image',
|
|
||||||
'type': 'block'}
|
|
||||||
self.assertEqual(expected_info, device_info)
|
|
||||||
|
|
||||||
@mock.patch.object(priv_rootwrap, 'execute', return_value=None)
|
|
||||||
@mock.patch('os.path.exists')
|
|
||||||
@mock.patch('os.path.islink')
|
|
||||||
@mock.patch('os.path.realpath')
|
|
||||||
def test_connect_local_volume_dev_exist(self, mock_realpath, mock_islink,
|
|
||||||
mock_exists, mock_execute):
|
|
||||||
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
|
|
||||||
conn = {'name': 'pool/image',
|
|
||||||
'auth_username': 'fake_user',
|
|
||||||
'hosts': ['192.168.10.2'],
|
|
||||||
'ports': ['6789']}
|
|
||||||
mock_realpath.return_value = '/dev/rbd0'
|
|
||||||
mock_islink.return_value = True
|
|
||||||
mock_exists.return_value = True
|
|
||||||
device_info = rbd_connector.connect_volume(conn)
|
|
||||||
execute_call1 = mock.call('which', 'rbd')
|
|
||||||
cmd = ['rbd', 'map', 'image', '--pool', 'pool', '--id', 'fake_user',
|
|
||||||
'--mon_host', '192.168.10.2:6789']
|
|
||||||
execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True)
|
|
||||||
mock_execute.assert_has_calls([execute_call1])
|
|
||||||
self.assertFalse(execute_call2 in mock_execute.mock_calls)
|
|
||||||
expected_info = {'path': '/dev/rbd/pool/image',
|
|
||||||
'type': 'block'}
|
|
||||||
self.assertEqual(expected_info, device_info)
|
|
||||||
|
|
||||||
@mock.patch.object(priv_rootwrap, 'execute', return_value=None)
|
|
||||||
def test_connect_local_volume_without_mons(self, mock_execute):
|
|
||||||
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
|
|
||||||
conn = {'name': 'pool/image',
|
|
||||||
'auth_username': 'fake_user'}
|
|
||||||
device_info = rbd_connector.connect_volume(conn)
|
|
||||||
execute_call1 = mock.call('which', 'rbd')
|
|
||||||
cmd = ['rbd', 'map', 'image', '--pool', 'pool', '--id', 'fake_user']
|
|
||||||
execute_call2 = mock.call(*cmd, root_helper=None, run_as_root=True)
|
|
||||||
mock_execute.assert_has_calls([execute_call1, execute_call2])
|
|
||||||
expected_info = {'path': '/dev/rbd/pool/image',
|
|
||||||
'type': 'block'}
|
|
||||||
self.assertEqual(expected_info, device_info)
|
|
||||||
|
|
||||||
@mock.patch.object(priv_rootwrap, 'execute', return_value=None)
|
|
||||||
def test_connect_local_volume_without_auth(self, mock_execute):
|
|
||||||
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
|
|
||||||
conn = {'name': 'pool/image',
|
|
||||||
'hosts': ['192.168.10.2'],
|
|
||||||
'ports': ['6789']}
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
rbd_connector.connect_volume,
|
|
||||||
conn)
|
|
||||||
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rbd')
|
|
||||||
@mock.patch('os_brick.initiator.linuxrbd.rados')
|
|
||||||
@mock.patch.object(linuxrbd.RBDVolumeIOWrapper, 'close')
|
|
||||||
def test_disconnect_volume(self, volume_close, mock_rados, mock_rbd):
|
|
||||||
"""Test the disconnect volume case."""
|
|
||||||
rbd_connector = rbd.RBDConnector(None)
|
|
||||||
device_info = rbd_connector.connect_volume(self.connection_properties)
|
|
||||||
rbd_connector.disconnect_volume(
|
|
||||||
self.connection_properties, device_info)
|
|
||||||
|
|
||||||
self.assertEqual(1, volume_close.call_count)
|
|
||||||
|
|
||||||
@mock.patch.object(priv_rootwrap, 'execute', return_value=None)
|
|
||||||
def test_disconnect_local_volume(self, mock_execute):
|
|
||||||
rbd_connector = rbd.RBDConnector(None, do_local_attach=True)
|
|
||||||
conn = {'name': 'pool/image',
|
|
||||||
'auth_username': 'fake_user',
|
|
||||||
'hosts': ['192.168.10.2'],
|
|
||||||
'ports': ['6789']}
|
|
||||||
rbd_connector.disconnect_volume(conn, None)
|
|
||||||
|
|
||||||
dev_name = '/dev/rbd/pool/image'
|
|
||||||
cmd = ['rbd', 'unmap', dev_name, '--id', 'fake_user',
|
|
||||||
'--mon_host', '192.168.10.2:6789']
|
|
||||||
mock_execute.assert_called_once_with(*cmd, root_helper=None,
|
|
||||||
run_as_root=True)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
rbd_connector = rbd.RBDConnector(None)
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
rbd_connector.extend_volume,
|
|
||||||
self.connection_properties)
|
|
|
@ -1,77 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
|
|
||||||
from os_brick.initiator.connectors import remotefs
|
|
||||||
from os_brick.remotefs import remotefs as remotefs_client
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteFsConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
"""Test cases for Remote FS initiator class."""
|
|
||||||
TEST_DEV = '172.18.194.100:/var/nfs'
|
|
||||||
TEST_PATH = '/mnt/test/df0808229363aad55c27da50c38d6328'
|
|
||||||
TEST_BASE = '/mnt/test'
|
|
||||||
TEST_NAME = '9c592d52-ce47-4263-8c21-4ecf3c029cdb'
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(RemoteFsConnectorTestCase, self).setUp()
|
|
||||||
self.connection_properties = {
|
|
||||||
'export': self.TEST_DEV,
|
|
||||||
'name': self.TEST_NAME}
|
|
||||||
self.connector = remotefs.RemoteFsConnector(
|
|
||||||
'nfs', root_helper='sudo',
|
|
||||||
nfs_mount_point_base=self.TEST_BASE,
|
|
||||||
nfs_mount_options='vers=3')
|
|
||||||
|
|
||||||
@mock.patch('os_brick.remotefs.remotefs.ScalityRemoteFsClient')
|
|
||||||
def test_init_with_scality(self, mock_scality_remotefs_client):
|
|
||||||
remotefs.RemoteFsConnector('scality', root_helper='sudo')
|
|
||||||
self.assertEqual(1, mock_scality_remotefs_client.call_count)
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = remotefs.RemoteFsConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
expected = self.TEST_BASE
|
|
||||||
actual = self.connector.get_search_path()
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
@mock.patch.object(remotefs_client.RemoteFsClient, 'mount')
|
|
||||||
def test_get_volume_paths(self, mock_mount):
|
|
||||||
path = ("%(path)s/%(name)s" % {'path': self.TEST_PATH,
|
|
||||||
'name': self.TEST_NAME})
|
|
||||||
expected = [path]
|
|
||||||
actual = self.connector.get_volume_paths(self.connection_properties)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
@mock.patch.object(remotefs_client.RemoteFsClient, 'mount')
|
|
||||||
@mock.patch.object(remotefs_client.RemoteFsClient, 'get_mount_point',
|
|
||||||
return_value="something")
|
|
||||||
def test_connect_volume(self, mount_point_mock, mount_mock):
|
|
||||||
"""Test the basic connect volume case."""
|
|
||||||
self.connector.connect_volume(self.connection_properties)
|
|
||||||
|
|
||||||
def test_disconnect_volume(self):
|
|
||||||
"""Nothing should happen here -- make sure it doesn't blow up."""
|
|
||||||
self.connector.disconnect_volume(self.connection_properties, {})
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
self.connection_properties)
|
|
|
@ -1,273 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 json
|
|
||||||
import mock
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.connectors import scaleio
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class ScaleIOConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
"""Test cases for ScaleIO connector."""
|
|
||||||
|
|
||||||
# Fake volume information
|
|
||||||
vol = {
|
|
||||||
'id': 'vol1',
|
|
||||||
'name': 'test_volume',
|
|
||||||
'provider_id': 'vol1'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fake SDC GUID
|
|
||||||
fake_guid = 'FAKE_GUID'
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(ScaleIOConnectorTestCase, self).setUp()
|
|
||||||
|
|
||||||
self.fake_connection_properties = {
|
|
||||||
'hostIP': test_connector.MY_IP,
|
|
||||||
'serverIP': test_connector.MY_IP,
|
|
||||||
'scaleIO_volname': self.vol['name'],
|
|
||||||
'scaleIO_volume_id': self.vol['provider_id'],
|
|
||||||
'serverPort': 443,
|
|
||||||
'serverUsername': 'test',
|
|
||||||
'serverPassword': 'fake',
|
|
||||||
'serverToken': 'fake_token',
|
|
||||||
'iopsLimit': None,
|
|
||||||
'bandwidthLimit': None
|
|
||||||
}
|
|
||||||
|
|
||||||
# Formatting string for REST API calls
|
|
||||||
self.action_format = "instances/Volume::{}/action/{{}}".format(
|
|
||||||
self.vol['id'])
|
|
||||||
self.get_volume_api = 'types/Volume/instances/getByName::{}'.format(
|
|
||||||
self.vol['name'])
|
|
||||||
|
|
||||||
# Map of REST API calls to responses
|
|
||||||
self.mock_calls = {
|
|
||||||
self.get_volume_api:
|
|
||||||
self.MockHTTPSResponse(json.dumps(self.vol['id'])),
|
|
||||||
self.action_format.format('addMappedSdc'):
|
|
||||||
self.MockHTTPSResponse(''),
|
|
||||||
self.action_format.format('setMappedSdcLimits'):
|
|
||||||
self.MockHTTPSResponse(''),
|
|
||||||
self.action_format.format('removeMappedSdc'):
|
|
||||||
self.MockHTTPSResponse(''),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Default error REST response
|
|
||||||
self.error_404 = self.MockHTTPSResponse(content=dict(
|
|
||||||
errorCode=0,
|
|
||||||
message='HTTP 404',
|
|
||||||
), status_code=404)
|
|
||||||
|
|
||||||
# Patch the request and os calls to fake versions
|
|
||||||
self.mock_object(requests, 'get', self.handle_scaleio_request)
|
|
||||||
self.mock_object(requests, 'post', self.handle_scaleio_request)
|
|
||||||
self.mock_object(os.path, 'isdir', return_value=True)
|
|
||||||
self.mock_object(os, 'listdir',
|
|
||||||
return_value=["emc-vol-{}".format(self.vol['id'])])
|
|
||||||
|
|
||||||
# The actual ScaleIO connector
|
|
||||||
self.connector = scaleio.ScaleIOConnector(
|
|
||||||
'sudo', execute=self.fake_execute)
|
|
||||||
|
|
||||||
class MockHTTPSResponse(requests.Response):
|
|
||||||
"""Mock HTTP Response
|
|
||||||
|
|
||||||
Defines the https replies from the mocked calls to do_request()
|
|
||||||
"""
|
|
||||||
def __init__(self, content, status_code=200):
|
|
||||||
super(ScaleIOConnectorTestCase.MockHTTPSResponse,
|
|
||||||
self).__init__()
|
|
||||||
|
|
||||||
self._content = content
|
|
||||||
self.encoding = 'UTF-8'
|
|
||||||
self.status_code = status_code
|
|
||||||
|
|
||||||
def json(self, **kwargs):
|
|
||||||
if isinstance(self._content, six.string_types):
|
|
||||||
return super(ScaleIOConnectorTestCase.MockHTTPSResponse,
|
|
||||||
self).json(**kwargs)
|
|
||||||
|
|
||||||
return self._content
|
|
||||||
|
|
||||||
@property
|
|
||||||
def text(self):
|
|
||||||
if not isinstance(self._content, six.string_types):
|
|
||||||
return json.dumps(self._content)
|
|
||||||
|
|
||||||
self._content = self._content.encode('utf-8')
|
|
||||||
return super(ScaleIOConnectorTestCase.MockHTTPSResponse,
|
|
||||||
self).text
|
|
||||||
|
|
||||||
def fake_execute(self, *cmd, **kwargs):
|
|
||||||
"""Fakes the rootwrap call"""
|
|
||||||
return self.fake_guid, None
|
|
||||||
|
|
||||||
def fake_missing_execute(self, *cmd, **kwargs):
|
|
||||||
"""Error when trying to call rootwrap drv_cfg"""
|
|
||||||
raise putils.ProcessExecutionError("Test missing drv_cfg.")
|
|
||||||
|
|
||||||
def handle_scaleio_request(self, url, *args, **kwargs):
|
|
||||||
"""Fake REST server"""
|
|
||||||
api_call = url.split(':', 2)[2].split('/', 1)[1].replace('api/', '')
|
|
||||||
|
|
||||||
if 'setMappedSdcLimits' in api_call:
|
|
||||||
self.assertNotIn("iops_limit", kwargs['data'])
|
|
||||||
if "iopsLimit" not in kwargs['data']:
|
|
||||||
self.assertIn("bandwidthLimitInKbps",
|
|
||||||
kwargs['data'])
|
|
||||||
elif "bandwidthLimitInKbps" not in kwargs['data']:
|
|
||||||
self.assertIn("iopsLimit", kwargs['data'])
|
|
||||||
else:
|
|
||||||
self.assertIn("bandwidthLimitInKbps",
|
|
||||||
kwargs['data'])
|
|
||||||
self.assertIn("iopsLimit", kwargs['data'])
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self.mock_calls[api_call]
|
|
||||||
except KeyError:
|
|
||||||
return self.error_404
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
expected = "/dev/disk/by-id"
|
|
||||||
actual = self.connector.get_search_path()
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
||||||
@mock.patch.object(scaleio.ScaleIOConnector, '_wait_for_volume_path')
|
|
||||||
def test_get_volume_paths(self, mock_wait_for_path, mock_exists):
|
|
||||||
mock_wait_for_path.return_value = "emc-vol-vol1"
|
|
||||||
expected = ['/dev/disk/by-id/emc-vol-vol1']
|
|
||||||
actual = self.connector.get_volume_paths(
|
|
||||||
self.fake_connection_properties)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = scaleio.ScaleIOConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
"""Successful connect to volume"""
|
|
||||||
self.connector.connect_volume(self.fake_connection_properties)
|
|
||||||
|
|
||||||
def test_connect_with_bandwidth_limit(self):
|
|
||||||
"""Successful connect to volume with bandwidth limit"""
|
|
||||||
self.fake_connection_properties['bandwidthLimit'] = '500'
|
|
||||||
self.test_connect_volume()
|
|
||||||
|
|
||||||
def test_connect_with_iops_limit(self):
|
|
||||||
"""Successful connect to volume with iops limit"""
|
|
||||||
self.fake_connection_properties['iopsLimit'] = '80'
|
|
||||||
self.test_connect_volume()
|
|
||||||
|
|
||||||
def test_connect_with_iops_and_bandwidth_limits(self):
|
|
||||||
"""Successful connect with iops and bandwidth limits"""
|
|
||||||
self.fake_connection_properties['bandwidthLimit'] = '500'
|
|
||||||
self.fake_connection_properties['iopsLimit'] = '80'
|
|
||||||
self.test_connect_volume()
|
|
||||||
|
|
||||||
def test_disconnect_volume(self):
|
|
||||||
"""Successful disconnect from volume"""
|
|
||||||
self.connector.disconnect_volume(self.fake_connection_properties, None)
|
|
||||||
|
|
||||||
def test_error_id(self):
|
|
||||||
"""Fail to connect with bad volume name"""
|
|
||||||
self.fake_connection_properties['scaleIO_volume_id'] = 'bad_id'
|
|
||||||
self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse(
|
|
||||||
dict(errorCode='404', message='Test volume not found'), 404)
|
|
||||||
|
|
||||||
self.assertRaises(exception.BrickException, self.test_connect_volume)
|
|
||||||
|
|
||||||
def test_error_no_volume_id(self):
|
|
||||||
"""Faile to connect with no volume id"""
|
|
||||||
self.fake_connection_properties['scaleIO_volume_id'] = None
|
|
||||||
self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse(
|
|
||||||
'null', 200)
|
|
||||||
|
|
||||||
self.assertRaises(exception.BrickException, self.test_connect_volume)
|
|
||||||
|
|
||||||
def test_error_bad_login(self):
|
|
||||||
"""Fail to connect with bad authentication"""
|
|
||||||
self.mock_calls[self.get_volume_api] = self.MockHTTPSResponse(
|
|
||||||
'null', 401)
|
|
||||||
|
|
||||||
self.mock_calls['login'] = self.MockHTTPSResponse('null', 401)
|
|
||||||
self.mock_calls[self.action_format.format(
|
|
||||||
'addMappedSdc')] = self.MockHTTPSResponse(
|
|
||||||
dict(errorCode=401, message='bad login'), 401)
|
|
||||||
self.assertRaises(exception.BrickException, self.test_connect_volume)
|
|
||||||
|
|
||||||
def test_error_bad_drv_cfg(self):
|
|
||||||
"""Fail to connect with missing rootwrap executable"""
|
|
||||||
self.connector.set_execute(self.fake_missing_execute)
|
|
||||||
self.assertRaises(exception.BrickException, self.test_connect_volume)
|
|
||||||
|
|
||||||
def test_error_map_volume(self):
|
|
||||||
"""Fail to connect with REST API failure"""
|
|
||||||
self.mock_calls[self.action_format.format(
|
|
||||||
'addMappedSdc')] = self.MockHTTPSResponse(
|
|
||||||
dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR,
|
|
||||||
message='Test error map volume'), 500)
|
|
||||||
|
|
||||||
self.assertRaises(exception.BrickException, self.test_connect_volume)
|
|
||||||
|
|
||||||
@mock.patch('time.sleep')
|
|
||||||
def test_error_path_not_found(self, sleep_mock):
|
|
||||||
"""Timeout waiting for volume to map to local file system"""
|
|
||||||
self.mock_object(os, 'listdir', return_value=["emc-vol-no-volume"])
|
|
||||||
self.assertRaises(exception.BrickException, self.test_connect_volume)
|
|
||||||
self.assertTrue(sleep_mock.called)
|
|
||||||
|
|
||||||
def test_map_volume_already_mapped(self):
|
|
||||||
"""Ignore REST API failure for volume already mapped"""
|
|
||||||
self.mock_calls[self.action_format.format(
|
|
||||||
'addMappedSdc')] = self.MockHTTPSResponse(
|
|
||||||
dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR,
|
|
||||||
message='Test error map volume'), 500)
|
|
||||||
|
|
||||||
self.test_connect_volume()
|
|
||||||
|
|
||||||
def test_error_disconnect_volume(self):
|
|
||||||
"""Fail to disconnect with REST API failure"""
|
|
||||||
self.mock_calls[self.action_format.format(
|
|
||||||
'removeMappedSdc')] = self.MockHTTPSResponse(
|
|
||||||
dict(errorCode=self.connector.VOLUME_ALREADY_MAPPED_ERROR,
|
|
||||||
message='Test error map volume'), 500)
|
|
||||||
|
|
||||||
self.assertRaises(exception.BrickException,
|
|
||||||
self.test_disconnect_volume)
|
|
||||||
|
|
||||||
def test_disconnect_volume_not_mapped(self):
|
|
||||||
"""Ignore REST API failure for volume not mapped"""
|
|
||||||
self.mock_calls[self.action_format.format(
|
|
||||||
'removeMappedSdc')] = self.MockHTTPSResponse(
|
|
||||||
dict(errorCode=self.connector.VOLUME_NOT_MAPPED_ERROR,
|
|
||||||
message='Test error map volume'), 500)
|
|
||||||
|
|
||||||
self.test_disconnect_volume()
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
self.connector.extend_volume,
|
|
||||||
self.fake_connection_properties)
|
|
|
@ -1,87 +0,0 @@
|
||||||
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# 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 mock
|
|
||||||
|
|
||||||
from os_brick import exception
|
|
||||||
from os_brick.initiator.connectors import sheepdog
|
|
||||||
from os_brick.initiator import linuxsheepdog
|
|
||||||
from os_brick.tests.initiator import test_connector
|
|
||||||
|
|
||||||
|
|
||||||
class SheepdogConnectorTestCase(test_connector.ConnectorTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(SheepdogConnectorTestCase, self).setUp()
|
|
||||||
|
|
||||||
self.hosts = ['fake_hosts']
|
|
||||||
self.ports = ['fake_ports']
|
|
||||||
self.volume = 'fake_volume'
|
|
||||||
|
|
||||||
self.connection_properties = {
|
|
||||||
'hosts': self.hosts,
|
|
||||||
'name': self.volume,
|
|
||||||
'ports': self.ports,
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_connector_properties(self):
|
|
||||||
props = sheepdog.SheepdogConnector.get_connector_properties(
|
|
||||||
'sudo', multipath=True, enforce_multipath=True)
|
|
||||||
|
|
||||||
expected_props = {}
|
|
||||||
self.assertEqual(expected_props, props)
|
|
||||||
|
|
||||||
def test_get_search_path(self):
|
|
||||||
sd_connector = sheepdog.SheepdogConnector(None)
|
|
||||||
path = sd_connector.get_search_path()
|
|
||||||
self.assertIsNone(path)
|
|
||||||
|
|
||||||
def test_get_volume_paths(self):
|
|
||||||
sd_connector = sheepdog.SheepdogConnector(None)
|
|
||||||
expected = []
|
|
||||||
actual = sd_connector.get_volume_paths(self.connection_properties)
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_connect_volume(self):
|
|
||||||
"""Test the connect volume case."""
|
|
||||||
sd_connector = sheepdog.SheepdogConnector(None)
|
|
||||||
device_info = sd_connector.connect_volume(self.connection_properties)
|
|
||||||
|
|
||||||
# Ensure expected object is returned correctly
|
|
||||||
self.assertIsInstance(device_info['path'],
|
|
||||||
linuxsheepdog.SheepdogVolumeIOWrapper)
|
|
||||||
|
|
||||||
@mock.patch.object(linuxsheepdog.SheepdogVolumeIOWrapper, 'close')
|
|
||||||
def test_disconnect_volume(self, volume_close):
|
|
||||||
"""Test the disconnect volume case."""
|
|
||||||
sd_connector = sheepdog.SheepdogConnector(None)
|
|
||||||
device_info = sd_connector.connect_volume(self.connection_properties)
|
|
||||||
sd_connector.disconnect_volume(self.connection_properties, device_info)
|
|
||||||
|
|
||||||
self.assertEqual(1, volume_close.call_count)
|
|
||||||
|
|
||||||
def test_disconnect_volume_with_invalid_handle(self):
|
|
||||||
"""Test the disconnect volume case with invalid handle."""
|
|
||||||
sd_connector = sheepdog.SheepdogConnector(None)
|
|
||||||
device_info = {'path': 'fake_handle'}
|
|
||||||
self.assertRaises(exception.InvalidIOHandleObject,
|
|
||||||
sd_connector.disconnect_volume,
|
|
||||||
self.connection_properties,
|
|
||||||
device_info)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
sd_connector = sheepdog.SheepdogConnector(None)
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
sd_connector.extend_volume,
|
|
||||||
self.connection_properties)
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue