def test__can_watch_any_signal(self): django_signal = pick_django_signal() manager = SignalsManager() manager.watch( django_signal, sentinel.callback, sender=sentinel.sender, weak=sentinel.weak, dispatch_uid=sentinel.dispatch_uid, ) self.assertThat(manager._signals, HasLength(1)) [signal] = manager._signals self.assertThat( signal.connect, MatchesPartialCall( django_signal.connect, sentinel.callback, sender=sentinel.sender, weak=sentinel.weak, dispatch_uid=sentinel.dispatch_uid, ), ) self.assertThat( signal.disconnect, MatchesPartialCall( django_signal.disconnect, sentinel.callback, sender=sentinel.sender, dispatch_uid=sentinel.dispatch_uid, ), )
def test__can_watch_fields(self): connect_to_field_change = self.patch_autospec( signals_module, "connect_to_field_change") connect_to_field_change.return_value = ( sentinel.connect, sentinel.disconnect, ) manager = SignalsManager() manager.watch_fields(sentinel.callback, sentinel.model, sentinel.fields, sentinel.delete) self.assertThat( manager._signals, Equals({Signal(sentinel.connect, sentinel.disconnect)}), ) self.assertThat( connect_to_field_change, MockCalledOnceWith( sentinel.callback, sentinel.model, sentinel.fields, sentinel.delete, ), )
def test__add_adds_the_signal(self): manager = SignalsManager() signal = self.make_Signal() self.assertThat(manager.add(signal), Is(signal)) self.assertThat(manager._signals, Equals({signal})) # The manager is in its "new" state, neither enabled nor disabled, so # the signal is not asked to connect or disconnect yet. self.assertThat(signal.connect, MockNotCalled()) self.assertThat(signal.disconnect, MockNotCalled())
def test__can_watch_config(self): callback = lambda: None config_name = factory.make_name("config") manager = SignalsManager() manager.watch_config(callback, config_name) self.assertThat(manager._signals, HasLength(1)) [signal] = manager._signals self.assertThat( signal.connect, MatchesPartialCall(Config.objects.config_changed_connect, config_name, callback)) self.assertThat( signal.disconnect, MatchesPartialCall(Config.objects.config_changed_disconnect, config_name, callback))
def test__add_disconnects_signal_if_manager_is_disabled(self): manager = SignalsManager() manager.disable() signal = self.make_Signal() manager.add(signal) self.assertThat(signal.connect, MockNotCalled()) self.assertThat(signal.disconnect, MockCalledOnceWith())
def test__remove_removes_the_signal(self): manager = SignalsManager() signal = self.make_Signal() manager.add(signal) manager.remove(signal) self.assertThat(manager._signals, HasLength(0)) self.assertThat(signal.connect, MockNotCalled()) self.assertThat(signal.disconnect, MockNotCalled())
def test__disable_disables_all_signals(self): manager = SignalsManager() signals = [self.make_Signal(), self.make_Signal()] for signal in signals: manager.add(signal) manager.disable() self.assertThat( signals, AllMatch( MatchesAll( AfterPreprocessing((lambda signal: signal.connect), MockNotCalled()), AfterPreprocessing((lambda signal: signal.disconnect), MockCalledOnceWith()), )))
# Copyright 2016 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to DHCPSnippet changes.""" __all__ = ["signals"] from django.db.models.signals import post_delete from maasserver.models import DHCPSnippet from maasserver.utils.signals import SignalsManager signals = SignalsManager() def post_delete_dhcp_snippet_clean_values(sender, instance, **kwargs): """Removes the just-deleted DHCPSnippet's set of values.""" for value in instance.value.previous_versions(): value.delete() signals.watch(post_delete, post_delete_dhcp_snippet_clean_values, sender=DHCPSnippet) # Enable all signals by default. signals.enable()
"signals", ] from maasserver.enum import NODE_STATUS_CHOICES_DICT from maasserver.models import ( Event, Node, ) from maasserver.models.node import NODE_STATUS from maasserver.utils.signals import SignalsManager from provisioningserver.events import ( EVENT_DETAILS, EVENT_TYPES, ) signals = SignalsManager() # Useful to disconnect this in testing. TODO: Use the signals manager instead. STATE_TRANSITION_EVENT_CONNECT = True def emit_state_transition_event(instance, old_values, **kwargs): """Send a status transition event.""" if not STATE_TRANSITION_EVENT_CONNECT: return node = instance [old_status] = old_values type_name = EVENT_TYPES.NODE_CHANGED_STATUS event_details = EVENT_DETAILS[type_name] description = "From '%s' to '%s'" % (
# Copyright 2016 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to boot source changes.""" from django.db.models.signals import post_delete, post_save from twisted.internet import reactor from maasserver.bootsources import cache_boot_sources from maasserver.models.bootsource import BootSource from maasserver.utils.orm import post_commit_do from maasserver.utils.signals import SignalsManager signals = SignalsManager() def save_boot_source_cache(sender, instance, *args, **kwargs): """On first run the ImportResourceService sets the default BootSource then caches the stream's contents as normal. Setting the default BootSource triggers this signal. This prevents updating the cache twice. """ # Don't run if the first row and newly created. if instance.id != 1 and BootSource.objects.count() != 0: update_boot_source_cache(sender, instance, *args, **kwargs) def update_boot_source_cache(sender, instance, *args, **kwargs): """Update the `BootSourceCache` using the updated source. This only begins after a successful commit to the database, and is then run in a thread. Nothing waits for its completion. """
__all__ = ["signals"] from maasserver.models import Event from maasserver.preseed import CURTIN_INSTALL_LOG from maasserver.utils.signals import SignalsManager from metadataserver.enum import ( RESULT_TYPE, SCRIPT_STATUS, SCRIPT_STATUS_CHOICES, SCRIPT_STATUS_FAILED, SCRIPT_STATUS_RUNNING, ) from metadataserver.models.scriptresult import ScriptResult from provisioningserver.events import EVENT_TYPES signals = SignalsManager() def emit_script_result_status_transition_event(script_result, old_values, **kwargs): """Send a status transition event.""" [old_status] = old_values if script_result.physical_blockdevice and script_result.interface: script_name = "%s on %s and %s" % ( script_result.name, script_result.physical_blockdevice.name, script_result.interface.name, ) elif script_result.physical_blockdevice: script_name = "%s on %s" % (
# Copyright 2017 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to user changes.""" __all__ = [ "signals", ] from django.contrib.auth.models import User from django.db.models.signals import pre_delete from maasserver.utils.signals import SignalsManager signals = SignalsManager() USER_CLASSES = [ User, ] def pre_delete_set_event_username(sender, instance, **kwargs): """Set username for events that reference user being deleted.""" for event in instance.event_set.all(): event.username = instance.username event.save() for klass in USER_CLASSES: signals.watch(pre_delete, pre_delete_set_event_username, sender=klass) # Enable all signals by default. signals.enable()
# Copyright 2020 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to PodHints changes.""" from django.db.models.signals import m2m_changed from maasserver.models import PodHints from maasserver.utils.signals import SignalsManager signals = SignalsManager() def pod_nodes_changed(sender, instance, action, reverse, model, pk_set, **kwargs): if action == "post_remove": # Recalculate resources based on the remaining Nodes. instance.pod.sync_hints_from_nodes() signals.watch(m2m_changed, pod_nodes_changed, sender=PodHints.nodes.through) # Enable all signals by default signals.enable()
# Copyright 2016 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to boot resource file changes.""" __all__ = ["signals"] from django.db.models.signals import post_delete from maasserver.models.bootresourcefile import BootResourceFile from maasserver.models.largefile import LargeFile from maasserver.utils.signals import SignalsManager signals = SignalsManager() def delete_large_file(sender, instance, **kwargs): """Call delete on the LargeFile, now that the relation has been removed. If this was the only resource file referencing this LargeFile then it will be delete. This is done using the `post_delete` signal because only then has the relation been removed. """ try: largefile = instance.largefile except LargeFile.DoesNotExist: pass # Nothing to do. else: if largefile is not None: largefile.delete()
] from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import ( post_delete, post_init, post_save, pre_delete, pre_save, ) from maasserver.models import StaticIPAddress from maasserver.utils.signals import SignalsManager from provisioningserver.logger import LegacyLogger log = LegacyLogger() signals = SignalsManager() def pre_delete_record_relations_on_delete(sender, instance, **kwargs): """Store the instance's bmc_set for use in post_delete. It is coerced to a set to force it to be evaluated here in the pre_delete handler. Otherwise, the call will be deferred until evaluated in post_delete, where the results will be invalid because the instance will be gone. This information is necessary so any BMC's using this deleted IP address can be called on in post_delete to make their own StaticIPAddresses. """ instance.__previous_bmcs = set(instance.bmc_set.all()) instance.__previous_dnsresources = set(instance.dnsresource_set.all())
from maasserver.models.node import Controller, Node from maasserver.models.staticipaddress import StaticIPAddress from maasserver.utils.signals import SignalsManager from provisioningserver.logger import LegacyLogger INTERFACE_CLASSES = [ BondInterface, BridgeInterface, Interface, PhysicalInterface, UnknownInterface, VLANInterface, ] signals = SignalsManager() log = LegacyLogger() class InterfaceVisitingThreadLocal(threading.local): """Since infinite recursion could occur in an arbitrary interface hierarchy, use thread-local storage to ensure that each interface is only visited once. """ def __init__(self): super().__init__() self.visiting = set()
from maasserver.utils.threads import deferToDatabase from provisioningserver.logger import LegacyLogger from provisioningserver.rpc.exceptions import UnknownPowerType from provisioningserver.utils.twisted import ( asynchronous, callOut, FOREVER, synchronous, ) from twisted.internet import reactor log = LegacyLogger() signals = SignalsManager() # Amount of time to wait after a node status has been updated to # perform a power query. WAIT_TO_QUERY = timedelta(seconds=20) @asynchronous(timeout=45) def update_power_state_of_node(system_id): """Query and update the power state of the given node. :return: The new power state of the node, a member of the `POWER_STATE` enum, or `None` which denotes that the status could not be queried or updated for any of a number of reasons; check the log. """
RegionController, Service, ) from maasserver.utils.signals import SignalsManager from metadataserver.models.nodekey import NodeKey NODE_CLASSES = [ Node, Machine, Device, Controller, RackController, RegionController, ] signals = SignalsManager() def pre_delete_update_events(sender, instance, **kwargs): """Update node hostname and id for events related to the node.""" instance.event_set.all().update(node_hostname=instance.hostname, node_id=None) for klass in NODE_CLASSES: signals.watch(pre_delete, pre_delete_update_events, sender=klass) def post_init_store_previous_status(sender, instance, **kwargs): """Store the pre_save status of the instance.""" instance.__previous_status = instance.status
RackController, RegionController, ) from maasserver.utils.signals import SignalsManager from provisioningserver.events import EVENT_DETAILS, EVENT_TYPES NODE_CLASSES = [ Node, Machine, Device, Controller, RackController, RegionController, ] signals = SignalsManager() # Useful to disconnect this in testing. TODO: Use the signals manager instead. STATE_TRANSITION_EVENT_CONNECT = True def emit_state_transition_event(instance, old_values, **kwargs): """Send a status transition event.""" if (instance.node_type != NODE_TYPE.MACHINE or not STATE_TRANSITION_EVENT_CONNECT): return node = instance [old_status] = old_values type_name = EVENT_TYPES.NODE_CHANGED_STATUS event_details = EVENT_DETAILS[type_name]
# Copyright 2016 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Delete KeySource when no more keys are present.""" __all__ = ["signals"] from django.db.models.signals import post_delete from maasserver.models import KeySource, SSHKey from maasserver.utils.signals import SignalsManager signals = SignalsManager() def post_delete_keysource_when_no_more_keys(sender, instance, **kwargs): """Delete Keysource when no more keys.""" keysource = None try: keysource = instance.keysource except KeySource.DoesNotExist: pass # Nothing to do. else: if keysource is not None: if not keysource.sshkey_set.exists(): keysource.delete() signals.watch(post_delete, post_delete_keysource_when_no_more_keys, sender=SSHKey) # Enable all signals by default.
__all__ = [ "signals", ] from django.db.models.signals import ( post_delete, post_save, ) from maasserver.enum import FILESYSTEM_GROUP_TYPE from maasserver.models.blockdevice import BlockDevice from maasserver.models.filesystemgroup import FilesystemGroup from maasserver.models.physicalblockdevice import PhysicalBlockDevice from maasserver.models.virtualblockdevice import VirtualBlockDevice from maasserver.utils.signals import SignalsManager signals = SignalsManager() senders = {BlockDevice, PhysicalBlockDevice, VirtualBlockDevice} def update_filesystem_group(sender, instance, **kwargs): """Update all filesystem groups that this block device belongs to. Also if a virtual block device name has does not equal its filesystem group then update its filesystem group with the new name. """ block_device = instance.actual_instance groups = FilesystemGroup.objects.filter_by_block_device(block_device) for group in groups: # Re-save the group so the VirtualBlockDevice is updated. This will # fix the size of the VirtualBlockDevice if the size of this block # device has changed.
# Copyright 2016 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to BMC changes.""" from django.db.models.signals import post_delete, post_save, pre_delete from maasserver.enum import BMC_TYPE from maasserver.models import BMC, Pod, PodHints from maasserver.utils.signals import SignalsManager BMC_CLASSES = [BMC, Pod] signals = SignalsManager() def pre_delete_bmc_clean_orphaned_ip(sender, instance, **kwargs): """Stash the soon-to-be-deleted BMC's ip_address for use in post_delete.""" instance.__previous_ip_address = instance.ip_address for klass in BMC_CLASSES: signals.watch(pre_delete, pre_delete_bmc_clean_orphaned_ip, sender=klass) def post_delete_bmc_clean_orphaned_ip(sender, instance, **kwargs): """Removes the just-deleted BMC's ip_address if nobody else is using it. The potentially orphaned ip_address was stashed in the instance by the pre-delete signal handler. """ if instance.__previous_ip_address is None:
"""Respond to IP range changes.""" __all__ = [ "signals", ] from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import ( post_delete, post_save, ) from maasserver.models import IPRange from maasserver.utils.signals import SignalsManager signals = SignalsManager() def post_save_check_range_utilization(sender, instance, created, **kwargs): # Be careful when checking for the subnet. In rare cases, such as a # cascading delete, Django can sometimes pass stale model objects into # signal handlers, which will raise unexpected DoesNotExist exceptions, # and/or otherwise invalidate foreign key fields. # See bug #1702527 for more details. try: if instance.subnet is None: return except ObjectDoesNotExist: return instance.subnet.update_allocation_notification()
# GNU Affero General Public License version 3 (see the file LICENSE). """Respond to large file changes.""" __all__ = ["signals"] from django.db.models.signals import post_delete from maasserver.models.largefile import ( delete_large_object_content_later, LargeFile, ) from maasserver.utils.orm import post_commit_do from maasserver.utils.signals import SignalsManager signals = SignalsManager() def delete_large_object(sender, instance, **kwargs): """Delete the large object when the `LargeFile` is deleted. This is done using the `post_delete` signal instead of overriding delete on `LargeFile`, so it works correctly for both the model and `QuerySet`. """ if instance.content is not None: post_commit_do(delete_large_object_content_later, instance.content) signals.watch(post_delete, delete_large_object, LargeFile)
# Copyright 2016 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to partition changes.""" from django.db.models.signals import post_delete from maasserver.models.partition import Partition from maasserver.models.partitiontable import PartitionTable from maasserver.utils.signals import SignalsManager signals = SignalsManager() def delete_partition_table(sender, instance, **kwargs): """Delete the partition table if this is the last partition on the partition table.""" try: partition_table = instance.partition_table except PartitionTable.DoesNotExist: pass # Nothing to do. else: if partition_table.partitions.count() == 0: partition_table.delete() signals.watch(post_delete, delete_partition_table, Partition) # Enable all signals by default.
# Copyright 2017 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to ControllerInfo changes.""" __all__ = ["signals"] from django.db.models.signals import post_delete, post_save from maasserver.models import ControllerInfo, RackController, RegionController from maasserver.models.controllerinfo import update_version_notifications from maasserver.utils.signals import SignalsManager from provisioningserver.logger import LegacyLogger log = LegacyLogger() signals = SignalsManager() def post_save__update_version_notifications( sender, instance, created, **kwargs ): update_version_notifications() def post_delete__update_version_notifications(sender, instance, **kwargs): update_version_notifications() signals.watch( post_save, post_save__update_version_notifications, sender=ControllerInfo )
# Copyright 2012-2016 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Signals called when config values changed.""" __all__ = [ "signals", ] from maasserver.utils.signals import SignalsManager signals = SignalsManager() def dns_kms_setting_changed(sender, instance, created, **kwargs): from maasserver.models.domain import dns_kms_setting_changed dns_kms_setting_changed() # Changes to windows_kms_host. signals.watch_config(dns_kms_setting_changed, "windows_kms_host") # Enable all signals by default. signals.enable()
# Copyright 2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Respond to Subnet CIDR changes.""" __all__ = ["signals"] from django.db.models.signals import post_save from maasserver.enum import IPADDRESS_TYPE from maasserver.models import StaticIPAddress, Subnet from maasserver.utils.signals import SignalsManager signals = SignalsManager() def update_referenced_ip_addresses(subnet): """Updates the `StaticIPAddress`'s to ensure that they are linked to the correct subnet.""" # Remove the IP addresses that no longer fall with in the CIDR. remove_ips = StaticIPAddress.objects.filter( alloc_type=IPADDRESS_TYPE.USER_RESERVED, subnet_id=subnet.id) remove_ips = remove_ips.extra(where=["NOT(ip << %s)"], params=[subnet.cidr]) remove_ips.update(subnet=None) # Add the IP addresses that now fall into CIDR. add_ips = StaticIPAddress.objects.filter(subnet__isnull=True) add_ips = add_ips.extra(where=["ip << %s"], params=[subnet.cidr]) add_ips.update(subnet_id=subnet.id)
__all__ = [ "signals", ] from django.db.models.signals import ( post_delete, post_save, ) from maasserver.models.node import RackController from maasserver.models.regioncontrollerprocess import RegionControllerProcess from maasserver.models.regionrackrpcconnection import RegionRackRPCConnection from maasserver.models.service import Service from maasserver.utils.signals import SignalsManager signals = SignalsManager() def update_rackd_status(sender, instance, **kwargs): """Update status of the rackd service for the rack controller the RPC connection was added or removed. """ Service.objects.create_services_for(instance.rack_controller) instance.rack_controller.update_rackd_status() signals.watch(post_save, update_rackd_status, sender=RegionRackRPCConnection) signals.watch(post_delete, update_rackd_status, sender=RegionRackRPCConnection)