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-132813 / 25.04 / Convert pool.snapshottask to new API #15164

Merged
merged 8 commits into from
Dec 10, 2024
3 changes: 1 addition & 2 deletions src/middlewared/middlewared/api/base/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from pydantic.main import IncEx
from typing_extensions import Annotated

from middlewared.api.base.types.base import SECRET_VALUE
from middlewared.api.base.types.base.string import LongStringWrapper
from middlewared.api.base.types.string import SECRET_VALUE, LongStringWrapper
from middlewared.utils.lang import undefined


Expand Down
4 changes: 2 additions & 2 deletions src/middlewared/middlewared/api/base/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .base import * # noqa
from .fc import * # noqa
from .filesystem import * # noqa
from .iscsi import * # noqa
from .string import * # noqa
from .user import * # noqa
from .filesystem import * # noqa

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from pydantic_core import CoreSchema, core_schema, PydanticKnownError
from typing_extensions import Annotated

from middlewared.api.base.validators import time_validator
from middlewared.utils.netbios import validate_netbios_name, validate_netbios_domain
from middlewared.validators import Time
from zettarepl.snapshot.name import validate_snapshot_naming_schema

__all__ = ["HttpUrl", "LongString", "NonEmptyString", "LongNonEmptyString", "SECRET_VALUE", "TimeString", "NetbiosDomain", "NetbiosName"]

HttpUrl = Annotated[_HttpUrl, AfterValidator(str)]
__all__ = [
"HttpUrl", "LongString", "NonEmptyString", "LongNonEmptyString", "SECRET_VALUE", "TimeString", "NetbiosDomain",
"NetbiosName", "SnapshotNameSchema"
]


class LongStringWrapper:
Expand Down Expand Up @@ -47,17 +50,18 @@ def __get_pydantic_core_schema__(
)


HttpUrl = Annotated[_HttpUrl, AfterValidator(str)]
# By default, our strings are no more than 1024 characters long. This string is 2**31-1 characters long (SQLite limit).
LongString = Annotated[
LongStringWrapper,
BeforeValidator(LongStringWrapper),
PlainSerializer(lambda x: x.value if isinstance(x, LongStringWrapper) else x),
]

NonEmptyString = Annotated[str, Field(min_length=1)]
LongNonEmptyString = Annotated[LongString, Field(min_length=1)]
TimeString = Annotated[str, AfterValidator(Time())]
TimeString = Annotated[str, AfterValidator(time_validator)]
NetbiosDomain = Annotated[str, AfterValidator(validate_netbios_domain)]
NetbiosName = Annotated[str, AfterValidator(validate_netbios_name)]
SnapshotNameSchema = Annotated[str, AfterValidator(lambda val: validate_snapshot_naming_schema(val) or val)]

SECRET_VALUE = "********"
23 changes: 23 additions & 0 deletions src/middlewared/middlewared/api/base/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from datetime import time
import re


def match_validator(pattern: re.Pattern, explanation: str | None = None):
def validator(value: str):
assert (value is None or pattern.match(value)), (explanation or f"Value does not match {pattern!r} pattern")
return value

return validator


def time_validator(value: str):
try:
hours, minutes = value.split(':')
except ValueError:
raise ValueError('Time should be in 24 hour format like "18:00"')
else:
try:
time(int(hours), int(minutes))
except TypeError:
raise ValueError('Time should be in 24 hour format like "18:00"')
return value

This file was deleted.

10 changes: 0 additions & 10 deletions src/middlewared/middlewared/api/base/validators/string.py

This file was deleted.

3 changes: 2 additions & 1 deletion src/middlewared/middlewared/api/v25_04_0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
from .iscsi_extent import * # noqa
from .keychain import * # noqa
from .netdata import * # noqa
from .pool_scrub import * # noqa
from .pool import * # noqa
from .pool_resilver import * # noqa
from .pool_scrub import * # noqa
from .pool_snapshottask import * # noqa
from .privilege import * # noqa
from .reporting import * # noqa
from .reporting_exporters import * # noqa
Expand Down
120 changes: 120 additions & 0 deletions src/middlewared/middlewared/api/v25_04_0/pool_snapshottask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from typing import Any, Literal

from pydantic import Field

from middlewared.api.base import BaseModel, ForUpdateMetaclass, TimeString, SnapshotNameSchema
from .common import CronModel


