From: Timothy Q Nguyen Date: Thu, 26 Mar 2026 19:16:51 +0000 (-0700) Subject: python-common: multi-line for yaml X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=0a73f686c8734e6545fef48275af56502300a4e7;p=ceph.git python-common: multi-line for yaml Currently when running the command ceph orch ls --export multi-line strings are processed using default interpretation meaning they could be printed with quotations instead of the expected | . This causes a hassle for users who want to copy their specifications to new yaml files as they have to manually modify the yaml to remove the quotes and add | . My code fixes this by modifying the yaml representer to check for multi-line strings and wrapping them in a YamlLiteralString class which will mark the multi-line string to be processed with | . I've also added unit tests. Signed-off-by: Timothy Q Nguyen --- diff --git a/src/python-common/ceph/deployment/service_spec.py b/src/python-common/ceph/deployment/service_spec.py index 2f6d6f944d2f..9102114a0a80 100644 --- a/src/python-common/ceph/deployment/service_spec.py +++ b/src/python-common/ceph/deployment/service_spec.py @@ -94,6 +94,19 @@ def handle_type_error(method: FuncT) -> FuncT: return cast(FuncT, inner) +class YamlLiteralString(str): + """ + Class used as marker for yaml representer to properly format + multi-line strings for yaml export + """ + @staticmethod + def represent_as_literal(dumper: 'yaml.SafeDumper', data: str) -> Any: + return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|') + + +yaml.add_representer(YamlLiteralString, YamlLiteralString.represent_as_literal) + + class HostPlacementSpec(NamedTuple): hostname: str network: str @@ -1321,7 +1334,15 @@ class ServiceSpec(object): @staticmethod def yaml_representer(dumper: 'yaml.SafeDumper', data: 'ServiceSpec') -> Any: - return dumper.represent_dict(cast(Mapping, data.to_json().items())) + json_data = data.to_json() + spec = json_data.get('spec', {}) + + # Marking multi-line strings with the YamlLiteralString class + for key, value in spec.items(): + if isinstance(value, str) and '\n' in value: + spec[key] = YamlLiteralString(value) + + return dumper.represent_dict(cast(Mapping, json_data.items())) yaml.add_representer(ServiceSpec, ServiceSpec.yaml_representer) diff --git a/src/python-common/ceph/tests/test_service_spec.py b/src/python-common/ceph/tests/test_service_spec.py index f4628051530e..559d082340ec 100644 --- a/src/python-common/ceph/tests/test_service_spec.py +++ b/src/python-common/ceph/tests/test_service_spec.py @@ -18,6 +18,7 @@ from ceph.deployment.service_spec import ( PrometheusSpec, RGWSpec, ServiceSpec, + YamlLiteralString, ) from ceph.deployment.drive_group import DriveGroupSpec from ceph.deployment.hostspec import SpecValidationError @@ -1422,3 +1423,37 @@ def test_non_osd_services_do_not_get_default_termination_if_not_provided(spec_da spec_section = j.get('spec', {}) assert 'termination_grace_period_seconds' not in spec_section + +def test_yaml_literal_string_class_represents_multiline_strings_as_literal(): + multiline_string = "test1\ntest2\n" + dumped = yaml.dump(YamlLiteralString(multiline_string)) + + assert dumped.startswith('|') + +def test_yaml_representer_can_handle_multiline_strings_for_export(): + spec_data = """ +service_type: iscsi +service_id: iscsi +placement: + label: iscsi +spec: + pool: testpool + ssl_cert: | + -----BEGIN CERTIFICATE----- + FILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLER + FILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLER + -----END CERTIFICATE----- + ssl_key: | + -----BEGIN PRIVATE KEY----- + FILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLER + FILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLERFILLER + -----END PRIVATE KEY----- +""" + + data = yaml.safe_load(spec_data) + spec_obj = ServiceSpec.from_json(data) + + dumped = yaml.dump(spec_obj, default_flow_style=False) + + assert 'ssl_cert: |' in dumped + assert 'ssl_key: |' in dumped