Closes: #18588: Relabel Service to Application Service (#19900)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run

* Closes: #18588: Relabel Service model to Application Service

Updates the `verbose_name` of the `Service` and `ServiceTemplate` models to "Application Service" and
"Application Service Template" respectively. This serves as the foundational change for relabeling
the model throughout the user interface to reduce ambiguity.

To preserve backward compatibility for the REST and GraphQL APIs, the test suites have been updated
to assert the stability of the original field and parameter names. This includes:

*   Using `filter_name_map` in the filterset test case to ensure API query parameters remain
    `service` and `service_id`.
*   Employing the GraphQL test suite's aliasing mechanism to ensure the public schema remains
    unchanged despite the underlying `verbose_name` modification.

Subsequent commits will address UI-specific labels in navigation, tables, forms, and templates.

* Rename to Application Services/Application Service Templates in nav menu

* Rename ~service to ~'Application Service' in templates

This was done for both the Service model and Service Template model
appearances in templates where the word was hardcoded.

* Change ~service to ~'application service' hardcoded strings in Python files

* Update ~service to ~'application service' in docs
This commit is contained in:
Jason Novinger 2025-07-21 09:22:27 -04:00 committed by GitHub
parent 4e0e4598b0
commit 59e1d3a607
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 56 additions and 33 deletions

View File

@ -62,8 +62,8 @@ VRF modeling in NetBox very closely follows what you find in real-world network
An often overlooked component of IPAM, NetBox also tracks autonomous system (AS) numbers and their assignment to sites. Both 16- and 32-bit AS numbers are supported, and like aggregates each ASN is assigned to an authoritative RIR.
## Service Mapping
## Application Service Mapping
NetBox models network applications as discrete service objects associated with devices and/or virtual machines, and optionally with specific IP addresses attached to those parent objects. These can be used to catalog the applications running on your network for reference by other objects or integrated tools.
To model services in NetBox, begin by creating a service template defining the name, protocol, and port number(s) on which the service listens. This template can then be easily instantiated to "attach" new services to a device or virtual machine. It's also possible to create new services by hand, without a template, however this approach can be tedious.
To model application services in NetBox, begin by creating an application service template defining the name, protocol, and port number(s) on which the service listens. This template can then be easily instantiated to "attach" new services to a device or virtual machine. It's also possible to create new application services by hand, without a template, however this approach can be tedious.

View File

@ -1,14 +1,18 @@
# Services
# Application Services
A service represents a layer seven application available on a device or virtual machine. For example, a service might be created in NetBox to represent an HTTP server running on TCP/8000. Each service may optionally be further bound to one or more specific interfaces assigned to the selected device or virtual machine.
An application service represents a layer seven application available on a device or virtual machine. For example, a service might be created in NetBox to represent an HTTP server running on TCP/8000. Each service may optionally be further bound to one or more specific interfaces assigned to the selected device or virtual machine.
To aid in the efficient creation of services, users may opt to first create a [service template](./servicetemplate.md) from which service definitions can be quickly replicated.
To aid in the efficient creation of application services, users may opt to first create an [application service template](./servicetemplate.md) from which service definitions can be quickly replicated.
!!! note "Changed in NetBox v4.4"
Previously, application services were referred to simply as "services". The name has been changed in the UI to better reflect their intended use. There is no change to the name of the model or in any programmatic NetBox APIs.
## Fields
### Parent
The parent object to which the service is assigned. This must be one of [Device](../dcim/device.md),
The parent object to which the application service is assigned. This must be one of [Device](../dcim/device.md),
[VirtualMachine](../virtualization/virtualmachine.md), or [FHRP Group](./fhrpgroup.md).
!!! note "Changed in NetBox v4.3"

View File

@ -1,6 +1,10 @@
# Service Templates
# Application Service Templates
Service templates can be used to instantiate [services](./service.md) on [devices](../dcim/device.md) and [virtual machines](../virtualization/virtualmachine.md).
Application service templates can be used to instantiate [application services](./service.md) on [devices](../dcim/device.md) and [virtual machines](../virtualization/virtualmachine.md).
!!! note "Changed in NetBox v4.4"
Previously, application service templates were referred to simply as "service templates". The name has been changed in the UI to better reflect their intended use. There is no change to the name of the model or in any programmatic NetBox APIs.
## Fields

