cinder-zfs-driver 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cinder_zfs_driver-0.9.0/PKG-INFO +61 -0
- cinder_zfs_driver-0.9.0/README.md +51 -0
- cinder_zfs_driver-0.9.0/pyproject.toml +25 -0
- cinder_zfs_driver-0.9.0/pyproject.toml.orig +19 -0
- cinder_zfs_driver-0.9.0/src/cinder/zfs/__init__.py +1 -0
- cinder_zfs_driver-0.9.0/src/cinder/zfs/privsep.py +27 -0
- cinder_zfs_driver-0.9.0/src/cinder_zfs_driver/__init__.py +1 -0
- cinder_zfs_driver-0.9.0/src/cinder_zfs_driver/ctl.py +129 -0
- cinder_zfs_driver-0.9.0/src/cinder_zfs_driver/driver.py +662 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cinder-zfs-driver
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Cinder ZFS driver for FreeBSD
|
|
5
|
+
Author: joshiggins
|
|
6
|
+
Author-email: joshiggins <josh@joshh.info>
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# cinder-zfs-driver
|
|
12
|
+
|
|
13
|
+
A Cinder volume driver for FreeBSD that stores volumes as ZFS zvols.
|
|
14
|
+
|
|
15
|
+
## Status
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- Volume create, delete, extend, clone and revert to snapshot
|
|
20
|
+
- Snapshots are ZFS snapshots, with deferred destroy while clones depend on them
|
|
21
|
+
- Glance image download to a volume and volume upload to an image
|
|
22
|
+
- Local attach to a nova-bhyve instance on the same host
|
|
23
|
+
- Remote iSCSI volume export served by CTL
|
|
24
|
+
- Sparse or fully reserved zvols on disk
|
|
25
|
+
|
|
26
|
+
### Roadmap
|
|
27
|
+
|
|
28
|
+
- Remote iSCSI attach / os-brick support
|
|
29
|
+
- Snapshot export over iSCSI
|
|
30
|
+
- Per-initiator ACLs, mutual CHAP and multipath portals
|
|
31
|
+
- Volume migration between two backends using `zfs send` / `zfs receive`
|
|
32
|
+
- Add support for NVMe over Fabrics remote attach
|
|
33
|
+
|
|
34
|
+
### Limitations
|
|
35
|
+
|
|
36
|
+
- No remote attach / os-brick support so nova and the cinder backend must run on the same host
|
|
37
|
+
- Remote snapshot connections are refused
|
|
38
|
+
|
|
39
|
+
## Host requirements
|
|
40
|
+
|
|
41
|
+
`zfs allow` delegation is needed for the dataset holding volumes.
|
|
42
|
+
|
|
43
|
+
Remote export needs ctld running against the driver's config file:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
sysrc ctld_flags="-f /var/db/cinder/ctl.conf" ctld_enable=YES
|
|
47
|
+
service ctld start
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
```ini
|
|
53
|
+
[zfs]
|
|
54
|
+
volume_driver = cinder_zfs_driver.driver.ZFSVolumeDriver
|
|
55
|
+
volume_backend_name = ZFS
|
|
56
|
+
zfs_dataset = zroot/cinder
|
|
57
|
+
zfs_sparse_volumes = true
|
|
58
|
+
use_chap_auth = true
|
|
59
|
+
# zfs_ctld_config = /var/db/cinder/ctl.conf
|
|
60
|
+
# zfs_local_hosts = alias1,alias2
|
|
61
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# cinder-zfs-driver
|
|
2
|
+
|
|
3
|
+
A Cinder volume driver for FreeBSD that stores volumes as ZFS zvols.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Volume create, delete, extend, clone and revert to snapshot
|
|
10
|
+
- Snapshots are ZFS snapshots, with deferred destroy while clones depend on them
|
|
11
|
+
- Glance image download to a volume and volume upload to an image
|
|
12
|
+
- Local attach to a nova-bhyve instance on the same host
|
|
13
|
+
- Remote iSCSI volume export served by CTL
|
|
14
|
+
- Sparse or fully reserved zvols on disk
|
|
15
|
+
|
|
16
|
+
### Roadmap
|
|
17
|
+
|
|
18
|
+
- Remote iSCSI attach / os-brick support
|
|
19
|
+
- Snapshot export over iSCSI
|
|
20
|
+
- Per-initiator ACLs, mutual CHAP and multipath portals
|
|
21
|
+
- Volume migration between two backends using `zfs send` / `zfs receive`
|
|
22
|
+
- Add support for NVMe over Fabrics remote attach
|
|
23
|
+
|
|
24
|
+
### Limitations
|
|
25
|
+
|
|
26
|
+
- No remote attach / os-brick support so nova and the cinder backend must run on the same host
|
|
27
|
+
- Remote snapshot connections are refused
|
|
28
|
+
|
|
29
|
+
## Host requirements
|
|
30
|
+
|
|
31
|
+
`zfs allow` delegation is needed for the dataset holding volumes.
|
|
32
|
+
|
|
33
|
+
Remote export needs ctld running against the driver's config file:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
sysrc ctld_flags="-f /var/db/cinder/ctl.conf" ctld_enable=YES
|
|
37
|
+
service ctld start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
```ini
|
|
43
|
+
[zfs]
|
|
44
|
+
volume_driver = cinder_zfs_driver.driver.ZFSVolumeDriver
|
|
45
|
+
volume_backend_name = ZFS
|
|
46
|
+
zfs_dataset = zroot/cinder
|
|
47
|
+
zfs_sparse_volumes = true
|
|
48
|
+
use_chap_auth = true
|
|
49
|
+
# zfs_ctld_config = /var/db/cinder/ctl.conf
|
|
50
|
+
# zfs_local_hosts = alias1,alias2
|
|
51
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cinder-zfs-driver"
|
|
3
|
+
version = "0.9.0"
|
|
4
|
+
description = "Cinder ZFS driver for FreeBSD"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "joshiggins"
|
|
11
|
+
email = "josh@joshh.info"
|
|
12
|
+
|
|
13
|
+
[project.license]
|
|
14
|
+
text = "Apache-2.0"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[tool.uv.build-backend]
|
|
21
|
+
module-name = [
|
|
22
|
+
"cinder_zfs_driver",
|
|
23
|
+
"cinder.zfs",
|
|
24
|
+
]
|
|
25
|
+
namespace = true
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cinder-zfs-driver"
|
|
3
|
+
version = "0.9.0"
|
|
4
|
+
description = "Cinder ZFS driver for FreeBSD"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "joshiggins", email = "josh@joshh.info" }
|
|
8
|
+
]
|
|
9
|
+
license = {text = "Apache-2.0"}
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
15
|
+
build-backend = "uv_build"
|
|
16
|
+
|
|
17
|
+
[tool.uv.build-backend]
|
|
18
|
+
module-name = ["cinder_zfs_driver", "cinder.zfs"]
|
|
19
|
+
namespace = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Namespace shim for the cinder-zfs-driver privsep entrypoints."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Privileged ctld reload."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
|
|
6
|
+
import cinder.privsep
|
|
7
|
+
from oslo_log import log as logging
|
|
8
|
+
|
|
9
|
+
from cinder_zfs_driver.ctl import CTLD_PIDFILE
|
|
10
|
+
|
|
11
|
+
LOG = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cinder.privsep.sys_admin_pctxt.entrypoint
|
|
15
|
+
def reload_ctld():
|
|
16
|
+
"""Resolve ctld's pid from its pidfile and send it SIGHUP."""
|
|
17
|
+
try:
|
|
18
|
+
with open(CTLD_PIDFILE) as handle:
|
|
19
|
+
pid = int(handle.read().strip())
|
|
20
|
+
except (OSError, ValueError) as err:
|
|
21
|
+
raise RuntimeError(
|
|
22
|
+
f'ctld is not running (no usable {CTLD_PIDFILE}). Set '
|
|
23
|
+
'ctld_enable=YES and ctld_flags="-f <zfs_ctld_config>" in '
|
|
24
|
+
'rc.conf and start the service.'
|
|
25
|
+
) from err
|
|
26
|
+
LOG.info('reloading ctld (pid %d)', pid)
|
|
27
|
+
os.kill(pid, signal.SIGHUP)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cinder volume driver for ZFS on FreeBSD."""
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""ctl.conf rendering and the export table."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
from oslo_concurrency import lockutils
|
|
9
|
+
from oslo_log import log as logging
|
|
10
|
+
|
|
11
|
+
LOG = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# root ctld parses the file, so a value failing these is refused, not escaped
|
|
14
|
+
VOLUME_NAME_RE = re.compile(r'^volume-[0-9a-f-]{36}$')
|
|
15
|
+
IQN_RE = re.compile(r'^[A-Za-z0-9.:-]+$')
|
|
16
|
+
CHAP_RE = re.compile(r'^[A-Za-z0-9_.-]+$')
|
|
17
|
+
DEVICE_PATH_RE = re.compile(r'^/dev/zvol/[A-Za-z0-9/_.-]+$')
|
|
18
|
+
LISTEN_RE = re.compile(r'^[A-Za-z0-9.\[\]:-]+:[0-9]+$')
|
|
19
|
+
|
|
20
|
+
PORTAL_GROUP = 'cinder'
|
|
21
|
+
CTLD_PIDFILE = '/var/run/ctld.pid'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def checked(value, pattern, what):
|
|
25
|
+
"""Return the value if it matches the pattern."""
|
|
26
|
+
if value is None or not pattern.match(value):
|
|
27
|
+
raise ValueError(f'refusing to render {what} {value!r} into ctl.conf')
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass(frozen=True)
|
|
32
|
+
class Export:
|
|
33
|
+
"""One volume exported as an iSCSI target with a single LUN."""
|
|
34
|
+
|
|
35
|
+
volume_name: str
|
|
36
|
+
iqn: str
|
|
37
|
+
device_path: str
|
|
38
|
+
chap_username: str | None = None
|
|
39
|
+
chap_password: str | None = None
|
|
40
|
+
|
|
41
|
+
def __post_init__(self):
|
|
42
|
+
"""Validate every field against its pattern."""
|
|
43
|
+
checked(self.volume_name, VOLUME_NAME_RE, 'volume name')
|
|
44
|
+
checked(self.iqn, IQN_RE, 'target IQN')
|
|
45
|
+
checked(self.device_path, DEVICE_PATH_RE, 'device path')
|
|
46
|
+
if (self.chap_username is None) != (self.chap_password is None):
|
|
47
|
+
raise ValueError('CHAP username and password go together')
|
|
48
|
+
if self.chap_username is not None:
|
|
49
|
+
checked(self.chap_username, CHAP_RE, 'CHAP username')
|
|
50
|
+
checked(self.chap_password, CHAP_RE, 'CHAP password')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def render(exports, listen):
|
|
54
|
+
"""Return the complete ctl.conf text for these exports, sorted by IQN."""
|
|
55
|
+
checked(listen, LISTEN_RE, 'listen address')
|
|
56
|
+
lines = [
|
|
57
|
+
'# Generated by cinder-zfs-driver; do not edit. Exports are',
|
|
58
|
+
'# rebuilt from the cinder database on every change.',
|
|
59
|
+
'',
|
|
60
|
+
f'portal-group "{PORTAL_GROUP}" {{',
|
|
61
|
+
' discovery-auth-group no-authentication',
|
|
62
|
+
f' listen {listen}',
|
|
63
|
+
'}',
|
|
64
|
+
]
|
|
65
|
+
for export in sorted(exports, key=lambda e: e.iqn):
|
|
66
|
+
lines += ['', f'target "{export.iqn}" {{']
|
|
67
|
+
if export.chap_username is not None:
|
|
68
|
+
lines.append(
|
|
69
|
+
f' chap "{export.chap_username}" "{export.chap_password}"'
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
lines.append(' auth-group no-authentication')
|
|
73
|
+
lines += [
|
|
74
|
+
f' portal-group "{PORTAL_GROUP}"',
|
|
75
|
+
f' lun 0 {{ path "{export.device_path}" }}',
|
|
76
|
+
'}',
|
|
77
|
+
]
|
|
78
|
+
return '\n'.join(lines) + '\n'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CtlTable:
|
|
82
|
+
"""The set of current exports, mirrored to a config file and to ctld."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, path, listen, reload):
|
|
85
|
+
"""Record the config path, listen address and reload callable."""
|
|
86
|
+
self._path = path
|
|
87
|
+
self._listen = listen
|
|
88
|
+
self._reload = reload
|
|
89
|
+
self._exports = {}
|
|
90
|
+
|
|
91
|
+
@lockutils.synchronized('cinder-zfs-ctl')
|
|
92
|
+
def add(self, export):
|
|
93
|
+
"""Publish an export."""
|
|
94
|
+
self._exports[export.volume_name] = export
|
|
95
|
+
self._sync()
|
|
96
|
+
|
|
97
|
+
@lockutils.synchronized('cinder-zfs-ctl')
|
|
98
|
+
def remove(self, volume_name):
|
|
99
|
+
"""Withdraw an export."""
|
|
100
|
+
self._exports.pop(volume_name, None)
|
|
101
|
+
self._sync()
|
|
102
|
+
|
|
103
|
+
def _sync(self):
|
|
104
|
+
"""Rewrite the file and reload ctld if the content changed."""
|
|
105
|
+
text = render(self._exports.values(), self._listen)
|
|
106
|
+
try:
|
|
107
|
+
with open(self._path) as handle:
|
|
108
|
+
if handle.read() == text:
|
|
109
|
+
return
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
directory = os.path.dirname(self._path)
|
|
113
|
+
handle, staged = tempfile.mkstemp(dir=directory, prefix='.ctl.conf.')
|
|
114
|
+
try:
|
|
115
|
+
with os.fdopen(handle, 'w') as f:
|
|
116
|
+
f.write(text)
|
|
117
|
+
os.chmod(staged, 0o640)
|
|
118
|
+
os.rename(staged, self._path)
|
|
119
|
+
except OSError:
|
|
120
|
+
try:
|
|
121
|
+
os.unlink(staged)
|
|
122
|
+
except OSError:
|
|
123
|
+
pass
|
|
124
|
+
raise
|
|
125
|
+
LOG.info(
|
|
126
|
+
'wrote %(count)d export(s) to %(path)s',
|
|
127
|
+
{'count': len(self._exports), 'path': self._path},
|
|
128
|
+
)
|
|
129
|
+
self._reload()
|
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"""Cinder volume driver for ZFS on FreeBSD."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
|
|
7
|
+
from cinder import exception, interface
|
|
8
|
+
from cinder.common import constants
|
|
9
|
+
from cinder.i18n import _
|
|
10
|
+
from cinder.image import image_utils
|
|
11
|
+
from cinder.volume import configuration, driver, volume_utils
|
|
12
|
+
from oslo_concurrency import processutils
|
|
13
|
+
from oslo_config import cfg
|
|
14
|
+
from oslo_log import log as logging
|
|
15
|
+
from oslo_utils import units
|
|
16
|
+
|
|
17
|
+
from cinder_zfs_driver import ctl
|
|
18
|
+
|
|
19
|
+
LOG = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# keep this in sync with nova-bhyve
|
|
22
|
+
LOCAL_VOLUME_TYPE = 'zfs_local'
|
|
23
|
+
MAX_PROMOTIONS = 32
|
|
24
|
+
ZVOL_DEV_DIR = '/dev/zvol'
|
|
25
|
+
|
|
26
|
+
zfs_opts = [
|
|
27
|
+
cfg.StrOpt(
|
|
28
|
+
'zfs_dataset',
|
|
29
|
+
default='cinder/volumes',
|
|
30
|
+
help='Dataset under which volume zvols are created.',
|
|
31
|
+
),
|
|
32
|
+
cfg.BoolOpt(
|
|
33
|
+
'zfs_sparse_volumes',
|
|
34
|
+
default=True,
|
|
35
|
+
help='Create zvols with zfs create -s.',
|
|
36
|
+
),
|
|
37
|
+
cfg.StrOpt(
|
|
38
|
+
'zfs_ctld_config',
|
|
39
|
+
default='/var/db/cinder/ctl.conf',
|
|
40
|
+
help='Path of the ctld configuration file this driver writes.',
|
|
41
|
+
),
|
|
42
|
+
cfg.ListOpt(
|
|
43
|
+
'zfs_local_hosts',
|
|
44
|
+
default=[],
|
|
45
|
+
help='Additional connector host names treated as this node.',
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
CONF = cfg.CONF
|
|
50
|
+
CONF.register_opts(zfs_opts, group=configuration.SHARED_CONF_GROUP)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def describe(value):
|
|
54
|
+
"""Return a short identifying string for a driver call argument."""
|
|
55
|
+
if isinstance(value, (list, tuple)):
|
|
56
|
+
text = '[{}]'.format(', '.join(describe(i) for i in value))
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
for attr in ('id', 'request_id'):
|
|
60
|
+
ident = getattr(value, attr, None)
|
|
61
|
+
if ident is not None:
|
|
62
|
+
return f'{type(value).__name__}<{ident}>'
|
|
63
|
+
text = repr(value)
|
|
64
|
+
except Exception:
|
|
65
|
+
return type(value).__name__
|
|
66
|
+
if len(text) > 120:
|
|
67
|
+
return text[:120] + '...'
|
|
68
|
+
return text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def local_names(extra):
|
|
72
|
+
"""Return every name this node answers to in lowercase."""
|
|
73
|
+
names = {socket.gethostname(), socket.getfqdn()}
|
|
74
|
+
names |= {name.split('.')[0] for name in set(names)}
|
|
75
|
+
return {name.lower() for name in names if name} | {
|
|
76
|
+
name.lower() for name in extra
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def log_call(method):
|
|
81
|
+
"""Wrap a driver method so each call is logged before the body runs."""
|
|
82
|
+
|
|
83
|
+
@functools.wraps(method)
|
|
84
|
+
def wrapper(self, *args, **kwargs):
|
|
85
|
+
"""Log the call, then run the wrapped method."""
|
|
86
|
+
described = [describe(arg) for arg in args]
|
|
87
|
+
described += [f'{key}={describe(val)}' for key, val in kwargs.items()]
|
|
88
|
+
LOG.info('%s(%s)', method.__name__, ', '.join(described))
|
|
89
|
+
return method(self, *args, **kwargs)
|
|
90
|
+
|
|
91
|
+
return wrapper
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@interface.volumedriver
|
|
95
|
+
class ZFSVolumeDriver(driver.VolumeDriver):
|
|
96
|
+
"""Cinder volume driver backed by ZFS zvols."""
|
|
97
|
+
|
|
98
|
+
VERSION = '0.9.0'
|
|
99
|
+
|
|
100
|
+
CI_WIKI_NAME = 'StackBSD_ZFS_CI'
|
|
101
|
+
|
|
102
|
+
def __init__(self, *args, **kwargs):
|
|
103
|
+
"""Initialize the driver, register options and node names."""
|
|
104
|
+
super().__init__(*args, **kwargs)
|
|
105
|
+
self.configuration.append_config_values(zfs_opts)
|
|
106
|
+
self.hostname = socket.gethostname()
|
|
107
|
+
self.backend_name = (
|
|
108
|
+
self.configuration.safe_get('volume_backend_name') or 'ZFS'
|
|
109
|
+
)
|
|
110
|
+
self.protocol = constants.ISCSI
|
|
111
|
+
# computed once, getfqdn can block on DNS
|
|
112
|
+
self._node_names = local_names(self.configuration.zfs_local_hosts)
|
|
113
|
+
self._ctl_table = None
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def get_driver_options(cls):
|
|
117
|
+
"""Return the options this driver consumes."""
|
|
118
|
+
additional_opts = cls._get_oslo_driver_opts(
|
|
119
|
+
'volume_backend_name',
|
|
120
|
+
'reserved_percentage',
|
|
121
|
+
'max_over_subscription_ratio',
|
|
122
|
+
'volume_dd_blocksize',
|
|
123
|
+
'target_prefix',
|
|
124
|
+
'target_ip_address',
|
|
125
|
+
'target_port',
|
|
126
|
+
'use_chap_auth',
|
|
127
|
+
'chap_username',
|
|
128
|
+
'chap_password',
|
|
129
|
+
)
|
|
130
|
+
return zfs_opts + additional_opts
|
|
131
|
+
|
|
132
|
+
def _dataset(self, volume):
|
|
133
|
+
"""Return the dataset name of a volume's zvol."""
|
|
134
|
+
return f'{self.configuration.zfs_dataset}/{volume.name}'
|
|
135
|
+
|
|
136
|
+
def _snapshot_dataset(self, snapshot):
|
|
137
|
+
"""Return the dataset name of a snapshot."""
|
|
138
|
+
dataset = self.configuration.zfs_dataset
|
|
139
|
+
return f'{dataset}/{snapshot.volume_name}@{snapshot.name}'
|
|
140
|
+
|
|
141
|
+
def _exists(self, dataset):
|
|
142
|
+
"""Report whether a dataset or snapshot is present."""
|
|
143
|
+
try:
|
|
144
|
+
self._execute('zfs', 'list', '-H', '-o', 'name', dataset)
|
|
145
|
+
except processutils.ProcessExecutionError as err:
|
|
146
|
+
if 'does not exist' in (err.stderr or ''):
|
|
147
|
+
return False
|
|
148
|
+
raise
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def _dependent_clones(self, dataset):
|
|
152
|
+
"""Return the clones holding snapshots of a volume."""
|
|
153
|
+
out, _err = self._execute(
|
|
154
|
+
'zfs',
|
|
155
|
+
'list',
|
|
156
|
+
'-H',
|
|
157
|
+
'-r',
|
|
158
|
+
'-t',
|
|
159
|
+
'snapshot',
|
|
160
|
+
'-o',
|
|
161
|
+
'clones',
|
|
162
|
+
dataset,
|
|
163
|
+
)
|
|
164
|
+
clones = []
|
|
165
|
+
for line in out.splitlines():
|
|
166
|
+
value = line.strip()
|
|
167
|
+
if value and value != '-':
|
|
168
|
+
clones += value.split(',')
|
|
169
|
+
return clones
|
|
170
|
+
|
|
171
|
+
def _promote_dependent_clones(self, dataset):
|
|
172
|
+
"""Move snapshots shared with clones onto those clones."""
|
|
173
|
+
for _attempt in range(MAX_PROMOTIONS):
|
|
174
|
+
clones = self._dependent_clones(dataset)
|
|
175
|
+
if not clones:
|
|
176
|
+
return
|
|
177
|
+
self._execute('zfs', 'promote', clones[0])
|
|
178
|
+
raise exception.VolumeDriverException(
|
|
179
|
+
message=_(
|
|
180
|
+
'%(dataset)s still has dependent clones after '
|
|
181
|
+
'%(count)d promotions'
|
|
182
|
+
)
|
|
183
|
+
% {'dataset': dataset, 'count': MAX_PROMOTIONS}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@log_call
|
|
187
|
+
def do_setup(self, context):
|
|
188
|
+
"""Nothing to set up."""
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
@log_call
|
|
192
|
+
def check_for_setup_error(self):
|
|
193
|
+
"""Check the dataset and the prerequisites for remote export."""
|
|
194
|
+
dataset = self.configuration.zfs_dataset
|
|
195
|
+
try:
|
|
196
|
+
self._execute('zfs', 'list', '-H', '-o', 'name', dataset)
|
|
197
|
+
except processutils.ProcessExecutionError as err:
|
|
198
|
+
raise exception.VolumeDriverException(
|
|
199
|
+
message=_(
|
|
200
|
+
'Dataset %(dataset)s is not usable by this '
|
|
201
|
+
'service: %(error)s'
|
|
202
|
+
)
|
|
203
|
+
% {'dataset': dataset, 'error': err.stderr or err}
|
|
204
|
+
) from err
|
|
205
|
+
if not os.path.exists(ctl.CTLD_PIDFILE):
|
|
206
|
+
LOG.warning(
|
|
207
|
+
'ctld is not running; remote export needs it started '
|
|
208
|
+
'with -f %s',
|
|
209
|
+
self.configuration.zfs_ctld_config,
|
|
210
|
+
)
|
|
211
|
+
config_dir = os.path.dirname(self.configuration.zfs_ctld_config)
|
|
212
|
+
if not os.access(config_dir, os.W_OK):
|
|
213
|
+
LOG.warning(
|
|
214
|
+
'%s is not writable; remote export needs it', config_dir
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def local_path(self, volume):
|
|
218
|
+
"""Return the device path of a volume's zvol."""
|
|
219
|
+
return os.path.join(ZVOL_DEV_DIR, self._dataset(volume))
|
|
220
|
+
|
|
221
|
+
def _provisioned_capacity_gb(self):
|
|
222
|
+
"""Return the total volsize of the zvols under the dataset."""
|
|
223
|
+
out, _err = self._execute(
|
|
224
|
+
'zfs',
|
|
225
|
+
'list',
|
|
226
|
+
'-Hp',
|
|
227
|
+
'-r',
|
|
228
|
+
'-t',
|
|
229
|
+
'volume',
|
|
230
|
+
'-o',
|
|
231
|
+
'volsize',
|
|
232
|
+
self.configuration.zfs_dataset,
|
|
233
|
+
)
|
|
234
|
+
return round(sum(int(line) for line in out.split()) / units.Gi, 2)
|
|
235
|
+
|
|
236
|
+
def _update_volume_stats(self):
|
|
237
|
+
"""Refresh the stats dict from the dataset's space accounting."""
|
|
238
|
+
dataset = self.configuration.zfs_dataset
|
|
239
|
+
sparse = self.configuration.zfs_sparse_volumes
|
|
240
|
+
out, _err = self._execute(
|
|
241
|
+
'zfs', 'list', '-Hp', '-o', 'available,used', dataset
|
|
242
|
+
)
|
|
243
|
+
available, used = (int(value) for value in out.split())
|
|
244
|
+
location_info = f'ZFSVolumeDriver:{self.hostname}:{dataset}'
|
|
245
|
+
pool = {
|
|
246
|
+
'pool_name': self.backend_name,
|
|
247
|
+
'total_capacity_gb': round((available + used) / units.Gi, 2),
|
|
248
|
+
'free_capacity_gb': round(available / units.Gi, 2),
|
|
249
|
+
'provisioned_capacity_gb': self._provisioned_capacity_gb(),
|
|
250
|
+
'reserved_percentage': self.configuration.reserved_percentage,
|
|
251
|
+
'max_over_subscription_ratio': (
|
|
252
|
+
self.configuration.max_over_subscription_ratio
|
|
253
|
+
),
|
|
254
|
+
'thin_provisioning_support': sparse,
|
|
255
|
+
'thick_provisioning_support': not sparse,
|
|
256
|
+
'location_info': location_info,
|
|
257
|
+
'QoS_support': False,
|
|
258
|
+
'multiattach': False,
|
|
259
|
+
'filter_function': self.get_filter_function(),
|
|
260
|
+
'goodness_function': self.get_goodness_function(),
|
|
261
|
+
'backend_state': 'up',
|
|
262
|
+
}
|
|
263
|
+
self._stats = {
|
|
264
|
+
'volume_backend_name': self.backend_name,
|
|
265
|
+
'vendor_name': 'StackBSD',
|
|
266
|
+
'driver_version': self.VERSION,
|
|
267
|
+
'storage_protocol': self.protocol,
|
|
268
|
+
'pools': [pool],
|
|
269
|
+
}
|
|
270
|
+
LOG.debug(
|
|
271
|
+
'%(free)s GB free of %(total)s GB on %(pool)s',
|
|
272
|
+
{
|
|
273
|
+
'free': pool['free_capacity_gb'],
|
|
274
|
+
'total': pool['total_capacity_gb'],
|
|
275
|
+
'pool': self.backend_name,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
@log_call
|
|
280
|
+
def create_volume(self, volume):
|
|
281
|
+
"""Create the volume's zvol."""
|
|
282
|
+
command = ['zfs', 'create']
|
|
283
|
+
if self.configuration.zfs_sparse_volumes:
|
|
284
|
+
command.append('-s')
|
|
285
|
+
command += ['-V', f'{volume.size}G', self._dataset(volume)]
|
|
286
|
+
self._execute(*command)
|
|
287
|
+
|
|
288
|
+
@log_call
|
|
289
|
+
def delete_volume(self, volume):
|
|
290
|
+
"""Destroy the volume's zvol after promoting dependent clones."""
|
|
291
|
+
dataset = self._dataset(volume)
|
|
292
|
+
if not self._exists(dataset):
|
|
293
|
+
return
|
|
294
|
+
self._promote_dependent_clones(dataset)
|
|
295
|
+
self._execute('zfs', 'destroy', '-r', dataset)
|
|
296
|
+
|
|
297
|
+
@log_call
|
|
298
|
+
def create_cloned_volume(self, volume, src_vref):
|
|
299
|
+
"""Create the volume as a clone of a snapshot of the source."""
|
|
300
|
+
snapshot = f'{self._dataset(src_vref)}@{volume.name}'
|
|
301
|
+
self._execute('zfs', 'snapshot', snapshot)
|
|
302
|
+
try:
|
|
303
|
+
self._execute('zfs', 'clone', snapshot, self._dataset(volume))
|
|
304
|
+
except processutils.ProcessExecutionError:
|
|
305
|
+
self._execute('zfs', 'destroy', '-d', snapshot)
|
|
306
|
+
raise
|
|
307
|
+
if volume.size > src_vref.size:
|
|
308
|
+
self.extend_volume(volume, volume.size)
|
|
309
|
+
|
|
310
|
+
@log_call
|
|
311
|
+
def create_volume_from_snapshot(self, volume, snapshot):
|
|
312
|
+
"""Create the volume as a clone of the snapshot."""
|
|
313
|
+
self._execute(
|
|
314
|
+
'zfs',
|
|
315
|
+
'clone',
|
|
316
|
+
self._snapshot_dataset(snapshot),
|
|
317
|
+
self._dataset(volume),
|
|
318
|
+
)
|
|
319
|
+
if volume.size > snapshot.volume_size:
|
|
320
|
+
self.extend_volume(volume, volume.size)
|
|
321
|
+
|
|
322
|
+
@log_call
|
|
323
|
+
def extend_volume(self, volume, new_size):
|
|
324
|
+
"""Grow the zvol to the new size."""
|
|
325
|
+
self._execute(
|
|
326
|
+
'zfs', 'set', f'volsize={new_size}G', self._dataset(volume)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
@log_call
|
|
330
|
+
def revert_to_snapshot(self, context, volume, snapshot):
|
|
331
|
+
"""Roll the zvol back to the snapshot."""
|
|
332
|
+
self._execute('zfs', 'rollback', self._snapshot_dataset(snapshot))
|
|
333
|
+
|
|
334
|
+
def snapshot_revert_use_temp_snapshot(self):
|
|
335
|
+
"""Report that revert needs no temporary snapshot."""
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
@log_call
|
|
339
|
+
def create_snapshot(self, snapshot):
|
|
340
|
+
"""Create the snapshot."""
|
|
341
|
+
self._execute('zfs', 'snapshot', self._snapshot_dataset(snapshot))
|
|
342
|
+
|
|
343
|
+
@log_call
|
|
344
|
+
def delete_snapshot(self, snapshot):
|
|
345
|
+
"""Destroy the snapshot, deferring while clones depend on it."""
|
|
346
|
+
dataset = self._snapshot_dataset(snapshot)
|
|
347
|
+
if not self._exists(dataset):
|
|
348
|
+
return
|
|
349
|
+
self._execute('zfs', 'destroy', '-d', dataset)
|
|
350
|
+
|
|
351
|
+
def _is_local_connector(self, connector):
|
|
352
|
+
"""Report whether the connector's host names this node."""
|
|
353
|
+
host = ((connector or {}).get('host') or '').lower()
|
|
354
|
+
if not host:
|
|
355
|
+
return False
|
|
356
|
+
return (
|
|
357
|
+
host in self._node_names or host.split('.')[0] in self._node_names
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def _ctl(self):
|
|
361
|
+
"""Return the CTL export table, building it on first use."""
|
|
362
|
+
if self._ctl_table is None:
|
|
363
|
+
from cinder.zfs import privsep as zfs_privsep
|
|
364
|
+
|
|
365
|
+
self._ctl_table = ctl.CtlTable(
|
|
366
|
+
self.configuration.zfs_ctld_config,
|
|
367
|
+
self._portal(),
|
|
368
|
+
zfs_privsep.reload_ctld,
|
|
369
|
+
)
|
|
370
|
+
return self._ctl_table
|
|
371
|
+
|
|
372
|
+
def _portal(self):
|
|
373
|
+
"""Return the configured portal address as ip:port."""
|
|
374
|
+
conf = self.configuration
|
|
375
|
+
return f'{conf.target_ip_address}:{conf.target_port}'
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def _chap_from(provider_auth):
|
|
379
|
+
"""Split a 'CHAP <user> <secret>' provider_auth, or (None, None)."""
|
|
380
|
+
if not provider_auth:
|
|
381
|
+
return None, None
|
|
382
|
+
method, username, password = provider_auth.split()
|
|
383
|
+
if method != 'CHAP':
|
|
384
|
+
raise exception.VolumeBackendAPIException(
|
|
385
|
+
data=_('unsupported provider_auth method %s') % method
|
|
386
|
+
)
|
|
387
|
+
return username, password
|
|
388
|
+
|
|
389
|
+
@log_call
|
|
390
|
+
def ensure_export(self, context, volume):
|
|
391
|
+
"""Rebuild a remote export from the volume's provider fields."""
|
|
392
|
+
if not volume.provider_location:
|
|
393
|
+
return
|
|
394
|
+
username, password = self._chap_from(volume.provider_auth)
|
|
395
|
+
self._ctl().add(
|
|
396
|
+
ctl.Export(
|
|
397
|
+
volume_name=volume.name,
|
|
398
|
+
iqn=volume.provider_location.split()[1],
|
|
399
|
+
device_path=self.local_path(volume),
|
|
400
|
+
chap_username=username,
|
|
401
|
+
chap_password=password,
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
@log_call
|
|
406
|
+
def create_export(self, context, volume, connector):
|
|
407
|
+
"""Export the volume as an iSCSI target for a remote connector."""
|
|
408
|
+
if self._is_local_connector(connector):
|
|
409
|
+
return None
|
|
410
|
+
if self.configuration.use_chap_auth:
|
|
411
|
+
username = (
|
|
412
|
+
self.configuration.chap_username
|
|
413
|
+
or volume_utils.generate_username()
|
|
414
|
+
)
|
|
415
|
+
password = (
|
|
416
|
+
self.configuration.chap_password
|
|
417
|
+
or volume_utils.generate_password()
|
|
418
|
+
)
|
|
419
|
+
provider_auth = f'CHAP {username} {password}'
|
|
420
|
+
else:
|
|
421
|
+
username = password = provider_auth = None
|
|
422
|
+
iqn = self.configuration.target_prefix + volume.name
|
|
423
|
+
self._ctl().add(
|
|
424
|
+
ctl.Export(
|
|
425
|
+
volume_name=volume.name,
|
|
426
|
+
iqn=iqn,
|
|
427
|
+
device_path=self.local_path(volume),
|
|
428
|
+
chap_username=username,
|
|
429
|
+
chap_password=password,
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
return {
|
|
433
|
+
'provider_location': f'{self._portal()},1 {iqn} 0',
|
|
434
|
+
'provider_auth': provider_auth,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
@log_call
|
|
438
|
+
def remove_export(self, context, volume):
|
|
439
|
+
"""Withdraw the volume's iSCSI export."""
|
|
440
|
+
if not volume.provider_location:
|
|
441
|
+
return
|
|
442
|
+
self._ctl().remove(volume.name)
|
|
443
|
+
|
|
444
|
+
@log_call
|
|
445
|
+
def create_export_snapshot(self, context, snapshot, connector):
|
|
446
|
+
"""Nothing to export."""
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
@log_call
|
|
450
|
+
def remove_export_snapshot(self, context, snapshot):
|
|
451
|
+
"""Nothing to withdraw."""
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
@log_call
|
|
455
|
+
def initialize_connection(self, volume, connector, **kwargs):
|
|
456
|
+
"""Return the local device node or the exported iSCSI target."""
|
|
457
|
+
if self._is_local_connector(connector):
|
|
458
|
+
return {
|
|
459
|
+
'driver_volume_type': LOCAL_VOLUME_TYPE,
|
|
460
|
+
'data': {
|
|
461
|
+
'device_path': self.local_path(volume),
|
|
462
|
+
'volume_id': volume.id,
|
|
463
|
+
},
|
|
464
|
+
}
|
|
465
|
+
if not volume.provider_location:
|
|
466
|
+
raise exception.VolumeBackendAPIException(
|
|
467
|
+
data=_('volume %s has no export to connect to') % volume.name
|
|
468
|
+
)
|
|
469
|
+
portal_field, iqn, lun = volume.provider_location.split()
|
|
470
|
+
data = {
|
|
471
|
+
'target_discovered': False,
|
|
472
|
+
'target_iqn': iqn,
|
|
473
|
+
'target_portal': portal_field.split(',')[0],
|
|
474
|
+
'target_lun': int(lun),
|
|
475
|
+
'volume_id': volume.id,
|
|
476
|
+
'discard': True,
|
|
477
|
+
}
|
|
478
|
+
username, password = self._chap_from(volume.provider_auth)
|
|
479
|
+
if username is not None:
|
|
480
|
+
data['auth_method'] = 'CHAP'
|
|
481
|
+
data['auth_username'] = username
|
|
482
|
+
data['auth_password'] = password
|
|
483
|
+
return {'driver_volume_type': 'iscsi', 'data': data}
|
|
484
|
+
|
|
485
|
+
@log_call
|
|
486
|
+
def terminate_connection(self, volume, connector, **kwargs):
|
|
487
|
+
"""Nothing per-connection to undo."""
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
@log_call
|
|
491
|
+
def initialize_connection_snapshot(self, snapshot, connector, **kwargs):
|
|
492
|
+
"""Return the local device node of the snapshot's zvol."""
|
|
493
|
+
if not self._is_local_connector(connector):
|
|
494
|
+
raise exception.VolumeBackendAPIException(
|
|
495
|
+
data=_(
|
|
496
|
+
'snapshot %(name)s can only be attached on %(storage)s, '
|
|
497
|
+
'not from %(requester)s'
|
|
498
|
+
)
|
|
499
|
+
% {
|
|
500
|
+
'name': snapshot.name,
|
|
501
|
+
'storage': self.hostname,
|
|
502
|
+
'requester': (connector or {}).get('host'),
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
return {
|
|
506
|
+
'driver_volume_type': LOCAL_VOLUME_TYPE,
|
|
507
|
+
'data': {
|
|
508
|
+
'device_path': os.path.join(
|
|
509
|
+
ZVOL_DEV_DIR, self._snapshot_dataset(snapshot)
|
|
510
|
+
),
|
|
511
|
+
'volume_id': snapshot.id,
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@log_call
|
|
516
|
+
def terminate_connection_snapshot(self, snapshot, connector, **kwargs):
|
|
517
|
+
"""Nothing per-connection to undo."""
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
@log_call
|
|
521
|
+
def validate_connector(self, connector):
|
|
522
|
+
"""Accept any connector."""
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
@log_call
|
|
526
|
+
def clone_image(
|
|
527
|
+
self, context, volume, image_location, image_meta, image_service
|
|
528
|
+
):
|
|
529
|
+
"""Decline to clone the image."""
|
|
530
|
+
return None, False
|
|
531
|
+
|
|
532
|
+
@log_call
|
|
533
|
+
def copy_image_to_volume(
|
|
534
|
+
self, context, volume, image_service, image_id, disable_sparse=False
|
|
535
|
+
):
|
|
536
|
+
"""Fetch the image raw onto the volume's device node."""
|
|
537
|
+
image_utils.fetch_to_raw(
|
|
538
|
+
context,
|
|
539
|
+
image_service,
|
|
540
|
+
image_id,
|
|
541
|
+
self.local_path(volume),
|
|
542
|
+
self.configuration.volume_dd_blocksize,
|
|
543
|
+
size=volume.size,
|
|
544
|
+
disable_sparse=disable_sparse,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
@log_call
|
|
548
|
+
def copy_volume_to_image(self, context, volume, image_service, image_meta):
|
|
549
|
+
"""Upload the volume's device node as an image."""
|
|
550
|
+
volume_utils.upload_volume(
|
|
551
|
+
context, image_service, image_meta, self.local_path(volume), volume
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
@log_call
|
|
555
|
+
def migrate_volume(self, context, volume, host):
|
|
556
|
+
"""Report the volume as migrated in place."""
|
|
557
|
+
return True, None
|
|
558
|
+
|
|
559
|
+
@log_call
|
|
560
|
+
def update_migrated_volume(
|
|
561
|
+
self, ctxt, volume, new_volume, original_volume_status
|
|
562
|
+
):
|
|
563
|
+
"""Report that the backend volume kept the new volume's identity."""
|
|
564
|
+
return {
|
|
565
|
+
'_name_id': new_volume.name_id,
|
|
566
|
+
'provider_location': new_volume.provider_location,
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
@log_call
|
|
570
|
+
def retype(self, context, volume, new_type, diff, host):
|
|
571
|
+
"""Report the retype as handled in place."""
|
|
572
|
+
return True, None
|
|
573
|
+
|
|
574
|
+
@log_call
|
|
575
|
+
def accept_transfer(self, context, volume, new_user, new_project):
|
|
576
|
+
"""Nothing to change on ownership transfer."""
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
@log_call
|
|
580
|
+
def manage_existing(self, volume, existing_ref):
|
|
581
|
+
"""Accept the existing zvol as-is."""
|
|
582
|
+
pass
|
|
583
|
+
|
|
584
|
+
@log_call
|
|
585
|
+
def manage_existing_get_size(self, volume, existing_ref):
|
|
586
|
+
"""Report the smallest size Cinder accepts."""
|
|
587
|
+
return 1
|
|
588
|
+
|
|
589
|
+
@log_call
|
|
590
|
+
def get_manageable_volumes(
|
|
591
|
+
self, cinder_volumes, marker, limit, offset, sort_keys, sort_dirs
|
|
592
|
+
):
|
|
593
|
+
"""Report no manageable volumes."""
|
|
594
|
+
return []
|
|
595
|
+
|
|
596
|
+
@log_call
|
|
597
|
+
def unmanage(self, volume):
|
|
598
|
+
"""Leave the zvol in place."""
|
|
599
|
+
pass
|
|
600
|
+
|
|
601
|
+
@log_call
|
|
602
|
+
def manage_existing_snapshot(self, snapshot, existing_ref):
|
|
603
|
+
"""Accept the existing snapshot as-is."""
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
@log_call
|
|
607
|
+
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
|
|
608
|
+
"""Report the smallest size Cinder accepts."""
|
|
609
|
+
return 1
|
|
610
|
+
|
|
611
|
+
@log_call
|
|
612
|
+
def get_manageable_snapshots(
|
|
613
|
+
self, cinder_snapshots, marker, limit, offset, sort_keys, sort_dirs
|
|
614
|
+
):
|
|
615
|
+
"""Report no manageable snapshots."""
|
|
616
|
+
return []
|
|
617
|
+
|
|
618
|
+
@log_call
|
|
619
|
+
def unmanage_snapshot(self, snapshot):
|
|
620
|
+
"""Leave the snapshot in place."""
|
|
621
|
+
pass
|
|
622
|
+
|
|
623
|
+
@log_call
|
|
624
|
+
def create_group(self, context, group):
|
|
625
|
+
"""Accept the group without backend state."""
|
|
626
|
+
pass
|
|
627
|
+
|
|
628
|
+
@log_call
|
|
629
|
+
def delete_group(self, context, group, volumes):
|
|
630
|
+
"""Delete the group without backend state."""
|
|
631
|
+
return None, None
|
|
632
|
+
|
|
633
|
+
@log_call
|
|
634
|
+
def update_group(
|
|
635
|
+
self, context, group, add_volumes=None, remove_volumes=None
|
|
636
|
+
):
|
|
637
|
+
"""Accept membership changes without backend state."""
|
|
638
|
+
return None, None, None
|
|
639
|
+
|
|
640
|
+
@log_call
|
|
641
|
+
def create_group_from_src(
|
|
642
|
+
self,
|
|
643
|
+
context,
|
|
644
|
+
group,
|
|
645
|
+
volumes,
|
|
646
|
+
group_snapshot=None,
|
|
647
|
+
snapshots=None,
|
|
648
|
+
source_group=None,
|
|
649
|
+
source_vols=None,
|
|
650
|
+
):
|
|
651
|
+
"""Create the group without backend state."""
|
|
652
|
+
return None, None
|
|
653
|
+
|
|
654
|
+
@log_call
|
|
655
|
+
def create_group_snapshot(self, context, group_snapshot, snapshots):
|
|
656
|
+
"""Create the group snapshot without backend state."""
|
|
657
|
+
return None, None
|
|
658
|
+
|
|
659
|
+
@log_call
|
|
660
|
+
def delete_group_snapshot(self, context, group_snapshot, snapshots):
|
|
661
|
+
"""Delete the group snapshot without backend state."""
|
|
662
|
+
return None, None
|