package Cpanel::Easy;

# cpanel - Cpanel/Easy.pm                         Copyright(c) 2016 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

#### if methods here would make sense elsewhere put them in another::class and the use base 'another::class'; ##
#### may even want to:  Cpanel::BaseMethods that has a collection of really really base level functions ##

use lib '/var/cpanel/perl/easy', '/usr/local/cpanel', '/usr/local/cpanel/scripts';
use strict;
use warnings;
no warnings qw(redefine);

# base::ball is a (poorly named via word play/pun) shortcut that use()s base recursively to avoid having to maintain a large 'use base' list of modules when we add or remove one.
# Typically what it does is a very bad idea but since we control the modules in question it simplifies things here.
use Cpanel::CPAN::base::ball qw(Cpanel::Easy::Utils);

use Cpanel::OSSys;
use Unix::PID;
use IPC::Open3;
use Carp;
use File::Copy::Recursive;
use Cpanel::Config           ();
use Cpanel::SafeDir          ();
use Cpanel::Sys              ();
use Cpanel::Rand             ();
use Cpanel::Version::Compare ();
use Cpanel::Version::Tiny    ();

use BSD::Resource qw(setrlimit RLIM_INFINITY RLIMIT_RSS RLIMIT_AS);

use constant ON_BUT_INVALID => 1;
use constant ON_AND_VALID   => 2;

