Add documentation

This patchs adds the first revision of the documentation.

There are still parts that are missing in sections like the internals
and the connection sections.
This commit is contained in:
Gorka Eguileor 2018-02-15 15:01:55 +01:00
parent d15a6d7063
commit b49f5d8918
17 changed files with 1406 additions and 72 deletions

View File

@ -20,6 +20,8 @@ Report bugs at https://github.com/akrog/cinderlib/issues.
If you are reporting a bug, please include:
* Your operating system name and version.
* Storage backend and configuration used (replacing sensitive information with
asterisks).
* Any details about your local setup that might be helpful in troubleshooting.
* Detailed steps to reproduce the bug.
@ -32,8 +34,16 @@ and "help wanted" is open to whoever wants to implement it.
Implement Features
~~~~~~~~~~~~~~~~~~
Look through the GitHub issues for features. Anything tagged with "enhancement"
and "help wanted" is open to whoever wants to implement it.
Look through the GitHub issues and the :doc:`todo` file for features. Anything
tagged with "enhancement" and "help wanted" is open to whoever wants to
implement it.
Write tests
~~~~~~~~~~~
We currently lack decent test coverage, so feel free to look into our existing
tests to add missing tests, because any test that increases our coverage is
more than welcome.
Write Documentation
~~~~~~~~~~~~~~~~~~~
@ -62,35 +72,89 @@ Ready to contribute? Here's how to set up `cinderlib` for local development.
1. Fork the `cinderlib` repo on GitHub.
2. Clone your fork locally::
$ git clone git@github.com:your_name_here/cinderlib.git
$ git clone git@github.com:YOUR_NAME_HERE/cinderlib.git
3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
3. Install tox::
$ mkvirtualenv cinderlib
$ cd cinderlib/
$ python setup.py develop
$ sudo dnf install python2-tox
4. Create a branch for local development::
4. Generate a virtual environment, for example for Python 2.7::
$ tox --notest -epy27
5. Create a branch for local development::
$ git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox::
6. When you're done making changes, you can check that your changes pass flake8
and the tests with::
$ tox -eflake8
$ tox -epy27
Or if you don't want to create a specific environment for flake8 you can run
things directly without tox::
$ source .tox/py27/bin/activate
$ flake8 cinderlib tests
$ python setup.py test or py.test
$ tox
$ python setup.py test
To get flake8 and tox, just pip install them into your virtualenv.
6. Commit your changes and push your branch to GitHub::
7. Commit your changes making sure the commit message is descriptive enough,
covering the patch changes as well as why the patch might be necessary. The
commit message should also conform to the `50/72 rule
<https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_.
$ git add .
$ git commit -m "Your detailed description of your changes."
$ git commit
8. Push your branch to GitHub::
$ git push origin name-of-your-bugfix-or-feature
7. Submit a pull request through the GitHub website.
9. Submit a pull request through the GitHub website.
LVM Backend
-----------
You may not have a fancy storage array, but that doesn't mean that you cannot
use *cinderlib*, because you can just use NFS or LVM. Here we are going to see
how to setup an LVM backend that we can use with *cinderlib*.
First you create your LVM, which is a 22GB file that is currently only using
1MB, and then you mount it as a loopback device and create a PV and VG on the
loopback device.
.. code-block:: shell
$ dd if=/dev/zero of=cinder-volumes bs=1048576 seek=22527 count=1
$ sudo lodevice=`losetup -f`
$ sudo losetup $lodevice ./cinder-volumes
$ sudo pvcreate $lodevice
$ sudo vgcreate cinder-volumes $lodevice
$ sudo vgscan --cache
Now you can use this LVM backend in *cinderlib*:
.. code-block:: python
import cinderlib
lvm = cl.Backend(volume_driver='cinder.volume.drivers.lvm.LVMVolumeDriver',
volume_group='cinder-volumes',
iscsi_protocol='iscsi',
iscsi_helper='lioadm',
volume_backend_name='lvm_iscsi')
vol = lvm.volume_create(size=1)
attach = vol.attach()
pp('Volume %s attached to %s' % (vol.id, attach.path))
vol.detach()
vol.delete()
Pull Request Guidelines
-----------------------
@ -101,14 +165,14 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
3. The pull request should work for Python 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check
https://travis-ci.org/akrog/cinderlib/pull_requests
and make sure that the tests pass for all supported Python versions.
3. The pull request should work for Python 2.7, 3.3, 3.4 and 3.5, and for PyPy.
Check https://travis-ci.org/akrog/cinderlib/pull_requests and make sure that
the tests pass for all supported Python versions.
Tips
----
To run a subset of tests::
$ python -m unittest tests.test_cinderlib
$ source .tox/py27/bin/activate
$ python -m unittest tests.test_cinderlib.TestCinderlib.test_lib_setup

View File

@ -1,8 +1,6 @@
Cinder Library
===============================
.. image:: https://img.shields.io/pypi/v/cinderlib.svg
:target: https://pypi.python.org/pypi/cinderlib
@ -26,43 +24,13 @@ Cinder.
* Free software: Apache Software License 2.0
* Documentation: https://cinderlib.readthedocs.io.
This library is currently at an Alpha status and is primarily intended as a
proof of concept at this stage. While some drivers have been manually
validated most drivers have not, so there's a good chance that they could
experience issues.
This library is currently in Alpha stage and is primarily intended as a proof
of concept at this stage. While some drivers have been manually validated most
drivers have not, so there's a good chance that they could experience issues.
When using this library one should be aware that this is in no way close to the
robustness or feature richness that the Cinder project provides. Some of the
more obvious limitations are:
* There are no argument validation on the methods so it's a classic GIGO_
library.
* The logic has been kept to a minimum and higher functioning logic is expected
to be in the caller. For example you can delete a volume that still has
snapshots, and the end results will depend on the Cinder driver and the
storage array, so you will have some that will delete the snapshots and
others that will leave them there.
* There is no CI, or unit tests for that matter, and certainly nothing so fancy
as third party vendor CIs, so things being broken could be considered the
norm.
* Only a subset number of basic operations are supported by the library.
The minimum version of Cinder required by this library is Pike; although,
depending on my my availability, I may make the library support Ocata as well.
Since it's using Cinder's code the library is still bound by the same
restrictions and behaviors of the drivers running under the standard Cinder
services, which means that not all operations will behave consistently across
drivers. For example you can find drivers where cloning is a cheap operation
performed by the storage array whereas other will actually create a new volume,
attach the source and the new volume and perform a full copy of the data.
If a driver in Cinder requires external libraries or packages they will also
be required by the library and will need to be manually installed.
For more detailed information please refer to the `official project
documentation`_ and `OpenStack's Cinder volume driver configuration
documentation`_.
robustness or feature richness that the Cinder project provides, for detailed
information on the current limitations please refer to the documentation.
Due to the limited access to Cinder backends and time constraints the list of
drivers that have been manually tested are (I'll try to test more):
@ -95,6 +63,17 @@ Features
- Local attach
- Local detach
- Validate connector
* Code should support multiple concurrent connections to a volume, though this
has not yet been tested.
Demo
----
.. raw:: html
<a href="https://asciinema.org/a/TcTR7Lu7jI0pEsd9ThEn01l7n?autoplay=1"
target="_blank"><img
src="https://asciinema.org/a/TcTR7Lu7jI0pEsd9ThEn01l7n.png"/></a>
Example
-------
@ -157,7 +136,9 @@ control LVM and do the attach) and execute:
exit()
Now we can check that the logical volume is there, exported, and attached to
our system::
our system:
.. code-block:: shell
# lvdisplay
# targetcli ls
@ -185,7 +166,15 @@ And now let's run a new `python` interpreter and clean things up:
# Finally delete the volume
vol.delete()
We should confirm that the logical volume is no longer there, there's nothing
exported or attached to our system:
.. code-block:: shell
# lvdisplay
# targetcli ls
# iscsiadm -m session
# lsblk
.. _GIGO: https://en.wikipedia.org/wiki/Garbage_in,_garbage_out
.. _official project documentation: https://readthedocs.org/projects/cinderlib/badge/?version=latest

33
TODO.rst Normal file
View File

@ -0,0 +1,33 @@
====
TODO
====
There are many things that need improvements in *cinderlib*, this is a simple
list to keep track of the most relevant topics.
- Connect & attach snapshot for drivers that support it.
- Replication and failover support
- QoS
- Support custom features via extra specs
- Unit tests
- Integration tests
- Parameter validation
- Support using *cinderlib* without cinder to just handle the attach/detach
- Add .py examples
- Support other Cinder releases besides Pike
- Add support for new Attach/Detach mechanism
- Consistency Groups
- Encryption
- Support name and description attributes in Volume and Snapshot
- Verify multiattach support
- Use created_at, updated_at, and deleted_at fields
- Use a list instead of a set in Volume.snapshots so they are ordered, which
can be useful to restore to the latest snapshot as well as to delete them in
reverse order of creation.
- Revert to snapshot support.
- Add documentation to connect remote host. `use_multipath_for_image_xfer` and
the `enforce_multipath_for_image_xfer` options.
- Complete internals documentation.
- Document the code.
- Should *cinderlib* support working with sqlite instead of just RAM?
- Improve serialization to limit following of references to other objects.

View File

@ -1,16 +1,35 @@
Welcome to Cinder Library's documentation!
======================================
==========================================
Contents:
.. image:: https://img.shields.io/pypi/v/cinderlib.svg
:target: https://pypi.python.org/pypi/cinderlib
.. image:: https://readthedocs.org/projects/cinderlib/badge/?version=latest
:target: https://cinderlib.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/pypi/pyversions/cinderlib.svg
:target: https://pypi.python.org/pypi/cinderlib
.. image:: https://img.shields.io/:license-apache-blue.svg
:target: http://www.apache.org/licenses/LICENSE-2.0
Cinder Library is a Python library that allows using Cinder storage drivers not
only outside of OpenStack but also outside of Cinder, which means there's no
need to run MySQL, RabbitMQ, Cinder API, Scheduler, or Volume services to be
able to manage your storage.
.. toctree::
:maxdepth: 2
readme
introduction
installation
usage
contributing
authorshistory
internals
authors
todo
history
Indices and tables
==================
@ -18,3 +37,5 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _GIGO: https://en.wikipedia.org/wiki/Garbage_in,_garbage_out

View File

@ -8,13 +8,53 @@ Installation
Stable release
--------------
To install Cinder Library, run this command in your terminal:
The Cinder Library is an interfacing library that doesn't have any storage
driver and expects Cinder drivers to be properly installed in the system to run
properly.
Drivers
_______
For Red Hat distributions the recommendation is to use RPMs to install the
Cinder drivers instead of using `pip`. If we don't have access to the
`Red Hat OpenStack Platform packages
<https://www.redhat.com/en/technologies/linux-platforms/openstack-platform>`_
we can use the `RDO community packages <https://www.rdoproject.org/>`_.
On CentOS, the Extras repository provides the RPM that enables the OpenStack
repository. Extras is enabled by default on CentOS 7, so you can simply install
the RPM to set up the OpenStack repository:
.. code-block:: console
$ pip install cinderlib
# yum install -y centos-release-openstack-pike
# yum-config-manager --enable openstack-pike
# yum update -y
# yum install -y openstack-cinder
This is the preferred method to install Cinder Library, as it will always install the most recent stable release.
On RHEL and Fedora, you'll need to download and install the RDO repository RPM
to set up the OpenStack repository:
.. code-block:: console
# yum install -y https://repos.fedorapeople.org/repos/openstack/openstack-pike/rdo-release-pike-1.noarch.rpm
# yum-config-manager --enable openstack-pike
# sudo yum update -y
# yum install -y openstack-cinder
Library
_______
To install Cinder Library we'll use PyPI, so we'll make sure to have the `pip`_
command available:
.. code-block:: console
# yum install -y python-pip
# pip install cinderlib
This is the preferred method to install Cinder Library, as it will always
install the most recent stable release.
If you don't have `pip`_ installed, this `Python installation guide`_ can guide
you through the process.
@ -22,11 +62,26 @@ you through the process.
.. _pip: https://pip.pypa.io
.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
From source
-----------
From sources
------------
Drivers
_______
The sources for Cinder Library can be downloaded from the `Github repo`_.
If we don't have a packaged version or if we want to use a virtual environment
we can install the drivers from source:
.. code-block:: console
$ virtualenv cinder
$ source cinder/bin/activate
$ pip install git+https://github.com/openstack/cinder.git@stable/pike
Library
_______
The sources for Cinder Library can be downloaded from the `Github repo`_ to use
the latest version of the library.
You can either clone the public repository:
@ -44,7 +99,7 @@ Once you have a copy of the source, you can install it with:
.. code-block:: console
$ python setup.py install
# python setup.py install
.. _Github repo: https://github.com/akrog/cinderlib

