Jeremy Stretch 2 лет назад
Родитель
Сommit
2afce6c94b

+ 2 - 8
netbox/circuits/models/circuits.py

@@ -1,4 +1,3 @@
-from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
@@ -7,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
 from circuits.choices import *
 from circuits.choices import *
 from dcim.models import CabledObjectModel
 from dcim.models import CabledObjectModel
 from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
 from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
-from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin
+from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin
 
 
 __all__ = (
 __all__ = (
     'Circuit',
     'Circuit',
@@ -30,7 +29,7 @@ class CircuitType(OrganizationalModel):
         verbose_name_plural = _('circuit types')
         verbose_name_plural = _('circuit types')
 
 
 
 
-class Circuit(ImageAttachmentsMixin, PrimaryModel):
+class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
     """
     """
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
     circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
@@ -88,11 +87,6 @@ class Circuit(ImageAttachmentsMixin, PrimaryModel):
         help_text=_("Committed rate")
         help_text=_("Committed rate")
     )
     )
 
 
-    # Generic relations
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     # Cache associated CircuitTerminations
     # Cache associated CircuitTerminations
     termination_a = models.ForeignKey(
     termination_a = models.ForeignKey(
         to='circuits.CircuitTermination',
         to='circuits.CircuitTermination',

+ 3 - 13
netbox/circuits/models/providers.py

@@ -1,10 +1,10 @@
-from django.contrib.contenttypes.fields import GenericRelation
 from django.db import models
 from django.db import models
 from django.db.models import Q
 from django.db.models import Q
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from netbox.models import PrimaryModel
 from netbox.models import PrimaryModel
+from netbox.models.features import ContactsMixin
 
 
 __all__ = (
 __all__ = (
     'ProviderNetwork',
     'ProviderNetwork',
@@ -13,7 +13,7 @@ __all__ = (
 )
 )
 
 
 
 
-class Provider(PrimaryModel):
+class Provider(ContactsMixin, PrimaryModel):
     """
     """
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
     stores information pertinent to the user's relationship with the Provider.
     stores information pertinent to the user's relationship with the Provider.
@@ -35,11 +35,6 @@ class Provider(PrimaryModel):
         blank=True
         blank=True
     )
     )
 
 
-    # Generic relations
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     clone_fields = ()
     clone_fields = ()
 
 
     class Meta:
     class Meta:
@@ -54,7 +49,7 @@ class Provider(PrimaryModel):
         return reverse('circuits:provider', args=[self.pk])
         return reverse('circuits:provider', args=[self.pk])
 
 
 
 
-class ProviderAccount(PrimaryModel):
+class ProviderAccount(ContactsMixin, PrimaryModel):
     """
     """
     This is a discrete account within a provider.  Each Circuit belongs to a Provider Account.
     This is a discrete account within a provider.  Each Circuit belongs to a Provider Account.
     """
     """
@@ -73,11 +68,6 @@ class ProviderAccount(PrimaryModel):
         blank=True
         blank=True
     )
     )
 
 
-    # Generic relations
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     clone_fields = ('provider', )
     clone_fields = ('provider', )
 
 
     class Meta:
     class Meta:

+ 3 - 14
netbox/dcim/models/devices.py

@@ -3,7 +3,6 @@ import yaml
 
 
 from functools import cached_property
 from functools import cached_property
 
 
-from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
@@ -20,7 +19,7 @@ from extras.models import ConfigContextModel
 from extras.querysets import ConfigContextModelQuerySet
 from extras.querysets import ConfigContextModelQuerySet
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
-from netbox.models.features import ImageAttachmentsMixin
+from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
 from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
 from utilities.tracking import TrackingModelMixin
 from utilities.tracking import TrackingModelMixin
@@ -45,15 +44,10 @@ __all__ = (
 # Device Types
 # Device Types
 #
 #
 
 
-class Manufacturer(OrganizationalModel):
+class Manufacturer(ContactsMixin, OrganizationalModel):
     """
     """
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
     """
     """
-    # Generic relations
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     class Meta:
     class Meta:
         ordering = ('name',)
         ordering = ('name',)
         verbose_name = _('manufacturer')
         verbose_name = _('manufacturer')
@@ -531,7 +525,7 @@ def update_interface_bridges(device, interface_templates, module=None):
             interface.save()
             interface.save()
 
 
 
 
-class Device(ImageAttachmentsMixin, PrimaryModel, ConfigContextModel, TrackingModelMixin):
+class Device(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, ConfigContextModel, TrackingModelMixin):
     """
     """
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
     A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
     DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
     DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@@ -758,11 +752,6 @@ class Device(ImageAttachmentsMixin, PrimaryModel, ConfigContextModel, TrackingMo
         to_field='device'
         to_field='device'
     )
     )
 
 
