]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
qa/workunits/smb: add tests for rate limiting
authorAvan Thakkar <athakkar@redhat.com>
Mon, 2 Feb 2026 07:52:47 +0000 (13:22 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Tue, 17 Feb 2026 08:57:05 +0000 (14:27 +0530)
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
qa/suites/orch/cephadm/smb/tasks/deploy_smb_mgr_res_basic.yaml
qa/suites/orch/cephadm/smb/tasks/deploy_smb_mgr_res_dom.yaml
qa/workunits/smb/tests/pytest.ini
qa/workunits/smb/tests/smbutil.py
qa/workunits/smb/tests/test_hosts_access.py
qa/workunits/smb/tests/test_ratelimit.py [new file with mode: 0644]

index 3b0df26513b5c02bafa72dd3d6668a80e7f0923b..85f5517890621e730050c4e0a7a8413e7404efb6 100644 (file)
@@ -93,7 +93,7 @@ tasks:
     timeout: 1h
     clients:
       client.0:
-        - [default, hosts_access]
+        - [default, hosts_access, rate_limiting]
 
 - cephadm.shell:
     host.a:
index c0fb9cb747a88e98cfe34e3bc798752b339442cb..59e5e4cb01808c7a07a2de38cfa899c042050060 100644 (file)
@@ -91,7 +91,7 @@ tasks:
     timeout: 1h
     clients:
       client.0:
-        - [default, hosts_access]
+        - [default, hosts_access, rate_limiting]
 
 - cephadm.shell:
     host.a:
index 1ae9ec0c814ddb4096b274581bada266c3ba55e0..dd7f2201b5a7867ba676315a1770c6104b9ce4c2 100644 (file)
@@ -4,3 +4,4 @@ addopts = -m 'default'
 markers =
     default: Default tests
     hosts_access: Host access tests
+    rate_limiting: Rate limit tests
index 97c1d6808f780cfbd9e5c775628ee2ea359f037a..2ca8c7b7be8b368854b109736ebd9a2db9dcb23a 100644 (file)
@@ -1,6 +1,9 @@
 import base64
 import contextlib
 import pathlib
+import time
+
+import cephutil
 
 import smbclient
 from smbprotocol.header import NtStatus
@@ -154,6 +157,56 @@ class PathWrapper:
         with self.open(mode='w') as fh:
             fh.write(txt)
 
+    def write_bytes(self, data):
+        """Open the file in binary mode, write bytes to it, and close the file."""
+        with self.open(mode='wb') as fh:
+            fh.write(data)
+
     def unlink(self):
         """Unlink (remove) a file."""
         smbclient.remove(str(self.share_path))
+
+
+def get_shares(smb_cfg):
+    """Get all SMB shares."""
+    jres = cephutil.cephadm_shell_cmd(
+        smb_cfg,
+        ["ceph", "smb", "show", "ceph.smb.share"],
+        load_json=True,
+    )
+    assert jres.obj
+    resources = jres.obj['resources']
+    assert len(resources) > 0
+    assert all(r['resource_type'] == 'ceph.smb.share' for r in resources)
+    return resources
+
+
+def get_share_by_id(smb_cfg, cluster_id, share_id):
+    """Get a specific share by cluster_id and share_id."""
+    shares = get_shares(smb_cfg)
+    for share in shares:
+        if share['cluster_id'] == cluster_id and share['share_id'] == share_id:
+            return share
+    return None
+
+
+def apply_share_config(smb_cfg, share):
+    """Apply share configuration via the apply command."""
+    jres = cephutil.cephadm_shell_cmd(
+        smb_cfg,
+        ['ceph', 'smb', 'apply', '-i-'],
+        input_json={'resources': [share]},
+        load_json=True,
+    )
+    assert jres.returncode == 0
+    assert jres.obj and jres.obj.get('success')
+    assert 'results' in jres.obj
+    _results = jres.obj['results']
+    assert len(_results) == 1, "more than one result found"
+    _result = _results[0]
+    assert 'resource' in _result
+    resources_ret = _result['resource']
+    assert resources_ret['resource_type'] == 'ceph.smb.share'
+    # sleep to ensure the settings got applied in smbd
+    time.sleep(60)
+    return resources_ret
index 3dce18e49e6aab69e59525744c63d3f7d892ebf0..21575f02ca17286533f1dff9970c4052cd07692f 100644 (file)
@@ -1,48 +1,11 @@
 import copy
-import time
 
 import pytest
 import smbprotocol
 
-import cephutil
 import smbutil
 
 
-def _get_shares(smb_cfg):
-    jres = cephutil.cephadm_shell_cmd(
-        smb_cfg,
-        ["ceph", "smb", "show", "ceph.smb.share"],
-        load_json=True,
-    )
-    assert jres.obj
-    resources = jres.obj['resources']
-    assert len(resources) > 0
-    assert all(r['resource_type'] == 'ceph.smb.share' for r in resources)
-    return resources
-
-
-def _apply(smb_cfg, share):
-    jres = cephutil.cephadm_shell_cmd(
-        smb_cfg,
-        ['ceph', 'smb', 'apply', '-i-'],
-        input_json={'resources': [share]},
-        load_json=True,
-    )
-    assert jres.returncode == 0
-    assert jres.obj and jres.obj.get('success')
-    assert 'results' in jres.obj
-    _results = jres.obj['results']
-    assert len(_results) == 1, "more then one result found"
-    _result = _results[0]
-    assert 'resource' in _result
-    resources_ret = _result['resource']
-    assert resources_ret['resource_type'] == 'ceph.smb.share'
-    # sleep to ensure the settings got applied in smbd
-    # TODO: make this more dynamic somehow
-    time.sleep(60)
-    return resources_ret
-
-
 # BOGUS is an IP that should never be assigned to a test node running in
 # teuthology (or in general)
 BOGUS = '192.0.2.222'
@@ -56,7 +19,7 @@ class TestHostsAccessToggle1:
     @pytest.fixture(scope='class')
     def config(self, smb_cfg):
         filename = 'TestHostAcess1.txt'
-        orig = _get_shares(smb_cfg)[0]
+        orig = smbutil.get_shares(smb_cfg)[0]
         share_name = orig['name']
 
         print('Testing original share configuration...')
@@ -67,7 +30,7 @@ class TestHostsAccessToggle1:
         yield (filename, orig)
 
         print('Restoring original share configuration...')
-        _apply(smb_cfg, orig)
+        smbutil.apply_share_config(smb_cfg, orig)
         # With the IP restriction removed, access should succeed and we can
         # clean up our test file
         with smbutil.connection(smb_cfg, share_name) as sharep:
@@ -91,7 +54,7 @@ class TestHostsAccessToggle1:
         mod_share['hosts_access'] = [
             {'access': 'allow', 'address': BOGUS},
         ]
-        applied = _apply(smb_cfg, mod_share)
+        applied = smbutil.apply_share_config(smb_cfg, mod_share)
         assert applied['share_id'] == mod_share['share_id']
         assert applied['hosts_access'] == mod_share['hosts_access']
 
@@ -106,7 +69,7 @@ class TestHostsAccessToggle1:
         mod_share['hosts_access'] = [
             {'access': 'deny', 'address': BOGUS},
         ]
-        applied = _apply(smb_cfg, mod_share)
+        applied = smbutil.apply_share_config(smb_cfg, mod_share)
         assert applied['share_id'] == mod_share['share_id']
         assert applied['hosts_access'] == mod_share['hosts_access']
 
@@ -121,7 +84,7 @@ class TestHostsAccessToggle1:
             {'access': 'allow', 'address': BOGUS},
             {'access': 'allow', 'address': smb_cfg.default_client.ip_address},
         ]
