Power of Perl – Controlling and expanding Cpanel/WHM through the Cpanel/WHM API

When running a shared hosting environment, it is impossible to stay competitive without the use of some form of control panel. And in the world of linux shared web hosting, no control panel is as widely used as cPanel Inc‘s cPanel/WHM combo. The Cpanel team has put a lot of time and effort into the remote administration of WHM, and through that Cpanel. With a little ingenuity and not much work, really, this API can be extended to include any functions you can imagine, up to and including the system administration of the machine itself. So lets look at the basics thereof.

For this example I am going to use perl, for several very good reasons. The first is that 96% (roughly) of cPanel/WHM is programmed in perl and sits in uncompiled files that we can use for reference. Secondly, perl is inherently a system administration language and therefore gives us much more native system functionality, not to mention its powerful regular expression engine. And, lastly, I choose perl because the heart of this entire process is already done for us by cPanel themselves and available for download as a perl package here.

If you look through the package, you will see that it already has quite a bit of functionality built in. In fact, it really has most everything you would need to automate the web hosting process. However that’s not enough for us, we want more!

The first thing we are going to want to edit is the new subroutine in the Accounting package. You can see it is stubbed out already:

sub new {
    my ($host);
    my ($user);
    my ($accesshash);
    my ($error);
    my ($usessl);
    my ($timeout) = 300;
    my $self = {};

    bless($self);

    return ($self);
}

This function is going to need to establish 4 things that we will use for the API calls. First, a host name. Simple enough. Second, a user. Generally, this is root, though it can be just about anyone with WHM access. Third, an accesshash. This can be found in the user we specified’s home directory in a text file called .accesshash. If one doesn’t exist, login to WHM through the web interface (https://yoursever:2087/ and goto the remote access hash link and it will generate and create said text document. Finally, we need to know whether or not to use SSL to communicate. There are very few times when you want that to be no.

My changed new subroutine looks something like this:

sub new {
    my $class = shift;
    my $name = shift;
    my $user = shift;
    my $accesshash = shift;
    my $usessl = shift;
    my $self = {
       host => $name,
       user => $user,
       accesshash => $accesshash,
       error => "",
       usessl => $usessl,
       timeout => 300
    };
    bless($self, $class);
    return ($self);
}

This is an overly simplified example of a possible solution. In my production code, I use a Class::DBI based class to pull the info I need from a database and pass it into here where this subroutine extracts the information it needs. Pretty easy, isn’t it?

Next we create a stub program to use the package we just edited.

#! /usr/bin/perl -w

use lib "/usr/local/hsw/libs/";
use C::Accounting;
use Data::Dump;

my $name = shift @ARGV || "server1";
my $func = shift @ARGV || "listxmlapps";

open(AHASH, "/root/.accesshash");
my @access = ;
close AHASH;
my $accesshash = join("n", @access);

my $whm = C::Accounting($name.".mydomain.com", "root", $accesshash, 1);
print "Running ".$func." on ".$name." with ".join(" ", @ARGV)."n";
my $t;
eval '$t = $whm->'.$func.'(@ARGV)'; # If you have a better way of doing 
                                    # this, please let me know!
if($@) { print "ERROR : ".$@."n"; }
if($s->{error} ne "") { print "ERROR : ".$s->{error}."n"; }
Data::Dump->dump([[$t]]);

So here we take the first two arguments passed in on ARGV, and assign them to the host and a function for us to run on it. We get the access hash from root’s home directory then we create an Accounting object. I use eval to run the function that was passed in on Accounting object and pass it in what is left in ARGV, if anything. We do some error checking and display them if they exist, then I used Data::Dump to result to the screen. Simple, but it gives us what we need to see the API at work.

If we run this from the commandline, we get something like this:

-bash-3.2# ./testnsclient
Running listxmlapps on server1 with 
(
  "C::Dump",
  [
    [
      {
        app => [
                 "adddns",
                 "addip",
                 "applist",
                 "delip",
                 "dumpzone",
                 "fetchsslinfo",
                 "gethostname",
                 "getlanglist",
                 "killdns",
                 "listacls",
                 "listcrts",
                 "listips",
                 "listzones",
                 "loadavg",
                 "lookupnsip",
                 "myprivs",
                 "nvget",
                 "nvset",
                 "passwd",
                 "reboot",
                 "restartservice",
                 "sethostname",
                 "setresolvers",
                 "version",
               ],
      },
    ],
  ],
)

By throwing in a few arguments, we run a different function:

-bash-3.2# ./testnsclient server1 loadavg
Running loadavg on server1 with 
(
  "C::Dump",
  [[{ fifteen => "0.14", five => "0.18", one => "0.17" }]],
)

So awesome! Now what if we want to expand on this to do something cPanel doesn’t natively do? Suppose we wanted more information than what the loadavg command just gave us? Well, we need to goto the cPanel server itself. We navigate to /usr/local/cpanel/whostmgr/docroot and create a directory in there for our functions. We could create files directly in the docroot, but we don’t want our stuff interfering with cPanel’s, nor vice versa. I called mine hsw. In there, let’s create a file called load.cgi and put this inside:

#! /usr/bin/perl -w

use strict;
use CGI;

my $q = new CGI;
print $q->header();
open(ME, "|-", "cat /proc/loadavg");
while()
{
  print $_;
}

Chmod the file to be 755 and run it, it gives this:

-bash-3.2# ./load.cgi     
Content-Type: text/html; charset=ISO-8859-1

0.28 0.18 0.15 2/96 12631

Great! So that works. Now we head back to our C::Accounting package and we add a function to handle this new functionality.

sub load
{
    my ($self) = @_;
    my (%PKGS);

    my (@PAGE) = $self->whmreq("/hsw/load.cgi");

    if ( $self->{error} ne "" ) { return (); }

    foreach $_ (@PAGE) {
        s/n//g;
        my @contents = split(/s/, $_);
        $PKGS{"1min"}    = $contents[0];
        $PKGS{"5min"}    = $contents[1];
        $PKGS{"15min"}   = $contents[2];
        my @procs = split(///, $contents[3]);
        $PKGS{"running"} = $procs[0];
        $PKGS{"total"}   = $procs[1];
        $PKGS{"lastPID"} = $contents[4];
    }
    return %PKGS;
}

So we run the subroutine whmreq with the path to our script as an argument. We are given back an array of lines that were returned. We simply parse that out and assign the information to appropriate indexes in a hash table. We then pass the reference of the hash back. By running our stub-app, we get the following:

-bash-3.2# ./testnsclient server1 load
Running load on server1 with 
(
  "C::Dump",
  [
    [
      {
        "15min" => "0.10",
        "1min"  => "0.07",
        "5min"  => "0.13",
        lastPID => 21897,
        running => 1,
        total   => 89,
      },
    ],
  ],
)

Well, that gives you the basics you need to expand the capability of cPanel/WHM’s API to include your own functionality and subroutines. Now you can get your cpanel/WHM boxes, even the DNS Only versions, to do anything that perl can do… remotely and programmatically.

This is a very powerful tool, use it wisely.

Leave a Reply