]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/cephadm: adding UT for the new functionality 67308/head
authorRedouane Kachach <rkachach@ibm.com>
Mon, 9 Mar 2026 15:11:50 +0000 (16:11 +0100)
committerRedouane Kachach <rkachach@ibm.com>
Fri, 22 May 2026 18:21:30 +0000 (20:21 +0200)
Fixes: https://tracker.ceph.com/issues/75009
Signed-off-by: Redouane Kachach <rkachach@ibm.com>
src/pybind/mgr/cephadm/services/cephadmservice.py
src/pybind/mgr/cephadm/services/ingress.py
src/pybind/mgr/cephadm/services/nfs.py
src/pybind/mgr/cephadm/tests/services/test_iscsi.py
src/pybind/mgr/cephadm/tests/services/test_monitoring.py
src/pybind/mgr/cephadm/tests/services/test_nfs.py
src/pybind/mgr/cephadm/tests/services/test_rgw.py
src/pybind/mgr/cephadm/tests/test_certmgr.py

index 09080e27a191241a41aa9c28fba5b3576b9c81b8..eb65369368236ed1a422708cd882d8aa319ada25 100644 (file)
@@ -341,10 +341,10 @@ class CephadmService(metaclass=ABCMeta):
         if cert_source:
             deps.append(f'certificate_source: {cert_source}')
         if spec.ssl_cert and spec.ssl_key:
-            deps.append(f'ssl_cert: {str(utils.md5_hash(spec.ssl_cert))}')
-            deps.append(f'ssl_key: {str(utils.md5_hash(spec.ssl_key))}')
+            deps.append(f'ssl_cert: {str(utils.config_hash(spec.ssl_cert))}')
+            deps.append(f'ssl_key: {str(utils.config_hash(spec.ssl_key))}')
         if spec.ssl_ca_cert:
-            deps.append(f'ssl_ca_cert: {str(utils.md5_hash(spec.ssl_ca_cert))}')
+            deps.append(f'ssl_ca_cert: {str(utils.config_hash(spec.ssl_ca_cert))}')
 
         return sorted(deps)
 
index 41f70e7ba7dd768e81417273363e806def3bd86e..05d8525b9fd84de153f06367fe270c78336695b0 100644 (file)
@@ -9,7 +9,7 @@ from ceph.deployment.utils import is_ipv6
 from mgr_util import build_url
 from cephadm import utils
 from orchestrator import OrchestratorError, DaemonDescription
-from cephadm.services.cephadmservice import CephadmDaemonDeploySpec, CephService
+from cephadm.services.cephadmservice import CephadmDaemonDeploySpec, CephService, CephadmService
 from .service_registry import register_cephadm_service
 from cephadm.tlsobject_types import TLSCredentials
 from cephadm.schedule import get_placement_hosts
@@ -123,7 +123,6 @@ class IngressService(CephService):
             hosts = get_placement_hosts(spec, mgr.cache.get_schedulable_hosts(), mgr.cache.get_draining_hosts())
             deps.append(f'placement_hosts:{",".join(sorted(h.hostname for h in hosts))}')
 
-        from cephadm.services.cephadmservice import CephadmService
         parent_deps = CephadmService.get_dependencies(mgr, spec)
         return sorted(deps + parent_deps)
 
index d47adb3327ebf5801192b2b5b12eb98d8072d958..812ee482d813d51a9d9d7784f03d90e717fd668f 100644 (file)
@@ -8,7 +8,7 @@ from threading import Lock
 from typing import Dict, Tuple, Any, List, cast, Optional, TYPE_CHECKING
 from configparser import ConfigParser
 from io import StringIO
-
+from cephadm import utils
 from mgr_module import HandleCommandResult
 from mgr_module import NFS_POOL_NAME as POOL_NAME
 
index e5fc7695f228b574122ecc5924969cb66ae73fff..4bbfa72f3c336660c641d1323a5b78b5d5a6b171 100644 (file)
@@ -10,7 +10,6 @@ from cephadm.tests.fixtures import with_host, with_service, async_side_effect
 from cephadm.tlsobject_types import TLSCredentials
 from orchestrator._interface import DaemonDescription
 
