ZFS Automatic Snapshot Daemon
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

289 lines
11 KiB

''' Filesystem base classes and registry '''
from typing import Any, Type as TypingType, TypeVar, Tuple, Set, \
MutableSet, Mapping, MutableMapping, Callable, \
AsyncGenerator, Generic
from abc import ABC, abstractmethod
from enum import Flag
from functools import total_ordering
from datetime import datetime
from asyncio import Queue
# {k: disordered[k] for k in sorted(disordered)}
FilesystemT = TypeVar('FilesystemT', bound='FilesystemBase')
SnapshotKeyT = TypeVar('SnapshotKeyT', bound='FilesystemBase.SnapshotKey')
EventT = TypeVar('EventT', bound='FilesystemBase.Event')
@total_ordering # type: ignore
class FilesystemBase(ABC):
''' Base filesystem class inherited by filesystem implementations. '''
class Event(dict):
''' Filesystem event class '''
class Type(Flag):
''' Filesystem event type enumerator. '''
GENERIC = 0x00000000
MOUNT = 0x00010000
MOUNTED = MOUNT | 0x0001
UNMOUNTED = MOUNT | 0x0002
FILE = 0x00020000 # Mandatory
FILE_CREATED = FILE | 0x0001
FILE_MODIFIED = FILE | 0x0002
FILE_MOVED = FILE | 0x0004
FILE_DELETED = FILE | 0x0008
SNAPSHOT = 0x00040000
SNAPSHOT_CREATED = SNAPSHOT | 0x0001
SNAPSHOT_DELETED = SNAPSHOT | 0x0002
def __init__(self, filesystem: FilesystemT,
event_type: Type, **properties):
''' Construct a filesystem event. Properties are assigned to the
event object, which doubles as a dictionary. '''
# Optional standard properties:
#
# description: human-readable description of the event
# path: Path the event originated from
# target: Path the event was targeted at
# snapshot: The snapshot the event pertained to
super().__init__(properties)
self._filesystem = filesystem
self._type = event_type
def filesystem(self) -> FilesystemT:
''' Get the filesystem the event occurred on. '''
return self._filesystem
def type(self) -> Type:
''' Get the event type. '''
return self._type
@total_ordering # type: ignore
class SnapshotKey(ABC):
''' Base snapshot key class inherited by filesystem implementations. '''
@abstractmethod
def filesystem(self) -> FilesystemT:
''' Get the parent filesystem for the snapshot. '''
@abstractmethod
def tag(self) -> str:
''' Get the tag name for the snapshot. Each user-configured schedule has
a unique tag name. '''
@abstractmethod
def timestamp(self) -> datetime:
''' Get the datetime the snapshot was taken at. '''
def __hash__(self) -> int:
''' Snapshot keys are hashable. Snapshots having equal filesystems,
tags and timestamps will have equal hash values. '''
return hash((self.filesystem(), self.tag(), self.timestamp()))
def __lt__(self: Any, other) -> bool:
''' Snapshot keys are sortable, and are ordered by filesystem first,
tag second and timestamp last. '''
return (isinstance(other, self.__class__) and
(self.filesystem() < other.filesystem() or
self.filesystem() == other.filesystem() and
(self.tag() < other.tag() or
self.tag() == other.tag() and
self.timestamp() < other.timestamp())))
def __eq__(self, other) -> bool:
''' Snapshot keys are comparable and are equal if they have equal hash
values. '''
return hash(self) == hash(other)
@classmethod
@abstractmethod
def key(cls) -> str:
''' Retrieve the registry key for this filesystem class. '''
# Filesystem classes must add themselves to the filesystem registry as
# soon as they are imported.
@classmethod
@abstractmethod
async def filesystems(cls) -> Mapping[str, FilesystemT]:
''' Retrieve a dictionary of labels to filesystems managed by this
filesystem class. '''
@classmethod
@abstractmethod
def configure(cls) -> Tuple[Mapping[str, Any], Callable[[Any], None]]:
''' Retrieve tuple for configuring the filesystem. '''
# This method returns a tuple (global_scope, callback), consisting of
# a dictionary and a callback function. It is called each time a
# configuration file has loaded, before it is executed.
#
# The dictionary consists of variables to be deep copied and exposed
# on the global execution scope of the configuration file. This may be
# used by the Filesystem class to provide constants, functions and
# variables that the user will need in order to configure filesystems
# of this class.
# The callback is invoked as callback(options) once execution has
# completed. Options are retrived from a variable Filesystem.key() on
# the local execution scope of the configuration file and will equal
# None if no such variable was assigned by the user. The variable is
# typically a dictionary, but may be of any type, and may be used by
# the Filesystem class to retrieve configuration options the user has
# set in order to configure filesystems of this class. '''
@abstractmethod
def label(self) -> str:
''' Retrieve the filesystem label.'''
# Labels must allow users to easily identify the underlying
# filesystems. Device names, drive letters, volume labels and
# mountpoints should be avoided, since these are not persistent and do
# not uniquely identify a filesystem. If the underlying filesystem
# does not provide labels that satisfy these criteria, a means for the
# user to map unique labels to unique filesystem identifiers must be
# provided in the configuration file.
@abstractmethod
async def children(self) -> Mapping[str, FilesystemT]:
''' Get a dictionary of labels to filesystems that are children of this
filesystem. '''
# Some filesystem classes allow hierarchical filesystem labels,
# permitting recursive snapshots to be taken on a filesystem and its
# children. An empty dictionary may be returned if this feature is not
# supported.
@abstractmethod
async def snapshots(self) -> Set[SnapshotKey]:
''' Get a set of keys for snapshots managed by this filesystem
object. Snapshots taken on any children of the filesystem,
recursively or otherwise, are not included in the set. '''
@abstractmethod
async def take(self, tag: str, recursive: bool=False) -> SnapshotKey:
''' Take a snapshot on this filesystem and return the key. For recursive
snapshots, return the key of the filesystem the snapshot was
requested on. '''
@abstractmethod
async def delete(self, key: SnapshotKey, recursive: bool=False) -> None:
''' Delete a snapshot having a given key from this filesystem. For
recursive deletions, snapshots on children of the filesystem having
a tag and a timestamp equal to that of the parent snapshot are
deleted recursively. '''
@abstractmethod
def watch(self) -> AsyncGenerator[Event, None]:
''' Factory function that returns an asynchronous generator that
yields dispatched events from an internal event queue. The
generator ends when the filesystem is unmounted. '''
def __hash__(self) -> int:
''' Filesystems are hashable. Filesystems having equal labels will have
equal hash values. '''
return hash(self.label())
def __lt__(self, other) -> bool:
''' Filesystems are sortable and are ordered by label. '''
return (isinstance(other, self.__class__) and
(self == other or self.label() < other.label()))
def __eq__(self, other):
''' Filesystems are comparable and are equal if they have equal hash
values. '''
return hash(self) == hash(other)
class DispatcherMixin(Generic[EventT]):
''' Class for dispatching filesystem events to asynchronous generators
produced by a factory function. Events are consumed by iterating
over the generators. '''
def __init__(self):
self._event_queues: MutableSet[Queue] = set()
async def dispatch(self, *events: EventT):
''' Dispatch events to generators. '''
for event in events:
for queue in self._event_queues:
await queue.put(event)
def watch(self) -> AsyncGenerator[EventT, None]:
''' Factory function that returns an asynchronous generator that
yields dispatched events from an internal event queue. The
generator ends when the filesystem is unmounted. '''
async def generator(queue):
while True:
event: EventT = await queue.get()
yield event
if FilesystemBase.Event.Type.UNMOUNTED in event.type():
break
self._event_queues.remove(queue)
queue: Queue = Queue()
self._event_queues.add(queue)
return generator(queue)
class FilesystemRegistry:
''' Class that keeps a global registry of filesystem classes and labels '''
_filesystems: MutableMapping[str, FilesystemBase] = {}
_labels: MutableSet[str] = set()
def __init__(self):
pass
@classmethod
def filesystems(cls) -> Mapping[str, TypingType[FilesystemBase]]:
''' Retrieve a dictionary of keys to registered filesystem classes. '''
return cls._filesystems
@classmethod
def has(cls, key: str) -> bool:
''' Returns True if the filesystem key exists in the registry. '''
return key in cls._filesystems
@classmethod
def register(cls, klass: TypingType[FilesystemBase]) -> None:
''' Register a filesystem class. '''
assert not klass.key() in cls._filesystems
cls._filesystems[klass.key()] = klass
@classmethod
def labels(cls) -> Set[str]:
''' Retrieve the set of the labels in the registry. '''
return set(cls._labels)
@classmethod
def mounted(cls, label: str) -> bool:
''' Returns True if the filesystem label exists in the registry. '''
return label in cls._labels
@classmethod
def mount(cls, label: str) -> None:
''' Add a new filesystem label to the registry. Must be called once a
filesystem becomes available. '''
assert not label in cls._labels
cls._labels.add(label)
@classmethod
def unmount(cls, label: str) -> None:
''' Remove a filesystem label from the registry. Must be called if a
filesystem becomes unavailable, or the configuration file was
reloaded and a filesystem has disappeared from it. '''
assert label in cls._labels
cls._labels.remove(label)