%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /proc/self/root/sbin/
Upload File :
Create Path :
Current File : //proc/self/root/sbin/wp-toolkit-clone-awp-settings

#!/opt/cloudlinux/venv/bin/python3
# -*- coding: utf-8 -*-

# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2025 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

import argparse
import json
import logging
import os
import pwd
import subprocess
import sys
from urllib.parse import urlparse

from clwpos.logsetup import setup_logging
from clwpos.optimization_features import Feature

# Set up logger
logger = setup_logging(
    caller_name='wp-toolkit-clone-awp-settings',
    console_level=logging.INFO,
    file_level=logging.DEBUG,
    logfile_path='/var/log/clwpos/wp-toolkit-clone.log'
)

def get_wp_toolkit_instance_info(instance_id):
    """Get WP Toolkit instance information

    Returns JSON data from WP Toolkit API in the following format:
    {
      "id": 3,
      "siteUrl": "https://us17.com/wp1",
      "fullPath": "/var/www/vhosts/us17.com/httpdocs/wp1",
      "mainDomainId": 3,
      "path": "/httpdocs/wp1",
      "ownerId": 4,
      "name": "Exquisite Goods",
      "version": "6.8.1",
      "alive": true,
      "stateText": "Working",
      ...  // other fields
    }

    Key fields used by this script:
    - id: WordPress instance ID (for validation)
    - siteUrl: Full URL of the WordPress site (used to extract domain and wp_path)  
    - fullPath: Absolute filesystem path (used to determine site owner)

    Args:
        instance_id (str|int): WP Toolkit instance ID

    Returns:
        dict|None: Instance data on success, None on failure
    """
    logger.debug(f"Getting WP Toolkit instance info for ID: {instance_id}")

    try:
        # Simple panel detection - check if Plesk binary exists
        if os.path.exists('/usr/sbin/plesk'):
            cmd = ['/usr/sbin/plesk', 'ext', 'wp-toolkit']
        else:
            cmd = ['/usr/local/bin/wp-toolkit']

        cmd.extend(['-info', '-instance-id', str(instance_id), '-format', 'json'])

        result = subprocess.run(cmd, capture_output=True, text=True, check=True)

        instance = json.loads(result.stdout)

        if isinstance(instance, dict) and 'id' in instance:
            if str(instance.get('id')) == str(instance_id):
                return instance
            else:
                logger.error(f"Instance ID mismatch: expected {instance_id}, got {instance.get('id')}")
                return None
        else:
            logger.error(f"Invalid instance data structure: {type(instance)}")
            return None

    except subprocess.CalledProcessError as e:
        logger.error(f"WP Toolkit command failed with return code {e.returncode}")
        logger.debug(f"Command stderr: {e.stderr}")
        return None
    except json.JSONDecodeError as e:
        logger.error(f"Failed to parse WP Toolkit JSON output: {e}")
        return None
    except Exception as e:
        logger.error(f"Unexpected error getting instance info: {e}")
        return None

def extract_site_details(instance):
    """Extract domain and wp_path from WP Toolkit instance data"""
    if not instance or not isinstance(instance, dict):
        logger.error(f"Invalid instance data: {type(instance)}")
        return None, None

    try:
        site_url = instance.get('siteUrl', '')

        if not site_url:
            logger.error("Missing siteUrl in instance data")
            return None, None

        # Parse URL properly using standard library
        parsed = urlparse(site_url)
        domain = parsed.netloc

        # Get path and normalize it
        wp_path = parsed.path.strip('/')
        if not wp_path:
            wp_path = '/'

        return domain, wp_path

    except Exception as e:
        logger.error(f"Error extracting site details: {e}")
        return None, None

def get_site_owner(instance):
    """Get site owner by examining filesystem ownership using fullPath from WP Toolkit"""
    full_path = instance.get('fullPath', '')
    domain = instance.get('siteUrl', '').replace('https://', '').replace('http://', '').split('/')[0]

    try:
        if not full_path or not os.path.exists(full_path):
            logger.error(f"Full path does not exist: {full_path}")
            return None

        stat_info = os.stat(full_path)
        owner_uid = stat_info.st_uid
        owner = pwd.getpwuid(owner_uid).pw_name
        return owner

    except Exception as e:
        logger.error(f"Error getting site owner: {e}")
        return None

def get_awp_features(domain, wp_path, username):
    """Get AccelerateWP features for a site"""
    try:
        cmd = [
            'cloudlinux-awp-user', 
            '--user', username,
            'get',
            '--domain', domain,
            '--wp-path', wp_path
        ]

        result = subprocess.run(cmd, capture_output=True, text=True, check=True)

        features_data = json.loads(result.stdout)
        return features_data

    except subprocess.CalledProcessError as e:
        logger.error(f"cloudlinux-awp-user get failed: {e.stderr}")
        return None
    except json.JSONDecodeError as e:
        logger.error(f"Failed to parse AWP features JSON: {e}")
        return None
    except Exception as e:
        logger.error(f"Unexpected error getting AWP features: {e}")
        return None