-
 ceph_generated_cert = """-----BEGIN CERTIFICATE-----\nMIICxjCCAa4CEQDIZSujNBlKaLJzmvntjukjMA0GCSqGSIb3DQEBDQUAMCExDTAL\nBgNVBAoMBENlcGgxEDAOBgNVBAMMB2NlcGhhZG0wHhcNMjIwNzEzMTE0NzA3WhcN\nMzIwNzEwMTE0NzA3WjAhMQ0wCwYDVQQKDARDZXBoMRAwDgYDVQQDDAdjZXBoYWRt\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyyMe4DMA+MeYK7BHZMHB\nq7zjliEOcNgxomjU8qbf5USF7Mqrf6+/87XWqj4pCyAW8x0WXEr6A56a+cmBVmt+\nqtWDzl020aoId6lL5EgLLn6/kMDCCJLq++Lg9cEofMSvcZh+lY2f+1p+C+00xent\nrLXvXGOilAZWaQfojT2BpRnNWWIFbpFwlcKrlg2G0cFjV5c1m6a0wpsQ9JHOieq0\nSvwCixajwq3CwAYuuiU1wjI4oJO4Io1+g8yB3nH2Mo/25SApCxMXuXh4kHLQr/T4\n4hqisvG4uJYgKMcSIrWj5o25mclByGi1UI/kZkCUES94i7Z/3ihx4Bad0AMs/9tw\nFwIDAQABMA0GCSqGSIb3DQEBDQUAA4IBAQAf+pwz7Gd7mDwU2LY0TQXsK6/8KGzh\nHuX+ErOb8h5cOAbvCnHjyJFWf6gCITG98k9nxU9NToG0WYuNm/max1y/54f0dtxZ\npUo6KSNl3w6iYCfGOeUIj8isi06xMmeTgMNzv8DYhDt+P2igN6LenqWTVztogkiV\nxQ5ZJFFLEw4sN0CXnrZX3t5ruakxLXLTLKeE0I91YJvjClSBGkVJq26wOKQNHMhx\npWxeydQ5EgPZY+Aviz5Dnxe8aB7oSSovpXByzxURSabOuCK21awW5WJCGNpmqhWK\nZzACBDEstccj57c4OGV0eayHJRsluVr2e9NHRINZA3qdB37e6gsI1xHo\n-----END CERTIFICATE-----\n"""
 
 ceph_generated_key = """-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDLIx7gMwD4x5gr\nsEdkwcGrvOOWIQ5w2DGiaNTypt/lRIXsyqt/r7/ztdaqPikLIBbzHRZcSvoDnpr5\nyYFWa36q1YPOXTbRqgh3qUvkSAsufr+QwMIIkur74uD1wSh8xK9xmH6VjZ/7Wn4L\n7TTF6e2ste9cY6KUBlZpB+iNPYGlGc1ZYgVukXCVwquWDYbRwWNXlzWbprTCmxD0\nkc6J6rRK/AKLFqPCrcLABi66JTXCMjigk7gijX6DzIHecfYyj/blICkLExe5eHiQ\nctCv9PjiGqKy8bi4liAoxxIitaPmjbmZyUHIaLVQj+RmQJQRL3iLtn/eKHHgFp3Q\nAyz/23AXAgMBAAECggEAVoTB3Mm8azlPlaQB9GcV3tiXslSn+uYJ1duCf0sV52dV\nBzKW8s5fGiTjpiTNhGCJhchowqxoaew+o47wmGc2TvqbpeRLuecKrjScD0GkCYyQ\neM2wlshEbz4FhIZdgS6gbuh9WaM1dW/oaZoBNR5aTYo7xYTmNNeyLA/jO2zr7+4W\n5yES1lMSBXpKk7bDGKYY4bsX2b5RLr2Grh2u2bp7hoLABCEvuu8tSQdWXLEXWpXo\njwmV3hc6tabypIa0mj2Dmn2Dmt1ppSO0AZWG/WAizN3f4Z0r/u9HnbVrVmh0IEDw\n3uf2LP5o3msG9qKCbzv3lMgt9mMr70HOKnJ8ohMSKQKBgQDLkNb+0nr152HU9AeJ\nvdz8BeMxcwxCG77iwZphZ1HprmYKvvXgedqWtS6FRU+nV6UuQoPUbQxJBQzrN1Qv\nwKSlOAPCrTJgNgF/RbfxZTrIgCPuK2KM8I89VZv92TSGi362oQA4MazXC8RAWjoJ\nSu1/PHzK3aXOfVNSLrOWvIYeZQKBgQD/dgT6RUXKg0UhmXj7ExevV+c7oOJTDlMl\nvLngrmbjRgPO9VxLnZQGdyaBJeRngU/UXfNgajT/MU8B5fSKInnTMawv/tW7634B\nw3v6n5kNIMIjJmENRsXBVMllDTkT9S7ApV+VoGnXRccbTiDapBThSGd0wri/CuwK\nNWK1YFOeywKBgEDyI/XG114PBUJ43NLQVWm+wx5qszWAPqV/2S5MVXD1qC6zgCSv\nG9NLWN1CIMimCNg6dm7Wn73IM7fzvhNCJgVkWqbItTLG6DFf3/DPODLx1wTMqLOI\nqFqMLqmNm9l1Nec0dKp5BsjRQzq4zp1aX21hsfrTPmwjxeqJZdioqy2VAoGAXR5X\nCCdSHlSlUW8RE2xNOOQw7KJjfWT+WAYoN0c7R+MQplL31rRU7dpm1bLLRBN11vJ8\nMYvlT5RYuVdqQSP6BkrX+hLJNBvOLbRlL+EXOBrVyVxHCkDe+u7+DnC4epbn+N8P\nLYpwqkDMKB7diPVAizIKTBxinXjMu5fkKDs5n+sCgYBbZheYKk5M0sIxiDfZuXGB\nkf4mJdEkTI1KUGRdCwO/O7hXbroGoUVJTwqBLi1tKqLLarwCITje2T200BYOzj82\nqwRkCXGtXPKnxYEEUOiFx9OeDrzsZV00cxsEnX0Zdj+PucQ/J3Cvd0dWUspJfLHJ\n39gnaegswnz9KMQAvzKFdg==\n-----END PRIVATE KEY-----\n"""
@@ -78,9 +77,10 @@ class TestISCSIService:
     mgr.spec_store = MagicMock()
     mgr.spec_store.all_specs.get.return_value = iscsi_spec
 
+    @patch("cephadm.services.cephadmservice.CephadmService.get_dependencies", return_value=[])
     @patch("cephadm.services.cephadmservice.CephadmService.get_certificates",
            lambda instance, dspec, ips=None: TLSCredentials(ceph_generated_cert, ceph_generated_key))
-    def test_iscsi_client_caps(self):
+    def test_iscsi_client_caps(self, _get_deps):
 
         iscsi_daemon_spec = CephadmDaemonDeploySpec(
             host='host', daemon_id='a', service_name=self.iscsi_spec.service_name())
