custom fields on devices and ports in the web from config (#945)

* custom device field web display and edit

* make display work; relies on T::T calling dict slot or method with same syntax

* add storing port custom fields

* use resultset method instead, use cf_ prefix

* update Pg min ver for jsonb

* allow override of position and default for port custom fields

* support hidden for custom fields

* update description of Objects API class

* allow left and mid position for custom fields

* add custom fields in csv

* change port control sidebar label

* fix default missing bug on backend jobs
This commit is contained in:
Oliver Gorwits
2022-12-09 10:20:26 +00:00
committed by GitHub
parent d03eab02db
commit 1c7c749f0e
15 changed files with 225 additions and 44 deletions

View File

@@ -103,7 +103,7 @@ will take about 250MB including MIB files.
root:~# useradd -m -p x -s /bin/bash netdisco root:~# useradd -m -p x -s /bin/bash netdisco
Netdisco uses the PostgreSQL database server. Install PostgreSQL (at least Netdisco uses the PostgreSQL database server. Install PostgreSQL (at least
version 9.4) and then change to the PostgreSQL superuser (usually version 9.6) and then change to the PostgreSQL superuser (usually
C<postgres>). Create a new database and PostgreSQL user for the Netdisco C<postgres>). Create a new database and PostgreSQL user for the Netdisco
application: application:

View File

@@ -11,7 +11,7 @@ __PACKAGE__->load_namespaces(
); );
our # try to hide from kwalitee our # try to hide from kwalitee
$VERSION = 76; # schema version used for upgrades, keep as integer $VERSION = 77; # schema version used for upgrades, keep as integer
use Path::Class; use Path::Class;
use File::ShareDir 'dist_dir'; use File::ShareDir 'dist_dir';

View File

@@ -85,6 +85,8 @@ __PACKAGE__->add_columns(
{ data_type => "boolean", is_nullable => 0, default_value => \"false" }, { data_type => "boolean", is_nullable => 0, default_value => \"false" },
"pae_is_enabled", "pae_is_enabled",
{ data_type => "boolean", is_nullable => 1 }, { data_type => "boolean", is_nullable => 1 },
"custom_fields",
{ data_type => "jsonb", is_nullable => 0, default_value => \"{}" },
); );
__PACKAGE__->set_primary_key("ip"); __PACKAGE__->set_primary_key("ip");

View File

@@ -71,6 +71,8 @@ __PACKAGE__->add_columns(
{ data_type => "integer", is_nullable => 1 }, { data_type => "integer", is_nullable => 1 },
"lastchange", "lastchange",
{ data_type => "bigint", is_nullable => 1 }, { data_type => "bigint", is_nullable => 1 },
"custom_fields",
{ data_type => "jsonb", is_nullable => 0, default_value => \"{}" },
); );
__PACKAGE__->set_primary_key("port", "ip"); __PACKAGE__->set_primary_key("port", "ip");

View File