19
docs/internals.rst Normal file
View File

@ -0,0 +1,19 @@
=========
Internals
=========
Here we'll go over some of the implementation details within *cinderlib* as
well as explanations of how we've resolved the different issues that arise from
accessing the driver's directly from outside of the cinder-volume service.
Some of the issues *cinderlib* has had to resolve are:
- *Oslo config* configuration loading.
- Cinder-volume dynamic configuration loading.
- Privileged helper service.
- DLM configuration.
- Disabling of cinder logging.
- Direct DB access within drivers.
- *Oslo Versioned Objects* DB access methods such as `refresh` and `save`.
- Circular references in *Oslo Versioned Objects* for serialization.
- Using multiple drivers in the same process.

109
docs/introduction.rst Normal file
View File

@ -0,0 +1,109 @@
Cinder Library
==============
.. image:: https://img.shields.io/pypi/v/cinderlib.svg
:target: https://pypi.python.org/pypi/cinderlib
.. image:: https://readthedocs.org/projects/cinderlib/badge/?version=latest
:target: https://cinderlib.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/pypi/pyversions/cinderlib.svg
:target: https://pypi.python.org/pypi/cinderlib
.. image:: https://img.shields.io/:license-apache-blue.svg
:target: http://www.apache.org/licenses/LICENSE-2.0
Introduction
------------
Cinder Library is a Python library that allows using storage drivers provided
by Cinder outside of OpenStack and without needing to run the Cinder service,
so we don't need Keystone, MySQL, or RabbitMQ services to control our storage.
The library is currently in an early development stage and can be considered as
a proof of concept and not a finished product at this moment, so please
carefully go over the limitations section to avoid surprises.
Due to the limited access to Cinder backends and time constraints the list of
drivers that have been manually tested are:
- LVM
- XtremIO
- Kaminario
Features
--------
* Use a Cinder driver without running a DBMS, Message broker, or Cinder
services.
* Using multiple simultaneous drivers on the same program.
* Stateless: Support full serialization of objects and context to JSON or
string so the state can be restored.
* Basic operations support:
- Create volume
- Delete volume
- Extend volume
- Clone volume
- Create snapshot
- Delete snapshot
- Create volume from snapshot
- Connect volume
- Disconnect volume
- Local attach
- Local detach
- Validate connector
Demo
----
.. raw:: html
<script type="text/javascript" src="https://asciinema.org/a/TcTR7Lu7jI0pEsd9ThEn01l7n.js"
id="asciicast-TcTR7Lu7jI0pEsd9ThEn01l7n" async data-autoplay="false"
data-loop="false"></script>
Limitations
-----------
Being in its early development stages the library is in no way close to the
robustness or feature richness that the Cinder project provides. Some of the
more noticeable limitations one should be aware of are:
- Most methods don't perform argument validation so it's a classic GIGO_
library.
- The logic has been kept to a minimum and higher functioning logic is expected
to be handled by the caller.
- There is no CI, or unit tests for that matter, and certainly nothing so fancy
as third party vendor CIs, so things could be broken at any point.
- Only a subset of Cinder available operations are supported by the library.
- The only Cinder release that has been tested with the library has been Pike.
- Access to a small number of storage arrays has limited the number of drivers
that have been verified to work with cinderlib.
Besides *cinderlib's* own limitations the library also inherits some from
*Cinder's* code and will be bound by the same restrictions and behaviors of the
drivers as if they were running under the standard *Cinder* services. The most
notorious ones are:
- Dependency on the *eventlet* library.
- Behavior inconsistency on some operations across drivers. For example you
can find drivers where cloning is a cheap operation performed by the storage
array whereas other will actually create a new volume, attach the source and
new volume and perform a full copy of the data.
- External dependencies must be handled manually. So we'll have to take care of
any library, package, or CLI tool that is required by the driver.
- Relies on command execution via *sudo* for attach/detach operations as well
as some CLI tools.
.. _GIGO: https://en.wikipedia.org/wiki/Garbage_in,_garbage_out