__all__ = [
"PoolSnapshotTaskEntry", "PoolSnapshotTaskCreateArgs", "PoolSnapshotTaskCreateResult",
"PoolSnapshotTaskUpdateArgs", "PoolSnapshotTaskUpdateResult", "PoolSnapshotTaskDeleteArgs",
"PoolSnapshotTaskDeleteResult", "PoolSnapshotTaskMaxCountArgs", "PoolSnapshotTaskMaxCountResult",
"PoolSnapshotTaskMaxTotalCountArgs", "PoolSnapshotTaskMaxTotalCountResult", "PoolSnapshotTaskRunArgs",
"PoolSnapshotTaskRunResult", "PoolSnapshotTaskUpdateWillChangeRetentionForArgs",
"PoolSnapshotTaskUpdateWillChangeRetentionForResult", "PoolSnapshotTaskDeleteWillChangeRetentionForArgs",
"PoolSnapshotTaskDeleteWillChangeRetentionForResult"
]


class PoolSnapshotTaskCron(CronModel):
minute: str = "00"
begin: TimeString = "00:00"
end: TimeString = "23:59"


class PoolSnapshotTaskCreate(BaseModel):
dataset: str
recursive: bool = False
lifetime_value: int = 2
lifetime_unit: Literal["HOUR", "DAY", "WEEK", "MONTH", "YEAR"] = "WEEK"
enabled: bool = True
exclude: list[str] = []
naming_schema: SnapshotNameSchema = "auto-%Y-%m-%d_%H-%M"
allow_empty: bool = True
schedule: PoolSnapshotTaskCron = Field(default_factory=PoolSnapshotTaskCron)


class PoolSnapshotTaskUpdate(PoolSnapshotTaskCreate, metaclass=ForUpdateMetaclass):
fixate_removal_date: bool


class PoolSnapshotTaskUpdateWillChangeRetentionFor(PoolSnapshotTaskCreate, metaclass=ForUpdateMetaclass):
pass


class PoolSnapshotTaskDeleteOptions(BaseModel):
fixate_removal_date: bool = False


class PoolSnapshotTaskEntry(PoolSnapshotTaskCreate):
id: int
vmware_sync: bool
state: Any


class PoolSnapshotTaskCreateArgs(BaseModel):
data: PoolSnapshotTaskCreate


class PoolSnapshotTaskCreateResult(BaseModel):
result: PoolSnapshotTaskEntry


class PoolSnapshotTaskUpdateArgs(BaseModel):
id: int
data: PoolSnapshotTaskUpdate


class PoolSnapshotTaskUpdateResult(BaseModel):
result: PoolSnapshotTaskEntry


class PoolSnapshotTaskDeleteArgs(BaseModel):
id: int
options: PoolSnapshotTaskDeleteOptions = Field(default_factory=PoolSnapshotTaskDeleteOptions)


class PoolSnapshotTaskDeleteResult(BaseModel):
result: Literal[True]


class PoolSnapshotTaskMaxCountArgs(BaseModel):
pass


class PoolSnapshotTaskMaxCountResult(BaseModel):
result: int


class PoolSnapshotTaskMaxTotalCountArgs(BaseModel):
pass


class PoolSnapshotTaskMaxTotalCountResult(BaseModel):
result: int


class PoolSnapshotTaskRunArgs(BaseModel):
id: int


class PoolSnapshotTaskRunResult(BaseModel):
result: None


class PoolSnapshotTaskUpdateWillChangeRetentionForArgs(BaseModel):
id: int
data: PoolSnapshotTaskUpdateWillChangeRetentionFor


class PoolSnapshotTaskUpdateWillChangeRetentionForResult(BaseModel):
result: dict[str, list[str]]


class PoolSnapshotTaskDeleteWillChangeRetentionForArgs(BaseModel):
id: int


class PoolSnapshotTaskDeleteWillChangeRetentionForResult(BaseModel):
result: dict[str, list[str]]
72 changes: 24 additions & 48 deletions src/middlewared/middlewared/plugins/snapshot.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from datetime import datetime, time, timedelta
from datetime import time
import os

