Development Guides Home >> Quickstart Development Guide

Tutorial - Create a Standardized Hook

Introduction

This tutorial adds custom hooks to preserve a customized configuration file. The cPanel & WHM update process (upcp) sometimes overwrites customized files with the cPanel-provided default versions. If you experience this problem, use our Standardized Hooks system to preserve your customizations.

To solve this problem, this tutorial creates the following hooks:

  • A pre -stage hook that runs immediately before the upcp script to copy a customized Roundcube configuration file from /usr/local/cpanel/base/3rdparty/roundcube/config/config.inc.php to the config.inc.php.temp file.
  • A post -stage hook that runs immediately after the upcp script to copy the config.inc.php.temp file back into the config.inc.php file, and then deletes the config.inc.php.temp file.

This tutorial creates a Perl module, but you can also write hook action code in PHP or any other preferred programming language. For more information, read our Hook Action Code documentation.

Create a file to contain your hook action code

Create a new Perl file to contain your hook action code.

  • The filename that you use must meet the internal Perl interpreter's requirements for module names.
  • This tutorial uses the MyHook.pm file.

Package the module

This declaration instructs Perl to treat all of the file's functions as part of the MyHook module. For more information, read perldoc.perl.org's package documentation.

# Package this module
package MyHook;

Set the strict pragma and use warnings

These declarations instruct Perl to return errors if the file contains potentially-unsafe code.

You can omit the warnings declaration in production code, but we strongly recommend that you use it during development.

# Return errors if Perl experiences problems.
use strict;
use warnings;

Use additional modules

These declarations instruct Perl to use the following additional modules:

  • Cpanel::Logger — This module uses cPanel & WHM's logging functionality to write messages to log files.
  • File::Copy — This core Perl module includes methods to manipulate files.
  • JSON — This module properly encodes and decodes JSON.
# Use cPanel's error logging module.
use Cpanel::Logger;

# Use the core Perl module with file-copying functionality.
use File::Copy;

# Properly decode JSON.
use JSON;

Instantiate a new Cpanel::Logger object

Use the Cpanel::Logger module's new() method to instantiate a new logging object.

# Instantiate the cPanel logging object.
my $logger = Cpanel::Logger->new();

By default, the new() method creates an object that logs to the /usr/local/cpanel/logs/error_log file.

  • You can also specify a custom log location.

Create a describe() method to embed hook attributes in your code

Create a describe() method that contains the attributes for all of the hooks in the code. This step is optional, but it allows you to simplify the way in which you call the /usr/local/cpanel/bin/manage_hooks utility when you register the hooks.

For this tutorial, the describe() method includes the attributes for the following hooks:

  • The first hook executes the MyHook module's copyfile subroutine during the pre stage of the System category's upcp event.
  • The second hook executes the MyHook module's replacefile subroutine during the post stage of the System category's upcp event.

For more information, read our Guide to Standardized Hooks - The manage_hooks Utility documentation.

# Embed hook attributes alongside the action code.
sub describe {
    my $hooks = [
        {
            'category' => 'System',
            'event'    => 'upcp',
            'stage'    => 'pre',
            'hook'     => 'MyHook::copyfile',
            'exectype' => 'module',
        },{
            'category' => 'System',
            'event'    => 'upcp',
            'stage'    => 'post',
            'hook'     => 'MyHook::replacefile',
            'exectype' => 'module',
        }
    ];
    return $hooks;
}

Specify the files to manipulate

Create variables that contain the absolute paths for the configuration file that you wish to preserve and the temporary file that you wish to create in order to preserve it. The temporary file's location is arbitrary.

# Set the file to preserve and the temp file location.
my $preserve = "/usr/local/cpanel/base/3rdparty/roundcube/config/config.inc.php";
my $temp = "/usr/local/cpanel/base/3rdparty/roundcube/config/config.inc.php.temp";

Create the pre hook's subroutine

The system will run the copyfile subroutine immediately before the upcp process.

The following example:

  • Reads the entire @_ stream until end-of-line (EOL).
    • Every hook subroutine must perform this action.
    • Because this code uses the JSON module, it treats this data as a JSON-encoded data structure. After the system decodes the JSON string, the native data structure becomes a hash.
  • Adds a logging action to the subroutine. This step is optional, but we strongly recommend that you include it for debugging purposes. *Uses the File::Copy module's copy() function to copy the contents of the $preserve file into the $temp file.
    • If the $temp file does not already exist, the system will automatically create it.
    • If the copy fails, the system will log a failure message that includes any error messages. Failure does not block the upcp event.
# Before upcp, copy the file to the temp file.
sub copyfile {
    # Get the data that the system passes to the hook.
    my ( $context, $data ) = @_;

    # Add a log entry for debugging.
    $logger->info("***** Copy my file before upcp *****");

    # Copy my file.
    copy($preserve,$temp) or die "Copy failed: $!";
};

Make certain that you use the same subroutine names that you specified in the create a describe() method step.

Create the post hook's subroutine

The system will run the replacefile subroutine immediately after the upcp process.

The following example:

  • Reads the entire @_ stream until end-of-line (EOL).
    • Every hook subroutine must perform this action.
    • Because this code uses the JSON module, it treats this data as a JSON-encoded data structure. After the system decodes the JSON string, the native data structure becomes a hash.
  • Adds logging actions to the subroutine. This step is optional, but we strongly recommend that you include it for debugging purposes.
  • Uses the File::Copy module's copy() function to copy the contents of the $temp file back into the $preserve file.
    • If the $preserve file no longer exists, the system will automatically create it.
    • If the copy fails, the system will log a failure message that includes any error messages.
  • Deletes the $temp file.
    • This step is optional, but ensures that your system does not retain unnecessary files.
    • If the deletion fails, the system will log a failure message that includes any error messages.
# After upcp, copy the file back into place.
sub replacefile {
    # Get the data that the system passes to the hook.
    my ( $context, $data ) = @_;

    # Add a log entry for debugging.
    $logger->info("***** Replace the temp file *****");

    # Replace my file.
    copy($temp,$preserve) or die "Replacement failed: $!";

    # Add a log entry for debugging.
    $logger->info("***** Delete the temp file *****");

    # Delete the temp file to keep cruft off my system.
    unlink($temp) or die "Cruft removal failed: $!";
};

Make certain that you use the same subroutine names that you specified in the create a describe() method step.

Save the file

Save your hook action code to one of the following locations on your cPanel & WHM server:

  • /opt/cpanel/perl5/514/site_lib/x86_64-linux-64int
  • /opt/cpanel/perl5/514/site_lib
  • /var/cpanel/perl5/lib

You must save hook action code in one of these locations, or you will receive an @INC error when you attempt to register the hook.

Your final file should appear similar to the following example:

#!/usr/bin/perl

# Package this module.
package MyHook;

# Return errors if Perl experiences problems.
use strict;
use warnings;

# Use cPanel's error logging module.
use Cpanel::Logger;

# Use the core Perl module with file-copying functionality.
use File::Copy;

# Properly decode JSON.
use JSON;

# Instantiate the cPanel logging object.
my $logger = Cpanel::Logger->new();

# Embed hook attributes alongside the action code.
sub describe {
    my $hooks = [
        {
            'category' => 'System',
            'event'    => 'upcp',
            'stage'    => 'pre',
            'hook'     => 'MyHook::copyfile',
            'exectype' => 'module',
        },{
            'category' => 'System',
            'event'    => 'upcp',
            'stage'    => 'post',
            'hook'     => 'MyHook::replacefile',
            'exectype' => 'module',
        }
    ];
    return $hooks;
}

# Set the file to preserve and the temp file location.
my $preserve = "/usr/local/cpanel/base/3rdparty/roundcube/config/config.inc.php";
my $temp = "/usr/local/cpanel/base/3rdparty/roundcube/config/config.inc.php.temp";

# Before upcp, copy the file to the temp file.
sub copyfile {
    # Get the data that the system passes to the hook.
    my ( $context, $data ) = @_;

    # Add a log entry for debugging.
    $logger->info("***** Copy my file before upcp *****");

    # Copy my file.
    copy($preserve,$temp) or die "Copy failed: $!";
};

# After upcp, copy the file back into place.
sub replacefile {
    # Get the data that the system passes to the hook.
    my ( $context, $data ) = @_;

    # Add a log entry for debugging.
    $logger->info("***** Replace the temp file *****");

    # Replace my file.
    copy($temp,$preserve) or die "Replacement failed: $!";

    # Add a log entry for debugging.
    $logger->info("***** Delete the temp file *****");

    # Delete the temp file to keep cruft off my system.
    unlink($temp) or die "Cruft removal failed: $!";
};

1;

Register your hooks

Run the following command as the root user to register the two new hooks:

/usr/local/cpanel/bin/manage_hooks add module MyHook

If the system registered the hooks correctly, the script produces the following output:

Added hook for System::upcp to hooks registry
Added hook for System::upcp to hooks registry

If you do not include the describe() method in your custom code, you must include additional information when you run this script. For more information, read our Guide to Standardized Hooks - The manage_hooks Utility documentation.

Test your hooks

To test upcp hooks, click Click to Upgrade in WHM's Upgrade to Latest Version interface (WHM >> Home >> cPanel >> Upgrade to Latest Version) or run the following command:

/usr/local/cpanel/scripts/upcp

After the upcp process finishes, the /usr/local/cpanel/logs/error_log file should contain the following entries:

[2015-06-16 11:08:29 -0500] info [hook] ***** Copy my file before upcp *****
[2015-06-16 11:08:29 -0500] info [hook] ***** Replace the temp file *****
[2015-06-16 11:08:29 -0500] info [hook] ***** Delete the temp file *****

If some or all of these entries do not appear in the error log, or if you see error messages in the same portion of the file, you may need to troubleshoot issues on your system.

Troubleshooting

If you experience problems with hooks, the following troubleshooting methods may help you to find and fix the issue:

Check whether your hooks registered properly

To view a list of registered hooks, run the following command:

/usr/local/cpanel/bin/manage_hooks list

If the hooks registered successfully, the output should resemble the following example:

System:
    upcp:
        stage: pre
        weight: 100
        id: lb6vAuWAZ9JNmzIIEsiLAeJw
        exectype: module
        hook: MyHook::copyfile
        --
        stage: post
        weight: 200
        id: NhhoUZ1GnGSfpi3dpcClKrnT
        exectype: module
        hook: MyHook::replacefile

Add more logging to your hook action code

To cause your hook action code to log additional information, perform the following steps:

  1. Add the following subroutine to your hook action code:
    # Log extra info for debugging.
    sub _more_logging {
    	my ( $context, $data ) = @_;
    	my $pretty_json = JSON->new->pretty;
    	$logger->info( $pretty_json->encode($context) );
    	$logger->info( $pretty_json->encode($data) );
    }
  2. Add the following line of code to any points in your code that you believe may cause the problems:
    _more_logging( $context, $data );

This subroutine causes the function to log additional information about the hook event to the error log file, which appears similar to the following example:

[2015-06-16 11:08:29 -0500] info [hook] {
   "stage" : "pre",
   "point" : "main",
   "category" : "System",
   "event" : "upcp"
}