]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
python-common: make YAML representaition of ServiceSpec readable
authorSebastian Wagner <sebastian.wagner@suse.com>
Thu, 11 Jun 2020 09:54:45 +0000 (11:54 +0200)
committerSebastian Wagner <sebastian.wagner@suse.com>
Thu, 25 Jun 2020 10:33:19 +0000 (12:33 +0200)
* Add test for new yaml representation

Signed-off-by: Sebastian Wagner <sebastian.wagner@suse.com>
src/pybind/mgr/cephadm/tests/test_cephadm.py
src/pybind/mgr/cephadm/tests/test_spec.py
src/python-common/ceph/deployment/service_spec.py
src/python-common/ceph/tests/test_service_spec.py

index e80728c3871e006af3b759bab72281e489df8729..9311a7c3b68c53fa2d54fff879e0731271bc18a7 100644 (file)
@@ -111,7 +111,7 @@ class TestCephadm(object):
             assert wait(cephadm_module, c) == 'Scheduled rgw.r.z update...'
 
             c = cephadm_module.describe_service()
-            out = [o.to_json() for o in wait(cephadm_module, c)]
+            out = [dict(o.to_json()) for o in wait(cephadm_module, c)]
             expected = [
                 {
                     'placement': {'hosts': [{'hostname': 'test', 'name': '', 'network': ''}]},
@@ -126,8 +126,10 @@ class TestCephadm(object):
                         'count': 1,
                         'hosts': [{'hostname': 'test', 'name': '', 'network': ''}]
                     },
-                    'rgw_realm': 'r',
-                    'rgw_zone': 'z',
+                    'spec': {
+                        'rgw_realm': 'r',
+                        'rgw_zone': 'z',
+                    },
                     'service_id': 'r.z',
                     'service_name': 'rgw.r.z',
                     'service_type': 'rgw',
index ea4e8c8f73425c98dde26bf22d2c07d6503009bd..1a048d82d6f32ca8a277e304d119032c1f840aaf 100644 (file)
@@ -8,11 +8,9 @@ from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec, RGWSpec, \
 from orchestrator import DaemonDescription, OrchestratorError
 
 
-def test_spec_octopus():
-    # https://tracker.ceph.com/issues/44934
-    # Those are real user data from early octopus.
-    # Please do not modify those JSON values.
-    specs_text = """[
+@pytest.mark.parametrize(
+    "spec_json",
+    json.loads("""[
 {
   "placement": {
     "count": 1
@@ -70,10 +68,47 @@ def test_spec_octopus():
   "rgw_realm": "default-rgw-realm",
   "rgw_zone": "eu-central-1",
   "subcluster": "1"
+},
+{
+  "service_type": "osd",
+  "service_id": "osd_spec_default",
+  "placement": {
+    "host_pattern": "*"
+  },
+  "data_devices": {
+    "model": "MC-55-44-XZ"
+  },
+  "db_devices": {
+    "model": "SSD-123-foo"
+  },
+  "wal_devices": {
+    "model": "NVME-QQQQ-987"
+  }
 }
 ]
-"""
-    dds_text = """[
+""")
+)
+def test_spec_octopus(spec_json):
+    # https://tracker.ceph.com/issues/44934
+    # Those are real user data from early octopus.
+    # Please do not modify those JSON values.
+
+    spec = ServiceSpec.from_json(spec_json)
+    # just some verification that we can sill read old octopus specs
+    def convert_to_old_style_json(j):
+        j_c = dict(j.copy())
+        j_c.pop('service_name', None)
+        if 'spec' in j_c:
+            spec = j_c.pop('spec')
+            j_c.update(spec)
+        j_c.pop('objectstore', None)
+        return j_c
+    assert spec_json == convert_to_old_style_json(spec.to_json())
+
+
+@pytest.mark.parametrize(
+    "dd_json",
+    json.loads("""[
     {
         "hostname": "ceph-001",
         "container_id": "d94d7969094d",
@@ -207,21 +242,13 @@ def test_spec_octopus():
         "status": 1,
         "status_desc": "starting" 
     }
-]"""
-    specs_json = json.loads(specs_text)
-    dds_json = json.loads(dds_text)
-    specs = [ServiceSpec.from_json(j) for j in specs_json]
-    dds = [DaemonDescription.from_json(j) for j in dds_json]
-
-    # just some verification that we can sill read old octopus specs
-    def remove_service_name(j):
-        if 'service_name' in j:
-            j_c = j.copy()
-            del j_c['service_name']
-            return j_c
-        return j
-    assert specs_json == [remove_service_name(s.to_json()) for s in specs]
-    assert dds_json == [d.to_json() for d in dds]
+]""")
+)
+def test_dd_octopus(dd_json):
+    # https://tracker.ceph.com/issues/44934
+    # Those are real user data from early octopus.
+    # Please do not modify those JSON values.
+    assert dd_json == DaemonDescription.from_json(dd_json).to_json()
 
 
 @pytest.mark.parametrize("spec,dd,valid",
index 4d86d6fc167f5553c416b29ccb9068a89f84da26..d53a2832e364f99550fae998ac01bf05a226f91d 100644 (file)
@@ -1,10 +1,11 @@
 import fnmatch
 import re
-from collections import namedtuple
+from collections import namedtuple, OrderedDict
 from functools import wraps
 from typing import Optional, Dict, Any, List, Union, Callable, Iterator
 
 import six
+import yaml
 
 from ceph.deployment.hostspec import HostSpec
 
@@ -38,7 +39,7 @@ def handle_type_error(method):
             return method(cls, *args, **kwargs)
         except (TypeError, AttributeError) as e:
             error_msg = '{}: {}'.format(cls.__name__, e)
-        raise ServiceSpecValidationError(error_msg)
+            raise ServiceSpecValidationError(error_msg)
     return inner
 
 
@@ -470,16 +471,27 @@ class ServiceSpec(object):
         return n
 
     def to_json(self):
-        # type: () -> Dict[str, Any]
+        # type: () -> OrderedDict[str, Any]
+        ret: OrderedDict[str, Any] = OrderedDict()
+        ret['service_type'] = self.service_type
+        if self.service_id:
+            ret['service_id'] = self.service_id
+        ret['service_name'] = self.service_name()
+        ret['placement'] = self.placement.to_json()
+        if self.unmanaged:
+            ret['unmanaged'] = self.unmanaged
+
         c = {}
-        for key, val in self.__dict__.items():
+        for key, val in sorted(self.__dict__.items(), key=lambda tpl: tpl[0]):
+            if key in ret:
+                continue
             if hasattr(val, 'to_json'):
                 val = val.to_json()
             if val:
                 c[key] = val
-
-        c['service_name'] = self.service_name()
-        return c
+        if c:
+            ret['spec'] = c
+        return ret
 
     def validate(self):
         if not self.service_type:
@@ -497,6 +509,13 @@ class ServiceSpec(object):
     def one_line_str(self):
         return '<{} for service_name={}>'.format(self.__class__.__name__, self.service_name())
 
+    @staticmethod
+    def yaml_representer(dumper: 'yaml.SafeDumper', data: 'ServiceSpec'):
+        return dumper.represent_dict(data.to_json().items())
+
+
+yaml.add_representer(ServiceSpec, ServiceSpec.yaml_representer)
+
 
 class NFSServiceSpec(ServiceSpec):
     def __init__(self,
@@ -539,6 +558,9 @@ class NFSServiceSpec(ServiceSpec):
         return url
 
 
+yaml.add_representer(NFSServiceSpec, ServiceSpec.yaml_representer)
+
+
 class RGWSpec(ServiceSpec):
     """
     Settings to configure a (multisite) Ceph RGW
@@ -600,6 +622,9 @@ class RGWSpec(ServiceSpec):
         return f'beast {" ".join(ports)}'
 
 
+yaml.add_representer(RGWSpec, ServiceSpec.yaml_representer)
+
+
 class IscsiServiceSpec(ServiceSpec):
     def __init__(self,
                  service_type: str = 'iscsi',
@@ -644,3 +669,6 @@ class IscsiServiceSpec(ServiceSpec):
         if not self.api_password:
             raise ServiceSpecValidationError(
                 'Cannot add ISCSI: No Api password specified')
+
+
+yaml.add_representer(IscsiServiceSpec, ServiceSpec.yaml_representer)
index e72a37fbc632776e9bd7ce3426f4176e58f6c8cb..6aa26adc4239b8a668d45d175b830d5683a26b17 100644 (file)
@@ -109,3 +109,50 @@ def test_servicespec_map_test(s_type, o_spec, s_id):
     assert spec.validate() is None
     ServiceSpec.from_json(spec.to_json())
 
+
+def test_yaml():
+    y = """service_type: crash
+service_name: crash
+placement:
+  host_pattern: '*'
+---
+service_type: crash
+service_name: crash
+placement:
+  host_pattern: '*'
+unmanaged: true
+---
+service_type: rgw
+service_id: default-rgw-realm.eu-central-1.1
+service_name: rgw.default-rgw-realm.eu-central-1.1
+placement:
+  hosts:
+  - hostname: ceph-001
+    name: ''
+    network: ''
+spec:
+  rgw_realm: default-rgw-realm
+  rgw_zone: eu-central-1
+  subcluster: '1'
+---
+service_type: osd
+service_id: osd_spec_default
+service_name: osd.osd_spec_default
+placement:
+  host_pattern: '*'
+spec:
+  data_devices:
+    model: MC-55-44-XZ
+  db_devices:
+    model: SSD-123-foo
+  objectstore: bluestore
+  wal_devices:
+    model: NVME-QQQQ-987
+"""
+
+    for y in y.split('---\n'):
+        data = yaml.safe_load(y)
+        object = ServiceSpec.from_json(data)
+
+        assert yaml.dump(object) == y
+        assert yaml.dump(ServiceSpec.from_json(object.to_json())) == y