View File

@ -1,6 +1,6 @@
## Interfaces
[Virtual machine](./virtualmachine.md) interfaces behave similarly to device [interfaces](../dcim/interface.md): They can be assigned to VRFs, may have IP addresses, VLANs, and services attached to them, and so on. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them.
[Virtual machine](./virtualmachine.md) interfaces behave similarly to device [interfaces](../dcim/interface.md): They can be assigned to VRFs, may have IP addresses, VLANs, and so on. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them.
## Fields

View File

@ -660,7 +660,7 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFil
service_id = django_filters.ModelMultipleChoiceFilter(
field_name='services',
queryset=Service.objects.all(),
label=_('Service (ID)'),
label=_('Application Service (ID)'),
)
nat_inside_id = django_filters.ModelMultipleChoiceFilter(
field_name='nat_inside',

View File

@ -758,7 +758,7 @@ class ServiceTemplateForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Service Template')),
FieldSet('name', 'protocol', 'ports', 'description', 'tags', name=_('Application Service Template')),
)
class Meta:
@ -803,7 +803,7 @@ class ServiceForm(NetBoxModelForm):
FieldSet(
'parent_object_type', 'parent', 'name',
InlineFields('protocol', 'ports', label=_('Port(s)')),
'ipaddresses', 'description', 'tags', name=_('Service')
'ipaddresses', 'description', 'tags', name=_('Application Service')
),
)
@ -845,7 +845,7 @@ class ServiceForm(NetBoxModelForm):
class ServiceCreateForm(ServiceForm):
service_template = DynamicModelChoiceField(
label=_('Service template'),
label=_('Application Service template'),
queryset=ServiceTemplate.objects.all(),
required=False
)
@ -857,7 +857,7 @@ class ServiceCreateForm(ServiceForm):
FieldSet('service_template', name=_('From Template')),
FieldSet('name', 'protocol', 'ports', name=_('Custom')),
),
'ipaddresses', 'description', 'tags', name=_('Service')
'ipaddresses', 'description', 'tags', name=_('Application Service')
),
)
@ -886,4 +886,6 @@ class ServiceCreateForm(ServiceForm):
if not self.cleaned_data['description']:
self.cleaned_data['description'] = service_template.description
elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
raise forms.ValidationError(_("Must specify name, protocol, and port(s) if not using a service template."))
raise forms.ValidationError(
_("Must specify name, protocol, and port(s) if not using an application service template.")
)

View File

@ -55,8 +55,8 @@ class ServiceTemplate(ServiceBase, PrimaryModel):
class Meta:
ordering = ('name',)
verbose_name = _('service template')
verbose_name_plural = _('service templates')
verbose_name = _('application service template')
verbose_name_plural = _('application service templates')
class Service(ContactsMixin, ServiceBase, PrimaryModel):
@ -84,7 +84,7 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel):
related_name='services',
blank=True,
verbose_name=_('IP addresses'),
help_text=_("The specific IP addresses (if any) to which this service is bound")
help_text=_("The specific IP addresses (if any) to which this application service is bound")
)
clone_fields = ['protocol', 'ports', 'description', 'parent', 'ipaddresses', ]
@ -94,5 +94,5 @@ class Service(ContactsMixin, ServiceBase, PrimaryModel):
models.Index(fields=('parent_object_type', 'parent_object_id')),
)
ordering = ('protocol', 'ports', 'pk') # (protocol, port) may be non-unique
verbose_name = _('service')
verbose_name_plural = _('services')
verbose_name = _('application service')
verbose_name_plural = _('application services')

View File

@ -1162,6 +1162,7 @@ class ServiceTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
graphql_base_name = 'service_template'
@classmethod
def setUpTestData(cls):
@ -1197,6 +1198,7 @@ class ServiceTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
graphql_base_name = 'service'
@classmethod
def setUpTestData(cls):

