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## OverviewAs 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```bashmkdir -p /root/workspace/sitejetcd /root/workspace/sitejet# Copy the scripts operate.sh and detects_users.sh into this directorychmod +x operate.sh detects_users.sh```All logs, backups, and artifacts are stored within /root/workspace/sitejet/.Usage1. Audit Current Usage (detects_users.sh)Generate a full report of all accounts utilizing Sitejet, classified by their actual publication status.Bashsudo ./detects_users.shTo limit the audit strictly to accounts owned by root:Bashsudo SITEJET_OWNER_FILTER=root ./detects_users.shArtifact: /root/workspace/sitejet/logs/sitejet_pkg_verify_full.csv2. Execute Migration (operate.sh)This script is interactive. It is highly recommended to run a dry-run first.Dry-Run (Simulate changes):Bashsudo SITEJET_DRY=1 ./operate.sh --active-onlyLive 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.Bashsudo ./operate.sh --active-onlyThe Migration Lifecycle ExplainedWhen 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.## RollbackThe 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.DisclaimerThese 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 ExampleHere 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-onlySitejet migration final script (OWNER=root only)Workdir: /root/workspace/sitejetBackups dir: /root/workspace/sitejet/backupsLogs: /root/workspace/sitejet/logsDRY_RUN: 0NOTICE: 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/alphaDEBUG: Checking file: /var/cpanel/users/betauserDEBUG: Checking file: /var/cpanel/users/gammasiteDEBUG: 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): yDetection CSV: /root/workspace/sitejet/logs/sitejet_detection_details.csvCreate full backup of /var/cpanel/packages and /var/cpanel/features to workspace backups now? [y/n] (default: y): yBackups created at: /root/workspace/sitejet/backups/20261015T090001Detected feature key: sitejetCreate /var/cpanel/features/default_sitejet (Sitejet enabled) based on default? [y/n] (default: y): ySitejet-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): yDefault feature sitejet set to 0. Backup: /root/workspace/sitejet/backups/20261015T090001/features-default.bakPackages to clone (3): - Bronze Plan - Silver Plan - Corporate DedicatedProceed to clone packages and bind them to the /var/cpanel/features/default_sitejet feature list? [y/n] (default: y): yProcessing package: Bronze PlanProcessing package: Silver PlanProcessing package: Corporate DedicatedPreview of users CSV (first 20 lines): 1 user,pkg... 17 user1example,Bronze Plan 18 user2test,Silver Plan 19 user3hosting,Bronze Plan 20 user4site,Corporate DedicatedProceed with changepackage for all OWNER=root users in /root/workspace/sitejet/sitejet_users.csv? (Dry-run will simulate) [y/n] (default: n): yRun strict verification across all OWNER=root users and append to /root/workspace/sitejet/logs/sitejet_pkg_verify.csv? [y/n] (default: y): yVerification results appended to /root/workspace/sitejet/logs/sitejet_pkg_verify.csvMigration 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.csvRollback 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 bashset -o errexitset -o nounsetset -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 listSITEJET_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 dirsprepare_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_sitejetcreate_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 filedisable_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 <<EOFRollback 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 /varEOF}# ---- Main flow ----ensure_rootcheck_prereqsechoecho "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_ONLYif [ "${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 modeif [ "${1:-}" = "--gen-csv" ]; then OUTFILE="${2:-$USERS_CSV}" generate_users_csv echo "Generated users CSV at ${OUTFILE}" exit 0fi# Step 1: generate users.csv interactivelyif prompt_yesno "Generate users CSV (only OWNER=root) at ${USERS_CSV} now?" "y"; then generate_users_csvelse [ -f "$USERS_CSV" ] || die "Users CSV not found; aborting."fi# Read the count from the temporary filegen_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 reportif prompt_yesno "Run detailed detection and produce ${DETECTION_CSV}?" "y"; then detailed_detection_report echo "Detection CSV: ${DETECTION_CSV}"fi# Step 3: backupif 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 setupFEATURE_KEY="$(detect_feature_key)"echo "Detected feature key: ${FEATURE_KEY}"# 4a: Create default_sitejet feature listif 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 listgrep -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')echoecho "Packages to clone (${#packages_to_clone[@]}):"for p in "${packages_to_clone[@]}"; do echo " - $p"; doneif [ "${#packages_to_clone[@]}" -eq 0 ]; then die "No packages found to clone. Aborting."fiif ! prompt_yesno "Proceed to clone packages and bind them to the ${SITEJET_FEATURE_BASE} feature list?" "y"; then echo "Aborting before package operations." exit 0fi# Step 6: clone packages, bind to default_sitejet, LVE cloneSITEJET_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 foldercp -a "$USERS_CSV" "${BACKUP_PATH}/sitejet_users.csv" || truecp -a "$DETECTION_CSV" "${BACKUP_PATH}/sitejet_detection_details.csv" || truelog_action "SAVE_ARTIFACTS_TO_BACKUP" "${BACKUP_PATH}" "OK" "${BACKUP_PATH}"# Step 7: preview and confirm user migrationsechoecho "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 0fi# 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}" ;; esacdone < "$USERS_CSV"# Step 9: strict verification post-migrationif 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 hintsechoecho "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_instructionslog_action "RUN_COMPLETE" "$WORKDIR" "OK" "${BACKUP_PATH}"# Cleanuprm -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 errexitset -o nounsetset -o pipefailOUT_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 userstimestamp() { 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 setowner_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 usersfor 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" donedone# Summary counts by statusecho "$(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# finishrm -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