Taking Back Control: Granular Sitejet Management on cPanel Servers
The hosting landscape is constantly evolving. Recently, WebPros has heavily integrated Sitejet into the core cPanel experience. While Sitejet is a powerful builder, it’s not the right fit for every hosting provider. Many companies have already invested in competing site builders, prefer to keep their interface lightweight, or are looking forward to WebPros’ own upcoming “Nova” builder and don’t want to invest in Sitejet in the interim.
The challenge for sysadmins is that simply disabling Sitejet globally via the WHM Feature Manager does not affect already deployed websites, but it does prevent customers who are still developing and continuously editing their published Sitejet sites from using the editor. To avoid back-and-forth support tickets to re-enable the editor for those users, we need a way to opt out new users while keeping active users’ editors functional.
Today I’m sharing a script I designed to address this scenario by automating the complex tasks of segmenting users, cloning packages, and reassigning features—so you can manage your site builder offering on your own terms.
To disable Sitejet safely, you must perform a multi-step migration:
- Identify users actively publishing with Sitejet.
- Backup existing configurations.
- Create a new Feature List with Sitejet enabled.
- Clone existing hosting Packages and tie them to this new Feature List.
- Move the active Sitejet users to these new packages.
- Finally, disable Sitejet in the
defaultfeature list for everyone else.
Doing this manually via the WHM GUI for hundreds or thousands of accounts is practically impossible and highly prone to error.
Introducing the cPanel Sitejet Management Toolkit I have created two scripts to automate this lifecycle, now available on GitHub.
1. detects_users.sh (The Audit Tool) This script is non-invasive. It scans all accounts (or filters by OWNER=root) and generates a detailed CSV classification of Sitejet usage. It determines if a site is:
sitejet_standard_active: Published, deployed, and currently visible.sitejet_present_overridden: Metadata exists, but the user has manually uploaded anindex.php, effectively hiding the Sitejet site.sitejet_meta_only: The user opened the builder but never published.
This is your first step to understand your actual exposure before making changes.
2. operate.sh (The Migration Tool) This is an interactive script that performs the heavy lifting. It can operate in two modes: scanning all users with Sitejet data, or using the --active-only flag (recommended) to target strictly active, non-overridden sites.
It handles the entire workflow:
- Creates timestamped backups of
/var/cpanel/packagesand/var/cpanel/features. - Creates a
default_sitejetfeature list based on your current default, with Sitejet enabled. - Identifies the packages used by target users and clones them (e.g.,
StarterbecomesStarter_with-sitejet). - Crucially: It replicates CloudLinux LVE limits to the new cloned packages if present.
- Moves eligible users to the new
_with-sitejetpackages viawhmapi1 changepackage. - Provides the option to disable Sitejet in the standard
defaultfeature list.
The Result: An “Opt-In” Workflow After running these scripts, Sitejet is disabled for new signups and existing users on standard packages. If a client demands Sitejet, you simply move them to one of the _with-sitejet packages, and it appears in their cPanel interface. Active users never see a service interruption.
Conclusion Whether you are waiting for Nova, promoting a different builder, or just disagree with the default WebPros orientation, these scripts give you the logic needed to assert control over your cPanel environment without risking existing customer data.
readme.md
# cPanel Sitejet Migration & Management Tools
## Overview
As WebPros pushes Sitejet deeper into the cPanel ecosystem, many hosting providers require granular control over its availability. Some providers prefer not to invest in Sitejet due to existing partnerships with other site builders, or because they are awaiting WebPros' upcoming "Nova" builder and wish to disable Sitejet in the interim.
Simply disabling Sitejet globally can break existing customer sites.
This repository provides robust, defensive Bash tools to automate the migration of active Sitejet users to a dedicated, parallel packaging structure. This allows administrators to disable Sitejet in the `default` WHM feature list while ensuring uninterrupted service for clients actively using the builder.
## Features
* **`detects_users.sh`**: Audits the server and generates a CSV classifying usage states (active, overridden, metadata only). Supports filtering by `OWNER=root`.
* **Parallel Packaging Model**: Leaves standard packages untouched. Sitejet becomes an "opt-in" by assigning users to parallel packages (e.g., `packageName` -> `packageName_with-sitejet`).
* **CloudLinux Integration**: Automates the cloning of LVE limits from original packages to cloned packages using `lvectl` or `lve-manager`.
* **Defensive Design**:
* Requires root privileges.
* Syntactically strict Bash (errexit, nounset, pipefail).
* Interactive operation with explicit prompts.
* Full Dry-Run support (`SITEJET_DRY=1`).
* Automated backups of package and feature directories before modification.
## Prerequisites
* A cPanel & WHM server.
* Root access.
* `jq` installed (standard in recent cPanel installs, otherwise `dnf install jq`).
## Installation
```bash
mkdir -p /root/workspace/sitejet
cd /root/workspace/sitejet
# Copy the scripts operate.sh and detects_users.sh into this directory
chmod +x operate.sh detects_users.sh
```
All logs, backups, and artifacts are stored within /root/workspace/sitejet/.
Usage
1. Audit Current Usage (detects_users.sh)
Generate a full report of all accounts utilizing Sitejet, classified by their actual publication status.
Bash
sudo ./detects_users.sh
To limit the audit strictly to accounts owned by root:
Bash
sudo SITEJET_OWNER_FILTER=root ./detects_users.sh
Artifact: /root/workspace/sitejet/logs/sitejet_pkg_verify_full.csv
2. Execute Migration (operate.sh)
This script is interactive. It is highly recommended to run a dry-run first.
Dry-Run (Simulate changes):
Bash
sudo SITEJET_DRY=1 ./operate.sh --active-only
Live Execution (Recommended mode: Active Users Only):
The --active-only flag ensures only users with published, standard (not overridden) Sitejet sites are migrated to the parallel packages.
Bash
sudo ./operate.sh --active-only
The Migration Lifecycle Explained
When running operate.sh, the following workflow is executed based on your prompt responses:
Backup: /var/cpanel/packages and /var/cpanel/features are backed up to /root/workspace/sitejet/backups/[TIMESTAMP].
Feature List Creation: A new feature list named default_sitejet is created as a clone of default, with the Sitejet key enabled.
Package Cloning: Packages used by active Sitejet users are cloned. New package name format: [OriginalName]_with-sitejet.
LVE Replication: If CloudLinux is present, LVE limits from original packages are replicated to the _with-sitejet packages.
Disable Default: An option is provided to set the Sitejet feature key to 0 in the standard /var/cpanel/features/default file.
User Migration: Target users are moved to the new _with-sitejet packages via whmapi1 changepackage.
## Rollback
The scripts provide detailed rollback hints upon completion.
To rollback feature/package configurations:
Replace the contents of /var/cpanel/packages/ and /var/cpanel/features/ with the contents of the latest backup directory created by the script.
To rollback users:
Consult /root/workspace/sitejet/logs/sitejet_pkg_moves.log for the "before and after" state, and use whmapi1 changepackage to return them to their original package.
Disclaimer
These scripts modify core cPanel configurations and user packages. While designed defensively, they are provided "as-is". The user assumes all responsibility for execution. Always review dry-run output and ensure backups are functional before running live on a production environment.
---
### Part 3: Sanitized Execution Example
Here is the example execution, sanitized as requested. Usernames, specific package names (replaced with generic 'Bronze/Silver'), IDs, and timestamps have been changed.
```text
# /root/workspace/sitejet/operate.sh --active-only
Sitejet migration final script (OWNER=root only)
Workdir: /root/workspace/sitejet
Backups dir: /root/workspace/sitejet/backups
Logs: /root/workspace/sitejet/logs
DRY_RUN: 0
NOTICE: Running in ACTIVE_ONLY mode. Only users meeting strict publishing criteria will be included.
Generate users CSV (only OWNER=root) at /root/workspace/sitejet/sitejet_users.csv now? [y/n] (default: y): y
--- DEBUG: Starting generate_users_csv User Scan (find | while read) ---
--- FILTER: ENFORCING STRICT ACTIVE USER CRITERIA (Published, Deployed, No index.php override) ---
...
DEBUG: Checking file: /var/cpanel/users/alpha
DEBUG: Checking file: /var/cpanel/users/betauser
DEBUG: Checking file: /var/cpanel/users/gammasite
DEBUG: Checking file: /var/cpanel/users/deltacorp
--- DEBUG: Finishing generate_users_csv (Final Count: 28) ---
Detected 28 OWNER=root users with Sitejet metadata.
Run detailed detection and produce /root/workspace/sitejet/logs/sitejet_detection_details.csv? [y/n] (default: y): y
Detection CSV: /root/workspace/sitejet/logs/sitejet_detection_details.csv
Create full backup of /var/cpanel/packages and /var/cpanel/features to workspace backups now? [y/n] (default: y): y
Backups created at: /root/workspace/sitejet/backups/20261015T090001
Detected feature key: sitejet
Create /var/cpanel/features/default_sitejet (Sitejet enabled) based on default? [y/n] (default: y): y
Sitejet-enabled feature list created: /var/cpanel/features/default_sitejet
<not present>
Disable Sitejet in default feature file (set sitejet=0) now? This is reversible via backup. [y/n] (default: n): y
Default feature sitejet set to 0. Backup: /root/workspace/sitejet/backups/20261015T090001/features-default.bak
Packages to clone (3):
- Bronze Plan
- Silver Plan
- Corporate Dedicated
Proceed to clone packages and bind them to the /var/cpanel/features/default_sitejet feature list? [y/n] (default: y): y
Processing package: Bronze Plan
Processing package: Silver Plan
Processing package: Corporate Dedicated
Preview of users CSV (first 20 lines):
1 user,pkg
...
17 user1example,Bronze Plan
18 user2test,Silver Plan
19 user3hosting,Bronze Plan
20 user4site,Corporate Dedicated
Proceed with changepackage for all OWNER=root users in /root/workspace/sitejet/sitejet_users.csv? (Dry-run will simulate) [y/n] (default: n): y
Run strict verification across all OWNER=root users and append to /root/workspace/sitejet/logs/sitejet_pkg_verify.csv? [y/n] (default: y): y
Verification results appended to /root/workspace/sitejet/logs/sitejet_pkg_verify.csv
Migration completed. Artifacts and logs:
- Workspace: /root/workspace/sitejet
- Backups: /root/workspace/sitejet/backups/20261015T090001
- Actions log: /root/workspace/sitejet/logs/sitejet_actions.log
- Moves log: /root/workspace/sitejet/logs/sitejet_pkg_moves.log
- Detection details: /root/workspace/sitejet/logs/sitejet_detection_details.csv
- Verification CSV: /root/workspace/sitejet/logs/sitejet_pkg_verify.csv
Rollback hints:
- Review /root/workspace/sitejet/logs/sitejet_actions.log and /root/workspace/sitejet/logs/sitejet_pkg_moves.log for MOVED entries.
- To rollback a user:
whmapi1 changepackage user=<user> pkg=<original_pkg>
- To restore feature files from backup:
cp -a <BACKUP_DIR>/features-default.bak /var/cpanel/features/default
# If default_sitejet existed before the run, restore it too
[ -f "<BACKUP_DIR>/features-default_sitejet.bak" ] && cp -a <BACKUP_DIR>/features-default_sitejet.bak /var/cpanel/features/default_sitejet
- To restore all backed-up files:
rm -rf /var/cpanel/features /var/cpanel/packages
cp -a <BACKUP_DIR>/packages_features.<ts>.tgz /var
/root/workspace/sitejet/operate.sh
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
# operate.sh
# Interactive Sitejet migration script restricted to accounts owned by OWNER=root
#
# Usage:
# SITEJET_DRY=1 sudo /root/workspace/sitejet/operate.sh # dry-run preview (broad scope)
# sudo /root/workspace/sitejet/operate.sh --active-only # interactive commit (strict scope)
# sudo /root/workspace/sitejet/operate.sh --gen-csv # generate CSV only (broad scope)
#
# Requirements: run as root; jq and whmapi1 must be available.
# ---- Configuration ----
FEATURE_BASE="/var/cpanel/features/default"
# New configuration for the Sitejet-enabled feature list
SITEJET_FEATURE_BASE="/var/cpanel/features/default_sitejet"
PACKAGES_DIR="/var/cpanel/packages"
WORKDIR="/root/workspace/sitejet"
BACKUPS_DIR="${WORKDIR}/backups"
LOG_DIR="${WORKDIR}/logs"
USERS_CSV="${WORKDIR}/sitejet_users.csv"
DETECTION_CSV="${LOG_DIR}/sitejet_detection_details.csv"
VERIFICATION_CSV="${LOG_DIR}/sitejet_pkg_verify.csv"
ACTIONS_LOG="${LOG_DIR}/sitejet_actions.log"
DETAILED_DETECT_LOG="${LOG_DIR}/sitejet_detection.log"
DRY_RUN="${SITEJET_DRY:-0}"
TMPDIR="${WORKDIR}/tmp_$$"
# NEW: Flag to enforce strict active user criteria (1 = enforced, 0 = broad scan)
ACTIVE_ONLY=0
# ---- Helpers ----
timestamp(){ date -Iseconds; }
die(){ echo "$*" >&2; exit 1; }
ensure_root(){ [ "$(id -u)" -eq 0 ] || die "Run as root"; }
# Ensure dirs
prepare_dirs(){
mkdir -p "$WORKDIR" "$BACKUPS_DIR" "$LOG_DIR" "$TMPDIR"
touch "$ACTIONS_LOG" "$DETAILED_DETECT_LOG"
if ! grep -q '^user,' "$VERIFICATION_CSV" 2>/dev/null; then
echo "user,domain,status,index.html,index.php" > "$VERIFICATION_CSV"
fi
}
log_action(){
local action="$1" target="$2" result="$3" backup="$4"
echo "$(timestamp) | ${action} | ${target} | ${result} | ${backup:-}" >> "$ACTIONS_LOG"
}
prompt_yesno(){
local prompt="${1:-Continue?}" default="${2:-y}" answer
while :; do
read -r -p "$prompt [y/n] (default: $default): " answer
answer="${answer:-$default}"
case "${answer,,}" in
y|yes) return 0 ;;
n|no) return 1 ;;
*) echo "Please answer y or n." ;;
esac
done
}
# ---- Prereqs ----
check_prereqs(){
command -v jq >/dev/null 2>&1 || die "jq not found. Install jq and retry."
command -v whmapi1 >/dev/null 2>&1 || die "whmapi1 not found in PATH. Ensure whmapi1 available."
prepare_dirs
}
# ---- Backup utilities ----
create_timestamped_backup(){
local ts backup_dir
ts="$(date +%Y%m%dT%H%M%S)"
backup_dir="${BACKUPS_DIR}/${ts}"
mkdir -p "$backup_dir"
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY_RUN: would create backup at ${backup_dir}"
echo "$backup_dir"
return 0
fi
tar -C /var -czf "${backup_dir}/packages_features.${ts}.tgz" cpanel/packages cpanel/features || true
cp -a "$FEATURE_BASE" "${backup_dir}/features-default.bak" || true
# Backup the new feature base if it exists
[ -f "$SITEJET_FEATURE_BASE" ] && cp -a "$SITEJET_FEATURE_BASE" "${backup_dir}/features-default_sitejet.bak" || true
echo "${backup_dir}"
}
# ---- Detection & users CSV generation (only OWNER=root) ----
# OWNER=root check: read OWNER= value from /var/cpanel/users/<user>
is_owner_root(){
# Defensive initialization for nounset
local user=""
user="${1:-}"
local ufile="/var/cpanel/users/${user}"
if [ ! -f "$ufile" ]; then
return 1
fi
# Extract OWNER value if present
local owner
owner="$(awk -F= '/^OWNER=/{print $2; exit}' "$ufile" 2>/dev/null || echo)"
# Treat missing OWNER as root-owned
if [ -z "$owner" ] || [ "$owner" = "root" ]; then
return 0
fi
return 1
}
# NEW: Checks if a domain for a user meets the strict "active" criteria
# CRITERIA: Published (pub_status=1, date>0), Deployed (has webcard in files list),
# and NOT Overridden (no index.php in webcard folder)
check_active_user(){
local user="$1" domain="$2"
local meta="/home/${user}/.cpanel/sitejet/${domain}"
if [ ! -f "$meta" ]; then
return 1 # Metadata missing
fi
local dr_pub dr_lp files_list document_root webcard_dir
# CRITERIA 1: Published Status
dr_pub="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"
dr_lp="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"
if [ "$dr_pub" != "1" ] || [ "${dr_lp:-0}" -eq 0 ]; then
return 1 # Not published
fi
# CRITERIA 2: Active Deployment
files_list="${meta}-files"
if [ ! -f "$files_list" ] || ! grep -q 'webcard' "$files_list"; then
return 1 # No webcard deployment found
fi
# CRITERIA 3: Not Overridden
document_root="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"
document_root="${document_root:-/home/${user}/public_html}"
document_root="${document_root%/}"
local webcard_dir="${document_root}/webcard"
if [ -d "$webcard_dir" ] && [ -f "${webcard_dir}/index.php" ]; then
return 1 # index.php override detected (not standard active site)
fi
# All strict criteria met: standard active Sitejet user.
return 0
}
generate_users_csv(){
mkdir -p "$(dirname "$USERS_CSV")"
: > "$USERS_CSV"
# Ensure the CSV starts with a header for clean counting
echo "user,pkg" > "$USERS_CSV"
echo "--- DEBUG: Starting generate_users_csv User Scan (find | while read) ---" >&2
# Output the criteria being applied
if [ "$ACTIVE_ONLY" -eq 1 ]; then
echo "--- FILTER: ENFORCING STRICT ACTIVE USER CRITERIA (Published, Deployed, No index.php override) ---" >&2
fi
# FIX: Use find -print0 | while read -d $'\0' for safe iteration
find /var/cpanel/users/ -maxdepth 1 -type f -print0 | while IFS= read -r -d $'\0' ufile; do
# CRITICAL DEBUG STEP: This is now inside the robust loop structure
echo "DEBUG: Checking file: $ufile" >&2
user="$(basename "$ufile")"
local sjdir="/home/${user}/.cpanel/sitejet"
local has_eligible_domain="false"
# 1. Check if owned by root
if ! is_owner_root "$user"; then
echo "$(timestamp) SKIP_NON_ROOT_OWNER ${user}" >> "$DETAILED_DETECT_LOG"
continue
fi
# 2. Check for Sitejet metadata
if [ -d "$sjdir" ]; then
# Iterate over all domains/metadata files for this user
for meta in "$sjdir"/*; do
[ -f "$meta" ] || continue
[[ "$meta" =~ -files$ ]] && continue
local domain="$(basename "$meta")"
# FILTERING LOGIC
if [ "$ACTIVE_ONLY" -eq 1 ]; then
if check_active_user "$user" "$domain"; then
has_eligible_domain="true"
break # Found one active domain, this user is eligible
fi
else
# If not ACTIVE_ONLY, any Sitejet metadata makes them eligible
has_eligible_domain="true"
break
fi
done
# If user passed filtering (or no filtering was applied), add to CSV
if [ "$has_eligible_domain" = "true" ]; then
# Defensive read for package
pkg="$( { awk -F= '/^PLAN/ {print $2; exit}' "$ufile" 2>/dev/null || echo ""; } )"
pkg="${pkg:-unknown}"
echo "${user},${pkg}" >> "$USERS_CSV"
echo "$(timestamp) Detected Sitejet metadata for user ${user} (pkg=${pkg}, ACTIVE_ONLY=${ACTIVE_ONLY})" >> "$DETAILED_DETECT_LOG"
fi
fi
done
# Since the loop runs in a subshell, we count the lines written to the CSV file
# Note: 'wc -l' includes the header, so we subtract 1.
local final_count
final_count="$(wc -l < "$USERS_CSV")"
final_count=$((final_count > 0 ? final_count - 1 : 0))
echo "$final_count" > "${TMPDIR}/gen_count" # Passes count back to parent shell
log_action "GENERATE_USERS_CSV" "$USERS_CSV" "OK" ""
echo "--- DEBUG: Finishing generate_users_csv (Final Count: $final_count) ---" >&2
return 0
}
detailed_detection_report(){
: > "${TMPDIR}/detection.tmp"
echo "user,domain,document_root,publish_status,latest_publish_date,files_has_webcard" > "${TMPDIR}/detection.tmp"
while IFS=, read -r user pkg; do
[ -z "$user" ] && continue
# ensure root-owned still (defensive)
if ! is_owner_root "$user"; then
echo "${user},-,-,-,-,0" >> "${TMPDIR}/detection.tmp"
continue
fi
sitejet_dir="/home/${user}/.cpanel/sitejet"
if [ ! -d "$sitejet_dir" ]; then
echo "${user},-,-,-,-,0" >> "${TMPDIR}/detection.tmp"
continue
fi
for meta in "$sitejet_dir"/*; do
[ -f "$meta" ] || continue
[[ "$meta" =~ -files$ ]] && continue
domain="$(basename "$meta")"
dr="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"
pub="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"
lp="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"
files_list="${meta}-files"
fhw=0
if [ -f "$files_list" ] && grep -q 'webcard' "$files_list"; then
fhw=1
fi
echo "${user},${domain},${dr},${pub},${lp},${fhw}" >> "${TMPDIR}/detection.tmp"
done
done < "$USERS_CSV"
cp "${TMPDIR}/detection.tmp" "$DETECTION_CSV"
log_action "DETECTION_REPORT" "$DETECTION_CSV" "OK" ""
}
# ---- Feature key detection and feature list management ----
detect_feature_key(){
if [ -f "$FEATURE_BASE" ]; then
if grep -qi '^cpanel_sitejet_website_builder=' "$FEATURE_BASE"; then
echo "cpanel_sitejet_website_builder"
return 0
fi
if grep -qi '^sitejet=' "$FEATURE_BASE"; then
echo "sitejet"
return 0
fi
fi
echo "sitejet"
}
# Creates the sitejet-enabled feature list: default_sitejet
create_sitejet_feature_base(){
local backup_dir="$1" feature_key="$2"
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY_RUN: would create ${SITEJET_FEATURE_BASE} from ${FEATURE_BASE} and set ${feature_key}=1"
log_action "CREATE_SITEJET_FEATURE_BASE" "$SITEJET_FEATURE_BASE" "DRY_RUN" "$backup_dir"
return 0
fi
# Copy default feature file
cp -a "$FEATURE_BASE" "$SITEJET_FEATURE_BASE"
# Set the feature key to 1
if grep -qi "^${feature_key}=" "$SITEJET_FEATURE_BASE"; then
sed -i "s/^${feature_key}=.*/${feature_key}=1/I" "$SITEJET_FEATURE_BASE"
else
echo "${feature_key}=1" >> "$SITEJET_FEATURE_BASE"
fi
chown root:root "$SITEJET_FEATURE_BASE"
chmod 644 "$SITEJET_FEATURE_BASE"
log_action "CREATE_SITEJET_FEATURE_BASE" "$SITEJET_FEATURE_BASE" "OK" "$backup_dir"
}
# Disables Sitejet in the original default feature file
disable_sitejet_in_default(){
local backup_dir="$1" feature_key="$2"
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY_RUN: would backup and set ${feature_key}=0 in ${FEATURE_BASE}"
log_action "DISABLE_DEFAULT_FEATURE" "$FEATURE_BASE" "DRY_RUN" "$backup_dir"
return 0
fi
# Backup the original default feature file
cp -a "$FEATURE_BASE" "${backup_dir}/features-default.bak"
# Set the feature key to 0
if grep -qi "^${feature_key}=" "$FEATURE_BASE"; then
sed -i "s/^${feature_key}=.*/${feature_key}=0/I" "$FEATURE_BASE"
else
echo "${feature_key}=0" >> "$FEATURE_BASE"
fi
log_action "DISABLE_DEFAULT_FEATURE" "$FEATURE_BASE" "OK" "$backup_dir"
}
# ---- Per-package featurelist binding (updated to use SITEJET_FEATURE_BASE) ----
bind_package_featurelist_whmapi(){
local newpkg="$1" featurelist_name="$2"
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY_RUN: whmapi1 editpkg name=${newpkg} featurelist=${featurelist_name}"
log_action "BIND_PACKAGE_FEATURELIST" "${newpkg} to ${featurelist_name}" "DRY_RUN" ""
return 0
fi
# Use the single SITEJET_FEATURE_BASE name
if whmapi1 editpkg name="${newpkg}" featurelist="${featurelist_name}" >/dev/null 2>&1; then
log_action "BIND_PACKAGE_FEATURELIST" "${newpkg} to ${featurelist_name}" "OK" ""
else
log_action "BIND_PACKAGE_FEATURELIST" "${newpkg} to ${featurelist_name}" "FAIL" ""
fi
}
# ---- Clone package file and optional LVE replication ----
clone_package_file(){
# FINAL FIX: Use parameter expansion to safely assign the argument.
local oldpkg="${1:-}"
# The local declaration above should prevent the need for this check,
# but we keep it for logical safety (to skip empty package names).
if [ -z "$oldpkg" ]; then
log_action "CLONE_PACKAGE" "NULL_INPUT" "ERROR" ""
return 1
fi
local newpkg="${oldpkg}_with-sitejet"
local oldfile="${PACKAGES_DIR}/${oldpkg}"
local newfile="${PACKAGES_DIR}/${newpkg}"
if [ ! -f "$oldfile" ]; then
log_action "CLONE_PACKAGE" "${oldpkg}" "MISSING_SOURCE" ""
return 1
fi
if [ -f "$newfile" ]; then
log_action "CLONE_PACKAGE" "${newpkg}" "ALREADY_EXISTS" ""
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY_RUN: would cp ${oldfile} ${newfile}"
log_action "CLONE_PACKAGE" "${newpkg}" "DRY_RUN" ""
return 0
fi
cp -a "$oldfile" "$newfile"
log_action "CLONE_PACKAGE" "${newpkg}" "OK" ""
return 0
}
clone_lve_limits(){
# FIX: Two-step initialization to bypass aggressive 'nounset' behavior.
# 1. Declare 'oldpkg' locally and set to empty string.
local oldpkg=""
# 2. Assign the actual argument value from $1.
oldpkg="${1:-}"
# Now that oldpkg is guaranteed to be set, we can safely use it.
local newpkg="${oldpkg}_with-sitejet"
if [ -z "$oldpkg" ]; then
log_action "LVE_CLONE" "NULL_INPUT" "ERROR" ""
return 1
fi
if command -v lvectl >/dev/null 2>&1; then
if [ "$DRY_RUN" -eq 1 ]; then
log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "DRY_RUN" ""
return 0
fi
if lvectl package-clone --from="${oldpkg}" --to="${newpkg}" >/dev/null 2>&1; then
log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "OK" ""
else
log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "FAIL" ""
fi
return 0
fi
if command -v lve-manager >/dev/null 2>&1; then
if [ "$DRY_RUN" -eq 1 ]; then
log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "DRY_RUN" ""
return 0
fi
if lve-manager clone-package "${oldpkg}" "${newpkg}" >/dev/null 2>&1; then
log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "FAIL" ""
else
log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "FAIL" ""
fi
return 0
fi
log_action "LVE_CLONE" "${oldpkg}->${newpkg}" "NO_TOOL" ""
echo "NOTICE: No CloudLinux tool found; manual LVE cloning required for ${oldpkg} -> ${newpkg}"
}
# ---- Strict verification for a user (appends to verification CSV) ----
verify_user_strict(){
local user="$1"
local domain_field domain status has_index_html has_index_php dr pub lp files_list
domain_field="$(awk -F= '/^DNS/ {print $2; exit}' "/var/cpanel/users/$user" 2>/dev/null || true)"
domain="${domain_field:-unknown}"
status="no_sitejet_dir"; has_index_html=0; has_index_php=0
local sitejet_dir="/home/${user}/.cpanel/sitejet"
[ -d "$sitejet_dir" ] || { echo "${user},${domain},no_sitejet_dir,0,0" >> "$VERIFICATION_CSV"; return; }
for meta in "$sitejet_dir"/*; do
[ -f "$meta" ] || continue
[[ "$meta" =~ -files$ ]] && continue
dr="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"
pub="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"
lp="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"
files_list="${meta}-files"
if [ "$pub" = "1" ] && [ "${lp:-0}" -gt 0 ] && [ -f "$files_list" ] && grep -q 'webcard' "$files_list"; then
dr="${dr:-/home/${user}/public_html}"
dr="${dr%/}"
local webcard="${dr}/webcard"
if [ -d "$webcard" ]; then
[ -f "${webcard}/index.html" ] && has_index_html=1 || true
[ -f "${webcard}/index.php" ] && has_index_php=1 || true
if [ "$has_index_html" -eq 1 ] && [ "$has_index_php" -eq 0 ]; then
status="sitejet_standard_active"
break
else
status="sitejet_present_overridden"
fi
else
status="sitejet_meta_only"
fi
fi
done
echo "${user},${domain},${status},index.html=${has_index_html},index.php=${has_index_php}" >> "$VERIFICATION_CSV"
}
# ---- Change package for user (exit codes, structured logging) ----
change_package_for_user(){
local user="$1" oldpkg="$2" newpkg="${oldpkg}_with-sitejet"
if ! id "$user" &>/dev/null; then
echo "SKIP_USER_NOT_FOUND ${user}"
return 1
fi
if [ ! -f "${PACKAGES_DIR}/${newpkg}" ]; then
echo "SKIP_TARGET_PKG_MISSING ${user} ${newpkg}"
return 2
fi
local current_plan
current_plan="$(whmapi1 accountsummary user="$user" 2>/dev/null | awk -F': ' '/plan:/{print $2}' || true)"
current_plan="${current_plan:-$(awk -F= '/^PLAN/ {print $2; exit}' /var/cpanel/users/$user 2>/dev/null || true)}"
if [ "$current_plan" = "$newpkg" ]; then
echo "NOOP_ALREADY_ON_TARGET ${user} ${newpkg}"
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY_RUN Would move ${user} from ${current_plan} to ${newpkg}"
return 0
fi
if whmapi1 changepackage user="$user" pkg="$newpkg" >/dev/null 2>&1; then
echo "MOVED ${user} ${current_plan} -> ${newpkg}"
return 0
else
echo "FAIL_CHANGEPACKAGE ${user} -> ${newpkg}"
return 3
fi
}
# ---- Rollback hints ----
rollback_instructions(){
cat <<EOF
Rollback hints:
- Review ${ACTIONS_LOG} and ${LOG_DIR}/sitejet_pkg_moves.log for MOVED entries.
- To rollback a user:
whmapi1 changepackage user=<user> pkg=<original_pkg>
- To restore feature files from backup:
cp -a <BACKUP_DIR>/features-default.bak /var/cpanel/features/default
# If default_sitejet existed before the run, restore it too
[ -f "<BACKUP_DIR>/features-default_sitejet.bak" ] && cp -a <BACKUP_DIR>/features-default_sitejet.bak /var/cpanel/features/default_sitejet
- To restore all backed-up files:
rm -rf /var/cpanel/features /var/cpanel/packages
cp -a <BACKUP_DIR>/packages_features.<ts>.tgz /var
EOF
}
# ---- Main flow ----
ensure_root
check_prereqs
echo
echo "Sitejet migration final script (OWNER=root only)"
echo "Workdir: $WORKDIR"
echo "Backups dir: $BACKUPS_DIR"
echo "Logs: $LOG_DIR"
echo "DRY_RUN: $DRY_RUN"
echo
# Argument parsing for ACTIVE_ONLY
if [ "${1:-}" = "--active-only" ]; then
ACTIVE_ONLY=1
shift # Consume the argument
echo "NOTICE: Running in ACTIVE_ONLY mode. Only users meeting strict publishing criteria will be included."
fi
# CLI --gen-csv mode
if [ "${1:-}" = "--gen-csv" ]; then
OUTFILE="${2:-$USERS_CSV}"
generate_users_csv
echo "Generated users CSV at ${OUTFILE}"
exit 0
fi
# Step 1: generate users.csv interactively
if prompt_yesno "Generate users CSV (only OWNER=root) at ${USERS_CSV} now?" "y"; then
generate_users_csv
else
[ -f "$USERS_CSV" ] || die "Users CSV not found; aborting."
fi
# Read the count from the temporary file
gen_count="$(cat ${TMPDIR}/gen_count 2>/dev/null || echo 0)"
echo "Detected ${gen_count} OWNER=root users with Sitejet metadata."
log_action "GENERATE_USERS_SUMMARY" "$USERS_CSV" "COUNT=${gen_count}" ""
# Step 2: detailed detection report
if prompt_yesno "Run detailed detection and produce ${DETECTION_CSV}?" "y"; then
detailed_detection_report
echo "Detection CSV: ${DETECTION_CSV}"
fi
# Step 3: backup
if prompt_yesno "Create full backup of /var/cpanel/packages and /var/cpanel/features to workspace backups now?" "y"; then
BACKUP_PATH="$(create_timestamped_backup)"
echo "Backups created at: ${BACKUP_PATH}"
log_action "CREATE_BACKUP" "${BACKUP_PATH}" "OK" "${BACKUP_PATH}"
else
die "Backup is required before proceeding. Aborting."
fi
# Step 4: feature key detection and feature list setup
FEATURE_KEY="$(detect_feature_key)"
echo "Detected feature key: ${FEATURE_KEY}"
# 4a: Create default_sitejet feature list
if prompt_yesno "Create ${SITEJET_FEATURE_BASE} (Sitejet enabled) based on default?" "y"; then
create_sitejet_feature_base "$BACKUP_PATH" "$FEATURE_KEY"
echo "Sitejet-enabled feature list created: ${SITEJET_FEATURE_BASE}"
else
die "Creating the sitejet feature list is required. Aborting."
fi
# 4b: Disable Sitejet in default feature list
grep -n -i "^${FEATURE_KEY}=" "$FEATURE_BASE" 2>/dev/null || echo "<not present>"
if prompt_yesno "Disable Sitejet in default feature file (set ${FEATURE_KEY}=0) now? This is reversible via backup." "n"; then
disable_sitejet_in_default "$BACKUP_PATH" "$FEATURE_KEY"
echo "Default feature ${FEATURE_KEY} set to 0. Backup: ${BACKUP_PATH}/features-default.bak"
else
echo "Skipping disable of default feature as requested."
log_action "DISABLE_DEFAULT_FEATURE" "$FEATURE_BASE" "SKIP" "$BACKUP_PATH"
fi
# Step 5: build list of packages to clone (unique)
packages_to_clone=()
while IFS=, read -r u p; do
[ -z "$u" ] && continue
# ensure owner root for safety (defensive)
if ! is_owner_root "$u"; then
echo "$(timestamp) SKIP_NON_ROOT_OWNER_IN_CSV ${u}" >> "$DETAILED_DETECT_LOG"
continue
fi
packages_to_clone+=("$p")
done < "$USERS_CSV"
mapfile -t packages_to_clone < <(printf '%s\n' "${packages_to_clone[@]}" | awk '!seen[$0]++' | sed '/^$/d')
echo
echo "Packages to clone (${#packages_to_clone[@]}):"
for p in "${packages_to_clone[@]}"; do echo " - $p"; done
if [ "${#packages_to_clone[@]}" -eq 0 ]; then
die "No packages found to clone. Aborting."
fi
if ! prompt_yesno "Proceed to clone packages and bind them to the ${SITEJET_FEATURE_BASE} feature list?" "y"; then
echo "Aborting before package operations."
exit 0
fi
# Step 6: clone packages, bind to default_sitejet, LVE clone
SITEJET_FEATURE_LIST="$(basename "$SITEJET_FEATURE_BASE")"
for p in "${packages_to_clone[@]}"; do
# Defensive check for empty package name (from previous fix)
if [ -z "$p" ]; then
echo "Skipping empty package name in loop." >&2
continue
fi
# -------------------------------------------------------
echo
echo "Processing package: $p"
clone_package_file "$p" || echo "Warning: clone_package_file returned non-zero for $p"
newpkg="${p}_with-sitejet"
# Bind the new package to the single SITEJET_FEATURE_LIST
bind_package_featurelist_whmapi "$newpkg" "$SITEJET_FEATURE_LIST"
clone_lve_limits "$p"
done
# Save artifacts to backup folder
cp -a "$USERS_CSV" "${BACKUP_PATH}/sitejet_users.csv" || true
cp -a "$DETECTION_CSV" "${BACKUP_PATH}/sitejet_detection_details.csv" || true
log_action "SAVE_ARTIFACTS_TO_BACKUP" "${BACKUP_PATH}" "OK" "${BACKUP_PATH}"
# Step 7: preview and confirm user migrations
echo
echo "Preview of users CSV (first 20 lines):"
nl -ba "$USERS_CSV" | sed -n '1,20p'
if ! prompt_yesno "Proceed with changepackage for all OWNER=root users in ${USERS_CSV}? (Dry-run will simulate)" "n"; then
echo "Operator chose not to proceed with user reassignment."
exit 0
fi
# Step 8: reassign users and log
: > "${LOG_DIR}/sitejet_pkg_moves.log"
while IFS=, read -r user oldpkg; do
[ -z "$user" ] && continue
# enforce owner=root check again defensively
if ! is_owner_root "$user"; then
echo "$(timestamp) SKIP_NON_ROOT_OWNER ${user}" >> "${LOG_DIR}/sitejet_pkg_moves.log"
log_action "CHANGEPACKAGE" "user=${user}" "SKIP_NON_ROOT_OWNER" "${BACKUP_PATH}"
continue
fi
result="$(change_package_for_user "$user" "$oldpkg" 2>&1 || true)"
rc=$?
echo "$(timestamp) | ${result}" >> "${LOG_DIR}/sitejet_pkg_moves.log"
case "$rc" in
0) log_action "CHANGEPACKAGE" "user=${user}" "OK" "${BACKUP_PATH}" ;;
1) log_action "CHANGEPACKAGE" "user=${user}" "SKIP_USER_NOT_FOUND" "${BACKUP_PATH}" ;;
2) log_action "CHANGEPACKAGE" "user=${user}" "SKIP_TARGET_PKG_MISSING" "${BACKUP_PATH}" ;;
3) log_action "CHANGEPACKAGE" "user=${user}" "FAIL_CHANGEPACKAGE" "${BACKUP_PATH}" ;;
*) log_action "CHANGEPACKAGE" "user=${user}" "UNKNOWN_RC_${rc}" "${BACKUP_PATH}" ;;
esac
done < "$USERS_CSV"
# Step 9: strict verification post-migration
if prompt_yesno "Run strict verification across all OWNER=root users and append to ${VERIFICATION_CSV}?" "y"; then
while IFS=, read -r user oldpkg; do
[ -z "$user" ] && continue
if ! is_owner_root "$user"; then
echo "$(timestamp) SKIP_NON_ROOT_OWNER_IN_VERIFY ${user}" >> "$DETAILED_DETECT_LOG"
continue
fi
verify_user_strict "$user"
done < "$USERS_CSV"
log_action "POST_MIGRATION_VERIFICATION" "$VERIFICATION_CSV" "OK" "${BACKUP_PATH}"
echo "Verification results appended to ${VERIFICATION_CSV}"
fi
# Final summary and rollback hints
echo
echo "Migration completed. Artifacts and logs:"
echo " - Workspace: $WORKDIR"
echo " - Backups: $BACKUP_PATH"
echo " - Actions log: $ACTIONS_LOG"
echo " - Moves log: ${LOG_DIR}/sitejet_pkg_moves.log"
echo " - Detection details: ${DETECTION_CSV}"
echo " - Verification CSV: ${VERIFICATION_CSV}"
rollback_instructions
log_action "RUN_COMPLETE" "$WORKDIR" "OK" "${BACKUP_PATH}"
# Cleanup
rm -rf "$TMPDIR"
exit 0
/root/workspace/sitejet/detects_users.sh
#!/usr/bin/env bash
# detects_users.sh
# Strict Sitejet verification across all accounts that have /home/<user>/.cpanel/sitejet
# Writes CSV to /root/workspace/sitejet/logs/sitejet_pkg_verify_full.csv
# Requires: run as root; jq available in PATH.
#
# Usage:
# sudo /root/workspace/sitejet/detects_users.sh
# sudo SITEJET_OWNER_FILTER=root /root/workspace/sitejet/detects_users.sh # optional OWNER filter (see below)
set -o errexit
set -o nounset
set -o pipefail
OUT_DIR="/root/workspace/sitejet/logs"
OUT_FILE="${OUT_DIR}/sitejet_pkg_verify_full.csv"
TMPDIR="/root/workspace/sitejet/tmp_verify_$$"
OWNER_FILTER="${SITEJET_OWNER_FILTER:-}" # optional: if set (e.g., root), only include users whose OWNER is empty or equals this value; if empty include all users
timestamp() { date -Iseconds; }
die() { echo "$*" >&2; exit 1; }
# Prereqs and environment
[ "$(id -u)" -eq 0 ] || die "Run as root"
command -v jq >/dev/null 2>&1 || die "jq not found in PATH"
mkdir -p "$OUT_DIR" "$TMPDIR"
: > "$OUT_FILE"
echo "user,domain,status,index.html,index.php,document_root,publish_status,latest_publish_date,files_has_webcard" > "$OUT_FILE"
# Helper: check owner if OWNER_FILTER set
owner_allowed() {
local user="$1"
local ufile="/var/cpanel/users/${user}"
[ -f "$ufile" ] || return 1
if [ -z "$OWNER_FILTER" ]; then
return 0
fi
local owner
owner="$(awk -F= '/^OWNER=/{print $2; exit}' "$ufile" 2>/dev/null || echo)"
# treat missing owner as allowed when filtering for "root"
if [ -z "$owner" ] && [ "$OWNER_FILTER" = "root" ]; then
return 0
fi
if [ "$owner" = "$OWNER_FILTER" ]; then
return 0
fi
return 1
}
# Main loop: scan users
for ufile in /var/cpanel/users/*; do
[ -f "$ufile" ] || continue
user="$(basename "$ufile")"
# optional owner filter
if ! owner_allowed "$user"; then
continue
fi
sjdir="/home/${user}/.cpanel/sitejet"
[ -d "$sjdir" ] || continue
for meta in "$sjdir"/*; do
[ -f "$meta" ] || continue
# skip manifest files when parsing JSON (we handle manifest separately)
[[ "$meta" =~ -files$ ]] && continue
domain="$(basename "$meta")"
# extract metadata fields (jq required)
document_root="$(jq -r '.document_root // empty' "$meta" 2>/dev/null || echo)"
publish_status="$(jq -r '.publish_status // empty' "$meta" 2>/dev/null || echo)"
latest_publish_date="$(jq -r '.latest_publish_date // 0' "$meta" 2>/dev/null || echo 0)"
files_list="${meta}-files"
files_has_webcard=0
if [ -f "$files_list" ] && grep -q "webcard" "$files_list"; then
files_has_webcard=1
fi
status="no_sitejet_dir"
idx_html=0
idx_php=0
# strict publish checks
if [ "$publish_status" = "1" ] && [ "${latest_publish_date:-0}" -gt 0 ] && [ "$files_has_webcard" -eq 1 ]; then
# determine document_root fallback
if [ -z "$document_root" ]; then
document_root="/home/${user}/public_html"
fi
document_root="${document_root%/}"
webcard_dir="${document_root}/webcard"
if [ -d "$webcard_dir" ]; then
[ -f "${webcard_dir}/index.html" ] && idx_html=1 || true
[ -f "${webcard_dir}/index.php" ] && idx_php=1 || true
if [ "$idx_html" -eq 1 ] && [ "$idx_php" -eq 0 ]; then
status="sitejet_standard_active"
else
status="sitejet_present_overridden"
fi
else
status="sitejet_meta_only"
fi
else
status="sitejet_meta_only"
fi
# escape commas in document_root for CSV
safe_dr="$(printf '%s' "$document_root" | sed 's/,/\\,/g')"
printf '%s,%s,%s,%d,%d,%s,%s,%s,%d\n' \
"$user" "$domain" "$status" "$idx_html" "$idx_php" "$safe_dr" "$publish_status" "$latest_publish_date" "$files_has_webcard" \
>> "$OUT_FILE"
done
done
# Summary counts by status
echo "$(timestamp) Wrote verification CSV: $OUT_FILE"
awk -F, 'NR>1{cnt[$3]++}END{for (k in cnt) printf "%-30s %d\n", k, cnt[k]}' "$OUT_FILE" | sort -k2 -n -r
# finish
rm -rf "$TMPDIR"
exit 0
A possible bug when there are no accounts detected:
Disable Sitejet in default feature file (set sitejet=0) now? This is reversible via backup. [y/n] (default: n): y
Default feature sitejet set to 0. Backup: /root/workspace/sitejet/backups/20260105T102000/features-default.bak
/root/workspace/sitejet/operate.sh: line 594: packages_to_clone[@]: unbound variable
Packages to clone (0):
/root/workspace/sitejet/operate.sh: line 598: packages_to_clone[@]: unbound variable
—
It may also be goo to generate the csv first to have an idea :
/root/workspace/sitejet/operate.sh –gen-csv /root/workspace/sitejet/audit_results.csv
cat /root/workspace/sitejet/audit_results.csv
/root/workspace/sitejet/operate.sh –active-only –gen-csv /root/workspace/sitejet/active_audit.csv
cat /root/workspace/sitejet/audit_results.csv
Fixed to stop if no account is detected
TODO (non-dry mode should have default Y) for:
Disable Sitejet in default feature file (set sitejet=0) now? This is reversible via backup. [y/n] (default: n):
// indicating that it can also be modified via WHM > features manager
Proceed with changepackage for all OWNER=root users in /root/workspace/sitejet/sitejet_users.csv? (Dry-run will simulate) [y/n] (default: n):
# Changed 4b to default ‘y’
if prompt_yesno “Disable Sitejet in default feature file (set ${FEATURE_KEY}=0) now? (Also manageable via WHM Feature Manager)” “y”; then
# … logic …
fi
# Changed Step 7 to default ‘y’
if ! prompt_yesno “Proceed with changepackage for all OWNER=root users in ${USERS_CSV}?” “y”; then
# … logic …
fi
lve is not being updated after cloning, lve are stored in /etc/container/ve.cfg, so to clone it, either copy the values from lvectl and reply them using lvectl
lvectl package-list | grep -i ‘ProPlan’
ProPlan 200 20480M 0K 80 250 20480 1024
ProPlan 100 1024M 0K 20 100 1024 1024
unless lve was set using the lve package extension on whm > editing package (which then is defined at the level of the package file), e.g.:
QUOTA=5120
lve_inodes_hard=110000
lve_inodes_soft=100000
lve_mysql_cpu=DEFAULT
lve_mysql_io=DEFAULT
_PACKAGE_EXTENSIONS=lve
lve_ncpu=DEFAULT
lve_cpu=100%
lve_io=1024
lve_mem=
lve_pmem=1G
lve_nproc=100
lve_iops=1024
lve_ep=20