mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 13:06:30 -06:00
Merge branch 'feature' into 9102-cabling
This commit is contained in:
commit
bd950e9ca6
@ -1,5 +1,5 @@
|
|||||||
# Clusters
|
# Clusters
|
||||||
|
|
||||||
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant. Each cluster must have a unique name within its assigned group and/or site, if any.
|
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification) and operational status, and may optionally be assigned to a cluster group, site, and/or tenant. Each cluster must have a unique name within its assigned group and/or site, if any.
|
||||||
|
|
||||||
Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Virtual Machines
|
# Virtual Machines
|
||||||
|
|
||||||
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be assigned to exactly one cluster.
|
A virtual machine represents a virtual compute instance hosted within a cluster. Each VM must be assigned to a site and/or cluster, and may optionally be assigned to a particular host device within a cluster.
|
||||||
|
|
||||||
Like devices, each VM can be assigned a platform and/or functional role, and must have one of the following operational statuses assigned to it:
|
Like devices, each VM can be assigned a platform and/or functional role, and must have one of the following operational statuses assigned to it:
|
||||||
|
|
||||||
|
@ -9,8 +9,12 @@
|
|||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
|
* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
|
||||||
|
* [#5303](https://github.com/netbox-community/netbox/issues/5303) - A virtual machine may be assigned to a site and/or cluster
|
||||||
|
* [#8222](https://github.com/netbox-community/netbox/issues/8222) - Enable the assignment of a VM to a specific host device within a cluster
|
||||||
|
* [#8471](https://github.com/netbox-community/netbox/issues/8471) - Add `status` field to Cluster
|
||||||
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
|
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
|
||||||
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
|
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
|
||||||
|
* [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields
|
||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
@ -19,7 +23,13 @@
|
|||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
* extras.CustomField
|
* extras.CustomField
|
||||||
* Added `group_name` field
|
* Added `group_name` and `ui_visibility` fields
|
||||||
* ipam.IPAddress
|
* ipam.IPAddress
|
||||||
* The `nat_inside` field no longer requires a unique value
|
* The `nat_inside` field no longer requires a unique value
|
||||||
* The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses
|
* The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses
|
||||||
|
* virtualization.Cluster
|
||||||
|
* Added required `status` field (default value: `active`)
|
||||||
|
* virtualization.VirtualMachine
|
||||||
|
* Added `device` field
|
||||||
|
* The `site` field is now directly writable (rather than being inferred from the assigned cluster)
|
||||||
|
* The `cluster` field is now optional. A virtual machine must have a site and/or cluster assigned.
|
||||||
|
@ -84,13 +84,14 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
)
|
)
|
||||||
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
|
||||||
data_type = serializers.SerializerMethodField()
|
data_type = serializers.SerializerMethodField()
|
||||||
|
ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
||||||
'description', 'required', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum',
|
'description', 'required', 'filter_logic', 'ui_visibility', 'default', 'weight', 'validation_minimum',
|
||||||
'validation_regex', 'choices', 'created', 'last_updated',
|
'validation_maximum', 'validation_regex', 'choices', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_data_type(self, obj):
|
def get_data_type(self, obj):
|
||||||
|
@ -47,6 +47,19 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldVisibilityChoices(ChoiceSet):
|
||||||
|
|
||||||
|
VISIBILITY_READ_WRITE = 'read-write'
|
||||||
|
VISIBILITY_READ_ONLY = 'read-only'
|
||||||
|
VISIBILITY_HIDDEN = 'hidden'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(VISIBILITY_READ_WRITE, 'Read/Write'),
|
||||||
|
(VISIBILITY_READ_ONLY, 'Read-only'),
|
||||||
|
(VISIBILITY_HIDDEN, 'Hidden'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# CustomLinks
|
# CustomLinks
|
||||||
#
|
#
|
||||||
|
@ -62,7 +62,10 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = ['id', 'content_types', 'name', 'group_name', 'required', 'filter_logic', 'weight', 'description']
|
fields = [
|
||||||
|
'id', 'content_types', 'name', 'group_name', 'required', 'filter_logic', 'ui_visibility', 'weight',
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -37,6 +37,13 @@ class CustomFieldBulkEditForm(BulkEditForm):
|
|||||||
weight = forms.IntegerField(
|
weight = forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
ui_visibility = forms.ChoiceField(
|
||||||
|
label="UI visibility",
|
||||||
|
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
nullable_fields = ('group_name', 'description',)
|
nullable_fields = ('group_name', 'description',)
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ class CustomFieldCSVForm(CSVModelForm):
|
|||||||
fields = (
|
fields = (
|
||||||
'name', 'label', 'group_name', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic',
|
'name', 'label', 'group_name', 'type', 'content_types', 'required', 'description', 'weight', 'filter_logic',
|
||||||
'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
|
||||||
|
'ui_visibility',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
|
from extras.choices import CustomFieldVisibilityChoices
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CustomFieldsMixin',
|
'CustomFieldsMixin',
|
||||||
@ -42,8 +43,18 @@ class CustomFieldsMixin:
|
|||||||
Append form fields for all CustomFields assigned to this object type.
|
Append form fields for all CustomFields assigned to this object type.
|
||||||
"""
|
"""
|
||||||
for customfield in self._get_custom_fields(self._get_content_type()):
|
for customfield in self._get_custom_fields(self._get_content_type()):
|
||||||
|
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
|
||||||
|
continue
|
||||||
|
|
||||||
field_name = f'cf_{customfield.name}'
|
field_name = f'cf_{customfield.name}'
|
||||||
self.fields[field_name] = self._get_form_field(customfield)
|
self.fields[field_name] = self._get_form_field(customfield)
|
||||||
|
|
||||||
|
if customfield.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||||
|
self.fields[field_name].disabled = True
|
||||||
|
if self.fields[field_name].help_text:
|
||||||
|
self.fields[field_name].help_text += '<br />'
|
||||||
|
self.fields[field_name].help_text += '<i class="mdi mdi-alert-circle-outline"></i> ' \
|
||||||
|
'Field is set to read-only.'
|
||||||
|
|
||||||
# Annotate the field in the list of CustomField form fields
|
# Annotate the field in the list of CustomField form fields
|
||||||
self.custom_fields[field_name] = customfield
|
self.custom_fields[field_name] = customfield
|
||||||
|
@ -32,7 +32,7 @@ __all__ = (
|
|||||||
class CustomFieldFilterForm(FilterForm):
|
class CustomFieldFilterForm(FilterForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q',)),
|
(None, ('q',)),
|
||||||
('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required')),
|
('Attributes', ('content_types', 'type', 'group_name', 'weight', 'required', 'ui_visibility')),
|
||||||
)
|
)
|
||||||
content_types = ContentTypeMultipleChoiceField(
|
content_types = ContentTypeMultipleChoiceField(
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
@ -56,6 +56,12 @@ class CustomFieldFilterForm(FilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
ui_visibility = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(CustomFieldVisibilityChoices),
|
||||||
|
required=False,
|
||||||
|
label=_('UI visibility'),
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkFilterForm(FilterForm):
|
class CustomLinkFilterForm(FilterForm):
|
||||||
|
@ -43,7 +43,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
('Custom Field', (
|
('Custom Field', (
|
||||||
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'weight', 'required', 'description',
|
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'weight', 'required', 'description',
|
||||||
)),
|
)),
|
||||||
('Behavior', ('filter_logic',)),
|
('Behavior', ('filter_logic', 'ui_visibility')),
|
||||||
('Values', ('default', 'choices')),
|
('Values', ('default', 'choices')),
|
||||||
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||||
)
|
)
|
||||||
@ -58,6 +58,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
'type': StaticSelect(),
|
'type': StaticSelect(),
|
||||||
'filter_logic': StaticSelect(),
|
'filter_logic': StaticSelect(),
|
||||||
|
'ui_visibility': StaticSelect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
18
netbox/extras/migrations/0075_customfield_ui_visibility.py
Normal file
18
netbox/extras/migrations/0075_customfield_ui_visibility.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.0.4 on 2022-05-23 20:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0074_customfield_group_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='ui_visibility',
|
||||||
|
field=models.CharField(default='read-write', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
@ -136,6 +136,13 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text='Comma-separated list of available choices (for selection fields)'
|
help_text='Comma-separated list of available choices (for selection fields)'
|
||||||
)
|
)
|
||||||
|
ui_visibility = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=CustomFieldVisibilityChoices,
|
||||||
|
default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
|
||||||
|
verbose_name='UI visibility',
|
||||||
|
help_text='Specifies the visibility of custom field in the UI'
|
||||||
|
)
|
||||||
objects = CustomFieldManager()
|
objects = CustomFieldManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -28,12 +28,13 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
)
|
)
|
||||||
content_types = columns.ContentTypesColumn()
|
content_types = columns.ContentTypesColumn()
|
||||||
required = columns.BooleanColumn()
|
required = columns.BooleanColumn()
|
||||||
|
ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility")
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'weight', 'default',
|
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'weight', 'default',
|
||||||
'description', 'filter_logic', 'choices', 'created', 'last_updated',
|
'description', 'filter_logic', 'ui_visibility', 'choices', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||||
|
|
||||||
|
@ -36,13 +36,14 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'default': None,
|
'default': None,
|
||||||
'weight': 200,
|
'weight': 200,
|
||||||
'required': True,
|
'required': True,
|
||||||
|
'ui_visibility': CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex',
|
'name,label,type,content_types,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex,ui_visibility',
|
||||||
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3}',
|
'field4,Field 4,text,dcim.site,100,exact,,,,[a-z]{3},read-write',
|
||||||
'field5,Field 5,integer,dcim.site,100,exact,,1,100,',
|
'field5,Field 5,integer,dcim.site,100,exact,,1,100,,read-write',
|
||||||
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,',
|
'field6,Field 6,select,dcim.site,100,exact,"A,B,C",,,,read-write',
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
|
@ -9,7 +9,7 @@ from django.core.validators import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from extras.choices import ObjectChangeActionChoices
|
from extras.choices import CustomFieldVisibilityChoices, ObjectChangeActionChoices
|
||||||
from extras.utils import register_features
|
from extras.utils import register_features
|
||||||
from netbox.signals import post_clean
|
from netbox.signals import post_clean
|
||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
@ -100,7 +100,7 @@ class CustomFieldsMixin(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.custom_field_data
|
return self.custom_field_data
|
||||||
|
|
||||||
def get_custom_fields(self):
|
def get_custom_fields(self, omit_hidden=False):
|
||||||
"""
|
"""
|
||||||
Return a dictionary of custom fields for a single object in the form `{field: value}`.
|
Return a dictionary of custom fields for a single object in the form `{field: value}`.
|
||||||
|
|
||||||
@ -114,6 +114,10 @@ class CustomFieldsMixin(models.Model):
|
|||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
for field in CustomField.objects.get_for_model(self):
|
for field in CustomField.objects.get_for_model(self):
|
||||||
|
# Skip fields that are hidden if 'omit_hidden' is set
|
||||||
|
if omit_hidden and field.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_HIDDEN:
|
||||||
|
continue
|
||||||
|
|
||||||
value = self.custom_field_data.get(field.name)
|
value = self.custom_field_data.get(field.name)
|
||||||
data[field] = field.deserialize(value)
|
data[field] = field.deserialize(value)
|
||||||
|
|
||||||
@ -121,10 +125,10 @@ class CustomFieldsMixin(models.Model):
|
|||||||
|
|
||||||
def get_custom_fields_by_group(self):
|
def get_custom_fields_by_group(self):
|
||||||
"""
|
"""
|
||||||
Return a dictionary of custom field/value mappings organized by group.
|
Return a dictionary of custom field/value mappings organized by group. Hidden fields are omitted.
|
||||||
"""
|
"""
|
||||||
grouped_custom_fields = defaultdict(dict)
|
grouped_custom_fields = defaultdict(dict)
|
||||||
for cf, value in self.get_custom_fields().items():
|
for cf, value in self.get_custom_fields(omit_hidden=True).items():
|
||||||
grouped_custom_fields[cf.group_name][cf] = value
|
grouped_custom_fields[cf.group_name][cf] = value
|
||||||
|
|
||||||
return dict(grouped_custom_fields)
|
return dict(grouped_custom_fields)
|
||||||
|
@ -7,6 +7,7 @@ from django.db.models.fields.related import RelatedField
|
|||||||
from django_tables2.data import TableQuerysetData
|
from django_tables2.data import TableQuerysetData
|
||||||
|
|
||||||
from extras.models import CustomField, CustomLink
|
from extras.models import CustomField, CustomLink
|
||||||
|
from extras.choices import CustomFieldVisibilityChoices
|
||||||
from netbox.tables import columns
|
from netbox.tables import columns
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
|
|
||||||
@ -178,7 +179,10 @@ class NetBoxTable(BaseTable):
|
|||||||
|
|
||||||
# Add custom field & custom link columns
|
# Add custom field & custom link columns
|
||||||
content_type = ContentType.objects.get_for_model(self._meta.model)
|
content_type = ContentType.objects.get_for_model(self._meta.model)
|
||||||
custom_fields = CustomField.objects.filter(content_types=content_type)
|
custom_fields = CustomField.objects.filter(
|
||||||
|
content_types=content_type
|
||||||
|
).exclude(ui_visibility=CustomFieldVisibilityChoices.VISIBILITY_HIDDEN)
|
||||||
|
|
||||||
extra_columns.extend([
|
extra_columns.extend([
|
||||||
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
|
(f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields
|
||||||
])
|
])
|
||||||
|
@ -42,6 +42,14 @@
|
|||||||
<th scope="row">Weight</th>
|
<th scope="row">Weight</th>
|
||||||
<td>{{ object.weight }}</td>
|
<td>{{ object.weight }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Filter Logic</th>
|
||||||
|
<td>{{ object.get_filter_logic_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">UI Visibility</th>
|
||||||
|
<td>{{ object.get_ui_visibility_display }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -65,10 +73,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Filter Logic</th>
|
|
||||||
<td>{{ object.get_filter_logic_display }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,31 +78,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Cluster</h5>
|
||||||
Cluster
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Site</th>
|
||||||
|
<td>
|
||||||
|
{{ object.site|linkify|placeholder }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Cluster</th>
|
<th scope="row">Cluster</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.cluster.group %}
|
{% if object.cluster.group %}
|
||||||
{{ object.cluster.group|linkify }} /
|
{{ object.cluster.group|linkify }} /
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ object.cluster|linkify }}
|
{{ object.cluster|linkify|placeholder }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Cluster Type</th>
|
<th scope="row">Cluster Type</th>
|
||||||
<td>{{ object.cluster.type }}</td>
|
<td>{{ object.cluster.type }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Device</th>
|
||||||
|
<td>
|
||||||
|
{{ object.device|linkify|placeholder }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Resources</h5>
|
||||||
Resources
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -34,15 +34,16 @@ def post_data(data):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def create_test_device(name):
|
def create_test_device(name, site=None, **attrs):
|
||||||
"""
|
"""
|
||||||
Convenience method for creating a Device (e.g. for component testing).
|
Convenience method for creating a Device (e.g. for component testing).
|
||||||
"""
|
"""
|
||||||
|
if site is None:
|
||||||
site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1')
|
site, _ = Site.objects.get_or_create(name='Site 1', slug='site-1')
|
||||||
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer)
|
devicetype, _ = DeviceType.objects.get_or_create(model='Device Type 1', manufacturer=manufacturer)
|
||||||
devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1')
|
devicerole, _ = DeviceRole.objects.get_or_create(name='Device Role 1', slug='device-role-1')
|
||||||
device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole)
|
device = Device.objects.create(name=name, site=site, device_type=devicetype, device_role=devicerole, **attrs)
|
||||||
|
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import (
|
||||||
|
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer,
|
||||||
|
)
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
@ -45,6 +47,7 @@ class ClusterSerializer(NetBoxModelSerializer):
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||||
type = NestedClusterTypeSerializer()
|
type = NestedClusterTypeSerializer()
|
||||||
group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
|
group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
|
status = ChoiceField(choices=ClusterStatusChoices, required=False)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True, default=None)
|
site = NestedSiteSerializer(required=False, allow_null=True, default=None)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
@ -53,8 +56,8 @@ class ClusterSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', 'custom_fields',
|
'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'comments', 'tags',
|
||||||
'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -65,8 +68,9 @@ class ClusterSerializer(NetBoxModelSerializer):
|
|||||||
class VirtualMachineSerializer(NetBoxModelSerializer):
|
class VirtualMachineSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
||||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||||
site = NestedSiteSerializer(read_only=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
cluster = NestedClusterSerializer()
|
cluster = NestedClusterSerializer(required=False, allow_null=True)
|
||||||
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
role = NestedDeviceRoleSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||||
@ -77,9 +81,9 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip',
|
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
||||||
'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
@ -89,9 +93,9 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
|
|
||||||
class Meta(VirtualMachineSerializer.Meta):
|
class Meta(VirtualMachineSerializer.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip',
|
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
|
||||||
'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags',
|
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data',
|
||||||
'custom_fields', 'config_context', 'created', 'last_updated',
|
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
@ -54,7 +54,7 @@ class ClusterViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||||
queryset = VirtualMachine.objects.prefetch_related(
|
queryset = VirtualMachine.objects.prefetch_related(
|
||||||
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
||||||
)
|
)
|
||||||
filterset_class = filtersets.VirtualMachineFilterSet
|
filterset_class = filtersets.VirtualMachineFilterSet
|
||||||
|
|
||||||
|
@ -1,6 +1,28 @@
|
|||||||
from utilities.choices import ChoiceSet
|
from utilities.choices import ChoiceSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Clusters
|
||||||
|
#
|
||||||
|
|
||||||
|
class ClusterStatusChoices(ChoiceSet):
|
||||||
|
key = 'Cluster.status'
|
||||||
|
|
||||||
|
STATUS_PLANNED = 'planned'
|
||||||
|
STATUS_STAGING = 'staging'
|
||||||
|
STATUS_ACTIVE = 'active'
|
||||||
|
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||||
|
STATUS_OFFLINE = 'offline'
|
||||||
|
|
||||||
|
CHOICES = [
|
||||||
|
(STATUS_PLANNED, 'Planned', 'cyan'),
|
||||||
|
(STATUS_STAGING, 'Staging', 'blue'),
|
||||||
|
(STATUS_ACTIVE, 'Active', 'green'),
|
||||||
|
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
|
||||||
|
(STATUS_OFFLINE, 'Offline', 'red'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VirtualMachines
|
# VirtualMachines
|
||||||
#
|
#
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from ipam.models import VRF
|
from ipam.models import VRF
|
||||||
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
@ -90,6 +90,10 @@ class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Cluster type (slug)',
|
label='Cluster type (slug)',
|
||||||
)
|
)
|
||||||
|
status = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=ClusterStatusChoices,
|
||||||
|
null_value=None
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
@ -146,39 +150,48 @@ class VirtualMachineFilterSet(
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Cluster',
|
label='Cluster',
|
||||||
)
|
)
|
||||||
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device (ID)',
|
||||||
|
)
|
||||||
|
device = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__name',
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label='Device',
|
||||||
|
)
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='cluster__site__region',
|
field_name='site__region',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label='Region (ID)',
|
label='Region (ID)',
|
||||||
)
|
)
|
||||||
region = TreeNodeMultipleChoiceFilter(
|
region = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='cluster__site__region',
|
field_name='site__region',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Region (slug)',
|
label='Region (slug)',
|
||||||
)
|
)
|
||||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
field_name='cluster__site__group',
|
field_name='site__group',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label='Site group (ID)',
|
label='Site group (ID)',
|
||||||
)
|
)
|
||||||
site_group = TreeNodeMultipleChoiceFilter(
|
site_group = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
field_name='cluster__site__group',
|
field_name='site__group',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site group (slug)',
|
label='Site group (slug)',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='cluster__site',
|
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='cluster__site__slug',
|
field_name='site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
|
@ -2,7 +2,7 @@ from django import forms
|
|||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from ipam.models import VLAN, VRF
|
from ipam.models import VLAN, VRF
|
||||||
from netbox.forms import NetBoxModelBulkEditForm
|
from netbox.forms import NetBoxModelBulkEditForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -58,6 +58,12 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(ClusterStatusChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
tenant = DynamicModelChoiceField(
|
tenant = DynamicModelChoiceField(
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -85,7 +91,7 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('type', 'group', 'tenant',)),
|
(None, ('type', 'group', 'status', 'tenant',)),
|
||||||
('Site', ('region', 'site_group', 'site',)),
|
('Site', ('region', 'site_group', 'site',)),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
@ -100,9 +106,23 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
initial='',
|
initial='',
|
||||||
widget=StaticSelect(),
|
widget=StaticSelect(),
|
||||||
)
|
)
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
cluster = DynamicModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
required=False
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
device = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'cluster_id': '$cluster'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=DeviceRole.objects.filter(
|
queryset=DeviceRole.objects.filter(
|
||||||
@ -140,11 +160,11 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('cluster', 'status', 'role', 'tenant', 'platform')),
|
(None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform')),
|
||||||
('Resources', ('vcpus', 'memory', 'disk'))
|
('Resources', ('vcpus', 'memory', 'disk'))
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -223,8 +243,10 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
# See 5643
|
# See 5643
|
||||||
if 'pk' in self.initial:
|
if 'pk' in self.initial:
|
||||||
site = None
|
site = None
|
||||||
interfaces = VMInterface.objects.filter(pk__in=self.initial['pk']).prefetch_related(
|
interfaces = VMInterface.objects.filter(
|
||||||
'virtual_machine__cluster__site'
|
pk__in=self.initial['pk']
|
||||||
|
).prefetch_related(
|
||||||
|
'virtual_machine__site'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check interface sites. First interface should set site, further interfaces will either continue the
|
# Check interface sites. First interface should set site, further interfaces will either continue the
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import DeviceRole, Platform, Site
|
from dcim.models import Device, DeviceRole, Platform, Site
|
||||||
from ipam.models import VRF
|
from ipam.models import VRF
|
||||||
from netbox.forms import NetBoxModelCSVForm
|
from netbox.forms import NetBoxModelCSVForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -44,6 +44,10 @@ class ClusterCSVForm(NetBoxModelCSVForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text='Assigned cluster group'
|
help_text='Assigned cluster group'
|
||||||
)
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=ClusterStatusChoices,
|
||||||
|
help_text='Operational status'
|
||||||
|
)
|
||||||
site = CSVModelChoiceField(
|
site = CSVModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -59,7 +63,7 @@ class ClusterCSVForm(NetBoxModelCSVForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = ('name', 'type', 'group', 'site', 'comments')
|
fields = ('name', 'type', 'group', 'status', 'site', 'comments')
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
||||||
@ -67,11 +71,24 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
|||||||
choices=VirtualMachineStatusChoices,
|
choices=VirtualMachineStatusChoices,
|
||||||
help_text='Operational status'
|
help_text='Operational status'
|
||||||
)
|
)
|
||||||
|
site = CSVModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Assigned site'
|
||||||
|
)
|
||||||
cluster = CSVModelChoiceField(
|
cluster = CSVModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
help_text='Assigned cluster'
|
help_text='Assigned cluster'
|
||||||
)
|
)
|
||||||
|
device = CSVModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Assigned device within cluster'
|
||||||
|
)
|
||||||
role = CSVModelChoiceField(
|
role = CSVModelChoiceField(
|
||||||
queryset=DeviceRole.objects.filter(
|
queryset=DeviceRole.objects.filter(
|
||||||
vm_role=True
|
vm_role=True
|
||||||
@ -96,7 +113,8 @@ class VirtualMachineCSVForm(NetBoxModelCSVForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
||||||
|
'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.forms import LocalConfigContextFilterForm
|
from extras.forms import LocalConfigContextFilterForm
|
||||||
from ipam.models import VRF
|
from ipam.models import VRF
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
@ -35,7 +35,7 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||||||
model = Cluster
|
model = Cluster
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('group_id', 'type_id')),
|
('Attributes', ('group_id', 'type_id', 'status')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role')),
|
('Contacts', ('contact', 'contact_role')),
|
||||||
@ -50,6 +50,10 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Region')
|
label=_('Region')
|
||||||
)
|
)
|
||||||
|
status = MultipleChoiceField(
|
||||||
|
choices=ClusterStatusChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
site_group_id = DynamicModelMultipleChoiceField(
|
site_group_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -83,7 +87,7 @@ class VirtualMachineFilterForm(
|
|||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id')),
|
('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
|
||||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||||
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
@ -106,6 +110,11 @@ class VirtualMachineFilterForm(
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('Cluster')
|
label=_('Cluster')
|
||||||
)
|
)
|
||||||
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Device')
|
||||||
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -79,15 +79,19 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
|
|||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')),
|
('Cluster', ('name', 'type', 'group', 'status', 'tags')),
|
||||||
|
('Site', ('region', 'site_group', 'site')),
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'type', 'group', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags',
|
'name', 'type', 'group', 'status', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
|
widgets = {
|
||||||
|
'status': StaticSelect(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
|
class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
|
||||||
@ -161,6 +165,9 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
|
|||||||
|
|
||||||
|
|
||||||
class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
||||||
|
site = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all()
|
||||||
|
)
|
||||||
cluster_group = DynamicModelChoiceField(
|
cluster_group = DynamicModelChoiceField(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -172,7 +179,15 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
|||||||
cluster = DynamicModelChoiceField(
|
cluster = DynamicModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
query_params={
|
query_params={
|
||||||
'group_id': '$cluster_group'
|
'site_id': '$site',
|
||||||
|
'group_id': '$cluster_group',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
device = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'cluster_id': '$cluster'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
@ -193,7 +208,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Virtual Machine', ('name', 'role', 'status', 'tags')),
|
('Virtual Machine', ('name', 'role', 'status', 'tags')),
|
||||||
('Cluster', ('cluster_group', 'cluster')),
|
('Cluster', ('site', 'cluster_group', 'cluster', 'device')),
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
('Management', ('platform', 'primary_ip4', 'primary_ip6')),
|
('Management', ('platform', 'primary_ip4', 'primary_ip6')),
|
||||||
('Resources', ('vcpus', 'memory', 'disk')),
|
('Resources', ('vcpus', 'memory', 'disk')),
|
||||||
@ -203,8 +218,9 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
|
'name', 'status', 'site', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
|
||||||
'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
|
'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags',
|
||||||
|
'local_context_data',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
|
'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
|
||||||
|
18
netbox/virtualization/migrations/0030_cluster_status.py
Normal file
18
netbox/virtualization/migrations/0030_cluster_status.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.0.4 on 2022-05-19 19:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0029_created_datetimefield'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(default='active', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,28 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0153_created_datetimefield'),
|
||||||
|
('virtualization', '0030_cluster_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='dcim.site'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='device',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='dcim.device'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='cluster',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='virtualization.cluster'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,27 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def update_virtualmachines_site(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Automatically set the site for all virtual machines.
|
||||||
|
"""
|
||||||
|
VirtualMachine = apps.get_model('virtualization', 'VirtualMachine')
|
||||||
|
|
||||||
|
virtual_machines = VirtualMachine.objects.filter(cluster__site__isnull=False)
|
||||||
|
for vm in virtual_machines:
|
||||||
|
vm.site = vm.cluster.site
|
||||||
|
VirtualMachine.objects.bulk_update(virtual_machines, ['site'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0031_virtualmachine_site_device'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=update_virtualmachines_site,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@ -119,6 +119,11 @@ class Cluster(NetBoxModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=ClusterStatusChoices,
|
||||||
|
default=ClusterStatusChoices.STATUS_ACTIVE
|
||||||
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
to='tenancy.Tenant',
|
to='tenancy.Tenant',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -165,6 +170,9 @@ class Cluster(NetBoxModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('virtualization:cluster', args=[self.pk])
|
return reverse('virtualization:cluster', args=[self.pk])
|
||||||
|
|
||||||
|
def get_status_color(self):
|
||||||
|
return ClusterStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@ -187,10 +195,26 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
"""
|
"""
|
||||||
A virtual machine which runs inside a Cluster.
|
A virtual machine which runs inside a Cluster.
|
||||||
"""
|
"""
|
||||||
|
site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='virtual_machines',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
cluster = models.ForeignKey(
|
cluster = models.ForeignKey(
|
||||||
to='virtualization.Cluster',
|
to='virtualization.Cluster',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='virtual_machines'
|
related_name='virtual_machines',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
device = models.ForeignKey(
|
||||||
|
to='dcim.Device',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='virtual_machines',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
)
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
to='tenancy.Tenant',
|
to='tenancy.Tenant',
|
||||||
@ -276,7 +300,7 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
objects = ConfigContextModelQuerySet.as_manager()
|
objects = ConfigContextModelQuerySet.as_manager()
|
||||||
|
|
||||||
clone_fields = [
|
clone_fields = [
|
||||||
'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
|
'site', 'cluster', 'device', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -308,6 +332,28 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
# Must be assigned to a site and/or cluster
|
||||||
|
if not self.site and not self.cluster:
|
||||||
|
raise ValidationError({
|
||||||
|
'cluster': f'A virtual machine must be assigned to a site and/or cluster.'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Validate site for cluster & device
|
||||||
|
if self.cluster and self.cluster.site != self.site:
|
||||||
|
raise ValidationError({
|
||||||
|
'cluster': f'The selected cluster ({self.cluster} is not assigned to this site ({self.site}).'
|
||||||
|
})
|
||||||
|
if self.device and self.device.site != self.site:
|
||||||
|
raise ValidationError({
|
||||||
|
'device': f'The selected device ({self.device} is not assigned to this site ({self.site}).'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Validate assigned cluster device
|
||||||
|
if self.device and self.device not in self.cluster.devices.all():
|
||||||
|
raise ValidationError({
|
||||||
|
'device': f'The selected device ({self.device} is not assigned to this cluster ({self.cluster}).'
|
||||||
|
})
|
||||||
|
|
||||||
# Validate primary IP addresses
|
# Validate primary IP addresses
|
||||||
interfaces = self.interfaces.all()
|
interfaces = self.interfaces.all()
|
||||||
for field in ['primary_ip4', 'primary_ip6']:
|
for field in ['primary_ip4', 'primary_ip6']:
|
||||||
@ -336,10 +382,6 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def site(self):
|
|
||||||
return self.cluster.site
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Interfaces
|
||||||
|
@ -66,6 +66,7 @@ class ClusterTable(NetBoxTable):
|
|||||||
group = tables.Column(
|
group = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = tables.Column(
|
tenant = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -93,7 +94,7 @@ class ClusterTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Cluster
|
model = Cluster
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'contacts',
|
'pk', 'id', 'name', 'type', 'group', 'status', 'tenant', 'site', 'comments', 'device_count', 'vm_count',
|
||||||
'tags', 'created', 'last_updated',
|
'contacts', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
|
default_columns = ('pk', 'name', 'type', 'group', 'status', 'tenant', 'site', 'device_count', 'vm_count')
|
||||||
|
@ -30,9 +30,15 @@ class VirtualMachineTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
|
site = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
cluster = tables.Column(
|
cluster = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
device = tables.Column(
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
role = columns.ColoredLabelColumn()
|
role = columns.ColoredLabelColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
@ -56,11 +62,11 @@ class VirtualMachineTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VirtualMachine
|
model = VirtualMachine
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
|
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory',
|
||||||
'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
|
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@ from django.urls import reverse
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
|
from dcim.models import Site
|
||||||
from ipam.models import VLAN, VRF
|
from ipam.models import VLAN, VRF
|
||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
|
|
||||||
|
|
||||||
@ -85,6 +87,7 @@ class ClusterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
model = Cluster
|
model = Cluster
|
||||||
brief_fields = ['display', 'id', 'name', 'url', 'virtualmachine_count']
|
brief_fields = ['display', 'id', 'name', 'url', 'virtualmachine_count']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
|
'status': 'offline',
|
||||||
'comments': 'New comment',
|
'comments': 'New comment',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,9 +107,9 @@ class ClusterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
ClusterGroup.objects.bulk_create(cluster_groups)
|
ClusterGroup.objects.bulk_create(cluster_groups)
|
||||||
|
|
||||||
clusters = (
|
clusters = (
|
||||||
Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0]),
|
Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED),
|
||||||
Cluster(name='Cluster 2', type=cluster_types[0], group=cluster_groups[0]),
|
Cluster(name='Cluster 2', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED),
|
||||||
Cluster(name='Cluster 3', type=cluster_types[0], group=cluster_groups[0]),
|
Cluster(name='Cluster 3', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED),
|
||||||
)
|
)
|
||||||
Cluster.objects.bulk_create(clusters)
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
@ -115,16 +118,19 @@ class ClusterTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'name': 'Cluster 4',
|
'name': 'Cluster 4',
|
||||||
'type': cluster_types[1].pk,
|
'type': cluster_types[1].pk,
|
||||||
'group': cluster_groups[1].pk,
|
'group': cluster_groups[1].pk,
|
||||||
|
'status': ClusterStatusChoices.STATUS_STAGING,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Cluster 5',
|
'name': 'Cluster 5',
|
||||||
'type': cluster_types[1].pk,
|
'type': cluster_types[1].pk,
|
||||||
'group': cluster_groups[1].pk,
|
'group': cluster_groups[1].pk,
|
||||||
|
'status': ClusterStatusChoices.STATUS_STAGING,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Cluster 6',
|
'name': 'Cluster 6',
|
||||||
'type': cluster_types[1].pk,
|
'type': cluster_types[1].pk,
|
||||||
'group': cluster_groups[1].pk,
|
'group': cluster_groups[1].pk,
|
||||||
|
'status': ClusterStatusChoices.STATUS_STAGING,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -141,31 +147,49 @@ class VirtualMachineTest(APIViewTestCases.APIViewTestCase):
|
|||||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
clustergroup = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
|
clustergroup = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
Site(name='Site 3', slug='site-3'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
clusters = (
|
clusters = (
|
||||||
Cluster(name='Cluster 1', type=clustertype, group=clustergroup),
|
Cluster(name='Cluster 1', type=clustertype, site=sites[0], group=clustergroup),
|
||||||
Cluster(name='Cluster 2', type=clustertype, group=clustergroup),
|
Cluster(name='Cluster 2', type=clustertype, site=sites[1], group=clustergroup),
|
||||||
|
Cluster(name='Cluster 3', type=clustertype),
|
||||||
)
|
)
|
||||||
Cluster.objects.bulk_create(clusters)
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
|
device1 = create_test_device('device1', site=sites[0], cluster=clusters[0])
|
||||||
|
device2 = create_test_device('device2', site=sites[1], cluster=clusters[1])
|
||||||
|
|
||||||
virtual_machines = (
|
virtual_machines = (
|
||||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], local_context_data={'A': 1}),
|
VirtualMachine(name='Virtual Machine 1', site=sites[0], cluster=clusters[0], device=device1, local_context_data={'A': 1}),
|
||||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], local_context_data={'B': 2}),
|
VirtualMachine(name='Virtual Machine 2', site=sites[0], cluster=clusters[0], local_context_data={'B': 2}),
|
||||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], local_context_data={'C': 3}),
|
VirtualMachine(name='Virtual Machine 3', site=sites[0], cluster=clusters[0], local_context_data={'C': 3}),
|
||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'name': 'Virtual Machine 4',
|
'name': 'Virtual Machine 4',
|
||||||
|
'site': sites[1].pk,
|
||||||
'cluster': clusters[1].pk,
|
'cluster': clusters[1].pk,
|
||||||
|
'device': device2.pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Virtual Machine 5',
|
'name': 'Virtual Machine 5',
|
||||||
|
'site': sites[1].pk,
|
||||||
'cluster': clusters[1].pk,
|
'cluster': clusters[1].pk,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Virtual Machine 6',
|
'name': 'Virtual Machine 6',
|
||||||
'cluster': clusters[1].pk,
|
'site': sites[1].pk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Virtual Machine 7',
|
||||||
|
'cluster': clusters[2].pk,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from ipam.models import IPAddress, VRF
|
from ipam.models import IPAddress, VRF
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests
|
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.filtersets import *
|
from virtualization.filtersets import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -123,9 +123,9 @@ class ClusterTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
clusters = (
|
clusters = (
|
||||||
Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0], site=sites[0], tenant=tenants[0]),
|
Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED, site=sites[0], tenant=tenants[0]),
|
||||||
Cluster(name='Cluster 2', type=cluster_types[1], group=cluster_groups[1], site=sites[1], tenant=tenants[1]),
|
Cluster(name='Cluster 2', type=cluster_types[1], group=cluster_groups[1], status=ClusterStatusChoices.STATUS_STAGING, site=sites[1], tenant=tenants[1]),
|
||||||
Cluster(name='Cluster 3', type=cluster_types[2], group=cluster_groups[2], site=sites[2], tenant=tenants[2]),
|
Cluster(name='Cluster 3', type=cluster_types[2], group=cluster_groups[2], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[2], tenant=tenants[2]),
|
||||||
)
|
)
|
||||||
Cluster.objects.bulk_create(clusters)
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
@ -161,6 +161,10 @@ class ClusterTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_status(self):
|
||||||
|
params = {'status': [ClusterStatusChoices.STATUS_PLANNED, ClusterStatusChoices.STATUS_STAGING]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_type(self):
|
def test_type(self):
|
||||||
types = ClusterType.objects.all()[:2]
|
types = ClusterType.objects.all()[:2]
|
||||||
params = {'type_id': [types[0].pk, types[1].pk]}
|
params = {'type_id': [types[0].pk, types[1].pk]}
|
||||||
@ -221,9 +225,9 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
site_group.save()
|
site_group.save()
|
||||||
|
|
||||||
sites = (
|
sites = (
|
||||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
Site(name='Site 1', slug='site-1', region=regions[0], group=site_groups[0]),
|
||||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
Site(name='Site 2', slug='site-2', region=regions[1], group=site_groups[1]),
|
||||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
Site(name='Site 3', slug='site-3', region=regions[2], group=site_groups[2]),
|
||||||
)
|
)
|
||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
@ -248,6 +252,12 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
DeviceRole.objects.bulk_create(roles)
|
DeviceRole.objects.bulk_create(roles)
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
create_test_device('device1', cluster=clusters[0]),
|
||||||
|
create_test_device('device2', cluster=clusters[1]),
|
||||||
|
create_test_device('device3', cluster=clusters[2]),
|
||||||
|
)
|
||||||
|
|
||||||
tenant_groups = (
|
tenant_groups = (
|
||||||
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
|
||||||
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
|
||||||
@ -264,9 +274,9 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
vms = (
|
vms = (
|
||||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], platform=platforms[0], role=roles[0], tenant=tenants[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
|
VirtualMachine(name='Virtual Machine 1', site=sites[0], cluster=clusters[0], device=devices[0], platform=platforms[0], role=roles[0], tenant=tenants[0], status=VirtualMachineStatusChoices.STATUS_ACTIVE, vcpus=1, memory=1, disk=1, local_context_data={"foo": 123}),
|
||||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1], platform=platforms[1], role=roles[1], tenant=tenants[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2),
|
VirtualMachine(name='Virtual Machine 2', site=sites[1], cluster=clusters[1], device=devices[1], platform=platforms[1], role=roles[1], tenant=tenants[1], status=VirtualMachineStatusChoices.STATUS_STAGED, vcpus=2, memory=2, disk=2),
|
||||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2], platform=platforms[2], role=roles[2], tenant=tenants[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
|
VirtualMachine(name='Virtual Machine 3', site=sites[2], cluster=clusters[2], device=devices[2], platform=platforms[2], role=roles[2], tenant=tenants[2], status=VirtualMachineStatusChoices.STATUS_OFFLINE, vcpus=3, memory=3, disk=3),
|
||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(vms)
|
VirtualMachine.objects.bulk_create(vms)
|
||||||
|
|
||||||
@ -327,6 +337,13 @@ class VirtualMachineTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'cluster': [clusters[0].name, clusters[1].name]}
|
params = {'cluster': [clusters[0].name, clusters[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_device(self):
|
||||||
|
devices = Device.objects.all()[:2]
|
||||||
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'device': [devices[0].name, devices[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_region(self):
|
def test_region(self):
|
||||||
regions = Region.objects.all()[:2]
|
regions = Region.objects.all()[:2]
|
||||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from dcim.models import Site
|
||||||
from virtualization.models import *
|
from virtualization.models import *
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
class VirtualMachineTestCase(TestCase):
|
class VirtualMachineTestCase(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
|
|
||||||
cluster_type = ClusterType.objects.create(name='Test Cluster Type 1', slug='Test Cluster Type 1')
|
|
||||||
self.cluster = Cluster.objects.create(name='Test Cluster 1', type=cluster_type)
|
|
||||||
|
|
||||||
def test_vm_duplicate_name_per_cluster(self):
|
def test_vm_duplicate_name_per_cluster(self):
|
||||||
|
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
|
cluster = Cluster.objects.create(name='Cluster 1', type=cluster_type)
|
||||||
|
|
||||||
vm1 = VirtualMachine(
|
vm1 = VirtualMachine(
|
||||||
cluster=self.cluster,
|
cluster=cluster,
|
||||||
name='Test VM 1'
|
name='Test VM 1'
|
||||||
)
|
)
|
||||||
vm1.save()
|
vm1.save()
|
||||||
@ -43,3 +41,33 @@ class VirtualMachineTestCase(TestCase):
|
|||||||
# Two VMs assigned to the same Cluster and different Tenants should pass validation
|
# Two VMs assigned to the same Cluster and different Tenants should pass validation
|
||||||
vm2.full_clean()
|
vm2.full_clean()
|
||||||
vm2.save()
|
vm2.save()
|
||||||
|
|
||||||
|
def test_vm_mismatched_site_cluster(self):
|
||||||
|
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
|
clusters = (
|
||||||
|
Cluster(name='Cluster 1', type=cluster_type, site=sites[0]),
|
||||||
|
Cluster(name='Cluster 2', type=cluster_type, site=sites[1]),
|
||||||
|
Cluster(name='Cluster 3', type=cluster_type, site=None),
|
||||||
|
)
|
||||||
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
|
# VM with site only should pass
|
||||||
|
VirtualMachine(name='vm1', site=sites[0]).full_clean()
|
||||||
|
|
||||||
|
# VM with non-site cluster only should pass
|
||||||
|
VirtualMachine(name='vm1', cluster=clusters[2]).full_clean()
|
||||||
|
|
||||||
|
# VM with mismatched site & cluster should fail
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
VirtualMachine(name='vm1', site=sites[0], cluster=clusters[1]).full_clean()
|
||||||
|
|
||||||
|
# VM with cluster site but no direct site should fail
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
VirtualMachine(name='vm1', site=None, cluster=clusters[0]).full_clean()
|
||||||
|
@ -5,7 +5,7 @@ from netaddr import EUI
|
|||||||
from dcim.choices import InterfaceModeChoices
|
from dcim.choices import InterfaceModeChoices
|
||||||
from dcim.models import DeviceRole, Platform, Site
|
from dcim.models import DeviceRole, Platform, Site
|
||||||
from ipam.models import VLAN, VRF
|
from ipam.models import VLAN, VRF
|
||||||
from utilities.testing import ViewTestCases, create_tags
|
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
|
|
||||||
@ -101,9 +101,9 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
ClusterType.objects.bulk_create(clustertypes)
|
ClusterType.objects.bulk_create(clustertypes)
|
||||||
|
|
||||||
Cluster.objects.bulk_create([
|
Cluster.objects.bulk_create([
|
||||||
Cluster(name='Cluster 1', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
|
Cluster(name='Cluster 1', group=clustergroups[0], type=clustertypes[0], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[0]),
|
||||||
Cluster(name='Cluster 2', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
|
Cluster(name='Cluster 2', group=clustergroups[0], type=clustertypes[0], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[0]),
|
||||||
Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
|
Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[0]),
|
||||||
])
|
])
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
@ -112,6 +112,7 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'name': 'Cluster X',
|
'name': 'Cluster X',
|
||||||
'group': clustergroups[1].pk,
|
'group': clustergroups[1].pk,
|
||||||
'type': clustertypes[1].pk,
|
'type': clustertypes[1].pk,
|
||||||
|
'status': ClusterStatusChoices.STATUS_OFFLINE,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'site': sites[1].pk,
|
'site': sites[1].pk,
|
||||||
'comments': 'Some comments',
|
'comments': 'Some comments',
|
||||||
@ -119,15 +120,16 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,type",
|
"name,type,status",
|
||||||
"Cluster 4,Cluster Type 1",
|
"Cluster 4,Cluster Type 1,active",
|
||||||
"Cluster 5,Cluster Type 1",
|
"Cluster 5,Cluster Type 1,active",
|
||||||
"Cluster 6,Cluster Type 1",
|
"Cluster 6,Cluster Type 1,active",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'group': clustergroups[1].pk,
|
'group': clustergroups[1].pk,
|
||||||
'type': clustertypes[1].pk,
|
'type': clustertypes[1].pk,
|
||||||
|
'status': ClusterStatusChoices.STATUS_OFFLINE,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'site': sites[1].pk,
|
'site': sites[1].pk,
|
||||||
'comments': 'New comments',
|
'comments': 'New comments',
|
||||||
@ -166,24 +168,37 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
Platform.objects.bulk_create(platforms)
|
Platform.objects.bulk_create(platforms)
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
|
|
||||||
clusters = (
|
clusters = (
|
||||||
Cluster(name='Cluster 1', type=clustertype),
|
Cluster(name='Cluster 1', type=clustertype, site=sites[0]),
|
||||||
Cluster(name='Cluster 2', type=clustertype),
|
Cluster(name='Cluster 2', type=clustertype, site=sites[1]),
|
||||||
)
|
)
|
||||||
Cluster.objects.bulk_create(clusters)
|
Cluster.objects.bulk_create(clusters)
|
||||||
|
|
||||||
|
devices = (
|
||||||
|
create_test_device('device1', site=sites[0], cluster=clusters[0]),
|
||||||
|
create_test_device('device2', site=sites[1], cluster=clusters[1]),
|
||||||
|
)
|
||||||
|
|
||||||
VirtualMachine.objects.bulk_create([
|
VirtualMachine.objects.bulk_create([
|
||||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
|
VirtualMachine(name='Virtual Machine 1', site=sites[0], cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
|
||||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
|
VirtualMachine(name='Virtual Machine 2', site=sites[0], cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
|
||||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
|
VirtualMachine(name='Virtual Machine 3', site=sites[0], cluster=clusters[0], device=devices[0], role=deviceroles[0], platform=platforms[0]),
|
||||||
])
|
])
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'cluster': clusters[1].pk,
|
'cluster': clusters[1].pk,
|
||||||
|
'device': devices[1].pk,
|
||||||
|
'site': sites[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'platform': platforms[1].pk,
|
'platform': platforms[1].pk,
|
||||||
'name': 'Virtual Machine X',
|
'name': 'Virtual Machine X',
|
||||||
@ -200,14 +215,16 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,status,cluster",
|
"name,status,site,cluster,device",
|
||||||
"Virtual Machine 4,active,Cluster 1",
|
"Virtual Machine 4,active,Site 1,Cluster 1,device1",
|
||||||
"Virtual Machine 5,active,Cluster 1",
|
"Virtual Machine 5,active,Site 1,Cluster 1,device1",
|
||||||
"Virtual Machine 6,active,Cluster 1",
|
"Virtual Machine 6,active,Site 1,Cluster 1,",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
|
'site': sites[1].pk,
|
||||||
'cluster': clusters[1].pk,
|
'cluster': clusters[1].pk,
|
||||||
|
'device': devices[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'platform': platforms[1].pk,
|
'platform': platforms[1].pk,
|
||||||
'status': VirtualMachineStatusChoices.STATUS_STAGED,
|
'status': VirtualMachineStatusChoices.STATUS_STAGED,
|
||||||
@ -243,8 +260,8 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
cluster = Cluster.objects.create(name='Cluster 1', type=clustertype, site=site)
|
cluster = Cluster.objects.create(name='Cluster 1', type=clustertype, site=site)
|
||||||
virtualmachines = (
|
virtualmachines = (
|
||||||
VirtualMachine(name='Virtual Machine 1', cluster=cluster, role=devicerole),
|
VirtualMachine(name='Virtual Machine 1', site=site, cluster=cluster, role=devicerole),
|
||||||
VirtualMachine(name='Virtual Machine 2', cluster=cluster, role=devicerole),
|
VirtualMachine(name='Virtual Machine 2', site=site, cluster=cluster, role=devicerole),
|
||||||
)
|
)
|
||||||
VirtualMachine.objects.bulk_create(virtualmachines)
|
VirtualMachine.objects.bulk_create(virtualmachines)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user