#!/usr/bin/perl -w
###############################################################################
# $Id: firewalld.pm 1798488 2017-06-12 16:38:02Z arkurth $
###############################################################################
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################

=head1 NAME

VCL::Module::OS::Linux::firewall::firewalld.pm

=head1 DESCRIPTION

 This module provides VCL support for firewalld-based firewalls.

=cut

###############################################################################
package VCL::Module::OS::Linux::firewall::firewalld;

# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../../../..";

# Configure inheritance
use base qw(VCL::Module::OS::Linux::firewall::iptables);

# Specify the version of this module
our $VERSION = '2.5';

our @ISA;

# Specify the version of Perl to use
use 5.008000;

use strict;
use warnings;
use diagnostics;

use VCL::utils;

###############################################################################

=head1 OBJECT METHODS

=cut

#//////////////////////////////////////////////////////////////////////////////

=head2 initialize

 Parameters  : none
 Returns     : boolean
 Description : 

=cut

sub initialize {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my $arguments = shift || {};
	
	my $computer_name = $self->data->get_computer_hostname();
	
	notify($ERRORS{'DEBUG'}, 0, "initializing " . ref($self) . " object to control $computer_name");
	
	if (!$self->os->service_exists('firewalld')) {
		notify($ERRORS{'DEBUG'}, 0, ref($self) . " object not initialized to control $computer_name, firewalld service does not exist");
		return 0;
	}
	
	if (!$self->os->is_service_enabled('firewalld')) {
		notify($ERRORS{'DEBUG'}, 0, ref($self) . " object not initialized to control $computer_name, firewalld service is not enabled");
		return 0;
	}
	
	if (!$self->os->command_exists('firewall-cmd')) {
		notify($ERRORS{'DEBUG'}, 0, ref($self) . " object not initialized to control $computer_name, firewall-cmd command does not exist");
		return 0;
	}
	
	notify($ERRORS{'DEBUG'}, 0, ref($self) . " object initialized to control $computer_name");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 process_post_load

 Parameters  : none
 Returns     : boolean
 Description : Performs the initial iptables firewall configuration after an
               image is loaded:
               * Performs all of the tasks done by
                 iptables.pm::process_post_load except the pre-VCL 2.5 legacy
                 cleanup tasks
               * Removes the ssh protocol from the public zone

=cut

sub process_post_load {
	my $self = shift;
	if (ref($self) !~ /VCL::Module::OS::Linux::firewall/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my $computer_name = $self->data->get_computer_short_name();
	
	notify($ERRORS{'DEBUG'}, 0, "beginning firewalld post-load configuration on $computer_name");
	
	# Call subroutine in iptables.pm
	return unless $self->SUPER::process_post_load();
	
	# Remove ssh from public zone
	return unless $self->remove_service('public', 'ssh');
	
	$self->save_configuration();
	
	notify($ERRORS{'DEBUG'}, 0, "completed firewalld post-load configuration on $computer_name");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_all_direct_rules

 Parameters  : none
 Returns     : array
 Description : Calls 'firewall-cmd --permanent --direct --get-all-rules' and
               returns an array of strings.

=cut

sub get_all_direct_rules {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --permanent --direct --get-all-rules";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to retrieve all firewalld direct rules on $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve all firewalld direct rules on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# Rules should be in the format:
	# ipv4 filter vcl-pre_capture 0 --jump ACCEPT --protocol tcp --match comment --comment 'VCL: Allow traffic to SSH port 22 from any IP address (2017-04-07 17:19:21)' --match tcp --destination-port 22
	# ipv4 filter INPUT 0 --jump vcl-pre_capture --match comment --comment 'VCL: jump to rules added during the pre-capture stage (2017-04-07 17:19:21)'
	my @rules = grep(/^(ipv4|ipv6|eb)/, @$output);
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved all firewalld direct rules defined on $computer_name:\n" . join("\n", @rules));
	return @rules;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_direct_chain_rules

 Parameters  : $table_name, $chain_name
 Returns     : array
 Description : Calls 'firewall-cmd --permanent --direct --get-rules' and returns
               an array of strings.

=cut

sub get_direct_chain_rules {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($table_name, $chain_name) = @_;
	if (!$table_name) {
		notify($ERRORS{'WARNING'}, 0, "table name argument was not specified");
		return;
	}
	elsif (!$chain_name) {
		notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --permanent --direct --get-rules ipv4 $table_name $chain_name";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to retrieve firewalld direct rules defined for '$chain_name' chain in '$table_name' table on $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve firewalld direct rules defined for '$chain_name' chain in '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	
	# All rule lines should begin with an integer:
	#    0 --jump ACCEPT --source 10.25.7.2 --match comment --comment 'VCL: Allow traffic from management node (2017-04-07 15:36:24)'
	#    1 --jump ACCEPT --source 10.25.7.2
	my @rules = grep(/^\d+/, @$output);
	
	notify($ERRORS{'DEBUG'}, 0, "retrieved firewalld direct rules defined for '$chain_name' chain in '$table_name' table on $computer_name:\n" . join("\n", @rules));
	return @rules;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 save_configuration

 Parameters  : none
 Returns     : boolean
 Description : Calls 'firewall-cmd --reload'.

=cut

sub save_configuration {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --reload";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to reload firewalld configuration on $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to reload firewalld configuration on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return 0;
	}
	else {
		notify($ERRORS{'OK'}, 0, "reloaded firewalld configuration on $computer_name");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 create_chain

 Parameters  : $table_name, $chain_name
 Returns     : boolean
 Description : Creates a new chain. Returns true if the chain was successfully
               created or already exists.

=cut

sub create_chain {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($table_name, $chain_name) = @_;
	if (!defined($table_name)) {
		notify($ERRORS{'WARNING'}, 0, "table name argument was not specified");
		return;
	}
	elsif (!defined($chain_name)) {
		notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --permanent --direct --add-chain ipv4 $table_name $chain_name";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command");
		return;
	}
	elsif (grep(/ALREADY_ENABLED/i, @$output)) {
		notify($ERRORS{'OK'}, 0, "'$chain_name' chain in '$table_name' table already exists on $computer_name");
		return 1;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to create '$chain_name' chain in '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return 0;
	}
	elsif (!grep(/success/, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "potentially failed to create '$chain_name' chain in '$table_name' table on $computer_name, output does not contain 'success', exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return 0;
	}
	else {
		notify($ERRORS{'OK'}, 0, "created '$chain_name' chain in '$table_name' table on $computer_name");
		#$self->save_configuration();
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 remove_direct_chain_rules

 Parameters  : $table_name, $chain_name
 Returns     : boolean
 Description : Flushes (deletes) rules from the specified chain.

=cut

sub remove_direct_chain_rules {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($table_name, $chain_name) = @_;
	if (!defined($table_name)) {
		notify($ERRORS{'WARNING'}, 0, "table name argument was not specified");
		return;
	}
	elsif (!defined($chain_name)) {
		notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# !!! WARNING !!!
	# DON'T USE --remove-rules
	# With firewall-cmd version 0.4.3.2, this option removes rules from ALL direct chains, not just the one specified
	#my $command = "firewall-cmd --permanent --direct --remove-rules ipv4 $table_name $chain_name";
	
	my @rules = $self->get_direct_chain_rules($table_name, $chain_name);
	for my $rule (@rules) {
		# [--permanent] --direct --remove-rule { ipv4 | ipv6 | eb } table chain priority args
		my $command = "firewall-cmd --permanent --direct --remove-rule ipv4 $table_name $chain_name $rule";
		my ($exit_status, $output) = $self->os->execute($command, 0);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command");
			return;
		}
		elsif ($exit_status ne '0') {
			notify($ERRORS{'WARNING'}, 0, "failed to remove rule from '$chain_name' chain in '$table_name' table on $computer_name: '$rule', exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
			return 0;
		}
		else {
			notify($ERRORS{'OK'}, 0, "removed direct rule from '$chain_name' chain in '$table_name' table on $computer_name: '$rule'");
		}
	}
	
	notify($ERRORS{'OK'}, 0, "removed all direct rules from '$chain_name' chain in '$table_name' table on $computer_name");
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 delete_chain

 Parameters  : $table_name, $chain_name
 Returns     : boolean
 Description : Deletes an existing chain. Returns true if the chain was
               successfully deleted or doesn't exist.

=cut

sub delete_chain {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($table_name, $chain_name_argument) = @_;
	if (!defined($table_name)) {
		notify($ERRORS{'WARNING'}, 0, "table name argument was not specified");
		return;
	}
	elsif (!defined($chain_name_argument)) {
		notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified");
		return;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	my @chains_deleted;
	my @chain_names = $self->get_table_chain_names($table_name);
	for my $chain_name (@chain_names) {
		if ($chain_name !~ /^$chain_name_argument$/) {
			next;
		}
		
		# Delete all rules which reference the chain being deleted or else the chain can't be deleted
		# Do this BEFORE checking if the chain exists to clean up leftover references in direct.xml
		if (!$self->delete_chain_references($table_name, $chain_name)) {
			notify($ERRORS{'WARNING'}, 0, "unable to delete '$chain_name' chain from '$table_name' table on $computer_name, failed to delete all rules which reference the chain prior to deletion");
			return;
		}
	
		$self->remove_direct_chain_rules($table_name, $chain_name) || return;
		
		my $command = "firewall-cmd --permanent --direct --remove-chain ipv4 $table_name $chain_name";
		my ($exit_status, $output) = $self->os->execute($command, 0);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command");
			return;
		}
		elsif (grep(/NOT_ENABLED/i, @$output)) {
			notify($ERRORS{'OK'}, 0, "'$chain_name' chain in '$table_name' does not exist on $computer_name");
		}
		elsif ($exit_status ne '0') {
			notify($ERRORS{'WARNING'}, 0, "failed to delete '$chain_name' chain in '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
			return 0;
		}
		elsif (!grep(/success/, @$output)) {
			notify($ERRORS{'WARNING'}, 0, "potentially failed to delete '$chain_name' chain in '$table_name' table on $computer_name, output does not contain 'success', exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		}
		else {
			notify($ERRORS{'OK'}, 0, "deleted '$chain_name' chain in '$table_name' table on $computer_name");
			#$self->save_configuration();
		}
		
		if (!$self->clean_direct_xml($table_name . '.*jump\s+' . $chain_name)) {
			return;
		}
		
		notify($ERRORS{'OK'}, 0, "deleted '$chain_name' chain from '$table_name' table on $computer_name");
		push @chains_deleted, $chain_name;
	}
	
	if (!@chains_deleted) {
		notify($ERRORS{'DEBUG'}, 0, "no chains exist in '$table_name' table on $computer_name matching argument: '$chain_name_argument'");
	}
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 clean_direct_xml

 Parameters  : $regex_pattern
 Returns     : boolean
 Description : 

=cut

sub clean_direct_xml {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my $regex_pattern = shift;
	if (!defined($regex_pattern)) {
		notify($ERRORS{'WARNING'}, 0, "regex pattern argument was not supplied");
		return;
	}
	
	$self->os->firewall->save_configuration();
	
	my @keep_lines;
	my @prune_lines;
	my $file_path = '/etc/firewalld/direct.xml';
	my @lines = $self->os->get_file_contents($file_path);
	for my $line (@lines) {
		if ($line =~ /$regex_pattern/i) {
			push @prune_lines, $line;
		}
		else {
			push @keep_lines, $line;
		}
	}
	
	if (@prune_lines) {
		my $updated_contents = join("\n", @keep_lines);
		notify($ERRORS{'DEBUG'}, 0, "pruning the following lines from $file_path matching pattern: '$regex_pattern'\n" . join("\n", @prune_lines) . "\nnew file contents:\n$updated_contents");
		return $self->os->create_text_file($file_path, $updated_contents);
	}
	else {
		notify($ERRORS{'DEBUG'}, 0, "no lines were pruned from $file_path matching pattern: '$regex_pattern'");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _insert_rule

 Parameters  : $table_name, $chain_name, $argument_string
 Returns     : boolean
 Description : Executes the command to insert a firewalld direct rule. This is a
               helper subroutine and should only be called by
               iptable.pm::insert_rule.

=cut

sub _insert_rule {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($table_name, $chain_name, $argument_string) = @_;
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --permanent --direct --add-rule ipv4 $table_name $chain_name 0 $argument_string";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to add direct firewalld rule to $chain_name chain in $table_name table on $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to add direct firewalld rule to $chain_name chain in $table_name table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return 0;
	}
	else {
		notify($ERRORS{'OK'}, 0, "added direct firewalld rule to $chain_name chain in $table_name table on $computer_name, command: $command, output:\n" . join("\n", @$output));
		#$self->save_configuration();
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 _delete_rule

 Parameters  : $table_name, $chain_name, $rule_specification_string
 Returns     : boolean
 Description : Deletes a firewalld direct rule. This should only used as a
               helper subroutine.

=cut

sub _delete_rule {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($table_name, $chain_name, $rule_specification_string) = @_;
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --permanent --direct --remove-rule ipv4 $table_name $chain_name 0 $rule_specification_string";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete firewalld direct rule on $computer_name: $command");
		return;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to delete firewalld direct rule on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "deleted firewalld direct rule on $computer_name, command: '$command', output:\n" . join("\n", @$output));
		#$self->save_configuration();
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 remove_service

 Parameters  : $zone_name, $service
 Returns     : boolean
 Description : 

=cut

sub remove_service {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $service) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($service)) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	$service = 'tcp' unless $service;
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# [--permanent] [--zone=zone] --remove-service=serviceid[-serviceid]/service
	#            Remove the service from zone. If zone is omitted, default zone will be used. This option can be specified
	#            multiple times.
	
	my $command = "firewall-cmd --permanent --zone=$zone_name --remove-service=$service";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to remove $service service from '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;

	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to remove $service service from '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	elsif (grep(/NOT_ENABLED/, @$output)) {
		notify($ERRORS{'OK'}, 0, "$service service has not been added to '$zone_name' zone on $computer_name");
	}
	else {
		notify($ERRORS{'OK'}, 0, "removed $service service from '$zone_name' zone on $computer_name");
	}
	
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 create_zone

 Parameters  : $zone_name
 Returns     : boolean
 Description : Creates a new firewalld zone on the computer.

=cut

sub create_zone {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name) = @_;
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --permanent --new-zone=$zone_name";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to create '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if (grep(/NAME_CONFLICT/, @$output)) {
		# Error: NAME_CONFLICT: new_zone(): 'vcl-test'
		notify($ERRORS{'OK'}, 0, "'$zone_name' zone already exists on $computer_name");
		return 1;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to create '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "created '$zone_name' zone on $computer_name");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 delete_zone

 Parameters  : $zone_name
 Returns     : boolean
 Description : Deletes a firewalld zone from the computer.

=cut

sub delete_zone {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name) = @_;
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --permanent --delete-zone=$zone_name";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if (grep(/INVALID_ZONE/, @$output)) {
		# Error: INVALID_ZONE: vcl-test
		notify($ERRORS{'OK'}, 0, "'$zone_name' zone does not exist on $computer_name");
		return 1;
	}
	elsif ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to delete '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "deleted '$zone_name' zone on $computer_name");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 get_zone_info

 Parameters  : $zone_name
 Returns     : hash reference
 Description : Retrieves information about a firewalld zone from the computer
               and constructs a hash reference:
                  {
                    "forward-ports" => "",
                    "icmp-block-inversion" => "no",
                    "icmp-blocks" => "",
                    "interfaces" => "",
                    "masquerade" => "no",
                    "ports" => "",
                    "protocols" => "",
                    "rich rules" => "",
                    "services" => "",
                    "sourceports" => "",
                    "sources" => "",
                    "target" => "default"
                  }

=cut

sub get_zone_info {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name) = @_;
	my $computer_name = $self->data->get_computer_hostname();
	
	my $command = "firewall-cmd --info-zone $zone_name";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if ($exit_status ne '0' || grep(/Error:/, @$output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve info for '$zone_name' zone from $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	
	# vcl-test
	#   target: default
	#   icmp-block-inversion: no
	#   interfaces:
	#   sources:
	#   services:
	#   ports:
	#   protocols:
	#   masquerade: no
	#   forward-ports:
	#   sourceports:
	#   icmp-blocks:
	#   rich rules:

	my $zone_info = {};
	for my $line (@$output) {
		my ($property, $value) = $line =~ /\s*(\S[^:]+)\s*:\s*(.*)/g;
		if (!defined($property)) {
			notify($ERRORS{'DEBUG'}, 0, "ignoring line: '$line'") if ($line !~ /^$zone_name/);
			next;
		}
		$zone_info->{$property} = $value;
	}
	
	notify($ERRORS{'OK'}, 0, "retrieved info for '$zone_name' zone on $computer_name:\n" . format_data($zone_info));
	return $zone_info;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 set_zone_target

 Parameters  : $zone_name, $target
 Returns     : boolean
 Description : Sets the target for a firewalld zone.

=cut

sub set_zone_target {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $target) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($target)) {
		notify($ERRORS{'WARNING'}, 0, "target argument was not supplied");
		return;
	}
	elsif ($target !~ /^(ACCEPT|DROP|REJECT)$/i) {
		notify($ERRORS{'WARNING'}, 0, "target argument is not valid: $target, it must be 'ACCEPT', 'DROP', or 'REJECT'");
		return;
	}
	$target = uc($target);
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# --permanent [--zone=zone] --set-target=target
	#           Set the target of a permanent zone.  target is one of: default, ACCEPT, DROP, REJECT
	my $command = "firewall-cmd --permanent --zone=$zone_name --set-target=$target";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to set target of '$zone_name' zone to '$target' on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to set target of '$zone_name' zone to '$target' on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	else {
		notify($ERRORS{'OK'}, 0, "set target of '$zone_name' zone to '$target' on $computer_name");
		return 1;
	}
}

#//////////////////////////////////////////////////////////////////////////////

=head2 add_source

 Parameters  : $zone_name, $source
 Returns     : boolean
 Description : 

=cut

sub add_source {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $source) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($source)) {
		notify($ERRORS{'WARNING'}, 0, "source argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# [--permanent] [--zone=zone] --add-source=source[/mask]|MAC|ipset:ipset
	
	my $command = "firewall-cmd --permanent --zone=$zone_name --add-source=$source";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to add '$source' source to '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to add source to '$zone_name' zone on $computer_name: $source, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	elsif (grep(/ALREADY_ENABLED/, @$output)) {
		# Warning: ALREADY_ENABLED: 10.1.2.3
		notify($ERRORS{'OK'}, 0, "source was previously added to '$zone_name' zone on $computer_name: $source");
	}
	else {
		notify($ERRORS{'OK'}, 0, "added source to '$zone_name' zone on $computer_name: $source");
	}
	
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 remove_source

 Parameters  : $zone_name, $source
 Returns     : boolean
 Description : 

=cut

sub remove_source {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $source) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($source)) {
		notify($ERRORS{'WARNING'}, 0, "source argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# [--permanent] --remove-source=source[/mask]|MAC|ipset:ipset
	
	my $command = "firewall-cmd --permanent --zone=$zone_name --remove-source=$source";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to remove '$source' source from '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to remove source from '$zone_name' zone on $computer_name: $source, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	elsif (grep(/NOT_ENABLED/, @$output)) {
		notify($ERRORS{'OK'}, 0, "source is not specified in '$zone_name' zone on $computer_name: $source");
	}
	else {
		notify($ERRORS{'OK'}, 0, "removed source from '$zone_name' zone on $computer_name: $source");
	}
	
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 add_interface

 Parameters  : $zone_name, $interface_name
 Returns     : boolean
 Description : 

=cut

sub add_interface {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $interface_name) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($interface_name)) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# [--permanent] [--zone=zone] --add-interface=interface
	
	my $command = "firewall-cmd --permanent --zone=$zone_name --add-interface=$interface_name";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to add '$interface_name' interface to '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to add interface to '$zone_name' zone on $computer_name: $interface_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	elsif (grep(/already bound/, @$output)) {
		# The interface is under control of NetworkManager and already bound to 'public'
		notify($ERRORS{'OK'}, 0, "interface is already bound to '$zone_name' zone on $computer_name: $interface_name");
	}
	else {
		notify($ERRORS{'OK'}, 0, "bound interface to '$zone_name' zone on $computer_name: $interface_name");
	}
	
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 add_port

 Parameters  : $zone_name, $port, $protocol (optional)
 Returns     : boolean
 Description : 

=cut

sub add_port {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $port, $protocol) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($port)) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	$protocol = 'tcp' unless $protocol;
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# [--permanent] [--zone=zone] --add-port=portid[-portid]/protocol [--timeout=timeval]
	
	my $command = "firewall-cmd --permanent --zone=$zone_name --add-port=$port/$protocol";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to add port $port/$protocol to '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to add port $port/$protocol to '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	elsif (grep(/ALREADY_ENABLED/, @$output)) {
		notify($ERRORS{'OK'}, 0, "port $port/$protocol was previously added to '$zone_name' zone on $computer_name");
	}
	else {
		notify($ERRORS{'OK'}, 0, "added port $port/$protocol to '$zone_name' zone on $computer_name");
	}
	
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 remove_port

 Parameters  : $zone_name, $port, $protocol (optional)
 Returns     : boolean
 Description : 

=cut

sub remove_port {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $port, $protocol) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($port)) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	$protocol = 'tcp' unless $protocol;
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# [--permanent] [--zone=zone] --remove-port=portid[-portid]/protocol
	my $command = "firewall-cmd --permanent --zone=$zone_name --remove-port=$port/$protocol";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to remove port $port/$protocol from '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;

	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to remove port $port/$protocol from '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	elsif (grep(/NOT_ENABLED/, @$output)) {
		notify($ERRORS{'OK'}, 0, "port $port/$protocol has not been added from '$zone_name' zone on $computer_name");
	}
	else {
		notify($ERRORS{'OK'}, 0, "removed port $port/$protocol from '$zone_name' zone on $computer_name");
	}
	
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

=head2 add_service

 Parameters  : $zone_name, $service
 Returns     : boolean
 Description : 

=cut

sub add_service {
	my $self = shift;
	if (ref($self) !~ /VCL::Module/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}
	
	my ($zone_name, $service) = @_;
	if (!defined($zone_name)) {
		notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied");
		return;
	}
	elsif (!defined($service)) {
		notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied");
		return;
	}
	$service = 'tcp' unless $service;
	
	my $computer_name = $self->data->get_computer_hostname();
	
	# [--permanent] [--zone=zone] --add-service=service [--timeout=timeval]
	my $command = "firewall-cmd --permanent --zone=$zone_name --add-service=$service";
	my ($exit_status, $output) = $self->os->execute($command, 0);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to execute command to add $service service to '$zone_name' zone on $computer_name: $command");
		return;
	}
	
	# Remove color controls
	(my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g;
	
	if ($exit_status ne '0') {
		notify($ERRORS{'WARNING'}, 0, "failed to add $service service to '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string");
		return;
	}
	elsif (grep(/ALREADY_ENABLED/, @$output)) {
		notify($ERRORS{'OK'}, 0, "$service service was previously added to '$zone_name' zone on $computer_name");
	}
	else {
		notify($ERRORS{'OK'}, 0, "added $service service to '$zone_name' zone on $computer_name");
	}
	
	return 1;
}

#//////////////////////////////////////////////////////////////////////////////

1;
__END__

=head1 SEE ALSO

L<http://cwiki.apache.org/VCL/>

=cut
