Skip to content
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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

mgrimesix
Copy link
Contributor

@mgrimesix mgrimesix commented Jan 8, 2025

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.

@mgrimesix mgrimesix added the WIP label Jan 8, 2025
@bugclerk
Copy link
Contributor

bugclerk commented Jan 8, 2025

@bugclerk bugclerk changed the title Convert NFS config to new api. NAS-131665 / 25.04 / Convert NFS config to new api. Jan 8, 2025
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.
@mgrimesix mgrimesix removed the WIP label Jan 13, 2025
@mgrimesix mgrimesix requested review from a team and anodos325 January 13, 2025 16:14
@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)
Copy link
Contributor

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).

Copy link
Contributor Author

@mgrimesix mgrimesix Jan 13, 2025

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.

Copy link
Contributor

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')]}

Copy link
Contributor Author

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.

Copy link
Contributor

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))]
Copy link
Contributor

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
Copy link
Contributor

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 """
Copy link
Contributor

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)
Copy link
Contributor

@creatorcary creatorcary Jan 13, 2025

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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)

Comment on lines 135 to 136
if not_unique:
raise ValueError(f"Entries must be unique, the following are not: {not_unique}")
Copy link
Contributor

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
Copy link
Contributor

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 """
Copy link
Contributor

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.

Comment on lines 65 to 70
@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
Copy link
Contributor

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)
Copy link
Contributor

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.

Comment on lines 158 to 159
if not_valid:
raise ValueError(f"Cannot contain spaces or quotes: {not_valid}")
Copy link
Contributor

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.

Comment on lines 672 to 673
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"),
Copy link
Contributor

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):
Copy link
Contributor

@creatorcary creatorcary Jan 13, 2025

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.

Copy link
Contributor Author

@mgrimesix mgrimesix Jan 13, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants