#!/usr/bin/perl -CSD use warnings; use strict; use Carp; use Data::Validate::IP; use English qw( -no_match_vars ); use Getopt::Long; use IO::Interface; use IO::Socket; use IPC::Open3; use List::Util qw(any max); use Net::Netmask; use Pod::Usage; use Regexp::IPv6 qw($IPv6_re); use Socket; use Storable qw(dclone); use Sys::Hostname; use YAML::XS; our $VERSION = 'UNRELEASED'; # defaults / option handling {{{ open my $hh, '<', '/etc/hostname' or croak "Error opening /etc/hostname: $ERRNO"; my $host = <$hh>; close $hh or croak 'Error closing file handling for /etc/hostname'; chomp $host; length $host or croak "Fatal error retrieving hostname [$host]"; my $advertised_ip; my $bond_miimon; my $bond_mode; my $bond_slaves; my $broadcast; my $clone_from; my $clone_to; my $dbnode; my $dhcp; my $v6dhcp; my @dns_nameservers; my $gateway; my $v6gateway; my $help; my $hwaddr; my $inputfile = '/etc/ngcp-config/network.yml'; my $internal_iface; my $ip; my $ip_v6; my $man; my $manual; my $v6manual; my $move_from; my $move_to; my $mtu; my $netmask; my $netmask_ip_v6; my $offload_lro; my $offload_tso; my $openvpn_ca_file; my $openvpn_ca_template; my $openvpn_ca_inline; my $openvpn_cert_file; my $openvpn_cert_template; my $openvpn_cert_inline; my $openvpn_key_file; my $openvpn_key_template; my $openvpn_key_inline; my $outputfile = $inputfile; my $peer; my @remove_host; my @remove_interface; my @roles; my @set_interface; my $shared_ip; my $shared_ip_v6; my $sysname; my $sysdescr; my $syslocation; my @type; my $verbose; my $version; my $vlan_raw_device; GetOptions( 'advertised-ip=s' => \$advertised_ip, 'bond-miimon=s' => \$bond_miimon, 'bond-mode=s' => \$bond_mode, 'bond-slaves=s' => \$bond_slaves, 'broadcast=s' => \$broadcast, 'clone-from=s' => \$clone_from, 'clone-to=s' => \$clone_to, 'dbnode:i' => \$dbnode, 'dhcp=s' => \$dhcp, 'dhcp-ipv6=s' => \$v6dhcp, 'dns=s' => \@dns_nameservers, 'gateway=s' => \$gateway, 'gateway-ipv6=s' => \$v6gateway, 'help' => \$help, 'host=s' => \$host, 'hwaddr=s' => \$hwaddr, 'input-file=s' => \$inputfile, 'internal-iface=s' => \$internal_iface, 'ip=s' => \$ip, 'ipv6=s' => \$ip_v6, 'man' => \$man, 'manual=s' => \$manual, 'manual-ipv6=s' => \$v6manual, 'move-from=s' => \$move_from, 'move-to=s' => \$move_to, 'mtu=s' => \$mtu, 'netmask=s' => \$netmask, 'netmask-ipv6=s' => \$netmask_ip_v6, 'offload-lro=s' => \$offload_lro, 'offload-tso=s' => \$offload_tso, 'openvpn-ca-file=s' => \$openvpn_ca_file, 'openvpn-ca-template=s' => \$openvpn_ca_template, 'openvpn-ca-inline=s' => \$openvpn_ca_inline, 'openvpn-cert-file=s' => \$openvpn_cert_file, 'openvpn-cert-template=s' => \$openvpn_cert_template, 'openvpn-cert-inline=s' => \$openvpn_cert_inline, 'openvpn-key-file=s' => \$openvpn_key_file, 'openvpn-key-template=s' => \$openvpn_key_template, 'openvpn-key-inline=s' => \$openvpn_key_inline, 'output-file=s' => \$outputfile, 'peer=s' => \$peer, 'remove-host=s' => \@remove_host, 'remove-interface=s' => \@remove_interface, 'role=s' => \@roles, 'set-interface=s' => \@set_interface, 'shared-ip=s' => \$shared_ip, 'shared-ipv6=s' => \$shared_ip_v6, 'sysname=s' => \$sysname, 'sysdescr=s' => \$sysdescr, 'syslocation=s' => \$syslocation, 'type=s' => \@type, 'verbose' => \$verbose, 'version' => \$version, 'vlan-raw-device=s' => \$vlan_raw_device, ) or pod2usage(2); if ($help) { pod2usage(0); } if ($version) { print "$PROGRAM_NAME, v$version\n"; exit 0; } if ($man) { pod2usage( -exitstatus => 0, -verbose => 2 ); } # validate input if ($ip) { logger("validating specified IP address $ip"); if ( is_ipv4($ip) || $ip =~ /^(auto|none|delete)$/msx ) { logger('valid IPv4 address'); } else { croak "Specified IP $ip is not a valid IPv4 address"; } } if ($ip_v6) { logger("validating specified IP address $ip_v6"); if ( is_ipv6($ip_v6) || $ip_v6 =~ /^(auto|none|delete)$/msx ) { logger('valid IPv6 address'); } else { croak "Specified IP $ip_v6 is not a valid IPv6 address"; } } foreach my $opt ( $bond_miimon, $bond_mode, $broadcast, $clone_from, $clone_to, $dbnode, @dns_nameservers, $gateway, @set_interface, $host, $hwaddr, $internal_iface, $ip, $ip_v6, $move_from, $move_to, $netmask, $peer, @remove_host, @remove_interface, @roles, @type, $vlan_raw_device, $netmask_ip_v6, $v6gateway, $mtu, $v6dhcp, $manual, $v6manual ) { if ( defined $opt && $opt =~ /\s/msx ) { logger("invalid option argument $opt"); croak "Command line option does not accept whitespace in '$opt'."; } } # }}} logger("reading input file $inputfile"); my $yaml = YAML::XS::LoadFile($inputfile) or croak "File $inputfile could not be read"; if ( -e "$outputfile" && !-w "$outputfile" ) { croak "Could not open $outputfile for writing (are you user root?)"; } my $spce; if ( defined $yaml->{hosts}->{self} ) { logger('host "self" identified and set, assuming spce system'); $host = 'self'; $spce = 1; } # helper functions {{{ sub logger { if ($verbose) { print "@_\n" or croak 'Error sending log output'; } return; } sub get_interface_details { my $interface = shift or croak "Usage: $PROGRAM_NAME "; my $setting = shift or croak "Usage: $PROGRAM_NAME "; my $s = IO::Socket::INET->new( Proto => 'tcp' ); return $s->$setting($interface); } sub get_ip6_addrs { my $interface = shift or croak 'Usage: get_ip6_addrs '; my $cmd = 'ip'; my @args = ( '-6', 'addr', 'show', 'dev', $interface, 'scope', 'global' ); logger("get_ip6_addr for device $interface"); logger("$cmd @args"); my $childpid = open3 'HIS_WRITE', 'HIS_OUT', 'HIS_ERR', $cmd, @args; my @stderr = ; my @stdout = ; logger("stdout: @stdout"); logger("stderr: @stderr"); close HIS_OUT or croak 'Failed to close stdout'; close HIS_ERR or croak 'Failed to close stderr'; waitpid $childpid, 0; if ($CHILD_ERROR) { croak "Problem with execution [return code $CHILD_ERROR]:\n@stderr"; } return @stdout; } sub get_ip6_addr { my @stdout = get_ip6_addrs(@_); foreach my $line (@stdout) { if ( $line =~ /^\s*inet6\s+($IPv6_re)\/\d+.*scope.*global.*$/msx ) { return $1; } } return; } sub get_ip6_mask { my @stdout = get_ip6_addrs(@_); foreach my $line (@stdout) { if ( $line =~ /^\s*inet6\s+($IPv6_re)\/(\d+).*scope.*global.*$/msx ) { return $2; } } return; } sub set_interface { my $iface = shift; logger("set_interface: interface = $iface"); logger("set_interface: host = $host"); if ( defined $ip && $ip =~ /^auto$/msx ) { logger("get_interface_details( $iface, 'if_addr' );"); $ip = get_interface_details( $iface, 'if_addr' ); } if ( defined $ip_v6 && $ip_v6 =~ /^auto$/msx ) { logger("get_ip6_addr( $iface );"); $ip_v6 = get_ip6_addr($iface); } if ( defined $netmask && $netmask =~ /^auto$/msx ) { logger("get_interface_details( $iface, 'if_netmask' );"); $netmask = get_interface_details( $iface, 'if_netmask' ); } if ( defined $netmask_ip_v6 && $netmask_ip_v6 =~ /^auto$/msx ) { logger("get_ip6_mask( $iface );"); $netmask_ip_v6 = get_ip6_mask($iface); } if ( defined $hwaddr && $hwaddr =~ /^auto$/msx ) { logger("get_interface_details( $iface, 'if_hwaddr' );"); $hwaddr = get_interface_details( $iface, 'if_hwaddr' ); } my $settings = { 'advertised_ip' => $advertised_ip, 'bond_miimon' => $bond_miimon, 'bond_mode' => $bond_mode, 'bond_slaves' => $bond_slaves, 'broadcast' => $broadcast, 'dhcp' => $dhcp, 'gateway' => $gateway, 'hwaddr' => $hwaddr, 'ip' => $ip, 'manual' => $manual, 'mtu' => $mtu, 'netmask' => $netmask, 'offload_lro' => $offload_lro, 'offload_tso' => $offload_tso, 'openvpn_ca_file' => $openvpn_ca_file, 'openvpn_ca_template' => $openvpn_ca_template, 'openvpn_ca_inline' => $openvpn_ca_inline, 'openvpn_cert_file' => $openvpn_cert_file, 'openvpn_cert_template' => $openvpn_cert_template, 'openvpn_cert_inline' => $openvpn_cert_inline, 'openvpn_key_file' => $openvpn_key_file, 'openvpn_key_template' => $openvpn_key_template, 'openvpn_key_inline' => $openvpn_key_inline, 'v6netmask' => $netmask_ip_v6, 'shared_ip' => $shared_ip, 'shared_v6ip' => $shared_ip_v6, 'v6dhcp' => $v6dhcp, 'v6gateway' => $v6gateway, 'v6ip' => $ip_v6, 'v6manual' => $v6manual, 'vlan_raw_device' => $vlan_raw_device, }; foreach my $k ( keys %{$settings} ) { if ( defined $settings->{$k} ) { if ( $settings->{$k} =~ /^none$/msx ) { logger("unsetting entry $k"); undef $yaml->{hosts}->{$host}->{$iface}->{$k}; } elsif ( $settings->{$k} =~ /^delete$/msx ) { logger("deleting entry $k"); delete $yaml->{hosts}->{$host}->{$iface}->{$k}; } else { if($k eq 'shared_ip' || $k eq 'shared_v6ip' || $k eq 'advertised_ip') { if (my ($matched) = grep { $_ eq $settings->{$k} } @{ $yaml->{hosts}->{$host}->{$iface}->{$k} }) { logger("not setting $k to $matched to avoid duplicates"); } else { logger("adding IP entry $k: $settings->{$k}"); push @{ $yaml->{hosts}->{$host}->{$iface}->{$k} }, $settings->{$k} } } else { logger("adding entry $k: $settings->{$k}"); $yaml->{hosts}->{$host}->{$iface}->{$k} = $settings->{$k}; } } } } if (@dns_nameservers) { foreach my $dns (@dns_nameservers) { logger("set_iface_config( $iface, 'dns_nameservers', $dns)"); set_iface_config( $iface, 'dns_nameservers', $dns ); } } if (@type) { foreach my $type (@type) { logger("set_iface_config( $iface, 'type', $type)"); set_iface_config( $iface, 'type', $type ); } } # add interface to list of available interfaces my $ifaces = $yaml->{hosts}->{$host}->{interfaces}; if ( !defined $ifaces ) { logger("no interfaces defined yet, adding interface $iface"); $yaml->{hosts}->{$host}->{interfaces}->[0] = "$iface"; } else { logger("interface = $iface"); if ( any { /^${iface}$/msx } @{$ifaces} ) { logger("interface $iface already listed on host $host"); } else { push @{$ifaces}, $iface; logger("adding $iface to list of interfaces on host $host"); } } return; } sub remove_interface { my $rem_iface = shift or croak 'Usage: remove_interface '; delete $yaml->{hosts}->{$host}->{$rem_iface}; my $ifaces = $yaml->{hosts}->{$host}->{interfaces}; if ( defined $ifaces ) { logger("removing interface @$ifaces as requested"); undef @{$ifaces}; } return; } sub set_host_key { my $key = shift or croak 'Usage: set_host_key '; my $value = shift or croak 'Usage: set_host_key '; $yaml->{hosts}->{$host}->{$key} = "$value"; logger("\$yaml->{hosts}->{\$host}->{$key} = $value"); return; } sub set_host_key_cond { my ($key, $value) = @_; if ( defined $value ) { logger("set_host_key('$key', $value)"); set_host_key($key, $value); } } sub set_iface_config { my $iface = shift or croak 'Usage: set_iface_config '; my $setting = shift or croak 'Usage: set_iface_config '; my $value = shift or croak 'Usage: set_iface_config '; if ( any { /^${value}$/msx } @{ $yaml->{hosts}->{$host}->{$iface}->{$setting} } ) { logger("$value for $setting on $iface already defined in host $host"); } else { push @{ $yaml->{hosts}->{$host}->{$iface}->{$setting} }, $value; logger( "\$yaml->{hosts}->{\$host}->{$iface}->{$setting} => $value"); } return; } sub set_role { my $new_role = shift or croak 'Usage: set_role '; if ( any { /^${new_role}$/msx } @{ $yaml->{hosts}->{$host}->{role} } ) { logger("role $new_role already defined in host $host"); } else { push @{ $yaml->{hosts}->{$host}->{role} }, $new_role; logger("\$yaml->{hosts}->{\$host}->{role} => $new_role"); } return; } sub set_dbnode { my $new_dbnode = shift; my @nodes = ( 0, ); foreach my $h (keys %{ $yaml->{hosts} }) { if($h ne $host && defined $yaml->{hosts}->{$h}->{dbnode}) { push @nodes, $yaml->{hosts}->{$h}->{dbnode}; } } # if no value is given we have 0 if($new_dbnode > 0) { if (scalar(@nodes) > 0 && any { $_ == $new_dbnode } @nodes ) { croak "dbnode already in use"; } } else { my $max = max @nodes; $new_dbnode = $max + 1; logger("use $new_dbnode"); } set_host_key('dbnode', $new_dbnode); return; } sub clone_settings { my $from = shift; my $to = shift; if ( defined $from && !defined $to ) { croak '--clone-from option must be used with --clone-to option together'; } if ( !defined $from && defined $to ) { croak '--clone-to option must be used with --clone-from option together'; } # ha, nothing to do for us if ( !defined $from && !defined $to ) { return; } logger("clone from = $from"); logger("clone to = $to"); if ( !defined $yaml->{hosts}->{$from} ) { croak "Host '$from' doesn\'t exist, refusing to clone settings."; } $yaml->{hosts}->{$to} = dclone($yaml->{hosts}->{$from}); # adjust peer on-the-fly to prevent user mistakes if ( defined $yaml->{hosts}->{$from}->{peer} ) { if ( $to !~ /[ab]$/ ) { carp "Target host $from not ending with 'a' or 'b', skipping automatic peer config."; } else { logger('Target host matching ".*[ab]$" pattern'); my $other_node; if ( $to =~ /a$/ ) { $other_node = $to =~ s/a$/b/r; } else { $other_node = $to =~ s/b$/a/r; } print "Setting peer for host '$to' to '$other_node'.\n"; $yaml->{hosts}->{$to}->{peer} = $other_node; } } print "Finished cloning host section '$from' to '$to'.\n" or croak 'Output error'; print "Please do not forget to manually adjust '$outputfile'!\n" or croak 'Output error'; return; } sub move_settings { my $from = shift; my $to = shift; if ( defined $from && !defined $to ) { croak '--move-from option must be used with --move-to option together'; } if ( !defined $from && defined $to ) { croak '--move-to option must be used with --move-from option together'; } # ha, nothing to do for us if ( !defined $from && !defined $to ) { return; } logger("from = $from"); logger("to = $to"); if (@roles) { foreach my $role (@roles) { logger("role = $role"); # get rid of the entry from the old section my @tmp = grep { !/^${role}$/msx } @{ $yaml->{hosts}->{$from}->{role} }; $yaml->{hosts}->{$from}->{role} = []; push @{ $yaml->{hosts}->{$from}->{role} }, @tmp; # add it to its new place push @{ $yaml->{hosts}->{$to}->{role} }, $role; } } if (@type) { foreach my $type (@type) { logger("type = $type"); # get rid of the entry from the old section my @tmp = grep { !/^${type}$/msx } @{ $yaml->{hosts}->{$host}->{$from}->{type} }; $yaml->{hosts}->{$host}->{$from}->{type} = []; push @{ $yaml->{hosts}->{$host}->{$from}->{type} }, @tmp; # add it to its new place, but only if not already defined yet if ( any { /^${type}$/msx } @{ $yaml->{hosts}->{$host}->{$to}->{type} } ) { logger("type $type is already defined on host $host, interface $to"); } else { push @{ $yaml->{hosts}->{$host}->{$to}->{type} }, $type; } } } return; } # }}} # main execution {{{ if (@set_interface) { foreach my $interface (@set_interface) { logger("set_interface($interface)"); set_interface($interface); } } if (@remove_host) { foreach my $rhost (@remove_host) { logger("removing host $rhost"); delete $yaml->{hosts}->{$rhost}; } } if (@remove_interface) { foreach my $riface (@remove_interface) { logger("remove_interface($riface)"); remove_interface($riface); } } if ( !defined $move_from || !defined $move_to ) { if ( @roles ) { foreach my $role (@roles) { logger("set_role($role)"); set_role($role); } } set_host_key_cond('peer', $peer); set_host_key_cond('internal_iface', $internal_iface); if ( defined $dbnode ) { logger("set_dbnode($dbnode)"); set_dbnode($dbnode); } set_host_key_cond('sysname', $sysname); set_host_key_cond('sysdescr', $sysdescr); set_host_key_cond('syslocation', $syslocation); } move_settings( $move_from, $move_to ); clone_settings ( $clone_from, $clone_to ); logger("writing output file $outputfile"); YAML::XS::DumpFile($outputfile, $yaml) or croak "Could not write YAML to $outputfile"; # }}} __END__ =head1 NAME ngcp-network - command line interface to ngcp's network configuration settings =head1 SYNOPSIS B [I...] =head1 DESCRIPTION B will read the given input file(s) and do something useful with the contents thereof. =head1 OPTIONS =over 8 =item B<--advertised-ip>=I Set advertised_ip configuration to specified argument. =item B<--bond-miimon>=I Set bond_miimon configuration to specified argument. =item B<--bond-mode>=I Set bond_mode configuration to specified argument. =item B<--bond-slaves>=I Set bond_slaves configuration to specified argument. =item B<--broadcast>=I Set broadcast configuration to specified argument. =item B<--clone-from>=I Clone specified I section, using specified I setting as its source. Please do not forget to manually adjust the resulting configuration file, no further checks like duplicated IPs are performed. Needs to be used in combination with the B<--clone-to> option. This option is useful especially in carrier setups with plenty of similar web/db/proxy/.... systems, where the host definition of a host like B should be used as base for another host B. Usage example: B<--clone-from=prx01a --clone-to=prx02a>. =item B<--clone-to>=I Clone specified I section, using specified I setting as its destination. Refer to B<--clone-from> for further information. =item B<--dbnode>=I Set dbnode configuration to specified number argument. If no value is provide it will use the next value available (max + 1). =item B<--dhcp>=I Set dhcp to yes or no, to use dhcp to set up this interface. =item B<--dhcp-ipv6>=I Set dhcp to yes or no, to use dhcp to set up this interface with IPv6. =item B<--dns>=I Set dns_nameservers configuration to specified argument. Can be specified multiple times in one single command line. =item B<--gateway>=I Set gateway configuration to specified argument. =item B<--gateway-ipv6>=I Set gateway configuration to specified argument. =item B<--host>=I Apply configuration changes for specified host entry instead of using the hostname of the currently running system. NOTE: If running the sip:provider CE edition this configuration option can't be changed as the only configured host is set to and supposed to be B there. =item B<--hwaddr>=I Set hwaddr configuration (MAC address of network device) to specified argument. =item B<--input-file>=I Use specified file as input, defaults to F if unset. =item B<--internal-iface>=I Set internal-iface configuration to specified argument. =item B<--ip>=I Set ip configuration (IPv4 address) to specified argument. If set to B and the selected interface is available on the running host then the IP address will be determined based on its current settings. =item B<--ipv6>=I Set ip configuration (IPv6 address) to specified argument. If set to B and the selected interface is available on the running host then the IP address will be determined based on its current settings. =item B<--manual>=I Set interface method set-up to "manual" (man interfaces(5)) for IPv4. =item B<--manual-ipv6>=I Set interface method set-up to "manual" (man interfaces(5)) for IPv6. =item B<--move-from>=I Move item from specified level (being host for B<--role> and interface for B<--type>). The item needs to be chosen via B<--type> or B<--role>. To be used in combination with B<--move-to>. =item B<--move-to>=I Move item to specified level (being host for B<--role> and interface for B<--type>). The item needs to be chosen via B<--type> or B<--role>. To be used in combination with B<--move-to>. =item B<--mtu>=I Set MTU configuration (Maximum Transmission Unit of the network device) to specified argument. =item B<--netmask>=I Set netmask configuration to specified argument. =item B<--netmask-ipv6>=I Set IPv6 netmask (prefix length) configuration to specified argument. =item B<--offload-lro>=I Set Large Receive Offload (LRO) to yes or no (on/off) for this interface. =item B<--offload-tso>=I Set TCP Segmentation Offload (TSO) to yes or no (on/off) for this interface. =item B<--openvpn->IB<->I=I Set one of the OpenVPN config options. I can be one of B (certificate authority PEM), B (certificate PEM), or B (private key PEM). I can be one of B (name of a local file including full path), B