-        applied = _apply(smb_cfg, mod_share)
+        applied = smbutil.apply_share_config(smb_cfg, mod_share)
         assert applied['share_id'] == mod_share['share_id']
         assert applied['hosts_access'] == mod_share['hosts_access']
 
@@ -135,7 +98,7 @@ class TestHostsAccessToggle1:
         mod_share['hosts_access'] = [
             {'access': 'deny', 'address': smb_cfg.default_client.ip_address},
         ]
-        applied = _apply(smb_cfg, mod_share)
+        applied = smbutil.apply_share_config(smb_cfg, mod_share)
         assert applied['share_id'] == mod_share['share_id']
         assert applied['hosts_access'] == mod_share['hosts_access']
 
@@ -150,7 +113,7 @@ class TestHostsAccessToggle1:
         mod_share['hosts_access'] = [
             {'access': 'allow', 'network': BOGUS_NET},
         ]
-        applied = _apply(smb_cfg, mod_share)
+        applied = smbutil.apply_share_config(smb_cfg, mod_share)
         assert applied['share_id'] == mod_share['share_id']
         assert applied['hosts_access'] == mod_share['hosts_access']
 
diff --git a/qa/workunits/smb/tests/test_ratelimit.py b/qa/workunits/smb/tests/test_ratelimit.py
new file mode 100644 (file)
index 0000000..7fe0028
--- /dev/null
@@ -0,0 +1,379 @@
+import time
+import pytest
+
+import cephutil
+import smbutil
+
+
+def _update_qos(smb_cfg, cluster_id, share_id, **qos_params):
+    """Update QoS settings for a share using the CLI command."""
+    cmd = [
+        "ceph",
+        "smb",
+        "share",
+        "update",
+        "cephfs",
+        "qos",
+        "--cluster-id",
+        cluster_id,
+        "--share-id",
+        share_id,
+    ]
+
+    for param, value in qos_params.items():
+        if value is not None:
+            cmd.extend([f"--{param.replace('_', '-')}", str(value)])
+
+    jres = cephutil.cephadm_shell_cmd(
+        smb_cfg,
+        cmd,
+        load_json=True,
+    )
+
+    assert (
+        jres.returncode == 0
+    ), f"Command failed with return code {jres.returncode}"
+    assert jres.obj, "No JSON response from command"
+    assert jres.obj.get("success"), f"Command not successful: {jres.obj}"
+
+    assert "resource" in jres.obj, f"Response missing 'resource' key: {jres.obj}"
+
+    time.sleep(60)
+
+    return jres.obj["resource"]
+
+
+def _test_transfer_rate(share_conn, filename, data_size_mb=1, operation="write"):
+    """Test transfer rate by writing/reading a file and measuring time."""
+    data = b"X" * (data_size_mb * 1024 * 1024)
+
+    start_time = time.time()
+
+    if operation == "write":
+        file_path = share_conn / filename
+        file_path.write_bytes(data)
+    else:
+        file_path = share_conn / filename
+        with file_path.open("rb") as f:
+            _ = f.read()
+
+    end_time = time.time()
+    duration = end_time - start_time
+
+    data_size_mbits = data_size_mb * 8
+    actual_rate_mbps = data_size_mbits / duration
+
+    return duration, actual_rate_mbps
+
+
+@pytest.mark.rate_limiting
+class TestSMBRateLimiting:
+    @pytest.fixture(scope="class")
+    def config(self, smb_cfg):
+        """Setup initial configuration with a test file."""
+        orig = smbutil.get_shares(smb_cfg)[0]
+        share_name = orig["name"]
+        cluster_id = orig["cluster_id"]
+        share_id = orig["share_id"]
+
+        original_qos = None
+        if "cephfs" in orig and "qos" in orig["cephfs"]:
+            original_qos = orig["cephfs"]["qos"]
+
+        test_filename = "test_ratelimit_data.bin"
+        with smbutil.connection(smb_cfg, share_name) as sharep:
+            test_data = b"X" * (1 * 1024 * 1024)
+            file_path = sharep / test_filename
+            file_path.write_bytes(test_data)
+
+        yield {
+            "share_name": share_name,
+            "cluster_id": cluster_id,
+            "share_id": share_id,
+            "test_filename": test_filename,
+            "original_share": orig,
+            "original_qos": original_qos,
+        }
+
+        smbutil.apply_share_config(smb_cfg, orig)
+
+        with smbutil.connection(smb_cfg, share_name) as sharep:
+            (sharep / test_filename).unlink()
+
+    def test_qos_read_iops_limit(self, smb_cfg, config):
+        """Test read IOPS rate limiting."""
+        updated_share = _update_qos(
+            smb_cfg, config["cluster_id"], config["share_id"], read_iops_limit=100
+        )
+
+        assert updated_share is not None
+        assert updated_share["cluster_id"] == config["cluster_id"]
+        assert updated_share["share_id"] == config["share_id"]
+        assert "cephfs" in updated_share
+        assert "qos" in updated_share["cephfs"]
+        assert updated_share["cephfs"]["qos"]["read_iops_limit"] == 100
+
+        show_share = smbutil.get_share_by_id(
+            smb_cfg, config["cluster_id"], config["share_id"]
+        )
+        assert show_share["cephfs"]["qos"]["read_iops_limit"] == 100
+
+    def test_qos_write_iops_limit(self, smb_cfg, config):
+        """Test write IOPS rate limiting."""
+        updated_share = _update_qos(
+            smb_cfg, config["cluster_id"], config["share_id"], write_iops_limit=50
+        )
+
+        assert updated_share is not None
+        assert updated_share["cephfs"]["qos"]["write_iops_limit"] == 50
+
+        show_share = smbutil.get_share_by_id(
+            smb_cfg, config["cluster_id"], config["share_id"]
+        )
+        assert show_share["cephfs"]["qos"]["write_iops_limit"] == 50
+
+    def test_qos_read_bandwidth_limit(self, smb_cfg, config):
+        """Test read bandwidth rate limiting."""
+        read_bw_limit = 1048576
+
+        updated_share = _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            read_bw_limit=read_bw_limit,
+        )
+
+        assert updated_share["cephfs"]["qos"]["read_bw_limit"] == read_bw_limit
+
+        show_share = smbutil.get_share_by_id(
+            smb_cfg, config["cluster_id"], config["share_id"]
+        )
+        assert show_share["cephfs"]["qos"]["read_bw_limit"] == read_bw_limit
+
+    def test_qos_write_bandwidth_limit(self, smb_cfg, config):
+        """Test write bandwidth rate limiting."""
+        write_bw_limit = 2097152
+
+        updated_share = _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            write_bw_limit=write_bw_limit,
+        )
+
+        assert updated_share["cephfs"]["qos"]["write_bw_limit"] == write_bw_limit
+
+        show_share = smbutil.get_share_by_id(
+            smb_cfg, config["cluster_id"], config["share_id"]
+        )
+        assert show_share["cephfs"]["qos"]["write_bw_limit"] == write_bw_limit
+
+    def test_qos_delay_max(self, smb_cfg, config):
+        """Test delay_max settings."""
+        read_delay_max = 100
+        write_delay_max = 150
+        updated_share = _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            read_delay_max=read_delay_max,
+            write_delay_max=write_delay_max,
+        )
+
+        qos = updated_share["cephfs"]["qos"]
+        assert qos["read_delay_max"] == read_delay_max
+        assert qos["write_delay_max"] == write_delay_max
+
+        show_share = smbutil.get_share_by_id(
+            smb_cfg, config["cluster_id"], config["share_id"]
+        )
+        assert show_share["cephfs"]["qos"]["read_delay_max"] == read_delay_max
+        assert show_share["cephfs"]["qos"]["write_delay_max"] == write_delay_max
+
+    def test_qos_multiple_limits(self, smb_cfg, config):
+        """Test applying multiple QoS limits simultaneously."""
+        updated_share = _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            read_iops_limit=100,
+            write_iops_limit=50,
+            read_bw_limit=4194304,
+            write_bw_limit=2097152,
+            read_delay_max=100,
+            write_delay_max=150,
+        )
+
+        qos = updated_share["cephfs"]["qos"]
+        assert qos["read_iops_limit"] == 100
+        assert qos["write_iops_limit"] == 50
+        assert qos["read_bw_limit"] == 4194304
+        assert qos["write_bw_limit"] == 2097152
+        assert qos["read_delay_max"] == 100
+        assert qos["write_delay_max"] == 150
+
+    def test_qos_update_existing(self, smb_cfg, config):
+        """Test updating existing QoS settings."""
+        _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            read_iops_limit=100,
+            read_bw_limit=1048576,
+        )
+
+        updated_share = _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            read_iops_limit=200,
+            write_iops_limit=100,
+            read_bw_limit=2097152,
+        )
+
+        qos = updated_share["cephfs"]["qos"]
+        assert qos["read_iops_limit"] == 200
+        assert qos["write_iops_limit"] == 100
+        assert qos["read_bw_limit"] == 2097152
+
+    def test_qos_limits_clamping(self, smb_cfg, config):
+        """Test that QoS limits are properly clamped to maximum values."""
+        excessive_iops = 2_000_000  # Above IOPS_LIMIT_MAX = 1,000,000
+        excessive_bw = 2 << 40  # Above BYTES_LIMIT_MAX = 1 << 40 (1 TB)
+        excessive_delay = 500  # Above DELAY_MAX_LIMIT = 300
+
+        updated_share = _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            read_iops_limit=excessive_iops,
+            write_iops_limit=excessive_iops,
+            read_bw_limit=excessive_bw,
+            write_bw_limit=excessive_bw,
+            read_delay_max=excessive_delay,
+            write_delay_max=excessive_delay,
+        )
+
+        qos = updated_share["cephfs"]["qos"]
+        assert qos["read_iops_limit"] == 1_000_000  # IOPS_LIMIT_MAX
+        assert qos["write_iops_limit"] == 1_000_000  # IOPS_LIMIT_MAX
+        assert qos["read_bw_limit"] == 1 << 40  # BYTES_LIMIT_MAX (1 TB)
+        assert qos["write_bw_limit"] == 1 << 40  # BYTES_LIMIT_MAX (1 TB)
+        assert qos["read_delay_max"] == 300  # DELAY_MAX_LIMIT
+        assert qos["write_delay_max"] == 300  # DELAY_MAX_LIMIT
+
+    def test_qos_zero_values(self, smb_cfg, config):
+        """Test that zero values are handled correctly (should be treated as no limit)."""
+        updated_share = _update_qos(
+            smb_cfg,
+            config["cluster_id"],
+            config["share_id"],
+            read_iops_limit=0,
+            write_iops_limit=0,
+            read_bw_limit=0,
+            write_bw_limit=0,
+            read_delay_max=0,
+            write_delay_max=0,
+        )
+
+        assert "qos" not in updated_share["cephfs"]
+
+    def test_qos_apply_via_resources(self, smb_cfg, config):
+        """Test applying QoS settings via the apply command with resources JSON."""
+        current_share = smbutil.get_share_by_id(
+            smb_cfg, config["cluster_id"], config["share_id"]
+        )
+        assert (
+            current_share is not None
+        ), f"Share {config['cluster_id']}/{config['share_id']} not found"
+
+        share_resource = current_share.copy()
+
+        if "cephfs" not in share_resource:
+            share_resource["cephfs"] = {}
+
+        share_resource["cephfs"]["qos"] = {
+            "read_iops_limit": 300,
+            "write_iops_limit": 150,
+            "read_bw_limit": 3145728,  # 3 MB/s
+            "write_bw_limit": 1572864,  # 1.5 MB/s
+            "read_delay_max": 50,
+            "write_delay_max": 75,
+        }
+
+        updated_share = smbutil.apply_share_config(smb_cfg, share_resource)
+
+        assert updated_share is not None
+        assert "cephfs" in updated_share
+        assert "qos" in updated_share["cephfs"]
+        qos = updated_share["cephfs"]["qos"]
+        assert qos["read_iops_limit"] == 300
+        assert qos["write_iops_limit"] == 150
+        assert qos["read_bw_limit"] == 3145728
+        assert qos["write_bw_limit"] == 1572864
+        assert qos["read_delay_max"] == 50
+        assert qos["write_delay_max"] == 75
+
+
+class TestSMBRateLimitingPerformance:
+    @pytest.fixture(scope="class")
+    def perf_config(self, smb_cfg):
+        """Setup for performance tests."""
+        orig = smbutil.get_shares(smb_cfg)[0]
+        share_name = orig["name"]
+        cluster_id = orig["cluster_id"]
+        share_id = orig["share_id"]
+
+        perf_filename = "perf_test_data.bin"
+        with smbutil.connection(smb_cfg, share_name) as sharep:
+            # Create a 10MB file for performance testing
+            test_data = b"X" * (10 * 1024 * 1024)
+            file_path = sharep / perf_filename
+            file_path.write_bytes(test_data)
+
+        yield {
+            "share_name": share_name,
+            "cluster_id": cluster_id,
+            "share_id": share_id,
+            "perf_filename": perf_filename,
+            "original_share": orig,
+        }
+
+        with smbutil.connection(smb_cfg, share_name) as sharep:
+            (sharep / perf_filename).unlink()
+        smbutil.apply_share_config(smb_cfg, orig)
+
+    def test_bandwidth_limiting_effect(self, smb_cfg, perf_config):
+        """Test that bandwidth limiting actually affects transfer speeds."""
+        # Apply a low bandwidth limit (512 KB/s = 4194304 bits/s = 524288 bytes/s)
+        low_bw_limit = 524288  # 512 KB/s
+
+        _update_qos(
+            smb_cfg,
+            perf_config["cluster_id"],
+            perf_config["share_id"],
+            write_bw_limit=low_bw_limit,
+        )
+
+        # Test write performance with the limit
+        test_filename = "bw_limit_test.bin"
+        with smbutil.connection(smb_cfg, perf_config["share_name"]) as sharep:
+            duration, actual_rate = _test_transfer_rate(
+                sharep, test_filename, data_size_mb=2, operation="write"
+            )
+
+            # Calculate expected minimum time for 2MB at 512KB/s limit
+            expected_min_time = (2 * 1024 * 1024) / low_bw_limit  # 2MB / 512KB/s
+
+            print(f"Write test with {low_bw_limit} bytes/s limit:")
+            print(f"  Duration: {duration:.2f}s")
+            print(f"  Actual rate: {actual_rate:.2f} Mbps")
+            print(f"  Expected minimum time: {expected_min_time:.2f}s")
+
+            # The actual duration should be at least the expected minimum time
+            # Allow 20% tolerance for test variability
+            assert (
+                duration >= expected_min_time * 0.8
+            ), f"Transfer too fast ({duration:.2f}s) for {low_bw_limit} bytes/s limit"
+
+            (sharep / test_filename).unlink()