-from typing import Dict, List, Optional, Union, cast
+from typing import Dict, List, Optional, Tuple, Union, cast
+import errno
import json
import yaml
from ceph.deployment.service_spec import PlacementSpec
+from object_format import ErrorResponseBase
from . import resourcelib, validation
from .enums import (
return err
+class InvalidInputError(ValueError, ErrorResponseBase):
+ summary_max = 1024
+
+ def __init__(self, msg: str, content: str) -> None:
+ super().__init__(msg)
+ self.content = content
+
+ def to_simplified(self) -> Simplified:
+ return {
+ 'input': self.content[: self.summary_max],
+ 'truncated_input': len(self.content) > self.summary_max,
+ 'msg': str(self),
+ 'success': False,
+ }
+
+ def format_response(self) -> Tuple[int, str, str]:
+ data = json.dumps(self.to_simplified())
+ return -errno.EINVAL, data, "Invalid input"
+
+
class _RBase:
# mypy doesn't currently (well?) support class decorators adding methods
# so we use a base class to add this method to all our resource classes.
]
-def load_text(blob: str) -> List[SMBResource]:
+def load_text(
+ blob: str, *, input_sample_max: int = 1024
+) -> List[SMBResource]:
"""Given JSON or YAML return a list of SMBResource objects deserialized
from the input.
"""
+ json_err = None
try:
- data = yaml.safe_load(blob)
- except ValueError:
- pass
- try:
+ # apparently JSON is not always as strict subset of YAML
+ # therefore trying to parse as JSON first is not a waste:
+ # https://john-millikin.com/json-is-not-a-yaml-subset
data = json.loads(blob)
- except ValueError:
- pass
- return load(data)
+ except ValueError as err:
+ json_err = err
+ try:
+ data = yaml.safe_load(blob) if json_err else data
+ except (ValueError, yaml.parser.ParserError) as err:
+ raise InvalidInputError(str(err), blob) from err
+ if not isinstance(data, (list, dict)):
+ raise InvalidInputError("input must be an object or list", blob)
+ return load(cast(Simplified, data))
def load(data: Simplified) -> List[SMBResource]:
assert share.login_control[3].name == 'delbard'
assert share.login_control[3].category == enums.LoginCategory.USER
assert share.login_control[3].access == enums.LoginAccess.NONE
+
+
+@pytest.mark.parametrize(
+ "params",
+ [
+ # single share json
+ {
+ "txt": """
+{
+ "resource_type": "ceph.smb.share",
+ "cluster_id": "foo",
+ "share_id": "bar",
+ "cephfs": {"volume": "zippy", "path": "/"}
+}
+""",
+ 'simplified': [
+ {
+ 'resource_type': 'ceph.smb.share',
+ 'cluster_id': 'foo',
+ 'share_id': 'bar',
+ 'intent': 'present',
+ 'name': 'bar',
+ 'cephfs': {
+ 'volume': 'zippy',
+ 'path': '/',
+ 'provider': 'samba-vfs',
+ },
+ 'browseable': True,
+ 'readonly': False,
+ }
+ ],
+ },
+ # single share yaml
+ {
+ "txt": """
+resource_type: ceph.smb.share
+cluster_id: foo
+share_id: bar
+cephfs: {volume: zippy, path: /}
+""",
+ 'simplified': [
+ {
+ 'resource_type': 'ceph.smb.share',
+ 'cluster_id': 'foo',
+ 'share_id': 'bar',
+ 'intent': 'present',
+ 'name': 'bar',
+ 'cephfs': {
+ 'volume': 'zippy',
+ 'path': '/',
+ 'provider': 'samba-vfs',
+ },
+ 'browseable': True,
+ 'readonly': False,
+ }
+ ],
+ },
+ # invalid share yaml
+ {
+ "txt": """
+resource_type: ceph.smb.share
+""",
+ 'exc_type': ValueError,
+ 'error': 'missing',
+ },
+ # invalid input
+ {
+ "txt": """
+:
+""",
+ 'exc_type': ValueError,
+ 'error': 'parsing',
+ },
+ # invalid json, but useless yaml
+ {
+ "txt": """
+slithy
+""",
+ 'exc_type': ValueError,
+ 'error': 'input',
+ },
+ ],
+)
+def test_load_text(params):
+ if 'simplified' in params:
+ loaded = smb.resources.load_text(params['txt'])
+ assert params['simplified'] == [r.to_simplified() for r in loaded]
+ else:
+ with pytest.raises(params['exc_type'], match=params['error']):
+ smb.resources.load_text(params['txt'])