Skip to content

Commit

Permalink
Convert NFS config to new api.
Browse files Browse the repository at this point in the history
Added a common 'TcpPort' type.

Fix tcp port exclude option.
Convert bindip_choices to new api.

Convert NFS share CRUD to new api.
Adds a new pydantic type: Dir
  • Loading branch information
mgrimesix committed Jan 10, 2025
1 parent 24e4106 commit 44c4ff3
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 126 deletions.
1 change: 1 addition & 0 deletions src/middlewared/middlewared/api/base/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .fc import * # noqa
from .filesystem import * # noqa
from .iscsi import * # noqa
from .network import * # noqa
from .string import * # noqa
from .user import * # noqa
23 changes: 22 additions & 1 deletion src/middlewared/middlewared/api/base/types/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
from typing import Annotated

from pydantic import Field
from pydantic.functional_validators import AfterValidator

__all__ = ["UnixPerm"]
__all__ = ["UnixPerm", "Dir"]


def validate_unix_perm(value: str) -> str:
Expand All @@ -18,3 +20,22 @@ def validate_unix_perm(value: str) -> str:


UnixPerm = Annotated[str, AfterValidator(validate_unix_perm)]


def validate_host_path(value: str) -> str:
if value is None:
return

if value:
if not os.path.exists(value):
raise ValueError(
'Path does not exist (underlying dataset may be locked or the path is just missing).',
)
else:
if not os.path.isdir(value):
raise ValueError('This path is not a directory.')

return value


Dir = Annotated[str, Field(min_length=1), AfterValidator(validate_host_path)]
20 changes: 20 additions & 0 deletions src/middlewared/middlewared/api/base/types/network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import functools
from typing import Annotated
from pydantic import Field

__all__ = ["TcpPort", "exclude_tcp_ports"]


def _exclude_port_validation(value: int, *, ports: list[int]) -> int:
if value in ports:
raise ValueError(
f'{value} is a reserved for internal use. Please select another value.'
)
return value


def exclude_tcp_ports(ports: list[int]):
return functools.partial(_exclude_port_validation, ports=ports or [])


TcpPort = Annotated[int, Field(ge=1, le=65535)]
1 change: 1 addition & 0 deletions src/middlewared/middlewared/api/v25_04_0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .keychain import * # noqa
from .k8s_to_docker import * # noqa
from .netdata import * # noqa
from .nfs import * # noqa
from .pool import * # noqa
from .pool_resilver import * # noqa
from .pool_scrub import * # noqa
Expand Down
164 changes: 164 additions & 0 deletions src/middlewared/middlewared/api/v25_04_0/nfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import re
from typing import Annotated, Literal

from pydantic import (
ConfigDict, Field, IPvAnyAddress, IPvAnyNetwork,
AfterValidator, field_validator
)

from middlewared.api.base import (
BaseModel, Excluded, excluded_field, ForUpdateMetaclass, NonEmptyString,
single_argument_args, single_argument_result,
TcpPort, exclude_tcp_ports, Dir
)

__all__ = ["NfsEntry",
"NfsUpdateArgs", "NfsUpdateResult",
"NfsBindipChoicesArgs", "NfsBindipChoicesResult",
"NfsShareEntry",
"NfsShareCreateArgs", "NfsShareCreateResult",
"NfsShareUpdateArgs", "NfsShareUpdateResult",
"NfsShareDeleteArgs", "NfsShareDeleteResult"]

NFS_protocols = Literal["NFSV3", "NFSV4"]
NFS_RDMA_DEFAULT_PORT = 20049
EXCLUDED_PORTS = [NFS_RDMA_DEFAULT_PORT]


