Skip to content

Commit 275b911

Browse files
authored
NAS-136694 / 25.10 / Perform better NQN validation (#16733)
1 parent 7deb7bf commit 275b911

File tree

7 files changed

+63
-10
lines changed

7 files changed

+63
-10
lines changed

src/middlewared/middlewared/api/base/types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .ldap import * # noqa
66
from .list import * # noqa
77
from .network import * # noqa
8+
from .nvmet import * # noqa
89
from .string import * # noqa
910
from .urls import * # noqa
1011
from .user import * # noqa
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import re
2+
from typing import Annotated
3+
from uuid import UUID
4+
5+
from pydantic import AfterValidator, Field
6+
7+
__all__ = [
8+
"NQN"
9+
]
10+
11+
MIN_NQN_LEN = 11
12+
MAX_NQN_LEN = 223
13+
14+
NVMET_NQN_UUID_PREFIX = "nqn.2014-08.org.nvmexpress:uuid:"
15+
NVMET_NQN_UUID_PREFIX_LEN = 32
16+
NVMET_DISCOVERY_NQN = "nqn.2014-08.org.nvmexpress.discovery"
17+
UUID_STRING_LEN = 36
18+
NVMET_NQN_UUID_LEN = NVMET_NQN_UUID_PREFIX_LEN + UUID_STRING_LEN
19+
20+
NQN_DATE_PATTERN = re.compile(r"^nqn.\d{4}-\d{2}\..*")
21+
DOMAIN_PATTERN = re.compile(r'^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,}$')
22+
23+
24+
def _validate_nqn(nqn: str):
25+
if nqn == NVMET_DISCOVERY_NQN:
26+
return nqn
27+
elif nqn.startswith(NVMET_NQN_UUID_PREFIX):
28+
# Check for "nqn.2014-08.org.nvmexpress:uuid:11111111-2222-3333-4444-555555555555"
29+
if len(nqn) != NVMET_NQN_UUID_LEN:
30+
raise ValueError(f'{nqn}: uuid is incorrect length')
31+
UUID(nqn[NVMET_NQN_UUID_PREFIX_LEN:])
32+
elif nqn.startswith("nqn."):
33+
# Check for "nqn.yyyy-mm.reverse.domain:user-string"
34+
if not NQN_DATE_PATTERN.match(nqn):
35+
raise ValueError(f'{nqn}: must start with "nqn.YYYY-MM.<reverse.domain>"')
36+
# Now check the domain
37+
if not nqn[12:] or '.' not in nqn[12:]:
38+
raise ValueError(f'{nqn}: must start with "nqn.YYYY-MM.<reverse.domain>" - domain missing')
39+
reverse_domain = nqn[12:].split(':', 1)[0]
40+
domain_parts = reverse_domain.split('.')
41+
domain_parts.reverse()
42+
if not DOMAIN_PATTERN.match('.'.join(domain_parts)):
43+
raise ValueError(f'{nqn}: domain does not appear to be valid')
44+
else:
45+
raise ValueError(f'{nqn}: must start with "nqn"')
46+
return nqn
47+
48+
49+
NQN = Annotated[str, Field(min_length=MIN_NQN_LEN, max_length=MAX_NQN_LEN), AfterValidator(_validate_nqn)]

src/middlewared/middlewared/api/v25_10_0/nvmet_global.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from middlewared.api.base import BaseModel, Excluded, ForUpdateMetaclass, excluded_field, single_argument_args
1+
from middlewared.api.base import BaseModel, Excluded, ForUpdateMetaclass, NQN, excluded_field, single_argument_args
22

33
__all__ = [
44
"NVMetGlobalEntry",
@@ -44,6 +44,7 @@ class NVMetGlobalEntry(BaseModel):
4444
@single_argument_args('nvmet_update')
4545
class NVMetGlobalUpdateArgs(NVMetGlobalEntry, metaclass=ForUpdateMetaclass):
4646
id: Excluded = excluded_field()
47+
basenqn: NQN
4748

4849

4950
class NVMetGlobalUpdateResult(BaseModel):

src/middlewared/middlewared/api/v25_10_0/nvmet_host.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pydantic import Field, Secret, model_validator
44

5-
from middlewared.api.base import BaseModel, Excluded, ForUpdateMetaclass, NonEmptyString, excluded_field
5+
from middlewared.api.base import NQN, BaseModel, Excluded, ForUpdateMetaclass, NonEmptyString, excluded_field
66

77
__all__ = [
88
"NVMetHostEntry",
@@ -49,6 +49,7 @@ class NVMetHostEntry(BaseModel):
4949

5050
class NVMetHostCreate(NVMetHostEntry):
5151
id: Excluded = excluded_field()
52+
hostnqn: NQN
5253

5354
@model_validator(mode='after')
5455
def validate_attrs(self):

src/middlewared/middlewared/api/v25_10_0/nvmet_subsys.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from typing import Annotated, Literal, Optional
1+
from typing import Literal, Optional
22

33
from pydantic import Field
44

5-
from middlewared.api.base import BaseModel, Excluded, ForUpdateMetaclass, NonEmptyString, excluded_field
5+
from middlewared.api.base import NQN, BaseModel, Excluded, ForUpdateMetaclass, NonEmptyString, excluded_field
66

77
__all__ = [
88
"NVMetSubsysEntry",
@@ -14,8 +14,6 @@
1414
"NVMetSubsysDeleteResult",
1515
]
1616

17-
MAX_NQN_LEN = 223
18-
1917

2018
class NVMetSubsysEntry(BaseModel):
2119
id: int
@@ -26,7 +24,7 @@ class NVMetSubsysEntry(BaseModel):
2624
If `subnqn` is not provided on creation, then this name will be appended to the `basenqn` from \
2725
`nvmet.global.config` to generate a subnqn.
2826
"""
29-
subnqn: Annotated[NonEmptyString, Field(max_length=MAX_NQN_LEN)] | None = None
27+
subnqn: NonEmptyString | None = None
3028
serial: str
3129
allow_any_host: bool = False
3230
"""Any host can access the storage associated with this subsystem (i.e. no access control)."""
@@ -66,6 +64,7 @@ class NVMetSubsysCreate(NVMetSubsysEntry):
6664
hosts: Excluded = excluded_field()
6765
namespaces: Excluded = excluded_field()
6866
ports: Excluded = excluded_field()
67+
subnqn: NQN | None = None
6968

7069

7170
class NVMetSubsysCreateArgs(BaseModel):

tests/api2/test_audit_nvmet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
REDACTED_SECRET = '********'
88
MB = 1024 * 1024
99
MB_100 = 100 * MB
10-
HOST1_NQN = 'nqn.host1'
10+
HOST1_NQN = 'nqn.2011-06.com.truenas:uuid-68bf9433-63ef-49f5-a921-4c0f8190fd94:host1'
1111
HOST1_KEY = 'DHHC-1:01:rxc6XaoJgTSN7GID7gPQidMAskFym01wNHdw5B3RA33UFFIc:'
1212
REDACTED_SECRET = '********'
1313
SUBSYS_NAME = 'subsys1'

tests/api2/test_nvmet_tcp.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
ZVOL_RESIZE_END_MB = 150
5454

5555
FAKE_HOSTNQN = 'nqn.2014-08.org.nvmexpress:uuid:48747223-7535-4f8e-a789-b8af8bfdea54'
56+
HOST1_NQN = 'nqn.2011-06.com.truenas:uuid-68bf9433-63ef-49f5-a921-4c0f8190fd94:host1'
57+
HOST2_NQN = 'nqn.2011-06.com.truenas:hostname2'
5658
DEVICE_TYPE_FILE = 'FILE'
5759
MB_10 = 100 * MB
5860
MB_100 = 100 * MB
@@ -1121,7 +1123,7 @@ def pick_from_list_by_id(_list, _id):
11211123
assert subsystems[0]['ports'][0] == fixture_port['id']
11221124

11231125
# Add a host -> no change
1124-
with nvmet_host('nqn.host1') as host1:
1126+
with nvmet_host(HOST1_NQN) as host1:
11251127
check_regular_query(1)
11261128
subsystems = check_verbose_query(1)
11271129
assert len(subsystems[0]['hosts']) == 0
@@ -1140,7 +1142,7 @@ def pick_from_list_by_id(_list, _id):
11401142
assert subsystems[0]['ports'][0] == fixture_port['id']
11411143

11421144
# Create/associate another host -> change
1143-
with nvmet_host('nqn.host2') as host2:
1145+
with nvmet_host(HOST2_NQN) as host2:
11441146
with nvmet_host_subsys(host2['id'], subsys1_id):
11451147
check_regular_query(1)
11461148
subsystems = check_verbose_query(1)

0 commit comments

Comments
 (0)