-
Notifications
You must be signed in to change notification settings - Fork 494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NAS-131665 / 25.04 / Convert NFS config to new api. #15336
base: master
Are you sure you want to change the base?
Conversation
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 Remove blocking calls in schema. Fixup remaining bits.
@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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will not do anything except evaluate to False
. Instead, we should define bindip: list[IPvAnyAddress] = []
(no field validator needed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bindip: list[IPvAnyAddress] = []
introduces a different problem. A bug/feature in pydantic.
See the inline comment I added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the bug?
>>> class A(BaseModel):
... attr: list[IPvAnyAddress] = []
...
>>> a=A()
>>> a.model_dump()
{'attr': []}
>>> a=A(attr=['127.0.0.1', '192.68.0.1'])
>>> a.model_dump()
{'attr': [IPv4Address('127.0.0.1'), IPv4Address('192.68.0.1')]}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a JSON serialization error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... that's a deeper issue then. We should be json serializing in our api, model_dump_json()
converts IPvAnyAddress
to string, for instance.
""" Specify a DNS domain (NFSv4 only) """ | ||
bindip: list[NonEmptyString] = Field(default_factory=[]) # Using IPvAnyAddress creates JSON serialization errors | ||
""" Limit the server IP addresses available for NFS """ | ||
mountd_port: Annotated[TcpPort | None, AfterValidator(exclude_tcp_ports(EXCLUDED_PORTS))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can consider defining Annotated[TcpPort | ... ]
as a type alias since it is reused.
""" Force Kerberos authentication on NFS shares. """ | ||
v4_domain: str | ||
""" Specify a DNS domain (NFSv4 only) """ | ||
bindip: list[NonEmptyString] = Field(default_factory=[]) # Using IPvAnyAddress creates JSON serialization errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either default=[]
or default_factory=list
. In fact the prefered way is just to define bindip: list[NonEmptyString] = []
without needing Field
.
|
||
@single_argument_result | ||
class NfsBindipChoicesResult(BaseModel): | ||
""" Return a dictionary of IP addresses """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove decorator and simply do result: dict
instead of using ConfigDict
, or result: dict[str, str]
to be a little more precise.
@@ -222,8 +210,7 @@ async def nfs_compress(self, nfs): | |||
|
|||
return nfs | |||
|
|||
@accepts() | |||
@returns(Dict(additional_attrs=True)) | |||
@api_method(NfsBindipChoicesArgs, NfsBindipChoicesResult) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to new api, but why are we not just returning a list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is curious, but this method has had this format for ages (i.e. before my time). The callers expect a dictionary and changing that is outside of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I think this pattern can be useful when the value is different from what we want to display to the user. (That's the case with iscsi.portal.listen_ip_choices
whose dict values will be different depending on whether ALUA is on or off, but the dict keys will be the same)
if not_unique: | ||
raise ValueError(f"Entries must be unique, the following are not: {not_unique}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You go to the trouble of having a not_unique
list, and then error out on the first entry discovered. Should the if
block be de-indented?
""" Specify the security schema """ | ||
enabled: bool = True | ||
""" Enable or disable the share """ | ||
locked: bool | None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc string?
aliases: list[NonEmptyString] = [] | ||
""" IGNORED for now """ | ||
comment: str = "" | ||
""" User comment associated with share """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most docstrings (for NfsShareEntry
) appear to be terminated by a period. Maybe be consistent. Meh.
@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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem right for a few reasons:
- result of
all
is not being tested - Don't we need to instantiate
IPvAnyAddress(v)
to see if it'll throw an error?
Maybe something like:
def check_bind_ip(cls, field_value: list):
""" Custom validator for IP addresses to avoid JSON serialization errors """
for v in field_value:
IPvAnyAddress(v)
return field_value
@classmethod | ||
def check_valid_network(cls, field_value: list): | ||
""" Custom validator for IP networks """ | ||
all(isinstance(v, IPvAnyNetwork) for v in field_value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar comment as for check_bind_ip
.
if not_valid: | ||
raise ValueError(f"Cannot contain spaces or quotes: {not_valid}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar comment as for not_unique
above.
tests/api2/test_300_nfs.py
Outdated
pp(["192.168.0.0/64"], False, "does not appear to be an IPv4 or IPv6", id="IPv4 - invalid range"), | ||
pp(["bogus_network"], False, "does not appear to be an IPv4 or IPv6", id="IPv4 - invalid format"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the pydantic
code starts actually testing then I wouldn't be surprised if this text changed, e.g. "value is not a valid IPv4 or IPv6 address". (Anyway, we'll see.)
|
||
@field_validator('hosts') | ||
@classmethod | ||
def no_spaces_or_quotes(cls, field_value: list): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of a field validator, we have a couple ways to use regex for validation using Field(pattern=...)
or our own match_validator
in api/base/validators.py.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two things:
I don't think pydantic will apply a field pattern test to a list. It appears match_validator
is the same. I need to check each item in the list.
I need to do not match logic here. Pattern would work if it could operate on lists. match_validator
cannot do not validations.
Convert NFS config to new api.
Added a common 'TcpPort' type.
Passes NFS CI tests
Ignore the spurrious failure in test_008_check_root_dataset_settings.