]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/pybind/mgr/dashboard/services/rbd.py
import 15.2.0 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / rbd.py
index bb21e0abed8668e6da59117eb6e233577db670c2..d4a38f22b290fdc6d3a6de0c21a2193b79de0c0c 100644 (file)
@@ -1,11 +1,21 @@
 # -*- coding: utf-8 -*-
+# pylint: disable=unused-argument
 from __future__ import absolute_import
 
 import six
 
+import cherrypy
+
 import rbd
 
 from .. import mgr
+from ..tools import ViewCache
+from .ceph_service import CephService
+
+try:
+    from typing import List
+except ImportError:
+    pass  # For typing only
 
 
 RBD_FEATURES_NAME_MAPPING = {
@@ -25,7 +35,7 @@ def format_bitmask(features):
     """
     Formats the bitmask:
 
-    >>> format_bitmask(45)
+    @DISABLEDOCTEST: >>> format_bitmask(45)
     ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']
     """
     names = [val for key, val in RBD_FEATURES_NAME_MAPPING.items()
@@ -37,13 +47,14 @@ def format_features(features):
     """
     Converts the features list to bitmask:
 
-    >>> format_features(['deep-flatten', 'exclusive-lock', 'layering', 'object-map'])
+    @DISABLEDOCTEST: >>> format_features(['deep-flatten', 'exclusive-lock',
+        'layering', 'object-map'])
     45
 
-    >>> format_features(None) is None
+    @DISABLEDOCTEST: >>> format_features(None) is None
     True
 
-    >>> format_features('deep-flatten, exclusive-lock')
+    @DISABLEDOCTEST: >>> format_features('deep-flatten, exclusive-lock')
     32
     """
     if isinstance(features, six.string_types):
@@ -59,13 +70,43 @@ def format_features(features):
     return res
 
 
+def get_image_spec(pool_name, namespace, rbd_name):
+    namespace = '{}/'.format(namespace) if namespace else ''
+    return '{}/{}{}'.format(pool_name, namespace, rbd_name)
+
+
+def parse_image_spec(image_spec):
+    namespace_spec, image_name = image_spec.rsplit('/', 1)
+    if '/' in namespace_spec:
+        pool_name, namespace = namespace_spec.rsplit('/', 1)
+    else:
+        pool_name, namespace = namespace_spec, None
+    return pool_name, namespace, image_name
+
+
+def rbd_call(pool_name, namespace, func, *args, **kwargs):
+    with mgr.rados.open_ioctx(pool_name) as ioctx:
+        ioctx.set_namespace(namespace if namespace is not None else '')
+        func(ioctx, *args, **kwargs)
+
+
+def rbd_image_call(pool_name, namespace, image_name, func, *args, **kwargs):
+    def _ioctx_func(ioctx, image_name, func, *args, **kwargs):
+        with rbd.Image(ioctx, image_name) as img:
+            func(ioctx, img, *args, **kwargs)
+
+    return rbd_call(pool_name, namespace, _ioctx_func, image_name, func, *args, **kwargs)
+
+
 class RbdConfiguration(object):
     _rbd = rbd.RBD()
 
-    def __init__(self, pool_name='', image_name='', pool_ioctx=None, image_ioctx=None):
-        # type: (str, str, object, object) -> None
+    def __init__(self, pool_name='', namespace='', image_name='', pool_ioctx=None,
+                 image_ioctx=None):
+        # type: (str, str, str, object, object) -> None
         assert bool(pool_name) != bool(pool_ioctx)  # xor
         self._pool_name = pool_name
+        self._namespace = namespace if namespace is not None else ''
         self._image_name = image_name
         self._pool_ioctx = pool_ioctx
         self._image_ioctx = image_ioctx
@@ -76,17 +117,21 @@ class RbdConfiguration(object):
         return option if option.startswith('conf_') else 'conf_' + option
 
     def list(self):
-        # type: () -> [dict]
+        # type: () -> List[dict]
         def _list(ioctx):
             if self._image_name:  # image config
-                with rbd.Image(ioctx, self._image_name) as image:
-                    result = image.config_list()
+                try:
+                    with rbd.Image(ioctx, self._image_name) as image:
+                        result = image.config_list()
+                except rbd.ImageNotFound:
+                    result = []
             else:  # pool config
                 result = self._rbd.config_list(ioctx)
             return list(result)
 
         if self._pool_name:
             ioctx = mgr.rados.open_ioctx(self._pool_name)
+            ioctx.set_namespace(self._namespace)
         else:
             ioctx = self._pool_ioctx
 
@@ -96,6 +141,7 @@ class RbdConfiguration(object):
         # type: (str) -> str
         option_name = self._ensure_prefix(option_name)
         with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+            pool_ioctx.set_namespace(self._namespace)
             if self._image_name:
                 with rbd.Image(pool_ioctx, self._image_name) as image:
                     return image.metadata_get(option_name)
@@ -110,22 +156,23 @@ class RbdConfiguration(object):
         pool_ioctx = self._pool_ioctx
         if self._pool_name:  # open ioctx
             pool_ioctx = mgr.rados.open_ioctx(self._pool_name)
-            pool_ioctx.__enter__()
+            pool_ioctx.__enter__()  # type: ignore
+            pool_ioctx.set_namespace(self._namespace)  # type: ignore
 
         image_ioctx = self._image_ioctx
         if self._image_name:
             image_ioctx = rbd.Image(pool_ioctx, self._image_name)
-            image_ioctx.__enter__()
+            image_ioctx.__enter__()  # type: ignore
 
         if image_ioctx:
-            image_ioctx.metadata_set(option_name, option_value)
+            image_ioctx.metadata_set(option_name, option_value)  # type: ignore
         else:
             self._rbd.pool_metadata_set(pool_ioctx, option_name, option_value)
 
         if self._image_name:  # Name provided, so we opened it and now have to close it
-            image_ioctx.__exit__(None, None, None)
+            image_ioctx.__exit__(None, None, None)  # type: ignore
         if self._pool_name:
-            pool_ioctx.__exit__(None, None, None)
+            pool_ioctx.__exit__(None, None, None)  # type: ignore
 
     def remove(self, option_name):
         """
@@ -146,6 +193,7 @@ class RbdConfiguration(object):
 
         if self._pool_name:
             with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+                pool_ioctx.set_namespace(self._namespace)
                 _remove(pool_ioctx)
         else:
             _remove(self._pool_ioctx)
@@ -157,3 +205,166 @@ class RbdConfiguration(object):
                     self.set(option_name, option_value)
                 else:
                     self.remove(option_name)
+
+
+class RbdService(object):
+
+    @classmethod
+    def _rbd_disk_usage(cls, image, snaps, whole_object=True):
+        class DUCallback(object):
+            def __init__(self):
+                self.used_size = 0
+
+            def __call__(self, offset, length, exists):
+                if exists:
+                    self.used_size += length
+
+        snap_map = {}
+        prev_snap = None
+        total_used_size = 0
+        for _, size, name in snaps:
+            image.set_snap(name)
+            du_callb = DUCallback()
+            image.diff_iterate(0, size, prev_snap, du_callb,
+                               whole_object=whole_object)
+            snap_map[name] = du_callb.used_size
+            total_used_size += du_callb.used_size
+            prev_snap = name
+
+        return total_used_size, snap_map
+
+    @classmethod
+    def _rbd_image(cls, ioctx, pool_name, namespace, image_name):
+        with rbd.Image(ioctx, image_name) as img:
+
+            stat = img.stat()
+            stat['name'] = image_name
+            stat['id'] = img.id()
+            stat['pool_name'] = pool_name
+            stat['namespace'] = namespace
+            features = img.features()
+            stat['features'] = features
+            stat['features_name'] = format_bitmask(features)
+
+            # the following keys are deprecated
+            del stat['parent_pool']
+            del stat['parent_name']
+
+            stat['timestamp'] = "{}Z".format(img.create_timestamp()
+                                             .isoformat())
+
+            stat['stripe_count'] = img.stripe_count()
+            stat['stripe_unit'] = img.stripe_unit()
+
+            data_pool_name = CephService.get_pool_name_from_id(
+                img.data_pool_id())
+            if data_pool_name == pool_name:
+                data_pool_name = None
+            stat['data_pool'] = data_pool_name
+
+            try:
+                stat['parent'] = img.get_parent_image_spec()
+            except rbd.ImageNotFound:
+                # no parent image
+                stat['parent'] = None
+
+            # snapshots
+            stat['snapshots'] = []
+            for snap in img.list_snaps():
+                snap['timestamp'] = "{}Z".format(
+                    img.get_snap_timestamp(snap['id']).isoformat())
+                snap['is_protected'] = img.is_protected_snap(snap['name'])
+                snap['used_bytes'] = None
+                snap['children'] = []
+                img.set_snap(snap['name'])
+                for child_pool_name, child_image_name in img.list_children():
+                    snap['children'].append({
+                        'pool_name': child_pool_name,
+                        'image_name': child_image_name
+                    })
+                stat['snapshots'].append(snap)
+
+            # disk usage
+            img_flags = img.flags()
+            if 'fast-diff' in stat['features_name'] and \
+                    not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags:
+                snaps = [(s['id'], s['size'], s['name'])
+                         for s in stat['snapshots']]
+                snaps.sort(key=lambda s: s[0])
+                snaps += [(snaps[-1][0] + 1 if snaps else 0, stat['size'], None)]
+                total_prov_bytes, snaps_prov_bytes = cls._rbd_disk_usage(
+                    img, snaps, True)
+                stat['total_disk_usage'] = total_prov_bytes
+                for snap, prov_bytes in snaps_prov_bytes.items():
+                    if snap is None:
+                        stat['disk_usage'] = prov_bytes
+                        continue
+                    for ss in stat['snapshots']:
+                        if ss['name'] == snap:
+                            ss['disk_usage'] = prov_bytes
+                            break
+            else:
+                stat['total_disk_usage'] = None
+                stat['disk_usage'] = None
+
+            stat['configuration'] = RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).list()
+
+            return stat
+
+    @classmethod
+    def _rbd_image_names(cls, ioctx):
+        rbd_inst = rbd.RBD()
+        return rbd_inst.list(ioctx)
+
+    @classmethod
+    def _rbd_image_stat(cls, ioctx, pool_name, namespace, image_name):
+        return cls._rbd_image(ioctx, pool_name, namespace, image_name)
+
+    @classmethod
+    @ViewCache()
+    def rbd_pool_list(cls, pool_name, namespace=None):
+        rbd_inst = rbd.RBD()
+        with mgr.rados.open_ioctx(pool_name) as ioctx:
+            result = []
+            if namespace:
+                namespaces = [namespace]
+            else:
+                namespaces = rbd_inst.namespace_list(ioctx)
+                # images without namespace
+                namespaces.append('')
+            for current_namespace in namespaces:
+                ioctx.set_namespace(current_namespace)
+                names = cls._rbd_image_names(ioctx)
+                for name in names:
+                    try:
+                        stat = cls._rbd_image_stat(ioctx, pool_name, current_namespace, name)
+                    except rbd.ImageNotFound:
+                        # may have been removed in the meanwhile
+                        continue
+                    result.append(stat)
+            return result
+
+    @classmethod
+    def get_image(cls, image_spec):
+        pool_name, namespace, image_name = parse_image_spec(image_spec)
+        ioctx = mgr.rados.open_ioctx(pool_name)
+        if namespace:
+            ioctx.set_namespace(namespace)
+        try:
+            return cls._rbd_image(ioctx, pool_name, namespace, image_name)
+        except rbd.ImageNotFound:
+            raise cherrypy.HTTPError(404, 'Image not found')
+
+
+class RbdSnapshotService(object):
+
+    @classmethod
+    def remove_snapshot(cls, image_spec, snapshot_name, unprotect=False):
+        def _remove_snapshot(ioctx, img, snapshot_name, unprotect):
+            if unprotect:
+                img.unprotect_snap(snapshot_name)
+            img.remove_snap(snapshot_name)
+
+        pool_name, namespace, image_name = parse_image_spec(image_spec)
+        return rbd_image_call(pool_name, namespace, image_name,
+                              _remove_snapshot, snapshot_name, unprotect)