View File

@ -1 +0,0 @@
.. include:: ../README.rst

1
docs/todo.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../TODO.rst

249
docs/topics/backends.rst Normal file
View File

@ -0,0 +1,249 @@
========
Backends
========
The *Backend* class provides the abstraction to access a storage array with an
specific configuration, which usually constraints our ability to operate on the
backend to a single pool.
.. note::
While some drivers have been manually validated most drivers have not, so
there's a good chance that using any non tested driver will show unexpected
behavior.
If you are testing *cinderlib* with a non verified backend you should use
an exclusive pool for the validation so you don't have to be so careful
when creating resources as you know that everything within that pool is
related to *cinderlib* and can be deleted using the vendor's management
tool.
If you try the library with another storage array I would appreciate a note
on the library version, Cinder release, and results of your testing.
Initialization
--------------
Before we can have access to an storage array we have to initialize the
*Backend*, which only has one parameter defined and all other parameters are
not defined in the method prototype:
.. code-block:: python
class Backend(object):
def __init__(self, volume_backend_name, **driver_cfg):
There are two arguments that we'll always have to pass on the initialization,
one is the `volume_backend_name` that is the unique identifier that *cinderlib*
will use to identify this specific driver initialization, so we'll need to make
sure not to repeat the name, and the other one is the `volume_driver` which
refers to the Python path that points to the *Cinder* driver.
All other *Backend* configuration options are free-form keyword arguments
because each driver and storage array requires different information to
operate, some require credentials to be passed as parameters while others use a
file, some require the control address as well as the data addresses. This
behavior is inherited from the *Cinder* project.
To find what configuration options are available and which ones are compulsory
the best is going to the Vendor's documentation or to the `OpenStack's Cinder
volume driver configuration documentation`_.
.. attention::
Some drivers have external dependencies which we must satisfy before
initializing the driver or it may fail either on the initialization or when
running specific operations. For example Kaminario requires the *krest*
Python library, and Pure requires *purestorage* Python library.
Python library dependencies are usually documented in the
`driver-requirements.txt file
<https://github.com/openstack/cinder/blob/master/driver-requirements.txt>`_,
as for the CLI required tools we'll have to check in the Vendor's
documentation.
Cinder only supports using one driver at a time, as each process only handles
one backend, but *cinderlib* has overcome this limitation and supports having
multiple *Backends* simultaneously.
Let's see now initialization examples of some storage backends:
LVM
---
.. code-block:: python
import cinderlib
lvm = cinderlib.Backend(
volume_driver='cinder.volume.drivers.lvm.LVMVolumeDriver',
volume_group='cinder-volumes',
iscsi_protocol='iscsi',
iscsi_helper='lioadm',
volume_backend_name='lvm_iscsi',
)
XtremIO
-------
.. code-block:: python
import cinderlib
xtremio = cinderlib.Backend(
volume_driver='cinder.volume.drivers.dell_emc.xtremio.XtremIOISCSIDriver',
san_ip='10.10.10.1',
xtremio_cluster_name='xtremio_cluster',
san_login='xtremio_user',
san_password='xtremio_password',
volume_backend_name='xtremio',
)
Kaminario
---------
.. code-block:: python
import cinderlib
kaminario = cl.Backend(
volume_driver='cinder.volume.drivers.kaminario.kaminario_iscsi.KaminarioISCSIDriver',
san_ip='10.10.10.2',
san_login='kaminario_user',
san_password='kaminario_password',
volume_backend_name='kaminario_iscsi',
)
Available Backends
------------------
Usual procedure is to initialize a *Backend* and store it in a variable at the
same time so we can use it to manage our storage backend, but there are cases
where we may have lost the reference or we are in a place in our code where we
don't have access to the original variable.
For these situations we can use *cinderlib's* tracking of *Backends* through
the `backends` class dictionary where all created *Backends* are stored using
the `volume_backend_name` as the key.
.. code-block:: python
for backend in cinderlib.Backend.backends.values():
initialized_msg = '' if backend.initialized else 'not '
print('Backend %s is %sinitialized with configuration: %s' %
(backend.id, initialized_msg, backend.config))
Stats
-----
In *Cinder* all cinder-volume services periodically report the stats of their
backend to the cinder-scheduler services so they can do informed placing
decisions on operations such as volume creation and volume migration.
Some of the keys provided in the stats dictionary include:
- `driver_version`
- `free_capacity_gb`
- `storage_protocol`
- `total_capacity_gb`
- `vendor_name volume_backend_name`
Additional information can be found in the `Volume Stats section
<https://docs.openstack.org/cinder/pike/contributor/drivers.html#volume-stats>`_
within the Developer's Documentation.
Gathering stats is a costly operation for many storage backends, so we have the
possibility of retrieving the cached value that was retrieved the last time.
Here's an example of the output from the LVM *Backend*:
.. code-block:: python
>>> from pprint import pprint
>>> pprint(lvm.stats())
{'driver_version': '3.0.0',
'pools': [{'QoS_support': False,
'filter_function': None,
'free_capacity_gb': 20.9,
'goodness_function': None,
'location_info': 'LVMVolumeDriver:router:cinder-volumes:thin:0',
'max_over_subscription_ratio': 20.0,
'multiattach': False,
'pool_name': 'LVM',
'provisioned_capacity_gb': 0.0,
'reserved_percentage': 0,
'thick_provisioning_support': False,
'thin_provisioning_support': True,
'total_capacity_gb': '20.90',
'total_volumes': 1}],
'sparse_copy_volume': True,
'storage_protocol': 'iSCSI',
'vendor_name': 'Open Source',
'volume_backend_name': 'LVM'}
Available volumes
-----------------
Just like the *Backend* class keeps track of all the *Backend* instances in the
`backends` class attribute, each *Backend* instance will keep track of all the
volumes that have been created in the *Backend*, regardless of how they have
been created, and still exist in the storage backend. So all volumes that have
been successfully deleted will no longer be there.
We can access the *Volumes* with the `volumes` instance attribute of type
`set`.
So assuming that we have an `lvm` variable holding an initialized *Backend*
instance where we have created volumes we could list them with:
.. code-block:: python
for vol in lvm.volumes:
print('Volume %s has %s GB' % (vol.id, vol.size))
.. note::
The `volumes` attribute variable will only hold the volumes that are known
to this *cinderlib*, be it because we have created the volumes in this run
or because we have loaded them from a serialized source.
This should not be confused with a listing of the volumes within the pool
we are using.
Attributes
----------
The *Backend* class has no attributes of interest besides the `backends`
mentioned above and the `id`, `config`, and JSON related properties we'll see
later in the :doc:`serialization` section.
The `id` property refers to the `volume_backend_name`, which is also the key
used in the `backends` class attribute.
The `config` property will return a dictionary with only the volume backend's
name by default to limit unintended exposure of backend credentials on
serialization. If we want it to return all the configuration options we need
to pass `output_all_backend_info=True` on *cinderlib* initialization.
If we try to access any non-existent attribute in the *Backend*, *cinderlib*
will understand we are trying to access a *Cinder* driver attribute and will
try to retrieve it from the driver's instance. This is the case with the
`initialized` property we accessed in the backends listing example.
Other methods
-------------
All other methods available in the *Backend* class will be explained in their
relevant sections:
- `load` and `load_backend` will be explained together with `json` and `jsons`
properties in the :doc:`serialization` section.
- `create_volume` method will be covered in the :doc:`volumes` section.
- `validate_connector` will be explained in the :doc:`connections` section.
- `global_setup` has been covered in the :doc:`initialization` section.
.. _OpenStack's Cinder volume driver configuration documentation: https://docs.openstack.org/cinder/latest/configuration/block-storage/volume-drivers.html

