diff --git a/conf/pfsetacls/switch_acls.yml b/conf/pfsetacls/switch_acls.yml index f9e50a071c87..bf9a6990c5f1 100644 --- a/conf/pfsetacls/switch_acls.yml +++ b/conf/pfsetacls/switch_acls.yml @@ -13,8 +13,9 @@ - name: Load new acl into Cisco Switch cisco.ios.ios_acls: - config: "{{ acls.parsed }}" - state: replaced + config: "{{ acls.parsed }}"[% IF delete >= 1 %] + state: deleted[% ELSE %] + state: replaced[% END %] when: ansible_network_os == 'cisco.ios.ios' - name: Load new acl into Cisco WLC @@ -31,3 +32,34 @@ arubanetworks.aoscx.aoscx_config: src: "{{ acl_config }}" when: ansible_network_os == 'arubanetworks.aoscx.aoscx' + +[% FOREACH role IN interfaces_delete.keys %] +[% FOREACH interface IN interfaces_delete.$role %] + - name: remove acl on interface + cisco.ios.ios_config: + lines: + - no ip access-group [% role %] in + parents: "{{ item }}" + with_items: + - interface [% interface %] +[% END %] +[% END %] + +[% IF delete == 0 %] +[% FOREACH role IN interfaces.keys %] +[% FOREACH interface IN interfaces.$role %] + - name: Merge module attributes of given access-groups + cisco.ios.ios_acl_interfaces: + config: + - name: [% interface %] + access_groups: + - afi: ipv4 + acls: + - name: [% role %] + direction: in[% IF delete >= 1 %] + state: deleted[% ELSE %] + state: merged[% END %] + when: ansible_network_os == 'cisco.ios.ios' +[% END %] +[% END %] +[% END %] diff --git a/html/pfappserver/lib/pfappserver/Form/Config/Switch.pm b/html/pfappserver/lib/pfappserver/Form/Config/Switch.pm index 4f79241ecd20..70740ee9d9a4 100644 --- a/html/pfappserver/lib/pfappserver/Form/Config/Switch.pm +++ b/html/pfappserver/lib/pfappserver/Form/Config/Switch.pm @@ -145,6 +145,12 @@ has_field 'UrlMap' => label => 'Role by Web Auth URL', default => undef, ); +has_field 'InterfaceMap' => + ( + type => 'Toggle', + label => 'Interface to apply Role ACL', + default => undef, + ); has_field 'cliAccess' => ( type => 'Toggle', @@ -512,6 +518,7 @@ addRoleMapping("AccessListMapping", "accesslist"); addRoleMapping("VpnMapping", "vpn"); addRoleMapping("NetworkMapping", "network"); addRoleMapping("NetworkFromMapping", "networkfrom"); +addRoleMapping("InterfaceMapping", "interface"); sub _validate_acl_switch { my ($field) = @_; diff --git a/html/pfappserver/root/src/views/Configuration/switchGroups/_components/TheForm.vue b/html/pfappserver/root/src/views/Configuration/switchGroups/_components/TheForm.vue index 0a9e8a1af084..34b9f3172e98 100644 --- a/html/pfappserver/root/src/views/Configuration/switchGroups/_components/TheForm.vue +++ b/html/pfappserver/root/src/views/Configuration/switchGroups/_components/TheForm.vue @@ -177,6 +177,17 @@ /> + +

+
+
+ +
@@ -559,6 +570,7 @@ import { FormGroupRoleMapVpn, FormGroupRoleMapUrl, FormGroupRoleMapVlan, + FormGroupRoleMapInterface, FormGroupSnmpAuthProtocolTrap, FormGroupSnmpAuthPasswordTrap, FormGroupSnmpCommunityRead, @@ -587,6 +599,7 @@ import { FormGroupToggleUrlMap, FormGroupToggleVlanMap, FormGroupToggleNetworkMap, + FormGroupToggleInterfaceMap, FormGroupType, FormGroupUplink, FormGroupUplinkDynamic, @@ -640,6 +653,7 @@ const components = { FormGroupRoleMapVpn, FormGroupRoleMapUrl, FormGroupRoleMapVlan, + FormGroupRoleMapInterface, FormGroupSnmpAuthProtocolTrap, FormGroupSnmpAuthPasswordTrap, FormGroupSnmpCommunityRead, @@ -668,6 +682,7 @@ const components = { FormGroupToggleUrlMap, FormGroupToggleVlanMap, FormGroupToggleNetworkMap, + FormGroupToggleInterfaceMap, FormGroupType, FormGroupUplink, FormGroupUplinkDynamic, diff --git a/html/pfappserver/root/src/views/Configuration/switchGroups/_components/index.js b/html/pfappserver/root/src/views/Configuration/switchGroups/_components/index.js index f4d44d0de49e..7dca29e18afb 100644 --- a/html/pfappserver/root/src/views/Configuration/switchGroups/_components/index.js +++ b/html/pfappserver/root/src/views/Configuration/switchGroups/_components/index.js @@ -46,6 +46,7 @@ export { BaseFormGroupInput as FormGroupRoleMapVpn, BaseFormGroupInput as FormGroupRoleMapUrl, BaseFormGroupInput as FormGroupRoleMapVlan, + BaseFormGroupInput as FormGroupRoleMapInterface, BaseFormGroupInput as FormGroupSnmpAuthProtocolTrap, BaseFormGroupInputPassword as FormGroupSnmpAuthPasswordTrap, BaseFormGroupInput as FormGroupSnmpCommunityRead, @@ -83,6 +84,7 @@ export { BaseFormGroupToggleNYDefault as FormGroupToggleUrlMap, BaseFormGroupToggleNYDefault as FormGroupToggleVlanMap, BaseFormGroupToggleNYDefault as FormGroupToggleNetworkMap, + BaseFormGroupToggleNYDefault as FormGroupToggleInterfaceMap, BaseFormGroupToggleNYDefault as FormGroupVoipEnabled, BaseFormGroupToggleNYDefault as FormGroupVoipLldpDetect, BaseFormGroupToggleNYDefault as FormGroupVoipCdpDetect, diff --git a/html/pfappserver/root/src/views/Configuration/switches/_components/TheForm.vue b/html/pfappserver/root/src/views/Configuration/switches/_components/TheForm.vue index c61fe0a0e701..72361a7f5c0e 100644 --- a/html/pfappserver/root/src/views/Configuration/switches/_components/TheForm.vue +++ b/html/pfappserver/root/src/views/Configuration/switches/_components/TheForm.vue @@ -178,6 +178,21 @@ /> + +

+
+
+ + + +
@@ -487,6 +502,7 @@ import { FormGroupRoleMapVpn, FormGroupRoleMapUrl, FormGroupRoleMapVlan, + FormGroupRoleMapInterface, FormGroupSnmpAuthProtocolTrap, FormGroupSnmpAuthPasswordTrap, FormGroupSnmpCommunityRead, @@ -515,6 +531,7 @@ import { FormGroupToggleUrlMap, FormGroupToggleVlanMap, FormGroupToggleNetworkMap, + FormGroupToggleInterfaceMap, FormGroupType, FormGroupUplink, FormGroupUplinkDynamic, @@ -567,6 +584,7 @@ const components = { FormGroupRoleMapVpn, FormGroupRoleMapUrl, FormGroupRoleMapVlan, + FormGroupRoleMapInterface, FormGroupSnmpAuthProtocolTrap, FormGroupSnmpAuthPasswordTrap, FormGroupSnmpCommunityRead, @@ -595,6 +613,7 @@ const components = { FormGroupToggleUrlMap, FormGroupToggleVlanMap, FormGroupToggleNetworkMap, + FormGroupToggleInterfaceMap, FormGroupType, FormGroupUplink, FormGroupUplinkDynamic, diff --git a/html/pfappserver/root/src/views/Configuration/switches/_components/index.js b/html/pfappserver/root/src/views/Configuration/switches/_components/index.js index a90f456d5678..76ca2342142a 100644 --- a/html/pfappserver/root/src/views/Configuration/switches/_components/index.js +++ b/html/pfappserver/root/src/views/Configuration/switches/_components/index.js @@ -47,6 +47,7 @@ export { BaseFormGroupInput as FormGroupRoleMapVpn, BaseFormGroupInput as FormGroupRoleMapUrl, BaseFormGroupInput as FormGroupRoleMapVlan, + BaseFormGroupInput as FormGroupRoleMapInterface, BaseFormGroupInput as FormGroupSnmpAuthProtocolTrap, BaseFormGroupInputPassword as FormGroupSnmpAuthPasswordTrap, BaseFormGroupInput as FormGroupSnmpCommunityRead, @@ -78,6 +79,7 @@ export { BaseFormGroupInput as FormGroupDownloadableAclsLimit, BaseFormGroupInput as FormGroupAclsLimit, BaseFormGroupToggleNYDefault as FormGroupToggleAccessListMap, + BaseFormGroupToggleNYDefault as FormGroupToggleInterfaceMap, BaseFormGroupSwitch as FormGroupDeauthOnPrevious, BaseFormGroupToggleNYDefault as FormGroupToggleRoleMap, BaseFormGroupToggleNYDefault as FormGroupToggleVpnMap, diff --git a/html/pfappserver/root/src/views/Configuration/switches/_composables/useForm.js b/html/pfappserver/root/src/views/Configuration/switches/_composables/useForm.js index 779f05b001a7..b8787a91ef8a 100644 --- a/html/pfappserver/root/src/views/Configuration/switches/_composables/useForm.js +++ b/html/pfappserver/root/src/views/Configuration/switches/_composables/useForm.js @@ -158,6 +158,24 @@ const useForm = (props, context) => { // inspect meta placeholder for `NetworkMap` const { NetworkMap: { placeholder } = {} } = meta.value return placeholder === 'Y' + + const isInterfaceMap = computed(() => { + // inspect form value for `InterfaceMap` + const { InterfaceMap } = form.value + if (InterfaceMap !== null) + return InterfaceMap === 'Y' + + // inspect meta placeholder for `InterfaceMap` + const { InterfaceMap: { placeholder } = {} } = meta.value + return placeholder === 'Y' + }) + + const roles = ref(baseRoles) + $store.dispatch('$_roles/all').then(allRoles => { + roles.value = [ + ...roles.value, + ...allRoles.map(role => role.id) + ] }) const isUsePushACLs = computed(() => { @@ -204,6 +222,7 @@ const useForm = (props, context) => { isUrlMap, isVlanMap, isNetworkMap, + isInterfaceMap, roles, isUsePushACLs, diff --git a/lib/pf/ConfigStore/Switch.pm b/lib/pf/ConfigStore/Switch.pm index 96717c55d49b..bfa7c5baf4e7 100644 --- a/lib/pf/ConfigStore/Switch.pm +++ b/lib/pf/ConfigStore/Switch.pm @@ -42,6 +42,7 @@ our %MappingKey = ( VpnMapping => 'vpn', NetworkMapping => 'network', NetworkFromMapping => 'networkfrom', + InterfaceMapping => 'interface', ); our %MappingKey2 = ( @@ -52,6 +53,7 @@ our %MappingKey2 = ( VpnMapping => 'Vpn', NetworkMapping => 'Network', NetworkFromMapping => 'NetworkFrom', + InterfaceMapping => 'Interface', ); =head2 Methods @@ -107,7 +109,7 @@ sub _expandMapping { # We put it back as a string so it works in the admin UI my $toset = {}; while (my ($attr, $val) = each %$switch) { - if ($attr =~ /(.*)(AccessList|Vlan|Url|Role|Vpn|Network|NetworkFrom)$/) { + if ($attr =~ /(.*)(AccessList|Vlan|Url|Role|Vpn|Interface|Network|NetworkFrom)$/) { my $type = $2; my $role = $1; if ($type eq 'AccessList' && ref($val) eq 'ARRAY') { @@ -131,7 +133,7 @@ sub _expandMapping { $switch->{$attr} = $val; } - for my $k (qw(AccessListMapping VlanMapping UrlMapping ControllerRoleMapping VpnMapping NetworkMapping NetworkFromMapping)) { + for my $k (qw(AccessListMapping VlanMapping UrlMapping ControllerRoleMapping VpnMapping InterfaceMapping NetworkMapping NetworkFromMapping)) { next if !exists $switch->{$k}; $switch->{$k} = [sort { $a->{role} cmp $b->{role} } @{$switch->{$k} // []}] } @@ -160,7 +162,7 @@ sub cleanupBeforeCommit { sub _flattenRoleMappings { my ( $switch ) = @_; - for my $namespace (qw(AccessListMapping VlanMapping UrlMapping ControllerRoleMapping VpnMapping NetworkMapping NetworkFromMapping)) { + for my $namespace (qw(AccessListMapping VlanMapping UrlMapping ControllerRoleMapping VpnMapping InterfaceMapping NetworkMapping NetworkFromMapping)) { my $list = $switch->{$namespace} // []; for my $mapping (@$list) { my $role = $mapping->{role}; @@ -171,7 +173,7 @@ sub _flattenRoleMappings { sub _deleteRoleMappings { my ( $switch ) = @_; - delete @{$switch}{qw(AccessListMapping VlanMapping UrlMapping ControllerRoleMapping VpnMapping NetworkMapping NetworkFromMapping)}; + delete @{$switch}{qw(AccessListMapping VlanMapping UrlMapping ControllerRoleMapping VpnMapping InterfaceMapping NetworkMapping NetworkFromMapping)}; } =head2 _normalizeUplink diff --git a/lib/pf/Switch.pm b/lib/pf/Switch.pm index 9b3ae76d34d0..502109a52192 100644 --- a/lib/pf/Switch.pm +++ b/lib/pf/Switch.pm @@ -57,7 +57,7 @@ use pf::roles::custom $ROLES_API_LEVEL; use pf::error::switch; use pf::util; use pf::util::radius qw(perform_disconnect); -use List::MoreUtils qw(any all); +use List::MoreUtils qw(any all uniq); use List::Util qw(first); use Scalar::Util qw(looks_like_number); use pf::StatsD; @@ -182,6 +182,7 @@ sub new { '_VpnMap' => 'enabled', '_NetworkMap' => 'enabled', '_NetworkFromMap' => 'disabled', + '_InterfaceMap' => 'enabled', '_UsePushACLs' => 'disabled', '_UseDownloadableACLs' => 'disabled', '_DownloadableACLsLimit' => 0, @@ -189,6 +190,7 @@ sub new { '_ACLsType' => undef, '_networks' => undef, '_networks_from' => undef, + '_interfaces' => undef, map { "_".$_ => $argv->{$_} } keys %$argv, }, $class; return $self; @@ -896,6 +898,42 @@ sub _parentRoleForNetwork { return undef; } +=item getInterfaceByName + +Get the switch-specific interface names in role in switches.conf + +=cut + +sub getInterfaceByName { + my ($self, $roleName) = @_; + my $logger = $self->logger; + + + if (!defined($self->{'_interfaces'}) || !defined($self->{'_interfaces'}{$roleName})) { + my $parent = _parentRoleForInterface($roleName); + if (defined $parent && length($parent)) { + return $self->getInterfaceByName($parent); + } + # VPN name doesn't exist + $pf::StatsD::statsd->increment(called() . ".error" ); + $logger->warn("No parameter ${roleName}Interface found in conf/switches.conf for the switch " . $self->{_id}); + return undef; + } + + # return if found + return $self->{'_interfaces'}->{$roleName} if (defined($self->{'_interfaces'}->{$roleName})); + + # otherwise log and return undef + $logger->trace("(".$self->{_id}.") No parameter ${roleName}Interface found in conf/switches.conf"); + return; +} + +sub _parentRoleForInterface { + my ($name) = @_; + # not yet supported + return undef; +} + =item getNetworkFromByName Get the switch-specific network from names in role in switches.conf @@ -4286,7 +4324,8 @@ Generate Ansible configuration to push ACLs =cut sub generateAnsibleConfiguration { - my ($self) = @_; + my ($self,$oldSwitchConfig, $delete) = @_; + $delete //= $FALSE; my %vars; umask(0002); my $tt = Template->new( @@ -4315,6 +4354,7 @@ sub generateAnsibleConfiguration { $vars{'switches'}{$switch_id}{'cliPwd'} = $self->{'_cliPwd'}; $vars{'switches'}{$switch_id}{'type'} = $self->{'_type'}; $vars{'switches'}{$switch_id}{'id'} = $switch_ip; + $vars{'switches'}{$switch_id}{'delete'} = $delete; switch($self->{'_type'}) { case /Cisco::ASA/ { $vars{'switches'}{$switch_id}{'ansible_network_os'} = "cisco.asa" } case /Cisco::WLC/ { $vars{'switches'}{$switch_id}{'ansible_network_os'} = "aireos" } @@ -4324,29 +4364,62 @@ sub generateAnsibleConfiguration { foreach my $role (keys %ConfigRoles) { my $acls = $self->getRoleAccessListByName($role); - next if !defined($acls); - my $out_acls; - my $in_acls; - while($acls =~ /([^\n]+)\n?/g) { - my $acl_line = $1; - if ($acl_line =~ /^(out\|)(.*)/) { - $out_acls .= $2."\n"; - } elsif ($acl_line =~ /^(in\|)(.*)/) { - $in_acls .= $2."\n"; + my $interfaces = $self->getInterfaceByName($role); + if ($interfaces) { + my @interfaces = split(',',$interfaces); + if ($delete) { + $vars{'switches'}{$switch_id}{'interfaces_delete'}{$role} = \@interfaces; } else { - $in_acls .= $acl_line."\n"; + $vars{'switches'}{$switch_id}{'interfaces'}{$role} = \@interfaces; } } - my $implicit_acl = $self->implicit_acl(); - if ((!defined($in_acls) || $in_acls eq "") && $implicit_acl) { - $vars{'switches'}{$switch_id}{'acls'}{$role} = $implicit_acl; - } elsif (defined($in_acls)) { - $vars{'switches'}{$switch_id}{'acls'}{$role} = $in_acls; + } + if (!$delete) { + # Remove useless acl on old interfaces + my %diff; + foreach my $old_role_interface (@{$oldSwitchConfig->{InterfaceMapping}}) { + #Old interface list + my @oldinterfaces = uniq split(',',$old_role_interface->{interface}); + my $newinterfaces = $self->getInterfaceByName($old_role_interface->{role}); + if ($newinterfaces) { + # New interface list + my @newinterfaces = uniq split(',',$newinterfaces); + @diff{ @oldinterfaces } = @oldinterfaces; + delete @diff{ @newinterfaces }; + @oldinterfaces = uniq %diff; + } + if (@oldinterfaces) { + $vars{'switches'}{$switch_id}{'interfaces_delete'}{$old_role_interface->{role}} = \@oldinterfaces; + } } - if ((!defined($out_acls) || $out_acls eq "") && $implicit_acl) { - $vars{'switches'}{$switch_id}{'acls'}{$role."out"} = $implicit_acl; - } elsif (defined($out_acls)) { - $vars{'switches'}{$switch_id}{'acls'}{$role."out"} = $out_acls; + } + + foreach my $role (keys %ConfigRoles) { + my $acls = $self->getRoleAccessListByName($role); + if (defined($acls)) { + my $out_acls; + my $in_acls; + while($acls =~ /([^\n]+)\n?/g) { + my $acl_line = $1; + if ($acl_line =~ /^(out\|)(.*)/) { + $out_acls .= $2."\n"; + } elsif ($acl_line =~ /^(in\|)(.*)/) { + $in_acls .= $2."\n"; + } else { + $in_acls .= $acl_line."\n"; + } + } + my $implicit_acl = $self->implicit_acl(); + if ((!defined($in_acls) || $in_acls eq "") && $implicit_acl) { + $vars{'switches'}{$switch_id}{'acls'}{$role} = $implicit_acl; + } elsif (defined($in_acls)) { + $vars{'switches'}{$switch_id}{'acls'}{$role} = $in_acls; + } + if ((!defined($out_acls) || $out_acls eq "") && $implicit_acl) { + $vars{'switches'}{$switch_id}{'acls'}{$role."out"} = $implicit_acl; + } elsif (defined($out_acls)) { + $vars{'switches'}{$switch_id}{'acls'}{$role."out"} = $out_acls; + } } } @@ -4354,7 +4427,7 @@ sub generateAnsibleConfiguration { $tt->process("$conf_dir/pfsetacls/inventory.cfg", \%vars, "$var_dir/conf/pfsetacls/$switch_id/inventory.yml") or die $tt->error(); $tt->process("$conf_dir/pfsetacls/ansible.cfg", \%vars, "$var_dir/conf/pfsetacls/$switch_id/ansible.cfg") or die $tt->error(); - $tt->process("$conf_dir/pfsetacls/switch_acls.yml", \%vars, "$var_dir/conf/pfsetacls/$switch_id/switch_acls.yml") or die $tt->error(); + $tt->process("$conf_dir/pfsetacls/switch_acls.yml", $vars{'switches'}{$switch_id}, "$var_dir/conf/pfsetacls/$switch_id/switch_acls.yml") or die $tt->error(); $tt->process("$conf_dir/pfsetacls/collections/requirements.yml", \%vars, "$var_dir/conf/pfsetacls/$switch_id/collections/requirements.yml") or die $tt->error(); find(\&pf::util::chown_pf, "$var_dir/conf/pfsetacls/$switch_id/"); if (-e "$var_dir/conf/pfsetacls/$switch_id/ansible.log") { unlink "$var_dir/conf/pfsetacls/$switch_id/ansible.log" }; diff --git a/lib/pf/UnifiedApi/Controller/Config.pm b/lib/pf/UnifiedApi/Controller/Config.pm index 05a6c7c237eb..fe929087e727 100644 --- a/lib/pf/UnifiedApi/Controller/Config.pm +++ b/lib/pf/UnifiedApi/Controller/Config.pm @@ -397,6 +397,7 @@ sub create { return 0; } + my $old_config = $self->item_from_store($id); $item = $self->cleanupItemForCreate($item); (my $status, $item, my $form) = $self->validate_item($item); if (is_error($status)) { @@ -410,7 +411,7 @@ sub create { $cs->create($id, $item); return unless($self->commit($cs)); - $self->post_create($id); + $self->post_create($id, $old_config); my $additional_out = $self->additional_create_out($form, $item); $self->stash( $self->primary_key => $id ); $self->res->headers->location($self->make_location_url($id)); @@ -522,15 +523,22 @@ sub remove { my $id = $self->id; my $cs = $self->config_store; + my $old_item = $self->item_from_store($id); + $self->pre_remove($id, $old_item); ($msg, my $deleted) = $cs->remove($id, 'id'); if (!$deleted) { return $self->render_error(422, "Unable to delete $id - $msg"); } return unless($self->commit($cs)); + $self->post_remove($id, $old_item); return $self->render(json => {message => "Deleted $id successfully"}, status => 200); } +sub post_remove { } + +sub pre_remove { } + sub addFormWarnings { my ($self, $form, $response) = @_; if ($form->has_pf_warnings) { @@ -568,7 +576,7 @@ sub update { my $id = $self->id; $cs->update($id, $new_data); return unless($self->commit($cs)); - $self->post_update($id); + $self->post_update($id, $old_item); $self->render(status => 200, json => $self->update_response($form)); } diff --git a/lib/pf/UnifiedApi/Controller/Config/Roles.pm b/lib/pf/UnifiedApi/Controller/Config/Roles.pm index 44ac81ed4cec..cdf4f39dc5c4 100644 --- a/lib/pf/UnifiedApi/Controller/Config/Roles.pm +++ b/lib/pf/UnifiedApi/Controller/Config/Roles.pm @@ -261,7 +261,7 @@ sub reassign_role_config_store_switch { my $i = 0; my $cachedConfig = $cs->cachedConfig; for my $sect ($cachedConfig->Sections()) { - for my $f (map { "${old}${_}" } qw(Role Url Vlan AccessList Vpn Network NetworkFrom) ) { + for my $f (map { "${old}${_}" } qw(Role Url Vlan AccessList Vpn Interface Network NetworkFrom) ) { next if !$cachedConfig->exists($sect, $f); $cachedConfig->delval($sect, $f); $i |= 1; diff --git a/lib/pf/UnifiedApi/Controller/Config/SwitchRole.pm b/lib/pf/UnifiedApi/Controller/Config/SwitchRole.pm index d165635d20f4..364b75651889 100644 --- a/lib/pf/UnifiedApi/Controller/Config/SwitchRole.pm +++ b/lib/pf/UnifiedApi/Controller/Config/SwitchRole.pm @@ -63,7 +63,7 @@ sub makeMappings { my %mapping; while (my ($k, $v) = each %$patch) { - next unless ($k =~ /(.*)(AccessList|Vlan|Url|Role|Vpn|Network|NetworkFrom)$/); + next unless ($k =~ /(.*)(AccessList|Vlan|Url|Role|Vpn|Interface|Network|NetworkFrom)$/); my $type = $2; my $role = $1; if ($type eq 'Role') { diff --git a/lib/pf/UnifiedApi/Controller/Config/Switches.pm b/lib/pf/UnifiedApi/Controller/Config/Switches.pm index c058a944b7fe..8e61bb81b433 100644 --- a/lib/pf/UnifiedApi/Controller/Config/Switches.pm +++ b/lib/pf/UnifiedApi/Controller/Config/Switches.pm @@ -28,6 +28,7 @@ use pf::ConfigStore::SwitchGroup; use pfappserver::Form::Config::Switch; use pf::db; use List::Util qw(first); +use pf::constants qw($TRUE $FALSE); BEGIN { local $pf::db::NO_DIE_ON_DBH_ERROR = 1; @@ -78,16 +79,24 @@ sub id { } sub post_update { - my ($self, $switch_id) = @_; + my ($self, $switch_id, $old) = @_; my $switch = pf::SwitchFactory->instantiate($switch_id); if ($switch) { - $switch->generateAnsibleConfiguration(); + $switch->generateAnsibleConfiguration($old,$FALSE); } } sub post_create { - my ($self, $switch_id) = @_; - $self->post_update($switch_id); + my ($self, $switch_id, $old) = @_; + $self->post_update($switch_id, $old); +} + +sub pre_remove { + my ($self, $switch_id, $old) = @_; + my $switch = pf::SwitchFactory->instantiate($switch_id); + if ($switch) { + $switch->generateAnsibleConfiguration($old,$TRUE); + } } =head2 standardPlaceholder @@ -121,6 +130,7 @@ sub cleanup_options { my $accessListMapping = $placeholder->{AccessListMapping}; my $urlMapping = $placeholder->{UrlMapping}; my $vpnMapping = $placeholder->{VpnMapping}; + my $interfaceMapping = $placeholder->{InterfaceMapping}; my $roleMapping = $placeholder->{ControllerRoleMapping}; my $networkMapping = $placeholder->{NetworkMapping}; my $networkMappingFrom = $placeholder->{NetworkMappingFrom}; @@ -130,6 +140,7 @@ sub cleanup_options { $meta->{"${r}AccessList"} = mapping_meta($r, $accessListMapping, 'accesslist', $self->json_false); $meta->{"${r}Url"} = mapping_meta($r, $urlMapping, 'url', $self->json_false); $meta->{"${r}Vpn"} = mapping_meta($r, $vpnMapping, 'vpn', $self->json_false); + $meta->{"${r}Interface"} = mapping_meta($r, $interfaceMapping, 'interface', $self->json_false); $meta->{"${r}Role"} = mapping_meta($r, $roleMapping, 'controller_role', $self->json_false); $meta->{"${r}Network"} = mapping_meta($r, $networkMapping, 'network', $self->json_false); $meta->{"${r}NetworkFrom"} = mapping_meta($r, $networkMappingFrom, 'networkfrom', $self->json_false); @@ -152,6 +163,25 @@ sub mapping_placeholder { return defined $m ? $m->{$f} : undef; } +sub validate_item { + my ($self, $item) = @_; + return 422, { message => "Duplicate interface detected" }, undef if $self->_duplicate_item($item); + return $self->SUPER::validate_item($item); +} + +sub _duplicate_item { + my ($self, $item) = @_; + my @interfaces; + foreach my $entry (@{$item->{'InterfaceMapping'}}) { + push(@interfaces, split(',',$entry->{'interface'})) if (defined $entry->{'interface'}); + } + my %duplicated; + foreach my $interface (@interfaces) { + next unless $duplicated{$interface}++; + return 1; + } +} + =head1 AUTHOR Inverse inc. diff --git a/lib/pfconfig/namespaces/config/Switch.pm b/lib/pfconfig/namespaces/config/Switch.pm index c990a88fbb0b..c0e55b5b92b3 100644 --- a/lib/pfconfig/namespaces/config/Switch.pm +++ b/lib/pfconfig/namespaces/config/Switch.pm @@ -86,11 +86,11 @@ sub build_child { $self->updateReverseLookup($name, $switch, qw(group)); # transforming vlans and roles to hashes - my %merged = ( Vlan => {}, Role => {}, AccessList => {} , Url => {} , Vpn => {} , Network => {}, NetworkFrom => {}); + my %merged = ( Vlan => {}, Role => {}, AccessList => {} , Url => {} , Vpn => {} , Interface => {}, Network => {}, NetworkFrom => {}); my %roles; - foreach my $key ( grep {/(Vlan|Role|AccessList|Url|Vpn|Network|NetworkFrom)$/} keys %{$switch} ) { + foreach my $key ( grep {/(Vlan|Role|AccessList|Url|Vpn|Interface|Network|NetworkFrom)$/} keys %{$switch} ) { next unless my $value = $switch->{$key}; - if ( my ( $type_key, $type ) = ( $key =~ /^(.+)(Vlan|Role|AccessList|Url|Vpn|Network|NetworkFrom)$/ ) ) { + if ( my ( $type_key, $type ) = ( $key =~ /^(.+)(Vlan|Role|AccessList|Url|Vpn|Interface|Network|NetworkFrom)$/ ) ) { $merged{$type}{$type_key} = $value; $roles{$type_key} = undef; } @@ -108,6 +108,7 @@ sub build_child { $switch->{networks} = $merged{Network}; $switch->{networks_from}= $merged{NetworkFrom}; + $switch->{interfaces} = $merged{Interface}; $switch->{VoIPEnabled} = ( $switch->{VoIPEnabled} =~ /^\s*(y|yes|true|enabled|1)\s*$/i ? 1