Source code for libioc.events

# Copyright (c) 2017-2019, Stefan Grönke
# Copyright (c) 2014-2018, iocage
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""ioc events collection."""
import typing
from timeit import default_timer as timer

import libioc.errors

# MyPy
import libzfs

EVENT_STATUS = (
    "pending",
    "done",
    "failed"
)


[docs]class Scope(list): """An independent event history scope.""" PENDING_COUNT: int def __init__(self) -> None: self.PENDING_COUNT = 0 super().__init__([])
[docs]class IocEvent: """The base event class of liblibioc.""" _scope: Scope identifier: typing.Optional[str] _started_at: float _stopped_at: float _pending: bool skipped: bool done: bool reverted: bool error: typing.Optional[typing.Union[bool, BaseException]] _rollback_steps: typing.List[typing.Callable[[], typing.Optional[ typing.Generator['IocEvent', None, None] ]]] _child_events: typing.List['IocEvent'] def __init__( self, message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: """Initialize an IocEvent.""" if scope is not None: self.scope = scope else: self.scope = Scope() for event in self.scope: if event.__hash__() == self.__hash__(): return event # type: ignore self._pending = False self.skipped = False self.done = True self.reverted = False self.error = None self._rollback_steps = [] self._child_events = [] self._rollback_steps = [] self.number = len(self.scope) + 1 self.parent_count = self.scope.PENDING_COUNT self.message = message @property def scope(self) -> Scope: """Return the currently used event scope.""" return self._scope @scope.setter def scope(self, scope: typing.Optional[Scope]) -> None: if scope is None: self._scope = Scope() else: self._scope = scope
[docs] def get_state_string( self, error: str="failed", skipped: str="skipped", done: str="done", pending: str="pending" ) -> str: """Get a humanreadable string according to the jail state.""" if self.error is not None: return error if self.skipped is True: return skipped if self.done is True: return done return pending
[docs] def child_event(self, event: 'IocEvent') -> 'IocEvent': """Append the event to the child_events for later notification.""" self._child_events.append(event) return event
[docs] def add_rollback_step(self, method: typing.Callable[[], None]) -> None: """Add a rollback step that is executed when the event fails.""" self._rollback_steps.append(method)
[docs] def rollback( self ) -> typing.Optional[typing.Generator['IocEvent', None, None]]: """Rollback all rollback steps in reverse order.""" if self.reverted is True: return self.reverted = True # Notify child_events in reverse order for event in reversed(self._child_events): rollback_actions = event.rollback() if rollback_actions is not None: for rollback_action in rollback_actions: yield rollback_action # Execute rollback steps in reverse order reversed_rollback_steps = reversed(self._rollback_steps) self._rollback_steps = [] for revert_step in reversed_rollback_steps: revert_events = revert_step() if revert_events is not None: for event in revert_events: yield event
@property def type(self) -> str: """ Return the events type. The event type is obtained from an IocEvent's class name. """ return type(self).__name__ @property def pending(self) -> bool: """Return True if the event is pending.""" return self._pending @pending.setter def pending(self, state: bool) -> None: """ Set the pending state. Changes invoke internal processing as for example the calculation of the event duration and the global PENDING_COUNT. """ current = self._pending new_state = (state is True) if current == new_state: return if new_state is True: try: self._started_at raise libioc.errors.EventAlreadyFinished(event=self) except AttributeError: self._started_at = float(timer()) if new_state is False: self._stopped_at = float(timer()) self._pending = new_state self.scope.PENDING_COUNT += 1 if (state is True) else -1 @property def duration(self) -> typing.Optional[float]: """Return the duration of finished events.""" try: return self._stopped_at - self._started_at except AttributeError: return None def _update_message( self, message: typing.Optional[str]=None, ) -> None: self.message = message
[docs] def begin(self, message: typing.Optional[str]=None) -> 'IocEvent': """Begin an event.""" self._update_message(message) self.pending = True self.done = False self.parent_count = self.scope.PENDING_COUNT - 1 return self
[docs] def end(self, message: typing.Optional[str]=None) -> 'IocEvent': """Successfully finish an event.""" self._update_message(message) self.done = True self.pending = False self.parent_count = self.scope.PENDING_COUNT return self
[docs] def step(self, message: typing.Optional[str]=None) -> 'IocEvent': """Reflect partial event progress.""" self._update_message(message) self.parent_count = self.scope.PENDING_COUNT return self
[docs] def skip(self, message: typing.Optional[str]=None) -> 'IocEvent': """Mark an event as skipped.""" self._update_message(message) self.skipped = True self.pending = False self.parent_count = self.scope.PENDING_COUNT return self
[docs] def fail( self, exception: bool=True, message: typing.Optional[str]=None ) -> 'IocEvent': """End an event with a failure.""" list(self.fail_generator(exception=exception, message=message)) return self
[docs] def fail_generator( self, exception: bool=True, message: typing.Optional[str]=None ) -> typing.Generator['IocEvent', None, None]: """End an event with a failure via a generator of rollback steps.""" self._update_message(message) self.error = exception actions = self.rollback() if isinstance(actions, typing.Generator): for action in actions: yield action self.pending = False self.parent_count = self.scope.PENDING_COUNT yield self
def __hash__(self) -> typing.Any: """Compare an event by its type and identifier.""" has_identifier = ("identifier" in self.__dir__()) is True identifier = "generic" if has_identifier is False else self.identifier return hash((self.type, identifier))
# Jail
[docs]class JailEvent(IocEvent): """Any event related to a jail.""" jail: 'libioc.Jail.JailGenerator' identifier: typing.Optional[str] def __init__( self, jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: try: self.identifier = jail.full_name except AttributeError: self.identifier = None self.jail = jail IocEvent.__init__(self, message=message, scope=scope)
[docs]class JailRename(JailEvent): """Change the name of a jail.""" current_name: str new_name: str def __init__( self, jail: 'libioc.Jail.JailGenerator', current_name: str, new_name: str, message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.current_name = current_name self.new_name = new_name JailEvent.__init__(self, jail=jail, message=message, scope=scope)
[docs]class JailStop(JailEvent): """Destroy the jail.""" pass
[docs]class JailRemove(JailEvent): """Remove the jail(2).""" pass
[docs]class TeardownSystemMounts(JailStop): """Teardown a jails mountpoints.""" pass
[docs]class JailResourceLimitAction(JailEvent): """Set or unset a jails resource limits.""" pass
[docs]class VnetEvent(JailEvent): """A group of events around VNET operations.""" pass
[docs]class JailNetworkSetup(VnetEvent): """Start VNET networks.""" pass
[docs]class JailNetworkTeardown(JailStop): """Teardown a jails network.""" pass
[docs]class VnetInterfaceConfig(JailNetworkSetup): """Configure VNET network interfaces and firewall.""" pass
[docs]class VnetSetupLocalhost(JailNetworkSetup): """Configure a VNET jails localhost.""" pass
[docs]class VnetSetRoutes(JailNetworkSetup): """Set a VNET jails network routes.""" pass
[docs]class JailAttach(JailEvent): """Remove the jail(2).""" pass
[docs]class DevFSEvent(JailEvent): """Group of events that occor on DevFS operations.""" pass
[docs]class MountDevFS(DevFSEvent): """Mount /dev into a jail.""" pass
[docs]class MountFdescfs(DevFSEvent): """Mount /dev/fd into a jail.""" pass
[docs]class FstabEvent(JailEvent): """Group of events that occor on Fstab operations.""" pass
[docs]class MountFstab(FstabEvent): """Mount entries from a jails fstab file.""" pass
[docs]class UnmountFstab(FstabEvent): """Unmount entries from a jails fstab file.""" pass
[docs]class JailHook(JailEvent): """Run jail hook.""" stdout: typing.Optional[str] def __init__( self, jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.stdout = None super().__init__( jail=jail, message=message, scope=scope )
[docs] def end( self, message: typing.Optional[str]=None, stdout: str="" ) -> 'IocEvent': """Successfully finish an event.""" self.stdout = stdout return super().end(message)
[docs] def fail( self, exception: bool=True, message: typing.Optional[str]=None, stdout: str="" ) -> 'IocEvent': """Successfully finish an event.""" self.stdout = stdout return super().fail( exception=exception, message=message )
[docs]class JailHookPrestart(JailHook): """Run jail prestart hook.""" pass
[docs]class JailHookStart(JailHook): """Run jail start hook.""" pass
[docs]class JailCommand(JailHook): """Run command in a jail.""" stdout: typing.Optional[str] stderr: typing.Optional[str] code: typing.Optional[int]
[docs]class JailHookCreated(JailHook): """Run jail created hook.""" pass
[docs]class JailHookPoststart(JailHook): """Run jail poststart hook.""" pass
[docs]class JailHookPrestop(JailHook): """Run jail prestop hook.""" pass
[docs]class JailHookStop(JailHook): """Run jail stop hook.""" pass
[docs]class JailHookPoststop(JailHook): """Run jail poststop hook.""" pass
[docs]class JailFstabUpdate(JailEvent): """Update a jails fstab file.""" def __init__( self, jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: JailEvent.__init__(self, jail=jail, message=message, scope=scope)
[docs]class JailResolverConfig(JailEvent): """Update a jails /etc/resolv.conf file.""" def __init__( self, jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: JailEvent.__init__(self, jail=jail, message=message, scope=scope)
[docs]class JailZFSShare(JailEvent): """Group of events that mounts or unmounts shared ZFS datasets.""" pass
[docs]class BasejailStorageConfig(JailEvent): """Mount or unmount basejail storage of a jail.""" pass
[docs]class AttachZFSDataset(JailZFSShare): """Mount an individual dataset when starting a jail with shared ZFS.""" def __init__( self, jail: 'libioc.Jail.JailGenerator', dataset: libzfs.ZFSDataset, scope: typing.Optional[Scope]=None ) -> None: msg = f"Dataset {dataset.name} was attached to Jail {jail.full_name}" JailEvent.__init__(self, jail=jail, message=msg, scope=scope)
[docs]class JailClone(JailEvent): """Clone a jail.""" def __init__( self, jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: JailEvent.__init__(self, jail=jail, message=message, scope=scope)
# Release
[docs]class ReleaseEvent(IocEvent): """Event related to a release.""" release: 'libioc.Release.ReleaseGenerator' def __init__( self, release: 'libioc.Release.ReleaseGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.identifier = release.full_name self.release = release IocEvent.__init__(self, message=message, scope=scope)
[docs]class ReleaseUpdate(ReleaseEvent): """Update a release.""" pass
[docs]class FetchRelease(ReleaseEvent): """Fetch release assets.""" pass
[docs]class ReleasePrepareStorage(FetchRelease): """Prepare the storage of a release before fetching it.""" pass
[docs]class ReleaseDownload(FetchRelease): """Download release assets.""" pass
[docs]class ReleaseAssetDownload(FetchRelease): """Download release assets.""" pass
[docs]class ReleaseExtraction(FetchRelease): """Extract a release asset.""" pass
[docs]class ReleaseCopyBase(FetchRelease): """Copy the basejail folders of a release into individual ZFS datasets.""" pass
[docs]class ReleaseConfiguration(FetchRelease): """Pre-configure a release with reasonable defaults.""" pass
[docs]class ReleaseUpdatePull(ReleaseUpdate): """Pull resource updater and patches from the remote.""" pass
[docs]class ReleaseUpdateDownload(ReleaseUpdate): """Download resource updates/patches.""" pass
# Resource
[docs]class ResourceEvent(IocEvent): """Event with a resource.""" def __init__( self, resource: 'libioc.Resource.Resource', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.identifier = resource.full_name IocEvent.__init__(self, message)
[docs]class ResourceUpdate(ResourceEvent): """Update a resource.""" pass
[docs]class RunResourceUpdate(ResourceUpdate): """Run the update of a resource.""" pass
[docs]class ExecuteResourceUpdate(ResourceUpdate): """Execute the updater script in a jail.""" pass
# ZFS
[docs]class ZFSEvent(IocEvent): """Event related to ZFS storage.""" def __init__( self, zfs_object: libzfs.ZFSObject, message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.identifier = zfs_object.name self.zfs_object = zfs_object IocEvent.__init__(self, message=message, scope=scope)
[docs]class ZFSDatasetRename(ZFSEvent): """Rename a ZFS dataset.""" def __init__( self, dataset: libzfs.ZFSDataset, message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: ZFSEvent.__init__( self, zfs_object=dataset, message=message, scope=scope )
[docs]class ZFSDatasetDestroy(ZFSEvent): """Rename a ZFS dataset.""" def __init__( self, dataset: libzfs.ZFSDataset, message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: ZFSEvent.__init__( self, zfs_object=dataset, message=message, scope=scope )
[docs]class ZFSSnapshotRename(ZFSEvent): """Rename a ZFS snapshot.""" def __init__( self, snapshot: libzfs.ZFSDataset, message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: ZFSEvent.__init__( self, zfs_object=snapshot, message=message, scope=scope )
[docs]class ZFSSnapshotClone(ZFSEvent): """Clone a ZFS snapshot to a target.""" def __init__( self, snapshot: libzfs.ZFSDataset, target: str, scope: typing.Optional[Scope]=None ) -> None: message = f"Could not clone to {target}" ZFSEvent.__init__( self, zfs_object=snapshot, message=message, scope=scope )
[docs]class ZFSSnapshotRollback(ZFSEvent): """Rollback a ZFS dataset to a snapshot.""" def __init__( self, snapshot: libzfs.ZFSSnapshot, target: str, scope: typing.Optional[Scope]=None ) -> None: message = f"Rolling back {target} to snapshot {snapshot.snapshot_name}" ZFSEvent.__init__( self, zfs_object=snapshot, message=message, scope=scope )
# Backup
[docs]class ResourceBackup(IocEvent): """Events that occur when backing up a resource.""" resource: 'libioc.Resource.Resource' def __init__( self, resource: 'libioc.Resource.Resource', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: if "name" in resource.__dir__(): self.identifier = resource.name else: self.identifier = resource.dataset_name self.resource = resource IocEvent.__init__( self, message=message, scope=scope )
[docs]class ExportConfig(ResourceBackup): """Event that occurs when the config of a resource is exported.""" pass
[docs]class ExportFstab(ResourceBackup): """Event that occurs when the fstab file of a jail is exported.""" pass
[docs]class ExportRootDataset(ResourceBackup): """Export a resources root dataset.""" pass
[docs]class ExportOtherDatasets(ResourceBackup): """Event that occurs when other resource datasets get exported.""" pass
[docs]class ExportOtherDataset(ResourceBackup): """Export one of a resources datasets.""" dataset: libzfs.ZFSDataset def __init__( self, resource: 'libioc.Resource.Resource', dataset: libzfs.ZFSDataset, flags: typing.Set[libzfs.SendFlag]=set(), message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.dataset = dataset self.flags = flags ResourceBackup.__init__( self, resource=resource, message=message, scope=scope ) # The identifier is the dataset name relative to the resource relative_dataset_name = dataset.name[len(resource.dataset_name):] self.identifier = str(self.identifier) + str(relative_dataset_name)
[docs]class BundleBackup(ResourceBackup): """Bundle exported data into a backup archive.""" destination: str def __init__( self, destination: str, resource: 'libioc.Resource.Resource', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.destination = destination ResourceBackup.__init__( self, resource=resource, message=message, scope=scope )
# Backup import/restore
[docs]class ExtractBundle(ResourceBackup): """Extract a bundled backup archive.""" destination: str resource: 'libioc.Resource.Resource' def __init__( self, source: str, destination: str, resource: 'libioc.Resource.Resource', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.destination = destination self.resource = resource ResourceBackup.__init__( self, resource=resource, message=message, scope=scope )
[docs]class ImportConfig(ResourceBackup): """Event that occurs when the config of a resource is imported.""" pass
[docs]class ImportFstab(ResourceBackup): """Event that occurs when the fstab file of a jail is imported.""" pass
[docs]class ImportRootDataset(ResourceBackup): """Import data from an an archived root dataset.""" pass
[docs]class ImportOtherDatasets(ResourceBackup): """Event that occurs when other resource datasets get exported.""" pass
[docs]class ImportOtherDataset(ResourceBackup): """Export one of a resources datasets.""" def __init__( self, resource: 'libioc.Resource.Resource', dataset_name: str, message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.identifier = dataset_name ResourceBackup.__init__( self, resource=resource, scope=scope ) @property def dataset_name(self) -> str: """Map the event identifier to dataset_name.""" return str(self.identifier)
# CLI
[docs]class JailRestart(JailEvent): """Restart a jail.""" pass
[docs]class JailSoftShutdown(JailEvent): """Soft-restart a jail.""" pass
[docs]class JailStart(JailEvent): """Start a jail.""" pass
[docs]class JailDependantsStart(JailEvent): """Start dependant jails.""" started_jails: typing.List['libioc.Jail.JailGenerator'] def __init__( self, jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: try: self.identifier = jail.full_name except AttributeError: self.identifier = None self.started_jails = [] JailEvent.__init__(self, jail=jail, message=message, scope=scope)
[docs] def end( self, message: typing.Optional[str]=None, started_jails: typing.List['libioc.Jail.JailGenerator']=[], ) -> 'IocEvent': """Successfully finish starting dependant Jails.""" self.started_jails = started_jails return JailEvent.end(self, message)
[docs]class JailDependantStart(JailEvent): """Start one dependant jail.""" pass
[docs]class JailProvisioning(JailEvent): """Provision a jail.""" pass
[docs]class JailProvisioningAssetDownload(JailEvent): """Provision a jail.""" pass
# PKG
[docs]class PkgEvent(IocEvent): """Collection of events related to Pkg.""" pass
[docs]class PackageFetch(PkgEvent): """Fetch packages for offline installation.""" packages: typing.List[str] def __init__( self, packages: typing.List[str], message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.identifier = "global" self.packages = packages IocEvent.__init__(self, message=message, scope=scope)
[docs]class BootstrapPkg(JailEvent, PkgEvent): """Bootstrap pkg within a jail.""" pass
[docs]class PackageInstall(JailEvent, PkgEvent): """Install packages in a jail.""" def __init__( self, packages: typing.List[str], jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.packages = packages JailEvent.__init__(self, jail=jail, message=message, scope=scope)
[docs]class PackageRemove(JailEvent, PkgEvent): """Remove packages from a jail.""" def __init__( self, packages: typing.List[str], jail: 'libioc.Jail.JailGenerator', message: typing.Optional[str]=None, scope: typing.Optional[Scope]=None ) -> None: self.packages = packages JailEvent.__init__(self, jail=jail, message=message, scope=scope)
[docs]class PackageConfiguration(JailEvent, PkgEvent): """Install packages in a jail.""" pass