129
docs/topics/connections.rst Normal file
View File

@ -0,0 +1,129 @@
===========
Connections
===========
When talking about attaching a *Cinder* volume there are three steps that must
happen before the volume is available in the host:
1. Retrieve connection information from the host where the volume is going to
be attached. Here we would be getting iSCSI initiator name and such
information.
2. Use the connection information from step 1 and make the volume accessible to
it in the storage backend returning the volume connection information. This
step entails exporting the volume and initializing the connection.
3. Attaching the volume to the host using the data retrieved on step 2.
If we are running *cinderlib* and doing the attach in the same host then all
steps will be done in the same host, but in many cases you may want to manage
the storage backend in one host and attach to another, in such cases steps 1
and 3 will happen in the host that needs the attach and step 2 on the node
running *cinderlib*.
In *OpenStack* there is a connection library called *OS-Brick* that is used by
*Cinder* and *Nova* (the compute component) to perform steps 1 and 3, but in
*cinderlib* we are not currently using it directly and instead we are
leveraging *Cinder*'s helper methods to do this for us.
This adds an unnecessary dependency on specific *Cinder* code that will be
removed in the future and also limits the usefulness of the library to abstract
*OS-Brick* library usage from *cinderlib* users.
*Connection* objects' most interesting attributes are:
- `connected`: Boolean that reflects if the connection is complete
- `volume`: The *Volume* to which this instance holds the connection
information.
- `connector`: Connection information from the host that is attaching. Such as
it's hostname, IP address, initiator name, etc.
- `connection_info`: The connection information the host requires to do the
attachment, such as IP address, target name, credentials, etc.
- `attach_info`: If we have done a local attachment this will hold all the
attachment information.
Local attach
------------
Doing a local attachment with *cinderlib* once we have created a volume is
really simple, we just have to call the `attach` method from the volume and
we'll get the *Connection* information from the attached volume, and once we
are done we call the `detach` method on the *Volume* or on the *Connection*
information that we got from `attach`.
.. code-block:: python
vol = lvm.create_volume(size=1)
attach = vol.attach()
with open(attach.path, 'w') as f:
f.write('*' * 100)
vol.detach()
As mentioned before we could have called `attach.detach()` instead of
`vol.detach()` and it would have had the same effect.
Remote connection
-----------------
For a remote connection it's a little more inconvenient at the moment, since
you'll have to manually use the *OS-Brick* library on the host that is going to
do the attachment.
.. note:: THIS SECTION IS INCOMPLETE
First we need to get the connection information on the host that is going to do
the attach:
.. code-block:: python
import os_brick
# Retrieve the connection information dictionary
Then we have to do the connection
.. code-block:: python
# Create the connection
attach = vol.connect(connector_dict)
# Return the volume connection information
.. code-block:: python
import os_brick
# Do the attachment
Multipath
---------
If we want to use multipathing for local attachments we must let the *Backend*
know when instantiating the driver by passing the
`use-multipath_for_image_xfer=True`:
.. code-block:: python
import cinderlib
lvm = cinderlib.Backend(
volume_driver='cinder.volume.drivers.lvm.LVMVolumeDriver',
volume_group='cinder-volumes',
iscsi_protocol='iscsi',
iscsi_helper='lioadm',
volume_backend_name='lvm_iscsi',
use-multipath_for_image_xfer=True,
)
Multi attach
------------
Multi attach support has just been added to *Cinder* in the Queens cycle, and
it's not currently supported by *cinderlib*.