View File

@ -1101,6 +1101,9 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPAddress.objects.all()
filterset = IPAddressFilterSet
ignore_fields = ('fhrpgroup',)
filter_name_map = {
'application_service': 'service',
}
@classmethod
def setUpTestData(cls):

View File

@ -209,8 +209,8 @@ IPAM_MENU = Menu(
label=_('Other'),
items=(
get_model_item('ipam', 'fhrpgroup', _('FHRP Groups')),
get_model_item('ipam', 'servicetemplate', _('Service Templates')),
get_model_item('ipam', 'service', _('Services')),
get_model_item('ipam', 'servicetemplate', _('Application Service Templates')),
get_model_item('ipam', 'service', _('Application Services')),
),
),
),

View File

@ -305,11 +305,11 @@
{% endif %}
<div class="card">
<h2 class="card-header">
{% trans "Services" %}
{% trans "Application Services" %}
{% if perms.ipam.add_service %}
<div class="card-actions">
<a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a service" %}
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add an application service" %}
</a>
</div>
{% endif %}

View File

@ -115,7 +115,7 @@
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
{% endif %}
<div class="card">
<h2 class="card-header">{% trans "Services" %}</h2>
<h2 class="card-header">{% trans "Application Services" %}</h2>
{% htmx_table 'ipam:service_list' ip_address_id=object.pk %}
</div>
{% plugin_right_page object %}

View File

@ -9,7 +9,7 @@
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Service Template" %}</h2>
<h2 class="card-header">{% trans "Application Service Template" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>

View File

@ -151,11 +151,11 @@
</div>
<div class="card">
<h2 class="card-header">
{% trans "Services" %}
{% trans "Application Services" %}
{% if perms.ipam.add_service %}
<div class="card-actions">
<a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a service" %}
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add an application service" %}
</a>
</div>
{% endif %}

View File

@ -33,6 +33,7 @@ class BaseFilterSetTests:
queryset = None
filterset = None
ignore_fields = tuple()
filter_name_map = {}
def get_m2m_filter_name(self, field):
"""
@ -46,7 +47,13 @@ class BaseFilterSetTests:
"""
Given a model field, return an iterable of (name, class) for each filter that should be defined on
the model's FilterSet class. If the appropriate filter class cannot be determined, it will be None.
filter_name_map provides a mechanism for developers to provide an actual field name for the
filter that is being resolved, given the field's actual name.
"""
# If an alias is not present in filter_name_map, then use field.name
filter_name = self.filter_name_map.get(field.name, field.name)
# ForeignKey & OneToOneField
if issubclass(field.__class__, ForeignKey) or type(field) is OneToOneRel:
@ -57,19 +64,20 @@ class BaseFilterSetTests:
# ForeignKeys to ObjectType need two filters: 'app.model' & PK
if field.related_model is ObjectType:
return [
(field.name, ContentTypeFilter),
(f'{field.name}_id', django_filters.ModelMultipleChoiceFilter),
(filter_name, ContentTypeFilter),
(f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter),
]
# ForeignKey to an MPTT-enabled model
if issubclass(field.related_model, MPTTModel) and field.model is not field.related_model:
return [(f'{field.name}_id', TreeNodeMultipleChoiceFilter)]
return [(f'{filter_name}_id', TreeNodeMultipleChoiceFilter)]
return [(f'{field.name}_id', django_filters.ModelMultipleChoiceFilter)]
return [(f'{filter_name}_id', django_filters.ModelMultipleChoiceFilter)]
# Many-to-many relationships (forward & backward)
elif type(field) in (ManyToManyField, ManyToManyRel):
filter_name = self.get_m2m_filter_name(field)
filter_name = self.filter_name_map.get(filter_name, filter_name)
# ManyToManyFields to ObjectType need two filters: 'app.model' & PK
if field.related_model is ObjectType:
@ -85,7 +93,7 @@ class BaseFilterSetTests:
return [('tag', TagFilter)]
# Unable to determine the correct filter class
return [(field.name, None)]
return [(filter_name, None)]
def test_id(self):
"""