anvil/Anvil/Tools/Network.pm

677 lines
24 KiB
Perl
Raw Normal View History

package Anvil::Tools::Network;
#
# This module contains methods used to deal with networking stuff.
#
use strict;
use warnings;
use Data::Dumper;
use Scalar::Util qw(weaken isweak);
our $VERSION = "3.0.0";
my $THIS_FILE = "Network.pm";
### Methods;
# find_matches
# get_ips
# get_network
# is_local
=pod
=encoding utf8
=head1 NAME
Anvil::Tools::Network
Provides all methods related to networking.
=head1 SYNOPSIS
use Anvil::Tools;
# Get a common object handle on all Anvil::Tools modules.
my $anvil = Anvil::Tools->new();
# Access to methods using '$anvil->Storage->X'.
#
=head1 METHODS
Methods in this module;
=cut
sub new
{
my $class = shift;
my $self = {
};
bless $self, $class;
return ($self);
}
# Get a handle on the Anvil::Tools object. I know that technically that is a sibling module, but it makes more
# sense in this case to think of it as a parent.
sub parent
{
my $self = shift;
my $parent = shift;
$self->{HANDLE}{TOOLS} = $parent if $parent;
# Defend against memory leads. See Scalar::Util'.
if (not isweak($self->{HANDLE}{TOOLS}))
{
weaken($self->{HANDLE}{TOOLS});
}
return ($self->{HANDLE}{TOOLS});
}
#############################################################################################################
# Public methods #
#############################################################################################################
=head2 find_matches
This takes two hash keys from prior C<< Network->get_ips() >> runs and finds which are on the same network.
Paramters;
=head3 first (required)
This is the hash key of the first machine being compared.
=head3 second (required)
This is the hash key of the second machine being compared.
=cut
sub find_matches
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Network->find_matches()" }});
my $first = defined $parameter->{first} ? $parameter->{first} : "";
my $second = defined $parameter->{second} ? $parameter->{second} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
first => $first,
second => $second,
}});
if (ref($anvil->data->{network}{$first}) ne "HASH")
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Network->find_matches()", parameter => "first" }});
return("");
}
if (ref($anvil->data->{network}{$second}) ne "HASH")
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Network->find_matches()", parameter => "second" }});
return("");
}
# Loop through the first, and on each interface with an IP/subnet, look for a match in the second.
my $match = {};
foreach my $first_interface (sort {$b cmp $a} keys %{$anvil->data->{network}{$first}{interface}})
{
my $first_ip = $anvil->data->{network}{$first}{interface}{$first_interface}{ip};
my $first_subnet = $anvil->data->{network}{$first}{interface}{$first_interface}{subnet};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
first => $first,
first_interface => $first_interface,
first_ip => $first_ip,
first_subnet => $first_subnet,
}});
if (($first_ip) && ($first_subnet))
{
# Look for a match.
my $first_network = $anvil->Network->get_network({
debug => $debug,
ip => $first_ip,
subnet => $first_subnet,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { a_network => $first_network }});
foreach my $second_interface (sort {$b cmp $a} keys %{$anvil->data->{network}{$second}{interface}})
{
my $second_ip = $anvil->data->{network}{$second}{interface}{$second_interface}{ip};
my $second_subnet = $anvil->data->{network}{$second}{interface}{$second_interface}{subnet};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
second => $second,
second_interface => $second_interface,
second_ip => $second_ip,
second_subnet => $second_subnet,
}});
if (($second_ip) && ($second_subnet))
{
# Do we have a match?
my $second_network = $anvil->Network->get_network({
debug => $debug,
ip => $second_ip,
subnet => $second_subnet,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
a_network => $first_network,
b_network => $second_network,
}});
if ($first_network eq $second_network)
{
# Match!
$match->{$first}{$first_interface} = $second_network;
$match->{$second}{$second_interface} = $second_network;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"${first}::${first_interface}" => $match->{$first}{$first_interface},
"${second}::${second_interface}" => $match->{$second}{$second_interface},
}});
}
}
}
}
}
return($match);
}
=head2 get_ips
This method checks the local system for interfaces and stores them in:
* C<< network::<target>::interface::<iface_name>::ip >> - If an IP address is set
* C<< network::<target>::interface::<iface_name>::subnet >> - If an IP is set
* C<< network::<target>::interface::<iface_name>::mac >> - Always set.
* C<< network::<target>::interface::<iface_name>::default_gateway >> = C<< 0 >> if not the default gateway, C<< 1 >> if so.
* C<< network::<target>::interface::<iface_name>::gateway >> = If the default gateway, this is the gateway IP address.
* C<< network::<target>::interface::<iface_name>::dns >> = If the default gateway, this is the comma-separated list of active DNS servers.
When called without a C<< target >>, C<< local >> is used.
To aid in look-up by MAC address, C<< network::mac::<mac_address>::iface >> is also set. Note that this is not target-dependent.
Parameters;
=head3 password (optional)
If C<< target >> is set, this is the password used to log into the remote system as the C<< remote_user >>. If it is not set, an attempt to connect without a password will be made (though this will usually fail).
=head3 port (optional, default 22)
If C<< target >> is set, this is the TCP port number used to connect to the remote machine.
=head3 remote_user (optional)
If C<< target >> is set, this is the user account that will be used when connecting to the remote system.
=head3 target (optional)
If set, the file will be read from the target machine. This must be either an IP address or a resolvable host name.
The file will be copied to the local system using C<< $anvil->Storage->rsync() >> and stored in C<< /tmp/<file_path_and_name>.<target> >>. if C<< cache >> is set, the file will be preserved locally. Otherwise it will be deleted once it has been read into memory.
B<< Note >>: the temporary file will be prefixed with the path to the file name, with the C<< / >> converted to C<< _ >>.
=cut
sub get_ips
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Network->get_ips()" }});
my $password = defined $parameter->{password} ? $parameter->{password} : "";
my $port = defined $parameter->{port} ? $parameter->{port} : 22;
my $remote_user = defined $parameter->{remote_user} ? $parameter->{remote_user} : "root";
my $target = defined $parameter->{target} ? $parameter->{target} : "local";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
password => $anvil->Log->is_secure($password),
port => $port,
remote_user => $remote_user,
target => $target,
}});
# Reading locally or remote?
my $in_iface = "";
my $shell_call = $anvil->data->{path}{exe}{ip}." addr list";
my $output = "";
if ($anvil->Network->is_remote($target))
{
# Remote call
($output, my $error, my $return_code) = $anvil->Remote->call({
debug => $debug,
shell_call => $shell_call,
target => $target,
user => $remote_user,
password => $password,
remote_user => $remote_user,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:error' => $error,
's3:return_code' => $return_code,
}});
}
else
{
# Local call.
($output, my $return_code) = $anvil->System->call({debug => $debug, shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:return_code' => $return_code,
}});
}
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
if ($line =~ /^\d+: (.*?): /)
{
$in_iface = $1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { in_iface => $in_iface }});
$anvil->data->{network}{$target}{interface}{$in_iface}{ip} = "" if not defined $anvil->data->{network}{$target}{interface}{$in_iface}{ip};
$anvil->data->{network}{$target}{interface}{$in_iface}{subnet} = "" if not defined $anvil->data->{network}{$target}{interface}{$in_iface}{subnet};
$anvil->data->{network}{$target}{interface}{$in_iface}{mac} = "" if not defined $anvil->data->{network}{$target}{interface}{$in_iface}{mac};
$anvil->data->{network}{$target}{interface}{$in_iface}{default_gateway} = 0 if not defined $anvil->data->{network}{$target}{interface}{$in_iface}{default_gateway};
$anvil->data->{network}{$target}{interface}{$in_iface}{gateway} = "" if not defined $anvil->data->{network}{$target}{interface}{$in_iface}{gateway};
$anvil->data->{network}{$target}{interface}{$in_iface}{dns} = "" if not defined $anvil->data->{network}{$target}{interface}{$in_iface}{dns};
}
next if not $in_iface;
if ($in_iface eq "lo")
{
# We don't care about 'lo'.
delete $anvil->data->{network}{$target}{interface}{$in_iface};
next;
}
if ($line =~ /inet (.*?)\/(.*?) /)
{
my $ip = $1;
my $cidr = $2;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { ip => $ip, cidr => $cidr }});
my $subnet = $cidr;
if (($cidr =~ /^\d{1,2}$/) && ($cidr >= 0) && ($cidr <= 32))
{
# Convert to subnet
$subnet = $anvil->Convert->cidr({cidr => $cidr});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { subnet => $subnet }});
}
$anvil->data->{network}{$target}{interface}{$in_iface}{ip} = $ip;
$anvil->data->{network}{$target}{interface}{$in_iface}{subnet} = $subnet;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"s1:network::${target}::interface::${in_iface}::ip" => $anvil->data->{network}{$target}{interface}{$in_iface}{ip},
"s2:network::${target}::interface::${in_iface}::subnet" => $anvil->data->{network}{$target}{interface}{$in_iface}{subnet},
}});
}
if ($line =~ /ether ([0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}) /i)
{
my $mac = $1;
$anvil->data->{network}{$target}{interface}{$in_iface}{mac} = $mac;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"network::${target}::interface::${in_iface}::mac" => $anvil->data->{network}{$target}{interface}{$in_iface}{mac},
}});
# We only record the mac in 'network::mac' if this isn't a bond.
my $test_file = "/proc/net/bonding/".$in_iface;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { test_file => $test_file }});
if (not -e $test_file)
{
$anvil->data->{network}{mac}{$mac}{iface} = $in_iface;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"network::mac::${mac}::iface" => $anvil->data->{network}{mac}{$mac}{iface},
}});
}
}
}
# Read the config files for the interfaces we've found. Use 'ls' to find the interface files. Then
# we'll read them all in.
$shell_call = $anvil->data->{path}{exe}{ls}." ".$anvil->data->{path}{directories}{ifcfg};
$output = "";
if ($anvil->Network->is_remote($target))
{
# Remote call
($output, my $error, my $return_code) = $anvil->Remote->call({
debug => $debug,
shell_call => $shell_call,
target => $target,
user => $remote_user,
password => $password,
remote_user => $remote_user,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:error' => $error,
's3:return_code' => $return_code,
}});
}
else
{
# Local call.
($output, my $return_code) = $anvil->System->call({debug => $debug, shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:return_code' => $return_code,
}});
}
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
next if $line !~ /^ifcfg-/;
my $full_path = $anvil->data->{path}{directories}{ifcfg}."/".$line;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { full_path => $full_path }});
my $file_body = $anvil->Storage->read_file({
debug => $debug,
file => $full_path,
target => $target,
password => $password,
port => $port,
remote_user => $remote_user,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"s1:full_path" => $full_path,
"s2:file_body" => $file_body,
}});
# Break it apart and store any variables.
my $temp = {};
my $interface = "";
foreach my $line (split/\n/, $file_body)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
next if $line =~ /^#/;
if ($line =~ /(.*?)=(.*)/)
{
my $variable = $1;
my $value = $2;
$value =~ s/^"(.*)"$/$1/;
$temp->{$variable} = $value;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "temp->{$variable}" => $temp->{$variable} }});
if (uc($variable) eq "DEVICE")
{
$interface = $value;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "interface" => $interface }});
}
}
if ($interface)
{
$anvil->data->{network}{$target}{interface}{$interface}{file} = $full_path;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"network::${target}::interface::${interface}::file" => $anvil->data->{network}{$target}{interface}{$interface}{file},
}});
foreach my $variable (sort {$a cmp $b} keys %{$temp})
{
$anvil->data->{network}{$target}{interface}{$interface}{variable}{$variable} = $temp->{$variable};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"network::${target}::interface::${interface}::file::variable::${variable}" => $anvil->data->{network}{$target}{interface}{$interface}{variable}{$variable},
}});
}
}
}
}
# Get the routing info.
my $lowest_metric = 99999999;
my $route_interface = "";
my $route_ip = "";
$shell_call = $anvil->data->{path}{exe}{ip}." route show";
$output = "";
if ($anvil->Network->is_remote($target))
{
# Remote call
($output, my $error, my $return_code) = $anvil->Remote->call({
debug => $debug,
shell_call => $shell_call,
target => $target,
user => $remote_user,
password => $password,
remote_user => $remote_user,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:error' => $error,
's3:return_code' => $return_code,
}});
}
else
{
# Local call.
($output, my $return_code) = $anvil->System->call({debug => $debug, shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:return_code' => $return_code,
}});
}
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
if ($line =~ /default via (.*?) dev (.*?) proto .*? metric (\d+)/i)
{
my $this_ip = $1;
my $this_interface = $2;
my $metric = $3;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:this_ip' => $this_ip,
's2:this_interface' => $this_interface,
's3:metric' => $metric,
's4:lowest_metric' => $lowest_metric,
}});
if ($metric < $lowest_metric)
{
$lowest_metric = $metric;
$route_interface = $this_interface;
$route_ip = $this_ip;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
lowest_metric => $lowest_metric,
route_interface => $route_interface,
route_ip => $route_ip,
}});
}
}
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
route_interface => $route_interface,
route_ip => $route_ip,
}});
# If I got a route, get the DNS.
if ($route_interface)
{
# I want to build the DNS list from only the interface that is used for routing.
my $in_interface = "";
my $dns_list = "";
my $dns_hash = {};
my $shell_call = $anvil->data->{path}{exe}{nmcli}." dev show";
my $output = "";
if ($anvil->Network->is_remote($target))
{
# Remote call
($output, my $error, my $return_code) = $anvil->Remote->call({
debug => $debug,
shell_call => $shell_call,
target => $target,
user => $remote_user,
password => $password,
remote_user => $remote_user,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:error' => $error,
's3:return_code' => $return_code,
}});
}
else
{
# Local call.
($output, my $return_code) = $anvil->System->call({debug => $debug, shell_call => $shell_call});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
's1:output' => $output,
's2:return_code' => $return_code,
}});
}
foreach my $line (split/\n/, $output)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }});
if ($line =~ /GENERAL.DEVICE:\s+(.*)$/)
{
$in_interface = $1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { in_interface => $in_interface }});
}
if (not $line)
{
$in_interface = "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { in_interface => $in_interface }});
}
next if $in_interface ne $route_interface;
if ($line =~ /IP4.DNS\[(\d+)\]:\s+(.*)/i)
{
my $order = $1;
my $ip = $2;
$dns_hash->{$order} = $ip;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "dns_hash->{$order}" => $dns_hash->{$order} }});
}
}
foreach my $order (sort {$a cmp $b} keys %{$dns_hash})
{
$dns_list .= $dns_hash->{$order}.", ";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"s1:dns_hash->{$order}" => $dns_hash->{$order},
"s2:dns_list" => $dns_list,
}});
}
$dns_list =~ s/, $//;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { dns_list => $dns_list }});
$anvil->data->{network}{$target}{interface}{$route_interface}{default_gateway} = 1;
$anvil->data->{network}{$target}{interface}{$route_interface}{gateway} = $route_ip;
$anvil->data->{network}{$target}{interface}{$route_interface}{dns} = $dns_list;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"network::${target}::interface::${route_interface}::default_gateway" => $anvil->data->{network}{$target}{interface}{$route_interface}{default_gateway},
"network::${target}::interface::${route_interface}::gateway" => $anvil->data->{network}{$target}{interface}{$route_interface}{gateway},
"network::${target}::interface::${route_interface}::dns" => $anvil->data->{network}{$target}{interface}{$route_interface}{dns},
}});
}
return(0);
}
=head2 get_network
This takes an IP address and subnet and returns the network it belongs too. For example;
my $network = $anvil->Network->get_network({ip => "10.2.4.1", subnet => "255.255.0.0"});
This would set C<< $network >> to C<< 10.2.0.0 >>.
If the network can't be caluclated for any reason, and empty string will be returned.
Parameters;
=head3 ip (required)
This is the IPv4 IP address being calculated.
=head3 subnet (required)
This is the subnet of the IP address being calculated.
=cut
sub get_network
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $network = "";
my $ip = defined $parameter->{ip} ? $parameter->{ip} : "";
my $subnet = defined $parameter->{subnet} ? $parameter->{subnet} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
ip => $ip,
subnet => $subnet,
}});
if (not $ip)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Network->get_network()", parameter => "ip" }});
return("");
}
if (not $subnet)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Network->get_network()", parameter => "subnet" }});
return("");
}
my $block = Net::Netmask->new($ip."/".$subnet);
my $base = $block->base();
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { base => $base }});
if ($anvil->Validate->is_ipv4({ip => $base}))
{
$network = $base;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { network => $network }});
}
return($network);
}
=head2 is_remote
This looks at the C<< target >> and determines if it relates to the local system or not. If the C<< target >> is remote, C<< 1 >> is returned. Otherwise, C<< 0 >> is returned.
if ($anvil->Network->is_remote($target))
{
# Do something remotely
}
else
{
# Do something locally
}
B<< NOTE >>: Unlike most methods, this one does not take a hash reference for the parameters. It takes the string directly.
=cut
sub is_remote
{
my $self = shift;
my $target = shift;
my $anvil = $self->parent;
my $remote = 0;
if (($target) && ($target ne "local") && ($target ne $anvil->_hostname) && ($target ne $anvil->_short_hostname))
{
# It's a remote system
$remote = 1;
}
return($remote);
}
# =head3
#
# Private Functions;
#
# =cut
#############################################################################################################
# Private functions #
#############################################################################################################
1;