View File

@ -0,0 +1,116 @@
==============
Initialization
==============
The cinderlib itself doesn't require an initialization, as it tries to provide
sensible settings, but in some cases we may want to modify these defaults to
fit a specific desired behavior and the library provides a mechanism to support
this.
Library initialization should be done before making any other library call,
including *Backend* initialization and loading serialized data, if we try to
do it after other calls the library will raise and `Exception`.
Provided *setup* method is `cinderlib.Backend.global_setup`, but for
convenience the library provides a reference to this class method in
`cinderlib.setup`
The method definition is as follows:
.. code-block:: python
@classmethod
def global_setup(cls, file_locks_path=None, disable_sudo=False,
suppress_requests_ssl_warnings=True, disable_logs=True,
non_uuid_ids=False, output_all_backend_info=False,
**log_params):
The meaning of the library's configuration options are:
file_locks_path
---------------
Cinder is a complex system that can support Active-Active deployments, and each
driver and storage backend has different restrictions, so in order to
facilitate mutual exclusion it provides 3 different types of locks depending
on the scope the driver requires:
- Between threads of the same process.
- Between different process on the same host.
- In all the OpenStack deployment.
Cinderlib doesn't currently support the third type of locks, but that should
not be an inconvenience for most cinderlib usage.
Cinder uses file locks for the between process locking and cinderlib uses that
same kind of locking for the third type of locks, which is also what Cinder
uses when not deployed in an Active-Active fashion.
Parameter defaults to `None`, which will use the current directory to store all
file locks required by the drivers.
disable_sudo
------------
There are some operations in *Cinder* drivers that require `sudo` privileges,
this could be because they are running Python code that requires it or because
they are running a command with `sudo`.
Attaching and detaching operations with *cinderlib* will also require `sudo`
privileges.
This configuration option allows us disabling all `sudo` operations when we
know we don't require them and we are running the process with a non
passwordless `sudo` user.
Defaults to `False`.
suppress_requests_ssl_warnings
------------------------------
Controls the suppression of the *requests* library SSL certificate warnings.
Defaults to `True`.
non_uuid_ids
------------
As mentioned in the :doc:`volumes` section we can provide resource IDs manually
at creation time, and some drivers even support non UUID identificators, but
since that's not a given validation will reject any non UUID value.
This configuration option allows us to disable the validation on the IDs, at
the user's risk.
Defaults to `False`.
output_all_backend_info
-----------------------
Whether to include the *Backend* configuration when serializing objects.
Detailed information can be found in the :doc:`serialization` section.
Defaults to `False`.
disable_logs
------------
*Cinder* drivers are meant to be run within a full blown service, so they can
be quite verbose in terms of logging, that's why *cinderlib* disables it by
default.
Defaults to `True`.
other keyword arguments
-----------------------
Any other keyword argument passed to the initialization method will be
considered a *Cinder* configuration option and passed directly to all the
drivers.
This can be useful to set additional logging configuration like debug log
level, or many other advanced features.
For a list of the possible configuration options one should look into the
*Cinder* project's documentation.