from middlewared.api import api_method
from middlewared.api.current import (
PoolSnapshotTaskEntry, PoolSnapshotTaskCreateArgs, PoolSnapshotTaskCreateResult, PoolSnapshotTaskUpdateArgs,
PoolSnapshotTaskUpdateResult, PoolSnapshotTaskDeleteArgs, PoolSnapshotTaskDeleteResult,
PoolSnapshotTaskMaxCountArgs, PoolSnapshotTaskMaxCountResult, PoolSnapshotTaskMaxTotalCountArgs,
PoolSnapshotTaskMaxTotalCountResult, PoolSnapshotTaskRunArgs, PoolSnapshotTaskRunResult
)
from middlewared.common.attachment import FSAttachmentDelegate
from middlewared.schema import accepts, returns, Bool, Cron, Dataset, Dict, Int, List, Patch, Str
from middlewared.schema import Cron
from middlewared.service import CallError, CRUDService, item_method, private, ValidationErrors
import middlewared.sqlalchemy as sa
from middlewared.utils.cron import croniter_for_schedule
from middlewared.utils.path import is_child
from middlewared.validators import ReplicationSnapshotNamingSchema


class PeriodicSnapshotTaskModel(sa.Model):
Expand Down Expand Up @@ -40,6 +45,7 @@ class Config:
datastore_extend_context = 'pool.snapshottask.extend_context'
namespace = 'pool.snapshottask'
cli_namespace = 'task.snapshot'
entry = PoolSnapshotTaskEntry

@private
async def extend_context(self, rows, extra):
Expand Down Expand Up @@ -69,29 +75,9 @@ async def extend(self, data, context):

return data

@accepts(
Dict(
'periodic_snapshot_create',
Dataset('dataset', required=True),
Bool('recursive', required=True),
List('exclude', items=[Dataset('item')]),
Int('lifetime_value', required=True),
Str('lifetime_unit', enum=['HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'], required=True),
Str('naming_schema', required=True, validators=[ReplicationSnapshotNamingSchema()]),
Cron(
'schedule',
defaults={
'minute': '00',
'begin': '00:00',
'end': '23:59',
},
required=True,
begin_end=True
),
Bool('allow_empty', default=True),
Bool('enabled', default=True),
register=True
),
@api_method(
PoolSnapshotTaskCreateArgs,
PoolSnapshotTaskCreateResult,
audit='Snapshot task create:',
audit_extended=lambda data: data['dataset']
)
Expand Down Expand Up @@ -158,16 +144,11 @@ async def do_create(self, data):

return await self.get_instance(data['id'])

@accepts(
Int('id', required=True),
Patch(
'periodic_snapshot_create',
'periodic_snapshot_update',
('add', {'name': 'fixate_removal_date', 'type': 'bool'}),
('attr', {'update': True})
),
@api_method(
PoolSnapshotTaskUpdateArgs,
PoolSnapshotTaskUpdateResult,
audit='Snapshot task update:',
audit_callback=True,
audit_callback=True
)
async def do_update(self, audit_callback, id_, data):
"""
Expand Down Expand Up @@ -255,14 +236,11 @@ async def do_update(self, audit_callback, id_, data):

return await self.get_instance(id_)

@accepts(
Int('id'),
Dict(
'options',
Bool('fixate_removal_date', default=False),
),
@api_method(
PoolSnapshotTaskDeleteArgs,
PoolSnapshotTaskDeleteResult,
audit='Snapshot task delete:',
audit_callback=True,
audit_callback=True
)
async def do_delete(self, audit_callback, id_, options):
"""
Expand Down Expand Up @@ -318,8 +296,7 @@ async def do_delete(self, audit_callback, id_, options):

return response

@accepts()
@returns(Int())
@api_method(PoolSnapshotTaskMaxCountArgs, PoolSnapshotTaskMaxCountResult)
def max_count(self):
"""
Returns a maximum amount of snapshots (per-dataset) the system can sustain.
Expand All @@ -329,8 +306,7 @@ def max_count(self):
# with too many, then File Explorer will show no snapshots available.
return 512

@accepts()
@returns(Int())
@api_method(PoolSnapshotTaskMaxTotalCountArgs, PoolSnapshotTaskMaxTotalCountResult)
def max_total_count(self):
"""
Returns a maximum amount of snapshots (total) the system can sustain.
Expand All @@ -341,7 +317,7 @@ def max_total_count(self):
return 10000

@item_method
@accepts(Int("id"))
@api_method(PoolSnapshotTaskRunArgs, PoolSnapshotTaskRunResult)
async def run(self, id_):
"""
Execute a Periodic Snapshot Task of `id`.
Expand Down
2 changes: 0 additions & 2 deletions src/middlewared/middlewared/plugins/snapshot_/removal_date.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import hashlib

from middlewared.service import Service, job, private


Expand Down
Loading
Loading