class NfsEntry(BaseModel):
id: int
servers: Annotated[int | None, Field(ge=1, le=256)]
""" Specify the number of nfsd. Default: Number of nfsd is equal number of CPU. """
allow_nonroot: bool
""" Allow non-root mount requests. This equates to 'insecure' share option. """
protocols: list[NFS_protocols]
""" Specify supported NFS protocols: NFSv3, NFSv4 or both can be listed. """
v4_krb: bool
""" Force Kerberos authentication on NFS shares. """
v4_domain: str
""" Specify a DNS domain (NFSv4 only) """
# NOTE: Using IPvAnyAddress in bindip will generate a JSON serialization error
bindip: list[NonEmptyString] = Field(default_factory=[])
""" Limit the server IP addresses available for NFS """
mountd_port: Annotated[TcpPort | None, AfterValidator(exclude_tcp_ports(EXCLUDED_PORTS))]
""" Specify the mountd port binding """
rpcstatd_port: Annotated[TcpPort | None, AfterValidator(exclude_tcp_ports(EXCLUDED_PORTS))]
""" Specify the rpc.statd port binding """
rpclockd_port: Annotated[TcpPort | None, AfterValidator(exclude_tcp_ports(EXCLUDED_PORTS))]
""" Specify the rpc.lockd port binding """
mountd_log: bool
""" Enable or disable mountd logging """
statd_lockd_log: bool
""" Enable or disable statd and lockd logging """
v4_krb_enabled: bool
""" Status of NFSv4 authentication requirement (status only) """
userd_manage_gids: bool
""" Enable to allow server to manage gids """
keytab_has_nfs_spn: bool
""" Report status of NFS Principal Name in keytab (status only)"""
managed_nfsd: bool
""" Report status of 'servers' field.
If True the number of nfsd are managed by the server. (status only)"""
rdma: bool
""" Enable or disable NFS over RDMA. Requires RDMA capable NIC """

@field_validator('bindip')
@classmethod
def check_bind_ip(cls, field_value: list):
""" Custom validator for IP addresses to avoid JSON serialization errors """
all(isinstance(v, IPvAnyAddress) for v in field_value)
return field_value


@single_argument_args('nfs_update')
class NfsUpdateArgs(NfsEntry, metaclass=ForUpdateMetaclass):
id: Excluded = excluded_field()
managed_nfsd: Excluded = excluded_field()
v4_krb_enabled: Excluded = excluded_field()
keytab_has_nfs_spn: Excluded = excluded_field()


class NfsUpdateResult(BaseModel):
result: NfsEntry


class NfsBindipChoicesArgs(BaseModel):
pass


@single_argument_result
class NfsBindipChoicesResult(BaseModel):
""" Return a dictionary of IP addresses """
model_config = ConfigDict(extra='allow')


class NfsShareCreate(BaseModel):
path: Dir
aliases: list[Dir] = []
comment: str = ""
networks: list[IPvAnyNetwork] = []
hosts: list[str] = []
ro: bool = False
maproot_user: str | None = None
maproot_group: str | None = None
mapall_user: str | None = None
mapall_group: str | None = None
security: list[Literal["SYS", "KRB5", "KRB5I", "KRB5P"]] = []
enabled: bool = False

@field_validator('networks', 'hosts')
@classmethod
def check_unique(cls, field_value: list):
""" Custom validator to confirm unique entries """
s = set()
not_unique = []
for i, v in enumerate(field_value):
if v in s:
# verrors.add(f"{self.name}.{i}", "This value is not unique.")
not_unique.append(v)
s.add(v)
if not_unique:
raise ValueError(f"Entries must be unique, the following are not: {not_unique}")

return field_value

@field_validator('hosts')
@classmethod
def no_spaces_or_quotes(cls, field_value: list):
""" Custom validator for host field: No spaces or quotes """
regex = re.compile(r'.*[\s"]')
not_valid = []
for v in field_value:
if v is not None and regex.match(v):
not_valid.append(v)
if not_valid:
raise ValueError(f"Cannot contain spaces or quotes: {not_valid}")


class NfsShareEntry(NfsShareCreate):
id: int
locked: bool


class NfsShareCreateArgs(BaseModel):
data: NfsShareCreate


class NfsShareCreateResult(BaseModel):
result: NfsShareEntry


class NfsShareUpdateArgs(NfsShareCreate, metaclass=ForUpdateMetaclass):
id: int


class NfsShareUpdateResult(BaseModel):
result: NfsShareEntry


class NfsShareDeleteArgs(BaseModel):
id: int


class NfsShareDeleteResult(BaseModel):
result: Literal[True]
Loading

0 comments on commit 44c4ff3

Please sign in to comment.