mirror of https://github.com/sipwise/ngcpcfg.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
11 KiB
375 lines
11 KiB
#!/usr/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
use open qw(:std :encoding(UTF-8));
|
|
|
|
use Storable qw(dclone);
|
|
use List::Util qw(any);
|
|
use Getopt::Long qw(:config posix_default no_ignorecase auto_help auto_version);
|
|
use Pod::Usage;
|
|
use YAML::XS qw();
|
|
use Kwalify;
|
|
|
|
our $VERSION = 'UNRELEASED';
|
|
|
|
sub miss_check
|
|
{
|
|
my ($miss, $text) = @_;
|
|
my $errors = 0;
|
|
|
|
foreach my $id (sort keys %{$miss}) {
|
|
warn " - $text $id\n";
|
|
$errors++;
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
sub dupe_check
|
|
{
|
|
my ($dupe, $text) = @_;
|
|
my $errors = 0;
|
|
|
|
foreach my $id (sort keys %{$dupe}) {
|
|
next if scalar @{$dupe->{$id}} <= 1;
|
|
|
|
warn " - $text $id (@{$dupe->{$id}})\n";
|
|
$errors++;
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
#
|
|
# Main
|
|
#
|
|
|
|
my $network_file = '/etc/ngcp-config/network.yml';
|
|
my $schema_path = '/usr/share/ngcp-cfg-schema/validate';
|
|
|
|
GetOptions(
|
|
'n|network-file=s' => \$network_file,
|
|
's|schema-dir=s' => \$schema_path,
|
|
) or pod2usage(2);
|
|
|
|
my $schema_tmpl_head_file = "$schema_path/tmpl-network-head.yml";
|
|
my $schema_tmpl_host_file = "$schema_path/tmpl-network-host.yml";
|
|
my $schema_tmpl_iface_file = "$schema_path/tmpl-network-iface.yml";
|
|
|
|
|
|
my $yaml = YAML::XS::LoadFile($network_file)
|
|
or die "cannot parse file $network_file";
|
|
my $schema = YAML::XS::LoadFile($schema_tmpl_head_file)
|
|
or die "cannot parse network header schema template fragment";
|
|
my $schema_host = YAML::XS::LoadFile($schema_tmpl_host_file)
|
|
or die "cannot parse network host schema template fragment";
|
|
my $schema_iface = YAML::XS::LoadFile($schema_tmpl_iface_file)
|
|
or die "cannot parse network interface schema template fragment";
|
|
|
|
# Data to track some manual out-of-schema validation.
|
|
my %miss_peer;
|
|
my %miss_iface;
|
|
my %dupe_iface_type;
|
|
my %dupe_dbnode;
|
|
my %dupe_ip;
|
|
my %dupe_mac;
|
|
|
|
# First pass to do an initial pre-fill.
|
|
foreach my $hostname (sort keys %{$yaml->{hosts}}) {
|
|
my $host = $yaml->{hosts}->{$hostname};
|
|
|
|
# Manual checks.
|
|
if (defined $host->{dbnode}) {
|
|
push @{$dupe_dbnode{$host->{dbnode}}}, $hostname;
|
|
}
|
|
|
|
# Fill host map
|
|
my $hostmap = {
|
|
type => 'map',
|
|
mapping => dclone($schema_host->{hostmap}),
|
|
};
|
|
if ($hostname =~ m/^(?:self|sp[1-9]|(db|prx)01[a-i])$/) {
|
|
$hostmap->{required} = 'yes';
|
|
} else {
|
|
$hostmap->{required} = 'no';
|
|
}
|
|
|
|
# Fill peer entry.
|
|
my $peermap = {
|
|
type => 'text',
|
|
};
|
|
if ($hostname =~ m/^(?:sp([1-9])|((?:db|web|lb|slb|prx|rtp)\d\d)([a-i]))$/) {
|
|
my $peer;
|
|
|
|
$peermap->{required} = 'yes';
|
|
|
|
if (defined $1 and $1 eq '1') {
|
|
$peer = 'sp2';
|
|
} elsif (defined $1 and $1 eq '2') {
|
|
$peer = 'sp1';
|
|
} elsif (defined $3 and $3 eq 'a') {
|
|
$peer = "$2b";
|
|
} elsif (defined $3 and $3 eq 'b') {
|
|
$peer = "$2a";
|
|
}
|
|
$peermap->{enum} = [ $peer ];
|
|
|
|
# Manual check.
|
|
$miss_peer{$peer}++ unless exists $yaml->{hosts}->{$peer};
|
|
} elsif ($hostname eq 'self') {
|
|
$peermap->{required} = 'no';
|
|
} else {
|
|
warn "[/hosts/<$hostname>] Unknown hostname pattern\n";
|
|
}
|
|
$hostmap->{mapping}->{peer} = $peermap;
|
|
|
|
# Fill the interface entries.
|
|
foreach my $iface (sort @{$host->{interfaces}}) {
|
|
if (not exists $host->{$iface}) {
|
|
$miss_iface{$iface}++;
|
|
next;
|
|
}
|
|
|
|
# Fill the interface.
|
|
my $ifacemap = {
|
|
type => 'map',
|
|
required => $iface eq 'lo' ? 'yes' : 'no',
|
|
mapping => dclone($schema_iface->{ifacemap}),
|
|
};
|
|
|
|
my $hwaddr_dupe_check = 1;
|
|
|
|
if ($iface =~ /\./) {
|
|
# ethX.Y type vlan interface
|
|
$ifacemap->{mapping}->{type}->{required} = 'yes';
|
|
$hwaddr_dupe_check = 0;
|
|
} elsif ($iface =~ m/^bond/ && $iface !~ m/:/) {
|
|
$ifacemap->{mapping}->{bond_miimon}->{required} = 'yes';
|
|
$ifacemap->{mapping}->{bond_mode}->{required} = 'yes';
|
|
$ifacemap->{mapping}->{bond_slaves}->{required} = 'yes';
|
|
} elsif ($iface =~ m/^eth/) {
|
|
$ifacemap->{mapping}->{hwaddr}->{required} = 'yes';
|
|
} elsif ($iface =~ m/^vlan/) {
|
|
$ifacemap->{mapping}->{type}->{required} = 'yes';
|
|
$ifacemap->{mapping}->{vlan_raw_device}->{required} = 'yes'
|
|
if $iface !~ m/:/;
|
|
$hwaddr_dupe_check = 0;
|
|
} elsif ($iface =~ m/^idrac/) {
|
|
$ifacemap->{mapping}->{type}->{required} = 'yes';
|
|
}
|
|
|
|
# Manual interface checks.
|
|
if (defined $host->{$iface}->{ip} and
|
|
$host->{$iface}->{ip} ne '' and
|
|
$host->{$iface}->{ip} ne '127.0.0.1' and
|
|
$iface !~ m/^dummy/ and $iface !~ m/^tun/) {
|
|
push @{$dupe_ip{$host->{$iface}->{ip}}}, "$hostname/$iface";
|
|
}
|
|
if ($hwaddr_dupe_check and
|
|
defined $host->{$iface}->{hwaddr} and
|
|
$host->{$iface}->{hwaddr} ne '' and
|
|
$host->{$iface}->{hwaddr} ne '00:00:00:00:00:00') {
|
|
push @{$dupe_mac{$host->{$iface}->{hwaddr}}}, "$hostname/$iface";
|
|
}
|
|
if (any { $_ eq 'api_int' } @{$host->{$iface}->{type}}) {
|
|
push @{$dupe_iface_type{"$hostname/api_int"}}, "$hostname/$iface";
|
|
}
|
|
|
|
# Interface aliases.
|
|
if ($iface =~ m/:/) {
|
|
$ifacemap->{mapping}->{ip}->{required} = 'yes';
|
|
$ifacemap->{mapping}->{netmask}->{required} = 'yes';
|
|
}
|
|
|
|
# Consistency checks.
|
|
if (defined $host->{$iface}->{dhcp}) {
|
|
$ifacemap->{mapping}->{ip}->{required} = 'no';
|
|
$ifacemap->{mapping}->{netmask}->{required} = 'no';
|
|
$ifacemap->{mapping}->{v6ip}->{required} = 'no';
|
|
} elsif (defined $host->{$iface}->{ip}) {
|
|
$ifacemap->{mapping}->{ip}->{required} = 'yes';
|
|
$ifacemap->{mapping}->{netmask}->{required} = 'yes';
|
|
$ifacemap->{mapping}->{v6ip}->{required} = 'no';
|
|
} elsif (defined $host->{$iface}->{v6ip}) {
|
|
$ifacemap->{mapping}->{ip}->{required} = 'no';
|
|
$ifacemap->{mapping}->{netmask}->{required} = 'no';
|
|
$ifacemap->{mapping}->{v6ip}->{required} = 'yes';
|
|
}
|
|
|
|
if (any { $_ eq 'ha_int' or $_ =~ /^(ssh|mon)_/ } @{$host->{$iface}->{type}}) {
|
|
$host->{$iface}->{shared_ip_only} = 'no';
|
|
$host->{$iface}->{shared_v6ip_only} = 'no';
|
|
}
|
|
|
|
if (defined $host->{$iface}->{netmask} and
|
|
($host->{$iface}->{shared_ip_only} || 'no') eq 'no' and
|
|
((defined $host->{$iface}->{shared_ip} and
|
|
@{$host->{$iface}->{shared_ip}}) or
|
|
(defined $host->{$iface}->{advertised_ip} and
|
|
@{$host->{$iface}->{advertised_ip}}))) {
|
|
$ifacemap->{mapping}->{ip}->{required} = 'yes';
|
|
}
|
|
if (defined $host->{$iface}->{v6netmask} and
|
|
($host->{$iface}->{shared_v6ip_only} || 'no') eq 'no' and
|
|
(defined $host->{$iface}->{shared_v6ip} and
|
|
@{$host->{$iface}->{shared_v6ip}})) {
|
|
$ifacemap->{mapping}->{v6ip}->{required} = 'yes';
|
|
}
|
|
|
|
$hostmap->{mapping}->{$iface} = $ifacemap;
|
|
}
|
|
|
|
# Commit the hostmap data.
|
|
$schema->{mapping}->{hosts}->{mapping}->{$hostname} = $hostmap;
|
|
}
|
|
|
|
# Second pass fixing up some of the schema, with the entire schema in place.
|
|
foreach my $hostname (sort keys %{$yaml->{hosts}}) {
|
|
my $host = $yaml->{hosts}->{$hostname};
|
|
my $hostmap = $schema->{mapping}->{hosts}->{mapping}->{$hostname};
|
|
|
|
foreach my $iface (sort @{$host->{interfaces}}) {
|
|
next unless exists $host->{$iface};
|
|
|
|
foreach my $slave (split ' ', $host->{$iface}->{bond_slaves} // '') {
|
|
my $slavemap = $hostmap->{mapping}->{$slave};
|
|
|
|
# The bonded slave inerfaces do not require ip nor netmask.
|
|
$slavemap->{mapping}->{ip}->{required} = 'no';
|
|
$slavemap->{mapping}->{netmask}->{required} = 'no';
|
|
}
|
|
}
|
|
}
|
|
|
|
# Make it possible to dump the generated schema.
|
|
if (defined $ENV{NGCP_KWALIFY_DUMP}) {
|
|
# YAML::XS::Dump returns raw UTF-8 strings.
|
|
binmode \*STDOUT, ':raw';
|
|
print YAML::XS::Dump($schema);
|
|
# Flush any lingering buffers, before switching the encoding back.
|
|
STDOUT->flush();
|
|
binmode \*STDOUT, ':encoding(UTF-8)';
|
|
}
|
|
|
|
#
|
|
# Validate the network.yml file dynamically.
|
|
#
|
|
|
|
if (defined $ENV{NGCP_KWALIFY_VERBOSE}) {
|
|
warn "Checking $network_file\n"
|
|
}
|
|
|
|
my $errors = 0;
|
|
|
|
eval {
|
|
Kwalify::validate($schema, $yaml);
|
|
} or do {
|
|
warn $@;
|
|
$errors++;
|
|
};
|
|
|
|
# Perform out-of-schema checks.
|
|
$errors += miss_check(\%miss_peer, '[/hosts/<host>] Missing peer');
|
|
$errors += miss_check(\%miss_iface, '[/hosts/<host>/<iface>] Missing definition for interface declared in [/hosts/<host>/interfaces]');
|
|
$errors += dupe_check(\%dupe_dbnode, '[/hosts/<host>/dbnode] Duplicate dbnode index');
|
|
$errors += dupe_check(\%dupe_iface_type, '[/hosts/<host>/<iface>/type] Duplicate interface type');
|
|
$errors += dupe_check(\%dupe_ip, '[/hosts/<host>/<iface>/ip] Duplicate interface IP');
|
|
$errors += dupe_check(\%dupe_mac, '[/hosts/<host>/<iface>/hwaddr] Duplicate interface MAC');
|
|
|
|
exit 1 if $errors;
|
|
|
|
1;
|
|
|
|
# vim: ts=4 sw=4 et
|
|
|
|
__END__
|
|
|
|
=encoding utf-8
|
|
|
|
=head1 NAME
|
|
|
|
ngcp-network-validator - dynamically validates the network.yml file
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
B<ngcp-network-validator> [I<option>...]
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
B<This program> generates a schema at run-time to accurately validate this
|
|
system specific network.yml, with the information gathered from the
|
|
same file.
|
|
|
|
=head1 OPTIONS
|
|
|
|
=over 8
|
|
|
|
=item B<-n>, B<--network-file> I<yaml-file>
|
|
|
|
The B<network.yml> file to validate.
|
|
|
|
=item B<-s>, B<--schema-dir> I<directory>
|
|
|
|
Set the schema directory to use containing the schema YAML validation
|
|
templates.
|
|
|
|
=item B<--help>
|
|
|
|
Print a help message.
|
|
|
|
=item B<--version>
|
|
|
|
Print the prorgam version.
|
|
|
|
=back
|
|
|
|
=head1 EXIT STATUS
|
|
|
|
=over 8
|
|
|
|
=item B<0>
|
|
|
|
The file validates correctly.
|
|
|
|
=item B<1>
|
|
|
|
The file has validation errors.
|
|
|
|
=back
|
|
|
|
=head1 ENVIRONMENT
|
|
|
|
=over 8
|
|
|
|
=item NGCP_KWALIFY_DUMP
|
|
|
|
When set, dumps the dynamically generated schema to stdout.
|
|
|
|
=item NGCP_KWALIFY_VERBOSE
|
|
|
|
When set, enable verbose mode.
|
|
|
|
=back
|
|
|
|
=head1 AUTHOR
|
|
|
|
Guillem Jover <gjover@sipwise.com>
|
|
|
|
=head1 LICENSE
|
|
|
|
Copyright © 2017 Sipwise GmbH, Austria
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|