View File

@ -0,0 +1,151 @@
=============
Serialization
=============
A *Cinder* driver is stateless on itself, but it still requires the right data
to work, and that's why the cinder-volume service takes care of storing the
state in the DB. This means that *cinderlib* will have to simulate the DB for
the drivers, as some operations actually return additional data that needs to
be kept and provided to any operation that follows.
During runtime all this information is stored in RAM, but what happens between
runs if we have not removed all our resources? And what will happen on a
system crash? What will happen is that *cinderlib* will lose all the data and
will no longer be able to manage any of the resource, leaving them stranded in
the storage backend.
What we have here is a requirement to provide a way to store the internal
*cinderclient* data, but that's not the only thing we'll want, as many systems
will require a mechanism to support over the wire transmission of *cinderlib*
objects. To solve this problem *cinderlib* provides a JSON encoding mechanism
that allows serialization and deserialization of its objects.
For the serialization process we have two type of methods, one called `json`
that converts to a JSON stored in a Python dictionary, and another called
`jsons` that will return a Python string representation. And for the
deserialization we have just one type of method called `load`.
To JSON
-------
As we mentioned before we have `json` and `jsons` methods, and these exist in
all *cinderlib* objects: *Backend*, *Volume*, *Snapshot*, and *Connection* as
well as in the library itself.
Current *cinderlib* serialization implementation is suboptimal for many cases
since we cannot limit how deep the serialization will go when there are other
objects references. This means that if we serialize a *Snapthot* this will
return not only the *Snapshot* information but also the information from the
*Volume* referenced in the *Snapshot*'s `volume` attribute, and when
serializing this *Volume* we will also serialize the `snapshots` field that
contain all the other snapshots as well as all its *Connections*. So in the
end a simple serialization of a *Snapshot* resulted in a JSON of the *Volume*
with all its *Snapshots*.
.. note::
We don't have to worry about circular references since *cinderlib* is
prepared to handle them.
Due to this limitation the serialization is mostly useful, at this point, to
store all the information from a *Volume*, from a *Backend*, or from all the
*Backends*.
Here is an easy way to save all the *Backend's* resources information from
*cinderlib*:
.. code-block:: python
with open('cinderlib.txt', 'w') as f:
f.write(cinderlib.jsons())
In a similar way we can also store a single *Backend* or a single *Volume*:
.. code-block:: python
vol = lvm.create_volume(size=1)
with open('lvm.txt', 'w') as f:
f.write(lvm.jsons())
with open('vol.txt', 'w') as f:
f.write(vol.jsons())
From JSON
---------
Just like we had the `json` and `jsons` methods in all the *cinderlib* objects,
we'll also have the `load` method to recreate a *cinderlib* internal
representation from JSON, be it stored in a Python string or a Python
dictionary.
So we have a `load` method in *Backend*, *Volume*, *Snapshot*, and *Connection*
as well as in the library itself, but here the library's `load` method is
different that it's `json` and `jsons` counterpart, as it will deserialize any
kind of *cinderlib* object.
Considering the files we created in the earlier examples we can easily load our
whole configuration with:
.. code-block:: python
# We must have initialized the Backends before reaching this point
with open('cinderlib.txt', 'r') as f:
data = f.read()
backends = cinderlib.load(data)
And for a specific backend or an individual volume:
.. code-block:: python
# We must have initialized the Backends before reaching this point
with open('lvm.txt', 'r') as f:
data = f.read()
lvm = cinderlib.load(data)
with open('vol.txt', 'r') as f:
data = f.read()
vol = cinderlib.load(data)
This is the preferred way to deserialize objects, but we could also use the
specific object's `load` method.
.. code-block:: python
# We must have initialized the Backends before reaching this point
with open('lvm.txt', 'r') as f:
data = f.read()
lvm = cinderlib.Backend.load(data)
with open('vol.txt', 'r') as f:
data = f.read()
vol = cinderlib.Volume.load(data)
Backend configuration
---------------------
When *cinderlib* serializes any object it also stores the *Backend* this object
belongs to, and by default and for security reasons, it only stores the
identifier of the backend, which is the `volume_backend_name`. Since we are
only storing a reference to the *Backend* this means that when you are going
through the deserialization process you require that the *Backend* the object
belonged to already present in *cinderlib*.
This should be OK for most *cinderlib* usages, since it's common practice to
store you storage backend connection information (credentials, addresses, etc.)
in a different location than your data, but there may be situations (for
example while testing) where we'll want to store everything in the same file,
not only the *cinderlib* representation of all the storage resources but also
the *Backend* configuration required to access the storage array.
To enable the serialization of the whole driver configuration we have to
specify `output_all_backend_info=True` on the *cinderlib* initialization
resulting in a self contained file with all the information required to manage
the resources.
This means that with this configuration option we won't need to configure the
*Backends* prior to loading the serialized JSON data, we can just load the data
and *cinderlib* will automatically setup the *Backends*.

65
docs/topics/snapshots.rst Normal file
View File