index c73f7fad620d3a0bb1e8f6a93eb5e81434438943..300e84516ab68b72ec4436461785a17f3a7d644b 100644 (file)
@@ -4,7 +4,7 @@ import json
 import urllib.parse
 import yaml
 import pytest
-from unittest.mock import Mock, patch, ANY
+from unittest.mock import Mock, patch, ANY, MagicMock
 
 from cephadm.serve import CephadmServe
 from cephadm.services.service_registry import service_registry
@@ -1187,6 +1187,58 @@ class TestMonitoring:
                     use_current_daemon_image=False,
                 )
 
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_no_op_when_other_daemons_remain_on_same_host_host_scope(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        HOST-scope service: when a sibling daemon is still running on the same
+        """
+        cephadm_module._init_cert_mgr()
+        cm = cephadm_module.cert_mgr
+        host = 'host1'
+
+        with with_host(cephadm_module, host):
+            grafana_svc = service_registry.get_service('grafana')
+            svc_name = 'grafana'
+
+            # Seed a HOST-scoped inline cert for this host
+            cm.save_cert(grafana_svc.cert_name, ceph_generated_cert,
+                         host=host, user_made=True, editable=False)
+            cm.save_key(grafana_svc.key_name, ceph_generated_key,
+                        host=host, user_made=True, editable=False)
+
+            assert cm.get_cert(grafana_svc.cert_name, host=host) is not None
+            assert cm.get_key(grafana_svc.key_name, host=host) is not None
+
+            mock_entry = MagicMock()
+            mock_entry.spec = MagicMock()
+            mock_entry.spec.ssl = True
+            mock_entry.spec.certificate_source = 'inline'
+            mock_spec_store = MagicMock()
+            mock_spec_store.__contains__ = MagicMock(return_value=True)
+            mock_spec_store.__getitem__ = MagicMock(return_value=mock_entry)
+
+            daemon = MagicMock()
+            daemon.daemon_type = 'grafana'
+            daemon.daemon_id = 'host1.0'
+            daemon.hostname = host
+            daemon.name.return_value = f'grafana.{daemon.daemon_id}'
+            daemon.service_name.return_value = svc_name
+
+            # Sibling still on the same host
+            sibling = MagicMock()
+            sibling.hostname = host
+            sibling.name.return_value = 'grafana.host1.1'
+
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon, sibling]):
+                grafana_svc.post_remove(daemon, is_failed_deploy=False)
+
+            # Cert must still be present — sibling is still on host1
+            assert cm.get_cert(grafana_svc.cert_name, host=host) is not None
+            assert cm.get_key(grafana_svc.key_name, host=host) is not None
+
     @patch("cephadm.serve.CephadmServe._run_cephadm")
     @patch("cephadm.module.CephadmOrchestrator.get_mgr_ip", lambda _: '1::4')
     @patch("cephadm.module.CephadmOrchestrator.get_fqdn", lambda a, b: 'host_fqdn')
@@ -1693,7 +1745,7 @@ class TestMonitoring:
                                     '    editable: false\n'
                                     '    options:\n'
                                     "      path: '/etc/grafana/provisioning/dashboards'"
-                            }}, ['secure_monitoring_stack:False'])
+                            }}, ['certificate_source: cephadm-signed', 'secure_monitoring_stack:False'])
 
     @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
     def test_grafana_no_anon_access(self, cephadm_module: CephadmOrchestrator):
@@ -1762,7 +1814,7 @@ class TestMonitoring:
                                     '    editable: false\n'
                                     '    options:\n'
                                     "      path: '/etc/grafana/provisioning/dashboards'"
-                            }}, ['secure_monitoring_stack:False'])
+                            }}, ['certificate_source: cephadm-signed', 'secure_monitoring_stack:False'])
 
     @patch("cephadm.serve.CephadmServe._run_cephadm")
     def test_monitoring_ports(self, _run_cephadm, cephadm_module: CephadmOrchestrator):
index 376fdea25e30cc834b600cec94fe7d796c19be73..faf45146fef042f1936292b1438be54951dba396 100644 (file)
@@ -259,7 +259,7 @@ class TestNFS:
                                 'balance static-rr\n    '
                                 'option httpchk HEAD / HTTP/1.0\n    '
                                 'server '
-                                + haproxy_generated_conf[1][0] + ' 1.2.3.7:80 check weight 100 inter 2s\n'
+                                + haproxy_generated_conf[1][1] + ' 1.2.3.7:80 check weight 100 inter 2s\n'
                         }
                 }
                 gen_config_lines = [line.rstrip() for line in haproxy_generated_conf[0]['files']['haproxy.cfg'].splitlines()]
@@ -351,7 +351,7 @@ class TestNFS:
                                 'balance static-rr\n    '
                                 'option httpchk HEAD / HTTP/1.0\n    '
                                 'server '
-                                + haproxy_generated_conf[1][0] + ' 1.2.3.7:80 check weight 100 inter 2s\n'
+                                + haproxy_generated_conf[1][1] + ' 1.2.3.7:80 check weight 100 inter 2s\n'
                         }
                 }
                 gen_config_lines = [line.rstrip() for line in haproxy_generated_conf[0]['files']['haproxy.cfg'].splitlines()]
index f292cad998bbfcce9126383b61e84377c39ca77b..ecf9510885b0a75508f6182dd3397e3a5623e107 100644 (file)
@@ -1,10 +1,19 @@
 
 import pytest
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock
 
+from cephadm.services.service_registry import service_registry
 from cephadm.module import CephadmOrchestrator
-from ceph.deployment.service_spec import RGWSpec
+from ceph.deployment.service_spec import RGWSpec, CertificateSource
 from cephadm.tests.fixtures import with_host, with_service, _run_cephadm
+from cephadm.tlsobject_types import TLSCredentials
+
+
+cephadm_root_ca = """-----BEGIN CERTIFICATE-----\nMIIE7DCCAtSgAwIBAgIUE8b2zZ64geu2ns3Zfn3/4L+Cf6MwDQYJKoZIhvcNAQEL\nBQAwFzEVMBMGA1UEAwwMY2VwaGFkbS1yb290MB4XDTI0MDYyNjE0NDA1M1oXDTM0\nMDYyNzE0NDA1M1owFzEVMBMGA1UEAwwMY2VwaGFkbS1yb290MIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEAsZRJsdtTr9GLG1lWFql5SGc46ldFanNJd1Gl\nqXq5vgZVKRDTmNgAb/XFuNEEmbDAXYIRZolZeYKMHfn0pouPRSel0OsC6/02ZUOW\nIuN89Wgo3IYleCFpkVIumD8URP3hwdu85plRxYZTtlruBaTRH38lssyCqxaOdEt7\nAUhvYhcMPJThB17eOSQ73mb8JEC83vB47fosI7IhZuvXvRSuZwUW30rJanWNhyZq\neS2B8qw2RSO0+77H6gA4ftBnitfsE1Y8/F9Z/f92JOZuSMQXUB07msznPbRJia3f\nueO8gOc32vxd1A1/Qzp14uX34yEGY9ko2lW226cZO29IVUtXOX+LueQttwtdlpz8\ne6Npm09pXhXAHxV/OW3M28MdXmobIqT/m9MfkeAErt5guUeC5y8doz6/3VQRjFEn\nRpN0WkblgnNAQ3DONPc+Qd9Fi/wZV2X7bXoYpNdoWDsEOiE/eLmhG1A2GqU/mneP\nzQ6u79nbdwTYpwqHpa+PvusXeLfKauzI8lLUJotdXy9EK8iHUofibB61OljYye6B\nG3b8C4QfGsw8cDb4APZd/6AZYyMx/V3cGZ+GcOV7WvsC8k7yx5Uqasm/kiGQ3EZo\nuNenNEYoGYrjb8D/8QzqNUTwlEh27/ps80tO7l2GGTvWVZL0PRZbmLDvO77amtOf\nOiRXMoUCAwEAAaMwMC4wGwYDVR0RBBQwEocQAAAAAAAAAAAAAAAAAAAAATAPBgNV\nHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAxwzX5AhYEWhTV4VUwUj5+\nqPdl4Q2tIxRokqyE+cDxoSd+6JfGUefUbNyBxDt0HaBq8obDqqrbcytxnn7mpnDu\nhtiauY+I4Amt7hqFOiFA4cCLi2mfok6g2vL53tvhd9IrsfflAU2wy7hL76Ejm5El\nA+nXlkJwps01Whl9pBkUvIbOn3pXX50LT4hb5zN0PSu957rjd2xb4HdfuySm6nW4\n4GxtVWfmGA6zbC4XMEwvkuhZ7kD2qjkAguGDF01uMglkrkCJT3OROlNBuSTSBGqt\ntntp5VytHvb7KTF7GttM3ha8/EU2KYaHM6WImQQTrOfiImAktOk4B3lzUZX3HYIx\n+sByO4P4dCvAoGz1nlWYB2AvCOGbKf0Tgrh4t4jkiF8FHTXGdfvWmjgi1pddCNAy\nn65WOCmVmLZPERAHOk1oBwqyReSvgoCFo8FxbZcNxJdlhM0Z6hzKggm3O3Dl88Xl\n5euqJjh2STkBW8Xuowkg1TOs5XyWvKoDFAUzyzeLOL8YSG+gXV22gPTUaPSVAqdb\nwd0Fx2kjConuC5bgTzQHs8XWA930U3XWZraj21Vaa8UxlBLH4fUro8H5lMSYlZNE\nJHRNW8BkznAClaFSDG3dybLsrzrBFAu/Qb5zVkT1xyq0YkepGB7leXwq6vjWA5Pw\nmZbKSphWfh0qipoqxqhfkw==\n-----END CERTIFICATE-----\n"""
+
+ceph_generated_cert = """-----BEGIN CERTIFICATE-----\nMIICxjCCAa4CEQDIZSujNBlKaLJzmvntjukjMA0GCSqGSIb3DQEBDQUAMCExDTAL\nBgNVBAoMBENlcGgxEDAOBgNVBAMMB2NlcGhhZG0wHhcNMjIwNzEzMTE0NzA3WhcN\nMzIwNzEwMTE0NzA3WjAhMQ0wCwYDVQQKDARDZXBoMRAwDgYDVQQDDAdjZXBoYWRt\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyyMe4DMA+MeYK7BHZMHB\nq7zjliEOcNgxomjU8qbf5USF7Mqrf6+/87XWqj4pCyAW8x0WXEr6A56a+cmBVmt+\nqtWDzl020aoId6lL5EgLLn6/kMDCCJLq++Lg9cEofMSvcZh+lY2f+1p+C+00xent\nrLXvXGOilAZWaQfojT2BpRnNWWIFbpFwlcKrlg2G0cFjV5c1m6a0wpsQ9JHOieq0\nSvwCixajwq3CwAYuuiU1wjI4oJO4Io1+g8yB3nH2Mo/25SApCxMXuXh4kHLQr/T4\n4hqisvG4uJYgKMcSIrWj5o25mclByGi1UI/kZkCUES94i7Z/3ihx4Bad0AMs/9tw\nFwIDAQABMA0GCSqGSIb3DQEBDQUAA4IBAQAf+pwz7Gd7mDwU2LY0TQXsK6/8KGzh\nHuX+ErOb8h5cOAbvCnHjyJFWf6gCITG98k9nxU9NToG0WYuNm/max1y/54f0dtxZ\npUo6KSNl3w6iYCfGOeUIj8isi06xMmeTgMNzv8DYhDt+P2igN6LenqWTVztogkiV\nxQ5ZJFFLEw4sN0CXnrZX3t5ruakxLXLTLKeE0I91YJvjClSBGkVJq26wOKQNHMhx\npWxeydQ5EgPZY+Aviz5Dnxe8aB7oSSovpXByzxURSabOuCK21awW5WJCGNpmqhWK\nZzACBDEstccj57c4OGV0eayHJRsluVr2e9NHRINZA3qdB37e6gsI1xHo\n-----END CERTIFICATE-----\n"""
+
+ceph_generated_key = """-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDLIx7gMwD4x5gr\nsEdkwcGrvOOWIQ5w2DGiaNTypt/lRIXsyqt/r7/ztdaqPikLIBbzHRZcSvoDnpr5\nyYFWa36q1YPOXTbRqgh3qUvkSAsufr+QwMIIkur74uD1wSh8xK9xmH6VjZ/7Wn4L\n7TTF6e2ste9cY6KUBlZpB+iNPYGlGc1ZYgVukXCVwquWDYbRwWNXlzWbprTCmxD0\nkc6J6rRK/AKLFqPCrcLABi66JTXCMjigk7gijX6DzIHecfYyj/blICkLExe5eHiQ\nctCv9PjiGqKy8bi4liAoxxIitaPmjbmZyUHIaLVQj+RmQJQRL3iLtn/eKHHgFp3Q\nAyz/23AXAgMBAAECggEAVoTB3Mm8azlPlaQB9GcV3tiXslSn+uYJ1duCf0sV52dV\nBzKW8s5fGiTjpiTNhGCJhchowqxoaew+o47wmGc2TvqbpeRLuecKrjScD0GkCYyQ\neM2wlshEbz4FhIZdgS6gbuh9WaM1dW/oaZoBNR5aTYo7xYTmNNeyLA/jO2zr7+4W\n5yES1lMSBXpKk7bDGKYY4bsX2b5RLr2Grh2u2bp7hoLABCEvuu8tSQdWXLEXWpXo\njwmV3hc6tabypIa0mj2Dmn2Dmt1ppSO0AZWG/WAizN3f4Z0r/u9HnbVrVmh0IEDw\n3uf2LP5o3msG9qKCbzv3lMgt9mMr70HOKnJ8ohMSKQKBgQDLkNb+0nr152HU9AeJ\nvdz8BeMxcwxCG77iwZphZ1HprmYKvvXgedqWtS6FRU+nV6UuQoPUbQxJBQzrN1Qv\nwKSlOAPCrTJgNgF/RbfxZTrIgCPuK2KM8I89VZv92TSGi362oQA4MazXC8RAWjoJ\nSu1/PHzK3aXOfVNSLrOWvIYeZQKBgQD/dgT6RUXKg0UhmXj7ExevV+c7oOJTDlMl\nvLngrmbjRgPO9VxLnZQGdyaBJeRngU/UXfNgajT/MU8B5fSKInnTMawv/tW7634B\nw3v6n5kNIMIjJmENRsXBVMllDTkT9S7ApV+VoGnXRccbTiDapBThSGd0wri/CuwK\nNWK1YFOeywKBgEDyI/XG114PBUJ43NLQVWm+wx5qszWAPqV/2S5MVXD1qC6zgCSv\nG9NLWN1CIMimCNg6dm7Wn73IM7fzvhNCJgVkWqbItTLG6DFf3/DPODLx1wTMqLOI\nqFqMLqmNm9l1Nec0dKp5BsjRQzq4zp1aX21hsfrTPmwjxeqJZdioqy2VAoGAXR5X\nCCdSHlSlUW8RE2xNOOQw7KJjfWT+WAYoN0c7R+MQplL31rRU7dpm1bLLRBN11vJ8\nMYvlT5RYuVdqQSP6BkrX+hLJNBvOLbRlL+EXOBrVyVxHCkDe+u7+DnC4epbn+N8P\nLYpwqkDMKB7diPVAizIKTBxinXjMu5fkKDs5n+sCgYBbZheYKk5M0sIxiDfZuXGB\nkf4mJdEkTI1KUGRdCwO/O7hXbroGoUVJTwqBLi1tKqLLarwCITje2T200BYOzj82\nqwRkCXGtXPKnxYEEUOiFx9OeDrzsZV00cxsEnX0Zdj+PucQ/J3Cvd0dWUspJfLHJ\n39gnaegswnz9KMQAvzKFdg==\n-----END PRIVATE KEY-----\n"""
 
 
 class TestRGWService:
@@ -61,3 +70,429 @@ class TestRGWService:
                     'key': 'rgw_run_sync_thread',
                 })
                 assert f == ('false' if disable_sync_traffic else 'true')
+
+    def _make_rgw_post_remove_fixtures(self, cephadm_module, host='host1', ssl=True, certificate_source='inline'):
+        """
+        Returns (cm, svc_name, spec, daemon, mock_spec_store) with all common
+        setup done.  The caller is responsible for patching spec_store and
+        cache.get_daemons_by_service.
+        """
+        cephadm_module._init_cert_mgr()
+        cm = cephadm_module.cert_mgr
+
+        spec = RGWSpec(
+            service_id='foo',
+            ssl=ssl,
+            certificate_source=certificate_source,
+            rgw_frontend_type='beast',
+        )
+        svc_name = spec.service_name()  # 'rgw.foo'
+
+        daemon = MagicMock()
+        daemon.daemon_type = 'rgw'
+        daemon.daemon_id = f'foo.{host}.0'
+        daemon.hostname = host
+        daemon.name.return_value = f'rgw.{daemon.daemon_id}'
+        daemon.service_name.return_value = svc_name
+
+        mock_entry = MagicMock()
+        mock_entry.spec = spec
+        mock_spec_store = MagicMock()
+        mock_spec_store.__contains__ = MagicMock(return_value=True)
+        mock_spec_store.__getitem__ = MagicMock(return_value=mock_entry)
+
+        return cm, svc_name, spec, daemon, mock_spec_store
+
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_no_op_when_requires_certificates_is_false(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        When requires_certificates is False, post_remove() must return
+        immediately without touching cert_mgr at all.
+        """
+        _, svc_name, _, daemon, _ = self._make_rgw_post_remove_fixtures(cephadm_module)
+
+        rgw_svc = service_registry.get_service('rgw')
+
+        with patch.object(type(rgw_svc), 'requires_certificates',
+                          new_callable=lambda: property(lambda self: False)):
+            with patch.object(rgw_svc.mgr.cert_mgr,
+                              'rm_inline_saved_cert_key_pair') as rm_mock:
+                rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                rm_mock.assert_not_called()
+
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_no_op_when_svc_not_in_spec_store(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        When the service is not found in spec_store, post_remove() must return
+        immediately without touching cert_mgr.
+        """
+        cephadm_module._init_cert_mgr()
+
+        spec = RGWSpec(service_id='foo', ssl=True, rgw_frontend_type='beast')
+        daemon = MagicMock()
+        daemon.daemon_type = 'rgw'
+        daemon.daemon_id = 'foo.host1.0'
+        daemon.hostname = 'host1'
+        daemon.name.return_value = 'rgw.foo.host1.0'
+        daemon.service_name.return_value = spec.service_name()
+
+        # spec_store explicitly does NOT contain the service
+        mock_spec_store = MagicMock()
+        mock_spec_store.__contains__ = MagicMock(return_value=False)
+
+        rgw_svc = service_registry.get_service('rgw')
+
+        with with_host(cephadm_module, 'host1'):
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cert_mgr,
+                              'rm_inline_saved_cert_key_pair') as rm_mock:
+                rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                rm_mock.assert_not_called()
+
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_no_op_when_other_daemons_remain_on_same_host(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        When another daemon of the same service is still running on the same
+        host, post_remove() must NOT clean up certs for that host yet.
+        """
+        host = 'host1'
+        cm, svc_name, _, daemon, mock_spec_store = \
+            self._make_rgw_post_remove_fixtures(cephadm_module, host=host)
+
+        # A sibling daemon still on the same host
+        sibling = MagicMock()
+        sibling.hostname = host
+        sibling.name.return_value = 'rgw.foo.host1.1'   # different name
+
+        rgw_svc = service_registry.get_service('rgw')
+
+        with with_host(cephadm_module, host):
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon, sibling]), \
+                 patch.object(cm, 'rm_inline_saved_cert_key_pair') as rm_mock:
+                rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                rm_mock.assert_not_called()
+
+    # SERVICE-scope cleanup branches
+
+    @patch('cephadm.cert_mgr.CertMgr.get_root_ca', lambda instance: cephadm_root_ca)
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_inline_cleanup_called_when_last_daemon_in_service(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        When the removed daemon is the very last one in the service (SERVICE
+        scope), rm_inline_saved_cert_key_pair() must be called with
+        service_name=svc_name and host=None.
+        """
+        host = 'host1'
+        cm, svc_name, _, daemon, mock_spec_store = \
+            self._make_rgw_post_remove_fixtures(cephadm_module, host=host)
+
+        rgw_svc = service_registry.get_service('rgw')
+
+        with with_host(cephadm_module, host):
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon]):   # only the daemon being removed
+                with patch.object(cm, 'rm_inline_saved_cert_key_pair') as rm_mock:
+                    rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                    rm_mock.assert_called_once_with(
+                        rgw_svc.cert_name,
+                        rgw_svc.key_name,
+                        service_name=svc_name,
+                        host=None,
+                        ca_cert_name=rgw_svc.ca_cert_name,
+                    )
+
+    @patch('cephadm.cert_mgr.CertMgr.get_root_ca', lambda instance: cephadm_root_ca)
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_inline_cleanup_skipped_when_other_daemons_on_other_hosts(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        SERVICE scope: when other daemons of the same service still exist on
+        OTHER hosts, the service-level cert must NOT be removed yet
+        (other_daemons_in_service=True → early return from _cleanup).
+        """
+        host = 'host1'
+        cm, svc_name, _, daemon, mock_spec_store = \
+            self._make_rgw_post_remove_fixtures(cephadm_module, host=host)
+
+        # A peer daemon on a different host
+        peer = MagicMock()
+        peer.hostname = 'host2'
+        peer.name.return_value = 'rgw.foo.host2.0'
+
+        rgw_svc = service_registry.get_service('rgw')
+
+        with with_host(cephadm_module, host):
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon, peer]):
+                with patch.object(cm, 'rm_inline_saved_cert_key_pair') as rm_mock:
+                    rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                    rm_mock.assert_not_called()
+
+    # ssl=False branch
+
+    @patch('cephadm.cert_mgr.CertMgr.get_root_ca', lambda instance: cephadm_root_ca)
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_cert_source_is_none_when_ssl_disabled(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        When spec.ssl=False, cert_source is forced to None inside post_remove().
+        Cleanup should still run (SERVICE scope, last daemon) but cert_source
+        passed to _cleanup_tls_creds_for_host must be None.
+        """
+        host = 'host1'
+        cm, svc_name, _, daemon, mock_spec_store = \
+            self._make_rgw_post_remove_fixtures(cephadm_module, host=host, ssl=False)
+
+        rgw_svc = service_registry.get_service('rgw')
+
+        with with_host(cephadm_module, host):
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon]):
+                with patch.object(cm, 'rm_inline_saved_cert_key_pair') as rm_mock:
+                    rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                    # cleanup still fires (last daemon), cert_source=None doesn't block it
+                    rm_mock.assert_called_once_with(
+                        rgw_svc.cert_name,
+                        rgw_svc.key_name,
+                        service_name=svc_name,
+                        host=None,
+                        ca_cert_name=rgw_svc.ca_cert_name,
+                    )
+
+    # Reference cert-source branch
+
+    @patch('cephadm.cert_mgr.CertMgr.get_root_ca', lambda instance: cephadm_root_ca)
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_reference_source_logs_and_still_cleans(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        When certificate_source='reference', post_remove() must still call
+        rm_inline_saved_cert_key_pair (the reference note is informational
+        only) AND emit the expected INFO log.
+        """
+        host = 'host1'
+        cm, svc_name, _, daemon, mock_spec_store = \
+            self._make_rgw_post_remove_fixtures(
+                cephadm_module, host=host,
+                certificate_source=CertificateSource.REFERENCE.value)
+
+        rgw_svc = service_registry.get_service('rgw')
+
+        with with_host(cephadm_module, host):
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon]):
+                with patch.object(cm, 'rm_inline_saved_cert_key_pair') as rm_mock, \
+                     patch('cephadm.services.cephadmservice.logger') as log_mock:
+                    rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                    rm_mock.assert_called_once()
+                    # The "reference; user-provided" info log must have fired
+                    assert any(
+                        'reference' in str(call_args)
+                        for call_args in log_mock.info.call_args_list
+                    )
+
+    @patch('cephadm.cert_mgr.CertMgr.get_root_ca', lambda instance: cephadm_root_ca)
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_cleans_cephadm_signed_leftovers_for_host(self, cephadm_module: CephadmOrchestrator):
+        """
+        Ensures RGW service post_remove() removes cephadm-signed
+        cert/key leftovers for (service, host) even if the CURRENT cert source
+        is not cephadm-signed.
+        """
+        cephadm_module._init_cert_mgr()
+        cm = cephadm_module.cert_mgr
+
+        host = 'host1'
+
+        with with_host(cephadm_module, host):
+            spec = RGWSpec(
+                service_id="foo",
+                ssl=True,
+                certificate_source="inline",
+                rgw_frontend_type="beast",
+            )
+            svc_name = spec.service_name()  # typically "rgw.foo"
+
+            # Register the self-signed cert/key pair
+            cm.register_self_signed_cert_key_pair(svc_name)
+
+            # Build a mock spec_store that satisfies the two access patterns
+            # post_remove() needs:
+            #   if svc_name not in self.mgr.spec_store: return
+            #   spec = self.mgr.spec_store[svc_name].spec
+            mock_entry = MagicMock()
+            mock_entry.spec = spec
+
+            mock_spec_store = MagicMock()
+            mock_spec_store.__contains__ = MagicMock(return_value=True)
+            mock_spec_store.__getitem__ = MagicMock(return_value=mock_entry)
+
+            # Minimal daemon mock used by post_remove()
+            daemon = MagicMock()
+            daemon.daemon_type = 'rgw'
+            daemon.daemon_id = 'foo.host1.0'
+            daemon.hostname = host
+            daemon.name.return_value = f'rgw.{daemon.daemon_id}'
+            daemon.service_name.return_value = svc_name
+
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service', return_value=[daemon]):
+
+                # Seed cephadm-signed leftovers for this host
+                cm.save_self_signed_cert_key_pair(
+                    svc_name,
+                    TLSCredentials(ceph_generated_cert, ceph_generated_key),
+                    host=host,
+                )
+
+                cert_name = cm.self_signed_cert(svc_name)
+                key_name = cm.self_signed_key(svc_name)
+
+                # Sanity: leftovers exist pre-cleanup
+                assert cm.get_cert(cert_name, host=host) is not None
+                assert cm.get_key(key_name, host=host) is not None
+
+                # Get RGW service instance
+                rgw_svc = service_registry.get_service('rgw')
+
+                # Ensure the call site is exercised + cleanup actually happens
+                with patch.object(cm, "try_rm_self_signed_cert_key_pair",
+                                  wraps=cm.try_rm_self_signed_cert_key_pair) as rm_mock:
+                    rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                    rm_mock.assert_called_once_with(svc_name, host)
+
+                # Assert cephadm-signed leftovers are gone
+                assert cm.get_cert(cert_name, host=host) is None
+                assert cm.get_key(key_name, host=host) is None
+
+    @patch('cephadm.cert_mgr.CertMgr.get_root_ca', lambda instance: cephadm_root_ca)
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_cleans_inline_certs_for_last_daemon_in_service(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        Ensures RGW service post_remove() actually removes
+        inline-saved cert/key from the cert store when the last daemon of the
+        service is removed (SERVICE scope → host=None cleanup).
+        """
+        cephadm_module._init_cert_mgr()
+        cm = cephadm_module.cert_mgr
+        host = 'host1'
+
+        with with_host(cephadm_module, host):
+            spec = RGWSpec(
+                service_id='foo',
+                ssl=True,
+                certificate_source='inline',
+                rgw_frontend_type='beast',
+            )
+            svc_name = spec.service_name()  # 'rgw.foo'
+
+            rgw_svc = service_registry.get_service('rgw')
+
+            # Seed inline cert/key for this service (SERVICE scope → service_name=svc_name)
+            cm.save_cert(rgw_svc.cert_name, ceph_generated_cert,
+                         service_name=svc_name, user_made=True, editable=False)
+            cm.save_key(rgw_svc.key_name, ceph_generated_key,
+                        service_name=svc_name, user_made=True, editable=False)
+
+            # Sanity: inline certs exist pre-cleanup
+            assert cm.get_cert(rgw_svc.cert_name, service_name=svc_name) is not None
+            assert cm.get_key(rgw_svc.key_name, service_name=svc_name) is not None
+
+            mock_entry = MagicMock()
+            mock_entry.spec = spec
+            mock_spec_store = MagicMock()
+            mock_spec_store.__contains__ = MagicMock(return_value=True)
+            mock_spec_store.__getitem__ = MagicMock(return_value=mock_entry)
+
+            daemon = MagicMock()
+            daemon.daemon_type = 'rgw'
+            daemon.daemon_id = f'foo.{host}.0'
+            daemon.hostname = host
+            daemon.name.return_value = f'rgw.{daemon.daemon_id}'
+            daemon.service_name.return_value = svc_name
+
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon]):   # only this daemon → last one
+                with patch.object(cm, 'rm_inline_saved_cert_key_pair',
+                                  wraps=cm.rm_inline_saved_cert_key_pair) as rm_mock:
+                    rgw_svc.post_remove(daemon, is_failed_deploy=False)
+                    rm_mock.assert_called_once_with(
+                        rgw_svc.cert_name,
+                        rgw_svc.key_name,
+                        service_name=svc_name,
+                        host=None,
+                        ca_cert_name=rgw_svc.ca_cert_name,
+                    )
+
+            # Assert inline certs are actually gone from the store
+            assert cm.get_cert(rgw_svc.cert_name, service_name=svc_name) is None
+            assert cm.get_key(rgw_svc.key_name, service_name=svc_name) is None
+
+    @patch('cephadm.cert_mgr.CertMgr.get_root_ca', lambda instance: cephadm_root_ca)
+    @patch("cephadm.serve.CephadmServe._run_cephadm", _run_cephadm('{}'))
+    def test_post_remove_preserves_inline_certs_when_other_daemons_remain_in_service(
+            self, cephadm_module: CephadmOrchestrator):
+        """
+        When other daemons of the same service still exist on other hosts,
+        inline certs must NOT be removed from the store (SERVICE scope cert
+        is shared across the whole service).
+        """
+        cephadm_module._init_cert_mgr()
+        cm = cephadm_module.cert_mgr
+        host = 'host1'
+
+        with with_host(cephadm_module, host):
+            spec = RGWSpec(
+                service_id='foo',
+                ssl=True,
+                certificate_source='inline',
+                rgw_frontend_type='beast',
+            )
+            svc_name = spec.service_name()
+
+            rgw_svc = service_registry.get_service('rgw')
+
+            # Seed inline cert/key
+            cm.save_cert(rgw_svc.cert_name, ceph_generated_cert,
+                         service_name=svc_name, user_made=True, editable=False)
+            cm.save_key(rgw_svc.key_name, ceph_generated_key,
+                        service_name=svc_name, user_made=True, editable=False)
+
+            mock_entry = MagicMock()
+            mock_entry.spec = spec
+            mock_spec_store = MagicMock()
+            mock_spec_store.__contains__ = MagicMock(return_value=True)
+            mock_spec_store.__getitem__ = MagicMock(return_value=mock_entry)
+
+            daemon = MagicMock()
+            daemon.daemon_type = 'rgw'
+            daemon.daemon_id = f'foo.{host}.0'
+            daemon.hostname = host
+            daemon.name.return_value = f'rgw.{daemon.daemon_id}'
+            daemon.service_name.return_value = svc_name
+
+            # A peer daemon still running on a different host
+            peer = MagicMock()
+            peer.hostname = 'host2'
+            peer.name.return_value = 'rgw.foo.host2.0'
+
+            with patch.object(cephadm_module, 'spec_store', mock_spec_store), \
+                 patch.object(cephadm_module.cache, 'get_daemons_by_service',
+                              return_value=[daemon, peer]):
+                rgw_svc.post_remove(daemon, is_failed_deploy=False)
+
+            # Inline certs must still be present — the service is still running on host2
+            assert cm.get_cert(rgw_svc.cert_name, service_name=svc_name) is not None
+            assert cm.get_key(rgw_svc.key_name, service_name=svc_name) is not None
index e4283300667436c7faa0d7c552d120d096f9b0ab..e6eb1db2262e754d008483875cb3e245ce8415eb 100644 (file)
@@ -1352,8 +1352,8 @@ class TestCertMgr(object):
 
         deps = CephadmService.get_dependencies(cephadm_module, spec)
         assert 'certificate_source: inline' in deps
-        assert f'ssl_cert: {utils.md5_hash("my-cert-data")}' in deps
-        assert f'ssl_key: {utils.md5_hash("my-key-data")}' in deps
+        assert f'ssl_cert: {utils.config_hash("my-cert-data")}' in deps
+        assert f'ssl_key: {utils.config_hash("my-key-data")}' in deps
 
     def test_grafana_get_dependencies_includes_parent_tls_deps(self, cephadm_module: CephadmOrchestrator):
         """GrafanaService.get_dependencies should include parent TLS deps (certificate_source)