File: //scripts/create_default_featurelist
#!/usr/local/cpanel/3rdparty/bin/perl
# Copyright 2025 WebPros International, LLC
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.
package scripts::create_default_featurelist;
use cPstrict;
=encoding utf-8
=head1 NAME
create_default_featurelist - Create default feature list with a specified feature and its dependencies
=head1 USAGE
create_default_featurelist --feature <feature_name> [--preferred-featurelist-name <name>] [--verbose] [--help]
=head1 DESCRIPTION
This script creates a default feature list that includes a specified feature and all
of its dependencies. The feature list is created using the WHM API v1 C<create_featurelist>
function.
This is a generic script that can be used by any plugin to create its default feature list.
The script will check if the feature list already exists before attempting to create it,
and will skip creation if it is already present on the system.
=head1 OPTIONS
=over 4
=item B<--feature>, B<-f> (required)
The name of the feature to include in the feature list. This feature and all of its
dependencies will be added to the created feature list.
=item B<--preferred-featurelist-name>, B<-p> (optional)
The preferred name for the feature list. If not provided, the feature name will be used.
The actual feature list name will be automatically prefixed with C<__default_>.
For example:
=over 4
=item * C<--feature standalone-pal --preferred-featurelist-name nova> creates C<__default_nova>
=item * C<--feature standalone-pal> (no preferred name) creates C<__default_standalone-pal>
=back
=item B<--verbose>, B<-v>
Enable verbose output. When enabled, the script will output detailed information about
the feature list creation process, including:
=over 4
=item * Feature names and dependencies being processed
=item * Existing feature lists on the system
=item * The exact whmapi1 command being executed
=item * Success and status messages
=back
=item B<--help>, B<-h>
Display the help message and exit.
=back
=head1 EXAMPLES
=over 4
=item Create feature list for standalone-pal (silent mode)
create_default_featurelist --feature standalone-pal
=item Create feature list with custom name
create_default_featurelist --feature standalone-pal --preferred-featurelist-name nova
=item Create feature list with verbose output
create_default_featurelist --feature standalone-pal --verbose
create_default_featurelist -f standalone-pal -v
=item Create feature list with custom name and verbose output
create_default_featurelist -f standalone-pal -p nova -v
=item Show help
create_default_featurelist --help
=back
=head1 EXIT STATUS
The script exits with status 0 on success. If the feature list creation fails,
the script exits with status 1 and outputs error messages to STDERR.
=head1 DEPENDENCIES
=over 4
=item * L<Cpanel::Features::Metadata> - To load feature dependencies
=item * L<Cpanel::Features::Lists> - To check existing feature lists
=item * L<Cpanel::SafeRun::Object> - To execute whmapi1 safely
=item * L<Getopt::Long> - For command-line option parsing
=back
=head1 SEE ALSO
L<whmapi1>, L<Cpanel::Features::Metadata>
=cut
use Cpanel::Features::Metadata ();
use Cpanel::SafeRun::Object ();
use Getopt::Long ();
use constant WHMAPI1_PATH => '/usr/local/cpanel/bin/whmapi1';
our $VERBOSE = 0;
run() if !caller;
sub run {
my $FEATURE_NAME;
my $PREFERRED_FEATURELIST_NAME;
my $DEFAULT_FEATURELIST_NAME;
# Parse command line options
Getopt::Long::GetOptions(
'verbose|v' => \$VERBOSE,
'help|h' => sub { print_help(); exit 0; },
'feature|f=s' => \$FEATURE_NAME,
'preferred-featurelist-name|p=s' => \$PREFERRED_FEATURELIST_NAME,
) or die "Invalid options\n";
if ( !$FEATURE_NAME ) {
die "Feature name is required. Use --feature <feature_name> to specify it.\n";
}
# Validate feature name
if ( length($FEATURE_NAME) < 1 || length($FEATURE_NAME) > 255 ) {
die "Feature name must be between 1 and 255 characters.\n";
}
# Validate feature name characters
if ( $FEATURE_NAME !~ /\A[a-zA-Z0-9_-]+\z/ ) {
die "Feature name contains invalid characters. Only alphanumeric characters, underscores (_), and hyphens (-) are allowed.\n";
}
# Validate preferred featurelist name if provided
if ( defined($PREFERRED_FEATURELIST_NAME) ) {
if ( length($PREFERRED_FEATURELIST_NAME) < 1 || length($PREFERRED_FEATURELIST_NAME) > 255 ) {
die "Preferred featurelist name must be between 1 and 255 characters.\n";
}
if ( $PREFERRED_FEATURELIST_NAME !~ /\A[a-zA-Z0-9_-]+\z/ ) {
die "Preferred featurelist name contains invalid characters. Only alphanumeric characters, underscores (_), and hyphens (-) are allowed.\n";
}
}
# default it to feature name if preferred featurelist name is not provided
my $featurelist_name = $PREFERRED_FEATURELIST_NAME // $FEATURE_NAME;
$DEFAULT_FEATURELIST_NAME = get_default_featurelist_name($featurelist_name);
_verbose("Starting feature list creation for Nova");
_verbose( "Feature name: " . $FEATURE_NAME );
_verbose( "Feature list name: " . $DEFAULT_FEATURELIST_NAME );
# verify that if the feature list already exists
if ( verify_if_featurelist_exists($DEFAULT_FEATURELIST_NAME) ) {
_verbose("Feature list already exists, exiting");
return 0;
}
# Populate the feature list for NOVA
_verbose("Populating feature list with dependencies");
# populate feature list will always include FEATURE_NAME in the list
# even if there are no dependencies
my $feature_list = populate_feature_list($FEATURE_NAME);
return 0 unless $feature_list;
_verbose( "Found " . scalar( keys %$feature_list ) . " features to add" );
# execute whmapi1 to create the feature list
_verbose("Creating feature list via whmapi1");
return create_feature_list( $DEFAULT_FEATURELIST_NAME, $feature_list );
}
=head2 populate_feature_list
Populates the feature list hash with the main feature and its dependencies.
This function loads the feature metadata for the specified feature and retrieves all
of its dependencies. The behavior differs based on whether a metadata file exists:
=over 4
=item * If metadata file exists and loads successfully: Returns the main feature plus all dependencies
=item * If metadata file exists but fails to load: Logs an error and returns undef (skips feature list creation)
=item * If metadata file does not exist: Returns only the main feature with no dependencies
=back
=head3 Parameters
=over 4
=item * C<$feature_name> - The name of the feature to load
=back
=head3 Returns
=over 4
=item * Hashref where keys are feature names and values are 1, representing enabled features
=item * C<undef> if metadata file exists but fails to load (indicates creation should be skipped)
=back
=head3 Example
my $features = populate_feature_list('standalone-pal');
# Returns: { 'standalone-pal' => 1, 'dependency1' => 1, 'dependency2' => 1 }
# Or: { 'standalone-pal' => 1 } if no metadata file exists
# Or: undef if metadata exists but is corrupted
=cut
sub populate_feature_list($feature_name) {
my $metadata = Cpanel::Features::Metadata->new( feature => $feature_name );
my $features = eval { $metadata->load()->dependencies; } // [];
if ( my $exception = $@ ) {
if ( $metadata->is_metadata_exists ) {
# something wrong with metadata load, so skip creating a default feature list
warn "Failed to load feature metadata for '$feature_name': '$exception'\n";
warn "Skipping default feature list creation due to metadata load failure.\n";
return;
}
else {
_verbose("The metadata file does not exist, creating feature list with main feature only");
}
}
_verbose( "Dependencies loaded: " . join( ', ', @{$features} ) ) if @{$features};
push @$features, $feature_name;
# convert to hash
my %feature_hash = map { $_ => 1 } @{$features};
return \%feature_hash;
}
=head2 create_feature_list
Creates the default feature list using the WHM API v1.
This function constructs the appropriate whmapi1 command with all the features
to be enabled and executes it using L<Cpanel::SafeRun::Object>. It handles
execution failures by logging errors to STDERR.
=head3 Parameters
=over 4
=item * C<$featurelist_name> - The name of the feature list to create
=item * C<$feature_list> - Hashref of features to enable (keys are feature names, values are 1)
=back
=head3 Returns
=over 4
=item * C<0> - Success (Unix exit code convention)
=item * C<1> - Failure (Unix exit code convention)
=back
=head3 Example
my $result = create_feature_list('__default_nova', { 'standalone-pal' => 1, 'feature2' => 1 });
if ($result == 0) {
print "Feature list created successfully\n";
}
=cut
sub create_feature_list ( $featurelist_name, $feature_list ) {
my @args = ( 'create_featurelist', 'featurelist=' . $featurelist_name );
foreach my $feature ( keys %$feature_list ) {
push @args, "$feature=1";
}
_verbose( "Executing: " . WHMAPI1_PATH . " " . join( ' ', @args ) );
my $result = Cpanel::SafeRun::Object->new(
program => WHMAPI1_PATH,
args => \@args,
);
if ( $result->exec_failed ) {
warn "Failed to execute whmapi1 for feature list '$featurelist_name'.\n";
warn $result->stderr . "\n";
return 1;
}
_verbose("Feature list created successfully");
_verbose( "Output: " . $result->stdout ) if $result->stdout;
return 0;
}
sub get_default_featurelist_name($feature_name) {
# Adding unique name to avoid conflicts
return '__default_' . $feature_name;
}
sub verify_if_featurelist_exists($featurelist_name) {
require Cpanel::Features::Lists;
_verbose("Checking if feature list already exists");
my @existing_featurelists = Cpanel::Features::Lists::get_feature_lists(1);
_verbose( "Existing feature lists: " . join( ', ', @existing_featurelists ) );
if ( grep { $_ eq $featurelist_name } @existing_featurelists ) {
warn "Feature list " . $featurelist_name . " already exists. Skipping creation.\n";
return 1;
}
# feature list is not yet available, so lets create one
return 0;
}
sub _verbose ($message) {
return unless $VERBOSE;
say "[VERBOSE] $message";
}
sub print_help {
print <<"END_HELP";
Usage: $0 --feature <feature_name> [OPTIONS]
Create a default feature list with the specified feature and its dependencies.
Required:
-f, --feature <name> The feature to include in the feature list
Options:
-p, --preferred-featurelist-name <name> Preferred name for the feature list (optional)
If not provided, uses the feature name
Final name will be prefixed with '__default_'
-v, --verbose Enable verbose output
-h, --help Show this help message
Examples:
$0 --feature standalone-pal # Create feature list
$0 -f standalone-pal -p nova # Custom feature list name
$0 --feature standalone-pal --verbose # With verbose output
$0 -f standalone-pal -p nova -v # All options
END_HELP
}
1;