632 lines
22 KiB
Python
Executable File
632 lines
22 KiB
Python
Executable File
#
|
|
# Copyright (c) 2013-2015, 2017 Wind River Systems, Inc.
|
|
#
|
|
# The right to copy, distribute, modify, or otherwise make use
|
|
# of this software may be licensed only pursuant to the terms
|
|
# of an applicable Wind River license agreement.
|
|
#
|
|
|
|
import logging
|
|
import re
|
|
|
|
from django.core.urlresolvers import reverse # noqa
|
|
from django import template
|
|
from django.template import defaultfilters as filters
|
|
from django.utils.translation import string_concat # noqa
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.utils.translation import ungettext_lazy
|
|
|
|
from horizon import exceptions
|
|
from horizon import tables
|
|
from openstack_dashboard import api
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
# ##########
|
|
# ACTIONS
|
|
# ##########
|
|
class CreateStorageVolume(tables.LinkAction):
|
|
name = "createstoragevolume"
|
|
verbose_name = ("Assign Storage Function")
|
|
url = "horizon:admin:inventory:addstoragevolume"
|
|
classes = ("ajax-modal", "btn-create")
|
|
icon = "plus"
|
|
|
|
def get_link_url(self, datum=None):
|
|
host_id = self.table.kwargs['host_id']
|
|
return reverse(self.url, args=(host_id,))
|
|
|
|
def allowed(self, request, datum):
|
|
host = self.table.kwargs['host']
|
|
self.verbose_name = _("Assign Storage Function")
|
|
|
|
classes = [c for c in self.classes if c != "disabled"]
|
|
self.classes = classes
|
|
|
|
if host._personality != 'storage':
|
|
return False
|
|
|
|
if host._administrative == 'unlocked':
|
|
if 'storage' in host._subfunctions:
|
|
if "disabled" not in self.classes:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
self.verbose_name = string_concat(self.verbose_name, ' ',
|
|
_("(Node Unlocked)"))
|
|
|
|
return True
|
|
|
|
|
|
class CreateDiskProfile(tables.LinkAction):
|
|
name = "creatediskprofile"
|
|
verbose_name = ("Create Storage Profile")
|
|
url = "horizon:admin:inventory:adddiskprofile"
|
|
classes = ("ajax-modal", "btn-create")
|
|
|
|
def get_link_url(self, datum=None):
|
|
host_id = self.table.kwargs['host_id']
|
|
return reverse(self.url, args=(host_id,))
|
|
|
|
def allowed(self, request, datum):
|
|
return True
|
|
|
|
|
|
class CreatePartition(tables.LinkAction):
|
|
name = "createpartition"
|
|
verbose_name = ("Create a new partition")
|
|
url = "horizon:admin:inventory:createpartition"
|
|
classes = ("ajax-modal", "btn-create")
|
|
icon = "plus"
|
|
|
|
def get_link_url(self, datum=None):
|
|
host_id = self.table.kwargs['host_id']
|
|
return reverse(self.url, args=(host_id,))
|
|
|
|
def allowed(self, request, datum):
|
|
host = self.table.kwargs['host']
|
|
self.verbose_name = _("Create a new partition")
|
|
|
|
classes = [c for c in self.classes if c != "disabled"]
|
|
self.classes = classes
|
|
|
|
if host._personality != 'storage':
|
|
return True
|
|
|
|
return True
|
|
|
|
|
|
class DeletePartition(tables.DeleteAction):
|
|
@staticmethod
|
|
def action_present(count):
|
|
return ungettext_lazy(
|
|
u"Delete Partition",
|
|
u"Delete Partitions",
|
|
count
|
|
)
|
|
|
|
@staticmethod
|
|
def action_past(count):
|
|
return ungettext_lazy(
|
|
u"Deleted Partition",
|
|
u"Deleted Partitions",
|
|
count
|
|
)
|
|
|
|
def allowed(self, request, partition=None):
|
|
host = self.table.kwargs['host']
|
|
PARTITION_IN_USE_STATUS = api.sysinv.PARTITION_IN_USE_STATUS
|
|
PARTITION_STATUS_MSG = api.sysinv.PARTITION_STATUS_MSG
|
|
|
|
if partition:
|
|
if partition.type_guid != api.sysinv.USER_PARTITION_PHYS_VOL:
|
|
return False
|
|
|
|
if (partition.status ==
|
|
PARTITION_STATUS_MSG[PARTITION_IN_USE_STATUS]):
|
|
return False
|
|
|
|
if partition.ipv_uuid:
|
|
return False
|
|
|
|
# Get all the partitions from the same disk.
|
|
disk_partitions = \
|
|
api.sysinv.host_disk_partition_list(request, host.uuid,
|
|
partition.idisk_uuid)
|
|
|
|
if partition.device_path:
|
|
partition_number = re.match('.*?([0-9]+)$',
|
|
partition.device_path).group(1)
|
|
for dpart in disk_partitions:
|
|
dpart_number = re.match('.*?([0-9]+)$',
|
|
dpart.device_path).group(1)
|
|
if int(dpart_number) > int(partition_number):
|
|
return False
|
|
|
|
return True
|
|
|
|
def delete(self, request, partition_id):
|
|
host_id = self.table.kwargs['host_id']
|
|
try:
|
|
api.sysinv.host_disk_partition_delete(request, partition_id)
|
|
except Exception as e:
|
|
msg = _('Failed to delete host %(hid)s partition %(pv)s. '
|
|
'%(e_msg)s') % {'hid': host_id,
|
|
'pv': partition_id,
|
|
'e_msg': e}
|
|
LOG.info(msg)
|
|
redirect = reverse('horizon:admin:inventory:detail',
|
|
args=(host_id,))
|
|
exceptions.handle(request, msg, redirect=redirect)
|
|
|
|
|
|
class EditPartition(tables.LinkAction):
|
|
name = "editpartition"
|
|
verbose_name = _("Edit")
|
|
url = "horizon:admin:inventory:editpartition"
|
|
|
|
classes = ("ajax-modal", "btn-edit")
|
|
|
|
def get_link_url(self, partition):
|
|
host_id = self.table.kwargs['host_id']
|
|
return reverse(self.url, args=(host_id, partition.uuid))
|
|
|
|
def allowed(self, request, partition=None):
|
|
host = self.table.kwargs['host']
|
|
PARTITION_IN_USE_STATUS = api.sysinv.PARTITION_IN_USE_STATUS
|
|
PARTITION_STATUS_MSG = api.sysinv.PARTITION_STATUS_MSG
|
|
|
|
if partition:
|
|
if partition.type_guid != api.sysinv.USER_PARTITION_PHYS_VOL:
|
|
return False
|
|
|
|
if (partition.status ==
|
|
PARTITION_STATUS_MSG[PARTITION_IN_USE_STATUS]):
|
|
return False
|
|
|
|
if partition.ipv_uuid:
|
|
return False
|
|
|
|
# Get all the partitions from the same disk.
|
|
disk_partitions = \
|
|
api.sysinv.host_disk_partition_list(request,
|
|
host.uuid,
|
|
partition.idisk_uuid)
|
|
|
|
if partition.device_path:
|
|
partition_number = re.match('.*?([0-9]+)$',
|
|
partition.device_path).group(1)
|
|
for dpart in disk_partitions:
|
|
dpart_number = re.match('.*?([0-9]+)$',
|
|
dpart.device_path).group(1)
|
|
if int(dpart_number) > int(partition_number):
|
|
return False
|
|
|
|
return True
|
|
|
|
# ##########
|
|
# TABLES
|
|
# ##########
|
|
|
|
|
|
def get_model_num(disk):
|
|
return disk.get_model_num()
|
|
|
|
|
|
def get_disk_info(disk):
|
|
template_name = 'admin/inventory/_disk_info.html'
|
|
context = {
|
|
"disk": disk,
|
|
"disk_info": disk.device_path,
|
|
"id": disk.uuid,
|
|
}
|
|
return template.loader.render_to_string(template_name, context)
|
|
|
|
|
|
class DisksTable(tables.DataTable):
|
|
uuid = tables.Column('uuid',
|
|
verbose_name=('UUID'))
|
|
disk_info = tables.Column(get_disk_info,
|
|
verbose_name=_("Disk info"),
|
|
attrs={'data-type': 'disk_info'})
|
|
type = tables.Column('device_type',
|
|
verbose_name=('Type'))
|
|
size = tables.Column('size_mib',
|
|
verbose_name=('Size (MiB)'))
|
|
available_size = tables.Column('available_mib',
|
|
verbose_name=('Available Size (MiB)'))
|
|
rpm = tables.Column('rpm',
|
|
verbose_name=('RPM'))
|
|
|
|
serial_id = tables.Column('serial_id',
|
|
verbose_name=('Serial ID'))
|
|
|
|
model_num = tables.Column(get_model_num,
|
|
verbose_name=('Model'))
|
|
|
|
def get_object_id(self, datum):
|
|
return unicode(datum.uuid)
|
|
|
|
class Meta(object):
|
|
name = "disks"
|
|
verbose_name = ("Disks")
|
|
columns = ('uuid', 'disk_info', 'type', 'size', 'available_size',
|
|
'rpm', 'serial_id', 'model_num')
|
|
multi_select = False
|
|
table_actions = ()
|
|
|
|
|
|
class EditStor(tables.LinkAction):
|
|
name = "editstoragevolume"
|
|
verbose_name = _("Edit")
|
|
url = "horizon:admin:inventory:editstoragevolume"
|
|
|
|
classes = ("ajax-modal", "btn-edit")
|
|
|
|
def get_link_url(self, stor):
|
|
host_id = self.table.kwargs['host_id']
|
|
return reverse(self.url, args=(host_id, stor.uuid))
|
|
|
|
def allowed(self, request, stor=None):
|
|
host = self.table.kwargs['host']
|
|
|
|
classes = [c for c in self.classes if c != "disabled"]
|
|
self.classes = classes
|
|
|
|
if host._administrative == 'unlocked':
|
|
if 'storage' in host._subfunctions:
|
|
if "disabled" not in self.classes:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
|
|
if stor and stor.function == 'osd':
|
|
forihostuuid = self.table.kwargs['host'].uuid
|
|
journal_stors = \
|
|
api.sysinv.host_stor_get_by_function(request, forihostuuid,
|
|
'journal')
|
|
|
|
if not journal_stors:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
|
|
return True
|
|
|
|
|
|
class DeleteStor(tables.DeleteAction):
|
|
@staticmethod
|
|
def action_present(count):
|
|
return ungettext_lazy(
|
|
u"Delete Journal",
|
|
u"Delete Journals",
|
|
count
|
|
)
|
|
|
|
@staticmethod
|
|
def action_past(count):
|
|
return ungettext_lazy(
|
|
u"Deleted Journal",
|
|
u"Deleted Journals",
|
|
count
|
|
)
|
|
|
|
def allowed(self, request, stor):
|
|
host = self.table.kwargs['host']
|
|
|
|
classes = [c for c in self.classes if c != "disabled"]
|
|
self.classes = classes
|
|
|
|
if host._administrative == 'unlocked':
|
|
if 'storage' in host._subfunctions:
|
|
if "disabled" not in self.classes:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
|
|
if stor:
|
|
return stor.function == 'journal'
|
|
|
|
def delete(self, request, obj_id):
|
|
api.sysinv.host_stor_delete(request, obj_id)
|
|
|
|
|
|
class StorageVolumesTable(tables.DataTable):
|
|
uuid = tables.Column('uuid',
|
|
verbose_name=('UUID'))
|
|
osdid = tables.Column('osdid',
|
|
verbose_name=('OSD ID'))
|
|
function = tables.Column('function',
|
|
verbose_name=('Function'))
|
|
idisk_uuid = tables.Column('idisk_uuid',
|
|
verbose_name=('Disk UUID'))
|
|
journal_path = tables.Column('journal_path',
|
|
verbose_name=('Journal Path'))
|
|
journal_size_mib = tables.Column('journal_size_mib',
|
|
verbose_name=('Journal MiB'))
|
|
journal_location = tables.Column('journal_location',
|
|
verbose_name=('Journal Location'))
|
|
|
|
def get_object_id(self, datum):
|
|
return unicode(datum.uuid)
|
|
|
|
class Meta(object):
|
|
name = "storagevolumes"
|
|
verbose_name = ("Storage Functions")
|
|
columns = ('uuid', 'function', 'osdid', 'idisk_uuid', 'journal_path',
|
|
'journal_size_mib', 'journal_location')
|
|
multi_select = False
|
|
row_actions = (DeleteStor, EditStor,)
|
|
table_actions = (CreateStorageVolume, CreateDiskProfile,)
|
|
|
|
|
|
class AddLocalVolumeGroup(tables.LinkAction):
|
|
name = "addlocalvolumegroup"
|
|
verbose_name = ("Add Local Volume Group")
|
|
url = "horizon:admin:inventory:addlocalvolumegroup"
|
|
classes = ("ajax-modal", "btn-create")
|
|
icon = "plus"
|
|
|
|
def get_link_url(self, datum=None):
|
|
host_id = self.table.kwargs['host_id']
|
|
return reverse(self.url, args=(host_id,))
|
|
|
|
def allowed(self, request, datum):
|
|
host = self.table.kwargs['host']
|
|
self.verbose_name = _("Add Local Volume Group")
|
|
classes = [c for c in self.classes if c != "disabled"]
|
|
self.classes = classes
|
|
|
|
if not host._administrative == 'locked':
|
|
if 'compute' in host._subfunctions and \
|
|
host.compute_config_required is False:
|
|
if "disabled" not in self.classes:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
self.verbose_name = string_concat(self.verbose_name, ' ',
|
|
_("(Node Unlocked)"))
|
|
|
|
# LVGs that are considered as "present" in the system are those
|
|
# in an adding or provisioned state.
|
|
current_lvg_states = [api.sysinv.LVG_ADD, api.sysinv.LVG_PROV]
|
|
ilvg_list = api.sysinv.host_lvg_list(request, host.uuid)
|
|
current_lvgs = [lvg.lvm_vg_name for lvg in ilvg_list
|
|
if lvg.vg_state in current_lvg_states]
|
|
compatible_lvgs = []
|
|
|
|
if host._personality == 'controller':
|
|
compatible_lvgs += [api.sysinv.LVG_CINDER_VOLUMES]
|
|
|
|
if 'compute' in host._subfunctions:
|
|
compatible_lvgs += [api.sysinv.LVG_NOVA_LOCAL]
|
|
|
|
allowed_lvgs = set(compatible_lvgs) - set(current_lvgs)
|
|
if not any(allowed_lvgs):
|
|
if "disabled" not in self.classes:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
self.verbose_name = string_concat(self.verbose_name, ' ',
|
|
_("(All Added)"))
|
|
|
|
return True # The action should always be displayed
|
|
|
|
|
|
class RemoveLocalVolumeGroup(tables.DeleteAction):
|
|
@staticmethod
|
|
def action_present(count):
|
|
return ungettext_lazy(
|
|
u"Delete Local Volume Group",
|
|
u"Delete Local Volume Groups",
|
|
count
|
|
)
|
|
|
|
@staticmethod
|
|
def action_past(count):
|
|
return ungettext_lazy(
|
|
u"Deleted Local Volume Group",
|
|
u"Deleted Local Volume Groups",
|
|
count
|
|
)
|
|
|
|
def allowed(self, request, lvg=None):
|
|
host = self.table.kwargs['host']
|
|
return ((((host._administrative == 'locked') or
|
|
(('compute' in host._subfunctions) and
|
|
(host.compute_config_required is True))) and
|
|
(lvg.lvm_vg_name == api.sysinv.LVG_NOVA_LOCAL)) or
|
|
((api.sysinv.CINDER_BACKEND_LVM in
|
|
api.sysinv.get_cinder_backend(request)) and
|
|
(lvg.lvm_vg_name == api.sysinv.LVG_CINDER_VOLUMES) and
|
|
(api.sysinv.LVG_ADD in lvg.vg_state)))
|
|
|
|
def delete(self, request, lvg_id):
|
|
host_id = self.table.kwargs['host_id']
|
|
try:
|
|
api.sysinv.host_lvg_delete(request, lvg_id)
|
|
except Exception as e:
|
|
msg = _('Failed to delete host %(hid)s local '
|
|
'volume group %(lvg)s '
|
|
'%(e_msg)s') % \
|
|
{'hid': host_id, 'lvg': lvg_id, 'e_msg': e}
|
|
|
|
redirect = reverse('horizon:admin:inventory:detail',
|
|
args=(host_id,))
|
|
exceptions.handle(request, msg, redirect=redirect)
|
|
|
|
|
|
class LocalVolumeGroupsTable(tables.DataTable):
|
|
name = tables.Column('lvm_vg_name',
|
|
link="horizon:admin:inventory:localvolumegroupdetail",
|
|
verbose_name=('Name'))
|
|
state = tables.Column('vg_state',
|
|
verbose_name=('State'))
|
|
access = tables.Column('lvm_vg_access',
|
|
verbose_name=('Access'))
|
|
size = tables.Column('lvm_vg_size',
|
|
verbose_name=('Size'),
|
|
filters=(filters.filesizeformat,))
|
|
pvs = tables.Column('lvm_cur_pv',
|
|
verbose_name=('Current Physical Volumes'))
|
|
lvs = tables.Column('lvm_cur_lv',
|
|
verbose_name=('Current Logical Volumes'))
|
|
|
|
def get_object_id(self, datum):
|
|
return unicode(datum.uuid)
|
|
|
|
def get_object_display(self, datum):
|
|
msg = datum.uuid
|
|
if datum.lvm_vg_name:
|
|
msg += " (%s)" % datum.lvm_vg_name
|
|
return unicode(msg)
|
|
|
|
class Meta(object):
|
|
name = "localvolumegroups"
|
|
verbose_name = ("Local Volume Groups")
|
|
columns = ('name', 'state', 'access', 'size', 'pvs', 'lvs',)
|
|
multi_select = False
|
|
row_actions = (RemoveLocalVolumeGroup,)
|
|
table_actions = (AddLocalVolumeGroup, CreateDiskProfile)
|
|
|
|
|
|
class AddPhysicalVolume(tables.LinkAction):
|
|
name = "addphysicalvolume"
|
|
verbose_name = ("Add Physical Volume")
|
|
url = "horizon:admin:inventory:addphysicalvolume"
|
|
classes = ("ajax-modal", "btn-create")
|
|
icon = "plus"
|
|
|
|
def get_link_url(self, datum=None):
|
|
host_id = self.table.kwargs['host_id']
|
|
return reverse(self.url, args=(host_id,))
|
|
|
|
def allowed(self, request, datum):
|
|
host = self.table.kwargs['host']
|
|
self.verbose_name = _("Add Physical Volume")
|
|
classes = [c for c in self.classes if c != "disabled"]
|
|
self.classes = classes
|
|
|
|
# cgts-vg, cinder-volumes: Allow adding to any controller
|
|
if host._personality == api.sysinv.PERSONALITY_CONTROLLER:
|
|
return True
|
|
|
|
# nova-local: Allow adding to any locked host with a compute
|
|
# subfunction. On an AIO, the previous check superceeds this.
|
|
if host._administrative != 'locked':
|
|
if 'compute' in host._subfunctions and \
|
|
host.compute_config_required is False:
|
|
if "disabled" not in self.classes:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
self.verbose_name = string_concat(self.verbose_name, ' ',
|
|
_("(Node Unlocked)"))
|
|
elif "nova-local" not in [
|
|
lvg.lvm_vg_name for lvg in
|
|
api.sysinv.host_lvg_list(request, host.uuid)]:
|
|
if "disabled" not in self.classes:
|
|
self.classes = [c for c in self.classes] + ['disabled']
|
|
self.verbose_name = string_concat(self.verbose_name, ' ',
|
|
_("(No nova-local LVG)"))
|
|
|
|
return True # The action should always be displayed
|
|
|
|
|
|
class RemovePhysicalVolume(tables.DeleteAction):
|
|
@staticmethod
|
|
def action_present(count):
|
|
return ungettext_lazy(
|
|
u"Delete Physical Volume",
|
|
u"Delete Physical Volumes",
|
|
count
|
|
)
|
|
|
|
@staticmethod
|
|
def action_past(count):
|
|
return ungettext_lazy(
|
|
u"Deleted Physical Volume",
|
|
u"Deleted Physical Volumes",
|
|
count
|
|
)
|
|
|
|
def allowed(self, request, pv=None):
|
|
host = self.table.kwargs['host']
|
|
return ((((host._administrative == 'locked') or
|
|
(('compute' in host._subfunctions) and
|
|
(host.compute_config_required is True))) and
|
|
(pv.lvm_vg_name == api.sysinv.LVG_NOVA_LOCAL)) or
|
|
((api.sysinv.CINDER_BACKEND_LVM in
|
|
api.sysinv.get_cinder_backend(request)) and
|
|
((pv.lvm_vg_name == api.sysinv.LVG_CINDER_VOLUMES) and
|
|
(api.sysinv.PV_ADD in pv.pv_state))))
|
|
|
|
def delete(self, request, pv_id):
|
|
host_id = self.table.kwargs['host_id']
|
|
try:
|
|
api.sysinv.host_pv_delete(request, pv_id)
|
|
except Exception as e:
|
|
msg = _('Failed to delete host %(hid)s physical volume %(pv)s. '
|
|
'%(e_msg)s') % {'hid': host_id, 'pv': pv_id, 'e_msg': e}
|
|
LOG.info(msg)
|
|
redirect = reverse('horizon:admin:inventory:detail',
|
|
args=(host_id,))
|
|
exceptions.handle(request, msg, redirect=redirect)
|
|
|
|
|
|
class PhysicalVolumesTable(tables.DataTable):
|
|
name = tables.Column('lvm_pv_name',
|
|
link="horizon:admin:inventory:physicalvolumedetail",
|
|
verbose_name=('Name'))
|
|
pv_state = tables.Column('pv_state',
|
|
verbose_name=('State'))
|
|
pv_type = tables.Column('pv_type',
|
|
verbose_name=('Type'))
|
|
disk_or_part_uuid = tables.Column('disk_or_part_uuid',
|
|
verbose_name=('Disk or Partition UUID'))
|
|
disk_or_part_device_path = tables.Column('disk_or_part_device_path',
|
|
verbose_name=('Disk or Partition'
|
|
' Device Path'))
|
|
lvm_vg_name = tables.Column('lvm_vg_name',
|
|
verbose_name=('LVM Volume Group Name'))
|
|
|
|
def get_object_id(self, datum):
|
|
return unicode(datum.uuid)
|
|
|
|
def get_object_display(self, datum):
|
|
msg = datum.uuid
|
|
if datum.lvm_pv_name:
|
|
msg += " (%s)" % datum.lvm_pv_name
|
|
return unicode(msg)
|
|
|
|
class Meta(object):
|
|
name = "physicalvolumes"
|
|
verbose_name = ("Physical Volumes")
|
|
columns = ('name', 'pv_state', 'pv_type', 'disk_or_part_uuid',
|
|
'disk_or_part_device_node', 'disk_or_part_device_path',
|
|
'lvm_vg_name')
|
|
multi_select = False
|
|
table_actions = (AddPhysicalVolume,)
|
|
row_actions = (RemovePhysicalVolume,)
|
|
|
|
|
|
class PartitionsTable(tables.DataTable):
|
|
uuid = tables.Column('uuid',
|
|
verbose_name=('UUID'))
|
|
size_mib = tables.Column('size_mib',
|
|
verbose_name=('Size (MiB)'))
|
|
device_path = tables.Column('device_path',
|
|
verbose_name=('Partition Device Path'))
|
|
type_name = tables.Column('type_name',
|
|
verbose_name=('Partition Type'))
|
|
ipv_uuid = tables.Column('ipv_uuid',
|
|
verbose_name=('Physical Volume UUID'))
|
|
idisk_uuid = tables.Column('disk_uuid',
|
|
verbose_name=('Disk UUID'))
|
|
status = tables.Column('status',
|
|
verbose_name=('Status'))
|
|
|
|
def get_object_id(self, datum):
|
|
return unicode(datum.uuid)
|
|
|
|
def get_object_display(self, datum):
|
|
msg = datum.uuid
|
|
if datum.device_path:
|
|
msg += " (%s)" % datum.device_path
|
|
return unicode(msg)
|
|
|
|
class Meta(object):
|
|
name = "partitions"
|
|
verbose_name = ("Partitions")
|
|
columns = ('uuid', 'device_path', 'size_mib', 'type_name', 'status')
|
|
multi_select = False
|
|
row_actions = (EditPartition, DeletePartition,)
|
|
table_actions = (CreatePartition,)
|