-    # Generic relations
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     objects = ConfigContextModelQuerySet.as_manager()
     objects = ConfigContextModelQuerySet.as_manager()
 
 
     clone_fields = (
     clone_fields = (

+ 2 - 8
netbox/dcim/models/power.py

@@ -1,4 +1,3 @@
-from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
@@ -8,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
 from dcim.choices import *
 from dcim.choices import *
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from netbox.models import PrimaryModel
 from netbox.models import PrimaryModel
-from netbox.models.features import ImageAttachmentsMixin
+from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from utilities.validators import ExclusionValidator
 from utilities.validators import ExclusionValidator
 from .device_components import CabledObjectModel, PathEndpoint
 from .device_components import CabledObjectModel, PathEndpoint
 
 
@@ -22,7 +21,7 @@ __all__ = (
 # Power
 # Power
 #
 #
 
 
-class PowerPanel(ImageAttachmentsMixin, PrimaryModel):
+class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
     """
     """
     A distribution point for electrical power; e.g. a data center RPP.
     A distribution point for electrical power; e.g. a data center RPP.
     """
     """
@@ -41,11 +40,6 @@ class PowerPanel(ImageAttachmentsMixin, PrimaryModel):
         max_length=100
         max_length=100
     )
     )
 
 
-    # Generic relations
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     prerequisite_models = (
     prerequisite_models = (
         'dcim.Site',
         'dcim.Site',
     )
     )

+ 2 - 5
netbox/dcim/models/racks.py

@@ -15,7 +15,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.svg import RackElevationSVG
 from dcim.svg import RackElevationSVG
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
-from netbox.models.features import ImageAttachmentsMixin
+from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.utils import array_to_string, drange, to_grams
 from utilities.utils import array_to_string, drange, to_grams
@@ -53,7 +53,7 @@ class RackRole(OrganizationalModel):
         return reverse('dcim:rackrole', args=[self.pk])
         return reverse('dcim:rackrole', args=[self.pk])
 
 
 
 
-class Rack(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
+class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
     """
     """
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
     Each Rack is assigned to a Site and (optionally) a Location.
     Each Rack is assigned to a Site and (optionally) a Location.
@@ -194,9 +194,6 @@ class Rack(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
         object_id_field='scope_id',
         object_id_field='scope_id',
         related_query_name='rack'
         related_query_name='rack'
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     clone_fields = (
     clone_fields = (
         'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
         'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',

+ 5 - 19
netbox/dcim/models/sites.py

@@ -8,7 +8,7 @@ from timezone_field import TimeZoneField
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models import NestedGroupModel, PrimaryModel
-from netbox.models.features import ImageAttachmentsMixin
+from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from utilities.fields import NaturalOrderingField
 from utilities.fields import NaturalOrderingField
 
 
 __all__ = (
 __all__ = (
@@ -23,22 +23,18 @@ __all__ = (
 # Regions
 # Regions
 #
 #
 
 
-class Region(NestedGroupModel):
+class Region(ContactsMixin, NestedGroupModel):
     """
     """
     A region represents a geographic collection of sites. For example, you might create regions representing countries,
     A region represents a geographic collection of sites. For example, you might create regions representing countries,
     states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
     states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
     also considered to be members of its parent and ancestor region(s).
     also considered to be members of its parent and ancestor region(s).
     """
     """
-    # Generic relations
     vlan_groups = GenericRelation(
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         content_type_field='scope_type',
         object_id_field='scope_id',
         object_id_field='scope_id',
         related_query_name='region'
         related_query_name='region'
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     class Meta:
     class Meta:
         constraints = (
         constraints = (
@@ -80,22 +76,18 @@ class Region(NestedGroupModel):
 # Site groups
 # Site groups
 #
 #
 
 
-class SiteGroup(NestedGroupModel):
+class SiteGroup(ContactsMixin, NestedGroupModel):
     """
     """
     A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
     A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
     within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
     within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
     nested recursively to form a hierarchy.
     nested recursively to form a hierarchy.
     """
     """
-    # Generic relations
     vlan_groups = GenericRelation(
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         content_type_field='scope_type',
         object_id_field='scope_id',
         object_id_field='scope_id',
         related_query_name='site_group'
         related_query_name='site_group'
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     class Meta:
     class Meta:
         constraints = (
         constraints = (
@@ -137,7 +129,7 @@ class SiteGroup(NestedGroupModel):
 # Sites
 # Sites
 #
 #
 
 
-class Site(ImageAttachmentsMixin, PrimaryModel):
+class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
     """
     """
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
     field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
     field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
@@ -235,9 +227,6 @@ class Site(ImageAttachmentsMixin, PrimaryModel):
         object_id_field='scope_id',
         object_id_field='scope_id',
         related_query_name='site'
         related_query_name='site'
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     clone_fields = (
     clone_fields = (
         'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'physical_address', 'shipping_address',
         'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'physical_address', 'shipping_address',
@@ -263,7 +252,7 @@ class Site(ImageAttachmentsMixin, PrimaryModel):
 # Locations
 # Locations
 #
 #
 
 
-class Location(ImageAttachmentsMixin, NestedGroupModel):
+class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
     """
     """
     A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
     A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
     site, or a room within a building, for example.
     site, or a room within a building, for example.
@@ -294,9 +283,6 @@ class Location(ImageAttachmentsMixin, NestedGroupModel):
         object_id_field='scope_id',
         object_id_field='scope_id',
         related_query_name='location'
         related_query_name='location'
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     clone_fields = ('site', 'parent', 'status', 'tenant', 'description')
     clone_fields = ('site', 'parent', 'status', 'tenant', 'description')
     prerequisite_models = (
     prerequisite_models = (

+ 3 - 5
netbox/ipam/models/l2vpn.py

@@ -1,4 +1,4 @@
-from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
+from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
@@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
 from ipam.choices import L2VPNTypeChoices
 from ipam.choices import L2VPNTypeChoices
 from ipam.constants import L2VPN_ASSIGNMENT_MODELS
 from ipam.constants import L2VPN_ASSIGNMENT_MODELS
 from netbox.models import NetBoxModel, PrimaryModel
 from netbox.models import NetBoxModel, PrimaryModel
+from netbox.models.features import ContactsMixin
 
 
 __all__ = (
 __all__ = (
     'L2VPN',
     'L2VPN',
@@ -16,7 +17,7 @@ __all__ = (
 )
 )
 
 
 
 
-class L2VPN(PrimaryModel):
+class L2VPN(ContactsMixin, PrimaryModel):
     name = models.CharField(
     name = models.CharField(
         verbose_name=_('name'),
         verbose_name=_('name'),
         max_length=100,
         max_length=100,
@@ -54,9 +55,6 @@ class L2VPN(PrimaryModel):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     clone_fields = ('type',)
     clone_fields = ('type',)
 
 

+ 13 - 0
netbox/netbox/models/features.py

@@ -25,6 +25,7 @@ __all__ = (
     'BookmarksMixin',
     'BookmarksMixin',
     'ChangeLoggingMixin',
     'ChangeLoggingMixin',
     'CloningMixin',
     'CloningMixin',
+    'ContactsMixin',
     'CustomFieldsMixin',
     'CustomFieldsMixin',
     'CustomLinksMixin',
     'CustomLinksMixin',
     'CustomValidationMixin',
     'CustomValidationMixin',
@@ -320,6 +321,18 @@ class ImageAttachmentsMixin(models.Model):
         abstract = True
         abstract = True
 
 
 
 
+class ContactsMixin(models.Model):
+    """
+    Enables the assignments of Contacts (via ContactAssignment).
+    """
+    contacts = GenericRelation(
+        to='tenancy.ContactAssignment'
+    )
+
+    class Meta:
+        abstract = True
+
+
 class BookmarksMixin(models.Model):
 class BookmarksMixin(models.Model):
     """
     """
     Enables support for user bookmarks.
     Enables support for user bookmarks.

+ 2 - 7
netbox/tenancy/models/tenants.py

@@ -1,10 +1,10 @@
-from django.contrib.contenttypes.fields import GenericRelation
 from django.db import models
 from django.db import models
 from django.db.models import Q
 from django.db.models import Q
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models import NestedGroupModel, PrimaryModel
+from netbox.models.features import ContactsMixin
 
 
 __all__ = (
 __all__ = (
     'Tenant',
     'Tenant',
@@ -36,7 +36,7 @@ class TenantGroup(NestedGroupModel):
         return reverse('tenancy:tenantgroup', args=[self.pk])
         return reverse('tenancy:tenantgroup', args=[self.pk])
 
 
 
 
-class Tenant(PrimaryModel):
+class Tenant(ContactsMixin, PrimaryModel):
     """
     """
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     department.
     department.
@@ -57,11 +57,6 @@ class Tenant(PrimaryModel):
         null=True
         null=True
     )
     )
 
 
-    # Generic relations
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     clone_fields = (
     clone_fields = (
         'group', 'description',
         'group', 'description',
     )
     )

+ 3 - 9
netbox/virtualization/models/clusters.py

@@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
 
 
 from dcim.models import Device
 from dcim.models import Device
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
+from netbox.models.features import ContactsMixin
 from virtualization.choices import *
 from virtualization.choices import *
 
 
 __all__ = (
 __all__ = (
@@ -28,20 +29,16 @@ class ClusterType(OrganizationalModel):
         return reverse('virtualization:clustertype', args=[self.pk])
         return reverse('virtualization:clustertype', args=[self.pk])
 
 
 
 
-class ClusterGroup(OrganizationalModel):
+class ClusterGroup(ContactsMixin, OrganizationalModel):
     """
     """
     An organizational group of Clusters.
     An organizational group of Clusters.
     """
     """
-    # Generic relations
     vlan_groups = GenericRelation(
     vlan_groups = GenericRelation(
         to='ipam.VLANGroup',
         to='ipam.VLANGroup',
         content_type_field='scope_type',
         content_type_field='scope_type',
         object_id_field='scope_id',
         object_id_field='scope_id',
         related_query_name='cluster_group'
         related_query_name='cluster_group'
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     class Meta:
     class Meta:
         ordering = ('name',)
         ordering = ('name',)
@@ -52,7 +49,7 @@ class ClusterGroup(OrganizationalModel):
         return reverse('virtualization:clustergroup', args=[self.pk])
         return reverse('virtualization:clustergroup', args=[self.pk])
 
 
 
 
-class Cluster(PrimaryModel):
+class Cluster(ContactsMixin, PrimaryModel):
     """
     """
     A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
     A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
     """
     """
@@ -101,9 +98,6 @@ class Cluster(PrimaryModel):
         object_id_field='scope_id',
         object_id_field='scope_id',
         related_query_name='cluster'
         related_query_name='cluster'
     )
     )
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
 
 
     clone_fields = (
     clone_fields = (
         'type', 'group', 'status', 'tenant', 'site',
         'type', 'group', 'status', 'tenant', 'site',

+ 2 - 6
netbox/virtualization/models/virtualmachines.py

@@ -12,6 +12,7 @@ from extras.models import ConfigContextModel
 from extras.querysets import ConfigContextModelQuerySet
 from extras.querysets import ConfigContextModelQuerySet
 from netbox.config import get_config
 from netbox.config import get_config
 from netbox.models import NetBoxModel, PrimaryModel
 from netbox.models import NetBoxModel, PrimaryModel
+from netbox.models.features import ContactsMixin
 from utilities.fields import CounterCacheField, NaturalOrderingField
 from utilities.fields import CounterCacheField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
@@ -24,7 +25,7 @@ __all__ = (
 )
 )
 
 
 
 
-class VirtualMachine(PrimaryModel, ConfigContextModel):
+class VirtualMachine(ContactsMixin, PrimaryModel, ConfigContextModel):
     """
     """
     A virtual machine which runs inside a Cluster.
     A virtual machine which runs inside a Cluster.
     """
     """
@@ -129,11 +130,6 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
         to_field='virtual_machine'
         to_field='virtual_machine'
     )
     )
 
 
-    # Generic relation
-    contacts = GenericRelation(
-        to='tenancy.ContactAssignment'
-    )
-
     objects = ConfigContextModelQuerySet.as_manager()
     objects = ConfigContextModelQuerySet.as_manager()
 
 
     clone_fields = (
     clone_fields = (