#!/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()