@@ -332,18 +332,44 @@ sub jq_insert {
my $happy = false; my $happy = false;
try { try {
schema(vars->{'tenant'})->txn_do(sub { schema(vars->{'tenant'})->txn_do(sub {
schema(vars->{'tenant'})->resultset('Admin')->populate([ if (scalar @$jobs == 1 and defined $jobs->[0]->{device} and
map {{ scalar grep {$_ eq $jobs->[0]->{action}} @{ setting('_inline_actions') || [] }) {
device => $_->{device},
device_key => $_->{device_key}, my $spec = $jobs->[0];
port => $_->{port}, my $row = undef;
action => $_->{action},
subaction => ($_->{extra} || $_->{subaction}), if ($spec->{port}) {
username => $_->{username}, $row = schema(vars->{'tenant'})->resultset('DevicePort')
userip => $_->{userip}, ->find($spec->{port}, $spec->{device});
status => 'queued', }
}} @$jobs else {
]); $row = schema(vars->{'tenant'})->resultset('Device')
->find($spec->{device});
}
die 'failed to find row for custom field update' unless $row;
$spec->{action} =~ s/^cf_//;
$spec->{subaction} = to_json( $spec->{subaction} );
$row->make_column_dirty('custom_fields');
$row->update({
custom_fields => \['jsonb_set(custom_fields, ?, ?)'
=> (qq{{$spec->{action}}}, $spec->{subaction}) ]
})->discard_changes();
}
else {
schema(vars->{'tenant'})->resultset('Admin')->populate([
map {{
device => $_->{device},
device_key => $_->{device_key},
port => $_->{port},
action => $_->{action},
subaction => ($_->{extra} || $_->{subaction}),
username => $_->{username},
userip => $_->{userip},
status => 'queued',
}} @$jobs
]);
}
}); });
$happy = true; $happy = true;
} }

View File

@@ -111,6 +111,7 @@ use App::Netdisco::Web::TypeAhead;
use App::Netdisco::Web::PortControl; use App::Netdisco::Web::PortControl;
use App::Netdisco::Web::Statistics; use App::Netdisco::Web::Statistics;
use App::Netdisco::Web::Password; use App::Netdisco::Web::Password;
use App::Netdisco::Web::CustomFields;
use App::Netdisco::Web::GenericReport; use App::Netdisco::Web::GenericReport;
sub _load_web_plugins { sub _load_web_plugins {
@@ -391,7 +392,7 @@ $swagger_doc->{tags} = [
{name => 'Search', {name => 'Search',
description => 'Search Operations'}, description => 'Search Operations'},
{name => 'Objects', {name => 'Objects',
description => 'Retrieve Device, Port, and associated Node Data'}, description => 'Device, Port, and associated Node Data'},
{name => 'Reports', {name => 'Reports',
description => 'Canned and Custom Reports'}, description => 'Canned and Custom Reports'},
]; ];

View File

@@ -0,0 +1,86 @@
package App::Netdisco::Web::CustomFields;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use App::Netdisco::DB::ResultSet::Device;
use App::Netdisco::DB::ResultSet::DevicePort;
use App::Netdisco::Web::Plugin;
my @inline_device_actions = ();
my @inline_device_port_actions = ();
foreach my $config (@{ setting('custom_fields')->{'device'} || [] }) {
if (! $config->{'name'}) {
error 'custom_field missing name';
next;
}
register_device_details({
%{ $config },
field => ('cf_' . $config->{'name'}),
label => ($config->{'label'} || ucfirst $config->{'name'}),
}) unless $config->{'hidden'};
push @inline_device_actions, $config->{'name'};
}
foreach my $config (@{ setting('custom_fields')->{'device_port'} || [] }) {
if (! $config->{'name'}) {
error 'custom_field missing name';
next;
}
register_device_port_column({
position => 'right', # or "mid" or "right"
default => undef, # or undef
%{ $config },
field => ('cf_' . $config->{'name'}),
label => ($config->{'label'} || ucfirst $config->{'name'}),
}) unless $config->{'hidden'};
push @inline_device_port_actions, $config->{'name'};
}
{
package App::Netdisco::DB::ResultSet::Device;
sub with_custom_fields {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{ '+columns' => {
map {( ('cf_'. $_) => \[ 'me.custom_fields ->> ?' => $_ ] )}
@inline_device_actions
}});
}
}
{
package App::Netdisco::DB::ResultSet::DevicePort;
sub with_custom_fields {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{ '+columns' => {
map {( ('cf_'. $_) => \[ 'me.custom_fields ->> ?' => $_ ] )}
@inline_device_port_actions
}});
}
}
set('_inline_actions' => [
map {'cf_' . $_} (@inline_device_actions, @inline_device_port_actions)
]);
true;

View File

@@ -23,8 +23,7 @@ ajax '/ajax/content/device/details' => require_login sub {
'+as' => ['has_snapshot'], '+as' => ['has_snapshot'],
join => 'snapshot', join => 'snapshot',
}, },
)->with_times() )->with_times->with_custom_fields->hri->all;
->hri->all;
my @power my @power
= schema(vars->{'tenant'})->resultset('DevicePower') = schema(vars->{'tenant'})->resultset('DevicePower')

View File