{
    my $def = {
        'state_file_expire_seconds' => 86400,
        'httpupdate_uri'            => '/cpanelsync/easy',

        # if prefs ever gets to be a complex hash change load_prefs_hashref()
        # to do a merge that can handle it (IE currently its a simple merge)
        'default_prefs' => {
            'verbose'       => 1,
            'notify_cpanel' => 1,
        },
        'prefs_details' => {

            #            'verbose'       => {
            #                'name'    => 'Verbose',
            #                'reverse' => 0,
            #            },
            'notify_cpanel' => {
                'name'    => 'Report Errors to cPanel',
                'reverse' => 0,
            },
        },
        'opt_mod_dir' => '/var/cpanel/perl/easy',
    };

    sub new {
        my ( $class, $args_ref ) = @_;
        my $self = ref $class ? $class : bless {}, $class;

        $self->{'POSIX::uname'} = [ Cpanel::OSSys::uname() ];
        $self->{'cpu_bits'} = $self->{'POSIX::uname'}->[4] =~ m{64} ? 64 : 32;

        $self->init_cache();
        $self->increase_memory_limits();

        $self->{'_'}{'cPanel::MemTest required MB'} = 90 if !defined $self->{'_'}{'cPanel::MemTest required MB'};

        $self->{'_'}{'post_fyi_info'}         = '';
        $self->{'_'}{'post_failure_fyi_info'} = '';
        $self->{'_'}{'debug_info_for_report'} = '';

        # TODO: Adjust this to more accurately reflect the amount of disk space required for the selected options.
        # A full build with all options enabled consumes 1.5GB in the build directory.
        $self->{'_'}{'cPanel::DiskTest required MB'} = 1024 if !defined $self->{'_'}{'cPanel::DiskTest required MB'};

        # This number is no longer used in the EasyApache display, but it is also added to the
        # Apache binaries and may be used by third parties to check for EA3 functionality.
        # In the future they should check the ea3 version instead
        $self->{'revision'} = '9999';

        $self->{'minimum_version'} = {
            'EDGE'    => { 'revision' => 17825, },
            'CURRENT' => { 'revision' => 17825, },
            'RELEASE' => { 'revision' => 17825, },
            'STABLE'  => { 'revision' => 17825, },
        };

        if ( $self->has_cloudlinux_support() ) {
            $self->{'revision'} .= ' +cloudlinux';
        }

        foreach my $dft ( keys %{$def} ) {
            next if exists $self->{$dft};
            $self->{$dft} = $def->{$dft};
        }

        foreach my $key ( keys %{$args_ref} ) {
            next if exists $self->{$key};
            $self->{$key} = $args_ref->{$key};
        }

        # Update httpupdate_uri with correct path so that tarballs (containing
        # 3rd-party source code) can download from same source as the perl
        # modules (as pulled by /usr/local/cpanel/scripts/cpanel_easy_sanity_check).
        #
        # NOTE: This regex should match the one in /usr/local/cpanel/scripts/cpanel_easy_sanity_check!!!
        # However, it has only been available since 11.38.2.7
        if ( Cpanel::Version::Compare::compare( $Cpanel::Version::Tiny::VERSION_BUILD, '>=', '11.38.2.7' ) ) {
            my $tier = $self->get_param('tier');

            # if tier not defined, keep the default
            if ($tier) {
                if ( $tier =~ /^easy(?:\-(?:\w+\-)?\d+\.\d+\.\d+)?$/ ) {
                    $self->{'httpupdate_uri'} = sprintf( '/cpanelsync/%s', $tier );
                }
                else {
                    croak "Incorrect 'tier' argument passed to EasyApache";
                }
            }
        }

        lib->import( $self->{'opt_mod_custom_dir'} )
          if $self->{'opt_mod_custom_dir'};

        $self->ensure_valid_obj_key( 'lang_obj', 'maketext', $args_ref, 'Cpanel::CPAN::Locale::Maketext::Pseudo' );

        # can't default to anything since it'd be wrong over half the time, so we force it to be specified
        croak 'param_obj needs to have a param method!' if ( !ref $self->{'param_obj'} || !$self->{'param_obj'}->can('param') );

        # same for ui_obj
        croak 'ui_obj needs to have an isa_ui method!' if ( !ref $self->{'ui_obj'} || !$self->{'ui_obj'}->can('isa_ui') );

        if ( $self->get_param('perldoc') ) {
            $self->{'ui_obj'}->perldoc( $self, '/var/cpanel/perl/easy/Cpanel/Easy.pm' );
            exit;    # just in case :)
        }

        # set these $self keys's to either a namespace safe string or ''
        $self->{'getos'}                = Cpanel::Sys::getos();
        $self->{'getreleaseversion'}    = [ Cpanel::Sys::getreleaseversion() ];
        $self->{'getos_releaseversion'} = $self->{'getreleaseversion'}->[0];
        $self->{'hostname'}             = Cpanel::Sys::gethostname() || 'localhost';

        # Below: its here if we want it, any good way to consistently use this (See get_std_variation_of_ns_list) ?
        # $self->{'getos_version'} = '';

        $self->{'generic_os'} = $self->{'POSIX::uname'}->[0] =~ m{Linux}i ? 'linux' : lc( $self->{'POSIX::uname'}->[0] );

        # Below: its here if we want it, any good way to consistently use this (See get_std_variation_of_ns_list) ?
        # $self->{'generic_os_version'} = ''; # here if we want it, any good way to consistently use this (See get_std_variation_of_ns_list)?

        $self->{'patch_src_dir'} = $self->{'opt_mod_src_dir'} . '/cppatch';

        foreach my $dir (
            $self->{'base_dir'},     $self->{'opt_mod_src_dir'},    $self->{'patch_src_dir'}, $self->{'rawenv_dir'}, $self->{'rawopts_dir'},
            $self->{'path_md5_dir'}, $self->{'opt_mod_custom_dir'}, '/opt',                   $self->{'runlog_dir'},
          ) {
            Cpanel::SafeDir::safemkdir($dir);
        }

        open $self->{'log_fh'}, '>', $self->{'log_file'};
        chmod 0600, $self->{'log_file'};

        # log or die or ???
        #   [ See print_to_log() for same question but with "reopen in append mode since its apparently is      not open anymore"]
        $self->cleanup_log_dir();

        $self->optlib_3109_check();

        if ( !-e $self->{'profile_main'} ) {
            File::Copy::Recursive::fcopy( $self->{'profile_default'}, $self->{'profile_main'}, )
              or $self->log_warn( [ q{fcopy '[_1]', [_2] failed: [_3]}, $self->{'profile_default'}, $self->{'profile_main'}, $!, ] );
        }

        $self->load_prefs_hashref();
        $self->do_env_details( 'Before RawENV', 1 );
        $self->set_rawenv() if $self->can('set_rawenv');
        $self->load_state_from_file();
        if ( ref $self->{'state'}{'custom_opt_mods_for_main_include_list'} eq 'ARRAY' ) {
            push @{ $self->{'state_config'}{'include'} }, @{ $self->{'state'}{'custom_opt_mods_for_main_include_list'} };
        }

        my %uniq;
        @{ $self->{'state_config'}{'include'} } = map { $uniq{$_}++ == 0 ? $_ : () } @{ $self->{'state_config'}{'include'} };

        $self->{'_'}{'cpsources'} = Cpanel::Config::loadcpsources();

        if ( -e $self->{'last_failed_file'} ) {
            $self->{'last_failed'} = $self->deserialize( $self->{'last_failed_file'} );
        }

        $self->{'use_sha'} = undef;
        $self->{'use_md5'} = undef;

        # allow use of MD5 if this touchfile exists. It is managed by a Tweak Setting in >= 11.50;
        # if SHA512 checksums are defined in targz.yaml, SHA512 will be used; even if $self->{'use_md5'} is set;
        if ( -e '/var/cpanel/allow_weak_checksums' ) {
            $self->{'use_md5'} = 1;
        }

        return $self;
    }

    sub ensure_valid_obj_key {
        my ( $self, $key, $meth, $args_ref, $ns ) = @_;

        if ( $self->is_namespace( $args_ref->{$key} ) ) {
            eval qq{ use $args_ref->{ $key }; };
            $self->{$key} = $args_ref->{$key}->new() if !ref $self->{$key};
            $self->{$key} = $args_ref->{$key}->new() if !$self->{$key}->can($meth);
        }

        if ( $self->{$key} ) {
            return if $self->{$key}->can($meth);
        }

        return if !$self->is_namespace($ns);

        eval qq{use $ns;};

        $self->{$key} = $ns->new() if !ref $self->{$key};
        $self->{$key} = $ns->new() if !$self->{$key}->can($meth);
    }

    sub run {
        my ( $self, $skip_help ) = @_;

        exit if $self->get_param('init-only');

        # TODO: if Cpanel::Locale -> load lexicons

        $self->{'_'}{'in-pretests'} = 1;
        $self->process_action();    # this may exit ...
        $self->process_help() if !$skip_help;    # this may exit ...
        $self->pid_setup();
        $self->php4_conflict_check();
        $self->memory_check()     if !$self->get_param('skip-memtest');
        $self->disk_space_check() if !$self->get_param('skip-disktest');
        if ( $self->get_param('only-tarballs-and-pkgs') ) {
            $self->_header();
            if ( chdir $self->{'opt_mod_src_dir'} ) {
                print "<pre>\n" if ref $self->{'ui_obj'} eq 'Cpanel::Easy::Apache::UI::HTML';
                $self->process_tarballs_and_pkgs();
                print "</pre>\n" if ref $self->{'ui_obj'} eq 'Cpanel::Easy::Apache::UI::HTML';
            }
            else {
                print $self->print_alert( q{Could not chdir into '[_1]': [_2]}, $self->{'opt_mod_src_dir'}, $! );
            }
            $self->_footer();
            exit;
        }

        if ( !$ENV{'SCRIPT_FILENAME'} && !$ENV{'HTTP_HOST'} ) {
            $self->ulimit_a_info();
        }
        $self->{'_'}{'cpanel_version_up_to_date'} = $self->get_param('skip-cpanel-version-check') ? 1 : $self->cpanel_version_check();
        $self->{'_'}{'in-pretests'} = 0;

        # if we havn't exit()ed yet:

        if ( defined $self->get_param('test-branch') ) {
            $self->print_alert_color( 'yellow', "The --test-branch command line switch is obsolete. Ignoring." );
        }

        my $test_branch_path = '/var/cpanel/use_easy_test_branch';
        if ( -e $test_branch_path ) {
            $self->print_alert_color( 'yellow', "Use of $test_branch_path is obsolete. Ignoring." );
        }

        # Alter the ENV so that /usr/bin/perl is the default Perl,
        # and will be used on the shebang line of the apxs and dbmanage scripts
        local $ENV{'PATH'} = '/usr/bin:' . $ENV{'PATH'};

        my $rc;

        if ( $self->get_param('build') ) {
            $rc = $self->{'ui_obj'}->install_profile( $self, $self->get_param('profile') );
        }
        else {
            if ( $self->configure_profile( $self->get_param('profile') ) ) {
                $self->set_param( 'profile', $self->{'profile_main'} );    # mdifications saved to _main already, lets use that one to build
                $rc = $self->{'ui_obj'}->install_profile( $self, $self->get_param('profile') );
            }
        }
        $self->_footer();                                                  # only does anything if the UI can(), it hasn't been called already, and _header() was called

        # TODO: if Cpanel::Locale -> unload lexicons

        exit 1 unless $rc;

        # Falls thru to "normal" exit
    }

    sub load_prefs_hashref {
        my ($self) = @_;
        my %tmp = %{ $self->deserialize( $self->{'prefs_file'} ) };

        # workaround for Case 31369
        if ( $self->get_param('makecpphp') ) {
            delete $tmp{'always_do_the_latest_phps'};
        }

        $self->{'_'}{'prefs'} = { %{ $self->{'default_prefs'} }, %tmp, };

        foreach my $prf ( keys %{ $self->{'_'}{'prefs'} } ) {
            $self->{'_'}{'prefs'}{$prf} = $self->get_param($prf)
              if defined $self->get_param($prf);
        }
    }

    sub save_prefs_hashref {
        my ($self) = @_;

        my %save = ( %{ $self->{'default_prefs'} }, %{ $self->{'_'}{'prefs'} }, );

        for my $pref ( keys %{ $self->{'prefs_details'} } ) {
            delete $save{$pref} if $self->{'prefs_details'}{$pref}{'no_save'};
        }

        my @return = $self->serialize( $self->{'prefs_file'}, \%save );
        $self->load_prefs_hashref();

        return @return;
    }

    sub get_basic_sig_handler {
        my ( $self, $signame, $exit, $skipcleanup, @maketext ) = @_;

        return sub {
            if ( $self->{'signals_seen'}{$signame} ) {
                print $self->{'lang_obj'}->maketext( q{Caught SIG '[_1]' again ([_2])...}, $signame, $self->{'signals_seen'}{$signame} + 1 ), "\n";

                $self->{'signals_seen'}{$signame}++;
                $self->update_runlog_entry( { 'SIG' => { $signame => $self->{'signals_seen'}{$signame}, }, } );
                $self->_footer();
                exit if $exit;
            }
            else {
                print $self->{'lang_obj'}->maketext( q{Caught SIG '[_1]', cleaning up...}, $signame ), "\n";
                $self->print_alert(@maketext) if @maketext;

                $self->{'signals_seen'}{$signame}++;
                $self->update_runlog_entry( { 'SIG' => { $signame => $self->{'signals_seen'}{$signame}, }, } );

                $self->{'_'}{'restore_backup'}++;
                if ( !$skipcleanup ) {
                    $self->cleanup if $self->can('cleanup');
                }
                $self->_footer();
                exit if $exit;
            }
        };
    }

    sub pid_setup {
        my ($self) = @_;

        $self->{'pid_obj'} = Unix::PID->new()
          if ref $self->{'pid_obj'} ne 'Unix::PID';

        return if !$self->{'pid_file'};
        return if $self->process_action(1);    # check if we are going to be doing valid "action", if so we skip the pid check

        $self->{'pid_obj'}->{'unlink_end_use_current_pid_only'} = 1;
        my $rc = $self->{'pid_obj'}->pid_file( $self->{'pid_file'} );

        if ( !$rc ) {
            $self->{'ui_obj'}->pid_file_failed( $self, $rc );
            $self->_footer();
            exit;
        }
    }

    sub configure_profile {
        my ( $self, $profile ) = @_;
        $profile = $self->determine_profile($profile);

        my $profile_setup = $self->deserialize($profile);

        if ( !keys %{$profile_setup} ) {

            $self->print_to_log_and_screen( $self->maketext( q{Did not get any data from '[_1]'. See '[_2]' for possible reasons why.}, $profile, '/usr/local/cpanel/logs/error_log', ), );
            return;
        }

        local $self->{'rc'}{'configure_profile'} = 1;

        return $self->{'ui_obj'}->configure_profile( $self, $profile, $profile_setup );
    }

    sub save_profile {
        my ( $self, $content, $profile ) = @_;
        $profile = $self->determine_profile( $profile, 1 );

        my $path = '/var/cpanel/easy/apache/profile/custom';
        opendir my $prof_dh, $self->{'profile_custom_dir'} or return;
        my @profiles = grep /\.yaml$/, map { "$path/$_" } readdir($prof_dh);
        closedir $prof_dh;

        my %seen_names;
        for my $yaml (@profiles) {
            last if $yaml eq $profile;

            my $pr_hr = $self->deserialize($yaml);
            if ( $pr_hr->{'_meta'}{'name'} =~ m{ \s [(] (\d+) [)] \z }xms ) {
                my $dig = $1;
                my $cpy = $pr_hr->{'_meta'}{'name'};
                $cpy =~ s{ \s [(] (\d+) [)] \z }{}xms;
                if ( exists $seen_names{$cpy} ) {
                    $seen_names{$cpy} = $dig if $dig > $seen_names{$cpy};
                }
                else {
                    $seen_names{$cpy} = $dig;
                }
            }
            else {
                if ( exists $seen_names{ $pr_hr->{'_meta'}{'name'} } ) {

                    # $seen_names{ $pr_hr->{'_meta'}{'name'} }++;
                }
                else {
                    $seen_names{ $pr_hr->{'_meta'}{'name'} } = 0;
                }
            }
        }

        my $cpy = $content->{'_meta'}{'name'} || '';    # Avoid "Use of uninitialized value" below
        $cpy =~ s{ \s [(] (\d+) [)] \z }{}xms;

        if ( exists $seen_names{$cpy} ) {
            my $num = $seen_names{$cpy} + 1;
            $content->{'_meta'}{'name'} = "$cpy ($num)";
        }

        $self->profile_manual_edit_fixup($content);     # no $force_working_profile here

        return $self->serialize( $profile, $content );  # and/or $self->{'working_profile'} w/ $content ??
    }

    sub profile_revision_check {
        my ( $self, $profile_hr, $abs_path_custom ) = @_;

        $self->print_alert("DEBUG: 1 - checking profile revision ($profile_hr->{'_meta'}{'revision'})") if $self->get_param('debug-profile-revision');
        $abs_path_custom = $self->determine_profile() if !$abs_path_custom;    # && $self->get_get_param('profile') =~ m{ \A cpanel [_] \w+ \z }xms;

        my $on_disk_hr = $self->deserialize($abs_path_custom);
        $profile_hr->{'_meta'}{'revision'} = $on_disk_hr->{'_meta'}{'revision'} if !$profile_hr->{'_meta'}{'revision'};

        if ( !$profile_hr->{'_meta'}{'revision'} ) {

            # saving as a new profile, need to get it from parent

            my $starting_profile = $self->determine_profile();
            $self->print_alert( q{DEBUG: attmepting to fetch revision from '[_1]'}, $starting_profile ) if $self->get_param('debug-profile-revision');
            my $starting_hr = $self->deserialize($starting_profile);
            $profile_hr->{'_meta'}{'revision'} = $starting_hr->{'_meta'}{'revision'};
        }

        $self->print_alert("DEBUG: 2 - checking profile revision ($profile_hr->{'_meta'}{'revision'})") if $self->get_param('debug-profile-revision');

        if ( eval { require Cpanel::CPAN::Data::Compare; } ) {
            $self->print_alert(" DEBUG Cpanel::CPAN::Data::Compare") if $self->get_param('debug-profile-revision');

            if ( !Cpanel::CPAN::Data::Compare::Compare( $on_disk_hr, $profile_hr, { ignore_hash_keys => ['_meta'] } ) ) {
                $self->print_alert(" DEBUG  looks like it changed") if $self->get_param('debug-profile-revision');

                # looks like it changed:
                $profile_hr->{'_meta'}{'revision'} = Cpanel::Rand::getranddata(64);
            }
        }
        else {

            # don't have a new enough build for Cpanel::CPAN::Data::Compare so try the CPAN one
            if ( eval { require Data::Compare } ) {
                $self->print_alert(" DEBUG Data::Compare") if $self->get_param('debug-profile-revision');
                my $on_disk_hr = $self->deserialize($abs_path_custom);
                if ( !Data::Compare::Compare( $on_disk_hr, $profile_hr, { ignore_hash_keys => ['_meta'] } ) ) {
                    $self->print_alert(" DEBUG  looks like it changed") if $self->get_param('debug-profile-revision');

                    # looks like it changed:
                    $profile_hr->{'_meta'}{'revision'} = Cpanel::Rand::getranddata(64);
                }
            }
            else {
                $self->print_alert( q{Please install the '[_1]' perl module.}, 'Data::Compare' );
                $profile_hr->{'_meta'}{'revision'} = Cpanel::Rand::getranddata(64);
                $profile_hr->{'_meta'}{'revision'} .= ' no Data::Compare';
            }
        }

        if ( !$profile_hr->{'_meta'}{'revision'} ) {
            $self->print_alert(" DEBUG  looks like it was not set, setting") if $self->get_param('debug-profile-revision');
            $profile_hr->{'_meta'}{'revision'} = Cpanel::Rand::getranddata(64);
        }

        $self->print_alert("DEBUG END RESULT ($profile_hr->{'_meta'}{'revision'})") if $self->get_param('debug-profile-revision');
    }

    sub verify_and_update_profile_main {
        my ( $self, $profile_hr ) = @_;

        # the "numbered" profile_hr would typically be resaved so that external apps could load it and check for 2 (IE like suphp)

      NS:
        foreach my $ns ( keys %{$profile_hr} ) {
            next NS if !$self->get_ns_value_from_profile( $ns, $profile_hr );

            my $easyconfig = $self->get_easyconfig_hr_from_ns_variations($ns);
            my $value =
                 $self->dependency_ok( $easyconfig, $ns )
              && $self->dry_run_ok( $easyconfig, $ns )
              ? ON_AND_VALID
              : ON_BUT_INVALID;

            $self->set_ns_value_in_profile( $ns, $profile_hr, $value );
        }

        return $self->save_profile( $profile_hr, $self->{'profile_main'} );
    }

    sub dry_run_ok {
        my ( $self, $opt, $ns ) = @_;
        return 1 if ref $opt->{'dryrun'} ne 'HASH';

        foreach my $dry ( sort { $a <=> $b } keys %{ $opt->{'dryrun'} } ) {

            my $name = $opt->{'dryrun'}{$dry}{'name'};
            $self->{'ui_obj'}->output( $self, 1, q{-- Begin dryrun test '[_1]' --}, $name );

            local $self->{'twiddler'}         = $self->get_twiddler_obj();
            local $|                          = $| + 1;
            local $self->{'twiddler_uniq_id'} = $self->{'twiddler'} ? $self->{'twiddler'}->get_uniq_str() : '';
            $self->print_to_screen( $self->get_start_twiddler() );

            if ( !defined $self->{'dryrun_cache'}{$name} ) {
                $self->{'dryrun_cache'}{$name} = [ $opt->{'dryrun'}{$dry}{'command'}->($self) ];
            }

            if ( !$self->{'dryrun_cache'}{$name}->[0] ) {
                my @rc_copy = @{ $self->{'dryrun_cache'}{$name} };
                $self->{'failed_opt_mod'}{$ns} = {
                    'fatal'  => $opt->{'dryrun_fails_fatal'},
                    'dryrun' => $dry,
                    'rc'     => shift(@rc_copy),
                    'msg'    => \@rc_copy,
                };
                $self->print_to_screen( $self->get_end_twiddler() );
                $self->{'ui_obj'}->output( $self, 1, q{dryrun test '[_1]' did not return true}, $name );
                return;
            }

            $self->print_to_screen( $self->get_end_twiddler() );

            $self->{'ui_obj'}->output( $self, 1, q{-- End dryrun test '[_1]' --}, $name );
            $self->print_to_log_and_screen("\n");
        }

        return 1;
    }

    sub dependency_ok {
        my ( $self, $opt, $name, $no_output ) = @_;

        my @maketext;

      OS:
        foreach my $os ( keys %{ $opt->{'depends'}{'os'} } ) {
            if ( $opt->{'depends'}{'os'}{$os} ) {
                if ( $self->{'getos'} ne $opt->{'depends'}{'os'}{$os} ) {
                    @maketext = ( q{Invalid OS for '[_1]': key = '[_2]'}, $self->{'getos'}, $os );
                }
            }
            else {
                if ( $self->{'getos'} ne $opt->{'depends'}{'os'}{$os} ) {
                    @maketext = ( q{Invalid OS for '[_1]': key = '[_2]'}, $self->{'getos'}, $os );
                }
            }
        }

        my $off_msg = 'off or "skipped"';
        my $on_msg  = 'on and not "skipped"';
      NS:
        foreach my $ns ( keys %{ $opt->{'depends'}{'optmods'} } ) {
            my $status = $self->get_ns_value_from_profile( $ns, $self->{'working_profile'} );
            my $chk_ec = $self->get_easyconfig_hr_from_ns_variations($ns);

            if ( $opt->{'depends'}{'optmods'}{$ns} ) {
                if ( !$status || ( $status && $chk_ec->{'skip'} ) ) {
                    my $l1 = $opt->{'name'}    || qq{No Name: $name;};
                    my $l2 = $chk_ec->{'name'} || qq{No Name: $ns};
                    @maketext = ( q{'[_1]' requires the option '[_2]' to be [_3].}, $l1, $l2, !$chk_ec->{'reverse'} ? $on_msg : $off_msg );
                }
            }
            else {
                if ( $status && !$chk_ec->{'skip'} ) {
                    my $l1 = $opt->{'name'}    || qq{No Name: $name;};
                    my $l2 = $chk_ec->{'name'} || qq{No Name: $ns};
                    @maketext = ( q{'[_1]' requires the option '[_2]' to be [_3].}, $l1, $l2, !$chk_ec->{'reverse'} ? $off_msg : $on_msg );
                }
            }
        }

        if (@maketext) {
            if ($no_output) {
                return ( 0, @maketext );
            }

            $self->{'ui_obj'}->output( $self, 1, @maketext );
            $self->print_to_log_and_screen("\n");
            return;
        }

        return 1;
    }

    sub increase_memory_limits {
        my ($self) = @_;

        my $maxmem = ( 256 * ( 1024 * 1024 ) );
        setrlimit( RLIMIT_RSS(), $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_RSS\n";
        setrlimit( RLIMIT_AS(),  $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_AS\n";
        $maxmem = ( 512 * ( 1024 * 1024 ) );
        setrlimit( RLIMIT_RSS(), $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_RSS\n";
        setrlimit( RLIMIT_AS(),  $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_AS\n";

        if ( defined $self->{'getos'} && $self->{'getos'} =~ /^(?:rhel|redhat|centos|cloudlinux)$/i && $self->{'getos_releaseversion'} ge '6.0' ) {
            $maxmem = ( 768 * ( 1024 * 1024 ) );
            setrlimit( RLIMIT_RSS(), $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_RSS\n";
            setrlimit( RLIMIT_AS(),  $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_AS\n";
            $maxmem = ( 1024 * ( 1024 * 1024 ) );
            setrlimit( RLIMIT_RSS(), $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_RSS\n";
            setrlimit( RLIMIT_AS(),  $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_AS\n";
        }

        set_infinite_memory_limits();

        $ENV{'MALLOC_ARENA_MAX'} = 1;
    }

    sub set_infinite_memory_limits {
        my $maxmem = RLIM_INFINITY();
        setrlimit( RLIMIT_RSS(), $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_RSS\n";
        setrlimit( RLIMIT_AS(),  $maxmem, $maxmem ) || print STDERR "$$: Unable to set RLIMIT_AS\n";
        delete $ENV{'MALLOC_ARENA_MAX'};
    }

    sub install_profile {
        my ( $self, $profile, $force_working_profile ) = @_;

        # Use root's private temp directory; create it if necessary.
        # Using /tmp causes disk space and security concerns. (See case 53448)
        $ENV{'TMP'} = $ENV{'TMPDIR'} = ( $ENV{'HOME'} || '/root' ) . '/tmp';
        if ( !-d $ENV{'TMP'} ) {
            Cpanel::SafeDir::safemkdir( $ENV{'TMP'} ) || return;
        }

        $self->load_prefs_hashref();

        $self->hook_script('/usr/local/cpanel/scripts/preeasyapache') or return;

        $self->perl_profile_init();

        $profile = $self->determine_profile($profile) if ref $profile ne 'HASH';

        local $SIG{'HUP'}  = $self->get_basic_sig_handler( 'HUP',  1 );
        local $SIG{'TERM'} = $self->get_basic_sig_handler( 'TERM', 1 );
        local $SIG{'INT'}  = $self->get_basic_sig_handler( 'INT',  1 );
        local $SIG{'QUIT'} = $self->get_basic_sig_handler( 'QUIT', 1 );

        if ( $ENV{'HTTP_PROXY'} ) {
            delete $ENV{'HTTP_PROXY'};
            $self->{'proxied_whm_connection'} = 1;
        }
        if ( $ENV{'HTTP_PROXIED'} ) {
            $self->{'proxied_whm_connection'} = 1;
        }

        $self->increase_memory_limits();
        $self->set_virtuozzo_memory_cflags();
        $self->raise_fd_setsize_in_system_includes();

        # eval's don't like this:
        # local $SIG{'__DIE__'}  = $self->get_basic_sig_handler( '__DIE__' , 1 ); # how will this work out with our custom DIE handler ??

        $self->debug(
            {
                'skip_caller_info' => 1,
                'message' => [ q{Starting at '[_1]'}, time() . " - $$" ]
            }
        );

        $self->print_alert( q{Verbose logfile is at '[_1]'}, $self->{'log_file'} );
        $self->ulimit_a_info();

        $self->setup();
        $self->{'working_profile'} =
          ref $profile eq 'HASH'
          ? $profile
          : $self->deserialize($profile);

        # Case 83657: In order to use EasyApache, you have to install Apache.
        # Let's make sure that the absolute essential component of EA is
        # selected, and it's available.  If not, bail out
        {
            my @ret = $self->is_valid_apache_selected();

            if ( !$ret[0] ) {
                shift @ret;
                $self->print_alert_color( 'red', @ret );
                $self->{'_'}{'restore_backup'}++;
                $self->cleanup();
                return;
            }
        }

        my ( $changed_hr, $loop_hr ) = $self->apply_one_pass_implies_logic( $self->{'working_profile'} );
        my $one_pass_fatal = 0;
        $self->handle_one_pass_hashes(
            $changed_hr,
            $loop_hr,
            sub { $self->print_alert_color( 'blue',   shift() ) },
            sub { $self->print_alert_color( 'yellow', shift() ) },
            sub { $self->print_alert_color( 'red',    shift() ); $one_pass_fatal = 1; },
        );

        if ($one_pass_fatal) {
            $self->{'_'}{'restore_backup'}++;
            $self->cleanup();
            return;
        }

        $self->profile_manual_edit_fixup( $self->{'working_profile'}, $force_working_profile );

        if ( defined $force_working_profile && ref $force_working_profile eq 'HASH' ) {
            for my $key ( keys %{$force_working_profile} ) {
                $self->set_ns_value_in_profile( $key, $self->{'working_profile'}, $force_working_profile->{$key} );
            }
        }
        $self->merge_in_cpphp_opts_if_applicable();

        # recalculate optmod ordering and dependencies after all profile fixups
        $self->{'state'} = $self->generate_state_hashref();

        # set these up for hooks after preeasyapache
        $self->{'cache'}{'hook_script_args'} = [
            $self->{'version'},
            $self->{'revision'},
            $self->get_hook_apache_version( $self->{'working_profile'} ),    # apache version (0 means we are not doing it)
            $self->get_hook_php_versions( $self->{'working_profile'} ),      # CSV php versions (0 means we are not doing it)
            ( $self->get_param('hook-args') ),                               # specifically in array context
        ];

        $self->update_runlog_entry( { 'profile' => $self->{'working_profile'}, } );

        if ( !chdir $self->{'opt_mod_src_dir'} ) {
            $self->{'ui_obj'}->output( $self, 0, q{Could not chdir into '[_1]': [_2]}, $self->{'opt_mod_src_dir'}, $! );
            return;
        }

        # store real path, /home might be a symlink
        $self->{'opt_mod_src_dir'} = $self->cwd();

        # so sources are available to other opt mods...
        if ( !$self->process_tarballs_and_pkgs() ) {
            $self->{'_'}{'restore_backup'}++;
            $self->cleanup();
            $self->print_alert( q{Verbose logfile is at '[_1]'}, $self->{'log_file'} );

            return;
        }

        if ( $self->get_param('simulate-failed-build') ) {
            $self->{'_'}{'restore_backup'}++;
            $self->print_alert(q{Halting build in failure simulation as per 'simulate-failed-build' flag});
        }
        else {
            Cpanel::FileUtils::touchfile( $self->{'cpaneleasy_build_running_file'} );    # no error reporting as /usr/local/apache may not exist, at which point this file would be moot anyway

            # Install all the RPMs specified in this profile
            if ( Cpanel::Version::Compare::compare( $Cpanel::Version::Tiny::VERSION_BUILD, '>=', '11.35' ) ) {
                my ( $status, $message ) = $self->process_rpms();
                if ( !$status ) {
                    $self->print_alert_color( 'red', q{RPM install/uninstall failed: [_1]}, $message );
                    return;
                }
            }

            $self->{'_'}{'internal_skip'} = {};

            my @begun_opt;
          OPT_MOD:
            foreach my $ns ( @{ $self->{'state'}{'order'} } ) {
                next OPT_MOD if $self->ns_is_include_kid_and_parent_is_off( $ns, $self->{'working_profile'} );

                next OPT_MOD if exists $self->{'_'}{'internal_skip'}{$ns};
                if ( $self->ns_is_include_that_has_versions($ns) ) {
                    my @vers = $ns->versions();                                                        # at this point we know that $ns->versions() returns goods
                    my $value = $self->get_ns_value_from_profile( $ns, $self->{'working_profile'} );

                    if ($value) {

                        # make sure it has at least one of its versions on or use "newest"
                        my $have_ver_chosen = 0;
                        foreach my $v (@vers) {
                            $have_ver_chosen++ if $self->get_ns_value_from_profile( $ns . '::' . $v, $self->{'working_profile'} );
                        }

                        if ( !$have_ver_chosen ) {
                            $self->set_ns_value_in_profile( $ns . '::' . $vers[-1], $self->{'working_profile'}, ON_AND_VALID );
                        }
                    }
                    else {

                        # make sure all its versions are skipped
                        foreach my $v (@vers) {
                            $self->{'_'}{'internal_skip'}{ $ns . '::' . $v } = 1;
                        }
                    }
                }

                if ( $self->get_param('only') ) {
                    next OPT_MOD if !$self->ns_is_or_belongs_to_list( $ns, $self->get_param('only') );
                }
                elsif ( $self->get_param('skip') ) {
                    next OPT_MOD if $self->ns_is_or_belongs_to_list( $ns, $self->get_param('skip') );
                }

                $self->perl_profile_stat('Before NS variation lookup');
                my $opt = $self->get_easyconfig_hr_from_ns_variations($ns);
                $self->perl_profile_stat('After NS variation lookup');

                if ( !$self->get_ns_value_from_profile( $ns, $self->{'working_profile'} ) ) {
                    if ( exists $opt->{'when_i_am_off'} ) {
                        if ( ref $opt->{'when_i_am_off'} eq 'CODE' && ( !$opt->{'skip'} || $opt->{'treat_as_off_while_skipped'} ) ) {
                            $self->{'ui_obj'}->output( $self, 0, q{-- Begin '[_1]' is off--}, $opt->{'name'} );
                            $opt->{'when_i_am_off'}->($self);
                            $self->{'ui_obj'}->output( $self, 0, q{-- End '[_1]' is off --}, $opt->{'name'} );
                            $self->print_to_log_and_screen("\n");
                        }
                    }
                    next OPT_MOD;
                }

                if ( exists $self->{'modify_later_easyconfig_hr'}{$ns} ) {
                    if ( ref $self->{'modify_later_easyconfig_hr'}{$ns} eq 'HASH' ) {
                        $opt = $self->merge_easyconfig_hrs_into_one( $opt, $self->{'modify_later_easyconfig_hr'}{$ns} );
                    }
                }

                if ( $opt->{'skip'} ) {
                    $self->set_ns_value_in_profile( $ns, $self->{'working_profile'}, ON_BUT_INVALID );
                    if ( exists $opt->{'when_i_am_off'} && ref $opt->{'when_i_am_off'} eq 'CODE' && $opt->{'treat_as_off_while_skipped'} ) {
                        $self->{'ui_obj'}->output( $self, 0, q{-- Begin '[_1]' is off--}, $opt->{'name'} );
                        $opt->{'when_i_am_off'}->($self);
                        $self->{'ui_obj'}->output( $self, 0, q{-- End '[_1]' is off --}, $opt->{'name'} );
                        $self->print_to_log_and_screen("\n");
                    }
                    next OPT_MOD;
                }

                if ( exists $self->{'state_config'}{'custom_opt_mods_loopkup'} && exists $self->{'state_config'}{'custom_opt_mods_loopkup'}{$ns} ) {
                    if ( $self->get_param('skip-custom-optmods') ) {
                        $self->print_alert_color( 'blue', q{Skipping '[_1]'}, "custom opt mod: '$opt->{'name'}' ($ns)" );
                        next OPT_MOD;
                    }
                    else {
                        $self->{'custom_opt_mods_were_run'}++;    # IE to be able to trigger "run with --skip-custom-optmods" instead of support dialog
                        $opt->{'name'} .= " (Custom Opt Mod)";
                    }
                }

                $self->{'ui_obj'}->output( $self, 0, q{-- Begin opt '[_1]' --}, $opt->{'name'} );
                push @begun_opt, { 'type' => 'opt', 'name' => $opt->{'name'} };

                my $start = $self->cwd();

                my $goback_to_start = sub {
                    my ( $self, $start ) = @_;
                    if ( $self->cwd() ne $start ) {
                        if ( !chdir $start ) {
                            $self->{'ui_obj'}->output( $self, 0, q{Could not chdir into '[_1]': [_2]}, $start, $! );
                            return;
                        }
                    }
                    return 1;
                };

                if ( $opt->{'src_cd2'} ) {
                    if ( !chdir $opt->{'src_cd2'} ) {
                        $self->{'ui_obj'}->output( $self, 1, q{Could not chdir into '[_1]': [_2]}, $opt->{'src_cd2'}, $! );
                        $self->add_error_detail( 'install_profile()', "chdir src_cd2 '$ns'", 0, q{Could not chdir into '[_1]': [_2]}, $opt->{'src_cd2'}, $! );
                        $self->{'_'}{'restore_backup'}++;
                        last OPT_MOD;
                    }
                }

                local $self->{'__'} = {};
                local $self->{'__'}{'meta'} = $opt->{'meta'};

                $self->perl_profile_stat('Before Dependency Check');
                my ( $depends_rc, @depends_text ) = $self->dependency_ok( $opt, $ns );
                $self->perl_profile_stat('After Dependency Check');
                if ( !$depends_rc ) {
                    $self->{'ui_obj'}->output( $self, 1, @depends_text );
                    $self->{'ui_obj'}->output( $self, 1, q{!! Dependency failed for '[_1]' skipping option !!}, $ns );
                    $self->print_to_log_and_screen("\n");

                    $self->set_ns_value_in_profile( $ns, $self->{'working_profile'}, ON_BUT_INVALID );

                    $goback_to_start->( $self, $start ) or last OPT_MOD;

                    if ( $opt->{'depend_fails_fatal'} ) {
                        $self->add_error_detail( 'install_profile()', "pre step loop", 0, q{!! Dependency failed for '[_1]' skipping option !!}, $ns );
                        $self->{'_'}{'restore_backup'}++;
                        last OPT_MOD;
                    }

                    next OPT_MOD;
                }

                # Install Perl modules required by this configuration item, if any
                if ( Cpanel::Version::Compare::compare( $Cpanel::Version::Tiny::VERSION_BUILD, '>=', '11.35' ) ) {
                    if ( $opt->{'perl_modules'} ) {
                        my ( $status, @msg ) = $self->perlinstall( keys %{ $opt->{'perl_modules'} } );
                        if ( !$status ) {
                            $self->print_alert_color( 'red', q{[_1]}, join( ' ', @msg ) );
                            return;
                        }
                    }
                }

                $self->perl_profile_stat('Before Dryrun');
                my ( $dry_rc, @dry_text ) = $self->dry_run_ok( $opt, $ns );
                $self->perl_profile_stat('After Dryrun');
                if ( !$dry_rc ) {
                    $self->{'ui_obj'}->output( $self, 1, @dry_text );
                    $self->{'ui_obj'}->output( $self, 1, q{!! Dry run failed for '[_1]' skipping option !!}, $ns );
                    $self->print_to_log_and_screen("\n");

                    $self->set_ns_value_in_profile( $ns, $self->{'working_profile'}, ON_BUT_INVALID );

                    $goback_to_start->( $self, $start ) or last OPT_MOD;

                    if ( $opt->{'dryrun_fails_fatal'} ) {
                        $self->add_error_detail( 'install_profile()', "pre step loop", 0, q{!! Dry run failed for '[_1]' skipping option !!}, $ns );

                        # case 3777:
                        $self->{'_'}{'restore_backup'}++;

                        # case 3777 ? $self->cleanup();
                        # case 3777 ? $self->print_alert( q{Verbose logfile is at '[_1]'}, $self->{'log_file'} );
                        # case 3777 ? return;

                        last OPT_MOD;
                    }
                    next OPT_MOD;
                }
                my @begun_step;
              OPT_MOD_STEP:
                foreach my $step ( sort { $a <=> $b } keys %{ $opt->{'step'} } ) {
                    if ( $self->{'__'}{'SKIP'} ) {
                        $self->{'ui_obj'}->output( $self, 2, @{ $opt->{'__'}{'SKIP'} }, )
                          if ref $opt->{'__'}{'SKIP'} eq 'ARRAY';
                        $goback_to_start->( $self, $start ) or last OPT_MOD;
                        next OPT_MOD;
                    }

                    $self->{'ui_obj'}->output( $self, 1, q{-- Begin step '[_1]' --}, $opt->{'step'}{$step}{'name'} );
                    push @begun_step, { 'type' => 'step', 'name' => $opt->{'step'}{$step}{'name'} };
                    $self->perl_profile_stat('Before Step');
                    my ( $rc, @maketext ) = $self->run_step( $step, $opt );
                    if ( !$rc ) {
                        $self->print_alert(@maketext);
                        $self->{'failed_opt_mod'}{$ns} = {
                            'fatal' => 1,
                            'step'  => $step,
                            'rc'    => $rc,
                            'msg'   => \@maketext,
                        };
                        $self->add_error_detail( "$opt->{'name'} ($ns)", "Step $step ($opt->{'step'}{$step}{'name'})", $rc, @maketext );    # this one needs some love
                        $self->{'_'}{'restore_backup'}++;
                        $self->cleanup();
                        $self->print_alert( q{Verbose logfile is at '[_1]'}, $self->{'log_file'} );

                        return;
                    }
                    $self->perl_profile_stat('After Step');
                    my $name = $opt->{'step'}{$step}{'name'};
                    $self->{'ui_obj'}->output( $self, 1, q{-- End step '[_1]' --}, $name );
                    pop @begun_step if $name eq $begun_step[-1]->{'name'};
                    $self->print_to_log_and_screen("\n");
                }
                continue {
                    $self->end_if_unended( \@begun_step );    # for 'next OPT_MOD' in above OPT_MOD_STEP loop
                }

                $self->end_if_unended( \@begun_step );        # for 'last OPT_MOD' in above OPT_MOD_STEP loop

                $goback_to_start->( $self, $start ) or last OPT_MOD;

                $self->{'ui_obj'}->output( $self, 0, q{-- End opt '[_1]' --}, $opt->{'name'} );
                pop @begun_opt if $opt->{'name'} eq $begun_opt[-1]->{'name'};
                $self->print_to_log_and_screen("\n");

            }
            continue {
                $self->end_if_unended( \@begun_opt );         # for 'next OPT_MOD' in above OPT_MOD loop
            }

            $self->end_if_unended( \@begun_opt );             # for 'last OPT_MOD' in above OPT_MOD loop

            $self->cleanup_tomcat_support_files();

            if ( !$self->get_param('skip-re-saveas') ) {
                my $save_as = ref $profile eq 'HASH' ? $self->{'profile_main'} : $profile;
                $save_as = $self->{'profile_main'} if $profile eq $save_as && $save_as =~ m{/};
                $save_as .= '.yaml' if $save_as !~ m{\.yaml}i;

                # cpanel*.yaml's are cpanel synced and are not allowed to be overwritten with custom goods
                $save_as =~ s{/(cpanel_?[^\\]\.yaml)}{/my_customized_$1}i;
                $save_as =~ s{/(q[^\\]\.yaml)}{/my_$1}i;      # profiles can't be named 'q'...

                $self->save_profile( $self->{'working_profile'}, $save_as );
            }
        }

        $self->cleanup();

        $self->perl_profiler_post_check();                    # up to main to call perl_profiler_reexec_check()

        if ( $self->{'_'}{'post_fyi_info'} ) {
            $self->print_alert_color( 'blue', "\nHere are some details that may be helpful:\n[_1]\n", $self->{'_'}{'post_fyi_info'} );
        }

        if ( $self->{'_'}{'post_failure_fyi_info'} && $self->{'_'}{'restore_backup'} ) {
            $self->print_alert_color( 'red', "\nHere are some details that may be helpful:\n[_1]\n", $self->{'_'}{'post_failure_fyi_info'} );
        }

        $self->print_alert( q{Verbose logfile is at '[_1]'}, $self->{'log_file'} );

        $self->debug(
            {
                'skip_caller_info' => 1,
                'message' => [ q{Ending at '[_1]'}, time() ]
            }
        );

        return 1;
    }

    sub run_step {
        my ( $self, $step, $opt ) = @_;

        if ( $self->step_has_run( $opt, $step ) ) {
            if ( $self->step_needs_rerun( $opt, $step ) ) {
                $self->mark_step_complete( $opt, $step, 0 );
                my $last_step = ( sort { $a <=> $b } keys %{ $opt->{'step'} } )[-1];
                if ( $step < $last_step ) {
                    foreach my $stp ( $step + 1 .. $last_step ) {
                        $self->mark_step_complete( $opt, $stp, 0 );
                    }
                }

                $self->mark_all_opts_after_this_as_run( $opt, 0 );
            }
        }

        return ( 1, 'step [_1] [_2] already run', ref $opt, $step )
          if $self->step_has_run( $opt, $step );

        local $self->{'twiddler'}         = $self->get_twiddler_obj();
        local $|                          = $| + 1;
        local $self->{'twiddler_uniq_id'} = $self->{'twiddler'} ? $self->{'twiddler'}->get_uniq_str() : '';
        $self->print_to_screen( $self->get_start_twiddler() );

        my ( $rc, @msg ) = $opt->{'step'}{$step}{'command'}->($self);

        $self->print_to_screen( $self->get_end_twiddler() );
        $self->mark_step_complete( $opt, $step, 1 );

        return ( $rc, @msg );
    }

    sub process_tarballs_and_pkgs {
        my ($self) = @_;

        if ( $self->cwd() ne $self->{'opt_mod_src_dir'} ) {
            $self->print_alert( q{Invalid CWD for '[_1]' method to be called: '[_2]'}, 'process_tarballs_and_pkgs', $self->cwd() );
            return;
        }

        if ( !File::Copy::Recursive::pathempty('.') ) {
            $self->print_alert( q{Could not empty path '[_1]'}, $self->cwd() );
            return;
        }

        Cpanel::SafeDir::safemkdir( $self->{'patch_src_dir'} );

        my $targz_md5sums = $self->get_targz_md5sums_hashref();

        if ( !%{$targz_md5sums} && !-e '/var/cpanel/easy_no_targz_yaml' && !$self->get_param('no-targz-yaml') ) {
            $self->print_alert( "Failed to get targz checksums", $self->{'log_file'} );
            return;
        }

        my @ensurepkgs;

        $self->perl_profile_stat('Before tarball check');
        $self->print_to_log_and_screen( $self->maketext('Checking that all tarballs are present and up to date.') );

      OPT_MOD_PRE:
        foreach my $ns ( @{ $self->{'state'}{'order'} } ) {
            next OPT_MOD_PRE if $self->ns_is_include_kid_and_parent_is_off( $ns, $self->{'working_profile'} );
            next OPT_MOD_PRE
              if !$self->get_ns_value_from_profile( $ns, $self->{'working_profile'} );

            if ( exists $self->{'state'}{'tarballs'}{$ns} ) {
                if ( ref $self->{'state'}{'tarballs'}{$ns} eq 'ARRAY' ) {
                    foreach my $tarball ( @{ $self->{'state'}{'tarballs'}{$ns} } ) {

                        # This will rerun modself
                        my $ec = $self->get_easyconfig_hr_from_ns_variations($ns);
                        next unless ( $ec->{'hastargz'} );
                        if ( $ec->{'alttargz'} ) {
                            $tarball .= ".$ec->{'alttargz'}";
                        }

                        my ( $rc, @text ) = $self->fetch_tarball_if_needed( $tarball, $targz_md5sums, $ns );
                        if ( !$rc ) {
                            $self->print_alert(@text);
                            $self->add_error_detail( $ns, 'ensuretargz', 0, @text );
                            return;
                        }
                    }
                }
            }
            $self->perl_profile_stat( 'After tarball check for ' . $ns );

            if ( exists $self->{'state'}{'ensurepkgs'}{$ns} ) {
                if ( ref $self->{'state'}{'ensurepkgs'}{$ns} eq 'ARRAY' ) {
                    foreach my $pkg ( @{ $self->{'state'}{'ensurepkgs'}{$ns} } ) {
                        if ( $pkg =~ m{ \A \S+ \z }xms ) {
                            push @ensurepkgs, $pkg;
                        }
                        else {
                            my @maketext = ( q{easyconfig key 'ensurepkg' in '[_1]' has an invalid value}, $ns, );
                            $self->print_alert(@maketext);
                            $self->add_error_detail( $ns, 'ensurepkg', 0, @maketext );

                            return;
                        }
                    }
                }
                else {
                    if ( $self->{'state'}{'ensurepkgs'}{$ns} =~ m{ \A \S+ \z }xms ) {
                        push @ensurepkgs, $self->{'state'}{'ensurepkgs'}{$ns};
                    }
                    else {
                        my @maketext = ( q{easyconfig key 'ensurepkg' in '[_1]' has an invalid value}, $ns, );
                        $self->print_alert(@maketext);
                        $self->add_error_detail( $ns, 'ensurepkg', 0, @maketext );

                        return;
                    }
                }
            }
        }

        @ensurepkgs = ( @ensurepkgs, $self->basic_ensurepkgs_list() );

        if (@ensurepkgs) {
            $self->fix_glibc_dummy();
            $self->centos5_openssl_fixup();
            $self->perl_profile_stat('Before package check');
            $self->print_to_log_and_screen( $self->maketext('Checking that all packages are present and up to date.') );

            # only use it if/when we need it *and* if we have the right
            # version with 'get_command_arrayref_only' support - case 874
            eval 'use Cpanel::SysPkgs 0.2;';
            if ($@) {
                $self->print_alert($@);
                $self->add_error_detail( 'process_tarballs_and_pkgs()', 'eval use Cpanel::SysPkgs 0.2;', 0, $@ );
                return;
            }

            $self->{'syspkg_obj'} = Cpanel::SysPkgs->new()
              if !$self->{'syspkg_obj'};

            if ( !$self->{'syspkg_obj'} ) {
                $self->print_alert_color( 'red', q{Package system object could not be created} );
                return;
            }

            if ( $self->{'syspkg_obj'}->can('checkconfig') && !$self->{'syspkg_obj'}->checkconfig() ) {
                $self->print_alert_color( 'red', q{'[_1]' did not return true}, ref( $self->{'syspkg_obj'}{'syspkgobj'} ) . ' checkconfig()' ) if $self->get_param('only-tarballs-and-pkgs');
                if ( $self->{'syspkg_obj'}->can('can_setup') && $self->{'syspkg_obj'}->can_setup() ) {

                    $self->print_alert_color( 'yellow', q{Trying to auto repair package system} );

                    if ( !$self->{'syspkg_obj'}->setup() ) {
                        $self->print_alert_color( 'red', q{'[_1]' did not return true}, ref( $self->{'syspkg_obj'}{'syspkgobj'} ) . ' setup()' ) if $self->get_param('only-tarballs-and-pkgs');
                        $self->print_alert_color( 'red', q{Package system can not be repaired automatically} );
                        $self->print_alert_color( 'red', q{Please visit [_1] for help with this error.}, 'http://go.cpanel.net/eaerror' );
                        return;
                    }
                    else {
                        $self->print_alert_color( 'green', q{Package system repaired automatically} );
                    }
                }
                else {
                    $self->print_alert_color( 'red', q{No method to auto repair package system} );
                    $self->print_alert_color( 'red', q{Please visit [_1] for help with this error.}, 'http://go.cpanel.net/eaerror' );
                    return;
                }
            }

            my %pkgs;
            @pkgs{@ensurepkgs} = ();

            my $cmd_ar = $self->{'syspkg_obj'}->ensure(
                'pkglist'                   => [ keys %pkgs ],
                'get_command_arrayref_only' => 1,
            );

            if ( $self->get_param('output-syspkg-details') || $self->get_param('only-tarballs-and-pkgs') ) {
                if ( ref $cmd_ar ) {
                    $self->print_alert_color( 'blue', q{ensure() system was '[_1]'}, join( ' ', @{$cmd_ar} ) );
                }
                else {
                    $self->print_alert_color( 'blue', q{direct ensure() call will be used (does not support get_command_arrayref_only): pkglist = '[_1]'}, join( ' ', keys %pkgs ) );
                }
            }

            # if we didn't get an array just call the method and assume ensure
            # without 'get_command_arrayref_only' support handles the BG issue
            # properly (IE by not calling system()) - case 874
            my ( $pkg_rc, $sys_exit ) =
              ref $cmd_ar eq 'ARRAY'
              ? $self->run_system_cmd($cmd_ar)
              : $self->{'syspkg_obj'}->ensure( 'pkglist' => [ keys %pkgs ] );

            if ( !defined $pkg_rc || !$pkg_rc || $self->get_param('output-syspkg-details') || $self->get_param('only-tarballs-and-pkgs') ) {
                require Cpanel::SysCmdRC;
                if ( $Cpanel::SysCmdRC::VERSION && $Cpanel::SysCmdRC::VERSION > 0.1 ) {
                    $self->print_alert_color(
                        'blue', q{ensure() rc was '[_1]', exit status was '[_2]', perl '$?' was '[_3]', exit sig '[_4]', core dump: '[_5]', shell '$?' was '[_6]'},
                        $pkg_rc, $sys_exit, $?,
                        Cpanel::SysCmdRC::get_exit_signal_from_rc($sys_exit),
                        Cpanel::SysCmdRC::get_core_dumped_from_rc($sys_exit),
                        Cpanel::SysCmdRC::get_exit_value_from_rc($sys_exit)
                    );
                }
                else {
                    $self->print_alert_color( 'blue', q{ensure() rc was '[_1]', exit status was '[_2]', shell '$?' was '[_3]'}, $pkg_rc, $sys_exit, $sys_exit >> 8 );
                }
            }

            if ( !defined $pkg_rc || !$pkg_rc || $self->get_param('simulate-failed-syspkgs') ) {
                $self->print_to_log_and_screen("\n\n");
                $self->print_alert_color( 'yellow', q{Simulating failed Cpanel::SysPkgs as per 'simulate-failed-syspkgs' flag} );
                my @maketext = ( q{Could not ensure pkglist '[_1]'}, join( ', ', keys %pkgs ), );
                $self->print_alert(@maketext);

                my ($name) = reverse( split( /::/, ref( $self->{'syspkg_obj'}->{'syspkgobj'} ) ) );
                if ($name) {
                    $self->print_alert_color( 'red', q{The server's system package manager, '[_1]', failed.}, $name );
                }
                if ( ref $cmd_ar ) {

                    $self->print_alert_color( 'red', qq{\nThis is the command that failed:\n\n\t[_1]\n\n}, join( ' ', @{$cmd_ar} ) );
                    $self->print_alert_color( 'red', qq{\nSince EasyApache was unable to resolve it automatically you should:\n\t1) Manually run the failed [_1]command (shown above) via SSH\n\t2) See if your particular error is addressed at http://go.cpanel.net/eaerror\n\t3) Resolve the [_1]problem manually\n\t4) Re-run EasyApache\n}, ( $name ? "$name " : '' ) );
                }
                else {
                    $self->print_alert_color( 'red', q{direct ensure() call will be used (does not support get_command_arrayref_only): pkglist = '[_1]'}, join( ' ', keys %pkgs ) );
                }

                $self->print_alert_color( 'red', q{Please visit [_1] for help with this error.}, 'http://go.cpanel.net/eaerror' );
                $self->print_to_log_and_screen("\n\n");
                $self->add_error_detail( 'process_tarballs_and_pkgs()', 'Cpanel::SysPkgs ensure() method', 0, @maketext );
                $self->{'_'}{'ensurepkgs_failure'} = 1;
                return;
            }
            $self->perl_profile_stat('After package check');
        }

        return 1;
    }

    sub get_twiddler_obj {
        my ($self) = @_;
        return if $self->{'_'}{'prefs'}{'verbose'};
        require Cpanel::CPAN::Text::Twiddler;
        my $twid = $self->{'ui_obj'}->get_twiddler_new_hashref($self);
        $twid->{'lang_obj'} = $self->{'lang_obj'};
        return Cpanel::CPAN::Text::Twiddler->new($twid);
    }

    sub get_start_twiddler {
        my ($self) = @_;
        return if $self->{'_'}{'prefs'}{'verbose'};
        $self->{'twiddler'}->get_start_twiddler();
    }

    sub get_next_twiddler {
        my ($self) = @_;
        return if $self->{'_'}{'prefs'}{'verbose'};
        $self->{'twiddler'}->get_next_twiddler();
    }

    sub get_end_twiddler {
        my ($self) = @_;
        return if $self->{'_'}{'prefs'}{'verbose'};
        $self->{'twiddler'}->get_end_twiddler();
    }

    sub setup {
        my ($self) = @_;
        $self->debug( { 'dumpobj' => 1, } );
        $self->{'ui_obj'}->setup($self);
    }

    sub cleanup {
        my ($self) = @_;

        $self->{'cleanup_has_run'} = 0 if !defined $self->{'cleanup_has_run'};

        $self->debug(
            {
                'dumpobj' => 1,
                'message' => [ q{DEBUG: cleanup() has already run status: '[_1]'}, $self->{'cleanup_has_run'}, ],
            }
        );
        return if $self->{'cleanup_has_run'};
        $self->{'cleanup_has_run'}++;
        $self->{'ui_obj'}->cleanup($self);

        unlink $self->{'cpaneleasy_build_running_file'};    # silent
    }

    sub determine_profile {
        my ( $self, $profile, $create ) = @_;

        $profile = $self->get_param('profile') if !defined $profile;
        $profile = $self->{'profile_main'}     if !defined $profile;

        # is arg or param is just a filename put it in the dir, otherwise assume the path is ok...
        $profile = $self->{'profile_custom_dir'} . '/' . $profile
          if $profile !~ m{/};

        $profile = Cpanel::FileUtils::cleanpath($profile);

        # ensure .yaml extension, makes it optional in param
        $profile =~ s{\.yaml$}{};
        $profile .= '.yaml';

        Cpanel::FileUtils::touchfile($profile) if $create;
        return $self->{'profile_main'} if !-e $profile;
        return $profile;
    }

    sub add_error_detail {
        my ( $self, $opt, $step, $rc, @text ) = @_;

        $self->debug();

        $opt  = 'no OPT passed to add_error_detail()'    if !defined $opt;
        $step = 'no STEP passed to add_error_detail()'   if !defined $step;
        $rc   = 'no RC passed to add_error_detail()'     if !defined $rc;
        @text = ('no TEXT passed to add_error_detail()') if !@text;

        if ( !exists $self->{'_'}{'error_is_in_order'}{$opt} ) {
            push @{ $self->{'_'}{'error_order'} }, $opt;
            $self->{'_'}{'error_is_in_order'}{$opt}++;
        }

        # do pseudo instead of $self->{'lang_obj'} since we want orig reported and that NS is being uses in new()
        $self->{'_'}{'error'}{$opt}{$step} = {
            'time' => time(),
            'rc'   => $rc,
            'text' => Cpanel::CPAN::Locale::Maketext::Pseudo->maketext(@text),
        };

        $self->{'_'}{'error'}{$opt}{$step}{'text'} .= "\n\n[last system cmd output]\n$self->{'last_run_system_cmd'}\n[/last system cmd output]" if $self->{'last_run_system_cmd'};
    }

    sub load_state_from_file {
        my ($self) = @_;

        if ( !-e $self->{'state_file'} ) {

            # warn "needs done cant skip" if $self->get_param('skip-create-state')
            $self->create_state_file();
        }
        else {
            $self->{'state'} = $self->deserialize( $self->{'state_file'} );

            if ( ref $self->{'state'} ne 'HASH' ) {

                # warn "needs done cant skip" if $self->get_param('skip-create-state');
                $self->create_state_file();
            }
            elsif ( $self->saved_state_is_old() || $self->get_param('force-create-state') ) {
                $self->create_state_file()
                  if !$self->get_param('skip-create-state')
                  || $self->get_param('force-create-state');
            }
        }
    }

    sub create_state_file {
        my ($self) = @_;

        $self->{'state'} = $self->generate_state_hashref();

        unlink $self->{'state_file_needs_redone_file'};

        if ( -e $self->{'state_file_needs_redone_file'} ) {
            $self->log_warn( [ q{unlink '[_1]' failed: [_2]}, $self->{'state_file_needs_redone_file'}, $!, ] );
        }

        return $self->serialize( $self->{'state_file'}, $self->{'state'} );

    }

    sub get_variations {
        my ( $self, $ns, $is_include ) = @_;

        if ( $is_include || grep /^$ns$/, @{ $self->{'state_config'}{'include'} } ) {
            eval "use $ns;";
            no strict 'refs';
            my $ec = ${ $ns . '::easyconfig' };
            return [] if ref $ec ne 'HASH';
            return [] if !exists $ec->{'variations'};
            return [] if ref $ec->{'variations'} ne 'ARRAY';
            return $ec->{'variations'};
        }

        return $self->{'variations'} || [];
    }

    sub end_if_unended {
        my ( $self, $begun ) = @_;
        if ( @{$begun} ) {
            while ( @{$begun} ) {
                my $unended = pop @{$begun};
                my ( $type, $name ) = @{$unended}{qw/type name/};
                my $indent = $type eq 'step' ? 1 : 0;
                $self->{'ui_obj'}->output( $self, $indent, qq{-- End [_1] '[_2]' --}, $type, $name );
            }
        }
    }
}

1;

__END__

=head1 What is Cpanel::Easy?

It is a framework for easy, unified, and maintainable interfaces that manage complex tasks.

The Cpanel::Easy framework runs off of "option modules" referred to as an "opt mod" for short.

An opt mod is a perl module which basically defines a hashref called '$easyconfig' as outlined in 'Opt mod anatomy'

This document is intended to be used as a reference in conjunction with the examples of cPanel opt mods.

While it does try to be complete and thorough, it is not an exhaustive or step by step guide to making an opt mod.

=head1 Opt mod anatomy

    package Cpanel::Easy::Apache::Option;

    # cpanel - Cpanel/Easy/Apache/Option.pm           Copyright(c) 2014 cPanel, Inc.
    #                                                           All rights Reserved.
    # copyright@cpanel.net                                         http://cpanel.net
    # This code is subject to the cPanel license. Unauthorized copying is prohibited

    our $easyconfig = {
        'verify_on'  => 'If you turn this on be sure to X, Y, and Z. Do you wish to turn this on still?', # UI prompts for verification (yes and no buttons) when toggled from off to on
        'verify_off' => 'If you turn this off you'll be unable to X, Y, and Z. Do you wish to turn this on still?', # UI prompts for verification (yes and no buttons) when toggled from on to off
        'name'      => 'Option support', # short label to describe
        'note'      => 'Short bit of FYI type info',
        'desc'      => 'Long blob of informative text',
        'ensurepkg' => [], # list of packages this opt mod needs (IE suitable for /usr/local/cpanel/scripts/installpkg)
        'src_cd2'   => '', # path to cd into to run steps, relative to $self->{'opt_mod_src_dir'}
        'dryrun_fails_fatal' => 1, # will do 'last OPT_MOD' instead of 'next OPT_MOD;' if dryrun fails
        'depend_fails_fatal' => 1, # will do 'last OPT_MOD' instead of 'next OPT_MOD;' if depend fails
        'hastargz'  => 1,  # see TARBALLS below
        'alttargz'  => '', # see 'Alternate Tarballs' below
        'haspatch'  => 1,  # see PATCHES below
        'cpversion' => [ # Tie an optmod to a minimum version of cPanel & WHM
            # 11.38 and earlier is not permitted
            { 'tier' => '11.40', 'minversion' => '11.40.1.16' },
            { 'tier' => '11.42', 'minversion' => '11.42.1.19' }
            # 11.44 and later is permitted
        ],
        'meta'      => {
            $misc data that steps have access to via $self->{'__'}{'meta'}
        },
        'url'       => 'http://httpd.apache.org/whatever.html', # URL to get [?] help/more info
        'license_url' => 'http://....', URL of licesne info, this causes a 'you must agree' in the note and verify_on
        'skip'      => 1, # tells the UI and install to not use/do this option
        'reverse'   => 1, # tells the UI and install to do the reverse logic with its setting in the save profile
        'treat_as_off_while_skipped' => 0, # tells the backend to still run when_i_am_off coderef while the optmod is marked as skipped
        'when_i_am_off' => sub {
              my ($self) = @_;
              # whatever, warn if something needs done, return not used since it's next()ing it anyway)
        },
        'configure' => {
            '--with-foo' => [],
            '--with-bar' => ['', 'path'],
        }, # set the default configure line if opt mod is a parent (IE apache or PHP) for its kids to modify
        'modself' => sub {
            my ($easy, $self_hr, $profile_hr) = @_;
            # do not show this PHP option in UI or run it if we have apache 1
            $self_hr->{'skip'} = 1 if $easy->get_ns_value_from_profile('Cpanel::Easy::Apache::1', $profile_hr);
        }, # called after variations are calculated to fine tune any items that are outside the scope of its NS, depends, implies, etc
        'step'      => {
            '0' => {
                'name' => 'Frabnogizing Option',
                'command' => sub {
                    # See "STEP HASHREF" below
                },
            },
            ...
        },
        'implies'  => {
            'Full::Name::Space::Foo' => 1, # when saved turns on foo
            'Full::Name::Space::Bar' => 0, # when saved turns off bar
        },
        'depends'   => {
            'optmods' => {
                'Full::Name::Space::Foo' => 1, # build needs foo to be on
                'Full::Name::Space::Bar' => 0, # build needs bar to be off
                ...
            },
            'os'    => {
                'linux'   => is_64bit ? 0 : 1, # not on 64 bit linux
                'gentoo'  => 0, # not on gentoo
                ...
            }
        },
        'dryrun'    => {
            '0' => {
                'name'    => 'x11libs',
                'command' => sub {
                     my ($self) = @_;
                     return my_returnable_function('libx11-devel');
                }
            },
            ...
        },
    };

    1;

=head1 Please do this!

If the opt mod is not the "root" of a "variation" (IE Foo/XYZ.pm for Foo::XYZ is a "root ", Foo/1/XYZ.pm for Foo::XYZ is a "variation")
it may be good to put a note in '__END__' about why it needed made if its not immediately apparent from its changes to the root $easyconfig

=head1 namespace/filename semantics

No matter what the filename is (as per the "variation" setup) the name space (IE package declaration) is the same.

package Foo::XYZ;

is in each one of:

Foo/XYZ.pm (root)
Foo/1/XTZ.pm (variation)
Foo/linux/XYZ.pm (variation)

=head1 cPanel & WHM version locking

The goal of the 'cpversion' hash key is to allow for an OptMod developer to tie it to one or more versions of cPanel
& WHM.   This obviates the need for constant duplication of 'modself' logic.

The value of 'cpversion' is an ARRAY reference.  Each entry in the ARRAY reference is a HASH reference that must
contain a 'tier' key, who's value is a cPanel & WHM tier (e.g. 11.40, 11.42, etc).  The HASH reference may optionally
contain a 'minversion' key, who's value is a full cPanel & WHM version that references a specific minor version in the
tier.

Examples:

   # Allow only cPanel & WHM 11.40 and newer
   'cpversion' => [ { 'tier' => '11.40' } ]

   # Allow cPanel & WHM 11.40.1.16 and newer, but nothing older.
   'cpversion' => [ { 'tier' => '11.40', 'minversion' => '11.40.1.16' } ]

   # Allow cPanel & WHM 11.40.1.16, 11.42.1.8, and newer
   'cpversion' => [
      { 'tier' => '11.40', 'minversion' => '11.40.1.16' },
      { 'tier' => '11.42', 'minversion' => '11.42.1.8' },
   ]

Notes:

  The locking assumes that any version of cPanel & WHM that is older than
  the oldest defined lock, is not allowed to be used.

  Conversely, the locking assumes that any version of cPanel & WHM that
  is newer than the latest defined lock, is allowed to be used.

=head1 step hashref

Its 'command' coderef's only argument is the main object:

   my ($self) =  @_;

When it starts it is in $self->{'opt_mod_src_dir'} or $self->{'opt_mod_src_dir'}/src_cd2 assuming src_cd2 is specified.

=head2 return value

It needs to return an array whose first argument is a 1 or a 0 based on if it was able to do its thing or not.

The rest of the array should be suitable to be passed to maketext().

Examples:

  return (1, 'ok');
  return (0, 'invalid name space string'); # not including it incase it was evil, like <b>xss</b>
  return (0, q{Could not chdir back into '[_1]': [_2]}, $dir, $!)

=head2 Communication between each step

If a step needs to get data from a previous step or send data to subsequent steps (or both) it'd use the $self->{'__'} hashref.
That hashref is local()ized for each opt-mod so is guaranteed to be empty in step '0' and does not require any maintenance to cleanup, either in the step or Cpanel::Easy's install_profile()

=head2 changing dir

When a step is run, it either in the $self->{'opt_mod_src_dir'}
or $self->{'opt_mod_src_dir'}/$easyconfig->{'src_cd2'} if $easyconfig->{'src_cd2'} is specified.

To change the working directory:

    my $start = $self->cwd();
    chdir $newdir or return (0, q{Could not chdir into '[_1]': [_2]}, $newdir, $!);

Before *ANY* return() be sure to change back:

    chdir $start or return (0, q{Could not chdir back into '[_1]': [_2]}, $start, $! );
    return @return;

=head2 TARBALLS

=head3 cPanel supplied

To facilitate Opt mods that need tarballs without the need for it to exist (IE be cpanelsync'ed even if its not needed)

the easyconfig hashref will need to have this key set to true:

 'hastargz' => 1,

Then the tarball would be placed in the repository's targz/ path matching the name space setup ( so we can go "I want the tarball for Cpanel::Easy::PHP5::3_13" ) and its md5sum into the targz.yaml file

This will allow us to cpanelsync the opt mods only and fetch the tarballs only when needed.

Of course if its in Cpanel/Easy/PHP5/3_13.pm.tar.gz it will just get synced every time and there's no need to do the hastargz key or put its md5sum in the yaml file.

(This what we want for Cpanel::Easy::OptLib modules and their tarballs)

=head3 Custom Opt mods

It still needs a 'hastargz' => 1 to use it, even though its included as part of the custom opt mod

=head2 PATCHES

To also have "when needed" download of patches that work much like TARBALLS above.

The differences are:

a) their name: Foo.pm would be Foo.pm.patch.tar.gz

b) it'd get untarred into $self->{'patch_src_dir'}

This is meant for opt mods that have source code (foo.pm.tar.gz) and patches to apply against it (Foo.pm.patch.tar.gz)
If an opt mod is a patch to another opt mods then you can just use 'hastargz' mechanism.

=head2 How can we make an opt mod change the behavior of an opt mod that comes later?

You can have the opt mod have a step that creates a 'modify_later_easyconfig_hr' entry for the name space you want to change.

    return $easy->add_to_modify_later_queue( 'Cpanel::Easy::Apache::1', $easyconfig_to_merge_in );

This entry is then merged with the completed, variations incorporated, easyconfig for that namespace.

You can surgically insert entries into dryrun or steps for example by using decimal numbers for the positional integer.

It'd be best to note in the parent's entry that if it ever changes to modify all related 'modify_later_easyconfig_hr' entries for it.

You won't necessarily be in the right directory when they get run later so the entry will be responsible for changing into and back out of the proper directory. See "changing dir" above for details.

Circumstance where this will be needed should be rare and great caution should be taken that it works well with other things using it.

See Cpanel/Easy/Apache/1/Dav.pm for an example of this.

=head2 Alternate Tarballs

Say you do not want to have one tarball with several large binaries for different platforms only to not use 80 MB of them.

You can specify alternates with the 'alttargz' key. Its value, if true, gets attached to the end of the normal tarball name as an additional extension.

Simply do this:

   'alttargz' => '', # initial value is empty string (will just default to normal tarball)
   'modself' => sub {}, # modify your alttargz value if/as needed to, say, 'linux.64'

And it will download and use

  targz/Cpanel/Easy/Whatever.pm.tar.gz.linux.64 instead of targz/Cpanel/Easy/Whatever.pm.tar.gz

every alttargz should have an entry in targz.yaml as usual.

=head1 System command info and reporting data:

$self->output_system_cmd([command, here]) - puts labled section of that command's output in the log and to the screen && in the report

$self->report_system_cmd([ command, here]) - only does it in the report, nothing to log and screen

$self->ls_ld_each_part_of( '/first/second/etc', $report_only, $skip_root) - does these commands with output_system_cmd

 ls -ld /first/second/etc
 ls -ld /first/second
 ls -ld /first
 ls -ld / (if $skip_root is false)

$self->ls_ld_each_part_of( '/first/second/etc', 1, $skip_root ) - does those commands with report_system_cmd

$self->tail_last_lines_of( $file, $number_of_lines, $report_only_boolean )

$self->_append_to_report( $data, $title );
$self->_append_to_report( $data );

=head1 CUSTOM OPT MODS

You can create your own 'drop in' custom option module using the Cpanel::Easy framework.

First determine your name space as per the rest of the system.

For example, the file structure of opt mods for use with the Cpanel::Easy::Apache namespace (even if they are Cpanel::Easy::SomethingBesidesApache ) go into /var/cpanel/easy/apache/custom_opt_mods/

So if your module is Cpanel::Easy::Apache::Foo it'd go into /var/cpanel/easy/apache/custom_opt_mods/Cpanel/Easy/Apache/Foo.pm

An example is an option module that adds a step to PHP that applies a patch to PHP5's source code before it builds:

This assumes that you have the Turkish locale patch for PHP5, which can be found at L<http://go.cpanel.net/customea>, in root's home directory:

  cd  /var/cpanel/easy/apache/custom_opt_mods/
  tar xzf ~/custom_opt_mod_turkish_locale_php5_patch.tar.gz

That's it! It will be there ready to use like any other ea3 option.
