diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index ce09862ae..1b6c3feb4 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -52,7 +52,7 @@ class ProviderForm(NetBoxModelForm): class Meta: model = Provider fields = [ - 'name', 'slug', 'asns', 'description', 'comments', 'tags', + 'name', 'slug', 'asns', 'description', 'owner', 'comments', 'tags', ] @@ -68,7 +68,7 @@ class ProviderAccountForm(NetBoxModelForm): class Meta: model = ProviderAccount fields = [ - 'provider', 'name', 'account', 'description', 'comments', 'tags', + 'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags', ] @@ -88,7 +88,7 @@ class ProviderNetworkForm(NetBoxModelForm): class Meta: model = ProviderNetwork fields = [ - 'provider', 'name', 'service_id', 'description', 'comments', 'tags', + 'provider', 'name', 'service_id', 'description', 'owner', 'comments', 'tags', ] @@ -96,7 +96,7 @@ class CircuitTypeForm(NetBoxModelForm): slug = SlugField() fieldsets = ( - FieldSet('name', 'slug', 'color', 'description', 'tags'), + FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'), ) class Meta: @@ -147,7 +147,7 @@ class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): model = Circuit fields = [ 'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate', - 'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags', + 'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'owner', 'comments', 'tags', ] widgets = { 'install_date': DatePicker(), @@ -244,7 +244,7 @@ class CircuitGroupForm(TenancyForm, NetBoxModelForm): class Meta: model = CircuitGroup fields = [ - 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags', + 'name', 'slug', 'description', 'tenant_group', 'tenant', 'owner', 'tags', ] @@ -317,7 +317,7 @@ class VirtualCircuitTypeForm(NetBoxModelForm): class Meta: model = VirtualCircuitType fields = [ - 'name', 'slug', 'color', 'description', 'tags', + 'name', 'slug', 'color', 'description', 'owner', 'tags', ] @@ -350,7 +350,7 @@ class VirtualCircuitForm(TenancyForm, NetBoxModelForm): model = VirtualCircuit fields = [ 'cid', 'provider_network', 'provider_account', 'type', 'status', 'description', 'tenant_group', 'tenant', - 'comments', 'tags', + 'owner', 'comments', 'tags', ] diff --git a/netbox/circuits/migrations/0053_owner.py b/netbox/circuits/migrations/0053_owner.py new file mode 100644 index 000000000..04fe46c61 --- /dev/null +++ b/netbox/circuits/migrations/0053_owner.py @@ -0,0 +1,68 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('circuits', '0052_extend_circuit_abs_distance_upper_limit'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='circuit', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='circuitgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='circuittype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='provider', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='provideraccount', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='providernetwork', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualcircuit', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualcircuittype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 0a683a381..6796b4f85 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -36,7 +36,8 @@ class DataSourceForm(NetBoxModelForm): class Meta: model = DataSource fields = [ - 'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'ignore_rules', 'comments', 'tags', + 'name', 'type', 'source_url', 'enabled', 'description', 'sync_interval', 'ignore_rules', 'owner', + 'comments', 'tags', ] widgets = { 'ignore_rules': forms.Textarea( diff --git a/netbox/core/migrations/0020_owner.py b/netbox/core/migrations/0020_owner.py new file mode 100644 index 000000000..ecb30fa3e --- /dev/null +++ b/netbox/core/migrations/0020_owner.py @@ -0,0 +1,24 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_configrevision_active'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='datasource', + name='owner', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name='+', + to='users.owner', + ), + ), + ] diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 32ea2d263..4e3037cfb 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -91,7 +91,7 @@ class RegionForm(NetBoxModelForm): class Meta: model = Region fields = ( - 'parent', 'name', 'slug', 'description', 'tags', 'comments', + 'parent', 'name', 'slug', 'description', 'owner', 'tags', 'comments', ) @@ -111,7 +111,7 @@ class SiteGroupForm(NetBoxModelForm): class Meta: model = SiteGroup fields = ( - 'parent', 'name', 'slug', 'description', 'comments', 'tags', + 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags', ) @@ -154,7 +154,7 @@ class SiteForm(TenancyForm, NetBoxModelForm): model = Site fields = ( 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone', - 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', + 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'owner', 'comments', 'tags', ) widgets = { 'physical_address': forms.Textarea( @@ -195,8 +195,8 @@ class LocationForm(TenancyForm, NetBoxModelForm): class Meta: model = Location fields = ( - 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', - 'facility', 'tags', 'comments', + 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'owner', + 'comments', 'tags', ) @@ -210,7 +210,7 @@ class RackRoleForm(NetBoxModelForm): class Meta: model = RackRole fields = [ - 'name', 'slug', 'color', 'description', 'tags', + 'name', 'slug', 'color', 'description', 'owner', 'tags', ] @@ -242,7 +242,7 @@ class RackTypeForm(NetBoxModelForm): fields = [ 'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', - 'weight_unit', 'description', 'comments', 'tags', + 'weight_unit', 'description', 'owner', 'comments', 'tags', ] @@ -288,7 +288,7 @@ class RackForm(TenancyForm, NetBoxModelForm): 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', - 'weight_unit', 'description', 'comments', 'tags', + 'weight_unit', 'description', 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -343,7 +343,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): class Meta: model = RackReservation fields = [ - 'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'rack', 'units', 'status', 'user', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] @@ -357,7 +357,7 @@ class ManufacturerForm(NetBoxModelForm): class Meta: model = Manufacturer fields = [ - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ] @@ -396,7 +396,7 @@ class DeviceTypeForm(NetBoxModelForm): fields = [ 'manufacturer', 'model', 'slug', 'default_platform', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', - 'description', 'comments', 'tags', + 'description', 'owner', 'comments', 'tags', ] widgets = { 'front_image': ClearableFileInput(attrs={ @@ -423,7 +423,7 @@ class ModuleTypeProfileForm(NetBoxModelForm): class Meta: model = ModuleTypeProfile fields = [ - 'name', 'description', 'schema', 'comments', 'tags', + 'name', 'description', 'schema', 'owner', 'comments', 'tags', ] @@ -452,7 +452,7 @@ class ModuleTypeForm(NetBoxModelForm): model = ModuleType fields = [ 'profile', 'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', - 'comments', 'tags', + 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -531,7 +531,7 @@ class DeviceRoleForm(NetBoxModelForm): class Meta: model = DeviceRole fields = [ - 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'comments', 'tags', + 'name', 'slug', 'parent', 'color', 'vm_role', 'config_template', 'description', 'owner', 'comments', 'tags', ] @@ -567,7 +567,7 @@ class PlatformForm(NetBoxModelForm): class Meta: model = Platform fields = [ - 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'comments', 'tags', + 'name', 'slug', 'parent', 'manufacturer', 'config_template', 'description', 'owner', 'comments', 'tags', ] @@ -677,7 +677,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm): 'name', 'role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', - 'comments', 'tags', 'local_context_data', + 'owner', 'comments', 'tags', 'local_context_data', ] def __init__(self, *args, **kwargs): @@ -788,7 +788,7 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm): model = Module fields = [ 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components', - 'adopt_components', 'description', 'comments', + 'adopt_components', 'description', 'owner', 'comments', ] def __init__(self, *args, **kwargs): @@ -828,7 +828,7 @@ class CableForm(TenancyForm, NetBoxModelForm): model = Cable fields = [ 'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color', - 'length', 'length_unit', 'description', 'comments', 'tags', + 'length', 'length_unit', 'description', 'owner', 'comments', 'tags', ] @@ -855,7 +855,7 @@ class PowerPanelForm(NetBoxModelForm): class Meta: model = PowerPanel fields = [ - 'site', 'location', 'name', 'description', 'comments', 'tags', + 'site', 'location', 'name', 'description', 'owner', 'comments', 'tags', ] @@ -887,7 +887,7 @@ class PowerFeedForm(TenancyForm, NetBoxModelForm): model = PowerFeed fields = [ 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', - 'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags' + 'max_utilization', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags' ] @@ -906,7 +906,7 @@ class VirtualChassisForm(NetBoxModelForm): class Meta: model = VirtualChassis fields = [ - 'name', 'domain', 'master', 'description', 'comments', 'tags', + 'name', 'domain', 'master', 'description', 'owner', 'comments', 'tags', ] widgets = { 'master': SelectWithPK(), @@ -1396,7 +1396,7 @@ class ConsolePortForm(ModularDeviceComponentForm): class Meta: model = ConsolePort fields = [ - 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', + 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags', ] @@ -1410,7 +1410,7 @@ class ConsoleServerPortForm(ModularDeviceComponentForm): class Meta: model = ConsoleServerPort fields = [ - 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags', + 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'owner', 'tags', ] @@ -1426,7 +1426,7 @@ class PowerPortForm(ModularDeviceComponentForm): model = PowerPort fields = [ 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', - 'description', 'tags', + 'description', 'owner', 'tags', ] @@ -1443,7 +1443,7 @@ class PowerOutletForm(ModularDeviceComponentForm): fieldsets = ( FieldSet( 'device', 'module', 'name', 'label', 'type', 'status', 'color', 'power_port', 'feed_leg', 'mark_connected', - 'description', 'tags', + 'description', 'owner', 'tags', ), ) @@ -1587,7 +1587,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): 'lag', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'poe_mode', 'poe_type', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address', - 'tags', + 'owner', 'tags', ] widgets = { 'speed': NumberWithOptions( @@ -1619,7 +1619,7 @@ class FrontPortForm(ModularDeviceComponentForm): model = FrontPort fields = [ 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected', - 'description', 'tags', + 'description', 'owner', 'tags', ] @@ -1633,7 +1633,8 @@ class RearPortForm(ModularDeviceComponentForm): class Meta: model = RearPort fields = [ - 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags', + 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'owner', + 'tags', ] @@ -1645,7 +1646,7 @@ class ModuleBayForm(ModularDeviceComponentForm): class Meta: model = ModuleBay fields = [ - 'device', 'module', 'name', 'label', 'position', 'description', 'tags', + 'device', 'module', 'name', 'label', 'position', 'description', 'owner', 'tags', ] @@ -1657,7 +1658,7 @@ class DeviceBayForm(DeviceComponentForm): class Meta: model = DeviceBay fields = [ - 'device', 'name', 'label', 'description', 'tags', + 'device', 'name', 'label', 'description', 'owner', 'tags', ] @@ -1782,7 +1783,7 @@ class InventoryItemForm(DeviceComponentForm): model = InventoryItem fields = [ 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'status', 'description', 'tags', + 'status', 'description', 'owner', 'tags', ] def __init__(self, *args, **kwargs): @@ -1828,9 +1829,6 @@ class InventoryItemForm(DeviceComponentForm): self.instance.component = None -# Device component roles -# - class InventoryItemRoleForm(NetBoxModelForm): slug = SlugField() @@ -1841,7 +1839,7 @@ class InventoryItemRoleForm(NetBoxModelForm): class Meta: model = InventoryItemRole fields = [ - 'name', 'slug', 'color', 'description', 'tags', + 'name', 'slug', 'color', 'description', 'owner', 'tags', ] @@ -1881,7 +1879,7 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm): class Meta: model = VirtualDeviceContext fields = [ - 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', + 'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'owner', 'comments', 'tags' ] @@ -1929,7 +1927,7 @@ class MACAddressForm(NetBoxModelForm): class Meta: model = MACAddress fields = [ - 'mac_address', 'interface', 'vminterface', 'description', 'tags', + 'mac_address', 'interface', 'vminterface', 'description', 'owner', 'tags', ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/migrations/0216_owner.py b/netbox/dcim/migrations/0216_owner.py new file mode 100644 index 000000000..a7c5aa899 --- /dev/null +++ b/netbox/dcim/migrations/0216_owner.py @@ -0,0 +1,243 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0215_rackreservation_status'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='cable', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='consoleport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='consoleserverport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='device', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='devicebay', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='devicerole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='devicetype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='frontport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='interface', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='inventoryitem', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='inventoryitemrole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='location', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='macaddress', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='manufacturer', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='module', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='modulebay', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='moduletype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='moduletypeprofile', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='platform', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='powerfeed', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='poweroutlet', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='powerpanel', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='powerport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rack', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rackreservation', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rackrole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='racktype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rearport', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='region', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='site', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='sitegroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualchassis', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualdevicecontext', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 9c44e0494..3e801a8e9 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -14,6 +14,7 @@ from dcim.fields import WWNField from dcim.models.mixins import InterfaceValidationMixin from netbox.choices import ColorChoices from netbox.models import OrganizationalModel, NetBoxModel +from netbox.models.mixins import OwnerMixin from utilities.fields import ColorField, NaturalOrderingField from utilities.mptt import TreeManager from utilities.ordering import naturalize_interface @@ -40,7 +41,7 @@ __all__ = ( ) -class ComponentModel(NetBoxModel): +class ComponentModel(OwnerMixin, NetBoxModel): """ An abstract model inherited by any model which has a parent Device. """ diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 37ee10604..39f02f5b1 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -179,7 +179,7 @@ class CustomFieldChoiceSetForm(ChangelogMessageMixin, forms.ModelForm): class Meta: model = CustomFieldChoiceSet - fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically') + fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', 'owner') def __init__(self, *args, initial=None, **kwargs): super().__init__(*args, initial=initial, **kwargs) @@ -480,7 +480,7 @@ class EventRuleForm(NetBoxModelForm): model = EventRule fields = ( 'object_types', 'name', 'description', 'enabled', 'event_types', 'conditions', 'action_type', - 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags' + 'action_object_type', 'action_object_id', 'action_data', 'owner', 'comments', 'tags' ) widgets = { 'conditions': forms.Textarea(attrs={'class': 'font-monospace'}), @@ -582,7 +582,7 @@ class TagForm(ChangelogMessageMixin, forms.ModelForm): class Meta: model = Tag fields = [ - 'name', 'slug', 'color', 'weight', 'description', 'object_types', + 'name', 'slug', 'color', 'weight', 'description', 'object_types', 'owner', ] @@ -606,7 +606,8 @@ class ConfigContextProfileForm(SyncedDataMixin, NetBoxModelForm): class Meta: model = ConfigContextProfile fields = ( - 'name', 'description', 'schema', 'data_source', 'data_file', 'auto_sync_enabled', 'comments', 'tags', + 'name', 'description', 'schema', 'data_source', 'data_file', 'auto_sync_enabled', 'owner', 'comments', + 'tags', ) @@ -701,7 +702,7 @@ class ConfigContextForm(ChangelogMessageMixin, SyncedDataMixin, forms.ModelForm) fields = ( 'name', 'weight', 'profile', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations', 'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', - 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_file', 'auto_sync_enabled', + 'tenant_groups', 'tenants', 'owner', 'tags', 'data_source', 'data_file', 'auto_sync_enabled', ) def __init__(self, *args, initial=None, **kwargs): diff --git a/netbox/extras/migrations/0134_owner.py b/netbox/extras/migrations/0134_owner.py new file mode 100644 index 000000000..1a01fd95b --- /dev/null +++ b/netbox/extras/migrations/0134_owner.py @@ -0,0 +1,89 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('extras', '0133_make_cf_minmax_decimal'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='configcontext', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='configcontextprofile', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='configtemplate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='customfield', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='customfieldchoiceset', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='customlink', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='eventrule', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='exporttemplate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='savedfilter', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tag', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='webhook', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index a9d233568..ea861e673 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -13,6 +13,7 @@ from extras.models.mixins import RenderTemplateMixin from extras.querysets import ConfigContextQuerySet from netbox.models import ChangeLoggedModel, PrimaryModel from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin +from netbox.models.mixins import OwnerMixin from utilities.data import deepmerge from utilities.jsonschema import validate_schema @@ -68,7 +69,7 @@ class ConfigContextProfile(SyncedDataMixin, PrimaryModel): sync_data.alters_data = True -class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLoggedModel): +class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, OwnerMixin, ChangeLoggedModel): """ A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B @@ -266,7 +267,13 @@ class ConfigContextModel(models.Model): # class ConfigTemplate( - RenderTemplateMixin, SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel + RenderTemplateMixin, + SyncedDataMixin, + CustomLinksMixin, + ExportTemplatesMixin, + OwnerMixin, + TagsMixin, + ChangeLoggedModel, ): name = models.CharField( verbose_name=_('name'), diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index b1d22ee0b..3dea7c1b0 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -21,6 +21,7 @@ from extras.choices import * from extras.data import CHOICE_SETS from netbox.models import ChangeLoggedModel from netbox.models.features import CloningMixin, ExportTemplatesMixin +from netbox.models.mixins import OwnerMixin from netbox.search import FieldTypes from utilities import filters from utilities.datetime import datetime_from_timestamp @@ -70,7 +71,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): } -class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class CustomField(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): object_types = models.ManyToManyField( to='contenttypes.ContentType', related_name='custom_fields', @@ -773,7 +774,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): raise ValidationError(_("Required field cannot be empty.")) -class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): """ Represents a set of choices available for choice and multi-choice custom fields. """ diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 7361d087d..52ced1835 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -25,6 +25,7 @@ from netbox.models import ChangeLoggedModel from netbox.models.features import ( CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin, has_feature ) +from netbox.models.mixins import OwnerMixin from utilities.html import clean_html from utilities.jinja2 import render_jinja2 from utilities.querydict import dict_to_querydict @@ -44,7 +45,7 @@ __all__ = ( ) -class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): +class EventRule(CustomFieldsMixin, ExportTemplatesMixin, OwnerMixin, TagsMixin, ChangeLoggedModel): """ An EventRule defines an action to be taken automatically in response to a specific set of events, such as when a specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a @@ -155,7 +156,7 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged return False -class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): +class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, OwnerMixin, ChangeLoggedModel): """ A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or delete in NetBox. The request will contain a representation of the object, which the remote application can act on. @@ -294,7 +295,7 @@ class Webhook(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedMo return render_jinja2(self.payload_url, context) -class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class CustomLink(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): """ A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template code to be rendered with an object as context. @@ -394,7 +395,14 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): } -class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, RenderTemplateMixin): +class ExportTemplate( + SyncedDataMixin, + CloningMixin, + ExportTemplatesMixin, + OwnerMixin, + ChangeLoggedModel, + RenderTemplateMixin, +): object_types = models.ManyToManyField( to='contenttypes.ContentType', related_name='export_templates', @@ -456,7 +464,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change return _context -class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): +class SavedFilter(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel): """ A set of predefined keyword parameters that can be reused to filter for specific objects. """ diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 0df76d7b3..dc98ae65b 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -8,6 +8,7 @@ from taggit.models import TagBase, GenericTaggedItemBase from netbox.choices import ColorChoices from netbox.models import ChangeLoggedModel from netbox.models.features import CloningMixin, ExportTemplatesMixin +from netbox.models.mixins import OwnerMixin from utilities.fields import ColorField from utilities.querysets import RestrictedQuerySet @@ -21,7 +22,7 @@ __all__ = ( # Tags # -class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase): +class Tag(CloningMixin, ExportTemplatesMixin, OwnerMixin, ChangeLoggedModel, TagBase): id = models.BigAutoField( primary_key=True ) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 399198c52..1e2f23da6 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -72,7 +72,7 @@ class VRFForm(TenancyForm, NetBoxModelForm): model = VRF fields = [ 'name', 'rd', 'enforce_unique', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'description', - 'comments', 'tags', + 'owner', 'comments', 'tags', ] labels = { 'rd': "RD", @@ -89,7 +89,7 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm): class Meta: model = RouteTarget fields = [ - 'name', 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'name', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] @@ -103,7 +103,7 @@ class RIRForm(NetBoxModelForm): class Meta: model = RIR fields = [ - 'name', 'slug', 'is_private', 'description', 'tags', + 'name', 'slug', 'is_private', 'description', 'owner', 'tags', ] @@ -123,7 +123,7 @@ class AggregateForm(TenancyForm, NetBoxModelForm): class Meta: model = Aggregate fields = [ - 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'prefix', 'rir', 'date_added', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] widgets = { 'date_added': DatePicker(), @@ -145,7 +145,7 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm): class Meta: model = ASNRange fields = [ - 'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'description', 'tags' + 'name', 'slug', 'rir', 'start', 'end', 'tenant_group', 'tenant', 'owner', 'description', 'tags' ] @@ -170,7 +170,7 @@ class ASNForm(TenancyForm, NetBoxModelForm): class Meta: model = ASN fields = [ - 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'comments', 'tags' + 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags' ] widgets = { 'date_added': DatePicker(), @@ -198,7 +198,7 @@ class RoleForm(NetBoxModelForm): class Meta: model = Role fields = [ - 'name', 'slug', 'weight', 'description', 'tags', + 'name', 'slug', 'weight', 'description', 'owner', 'tags', ] @@ -238,7 +238,7 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm): model = Prefix fields = [ 'prefix', 'vrf', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'scope_type', 'tenant_group', - 'tenant', 'description', 'comments', 'tags', + 'tenant', 'description', 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -276,7 +276,7 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): model = IPRange fields = [ 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated', - 'mark_utilized', 'description', 'comments', 'tags', + 'mark_utilized', 'description', 'owner', 'comments', 'tags', ] @@ -344,7 +344,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): model = IPAddress fields = [ 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside', - 'tenant_group', 'tenant', 'description', 'comments', 'tags', + 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] def __init__(self, *args, **kwargs): @@ -523,7 +523,7 @@ class FHRPGroupForm(NetBoxModelForm): model = FHRPGroup fields = ( 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description', - 'comments', 'tags', + 'owner', 'comments', 'tags', ) def save(self, *args, **kwargs): @@ -628,7 +628,7 @@ class VLANGroupForm(TenancyForm, NetBoxModelForm): class Meta: model = VLANGroup fields = [ - 'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tenant_group', 'tenant', 'tags', + 'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tenant_group', 'tenant', 'owner', 'tags', ] def __init__(self, *args, **kwargs): @@ -704,7 +704,7 @@ class VLANForm(TenancyForm, NetBoxModelForm): model = VLAN fields = [ 'site', 'group', 'vid', 'name', 'status', 'role', 'tenant_group', 'tenant', 'qinq_role', 'qinq_svlan', - 'description', 'comments', 'tags', + 'description', 'owner', 'comments', 'tags', ] @@ -717,7 +717,7 @@ class VLANTranslationPolicyForm(NetBoxModelForm): class Meta: model = VLANTranslationPolicy fields = [ - 'name', 'description', 'tags', + 'name', 'description', 'owner', 'tags', ] @@ -756,7 +756,7 @@ class ServiceTemplateForm(NetBoxModelForm): class Meta: model = ServiceTemplate - fields = ('name', 'protocol', 'ports', 'description', 'comments', 'tags') + fields = ('name', 'protocol', 'ports', 'description', 'owner', 'comments', 'tags') class ServiceForm(NetBoxModelForm): @@ -799,7 +799,7 @@ class ServiceForm(NetBoxModelForm): class Meta: model = Service fields = [ - 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'comments', 'tags', + 'name', 'protocol', 'ports', 'ipaddresses', 'description', 'owner', 'comments', 'tags', 'parent_object_type', ] diff --git a/netbox/ipam/migrations/0083_owner.py b/netbox/ipam/migrations/0083_owner.py new file mode 100644 index 000000000..307963ba0 --- /dev/null +++ b/netbox/ipam/migrations/0083_owner.py @@ -0,0 +1,124 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('ipam', '0082_add_prefix_network_containment_indexes'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='aggregate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='asn', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='asnrange', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='fhrpgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipaddress', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='iprange', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='prefix', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='rir', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='role', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='routetarget', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='service', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='servicetemplate', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vlan', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vlangroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vlantranslationpolicy', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vrf', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 14916a733..2eaf8a83a 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -11,7 +11,7 @@ from extras.models import CustomField, Tag from utilities.forms import BulkEditForm, CSVModelForm from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField from utilities.forms.mixins import CheckLastUpdatedMixin -from .mixins import ChangelogMessageMixin, CustomFieldsMixin, SavedFiltersMixin, TagsMixin +from .mixins import ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, SavedFiltersMixin, TagsMixin __all__ = ( 'NetBoxModelForm', @@ -21,7 +21,14 @@ __all__ = ( ) -class NetBoxModelForm(ChangelogMessageMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm): +class NetBoxModelForm( + ChangelogMessageMixin, + CheckLastUpdatedMixin, + CustomFieldsMixin, + OwnerMixin, + TagsMixin, + forms.ModelForm +): """ Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. @@ -100,7 +107,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): return customfield.to_form_field(for_csv_import=True) -class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, BulkEditForm): +class NetBoxModelBulkEditForm(ChangelogMessageMixin, CustomFieldsMixin, OwnerMixin, BulkEditForm): """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom fields and adding/removing tags. diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 4096ffb25..4ee11b0bb 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -4,11 +4,13 @@ from django.utils.translation import gettext as _ from core.models import ObjectType from extras.choices import * from extras.models import * -from utilities.forms.fields import DynamicModelMultipleChoiceField +from users.models import Owner +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField __all__ = ( 'ChangelogMessageMixin', 'CustomFieldsMixin', + 'OwnerMixin', 'SavedFiltersMixin', 'TagsMixin', ) @@ -118,3 +120,14 @@ class TagsMixin(forms.Form): object_type = ObjectType.objects.get_for_model(self._meta.model) if object_type and hasattr(self.fields['tags'].widget, 'add_query_param'): self.fields['tags'].widget.add_query_param('for_object_type_id', object_type.pk) + + +class OwnerMixin(forms.Form): + """ + Add an `owner` field to forms for models which support Owner assignment. + """ + owner = DynamicModelChoiceField( + queryset=Owner.objects.all(), + required=False, + label=_('Owner'), + ) diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index b06718136..59475b060 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _ from mptt.models import MPTTModel, TreeForeignKey from netbox.models.features import * +from netbox.models.mixins import OwnerMixin from utilities.mptt import TreeManager from utilities.querysets import RestrictedQuerySet from utilities.views import get_viewname @@ -120,7 +121,7 @@ class NetBoxModel(NetBoxFeatureSet, BaseModel): # NetBox internal base models # -class PrimaryModel(NetBoxModel): +class PrimaryModel(OwnerMixin, NetBoxModel): """ Primary models represent real objects within the infrastructure being modeled. """ @@ -138,7 +139,7 @@ class PrimaryModel(NetBoxModel): abstract = True -class NestedGroupModel(NetBoxFeatureSet, MPTTModel): +class NestedGroupModel(NetBoxFeatureSet, OwnerMixin, MPTTModel): """ Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest recursively using MPTT. Within each parent, each child instance must have a unique name. @@ -190,7 +191,7 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): }) -class OrganizationalModel(NetBoxModel): +class OrganizationalModel(OwnerMixin, NetBoxModel): """ Organizational models are those which are used solely to categorize and qualify other objects, and do not convey any real information about the infrastructure being modeled (for example, functional device roles). Organizational diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index e0d03d6e7..df4284f28 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -641,6 +641,7 @@ register_model_feature('image_attachments', lambda model: issubclass(model, Imag register_model_feature('jobs', lambda model: issubclass(model, JobsMixin)) register_model_feature('journaling', lambda model: issubclass(model, JournalingMixin)) register_model_feature('notifications', lambda model: issubclass(model, NotificationsMixin)) +register_model_feature('owner', lambda model: issubclass(model, OwnerMixin)) register_model_feature('synced_data', lambda model: issubclass(model, SyncedDataMixin)) register_model_feature('tags', lambda model: issubclass(model, TagsMixin)) diff --git a/netbox/netbox/models/mixins.py b/netbox/netbox/models/mixins.py index 13af8aaf5..10798796d 100644 --- a/netbox/netbox/models/mixins.py +++ b/netbox/netbox/models/mixins.py @@ -7,10 +7,27 @@ from utilities.conversion import to_grams, to_meters __all__ = ( 'DistanceMixin', + 'OwnerMixin', 'WeightMixin', ) +class OwnerMixin(models.Model): + """ + Adds a ForeignKey to users.Owner to indicate an object's owner. + """ + owner = models.ForeignKey( + to='users.Owner', + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + + class Meta: + abstract = True + + class WeightMixin(models.Model): weight = models.DecimalField( verbose_name=_('weight'), diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 40b795d11..c4022cbc2 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -22,6 +22,15 @@ {% endif %} + {% if form.owner %} +
+
+

{% trans "Ownership" %}

+
+ {% render_field form.owner %} +
+ {% endif %} + {% if form.comments %}
{% render_field form.comments %} diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 5b1bd7339..e442a9418 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -36,7 +36,7 @@ class TenantGroupForm(NetBoxModelForm): class Meta: model = TenantGroup fields = [ - 'parent', 'name', 'slug', 'description', 'tags', 'comments' + 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags', ] @@ -56,7 +56,7 @@ class TenantForm(NetBoxModelForm): class Meta: model = Tenant fields = ( - 'name', 'slug', 'group', 'description', 'comments', 'tags', + 'name', 'slug', 'group', 'description', 'owner', 'comments', 'tags', ) @@ -79,7 +79,7 @@ class ContactGroupForm(NetBoxModelForm): class Meta: model = ContactGroup - fields = ('parent', 'name', 'slug', 'description', 'tags', 'comments') + fields = ('parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags') class ContactRoleForm(NetBoxModelForm): @@ -91,7 +91,7 @@ class ContactRoleForm(NetBoxModelForm): class Meta: model = ContactRole - fields = ('name', 'slug', 'description', 'tags') + fields = ('name', 'slug', 'description', 'owner', 'tags') class ContactForm(NetBoxModelForm): @@ -117,7 +117,7 @@ class ContactForm(NetBoxModelForm): class Meta: model = Contact fields = ( - 'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'comments', 'tags', + 'groups', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', 'owner', 'comments', 'tags', ) widgets = { 'address': forms.Textarea(attrs={'rows': 3}), diff --git a/netbox/tenancy/migrations/0021_owner.py b/netbox/tenancy/migrations/0021_owner.py new file mode 100644 index 000000000..b6fedda88 --- /dev/null +++ b/netbox/tenancy/migrations/0021_owner.py @@ -0,0 +1,47 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('tenancy', '0020_remove_contactgroupmembership'), + ('users', '0015_owner'), + ] + + operations = [ + migrations.AddField( + model_name='contact', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='contactgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='contactrole', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tenant', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tenantgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 82f8d8315..2e47631a7 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -42,7 +42,7 @@ class ClusterTypeForm(NetBoxModelForm): class Meta: model = ClusterType fields = ( - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ) @@ -56,7 +56,7 @@ class ClusterGroupForm(NetBoxModelForm): class Meta: model = ClusterGroup fields = ( - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ) @@ -83,7 +83,7 @@ class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm): class Meta: model = Cluster fields = ( - 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'description', 'comments', 'tags', + 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'description', 'owner', 'comments', 'tags', ) @@ -236,7 +236,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): model = VirtualMachine fields = [ 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', - 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'comments', 'tags', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'owner', 'comments', 'tags', 'local_context_data', 'config_template', ] @@ -387,7 +387,7 @@ class VMInterfaceForm(InterfaceCommonForm, VMComponentForm): fields = [ 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mtu', 'description', 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'primary_mac_address', - 'tags', + 'owner', 'tags', ] labels = { 'mode': _('802.1Q Mode'), @@ -406,5 +406,5 @@ class VirtualDiskForm(VMComponentForm): class Meta: model = VirtualDisk fields = [ - 'virtual_machine', 'name', 'size', 'description', 'tags', + 'virtual_machine', 'name', 'size', 'description', 'owner', 'tags', ] diff --git a/netbox/virtualization/migrations/0049_owner.py b/netbox/virtualization/migrations/0049_owner.py new file mode 100644 index 000000000..657d325ec --- /dev/null +++ b/netbox/virtualization/migrations/0049_owner.py @@ -0,0 +1,54 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0015_owner'), + ('virtualization', '0048_populate_mac_addresses'), + ] + + operations = [ + migrations.AddField( + model_name='cluster', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='clustergroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='clustertype', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualdisk', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='virtualmachine', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='vminterface', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index aca2a7dbd..de6fde745 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -15,6 +15,7 @@ from extras.querysets import ConfigContextModelQuerySet from netbox.config import get_config from netbox.models import NetBoxModel, PrimaryModel from netbox.models.features import ContactsMixin, ImageAttachmentsMixin +from netbox.models.mixins import OwnerMixin from utilities.fields import CounterCacheField, NaturalOrderingField from utilities.ordering import naturalize_interface from utilities.query_functions import CollateAsChar @@ -263,7 +264,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co # -class ComponentModel(NetBoxModel): +class ComponentModel(OwnerMixin, NetBoxModel): """ An abstract model inherited by any model which has a parent VirtualMachine. """ diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index 1bf5b580c..241ac9c38 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -39,7 +39,7 @@ class TunnelGroupForm(NetBoxModelForm): class Meta: model = TunnelGroup fields = [ - 'name', 'slug', 'description', 'tags', + 'name', 'slug', 'description', 'owner', 'tags', ] @@ -67,7 +67,7 @@ class TunnelForm(TenancyForm, NetBoxModelForm): model = Tunnel fields = [ 'name', 'status', 'group', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'tenant_group', - 'tenant', 'comments', 'tags', + 'tenant', 'owner', 'comments', 'tags', ] @@ -307,7 +307,7 @@ class IKEProposalForm(NetBoxModelForm): model = IKEProposal fields = [ 'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm', 'group', - 'sa_lifetime', 'comments', 'tags', + 'sa_lifetime', 'owner', 'comments', 'tags', ] @@ -326,7 +326,7 @@ class IKEPolicyForm(NetBoxModelForm): class Meta: model = IKEPolicy fields = [ - 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments', 'tags', + 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'owner', 'comments', 'tags', ] @@ -344,7 +344,7 @@ class IPSecProposalForm(NetBoxModelForm): model = IPSecProposal fields = [ 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds', - 'sa_lifetime_data', 'comments', 'tags', + 'sa_lifetime_data', 'owner', 'comments', 'tags', ] @@ -363,7 +363,7 @@ class IPSecPolicyForm(NetBoxModelForm): class Meta: model = IPSecPolicy fields = [ - 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', + 'name', 'description', 'proposals', 'pfs_group', 'owner', 'comments', 'tags', ] @@ -386,7 +386,7 @@ class IPSecProfileForm(NetBoxModelForm): class Meta: model = IPSecProfile fields = [ - 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags', + 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'owner', 'comments', 'tags', ] @@ -417,8 +417,8 @@ class L2VPNForm(TenancyForm, NetBoxModelForm): class Meta: model = L2VPN fields = ( - 'name', 'slug', 'type', 'status', 'identifier', 'import_targets', 'export_targets', 'tenant', - 'description', 'comments', 'tags' + 'name', 'slug', 'type', 'status', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description', + 'owner', 'comments', 'tags' ) diff --git a/netbox/vpn/migrations/0010_owner.py b/netbox/vpn/migrations/0010_owner.py new file mode 100644 index 000000000..135084f84 --- /dev/null +++ b/netbox/vpn/migrations/0010_owner.py @@ -0,0 +1,68 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0015_owner'), + ('vpn', '0009_remove_redundant_indexes'), + ] + + operations = [ + migrations.AddField( + model_name='ikepolicy', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ikeproposal', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipsecpolicy', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipsecprofile', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='ipsecproposal', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='l2vpn', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tunnel', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='tunnelgroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ] diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 08f418e3c..b92874564 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -34,7 +34,7 @@ class WirelessLANGroupForm(NetBoxModelForm): class Meta: model = WirelessLANGroup fields = [ - 'parent', 'name', 'slug', 'description', 'tags', 'comments', + 'parent', 'name', 'slug', 'description', 'owner', 'comments', 'tags', ] @@ -64,7 +64,7 @@ class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm): model = WirelessLAN fields = [ 'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', - 'scope_type', 'description', 'comments', 'tags', + 'scope_type', 'description', 'owner', 'comments', 'tags', ] widgets = { 'auth_psk': PasswordInput( @@ -181,7 +181,7 @@ class WirelessLinkForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): fields = [ 'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b', 'status', 'ssid', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', - 'distance', 'distance_unit', 'description', 'comments', 'tags', + 'distance', 'distance_unit', 'description', 'owner', 'comments', 'tags', ] widgets = { 'auth_psk': PasswordInput( diff --git a/netbox/wireless/migrations/0016_owner.py b/netbox/wireless/migrations/0016_owner.py new file mode 100644 index 000000000..08167290c --- /dev/null +++ b/netbox/wireless/migrations/0016_owner.py @@ -0,0 +1,33 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0015_owner'), + ('wireless', '0015_extend_wireless_link_abs_distance_upper_limit'), + ] + + operations = [ + migrations.AddField( + model_name='wirelesslan', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='wirelesslangroup', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + migrations.AddField( + model_name='wirelesslink', + name='owner', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.owner' + ), + ), + ]