def copy_awp_features(source_features, source_domain, source_wp_path, target_domain, target_wp_path, target_username):
    """Copy AccelerateWP features from source to target"""
    if not source_features:
        logger.error("No source features data")
        return False

    if not isinstance(source_features, dict):
        logger.error(f"Source features is not a dict: {type(source_features)}")
        return False

    if 'docroots' not in source_features:
        logger.error("Missing 'docroots' key in source features data")
        return False

    # Normalize wp_path for comparison (both '/' and '' mean root WordPress)
    normalized_source_path = '' if source_wp_path == '/' else source_wp_path

    # Find the features for the specific WordPress site that is being cloned
    wp_features = None

    for i, docroot in enumerate(source_features.get('docroots', [])):
        # Check if this docroot contains our source domain
        docroot_domains = docroot.get('domains', [])
        logger.debug(f"Docroot {i}: domains={docroot_domains}")

        if source_domain not in docroot_domains:
            logger.debug(f"Skipping docroot {i} - does not contain domain {source_domain}")
            continue

        logger.debug(f"Docroot {i} contains source domain, checking WordPress sites...")
        wps = docroot.get('wps', [])
        logger.debug(f"Found {len(wps)} WordPress sites in this docroot")

        for j, wp_site in enumerate(wps):
            site_path = wp_site.get('path', '')
            # Normalize site_path for comparison (both '/' and '' mean root WordPress)
            normalized_site_path = '' if site_path == '/' else site_path
            logger.debug(f"  WP site {j}: path='{site_path}' (normalized: '{normalized_site_path}') - comparing with '{normalized_source_path}'")

            # Check if this is exactly the WordPress site that is being cloned
            if normalized_site_path == normalized_source_path:
                wp_features = wp_site.get('features', {})
                logger.debug(f"Found source site: {source_domain}:{source_wp_path}")
                logger.debug(f"Features available: {list(wp_features.keys()) if wp_features else 'None'}")
                break

        if wp_features is not None:
            break

    if wp_features is None:
        logger.error(f"Could not find source WordPress site: {source_domain}:{source_wp_path}")
        logger.error("Available sites in docroots:")
        for i, docroot in enumerate(source_features.get('docroots', [])):
            domains = docroot.get('domains', [])
            logger.error(f"  Docroot {i}: domains={domains}")
            for j, wp_site in enumerate(docroot.get('wps', [])):
                path = wp_site.get('path', '')
                logger.error(f"    WP site {j}: path='{path}'")
        return False

    if not wp_features:
        logger.info("No features configured on source site")
        return True

    features_to_copy = []

    # Use centralized feature mapping from Feature class
    for feature_name, feature_data in wp_features.items():
        if isinstance(feature_data, dict) and feature_data.get('enabled'):
            try:
                # Use centralized mapping from Feature class
                feature_obj = Feature(feature_name)
                cli_feature_name = feature_obj.to_interface_name()
                features_to_copy.append(cli_feature_name)
            except Exception as e:
                logger.warning(f"Unknown feature '{feature_name}': {e}")
                continue

    if not features_to_copy:
        logger.info("No features enabled on source site")
        return True

    logger.info(f"Copying features: {features_to_copy}")

    # Enable features on target
    success = True
    for feature in features_to_copy:
        try:
            cmd = [
                'cloudlinux-awp-user',
                '--user', target_username, 
                'enable',
                '--feature', feature,
                '--domain', target_domain,
                '--wp-path', target_wp_path,
                '--ignore-errors'
            ]

            result = subprocess.run(cmd, capture_output=True, text=True, check=True)
            logger.info(f"Enabled {feature}")

        except subprocess.CalledProcessError as e:
            logger.error(f"Failed to enable {feature}: {str(e)}")
            success = False
        except Exception as e:
            logger.error(f"Unexpected error enabling {feature}: {e}")
            success = False

    return success

def main():
    parser = argparse.ArgumentParser(description='Copy AccelerateWP settings from parent site to clone')
    parser.add_argument('--source-instance-id', required=True, help='Source WP Toolkit instance ID')
    parser.add_argument('--target-instance-id', required=True, help='Target WP Toolkit instance ID')
    parser.add_argument('--quiet', action='store_true', help='Suppress output messages')

    args, unknown = parser.parse_known_args()

    # Unknown arguments are normal in some contexts (e.g., WP Toolkit hook for cPanel)
    if unknown:
        logger.info(f"Unknown arguments: {unknown}")

    # Adjust logging level if quiet mode requested
    if args.quiet:
        logger.setLevel(logging.ERROR)
        for handler in logger.handlers:
            handler.setLevel(logging.ERROR)

    logger.info(f"Starting AWP copy: {args.source_instance_id} → {args.target_instance_id}")

    # Get source instance info
    source_instance = get_wp_toolkit_instance_info(args.source_instance_id)
    if not source_instance:
        logger.error("Failed to get source instance information")
        sys.exit(1)

    # Get target instance info
    target_instance = get_wp_toolkit_instance_info(args.target_instance_id)
    if not target_instance:
        logger.error("Failed to get target instance information")
        sys.exit(1)

    # Extract site details
    source_domain, source_wp_path = extract_site_details(source_instance)
    target_domain, target_wp_path = extract_site_details(target_instance)

    if not all([source_domain, target_domain]):
        logger.error("Failed to extract site details")
        sys.exit(1)

    # Get site owners
    source_username = get_site_owner(source_instance)
    target_username = get_site_owner(target_instance)

    if not all([source_username, target_username]):
        logger.error("Failed to determine site owners")
        sys.exit(1)

    logger.info(f"Source: {source_domain}:{source_wp_path} ({source_username})")
    logger.info(f"Target: {target_domain}:{target_wp_path} ({target_username})")

    # Get source AWP features
    source_features = get_awp_features(source_domain, source_wp_path, source_username)
    if not source_features:
        logger.error("Failed to get source AWP features")
        sys.exit(1)

    # Copy features to target
    if copy_awp_features(source_features, source_domain, source_wp_path, target_domain, target_wp_path, target_username):
        logger.info("AWP settings copied successfully")
        sys.exit(0)
    else:
        logger.error("Failed to copy AWP settings")
        sys.exit(1)

if __name__ == '__main__':
    main() 

Zerion Mini Shell 1.0