@@ -20,7 +20,7 @@ get '/ajax/content/device/ports' => require_login sub {
my $device = schema(vars->{'tenant'})->resultset('Device') my $device = schema(vars->{'tenant'})->resultset('Device')
->search_for_device($q) or send_error('Bad device', 400); ->search_for_device($q) or send_error('Bad device', 400);
my $set = $device->ports->with_properties; my $set = $device->ports->with_properties->with_custom_fields;
# refine by ports if requested # refine by ports if requested
my $f = param('f'); my $f = param('f');

View File

@@ -24,7 +24,7 @@ ajax '/ajax/portcontrol' => require_any_role [qw(admin port_control)] => sub {
'c_power' => 'power', 'c_power' => 'power',
); );
my $action = $action_map{ param('field') }; my $action = ($action_map{ param('field') } || param('field') || '');
my $subaction = ($action =~ m/^(?:power|portcontrol)/ my $subaction = ($action =~ m/^(?:power|portcontrol)/
? (param('action') ."-other") ? (param('action') ."-other")
: param('value')); : param('value'));
@@ -34,6 +34,7 @@ ajax '/ajax/portcontrol' => require_any_role [qw(admin port_control)] => sub {
my $act = "$action $subaction"; my $act = "$action $subaction";
$act =~ s/-other$//; $act =~ s/-other$//;
$act =~ s/^portcontrol/port/; $act =~ s/^portcontrol/port/;
$act =~ s/^device_port_custom_field_/custom_field: /;
schema(vars->{'tenant'})->resultset('DevicePortLog')->create({ schema(vars->{'tenant'})->resultset('DevicePortLog')->create({
ip => param('device'), ip => param('device'),

View File

@@ -112,7 +112,7 @@ sidebar_defaults:
search_device: search_device:
matchall: { default: checked } matchall: { default: checked }
device_ports: device_ports:
c_admin: { label: 'Port Controls', default: null, idx: 0 } c_admin: { label: 'Port Control and Editing', default: null, idx: 0 }
c_port: { label: 'Port', default: checked, idx: 1 } c_port: { label: 'Port', default: checked, idx: 1 }
c_descr: { label: 'Description', default: null, idx: 2 } c_descr: { label: 'Description', default: null, idx: 2 }
c_comment: { label: 'Last Comment', default: null, idx: 3 } c_comment: { label: 'Last Comment', default: null, idx: 3 }
@@ -275,6 +275,9 @@ community_rw: []
device_auth: [] device_auth: []
use_legacy_rancidexport: false use_legacy_rancidexport: false
use_legacy_sshcollector: false use_legacy_sshcollector: false
custom_fields:
device: []
device_port: []
get_credentials: "" get_credentials: ""
bulkwalk_off: false bulkwalk_off: false
bulkwalk_no: [] bulkwalk_no: []

View File

@@ -0,0 +1,11 @@
BEGIN;
ALTER TABLE device ADD COLUMN "custom_fields" jsonb DEFAULT '{}';
UPDATE device SET custom_fields = '{}';
ALTER TABLE device_port ADD COLUMN "custom_fields" jsonb DEFAULT '{}';
UPDATE device_port SET custom_fields = '{}';
COMMIT;

View File

@@ -79,14 +79,25 @@
<tr> <tr>
<td> <td>
[% config.label | html_entity %] [% config.label | html_entity %]
</td> [% IF config.editable AND user_can_port_control %]
<td> <i class="icon-edit nd_edit-icon nd_device-details-edit"></i>
[% TRY %]
[% INCLUDE "plugin/${config.name}/device_details.tt" %]
[% CATCH %]
<!-- dummy content required by Template Toolkit TRY -->
[% END %] [% END %]
</td> </td>
[% TRY %]
<td>
[% INCLUDE "plugin/${config.name}/device_details.tt" %]
</td>
[% CATCH %]
[% CLEAR %]
[% IF config.editable AND user_can_port_control %]
<td class="nd_editable-cell" contenteditable="true"
data-field="[% config.field | html_entity %]" data-for-device="[% d.ip | html_entity %]">
[% ELSE %]
<td>
[% END %]
[% d.${config.field} | html_entity %]
</td>
[% END %]
</tr> </tr>
[% END %] [% END %]
[% END %] [% END %]

View File

@@ -60,13 +60,26 @@
[% FOREACH config IN settings._extra_device_port_cols %] [% FOREACH config IN settings._extra_device_port_cols %]
[% NEXT UNLESS config.position == 'left' AND params.${config.name} %] [% NEXT UNLESS config.position == 'left' AND params.${config.name} %]
<td>
[% TRY %] [% TRY %]
<td>
[% INCLUDE "plugin/${config.name}/device_port_column.tt" %] [% INCLUDE "plugin/${config.name}/device_port_column.tt" %]
[% CATCH %] </td>
<!-- dummy content required by Template Toolkit TRY --> [% CATCH %]
[% CLEAR %]
[% IF config.editable AND user_can_port_control AND params.c_admin AND row.portctl %]
<td nowrap class="nd_editable-cell" contenteditable="true"
data-field="[% config.field | html_entity %]" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]">
<i class="icon-edit nd_edit-icon"></i>
[% ELSE %]
<td nowrap class="nd_editable-cell">
[% END %] [% END %]
</td> <div class="nd_editable-cell-content">
[% row.get_column(config.field) | html_entity %]
</div>
</td>
[% END %]
[% END %] [% END %]
[% IF params.c_port %] [% IF params.c_port %]
@@ -124,13 +137,26 @@
[% FOREACH config IN settings._extra_device_port_cols %] [% FOREACH config IN settings._extra_device_port_cols %]
[% NEXT UNLESS config.position == 'mid' AND params.${config.name} %] [% NEXT UNLESS config.position == 'mid' AND params.${config.name} %]
<td>
[% TRY %] [% TRY %]
<td>
[% INCLUDE "plugin/${config.name}/device_port_column.tt" %] [% INCLUDE "plugin/${config.name}/device_port_column.tt" %]
[% CATCH %] </td>
<!-- dummy content required by Template Toolkit TRY --> [% CATCH %]
[% CLEAR %]
[% IF config.editable AND user_can_port_control AND params.c_admin AND row.portctl %]
<td nowrap class="nd_editable-cell" contenteditable="true"
data-field="[% config.field | html_entity %]" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]">
<i class="icon-edit nd_edit-icon"></i>
[% ELSE %]
<td nowrap class="nd_editable-cell">
[% END %] [% END %]
</td> <div class="nd_editable-cell-content">
[% row.get_column(config.field) | html_entity %]
</div>
</td>
[% END %]
[% END %] [% END %]
[% IF params.c_descr %] [% IF params.c_descr %]
@@ -408,13 +434,26 @@
[% FOREACH config IN settings._extra_device_port_cols %] [% FOREACH config IN settings._extra_device_port_cols %]
[% NEXT UNLESS config.position == 'right' AND params.${config.name} %] [% NEXT UNLESS config.position == 'right' AND params.${config.name} %]
<td>
[% TRY %] [% TRY %]
<td>
[% INCLUDE "plugin/${config.name}/device_port_column.tt" %] [% INCLUDE "plugin/${config.name}/device_port_column.tt" %]
[% CATCH %] </td>
<!-- dummy content required by Template Toolkit TRY --> [% CATCH %]
[% CLEAR %]
[% IF config.editable AND user_can_port_control AND params.c_admin AND row.portctl %]
<td nowrap class="nd_editable-cell" contenteditable="true"
data-field="[% config.field | html_entity %]" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]">
<i class="icon-edit nd_edit-icon"></i>
[% ELSE %]
<td nowrap class="nd_editable-cell">
[% END %] [% END %]
</td> <div class="nd_editable-cell-content">
[% row.get_column(config.field) | html_entity %]
</div>
</td>
[% END %]
[% END %] [% END %]
</tr> </tr>
[% END %] [% END %]

View File

@@ -40,7 +40,7 @@
[% TRY %] [% TRY %]
[% PROCESS "plugin/${config.name}/device_port_column_csv.tt" %] [% PROCESS "plugin/${config.name}/device_port_column_csv.tt" %]
[% CATCH %] [% CATCH %]
[% myport.push('') %] [% myport.push( row.get_column(config.field) ) %]
[% END %] [% END %]
[% END %] [% END %]
@@ -53,7 +53,7 @@
[% TRY %] [% TRY %]
[% PROCESS "plugin/${config.name}/device_port_column_csv.tt" %] [% PROCESS "plugin/${config.name}/device_port_column_csv.tt" %]
[% CATCH %] [% CATCH %]
[% myport.push('') %] [% myport.push( row.get_column(config.field) ) %]
[% END %] [% END %]
[% END %] [% END %]
@@ -178,7 +178,7 @@
[% TRY %] [% TRY %]
[% PROCESS "plugin/${config.name}/device_port_column_csv.tt" %] [% PROCESS "plugin/${config.name}/device_port_column_csv.tt" %]
[% CATCH %] [% CATCH %]
[% myport.push('') %] [% myport.push( row.get_column(config.field) ) %]
[% END %] [% END %]
[% END %] [% END %]