mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Implement RestrictedQuerySet as a manager
This commit is contained in:
parent
5b6a6fb63e
commit
e23b2c4c4f
@ -8,6 +8,7 @@ from dcim.fields import ASNField
|
||||
from dcim.models import CableTermination
|
||||
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object
|
||||
from .choices import *
|
||||
@ -66,9 +67,10 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
|
||||
]
|
||||
@ -115,6 +117,8 @@ class CircuitType(ChangeLoggedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -300,6 +304,8 @@ class CircuitTermination(CableTermination):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
unique_together = ['circuit', 'term_side']
|
||||
|
@ -1,7 +1,9 @@
|
||||
from django.db.models import OuterRef, QuerySet, Subquery
|
||||
from django.db.models import OuterRef, Subquery
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
class CircuitQuerySet(QuerySet):
|
||||
class CircuitQuerySet(RestrictedQuerySet):
|
||||
|
||||
def annotate_sites(self):
|
||||
"""
|
||||
|
@ -25,7 +25,9 @@ from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, Ta
|
||||
from extras.utils import extras_features
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
from utilities.validators import ExclusionValidator
|
||||
from .device_component_templates import (
|
||||
@ -103,6 +105,8 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
class MPTTMeta:
|
||||
@ -244,6 +248,8 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
|
||||
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
@ -326,6 +332,8 @@ class RackGroup(MPTTModel, ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = ['site', 'parent', 'name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -388,6 +396,8 @@ class RackRole(ChangeLoggedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -526,6 +536,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'group', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width',
|
||||
'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
||||
@ -821,6 +833,8 @@ class RackReservation(ChangeLoggedModel):
|
||||
max_length=200
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'rack', 'units', 'tenant', 'user', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -900,6 +914,8 @@ class Manufacturer(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -982,9 +998,10 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = [
|
||||
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
]
|
||||
@ -1206,6 +1223,8 @@ class DeviceRole(ChangeLoggedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'color', 'vm_role', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -1263,6 +1282,8 @@ class Platform(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -1429,6 +1450,8 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face', 'comments',
|
||||
@ -1741,9 +1764,10 @@ class VirtualChassis(ChangeLoggedModel):
|
||||
max_length=30,
|
||||
blank=True
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['master', 'domain']
|
||||
|
||||
class Meta:
|
||||
@ -1813,6 +1837,8 @@ class PowerPanel(ChangeLoggedModel):
|
||||
max_length=50
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'rack_group', 'name']
|
||||
|
||||
class Meta:
|
||||
@ -1916,9 +1942,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'power_panel', 'rack_group', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||
'amperage', 'max_utilization', 'comments',
|
||||
@ -2084,6 +2111,8 @@ class Cable(ChangeLoggedModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id', 'type', 'status', 'label',
|
||||
'color', 'length', 'length_unit',
|
||||
|
@ -6,6 +6,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from extras.models import ObjectChange
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.utils import serialize_object
|
||||
from .device_components import (
|
||||
@ -26,6 +27,7 @@ __all__ = (
|
||||
|
||||
|
||||
class ComponentTemplateModel(models.Model):
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
@ -16,6 +16,7 @@ from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.choices import VMInterfaceTypeChoices
|
||||
@ -41,6 +42,8 @@ class ComponentModel(models.Model):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
@ -12,6 +12,7 @@ from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import deepmerge, render_jinja2
|
||||
from extras.choices import *
|
||||
from extras.constants import *
|
||||
@ -670,6 +671,8 @@ class ObjectChange(models.Model):
|
||||
editable=False
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
||||
'related_object_type', 'related_object_id', 'object_repr', 'object_data',
|
||||
|
@ -6,6 +6,7 @@ from taggit.models import TagBase, GenericTaggedItemBase
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.fields import ColorField
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
#
|
||||
@ -21,6 +22,8 @@ class Tag(TagBase, ChangeLoggedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:tag', args=[self.slug])
|
||||
|
||||
|
@ -2,6 +2,8 @@ from collections import OrderedDict
|
||||
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
class CustomFieldQueryset:
|
||||
"""
|
||||
@ -19,7 +21,7 @@ class CustomFieldQueryset:
|
||||
yield obj
|
||||
|
||||
|
||||
class ConfigContextQuerySet(QuerySet):
|
||||
class ConfigContextQuerySet(RestrictedQuerySet):
|
||||
|
||||
def get_for_object(self, obj):
|
||||
"""
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.db import models
|
||||
|
||||
from ipam.lookups import Host, Inet
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
class IPAddressManager(models.Manager):
|
||||
@ -13,5 +14,5 @@ class IPAddressManager(models.Manager):
|
||||
then re-cast this value to INET() so that records will be ordered properly. We are essentially re-casting each
|
||||
IP address as a /32 or /128.
|
||||
"""
|
||||
qs = super().get_queryset()
|
||||
qs = RestrictedQuerySet(self.model, using=self._db)
|
||||
return qs.order_by(Inet(Host('address')))
|
||||
|
@ -12,6 +12,7 @@ from dcim.models import Device, Interface
|
||||
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.models import VirtualMachine
|
||||
from .choices import *
|
||||
@ -74,9 +75,10 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
clone_fields = [
|
||||
'tenant', 'enforce_unique', 'description',
|
||||
@ -131,6 +133,8 @@ class RIR(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'is_private', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -179,9 +183,10 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
||||
clone_fields = [
|
||||
'rir', 'date_added', 'description',
|
||||
@ -274,6 +279,8 @@ class Role(ChangeLoggedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'weight', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -360,9 +367,9 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'description',
|
||||
@ -631,9 +638,9 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = IPAddressManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'is_primary',
|
||||
@ -828,6 +835,8 @@ class VLANGroup(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'site', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -923,9 +932,10 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
clone_fields = [
|
||||
'site', 'group', 'tenant', 'status', 'role', 'description',
|
||||
@ -1039,9 +1049,10 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'port', 'description']
|
||||
|
||||
class Meta:
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django.db.models import QuerySet
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
class PrefixQuerySet(QuerySet):
|
||||
class PrefixQuerySet(RestrictedQuerySet):
|
||||
|
||||
def annotate_depth(self, limit=None):
|
||||
"""
|
||||
|
@ -17,6 +17,7 @@ from dcim.models import Device
|
||||
from extras.models import CustomFieldModel, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from .exceptions import InvalidKey
|
||||
from .hashers import SecretValidationHasher
|
||||
from .querysets import UserKeyQuerySet
|
||||
@ -268,6 +269,8 @@ class SecretRole(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -333,9 +336,10 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
plaintext = None
|
||||
csv_headers = ['device', 'role', 'name', 'plaintext']
|
||||
|
||||
|
@ -7,6 +7,8 @@ from taggit.managers import TaggableManager
|
||||
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import serialize_object
|
||||
|
||||
|
||||
@ -40,6 +42,8 @@ class TenantGroup(MPTTModel, ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = TreeManager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'parent', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -104,9 +108,10 @@ class Tenant(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'group', 'description', 'comments']
|
||||
clone_fields = [
|
||||
'group', 'description',
|
||||
|
19
netbox/utilities/mptt.py
Normal file
19
netbox/utilities/mptt.py
Normal file
@ -0,0 +1,19 @@
|
||||
from mptt.managers import TreeManager as TreeManager_
|
||||
from mptt.querysets import TreeQuerySet as TreeQuerySet_
|
||||
|
||||
from django.db.models import Manager
|
||||
from .querysets import RestrictedQuerySet
|
||||
|
||||
|
||||
class TreeQuerySet(TreeQuerySet_, RestrictedQuerySet):
|
||||
"""
|
||||
Mate django-mptt's TreeQuerySet with our RestrictedQuerySet for permissions enforcement.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TreeManager(Manager.from_queryset(TreeQuerySet), TreeManager_):
|
||||
"""
|
||||
Extend django-mptt's TreeManager to incorporate RestrictedQuerySet().
|
||||
"""
|
||||
pass
|
@ -27,7 +27,7 @@ class RestrictedQuerySet(QuerySet):
|
||||
# Determine what constraints (if any) have been placed on this user for this action and model
|
||||
# TODO: Find a better way to ensure permissions are cached
|
||||
if not hasattr(user, '_object_perm_cache'):
|
||||
user.get_all_permisisons()
|
||||
user.get_all_permissions()
|
||||
obj_perm_attrs = user._object_perm_cache[permission_required]
|
||||
|
||||
# Filter the queryset to include only objects with allowed attributes
|
||||
|
@ -9,6 +9,7 @@ from dcim.models import Device
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from .choices import *
|
||||
|
||||
|
||||
@ -40,6 +41,8 @@ class ClusterType(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -79,6 +82,8 @@ class ClusterGroup(ChangeLoggedModel):
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'slug', 'description']
|
||||
|
||||
class Meta:
|
||||
@ -145,9 +150,10 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = ['name', 'type', 'group', 'site', 'comments']
|
||||
clone_fields = [
|
||||
'type', 'group', 'tenant', 'site',
|
||||
@ -269,9 +275,10 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
content_type_field='obj_type',
|
||||
object_id_field='obj_id'
|
||||
)
|
||||
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user