@ -0,0 +1,65 @@
=========
Snapshots
=========
The *Snapshot* class provides the abstraction layer required to perform all
operations on an existing snapshot, which means that the snapshot creation
operation must be invoked from other class instance, since the new snapshot we
want to create doesn't exist yet and we cannot use the *Snapshot* class to
manage it.
Create
------
Once we have a *Volume* instance we are ready to create snapshots from it, and
we can do it for attached as well as detached volumes.
.. note::
Some drivers, like the NFS, require assistance from the Compute service for
attached volumes, so they is currently no way of doing this with
*cinderlib*
Creating a snapshot can only be performed by the `create_snapshot` method from
our *Volume* instance, and once we have have created a snapshot it will be
tracked in the *Volume* instance's `snapshots` set.
Here is a simple code to create a snapshot and use the `snapshots` set to
verify that both, the returned value by the call as well as the entry added to
the `snapshots` attribute, reference the same object and that the `volume`
attribute in the *Snapshot* is referencing the source volume.
.. code-block:: python
vol = lvm.create_volume(size=1)
snap = vol.create_snapshot()
assert snap is list(vol.snapshots)[0]
assert vol is snap.volume
Delete
------
Once we have created a *Sanpshot* we can use its `delete` method to permanently
remove it from the storage backend.
Deleting a snapshot will remove its reference from the source *Volume*'s
`snapshots` set.
.. code-block:: python
vol = lvm.create_volume(size=1)
snap = vol.create_snapshot()
assert 1 == len(vol.snapshots)
snap.delete()
assert 0 == len(vol.snapshots)
Other methods
-------------
All other methods available in the *Snapshot* class will be explained in their
relevant sections:
- `load` will be explained together with `json` and `jsons` properties in the
:doc:`serialization` section.
- `create_volume` method has been covered in the :doc:`volumes` section.

60
docs/topics/tracking.rst Normal file
View File

