mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 17:08:41 -06:00
Initial work on power modeling (WIP)
This commit is contained in:
parent
0b95016e00
commit
5b753923b6
@ -443,3 +443,33 @@ RACK_DIMENSION_UNIT_CHOICES = (
|
|||||||
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
|
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
|
||||||
(LENGTH_UNIT_INCH, 'Inches'),
|
(LENGTH_UNIT_INCH, 'Inches'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Power feeds
|
||||||
|
POWERFEED_TYPE_PRIMARY = 1
|
||||||
|
POWERFEED_TYPE_REDUNDANT = 2
|
||||||
|
POWERFEED_TYPE_CHOICES = (
|
||||||
|
(POWERFEED_TYPE_PRIMARY, 'AC'),
|
||||||
|
(POWERFEED_TYPE_REDUNDANT, 'DC'),
|
||||||
|
)
|
||||||
|
POWERFEED_SUPPLY_AC = 1
|
||||||
|
POWERFEED_SUPPLY_DC = 2
|
||||||
|
POWERFEED_SUPPLY_CHOICES = (
|
||||||
|
(POWERFEED_SUPPLY_AC, 'AC'),
|
||||||
|
(POWERFEED_SUPPLY_DC, 'DC'),
|
||||||
|
)
|
||||||
|
POWERFEED_PHASE_SINGLE = 1
|
||||||
|
POWERFEED_PHASE_3PHASE = 3
|
||||||
|
POWERFEED_PHASE_CHOICES = (
|
||||||
|
(POWERFEED_PHASE_SINGLE, 'Single phase'),
|
||||||
|
(POWERFEED_PHASE_3PHASE, 'Three-phase'),
|
||||||
|
)
|
||||||
|
POWERFEED_STATUS_OFFLINE = 0
|
||||||
|
POWERFEED_STATUS_ACTIVE = 1
|
||||||
|
POWERFEED_STATUS_PLANNED = 2
|
||||||
|
POWERFEED_STATUS_FAILED = 4
|
||||||
|
POWERFEED_STATUS_CHOICES = (
|
||||||
|
(POWERFEED_STATUS_ACTIVE, 'Active'),
|
||||||
|
(POWERFEED_STATUS_OFFLINE, 'Offline'),
|
||||||
|
(POWERFEED_STATUS_PLANNED, 'Planned'),
|
||||||
|
(POWERFEED_STATUS_FAILED, 'Failed'),
|
||||||
|
)
|
||||||
|
@ -26,8 +26,8 @@ from .constants import *
|
|||||||
from .models import (
|
from .models import (
|
||||||
Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||||
Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
|
Device, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, Manufacturer,
|
||||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, PowerPortTemplate,
|
||||||
RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis
|
Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||||
)
|
)
|
||||||
|
|
||||||
DEVICE_BY_PK_RE = r'{\d+\}'
|
DEVICE_BY_PK_RE = r'{\d+\}'
|
||||||
@ -3156,3 +3156,169 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
null_label='-- None --',
|
null_label='-- None --',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power panels
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerPanelForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
rackgroup = ChainedModelChoiceField(
|
||||||
|
queryset=RackGroup.objects.all(),
|
||||||
|
chains=(
|
||||||
|
('site', 'site'),
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/rack-groups/',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerPanel
|
||||||
|
fields = [
|
||||||
|
'site', 'rackgroup', 'name',
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'site': APISelect(
|
||||||
|
api_url="/api/dcim/sites/",
|
||||||
|
filter_for={
|
||||||
|
'rackgroup': 'site_id',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPanelCSVForm(forms.ModelForm):
|
||||||
|
site = forms.ModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent site',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Site not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
group_name = forms.CharField(
|
||||||
|
help_text='Name of rack group',
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerPanel
|
||||||
|
fields = PowerPanel.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power feeds
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerFeedForm(BootstrapMixin, CustomFieldForm):
|
||||||
|
tags = TagField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerFeed
|
||||||
|
fields = [
|
||||||
|
'powerpanel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization',
|
||||||
|
'comments', 'tags',
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'site': APISelect(
|
||||||
|
api_url="/api/dcim/sites/",
|
||||||
|
filter_for={
|
||||||
|
'rackgroup': 'site_id',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'type': StaticSelect2(),
|
||||||
|
'status': StaticSelect2(),
|
||||||
|
'supply': StaticSelect2(),
|
||||||
|
'phase': StaticSelect2(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedCSVForm(forms.ModelForm):
|
||||||
|
type = CSVChoiceField(
|
||||||
|
choices=POWERFEED_TYPE_CHOICES,
|
||||||
|
required=False,
|
||||||
|
help_text='Primary or redundant'
|
||||||
|
)
|
||||||
|
status = CSVChoiceField(
|
||||||
|
choices=POWERFEED_STATUS_CHOICES,
|
||||||
|
required=False,
|
||||||
|
help_text='Operational status'
|
||||||
|
)
|
||||||
|
supply = CSVChoiceField(
|
||||||
|
choices=POWERFEED_SUPPLY_CHOICES,
|
||||||
|
required=False,
|
||||||
|
help_text='AC/DC'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PowerFeed
|
||||||
|
fields = PowerFeed.csv_headers
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=PowerFeed.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput
|
||||||
|
)
|
||||||
|
powerpanel = forms.ModelChoiceField(
|
||||||
|
queryset=PowerPanel.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/sites",
|
||||||
|
filter_for={
|
||||||
|
'rackgroup': 'site_id',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rackgroup = forms.ModelChoiceField(
|
||||||
|
queryset=RackGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/rack-groups",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(POWERFEED_TYPE_CHOICES),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(POWERFEED_STATUS_CHOICES),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
supply = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
voltage = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
amperage = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
phase = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(POWERFEED_PHASE_CHOICES),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
max_utilization = forms.IntegerField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
comments = forms.CharField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = [
|
||||||
|
'rackgroup', 'comments',
|
||||||
|
]
|
||||||
|
74
netbox/dcim/migrations/0072_powerfeeds.py
Normal file
74
netbox/dcim/migrations/0072_powerfeeds.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Generated by Django 2.1.7 on 2019-03-12 02:29
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0020_add_color_comments_changelog_to_tag'),
|
||||||
|
('dcim', '0071_device_components_add_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PowerFeed',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('type', models.PositiveSmallIntegerField(default=1)),
|
||||||
|
('status', models.PositiveSmallIntegerField(default=1)),
|
||||||
|
('supply', models.PositiveSmallIntegerField(default=1)),
|
||||||
|
('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])),
|
||||||
|
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
|
||||||
|
('phase', models.PositiveSmallIntegerField(default=1)),
|
||||||
|
('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
|
||||||
|
('comments', models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['powerpanel', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PowerPanel',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('rackgroup', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.RackGroup')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.Site')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['site', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='powerpanel',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.PowerPanel'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='rack',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Rack'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='tags',
|
||||||
|
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='powerpanel',
|
||||||
|
unique_together={('site', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='powerfeed',
|
||||||
|
unique_together={('powerpanel', 'name')},
|
||||||
|
),
|
||||||
|
]
|
@ -2668,3 +2668,134 @@ class Cable(ChangeLoggedModel):
|
|||||||
b_endpoint = b_path[-1][2]
|
b_endpoint = b_path[-1][2]
|
||||||
|
|
||||||
return a_endpoint, b_endpoint, path_status
|
return a_endpoint, b_endpoint, path_status
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerPanel(ChangeLoggedModel):
|
||||||
|
"""
|
||||||
|
A distribution point for electrical power; e.g. a data center RPP.
|
||||||
|
"""
|
||||||
|
site = models.ForeignKey(
|
||||||
|
to='Site',
|
||||||
|
on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
rackgroup = models.ForeignKey(
|
||||||
|
to='RackGroup',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_headers = ['site', 'rackgroup', 'name']
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['site', 'name']
|
||||||
|
unique_together = ['site', 'name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dcim:powerpanel', args=[self.pk])
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.site.name,
|
||||||
|
self.rackgroup.name if self.rackgroup else None,
|
||||||
|
self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeed(ChangeLoggedModel, CustomFieldModel):
|
||||||
|
"""
|
||||||
|
An electrical circuit delivered from a PowerPanel.
|
||||||
|
"""
|
||||||
|
powerpanel = models.ForeignKey(
|
||||||
|
to='PowerPanel',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='powerfeeds'
|
||||||
|
)
|
||||||
|
rack = models.ForeignKey(
|
||||||
|
to='Rack',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50
|
||||||
|
)
|
||||||
|
type = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_TYPE_CHOICES,
|
||||||
|
default=POWERFEED_TYPE_PRIMARY
|
||||||
|
)
|
||||||
|
status = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_STATUS_CHOICES,
|
||||||
|
default=POWERFEED_STATUS_ACTIVE
|
||||||
|
)
|
||||||
|
supply = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_SUPPLY_CHOICES,
|
||||||
|
default=POWERFEED_SUPPLY_AC
|
||||||
|
)
|
||||||
|
voltage = models.PositiveSmallIntegerField(
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
default=120
|
||||||
|
)
|
||||||
|
amperage = models.PositiveSmallIntegerField(
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
default=20
|
||||||
|
)
|
||||||
|
phase = models.PositiveSmallIntegerField(
|
||||||
|
choices=POWERFEED_PHASE_CHOICES,
|
||||||
|
default=POWERFEED_PHASE_SINGLE
|
||||||
|
)
|
||||||
|
max_utilization = models.PositiveSmallIntegerField(
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
||||||
|
default=80,
|
||||||
|
help_text="Maximum permissible draw (percentage)"
|
||||||
|
)
|
||||||
|
comments = models.TextField(
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
custom_field_values = GenericRelation(
|
||||||
|
to='extras.CustomFieldValue',
|
||||||
|
content_type_field='obj_type',
|
||||||
|
object_id_field='obj_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = TaggableManager(through=TaggedItem)
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'powerpanel', 'rack', 'name', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization',
|
||||||
|
'comments',
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['powerpanel', 'name']
|
||||||
|
unique_together = ['powerpanel', 'name']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('dcim:powerfeed', args=[self.pk])
|
||||||
|
|
||||||
|
def to_csv(self):
|
||||||
|
return (
|
||||||
|
self.powerpanel.name,
|
||||||
|
self.rack.name if self.rack else None,
|
||||||
|
self.name,
|
||||||
|
self.get_type_display(),
|
||||||
|
self.get_status_display(),
|
||||||
|
self.get_supply_display(),
|
||||||
|
self.voltage,
|
||||||
|
self.amperage,
|
||||||
|
self.get_phase_display(),
|
||||||
|
self.max_utilization,
|
||||||
|
self.comments,
|
||||||
|
)
|
||||||
|
@ -6,8 +6,9 @@ from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
|||||||
from .models import (
|
from .models import (
|
||||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||||
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
|
|
||||||
REGION_LINK = """
|
REGION_LINK = """
|
||||||
@ -786,3 +787,48 @@ class VirtualChassisTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ('pk', 'master', 'domain', 'member_count', 'actions')
|
fields = ('pk', 'master', 'domain', 'member_count', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power panels
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerPanelTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.LinkColumn()
|
||||||
|
powerfeed_count = tables.Column(
|
||||||
|
verbose_name='Feeds'
|
||||||
|
)
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_code=RACKROLE_ACTIONS,
|
||||||
|
attrs={
|
||||||
|
'td': {'class': 'text-right noprint'}
|
||||||
|
},
|
||||||
|
verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = PowerPanel
|
||||||
|
fields = ('pk', 'name', 'site', 'rackgroup', 'powerfeed_count', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power feeds
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerFeedTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
name = tables.LinkColumn()
|
||||||
|
powerpanel = tables.LinkColumn(
|
||||||
|
viewname='dcim:powerpanel',
|
||||||
|
args=[Accessor('powerpanel.pk')],
|
||||||
|
|
||||||
|
)
|
||||||
|
rack = tables.LinkColumn(
|
||||||
|
viewname='dcim:rack',
|
||||||
|
accessor=Accessor('rack.pk')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = PowerFeed
|
||||||
|
fields = ('pk', 'name', 'powerpanel', 'rack', 'type', 'status', 'supply', 'voltage', 'amperage', 'phase')
|
||||||
|
@ -6,7 +6,8 @@ from secrets.views import secret_add
|
|||||||
from . import views
|
from . import views
|
||||||
from .models import (
|
from .models import (
|
||||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
|
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
|
||||||
PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
|
PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site,
|
||||||
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
|
|
||||||
app_name = 'dcim'
|
app_name = 'dcim'
|
||||||
@ -279,4 +280,23 @@ urlpatterns = [
|
|||||||
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||||
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||||
|
|
||||||
|
# Power panels
|
||||||
|
url(r'^power-panels/$', views.PowerPanelListView.as_view(), name='powerpanel_list'),
|
||||||
|
url(r'^power-panels/add/$', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
|
||||||
|
url(r'^power-panels/import/$', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
|
||||||
|
url(r'^power-panels/delete/$', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
|
||||||
|
url(r'^power-panels/(?P<pk>\d+)/edit/$', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
|
||||||
|
url(r'^power-panels/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
|
||||||
|
|
||||||
|
# Racks
|
||||||
|
url(r'^power-feeds/$', views.PowerFeedListView.as_view(), name='powerfeed_list'),
|
||||||
|
url(r'^power-feeds/add/$', views.PowerFeedEditView.as_view(), name='powerfeed_add'),
|
||||||
|
url(r'^power-feeds/import/$', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
|
||||||
|
url(r'^power-feeds/edit/$', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
|
||||||
|
url(r'^power-feeds/delete/$', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
|
||||||
|
url(r'^power-feeds/(?P<pk>\d+)/$', views.PowerFeedView.as_view(), name='powerfeed'),
|
||||||
|
url(r'^power-feeds/(?P<pk>\d+)/edit/$', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
|
||||||
|
url(r'^power-feeds/(?P<pk>\d+)/delete/$', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
|
||||||
|
url(r'^power-feeds/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -30,8 +30,9 @@ from . import filters, forms, tables
|
|||||||
from .models import (
|
from .models import (
|
||||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||||
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||||
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||||
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -2114,3 +2115,113 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
|
|||||||
'form': form,
|
'form': form,
|
||||||
'return_url': self.get_return_url(request, device),
|
'return_url': self.get_return_url(request, device),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power panels
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerPanelListView(ObjectListView):
|
||||||
|
queryset = PowerPanel.objects.select_related(
|
||||||
|
'site', 'rackgroup'
|
||||||
|
).annotate(
|
||||||
|
rack_count=Count('powerfeeds')
|
||||||
|
)
|
||||||
|
table = tables.PowerPanelTable
|
||||||
|
template_name = 'dcim/powerpanel_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPanelCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.add_powerpanel'
|
||||||
|
model = PowerPanel
|
||||||
|
model_form = forms.PowerPanelForm
|
||||||
|
default_return_url = 'dcim:powerpanel_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPanelEditView(PowerPanelCreateView):
|
||||||
|
permission_required = 'dcim.change_powerpanel'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
|
permission_required = 'dcim.add_powerpanel'
|
||||||
|
model_form = forms.PowerPanelCSVForm
|
||||||
|
table = tables.PowerPanelTable
|
||||||
|
default_return_url = 'dcim:powerpanel_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_powerpanel'
|
||||||
|
queryset = PowerPanel.objects.select_related(
|
||||||
|
'site', 'rack_group'
|
||||||
|
).annotate(
|
||||||
|
rack_count=Count('powerfeeds')
|
||||||
|
)
|
||||||
|
table = tables.PowerPanelTable
|
||||||
|
default_return_url = 'dcim:powerpanel_list'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Power feeds
|
||||||
|
#
|
||||||
|
|
||||||
|
class PowerFeedListView(ObjectListView):
|
||||||
|
queryset = PowerFeed.objects.select_related(
|
||||||
|
'powerpanel', 'rack'
|
||||||
|
)
|
||||||
|
# filter = filters.PowerFeedFilter
|
||||||
|
# filter_form = forms.PowerFeedFilterForm
|
||||||
|
table = tables.PowerFeedTable
|
||||||
|
template_name = 'dcim/powerfeed_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedView(View):
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
powerfeed = get_object_or_404(PowerFeed.objects.select_related('panel', 'rack'), pk=pk)
|
||||||
|
|
||||||
|
return render(request, 'dcim/powerfeed.html', {
|
||||||
|
'powerfeed': powerfeed,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.add_powerfeed'
|
||||||
|
model = PowerFeed
|
||||||
|
model_form = forms.PowerFeedForm
|
||||||
|
template_name = 'dcim/powerfeed_edit.html'
|
||||||
|
default_return_url = 'dcim:powerfeed_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedEditView(PowerFeedCreateView):
|
||||||
|
permission_required = 'dcim.change_powerfeed'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
|
permission_required = 'dcim.delete_powerfeed'
|
||||||
|
model = PowerFeed
|
||||||
|
default_return_url = 'dcim:powerfeed_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
|
permission_required = 'dcim.add_powerfeed'
|
||||||
|
model_form = forms.PowerFeedCSVForm
|
||||||
|
table = tables.PowerFeedTable
|
||||||
|
default_return_url = 'dcim:powerfeed_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
|
permission_required = 'dcim.change_powerfeed'
|
||||||
|
queryset = PowerFeed.objects.select_related('powerpanel', 'rack')
|
||||||
|
# filter = filters.PowerFeedFilter
|
||||||
|
table = tables.PowerFeedTable
|
||||||
|
form = forms.PowerFeedBulkEditForm
|
||||||
|
default_return_url = 'dcim:powerfeed_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_powerfeed'
|
||||||
|
queryset = PowerFeed.objects.select_related('powerpanel', 'rack')
|
||||||
|
# filter = filters.PowerFeedFilter
|
||||||
|
table = tables.PowerFeedTable
|
||||||
|
default_return_url = 'dcim:powerfeed_list'
|
||||||
|
111
netbox/templates/dcim/powerfeed.html
Normal file
111
netbox/templates/dcim/powerfeed.html
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load tz %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<div class="row noprint">
|
||||||
|
<div class="col-sm-8 col-md-9">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a></li>
|
||||||
|
<li><a href="{{ powerfeed.site.get_absolute_url }}">{{ powerfeed.site }}</a></li>
|
||||||
|
{% if powerfeed.rackgroup %}
|
||||||
|
<li><a href="{{ powerfeed.rackgroup.get_absolute_url }}">{{ powerfeed.rackgroup }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li>{{ site }}</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 col-md-3">
|
||||||
|
<form action="{% url 'dcim:powerfeed_list' %}" method="get">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="Search power feeds" />
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="fa fa-search" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% if perms.dcim.change_powerfeed %}
|
||||||
|
<a href="{% url 'dcim:powerfeed_edit' pk=powerfeed.pk %}" class="btn btn-warning">
|
||||||
|
<span class="fa fa-pencil" aria-hidden="true"></span>
|
||||||
|
Edit this power feed
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if perms.dcim.delete_powerfeed %}
|
||||||
|
<a href="{% url 'dcim:powerfeed_delete' pk=powerfeed.pk %}" class="btn btn-danger">
|
||||||
|
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||||
|
Delete this power feed
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}{{ powerfeed }}{% endblock %}</h1>
|
||||||
|
{% include 'inc/created_updated.html' with obj=powerfeed %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Power Feed</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body attr-table">
|
||||||
|
<tr>
|
||||||
|
<td>Power Panel</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ powerfeed.powerpanel.get_absolute_url }}">{{ powerfeed.powerpanel }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Rack</td>
|
||||||
|
<td>
|
||||||
|
{% if powerfeed.rack %}
|
||||||
|
<a href="{{ powerfeed.rack.get_absolute_url }}">{{ powerfeed.rack }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>
|
||||||
|
<span class="label label-{{ powerfeed.get_type_class }}">{{ powerfeed.get_type_display }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td>
|
||||||
|
<span class="label label-{{ powerfeed.get_status_class }}">{{ powerfeed.get_status_display }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Supply</td>
|
||||||
|
<td>{{ powerfeed.get_supply_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Voltage</td>
|
||||||
|
<td>{{ powerfeed.voltage }}V</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Amperage</td>
|
||||||
|
<td>{{ powerfeed.amperage }}A</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Phase</td>
|
||||||
|
<td>{{ powerfeed.get_phase_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Max Utilization</td>
|
||||||
|
<td>{{ powerfeed.max_utilization }}%</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
45
netbox/templates/dcim/powerfeed_edit.html
Normal file
45
netbox/templates/dcim/powerfeed_edit.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends 'utilities/obj_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Power Feed</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.powerpanel %}
|
||||||
|
{% render_field form.rack %}
|
||||||
|
{% render_field form.name %}
|
||||||
|
{% render_field form.type %}
|
||||||
|
{% render_field form.status %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Characteristics</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.supply %}
|
||||||
|
{% render_field form.voltage %}
|
||||||
|
{% render_field form.amperage %}
|
||||||
|
{% render_field form.phase %}
|
||||||
|
{% render_field form.max_utilization %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tags</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Comments</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.comments %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
22
netbox/templates/dcim/powerfeed_list.html
Normal file
22
netbox/templates/dcim/powerfeed_list.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% if perms.dcim.add_powerfeed %}
|
||||||
|
{% add_button 'dcim:powerfeed_add' %}
|
||||||
|
{% import_button 'dcim:powerfeed_import' %}
|
||||||
|
{% endif %}
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Power Feeds{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerfeed_bulk_edit' bulk_delete_url='dcim:powerfeed_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 noprint">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
{% include 'inc/tags_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
18
netbox/templates/dcim/powerpanel_list.html
Normal file
18
netbox/templates/dcim/powerpanel_list.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="pull-right noprint">
|
||||||
|
{% if perms.dcim.add_powerpanel %}
|
||||||
|
{% add_button 'dcim:powerpanel_add' %}
|
||||||
|
{% import_button 'dcim:powerpanel_import' %}
|
||||||
|
{% endif %}
|
||||||
|
{% export_button content_type %}
|
||||||
|
</div>
|
||||||
|
<h1>{% block title %}Power Panels{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:powerpanel_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -368,6 +368,29 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="dropdown{% if request.path|contains:'/dcim/power' %} active{% endif %}">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Power <span class="caret"></span></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
{% if perms.dcim.add_powerfeed %}
|
||||||
|
<div class="buttons pull-right">
|
||||||
|
<a href="{% url 'dcim:powerfeed_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||||
|
<a href="{% url 'dcim:powerfeed_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% if perms.dcim.add_powerpanel %}
|
||||||
|
<div class="buttons pull-right">
|
||||||
|
<a href="{% url 'dcim:powerpanel_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||||
|
<a href="{% url 'dcim:powerpanel_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||||
|
Loading…
Reference in New Issue
Block a user