Exemplo n.º 1
0
    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,
            ),
        )
Exemplo n.º 2
0
    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,
            ),
        )
Exemplo n.º 3
0
 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())
Exemplo n.º 4
0
    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))
Exemplo n.º 5
0
 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())
Exemplo n.º 6
0
 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())
Exemplo n.º 7
0
 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()),
             )))
Exemplo n.º 8
0
# 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()
Exemplo n.º 9
0
    "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'" % (
Exemplo n.º 10
0
# 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.
    """
Exemplo n.º 11
0
__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" % (
Exemplo n.º 12
0
# 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()
Exemplo n.º 13
0
# 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()
Exemplo n.º 14
0
# 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()
Exemplo n.º 15
0
]

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())
Exemplo n.º 16
0
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()

Exemplo n.º 17
0
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.
    """
Exemplo n.º 18
0
    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
Exemplo n.º 19
0
    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]
Exemplo n.º 20
0
# 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.
Exemplo n.º 21
0
__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.
Exemplo n.º 22
0
# 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:
Exemplo n.º 23
0
"""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()
Exemplo n.º 24
0
# 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)

Exemplo n.º 25
0
# 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.
Exemplo n.º 26
0
# 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
)
Exemplo n.º 27
0
# 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()
Exemplo n.º 28
0
# 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)
Exemplo n.º 29
0
__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)