@ -0,0 +1,60 @@
Resource tracking
-----------------
*Cinderlib* users will surely have their own variables to keep track of the
*Backends*, *Volumes*, *Snapshots*, and *Connections*, but there may be cases
where this is not enough, be it because we are in a place in our code where we
don't have access to the original variables, because we want to iterate all
instances, or maybe we are running some manual tests and we have lost the
reference to a resource. F
For these cases we can use *cinderlib's* various tracking systems to access the
resources. These tracking systems are also used by *cinderlib* in the
serialization process.
*Cinderlib* keeps track of all:
- Initialized *Backends*.
- Existing volumes in a *Backend*.
- Connections to a volume.
- Local attachment to a volume.
- Snapshots for a given volume.
- Known volumes in this run, even deleted ones.
- Known snapshots in this run, even delete ones.
- Connections made in this run, even disconnected ones.
Initialized *Backends* are stored in a dictionary in `Backends.backends` using
the `volume_backend_name` as key.
Existing volumes in a *Backend* are stored in the *Backend* instance's
`volumes` attribute as a set.
Connections to a *Volume* are stored in the *Volume* instance's `connections`
attribute as a list.
The local attachment *Connection* of a volume is stored in the *Volume*
instance's `local_attach` attribute.
Existing *Snapshots* for a *Volume* are stored as a set in the *Volume*
instance's `snapshots` attribute.
Besides the existing resources we also have access to all resources that
*cinderlib* has known about in this run through the `objects` attribute that
can be found in `Volume`, `Snapshot`, and `Connection` classes.
We can use this information to display the status of all the resources we've
created and destroyed in this run:
.. code-block:: python
for vol in cinderlib.Volume.objects:
print('Volume %s is currently %s' % (vol.id, vol.status)
for snap in cinderlib.Snapshot.objects:
print('Snapshot %s for volume %s is currently %s' %
(snap.id, snap.volume.id, snap.status))
for conn in cinderlib.Connection.objects:
print('Connection from %s with ip %s to volume %s is %s' %
(conn.connector['host'], conn.connector['ip'],
conn.volume.id, conn.status))

226
docs/topics/volumes.rst Normal file
View File

@ -0,0 +1,226 @@
=======
Volumes
=======
The *Volume* class provides the abstraction layer required to perform all
operations on an existing volume, which means that there will be volume
creation operations that will be invoked from other class instances, since the
new volume we want to create doesn't exist yet and we cannot use the *Volume*
class to manage it.
Create
------
The base resource in storage is the volume, and to create one the *cinderlib*
provides three different mechanisms, each one with a different method that will
be called on the source of the new volume.
So we have:
- Empty volumes that have no resource source and will have to be created
directly on the *Backend* via the `create_volume` method.
- Cloned volumes that will be created from a source *Volume* using its `clone`
method.
- Volumes from a snapshot, where the creation is initiated by the
`create_volume` method from the *Snapshot* instance.
.. note::
*Cinder* NFS backends will create an image and not a directory where to
store files, which falls in line with *Cinder* being a Block Storage
provider and not filesystem provider like *Manila* is.
So assuming that we have an `lvm` variable holding an initialized *Backend*
instance we could create a new 1GB volume quite easily:
.. code-block:: python
print('Stats before creating the volume are:')
pprint(lvm.stats())
vol = lvm.create_volume(1)
pprint(lvm.stats())
Now, if we have a volume that already contains data and we want to create a new
volume that starts with the same contents we can use the source volume as the
cloning source:
.. code-block:: python
cloned_vol = vol.clone()
Some drivers support cloning to a bigger volume, so we could define the new
size in the call and the driver would take care of extending the volume after
cloning it, this is usually tightly linked to the `extend` operation support by
the driver.
Cloning to a greater size would look like this:
.. code-block:: python
new_size = vol.size + 1
cloned_bigger_volume = vol.clone(size=new_size)
.. note::
Cloning efficiency is directly linked to the storage backend in use, so it
will not have the same performance in all backends. While some backends
like the Ceph/RBD will be extremely efficient others may range from slow to
being actually implemented as a `dd` operation performed by the driver
attaching source and destination volumes.
.. code-block:: python
vol = snap.create_volume()
.. note::
Just like with the cloning functionality, not all storage backends can
efficiently handle creating a volume from a snapshot.
On volume creation we can pass additional parameters like a `name` or a
`description`, but these will be irrelevant for the actual volume creation and
will only be useful to us to easily identify our volumes or to store additional
information. This information is not written in the storage backend and lives
in RAM.
Available fields with their types can be found in `Cinder's Volume OVO
definition
<https://github.com/openstack/cinder/blob/stable/pike/cinder/objects/volume.py#L69-L126>`_,
but most of them are only relevant within the full *Cinder* service.
We can access these fields as if they were part of the *cinderlib* *Volume*
instance, since the class will try to retrieve any non *cinderlib* *Volume*
from *Cinder*'s internal OVO representation.
Some of the fields we could be interested in are:
- `id`: UUID-4 unique identifier for the volume.
- `user_id`: String identifier, in *Cinder* it's a UUID, but we can choose
here.
- `project_id`: String identifier, in *Cinder* it's a UUID, but we can choose
here.
- `snapshot_id`: ID of the source snapshot used to create the volume. This
will be filled by *cinderlib*.
- `host`: In *Cinder* used to store the *host@backend#pool* information, here
we can just keep some identification of the process that wrote this.
- `size`: Volume size in GBi.
- `availability_zone`: In case we want to define AZs.
- `status`: This represents the status of the volume, and the most important
statuses are `available`, `error`, `deleted`, `in-use`, `creating`.
- `attach_status`: This can be `attached` or `detached`.
- `scheduled_at`: Date-time when the volume was scheduled to be created.
Currently not being used by *cinderlib*.
- `launched_at`: Date-time when the volume creation was completed. Currently
not being used by *cinderlib*.
- `deleted`: Boolean value indicating whether the volume has already been
deleted. It will be filled by *cinderlib*.
- `terminated_at`: When the volume delete was sent to the backend.
- `deleted_at`: When the volume delete was completed.
- `display_name`: Name identifier, this is passed as `name` to all *cinderlib*
volume creation methods.
- `display_description`: Long description of the volume, this is passed as
`description` to all *cinderlib* volume creation methods.
- `source_volid`: ID of the source volume used to create this volume. This
will be filled by *cinderlib*.
- `bootable`: Not relevant for *cinderlib*, but maybe useful for the
*cinderlib* user.
.. note::
*Cinderlib* automatically generates a UUID for the `id` if one is not
provided at volume creation time, but the caller can actually provide a
specific `id`.
By default the `id` is limited to valid UUID and this is the only kind of
ID that is guaranteed to work on all drivers. For drivers that support non
UUID IDs we can instruct *cinderlib* to modify *Cinder*'s behavior and
allow them. This is done on *cinderlib* initialization time passing
`non_uuid_ids=True`.
Delete
------
Once we have created a *Volume* we can use its `delete` method to permanently
remove it from the storage backend.
In *Cinder* there are safeguards to prevent a delete operation from completing
if it has snapshots (unless the delete request comes with the `cascade` option
set to true), but here in *cinderlib* we don't, so it's the callers
responsibility to delete the snapshots.
Deleting a volume with snapshots doesn't have a defined behavior for *Cinder*
drivers, since it's never meant to happen, so some storage backends delete the
snapshots, other leave them as they were, and others will fail the request.
Example of creating and deleting a volume:
.. code-block:: python
vol = lvm.create_volume(size=1)
vol.delete()
.. attention::
When deleting a volume that was the source of a cloning operation some
backends cannot delete them (since they have copy-on-write clones) and they
just keep them as a silent volume that will be deleted when its snapshot
and clones are deleted.
Extend
------
Many storage backends and *Cinder* drivers support extending a volume to have
more space and you can do this via the `extend` method present in your *Volume*
instance.
If the *Cinder* driver doesn't implement the extend operation it will raise a
`NotImplementedError`.
The only parameter received by the `extend` method is the new size, and this
must always be greater than the current value because *cinderlib* is not
validating this at the moment.
Example of creating, extending, and deleting a volume:
.. code-block:: python
vol = lvm.create_volume(size=1)
print('Vol %s has %s GBi' % (vol.id, vol.size))
vol.extend(2)
print('Extended vol %s has %s GBi' % (vol.id, vol.size))
vol.delete()
Other methods
-------------
All other methods available in the *Volume* class will be explained in their
relevant sections:
- `load` will be explained together with `json` and `jsons` properties in the
:doc:`serialization` section.
- `create_snapshot` method will be covered in the :doc:`snapshots` section
together with the `snapshots` attribute.
- `attach`, `detach`, `connect`, and `disconnect` methods will be explained in
the :doc:`connections` section.

View File

@ -2,6 +2,54 @@
Usage
=====
To use Cinder Library in a project::
Providing a fully Object Oriented abstraction, instead of a classic method
invocation passing the resources to work on, *cinderlib* makes it easy to hit
the ground running when managing storage resources.
Once Cinder drivers and *cinderlib* are installed we just have to import the
library to start using it:
.. code-block:: python
import cinderlib
Usage documentation is not too long and it is recommended to read it all before
using the library to be sure we have at least a high level view of the
different aspects related to managing our storage with *cinderlib*.
Before going into too much detail there are some aspects we need to clarify to
make sure our terminology is in sync and we understand where each piece fits.
In *cinderlib* we have *Backends*, that refer to a storage array's specific
connection configuration so it usually doesn't refer to the whole storage. With
a backend we'll usually have access to the configured pool.
Resources managed by *cinderlib* are *Volumes* and *Snapshots*, and a *Volume*
can be created from a *Backend*, another *Volume*, or from a *Snapshot*, and a
*Snapshot* can only be created from a *Volume*.
Once we have a volume we can create *Connections* so it can be accessible from
other hosts or we can do a local *Attachment* of the volume which will retrieve
required local connection information of this host, create a *Connection* on
the storage to this host, and then do the local *Attachment*.
Given that Cinder drivers are not stateless, *cinderlib* cannot be either and
will keep track of all the resources in memory while the library is running,
and provides a JSON serialization mechanism to support saving and restoring the
state of all the resources.
For extended information on these topics please refer to their specific
sections.
.. toctree::
:maxdepth: 1
topics/initialization
topics/backends
topics/volumes
topics/snapshots
topics/connections
topics/serialization
topics/tracking
.. _GIGO: https://en.wikipedia.org/wiki/Garbage_in,_garbage_out