#!/usr/bin/env python3 """ P4 Command Blaster Description: This script automates the execution of several Perforce (P4) commands, handling server trust, logging in, and executing a set of predefined P4 commands. Results can be optionally written to files and uploaded to Azure Blob Storage. The script supports running a basic set of commands, with an option to execute additional commands using the --all flag. Setup: 1. Set Azure environment variables for Blob Storage access: Linux: export AZURE_ACCOUNT_NAME=your_account_name export AZURE_ACCOUNT_KEY=your_account_key Windows CMD: set AZURE_ACCOUNT_NAME=your_account_name set AZURE_ACCOUNT_KEY=your_account_key Windows PowerShell: $env:AZURE_ACCOUNT_NAME = "your_account_name" $env:AZURE_ACCOUNT_KEY = "your_account_key" 2. If running against multiple servers then create host_config.yaml studios: - name: "Studio1" hosts: - host: "ssl:host1.studio1.com:1999" user: "perforce" - host: "ssl:host2.studio1.com:1999" user: "perforce" - name: "Studio2" hosts: - host: "ssl:host1.studio2.com:1666" user: "user3" - host: "ssl:host2.studio2.com:1999" user: "perforce" Usage: - Run basic commands on single server: python cmd-blaster.py --host 'ssl:your_p4_hostname:port' - Run additional commands on single server: python cmd-blaster.py python cmd-blaster.py --host 'ssl:your_p4_hostname:port' --all - Optionally write output to files and upload to Azure Blob Storage. Blob Storage Organization: Outputs are stored in blobs named after the command with a date-time and hostname prefix. This structure aids in tracking changes and diagnosing issues over time. Example for ztag info Blob Path: script-outputs/2023/May/15/host1_studio2_com_1666/p4ztaginfo.txt Commands Supported: Basic: - p4 -ztag info: Retrieves server info. - p4 diskspace: Shows depot disk space usage. - p4 configure show allservers: Configuration variables. - p4 servers -J: Replication status check. With --all: - p4 triggers -o: Outputs the triggers table. - p4 extension --list --type extensions: Installed extensions. - p4 protect -o: Protections table. - p4 property -n P4.Swarm.URL -l: P4.Swarm.URL properties. Requirements: - Perforce command-line client (p4) - Azure Blob Storage account for outputs """ import argparse import subprocess import os import getpass import re from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient import socket import logging from datetime import datetime import yaml # Azure Storage Account Information - Read from environment variables # These are used for accessing Azure Blob Storage account_name = os.getenv('AZURE_ACCOUNT_NAME') account_key = os.getenv('AZURE_ACCOUNT_KEY') container_name = "script-outputs" studio_name = "DefaultStudio" P4TICKETS_PATH = "./.p4tickets" # Setup logging based on the verbosity level def setup_logging(verbose): # Initialize script logging if verbose: logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') else: logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # Set up logging for Azure SDK azure_logger = logging.getLogger('azure') if verbose: azure_logger.setLevel(logging.DEBUG) else: azure_logger.setLevel(logging.WARNING) # Function to load host configurations from YAML def load_host_config(file_path): if not os.path.exists(file_path): logging.error(f"Host configuration file not found at {file_path}.") logging.error("Please ensure the host configuration file is present and try again.") logging.info("Alternatively, you can specify a host directly using the --host argument.") logging.info("Example: python cmd-blaster.py --host 'ssl:your_p4_hostname:port'") exit(1) # Exit with an error code with open(file_path, 'r') as file: return yaml.safe_load(file) # Handles establishing trust with the Perforce server def handle_p4_trust(p4_port): try: # Check the trust status of the server subprocess.run(["p4", "-p", p4_port, "trust", "-y"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) except subprocess.CalledProcessError as e: # If trust is not established, prompt the user if "The authenticity of" in e.stderr: logging.error("Server authenticity is not established.") logging.error(e.stderr.splitlines()[1]) # Display the fingerprint message trust_decision = input("Do you want to trust this server? [y/N]: ") if trust_decision.lower() == 'y': try: # Establish trust subprocess.run(["p4", "-p", p4_port, "trust", "-y"], check=True, text=True) logging.info("Server trusted successfully.") except subprocess.CalledProcessError as trust_error: logging.error(f"Error establishing trust: {trust_error.stderr}") exit(1) else: logging.error("Trust not established. Exiting.") exit(1) # Handle Perforce login process def p4_login(p4_port): logging.debug("Starting P4 login process") # Handle trust before login handle_p4_trust(p4_port) # Set P4TICKETS environment variable os.environ['P4TICKETS'] = P4TICKETS_PATH # Check if a valid ticket exists for the specific P4PORT try: subprocess.run(["p4", "-p", p4_port, "login", "-s"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) logging.info(f"Valid P4 ticket found for {p4_port}.") except subprocess.CalledProcessError: # No valid ticket for this P4PORT, prompt for password logging.error(f"No valid P4 ticket found for {p4_port}. Please login to Perforce.") p4_password = getpass.getpass("Enter your P4 password: ") if p4_password: try: # Explicitly specify P4PORT in the login command subprocess.run(["p4", "-p", p4_port, "login"], input=p4_password, text=True, check=True) logging.info(f"P4 login successful for {p4_port}.") except subprocess.CalledProcessError as e: logging.error(f"Failed to login to Perforce at {p4_port}: {e.stderr}") exit(1) # Print Perforce settings for verification def print_p4_settings(p4_settings): logging.info("Using Perforce configuration:") for key, value in p4_settings.items(): logging.info(f" {key}: {value}") # Check for the existence of the P4 tickets file def check_p4tickets_file(p4_tickets_path): if p4_tickets_path: # Convert relative path to absolute path absolute_p4_tickets_path = os.path.abspath(p4_tickets_path) if os.path.exists(absolute_p4_tickets_path): logging.info(f"P4TICKETS file found at: {absolute_p4_tickets_path}") else: logging.error(f"P4TICKETS file not found at: {absolute_p4_tickets_path}") else: logging.error("P4TICKETS path not specified in configuration.") # Create Azure Blob container if it does not exist def create_blob_container_if_not_exists(blob_service_client, container_name): try: container_client = blob_service_client.get_container_client(container_name) if not container_client.exists(): container_client.create_container() logging.info(f"Container '{container_name}' created.") except Exception as e: logging.error(f"Error creating container: {e}") # Execute Perforce commands and capture their output def run_command(command, json_format=False): if json_format: # Insert -Mj right after 'p4' command.insert(1, "-Mj") command.insert(2, "-ztag") try: logging.debug(f"Executing command: {' '.join(command)}") result = subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) logging.debug(f"Command output: {result.stdout}") return result.stdout except subprocess.CalledProcessError as e: logging.error(f"Error executing command '{command}': {e.stderr}") return "" # Create a path for the blob based on the current date and hostname def create_blob_path(filename, studio_name, current_p4_host): now = datetime.now() sanitized_studio_name = re.sub(r'[^a-zA-Z0-9]', '_', studio_name) # Extract, sanitize hostname and port (excluding 'ssl:'), and remove colons hostname_and_port = re.sub(r'^ssl:', '', current_p4_host) sanitized_host_and_port = re.sub(r'[^a-zA-Z0-9]', '_', hostname_and_port) # Format the path path = f"script-outputs/{sanitized_studio_name}/{now.year}/{now.strftime('%B')}/{now.strftime('%d')}/{sanitized_host_and_port}/{filename}" return path # Upload command output to Azure Blob Storage def upload_to_azure_blob(output_bytes, filename, studio_name, current_host): blob_service_client = BlobServiceClient(account_url=f"https://{account_name}.blob.core.windows.net", credential=account_key) blob_path = create_blob_path(filename, studio_name, current_host) # Use current_host here blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_path) try: logging.info(f"Uploading output to Azure Blob Storage at {blob_path}.") blob_client.upload_blob(output_bytes, overwrite=True) logging.info(f"Upload successful: {filename}") except Exception as e: logging.error(f"Failed to upload {filename} to Azure Blob Storage: {e}") # Run specified P4 commands and handle their output def run_p4_command_and_upload(p4_settings, run_all, write_to_file_flag, studio_name, current_host, json_format): # base_command = ["p4", "-p", p4_settings.get("P4PORT")] base_command = ["p4", "-p", p4_settings["P4PORT"]] # Explicitly set P4PORT # Define your commands as before commands = [ (base_command + ["-ztag", "info"], "p4ztaginfo.txt"), (base_command + ["diskspace"], "depot-diskspace.txt"), (base_command + ["configure", "show", "allservers"], "servers-configuration.txt"), (base_command + ["servers", "-J"], "servers-journaling.txt") ] # Additional commands if --all is specified if run_all: commands += [ (base_command + ["triggers", "-o"], "triggers-table.txt"), (base_command + ["extension", "--list", "--type", "extensions"], "extensions-list.txt"), (base_command + ["protect", "-o"], "protections-table.txt"), (base_command + ["property", "-n", "P4.Swarm.URL", "-l"], "swarm-url-properties.txt") ] error_occurred = False # Flag to track if any command is empty for command, filename in commands: if json_format: filename = filename.replace('.txt', '.json') output = run_command(command, json_format=json_format) if output: if write_to_file_flag: write_to_file(output, filename) output_bytes = output.encode('utf-8') # Update blob path to include current host blob_path = create_blob_path(filename, studio_name, current_host) upload_to_azure_blob(output_bytes, filename, studio_name, current_host) # Pass current_host here else: logging.error(f"No output or error executing command: {' '.join(command)}") # Continue to the next command even if one fails continue # Write output to a file def write_to_file(output, file_path): logging.info(f"Writing output to a file: {file_path}.") with open(file_path, 'w') as file: file.write(output) logging.debug(f"Finished writing to file: {file_path}") # Run specified P4 commands for a single host def run_commands_for_host(studio_name, host_config, run_all, write_to_file_flag, blob_service_client, json_format, user): logging.info(f"Starting commands execution for host: {host_config['host']} as user {host_config['user']}") # Set Perforce environment variables for the host os.environ['P4PORT'] = host_config['host'] os.environ['P4USER'] = user if user else host_config['user'] os.environ['P4TICKETS'] = P4TICKETS_PATH # Use the centralized .p4tickets file # Each host might need its own P4 settings p4_settings = {'P4PORT': os.environ['P4PORT'], 'P4USER': os.environ['P4USER']} # Attempt to connect and handle any exceptions try: # Check if the host is reachable if is_host_reachable(host_config['host']): handle_p4_trust(os.environ['P4PORT']) p4_login(os.environ['P4PORT']) # Execute commands run_p4_command_and_upload(p4_settings, run_all, write_to_file_flag, studio_name, host_config['host'], json_format) else: logging.error(f"Host {host_config['host']} is not reachable.") except Exception as e: logging.error(f"Error executing commands for host {host_config['host']}: {e}") # Continue to the next host def is_host_reachable(host_string): try: # Split the host string to extract hostname and port _, hostname, port = host_string.split(':') port = int(port) # Convert port to an integer logging.debug(f"Attempting to reach host: {hostname} on port {port}") # Check if the host is reachable on the specified port with socket.create_connection((hostname, port), timeout=5) as conn: logging.info(f"Successfully connected to host {hostname} on port {port}") return True except socket.timeout: logging.warning(f"Timeout occurred when trying to connect to host {hostname} on port {port}") return False except socket.gaierror: logging.warning(f"Address-related error connecting to host {hostname} on port {port}") return False except socket.error as e: logging.warning(f"Error connecting to host {hostname} on port {port}: {e}") return False except Exception as e: logging.error(f"Unexpected error while trying to connect to host {hostname} on port {port}: {e}") return False # Main function to run the script def main(): try: global studio_name parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__, epilog="Copyright (c) 2023 Perforce Software, Inc." ) parser.add_argument("--all", action="store_true", help="Run all commands") parser.add_argument("--write-to-file", action="store_true", help="Optionally write output to a file before uploading") parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output") parser.add_argument("-s", "--studio", default="", help="Studio name for blob storage path") parser.add_argument("-hc", "--host-config", default="./host_config.yaml", help="Path to host configuration file (default: ./host_config.yaml)") parser.add_argument("--host", help="Specific host to run the commands on", default="") parser.add_argument("--user", help="Perforce username for login", default="") parser.add_argument("--json", action="store_true", help="Output in JSON format") args = parser.parse_args() # Set up logging based on the verbose flag setup_logging(args.verbose) logging.debug(f"Verbose mode: {'Enabled' if args.verbose else 'Disabled'}") specified_host = args.host json_format = args.json user = args.user.strip() # Handle Studio Name studio_name = args.studio.strip() if args.studio.strip() else studio_name if not studio_name: studio_name = input("Enter your Studio Name: ").strip() if not studio_name: logging.error("Studio Name is required.") exit(1) # Prompt for Azure credentials if not set global account_name, account_key if not account_name or not account_key: account_name = input("Enter Azure Account Name: ") account_key = getpass.getpass("Enter Azure Account Key: ") # Initialize Azure Blob Service Client blob_service_client = BlobServiceClient(account_url=f"https://{account_name}.blob.core.windows.net", credential=account_key) # Create the Azure blob container if needed create_blob_container_if_not_exists(blob_service_client, container_name) # Load host configurations only if no specific host is provided # Load host configurations only if no specific host is provided if specified_host == "": host_configs = load_host_config(args.host_config) # Loop through each studio and their hosts from the YAML file for studio in host_configs['studios']: studio_name = studio['name'] for host in studio['hosts']: # Check if the host is reachable if is_host_reachable(host['host']): run_commands_for_host(studio_name, host, args.all, args.write_to_file, blob_service_client, json_format, user) else: logging.warning(f"Skipping unreachable host: {host['host']}") else: # Process only the specified host if it's reachable if is_host_reachable(specified_host): #host_info = {'host': specified_host, 'user': os.getenv('P4USER')} host_info = {'host': specified_host, 'user': user if user else os.getenv('P4USER')} run_commands_for_host(studio_name, host_info, args.all, args.write_to_file, blob_service_client, json_format, user) else: logging.warning(f"Specified host is unreachable: {specified_host}") except KeyboardInterrupt: logging.info("\nExecution interrupted by user. Exiting...") exit(0) # Define run_commands_for_host function here... if __name__ == "__main__": main()