mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-11-12 16:26:12 +01:00
- Changed from pinned version 1.86.1 to latest - Requires OpenLiteSpeed binaries with Origin header forwarding support - Compatible with n8n 1.87.0+ which has strict Origin validation Note: This requires the OpenLiteSpeed binary that includes the Origin header forwarding patch in the proxy module. The patch is available in the CyberPanel OpenLiteSpeed distribution.
1535 lines
62 KiB
Python
1535 lines
62 KiB
Python
#!/usr/local/CyberCP/bin/python
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from random import randint
|
|
import socket
|
|
import shutil
|
|
import docker
|
|
|
|
sys.path.append('/usr/local/CyberCP')
|
|
|
|
try:
|
|
import django
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
from plogical import randomPassword
|
|
from plogical.acl import ACLManager
|
|
from dockerManager.dockerInstall import DockerInstall
|
|
except:
|
|
pass
|
|
|
|
from plogical.processUtilities import ProcessUtilities
|
|
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
|
import argparse
|
|
import threading as multi
|
|
|
|
class DockerDeploymentError(Exception):
|
|
def __init__(self, message, error_code=None, recovery_possible=True):
|
|
self.message = message
|
|
self.error_code = error_code
|
|
self.recovery_possible = recovery_possible
|
|
super().__init__(self.message)
|
|
|
|
class Docker_Sites(multi.Thread):
|
|
Wordpress = 1
|
|
Joomla = 2
|
|
|
|
# Error codes
|
|
ERROR_DOCKER_NOT_INSTALLED = 'DOCKER_NOT_INSTALLED'
|
|
ERROR_PORT_IN_USE = 'PORT_IN_USE'
|
|
ERROR_CONTAINER_FAILED = 'CONTAINER_FAILED'
|
|
ERROR_NETWORK_FAILED = 'NETWORK_FAILED'
|
|
ERROR_VOLUME_FAILED = 'VOLUME_FAILED'
|
|
ERROR_DB_FAILED = 'DB_FAILED'
|
|
|
|
def __init__(self, function_run, data):
|
|
multi.Thread.__init__(self)
|
|
self.function_run = function_run
|
|
self.data = data
|
|
try:
|
|
self.JobID = self.data['JobID'] ##JOBID will be file path where status is being written
|
|
except:
|
|
pass
|
|
try:
|
|
### set docker name for listing/deleting etc
|
|
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
|
self.DockerAppName = f"{self.data['name'].replace(' ', '')}-{self.data['name'].replace(' ', '-')}"
|
|
else:
|
|
self.DockerAppName = f"{self.data['name'].replace(' ', '')}_{self.data['name'].replace(' ', '-')}"
|
|
except:
|
|
pass
|
|
|
|
command = 'cat /etc/csf/csf.conf'
|
|
result = ProcessUtilities.outputExecutioner(command)
|
|
|
|
if result.find('SECTION:Initial Settings') > -1:
|
|
|
|
from plogical.csf import CSF
|
|
from plogical.virtualHostUtilities import virtualHostUtilities
|
|
currentSettings = CSF.fetchCSFSettings()
|
|
|
|
tcpIN = currentSettings['tcpIN']
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'TCPIN docker: {tcpIN}')
|
|
|
|
|
|
|
|
if tcpIN.find('8088') == -1:
|
|
|
|
ports = f'{tcpIN},8088'
|
|
|
|
portsPath = '/home/cyberpanel/' + str(randint(1000, 9999))
|
|
|
|
if os.path.exists(portsPath):
|
|
os.remove(portsPath)
|
|
|
|
writeToFile = open(portsPath, 'w')
|
|
writeToFile.write(ports)
|
|
writeToFile.close()
|
|
|
|
execPath = "sudo /usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/csf.py"
|
|
execPath = execPath + f" modifyPorts --protocol TCP_IN --ports " + portsPath
|
|
ProcessUtilities.executioner(execPath)
|
|
|
|
tcpOUT = currentSettings['tcpOUT']
|
|
if tcpOUT.find('8088') == -1:
|
|
|
|
ports = f'{tcpOUT},8088'
|
|
|
|
portsPath = '/home/cyberpanel/' + str(randint(1000, 9999))
|
|
|
|
if os.path.exists(portsPath):
|
|
os.remove(portsPath)
|
|
|
|
writeToFile = open(portsPath, 'w')
|
|
writeToFile.write(ports)
|
|
writeToFile.close()
|
|
|
|
execPath = "sudo /usr/local/CyberCP/bin/python " + virtualHostUtilities.cyberPanel + "/plogical/csf.py"
|
|
execPath = execPath + f" modifyPorts --protocol TCP_OUT --ports " + portsPath
|
|
ProcessUtilities.executioner(execPath)
|
|
|
|
|
|
def run(self):
|
|
try:
|
|
if self.function_run == 'DeployWPContainer':
|
|
self.DeployWPContainer()
|
|
elif self.function_run == 'SubmitDockersiteCreation':
|
|
self.SubmitDockersiteCreation()
|
|
elif self.function_run == 'DeployN8NContainer':
|
|
self.DeployN8NContainer()
|
|
|
|
|
|
except BaseException as msg:
|
|
logging.writeToFile(str(msg) + ' [Docker_Sites.run]')
|
|
|
|
def InstallDocker(self):
|
|
|
|
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
|
|
|
command = 'dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo'
|
|
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
|
|
if ReturnCode:
|
|
pass
|
|
else:
|
|
return 0, ReturnCode
|
|
|
|
command = 'dnf install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y'
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
|
|
if ReturnCode:
|
|
pass
|
|
else:
|
|
return 0, ReturnCode
|
|
|
|
command = 'systemctl enable docker'
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
|
|
if ReturnCode:
|
|
pass
|
|
else:
|
|
return 0, ReturnCode
|
|
|
|
command = 'systemctl start docker'
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
|
|
if ReturnCode:
|
|
pass
|
|
else:
|
|
return 0, ReturnCode
|
|
|
|
command = 'curl -L "https://github.com/docker/compose/releases/download/v2.23.2/docker-compose-linux-x86_64" -o /usr/bin/docker-compose'
|
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
|
|
|
if ReturnCode:
|
|
pass
|
|
else:
|
|
return 0, ReturnCode
|
|
|
|
command = 'chmod +x /usr/bin/docker-compose'
|
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
|
|
|
if ReturnCode:
|
|
return 1, None
|
|
else:
|
|
return 0, ReturnCode
|
|
|
|
else:
|
|
# Add Docker's official GPG key
|
|
command = 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg'
|
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
# Add Docker repository
|
|
command = 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null'
|
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
# Update package index
|
|
command = 'apt-get update'
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
# Install Docker packages
|
|
command = 'apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin'
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
# Enable and start Docker service
|
|
command = 'systemctl enable docker'
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
command = 'systemctl start docker'
|
|
ReturnCode = ProcessUtilities.executioner(command)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
# Install Docker Compose
|
|
command = 'curl -L "https://github.com/docker/compose/releases/download/v2.23.2/docker-compose-linux-$(uname -m)" -o /usr/local/bin/docker-compose'
|
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
command = 'chmod +x /usr/local/bin/docker-compose'
|
|
ReturnCode = ProcessUtilities.executioner(command, 'root', True)
|
|
if not ReturnCode:
|
|
return 0, ReturnCode
|
|
|
|
return 1, None
|
|
|
|
@staticmethod
|
|
def SetupProxy(port):
|
|
import xml.etree.ElementTree as ET
|
|
|
|
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
|
|
ConfPath = '/usr/local/lsws/conf/httpd_config.conf'
|
|
data = open(ConfPath, 'r').read()
|
|
StringCheck = f"127.0.0.1:{port}"
|
|
if data.find(StringCheck) == -1:
|
|
ProxyContent = f"""
|
|
extprocessor docker{port} {{
|
|
type proxy
|
|
address 127.0.0.1:{port}
|
|
maxConns 100
|
|
pcKeepAliveTimeout 3600
|
|
initTimeout 300
|
|
retryTimeout 0
|
|
respBuffer 0
|
|
}}
|
|
"""
|
|
|
|
WriteToFile = open(ConfPath, 'a')
|
|
WriteToFile.write(ProxyContent)
|
|
WriteToFile.close()
|
|
|
|
else:
|
|
ConfPath = '/usr/local/lsws/conf/httpd_config.xml'
|
|
data = open(ConfPath, 'r').read()
|
|
|
|
# Parse the XML
|
|
root = ET.fromstring(data)
|
|
|
|
# Find the <extProcessorList> node
|
|
ext_processor_list = root.find('extProcessorList')
|
|
|
|
# Create the new <extProcessor> node
|
|
new_ext_processor = ET.Element('extProcessor')
|
|
|
|
# Add child elements to the new <extProcessor>
|
|
ET.SubElement(new_ext_processor, 'type').text = 'proxy'
|
|
ET.SubElement(new_ext_processor, 'name').text = f'docker{port}'
|
|
ET.SubElement(new_ext_processor, 'address').text = f'127.0.0.1:{port}'
|
|
ET.SubElement(new_ext_processor, 'maxConns').text = '100'
|
|
ET.SubElement(new_ext_processor, 'pcKeepAliveTimeout').text = '3600'
|
|
ET.SubElement(new_ext_processor, 'initTimeout').text = '300'
|
|
ET.SubElement(new_ext_processor, 'retryTimeout').text = '0'
|
|
ET.SubElement(new_ext_processor, 'respBuffer').text = '0'
|
|
|
|
# Append the new <extProcessor> to the <extProcessorList>
|
|
ext_processor_list.append(new_ext_processor)
|
|
|
|
# Write the updated XML content to a new file or print it out
|
|
tree = ET.ElementTree(root)
|
|
tree.write(ConfPath, encoding='UTF-8', xml_declaration=True)
|
|
|
|
# Optionally, print the updated XML
|
|
ET.dump(root)
|
|
|
|
|
|
@staticmethod
|
|
def SetupN8NVhost(domain, port):
|
|
"""Setup n8n vhost with proper proxy configuration including Origin header"""
|
|
try:
|
|
vhost_path = f'/usr/local/lsws/conf/vhosts/{domain}/vhost.conf'
|
|
|
|
if not os.path.exists(vhost_path):
|
|
logging.writeToFile(f"Error: Vhost file not found at {vhost_path}")
|
|
return False
|
|
|
|
# Read existing vhost configuration
|
|
with open(vhost_path, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Check if context already exists
|
|
if 'context / {' in content:
|
|
logging.writeToFile("Context already exists, skipping...")
|
|
return True
|
|
|
|
# Add proxy context with proper headers for n8n
|
|
proxy_context = f'''
|
|
|
|
# N8N Proxy Configuration
|
|
context / {{
|
|
type proxy
|
|
handler docker{port}
|
|
addDefaultCharset off
|
|
websocket 1
|
|
|
|
extraHeaders <<<END_extraHeaders
|
|
RequestHeader unset X-Forwarded-For
|
|
RequestHeader set X-Forwarded-For $ip
|
|
RequestHeader set X-Forwarded-Proto https
|
|
RequestHeader set X-Forwarded-Host "{domain}"
|
|
RequestHeader set Host "{domain}"
|
|
END_extraHeaders
|
|
}}
|
|
'''
|
|
|
|
# Append at the end of file
|
|
with open(vhost_path, 'a') as f:
|
|
f.write(proxy_context)
|
|
|
|
logging.writeToFile(f"Successfully updated vhost for {domain}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f'Error setting up n8n vhost: {str(e)}')
|
|
return False
|
|
|
|
@staticmethod
|
|
def SetupHTAccess(port, htaccess):
|
|
### Update htaccess
|
|
|
|
StringCheck = f'docker{port}'
|
|
|
|
try:
|
|
Content = open(htaccess, 'r').read()
|
|
except:
|
|
Content = ''
|
|
|
|
print(f'value of content {Content}')
|
|
|
|
if Content.find(StringCheck) == -1:
|
|
HTAccessContent = f'''
|
|
RewriteEngine On
|
|
REWRITERULE ^(.*)$ HTTP://docker{port}/$1 [P]
|
|
'''
|
|
WriteToFile = open(htaccess, 'a')
|
|
WriteToFile.write(HTAccessContent)
|
|
WriteToFile.close()
|
|
|
|
# Takes
|
|
# ComposePath, MySQLPath, MySQLRootPass, MySQLDBName, MySQLDBNUser, MySQLPassword, CPUsMySQL, MemoryMySQL,
|
|
# port, SitePath, CPUsSite, MemorySite, ComposePath, SiteName
|
|
# finalURL, blogTitle, adminUser, adminPassword, adminEmail, htaccessPath, externalApp
|
|
|
|
def DeployWPContainer(self):
|
|
|
|
try:
|
|
logging.statusWriter(self.JobID, 'Checking if Docker is installed..,0')
|
|
|
|
command = 'docker --help'
|
|
result = ProcessUtilities.outputExecutioner(command)
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'return code of docker install {result}')
|
|
|
|
if result.find("not found") > -1:
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'About to run docker install function...')
|
|
|
|
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/dockerManager/dockerInstall.py"
|
|
ProcessUtilities.executioner(execPath)
|
|
|
|
logging.statusWriter(self.JobID, 'Docker is ready to use..,10')
|
|
|
|
self.data['ServiceName'] = self.data["SiteName"].replace(' ', '-')
|
|
|
|
WPSite = f'''
|
|
version: '3.8'
|
|
|
|
services:
|
|
'{self.data['ServiceName']}':
|
|
user: root
|
|
image: cyberpanel/openlitespeed:latest
|
|
ports:
|
|
- "{self.data['port']}:8088"
|
|
# - "443:443"
|
|
environment:
|
|
DB_NAME: "{self.data['MySQLDBName']}"
|
|
DB_USER: "{self.data['MySQLDBNUser']}"
|
|
DB_PASSWORD: "{self.data['MySQLPassword']}"
|
|
WP_ADMIN_EMAIL: "{self.data['adminEmail']}"
|
|
WP_ADMIN_USER: "{self.data['adminUser']}"
|
|
WP_ADMIN_PASSWORD: "{self.data['adminPassword']}"
|
|
WP_URL: {self.data['finalURL']}
|
|
DB_Host: '{self.data['ServiceName']}-db:3306'
|
|
SITE_NAME: '{self.data['SiteName']}'
|
|
volumes:
|
|
# - "/home/docker/{self.data['finalURL']}:/usr/local/lsws/Example/html"
|
|
- "/home/docker/{self.data['finalURL']}/data:/usr/local/lsws/Example/html"
|
|
depends_on:
|
|
- '{self.data['ServiceName']}-db'
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '{self.data['CPUsSite']}' # Use 50% of one CPU core
|
|
memory: {self.data['MemorySite']}M # Limit memory to 512 megabytes
|
|
'{self.data['ServiceName']}-db':
|
|
image: mariadb
|
|
restart: always
|
|
environment:
|
|
# ALLOW_EMPTY_PASSWORD=no
|
|
MYSQL_DATABASE: '{self.data['MySQLDBName']}'
|
|
MYSQL_USER: '{self.data['MySQLDBNUser']}'
|
|
MYSQL_PASSWORD: '{self.data['MySQLPassword']}'
|
|
MYSQL_ROOT_PASSWORD: '{self.data['MySQLPassword']}'
|
|
volumes:
|
|
- "/home/docker/{self.data['finalURL']}/db:/var/lib/mysql"
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '{self.data['CPUsMySQL']}' # Use 50% of one CPU core
|
|
memory: {self.data['MemoryMySQL']}M # Limit memory to 512 megabytes
|
|
'''
|
|
|
|
### WriteConfig to compose-file
|
|
|
|
command = f"mkdir -p /home/docker/{self.data['finalURL']}"
|
|
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
if result == 0:
|
|
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]')
|
|
return 0
|
|
|
|
TempCompose = f'/home/cyberpanel/{self.data["finalURL"]}-docker-compose.yml'
|
|
|
|
WriteToFile = open(TempCompose, 'w')
|
|
WriteToFile.write(WPSite)
|
|
WriteToFile.close()
|
|
|
|
command = f"mv {TempCompose} {self.data['ComposePath']}"
|
|
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
if result == 0:
|
|
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]')
|
|
return 0
|
|
|
|
command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}"
|
|
ProcessUtilities.executioner(command, 'root', True)
|
|
|
|
####
|
|
|
|
if ProcessUtilities.decideDistro() == ProcessUtilities.cent8 or ProcessUtilities.decideDistro() == ProcessUtilities.centos:
|
|
dockerCommand = 'docker compose'
|
|
else:
|
|
dockerCommand = 'docker-compose'
|
|
|
|
command = f"{dockerCommand} -f {self.data['ComposePath']} -p '{self.data['SiteName']}' up -d"
|
|
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(message)
|
|
|
|
if result == 0:
|
|
logging.statusWriter(self.JobID, f'Error {str(message)} . [404]')
|
|
return 0
|
|
|
|
logging.statusWriter(self.JobID, 'Bringing containers online..,50')
|
|
|
|
time.sleep(25)
|
|
|
|
### checking if everything ran properly
|
|
|
|
passdata = {}
|
|
passdata["JobID"] = None
|
|
passdata['name'] = self.data['ServiceName']
|
|
da = Docker_Sites(None, passdata)
|
|
retdata, containers = da.ListContainers()
|
|
|
|
containers = json.loads(containers)
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(str(containers))
|
|
|
|
### it means less then two containers which means something went wrong
|
|
if len(containers) < 2:
|
|
logging.writeToFile(f'Unkonwn error, containers not running. [DeployWPContainer]')
|
|
logging.statusWriter(self.JobID, f'Unkonwn error, containers not running. [DeployWPContainer]')
|
|
return 0
|
|
|
|
### Set up Proxy
|
|
|
|
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
|
|
execPath = execPath + f" SetupProxy --port {self.data['port']}"
|
|
ProcessUtilities.executioner(execPath)
|
|
|
|
### Set up ht access
|
|
|
|
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
|
|
execPath = execPath + f" SetupHTAccess --port {self.data['port']} --htaccess {self.data['htaccessPath']}"
|
|
ProcessUtilities.executioner(execPath, self.data['externalApp'])
|
|
|
|
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
|
group = 'nobody'
|
|
else:
|
|
group = 'nogroup'
|
|
|
|
command = f"chown -R nobody:{group} /home/docker/{self.data['finalURL']}/data"
|
|
ProcessUtilities.executioner(command)
|
|
|
|
### just restart ls for htaccess
|
|
|
|
from plogical.installUtilities import installUtilities
|
|
installUtilities.reStartLiteSpeedSocket()
|
|
|
|
logging.statusWriter(self.JobID, 'Completed. [200]')
|
|
|
|
# command = f"docker-compose -f {self.data['ComposePath']} ps -q wordpress"
|
|
# stdout = ProcessUtilities.outputExecutioner(command)
|
|
#
|
|
# self.ContainerID = stdout.rstrip('\n')
|
|
|
|
# command = f'docker-compose -f {self.data["ComposePath"]} exec {self.ContainerID} curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'
|
|
# result = ProcessUtilities.outputExecutioner(command)
|
|
#
|
|
# if os.path.exists(ProcessUtilities.debugPath):
|
|
# logging.writeToFile(result)
|
|
#
|
|
# command = f"docker-compose -f {self.data['ComposePath']} exec {self.ContainerID} chmod + wp-cli.phar"
|
|
# result = ProcessUtilities.outputExecutioner(command)
|
|
#
|
|
# if os.path.exists(ProcessUtilities.debugPath):
|
|
# logging.writeToFile(result)
|
|
#
|
|
# command = f"docker-compose -f {self.data['ComposePath']} exec {self.ContainerID} mv wp-cli.phar /bin/wp"
|
|
# result = ProcessUtilities.outputExecutioner(command)
|
|
#
|
|
# if os.path.exists(ProcessUtilities.debugPath):
|
|
# logging.writeToFile(result)
|
|
|
|
# command = f'docker-compose -f {self.data["ComposePath"]} exec {self.ContainerID} wp core install --url="http://{self.data["finalURL"]}" --title="{self.data["blogTitle"]}" --admin_user="{self.data["adminUser"]}" --admin_password="{self.data["adminPassword"]}" --admin_email="{self.data["adminEmail"]}" --path=. --allow-root'
|
|
# result = ProcessUtilities.outputExecutioner(command)
|
|
#
|
|
# if os.path.exists(ProcessUtilities.debugPath):
|
|
# logging.writeToFile(result)
|
|
|
|
except BaseException as msg:
|
|
logging.writeToFile(f'{str(msg)}. [DeployWPContainer]')
|
|
logging.statusWriter(self.JobID, f'Error {str(msg)} . [404]')
|
|
print(str(msg))
|
|
pass
|
|
|
|
def SubmitDockersiteCreation(self):
|
|
try:
|
|
|
|
from websiteFunctions.models import DockerSites, Websites
|
|
from websiteFunctions.website import WebsiteManager
|
|
|
|
tempStatusPath = self.data['JobID']
|
|
statusFile = open(tempStatusPath, 'w')
|
|
statusFile.writelines('Creating Website...,10')
|
|
statusFile.close()
|
|
|
|
Domain = self.data['Domain']
|
|
WPemal = self.data['WPemal']
|
|
Owner = self.data['Owner']
|
|
userID = self.data['userID']
|
|
MysqlCPU = self.data['MysqlCPU']
|
|
MYsqlRam = self.data['MYsqlRam']
|
|
SiteCPU = self.data['SiteCPU']
|
|
SiteRam = self.data['SiteRam']
|
|
sitename = self.data['sitename']
|
|
WPusername = self.data['WPusername']
|
|
WPpasswd = self.data['WPpasswd']
|
|
externalApp = self.data['externalApp']
|
|
|
|
currentTemp = tempStatusPath
|
|
|
|
DataToPass = {}
|
|
DataToPass['tempStatusPath'] = tempStatusPath
|
|
DataToPass['domainName'] = Domain
|
|
DataToPass['adminEmail'] = WPemal
|
|
DataToPass['phpSelection'] = "PHP 8.1"
|
|
DataToPass['websiteOwner'] = Owner
|
|
DataToPass['package'] = 'Default'
|
|
DataToPass['ssl'] = 1
|
|
DataToPass['dkimCheck'] = 0
|
|
DataToPass['openBasedir'] = 0
|
|
DataToPass['mailDomain'] = 0
|
|
DataToPass['apacheBackend'] = 0
|
|
UserID = userID
|
|
|
|
if Websites.objects.filter(domain=DataToPass['domainName']).count() == 0:
|
|
try:
|
|
website = Websites.objects.get(domain=DataToPass['domainName'])
|
|
|
|
if website.phpSelection == 'PHP 7.3':
|
|
website.phpSelection = 'PHP 8.0'
|
|
website.save()
|
|
|
|
if ACLManager.checkOwnership(website.domain, self.data['adminID'],
|
|
self.data['currentACL']) == 0:
|
|
statusFile = open(tempStatusPath, 'w')
|
|
statusFile.writelines('You dont own this site.[404]')
|
|
statusFile.close()
|
|
except:
|
|
|
|
ab = WebsiteManager()
|
|
coreResult = ab.submitWebsiteCreation(UserID, DataToPass)
|
|
coreResult1 = json.loads((coreResult).content)
|
|
logging.writeToFile("Creating website result....%s" % coreResult1)
|
|
reutrntempath = coreResult1['tempStatusPath']
|
|
while (1):
|
|
lastLine = open(reutrntempath, 'r').read()
|
|
logging.writeToFile("Error web creating lastline ....... %s" % lastLine)
|
|
if lastLine.find('[200]') > -1:
|
|
break
|
|
elif lastLine.find('[404]') > -1:
|
|
statusFile = open(currentTemp, 'w')
|
|
statusFile.writelines('Failed to Create Website: error: %s. [404]' % lastLine)
|
|
statusFile.close()
|
|
return 0
|
|
else:
|
|
statusFile = open(currentTemp, 'w')
|
|
statusFile.writelines('Creating Website....,20')
|
|
statusFile.close()
|
|
time.sleep(2)
|
|
|
|
statusFile = open(tempStatusPath, 'w')
|
|
statusFile.writelines('Creating DockerSite....,30')
|
|
statusFile.close()
|
|
|
|
webobj = Websites.objects.get(domain=Domain)
|
|
|
|
if webobj.dockersites_set.all().count() > 0:
|
|
logging.statusWriter(self.JobID, f'Docker container already exists on this domain. [404]')
|
|
return 0
|
|
|
|
dbname = randomPassword.generate_pass()
|
|
dbpasswd = randomPassword.generate_pass()
|
|
dbusername = randomPassword.generate_pass()
|
|
MySQLRootPass = randomPassword.generate_pass()
|
|
|
|
if DockerSites.objects.count() == 0:
|
|
port = '11000'
|
|
else:
|
|
port = str(int(DockerSites.objects.last().port) + 1)
|
|
|
|
f_data = {
|
|
"JobID": tempStatusPath,
|
|
"ComposePath": f"/home/docker/{Domain}/docker-compose.yml",
|
|
"MySQLPath": f'/home/{Domain}/public_html/sqldocker',
|
|
"MySQLRootPass": MySQLRootPass,
|
|
"MySQLDBName": dbname,
|
|
"MySQLDBNUser": dbusername,
|
|
"MySQLPassword": dbpasswd,
|
|
"CPUsMySQL": MysqlCPU,
|
|
"MemoryMySQL": MYsqlRam,
|
|
"port": port,
|
|
"SitePath": f'/home/{Domain}/public_html/wpdocker',
|
|
"CPUsSite": SiteCPU,
|
|
"MemorySite": SiteRam,
|
|
"SiteName": sitename,
|
|
"finalURL": Domain,
|
|
"blogTitle": sitename,
|
|
"adminUser": WPusername,
|
|
"adminPassword": WPpasswd,
|
|
"adminEmail": WPemal,
|
|
"htaccessPath": f'/home/{Domain}/public_html/.htaccess',
|
|
"externalApp": webobj.externalApp,
|
|
"docRoot": f"/home/{Domain}"
|
|
}
|
|
|
|
dockersiteobj = DockerSites(
|
|
admin=webobj, ComposePath=f"/home/{Domain}/docker-compose.yml",
|
|
SitePath=f'/home/{Domain}/public_html/wpdocker',
|
|
MySQLPath=f'/home/{Domain}/public_html/sqldocker', SiteType=Docker_Sites.Wordpress, MySQLDBName=dbname,
|
|
MySQLDBNUser=dbusername, CPUsMySQL=MysqlCPU, MemoryMySQL=MYsqlRam, port=port, CPUsSite=SiteCPU,
|
|
MemorySite=SiteRam,
|
|
SiteName=sitename, finalURL=Domain, blogTitle=sitename, adminUser=WPusername, adminEmail=WPemal
|
|
)
|
|
dockersiteobj.save()
|
|
|
|
if self.data['App'] == 'WordPress':
|
|
background = Docker_Sites('DeployWPContainer', f_data)
|
|
background.start()
|
|
elif self.data['App'] == 'n8n':
|
|
background = Docker_Sites('DeployN8NContainer', f_data)
|
|
background.start()
|
|
|
|
except BaseException as msg:
|
|
logging.writeToFile("Error Submit Docker site Creation ....... %s" % str(msg))
|
|
return 0
|
|
|
|
def DeleteDockerApp(self):
|
|
try:
|
|
|
|
command = f'docker-compose -f /home/docker/{self.data["domain"]}/docker-compose.yml down'
|
|
ProcessUtilities.executioner(command)
|
|
|
|
command = f'rm -rf /home/docker/{self.data["domain"]}'
|
|
ProcessUtilities.executioner(command)
|
|
|
|
command = f'rm -f /home/{self.data["domain"]}/public_html/.htaccess'
|
|
ProcessUtilities.executioner(command)
|
|
|
|
|
|
### forcefully delete containers
|
|
|
|
# Create a Docker client
|
|
client = docker.from_env()
|
|
|
|
FilerValue = self.DockerAppName
|
|
|
|
# Define the label to filter containers
|
|
label_filter = {'name': FilerValue}
|
|
|
|
# List containers matching the label filter
|
|
containers = client.containers.list(filters=label_filter)
|
|
|
|
logging.writeToFile(f'List of containers {str(containers)}')
|
|
|
|
|
|
for container in containers:
|
|
command = f'docker stop {container.short_id}'
|
|
ProcessUtilities.executioner(command)
|
|
|
|
command = f'docker rm {container.short_id}'
|
|
ProcessUtilities.executioner(command)
|
|
|
|
|
|
command = f"rm -rf /home/{self.data['domain']}/public_html/.htaccess'"
|
|
ProcessUtilities.executioner(command)
|
|
|
|
from plogical.installUtilities import installUtilities
|
|
installUtilities.reStartLiteSpeed()
|
|
|
|
except BaseException as msg:
|
|
logging.writeToFile("Error Delete Docker APP ....... %s" % str(msg))
|
|
return 0
|
|
|
|
## This function need site name which was passed while creating the app
|
|
def ListContainers(self):
|
|
try:
|
|
# Create a Docker client
|
|
client = docker.from_env()
|
|
|
|
# Debug logging
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'DockerAppName: {self.DockerAppName}')
|
|
|
|
# List all containers without filtering first
|
|
all_containers = client.containers.list(all=True)
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'Total containers found: {len(all_containers)}')
|
|
for container in all_containers:
|
|
logging.writeToFile(f'Container name: {container.name}')
|
|
|
|
# Now filter containers - handle both CentOS and Ubuntu naming
|
|
containers = []
|
|
|
|
# Get both possible name formats
|
|
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
|
|
search_name = self.DockerAppName # Already in hyphen format for CentOS
|
|
else:
|
|
# For Ubuntu, convert underscore to hyphen as containers use hyphens
|
|
search_name = self.DockerAppName.replace('_', '-')
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'Searching for containers with name containing: {search_name}')
|
|
|
|
for container in all_containers:
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'Checking container: {container.name} against filter: {search_name}')
|
|
if search_name.lower() in container.name.lower():
|
|
containers.append(container)
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'Filtered containers count: {len(containers)}')
|
|
|
|
json_data = "["
|
|
checker = 0
|
|
|
|
for container in containers:
|
|
try:
|
|
dic = {
|
|
'id': container.short_id,
|
|
'name': container.name,
|
|
'status': container.status,
|
|
'state': container.attrs.get('State', {}),
|
|
'health': container.attrs.get('State', {}).get('Health', {}).get('Status', 'unknown'),
|
|
'volumes': container.attrs['HostConfig']['Binds'] if 'HostConfig' in container.attrs else [],
|
|
'logs_50': container.logs(tail=50).decode('utf-8'),
|
|
'ports': container.attrs['HostConfig']['PortBindings'] if 'HostConfig' in container.attrs else {}
|
|
}
|
|
|
|
if checker == 0:
|
|
json_data = json_data + json.dumps(dic)
|
|
checker = 1
|
|
else:
|
|
json_data = json_data + ',' + json.dumps(dic)
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error processing container {container.name}: {str(e)}")
|
|
continue
|
|
|
|
json_data = json_data + ']'
|
|
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'Final JSON data: {json_data}')
|
|
|
|
return 1, json_data
|
|
|
|
except BaseException as msg:
|
|
logging.writeToFile("List Container ....... %s" % str(msg))
|
|
return 0, str(msg)
|
|
|
|
### pass container id and number of lines to fetch from logs
|
|
def ContainerLogs(self):
|
|
try:
|
|
# Create a Docker client
|
|
client = docker.from_env()
|
|
|
|
# Get the container by ID
|
|
container = client.containers.get(self.data['containerID'])
|
|
|
|
# Fetch last 'tail' logs for the container
|
|
logs = container.logs(tail=self.data['numberOfLines']).decode('utf-8')
|
|
|
|
return 1, logs
|
|
except BaseException as msg:
|
|
logging.writeToFile("List Container ....... %s" % str(msg))
|
|
return 0, str(msg)
|
|
|
|
### pass container id and number of lines to fetch from logs
|
|
|
|
def ContainerInfo(self):
|
|
try:
|
|
# Create a Docker client
|
|
client = docker.from_env()
|
|
|
|
# Get the container by ID
|
|
container = client.containers.get(self.data['containerID'])
|
|
|
|
# Fetch container stats
|
|
stats = container.stats(stream=False)
|
|
|
|
dic = {
|
|
'id': container.short_id,
|
|
'name': container.name,
|
|
'status': container.status,
|
|
'volumes': container.attrs['HostConfig']['Binds'] if 'HostConfig' in container.attrs else [],
|
|
'logs_50': container.logs(tail=50).decode('utf-8'),
|
|
'ports': container.attrs['HostConfig']['PortBindings'] if 'HostConfig' in container.attrs else {},
|
|
'memory': stats['memory_stats']['usage'],
|
|
'cpu' : stats['cpu_stats']['cpu_usage']['total_usage']
|
|
}
|
|
|
|
return 1, dic
|
|
except BaseException as msg:
|
|
logging.writeToFile("List Container ....... %s" % str(msg))
|
|
return 0, str(msg)
|
|
|
|
def RebuildApp(self):
|
|
self.DeleteDockerApp()
|
|
self.SubmitDockersiteCreation()
|
|
|
|
def RestartContainer(self):
|
|
try:
|
|
# Create a Docker client
|
|
client = docker.from_env()
|
|
|
|
# Get the container by ID
|
|
container = client.containers.get(self.data['containerID'])
|
|
|
|
container.restart()
|
|
|
|
return 1, None
|
|
except BaseException as msg:
|
|
logging.writeToFile("List Container ....... %s" % str(msg))
|
|
return 0, str(msg)
|
|
|
|
def StopContainer(self):
|
|
try:
|
|
# Create a Docker client
|
|
client = docker.from_env()
|
|
|
|
# Get the container by ID
|
|
container = client.containers.get(self.data['containerID'])
|
|
|
|
container.stop()
|
|
|
|
return 1, None
|
|
except BaseException as msg:
|
|
logging.writeToFile("List Container ....... %s" % str(msg))
|
|
return 0, str(msg)
|
|
|
|
##### N8N Container
|
|
|
|
def check_container_health(self, container_name, max_retries=3, delay=80):
|
|
"""
|
|
Check if a container is running, accepting healthy, unhealthy, and starting states
|
|
Total wait time will be 4 minutes (3 retries * 80 seconds)
|
|
"""
|
|
try:
|
|
# Format container name to match Docker's naming convention
|
|
formatted_name = f"{self.data['ServiceName']}-{container_name}-1"
|
|
logging.writeToFile(f'Checking container health for: {formatted_name}')
|
|
|
|
for attempt in range(max_retries):
|
|
client = docker.from_env()
|
|
container = client.containers.get(formatted_name)
|
|
|
|
if container.status == 'running':
|
|
health = container.attrs.get('State', {}).get('Health', {}).get('Status')
|
|
|
|
# Accept healthy, unhealthy, and starting states as long as container is running
|
|
if health in ['healthy', 'unhealthy', 'starting'] or health is None:
|
|
logging.writeToFile(f'Container {formatted_name} is running with status: {health}')
|
|
return True
|
|
else:
|
|
health_logs = container.attrs.get('State', {}).get('Health', {}).get('Log', [])
|
|
if health_logs:
|
|
last_log = health_logs[-1]
|
|
logging.writeToFile(f'Container health check failed: {last_log.get("Output", "")}')
|
|
|
|
logging.writeToFile(f'Container {formatted_name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}')
|
|
time.sleep(delay)
|
|
|
|
return False
|
|
|
|
except docker.errors.NotFound:
|
|
logging.writeToFile(f'Container {formatted_name} not found')
|
|
return False
|
|
except Exception as e:
|
|
logging.writeToFile(f'Error checking container health: {str(e)}')
|
|
return False
|
|
|
|
def verify_system_resources(self):
|
|
try:
|
|
# Check available disk space using root access
|
|
command = "df -B 1G /home/docker --output=avail | tail -1"
|
|
result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if result == 0:
|
|
raise DockerDeploymentError("Failed to check disk space")
|
|
available_gb = int(output.strip())
|
|
|
|
if available_gb < 5: # Require minimum 5GB free space
|
|
raise DockerDeploymentError(
|
|
f"Insufficient disk space. Need at least 5GB but only {available_gb}GB available.",
|
|
self.ERROR_VOLUME_FAILED
|
|
)
|
|
|
|
# Check if Docker is running and accessible
|
|
command = "systemctl is-active docker"
|
|
result, docker_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if result == 0:
|
|
raise DockerDeploymentError("Failed to check Docker status")
|
|
if docker_status.strip() != "active":
|
|
raise DockerDeploymentError("Docker service is not running")
|
|
|
|
# Check Docker system info for resource limits
|
|
command = "docker info --format '{{.MemTotal}}'"
|
|
result, total_memory = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if result == 0:
|
|
raise DockerDeploymentError("Failed to get Docker memory info")
|
|
|
|
# Convert total_memory from bytes to MB
|
|
total_memory_mb = int(total_memory.strip()) / (1024 * 1024)
|
|
|
|
# Calculate required memory from site and MySQL requirements
|
|
required_memory = int(self.data['MemoryMySQL']) + int(self.data['MemorySite'])
|
|
|
|
if total_memory_mb < required_memory:
|
|
raise DockerDeploymentError(
|
|
f"Insufficient memory. Need {required_memory}MB but only {int(total_memory_mb)}MB available",
|
|
'INSUFFICIENT_MEMORY'
|
|
)
|
|
|
|
# Verify Docker group and permissions
|
|
command = "getent group docker"
|
|
result, docker_group = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if result == 0 or not docker_group:
|
|
raise DockerDeploymentError("Docker group does not exist")
|
|
|
|
return True
|
|
|
|
except DockerDeploymentError as e:
|
|
raise e
|
|
except Exception as e:
|
|
raise DockerDeploymentError(f"Resource verification failed: {str(e)}")
|
|
|
|
def setup_docker_environment(self):
|
|
try:
|
|
# Create docker directory with root
|
|
command = f"mkdir -p /home/docker/{self.data['finalURL']}"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Set proper permissions
|
|
command = f"chown -R {self.data['externalApp']}:docker /home/docker/{self.data['finalURL']}"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Create docker network if doesn't exist
|
|
command = "docker network ls | grep cyberpanel"
|
|
network_exists = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if not network_exists:
|
|
command = "docker network create cyberpanel"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
raise DockerDeploymentError(f"Environment setup failed: {str(e)}")
|
|
|
|
def deploy_containers(self):
|
|
try:
|
|
# Write docker-compose file
|
|
command = f"cat > {self.data['ComposePath']} << 'EOF'\n{self.data['ComposeContent']}\nEOF"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Set proper permissions on compose file
|
|
command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Deploy with docker-compose
|
|
command = f"cd {os.path.dirname(self.data['ComposePath'])} && docker-compose up -d"
|
|
result = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
if "error" in result.lower():
|
|
raise DockerDeploymentError(f"Container deployment failed: {result}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
raise DockerDeploymentError(f"Deployment failed: {str(e)}")
|
|
|
|
def cleanup_failed_deployment(self):
|
|
try:
|
|
# Stop and remove containers
|
|
command = f"cd {os.path.dirname(self.data['ComposePath'])} && docker-compose down -v"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Remove docker directory
|
|
command = f"rm -rf /home/docker/{self.data['finalURL']}"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Remove compose file
|
|
command = f"rm -f {self.data['ComposePath']}"
|
|
ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Cleanup failed: {str(e)}")
|
|
return False
|
|
|
|
def monitor_deployment(self):
|
|
try:
|
|
# Format container names
|
|
n8n_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-1"
|
|
db_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-db-1"
|
|
|
|
logging.writeToFile(f'Monitoring containers: {n8n_container_name} and {db_container_name}')
|
|
|
|
# Check container health
|
|
command = f"docker ps -a --filter name={self.data['ServiceName']} --format '{{{{.Status}}}}'"
|
|
result, status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Only raise error if container is exited
|
|
if "exited" in status:
|
|
# Get container logs
|
|
command = f"docker logs {n8n_container_name}"
|
|
result, logs = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
raise DockerDeploymentError(f"Container exited. Logs: {logs}")
|
|
|
|
# Wait for database to be ready
|
|
max_retries = 30
|
|
retry_count = 0
|
|
db_ready = False
|
|
|
|
while retry_count < max_retries:
|
|
# Check if database container is ready
|
|
command = f"docker exec {db_container_name} pg_isready -U postgres"
|
|
result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
if "accepting connections" in output:
|
|
db_ready = True
|
|
break
|
|
|
|
# Check container status
|
|
command = f"docker inspect --format='{{{{.State.Status}}}}' {db_container_name}"
|
|
result, db_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Only raise error if database container is in a failed state
|
|
if db_status == 'exited':
|
|
raise DockerDeploymentError(f"Database container is in {db_status} state")
|
|
|
|
retry_count += 1
|
|
time.sleep(2)
|
|
logging.writeToFile(f'Waiting for database to be ready, attempt {retry_count}/{max_retries}')
|
|
|
|
if not db_ready:
|
|
raise DockerDeploymentError("Database failed to become ready within timeout period")
|
|
|
|
# Check n8n container status
|
|
command = f"docker inspect --format='{{{{.State.Status}}}}' {n8n_container_name}"
|
|
result, n8n_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
|
|
# Only raise error if n8n container is in a failed state
|
|
if n8n_status == 'exited':
|
|
raise DockerDeploymentError(f"n8n container is in {n8n_status} state")
|
|
|
|
logging.writeToFile(f'Deployment monitoring completed successfully. n8n status: {n8n_status}, database ready: {db_ready}')
|
|
return True
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f'Error during monitoring: {str(e)}')
|
|
raise DockerDeploymentError(f"Monitoring failed: {str(e)}")
|
|
|
|
def handle_deployment_failure(self, error, cleanup=True):
|
|
"""
|
|
Handle deployment failures and attempt recovery
|
|
"""
|
|
try:
|
|
logging.writeToFile(f'Deployment failed: {str(error)}')
|
|
|
|
if cleanup:
|
|
self.cleanup_failed_deployment()
|
|
|
|
if isinstance(error, DockerDeploymentError):
|
|
if error.error_code == self.ERROR_DOCKER_NOT_INSTALLED:
|
|
# Attempt to install Docker
|
|
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/dockerManager/dockerInstall.py"
|
|
ProcessUtilities.executioner(execPath)
|
|
return True
|
|
|
|
elif error.error_code == self.ERROR_PORT_IN_USE:
|
|
# Find next available port
|
|
new_port = int(self.data['port']) + 1
|
|
while new_port < 65535:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
result = sock.connect_ex(('127.0.0.1', new_port))
|
|
sock.close()
|
|
if result != 0:
|
|
self.data['port'] = str(new_port)
|
|
return True
|
|
new_port += 1
|
|
|
|
elif error.error_code == self.ERROR_DB_FAILED:
|
|
# Attempt database recovery
|
|
return self.recover_database()
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f'Error during failure handling: {str(e)}')
|
|
return False
|
|
|
|
def recover_database(self):
|
|
"""
|
|
Attempt to recover the database container
|
|
"""
|
|
try:
|
|
client = docker.from_env()
|
|
db_container_name = f"{self.data['ServiceName']}-db"
|
|
|
|
try:
|
|
db_container = client.containers.get(db_container_name)
|
|
|
|
if db_container.status == 'running':
|
|
exec_result = db_container.exec_run(
|
|
'pg_isready -U postgres'
|
|
)
|
|
|
|
if exec_result.exit_code != 0:
|
|
db_container.restart()
|
|
time.sleep(10)
|
|
|
|
if self.check_container_health(db_container_name):
|
|
return True
|
|
|
|
except docker.errors.NotFound:
|
|
pass
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f'Database recovery failed: {str(e)}')
|
|
return False
|
|
|
|
def log_deployment_metrics(self, metrics):
|
|
"""
|
|
Log deployment metrics for analysis
|
|
"""
|
|
if metrics:
|
|
try:
|
|
log_file = f"/var/log/cyberpanel/docker/{self.data['ServiceName']}_metrics.json"
|
|
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
|
|
with open(log_file, 'w') as f:
|
|
json.dump(metrics, f, indent=2)
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f'Error logging metrics: {str(e)}')
|
|
|
|
def DeployN8NContainer(self):
|
|
"""
|
|
Main deployment method with error handling
|
|
"""
|
|
max_retries = 3
|
|
current_try = 0
|
|
|
|
while current_try < max_retries:
|
|
try:
|
|
logging.statusWriter(self.JobID, 'Starting deployment verification...,0')
|
|
|
|
# Check Docker installation
|
|
command = 'docker --help'
|
|
result = ProcessUtilities.outputExecutioner(command)
|
|
if result.find("not found") > -1:
|
|
if os.path.exists(ProcessUtilities.debugPath):
|
|
logging.writeToFile(f'About to run docker install function...')
|
|
|
|
# Call InstallDocker to install Docker
|
|
install_result, error = self.InstallDocker()
|
|
if not install_result:
|
|
logging.statusWriter(self.JobID, f'Failed to install Docker: {error} [404]')
|
|
return 0
|
|
|
|
logging.statusWriter(self.JobID, 'Docker installation verified...,20')
|
|
|
|
# Verify system resources
|
|
self.verify_system_resources()
|
|
logging.statusWriter(self.JobID, 'System resources verified...,10')
|
|
|
|
# Create directories
|
|
command = f"mkdir -p /home/docker/{self.data['finalURL']}"
|
|
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if result == 0:
|
|
raise DockerDeploymentError(f"Failed to create directories: {message}")
|
|
logging.statusWriter(self.JobID, 'Directories created...,30')
|
|
|
|
# Generate and write docker-compose file
|
|
self.data['ServiceName'] = self.data["SiteName"].replace(' ', '-')
|
|
compose_config = self.generate_compose_config()
|
|
|
|
TempCompose = f'/home/cyberpanel/{self.data["finalURL"]}-docker-compose.yml'
|
|
with open(TempCompose, 'w') as f:
|
|
f.write(compose_config)
|
|
|
|
command = f"mv {TempCompose} {self.data['ComposePath']}"
|
|
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if result == 0:
|
|
raise DockerDeploymentError(f"Failed to move compose file: {message}")
|
|
|
|
command = f"chmod 600 {self.data['ComposePath']} && chown root:root {self.data['ComposePath']}"
|
|
ProcessUtilities.executioner(command, 'root', True)
|
|
logging.statusWriter(self.JobID, 'Docker compose file created...,40')
|
|
|
|
# Deploy containers
|
|
if ProcessUtilities.decideDistro() == ProcessUtilities.cent8 or ProcessUtilities.decideDistro() == ProcessUtilities.centos:
|
|
dockerCommand = 'docker compose'
|
|
else:
|
|
dockerCommand = 'docker-compose'
|
|
|
|
command = f"{dockerCommand} -f {self.data['ComposePath']} -p '{self.data['SiteName']}' up -d"
|
|
result, message = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
|
if result == 0:
|
|
raise DockerDeploymentError(f"Failed to deploy containers: {message}")
|
|
logging.statusWriter(self.JobID, 'Containers deployed...,60')
|
|
|
|
# Wait for containers to be healthy
|
|
time.sleep(25)
|
|
if not self.check_container_health(f"{self.data['ServiceName']}-db") or \
|
|
not self.check_container_health(self.data['ServiceName']):
|
|
raise DockerDeploymentError("Containers failed to reach healthy state", self.ERROR_CONTAINER_FAILED)
|
|
logging.statusWriter(self.JobID, 'Containers healthy...,70')
|
|
|
|
# Setup proxy
|
|
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
|
|
execPath = execPath + f" SetupProxy --port {self.data['port']}"
|
|
ProcessUtilities.executioner(execPath)
|
|
logging.statusWriter(self.JobID, 'Proxy configured...,80')
|
|
|
|
# Setup n8n vhost configuration instead of htaccess
|
|
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/DockerSites.py"
|
|
execPath = execPath + f" SetupN8NVhost --domain {self.data['finalURL']} --port {self.data['port']}"
|
|
ProcessUtilities.executioner(execPath)
|
|
logging.statusWriter(self.JobID, 'N8N vhost configured...,90')
|
|
|
|
# Restart web server
|
|
from plogical.installUtilities import installUtilities
|
|
installUtilities.reStartLiteSpeedSocket()
|
|
|
|
# Monitor deployment
|
|
metrics = self.monitor_deployment()
|
|
self.log_deployment_metrics(metrics)
|
|
|
|
logging.statusWriter(self.JobID, 'Deployment completed successfully. [200]')
|
|
return True
|
|
|
|
except DockerDeploymentError as e:
|
|
logging.writeToFile(f'Deployment error: {str(e)}')
|
|
|
|
if self.handle_deployment_failure(e):
|
|
current_try += 1
|
|
continue
|
|
else:
|
|
logging.statusWriter(self.JobID, f'Deployment failed: {str(e)} [404]')
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f'Unexpected error: {str(e)}')
|
|
self.handle_deployment_failure(e)
|
|
logging.statusWriter(self.JobID, f'Deployment failed: {str(e)} [404]')
|
|
return False
|
|
|
|
logging.statusWriter(self.JobID, f'Deployment failed after {max_retries} attempts [404]')
|
|
return False
|
|
|
|
def generate_compose_config(self):
|
|
"""
|
|
Generate the docker-compose configuration with improved security and reliability
|
|
"""
|
|
postgres_config = {
|
|
'image': 'postgres:16-alpine',
|
|
'user': 'root',
|
|
'healthcheck': {
|
|
'test': ["CMD-SHELL", "pg_isready -U postgres"],
|
|
'interval': '10s',
|
|
'timeout': '5s',
|
|
'retries': 5,
|
|
'start_period': '30s'
|
|
},
|
|
'environment': {
|
|
'POSTGRES_USER': 'postgres',
|
|
'POSTGRES_PASSWORD': self.data['MySQLPassword'],
|
|
'POSTGRES_DB': self.data['MySQLDBName']
|
|
}
|
|
}
|
|
|
|
n8n_config = {
|
|
'image': 'docker.n8n.io/n8nio/n8n',
|
|
'user': 'root',
|
|
'healthcheck': {
|
|
'test': ["CMD", "wget", "--spider", "http://localhost:5678"],
|
|
'interval': '20s',
|
|
'timeout': '10s',
|
|
'retries': 3
|
|
},
|
|
'environment': {
|
|
'DB_TYPE': 'postgresdb',
|
|
'DB_POSTGRESDB_HOST': f"{self.data['ServiceName']}-db",
|
|
'DB_POSTGRESDB_PORT': '5432',
|
|
'DB_POSTGRESDB_DATABASE': self.data['MySQLDBName'],
|
|
'DB_POSTGRESDB_USER': 'postgres',
|
|
'DB_POSTGRESDB_PASSWORD': self.data['MySQLPassword'],
|
|
'N8N_HOST': f"{self.data['finalURL']}",
|
|
'N8N_PORT': '5678',
|
|
'NODE_ENV': 'production',
|
|
'N8N_EDITOR_BASE_URL': f"https://{self.data['finalURL']}",
|
|
'WEBHOOK_URL': f"https://{self.data['finalURL']}",
|
|
'WEBHOOK_TUNNEL_URL': f"https://{self.data['finalURL']}",
|
|
'N8N_PUSH_BACKEND': 'sse',
|
|
'GENERIC_TIMEZONE': 'UTC',
|
|
'N8N_ENCRYPTION_KEY': 'auto',
|
|
'N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS': 'true',
|
|
'DB_POSTGRESDB_SCHEMA': 'public',
|
|
'N8N_PROTOCOL': 'https',
|
|
'N8N_SECURE_COOKIE': 'true',
|
|
'N8N_PROXY_HOPS': '1',
|
|
'N8N_ALLOWED_ORIGINS': f"https://{self.data['finalURL']}",
|
|
'N8N_ALLOW_CONNECTIONS_FROM': '*'
|
|
}
|
|
}
|
|
|
|
return f'''version: '3.8'
|
|
|
|
volumes:
|
|
db_storage:
|
|
driver: local
|
|
n8n_storage:
|
|
driver: local
|
|
|
|
services:
|
|
'{self.data['ServiceName']}-db':
|
|
image: {postgres_config['image']}
|
|
user: {postgres_config['user']}
|
|
restart: always
|
|
healthcheck:
|
|
test: {postgres_config['healthcheck']['test']}
|
|
interval: {postgres_config['healthcheck']['interval']}
|
|
timeout: {postgres_config['healthcheck']['timeout']}
|
|
retries: {postgres_config['healthcheck']['retries']}
|
|
start_period: {postgres_config['healthcheck']['start_period']}
|
|
environment:
|
|
- POSTGRES_USER={postgres_config['environment']['POSTGRES_USER']}
|
|
- POSTGRES_PASSWORD={postgres_config['environment']['POSTGRES_PASSWORD']}
|
|
- POSTGRES_DB={postgres_config['environment']['POSTGRES_DB']}
|
|
volumes:
|
|
- "/home/docker/{self.data['finalURL']}/db:/var/lib/postgresql/data"
|
|
networks:
|
|
- n8n-network
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '{self.data["CPUsMySQL"]}'
|
|
memory: {self.data["MemoryMySQL"]}M
|
|
|
|
'{self.data['ServiceName']}':
|
|
image: {n8n_config['image']}
|
|
user: {n8n_config['user']}
|
|
restart: always
|
|
healthcheck:
|
|
test: {n8n_config['healthcheck']['test']}
|
|
interval: {n8n_config['healthcheck']['interval']}
|
|
timeout: {n8n_config['healthcheck']['timeout']}
|
|
retries: {n8n_config['healthcheck']['retries']}
|
|
environment:
|
|
- DB_TYPE={n8n_config['environment']['DB_TYPE']}
|
|
- DB_POSTGRESDB_HOST={n8n_config['environment']['DB_POSTGRESDB_HOST']}
|
|
- DB_POSTGRESDB_PORT={n8n_config['environment']['DB_POSTGRESDB_PORT']}
|
|
- DB_POSTGRESDB_DATABASE={n8n_config['environment']['DB_POSTGRESDB_DATABASE']}
|
|
- DB_POSTGRESDB_USER={n8n_config['environment']['DB_POSTGRESDB_USER']}
|
|
- DB_POSTGRESDB_PASSWORD={n8n_config['environment']['DB_POSTGRESDB_PASSWORD']}
|
|
- DB_POSTGRESDB_SCHEMA={n8n_config['environment']['DB_POSTGRESDB_SCHEMA']}
|
|
- N8N_HOST={n8n_config['environment']['N8N_HOST']}
|
|
- N8N_PORT={n8n_config['environment']['N8N_PORT']}
|
|
- NODE_ENV={n8n_config['environment']['NODE_ENV']}
|
|
- N8N_EDITOR_BASE_URL={n8n_config['environment']['N8N_EDITOR_BASE_URL']}
|
|
- WEBHOOK_URL={n8n_config['environment']['WEBHOOK_URL']}
|
|
- WEBHOOK_TUNNEL_URL={n8n_config['environment']['WEBHOOK_TUNNEL_URL']}
|
|
- N8N_PUSH_BACKEND={n8n_config['environment']['N8N_PUSH_BACKEND']}
|
|
- GENERIC_TIMEZONE={n8n_config['environment']['GENERIC_TIMEZONE']}
|
|
- N8N_ENCRYPTION_KEY={n8n_config['environment']['N8N_ENCRYPTION_KEY']}
|
|
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS={n8n_config['environment']['N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS']}
|
|
- N8N_PROTOCOL={n8n_config['environment']['N8N_PROTOCOL']}
|
|
- N8N_SECURE_COOKIE={n8n_config['environment']['N8N_SECURE_COOKIE']}
|
|
ports:
|
|
- "{self.data['port']}:5678"
|
|
depends_on:
|
|
- {self.data['ServiceName']}-db
|
|
volumes:
|
|
- "/home/docker/{self.data['finalURL']}/data:/home/node/.n8n"
|
|
networks:
|
|
- n8n-network
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '{self.data["CPUsSite"]}'
|
|
memory: {self.data["MemorySite"]}M
|
|
|
|
networks:
|
|
n8n-network:
|
|
driver: bridge
|
|
name: {self.data['ServiceName']}_network'''
|
|
|
|
def Main():
|
|
try:
|
|
|
|
parser = argparse.ArgumentParser(description='CyberPanel Docker Sites')
|
|
parser.add_argument('function', help='Specify a function to call!')
|
|
parser.add_argument('--port', help='')
|
|
parser.add_argument('--htaccess', help='')
|
|
parser.add_argument('--externalApp', help='')
|
|
parser.add_argument('--domain', help='')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.function == "SetupProxy":
|
|
Docker_Sites.SetupProxy(args.port)
|
|
elif args.function == 'SetupHTAccess':
|
|
Docker_Sites.SetupHTAccess(args.port, args.htaccess)
|
|
elif args.function == 'SetupN8NVhost':
|
|
Docker_Sites.SetupN8NVhost(args.domain, args.port)
|
|
elif args.function == 'DeployWPDocker':
|
|
# Takes
|
|
# ComposePath, MySQLPath, MySQLRootPass, MySQLDBName, MySQLDBNUser, MySQLPassword, CPUsMySQL, MemoryMySQL,
|
|
# port, SitePath, CPUsSite, MemorySite, SiteName
|
|
# finalURL, blogTitle, adminUser, adminPassword, adminEmail, htaccessPath, externalApp
|
|
data = {
|
|
"JobID": '/home/cyberpanel/hey.txt',
|
|
"ComposePath": "/home/docker.cyberpanel.net/docker-compose.yml",
|
|
"MySQLPath": '/home/docker.cyberpanel.net/public_html/sqldocker',
|
|
"MySQLRootPass": 'testdbwp12345',
|
|
"MySQLDBName": 'testdbwp',
|
|
"MySQLDBNUser": 'testdbwp',
|
|
"MySQLPassword": 'testdbwp12345',
|
|
"CPUsMySQL": '2',
|
|
"MemoryMySQL": '512',
|
|
"port": '8000',
|
|
"SitePath": '/home/docker.cyberpanel.net/public_html/wpdocker',
|
|
"CPUsSite": '2',
|
|
"MemorySite": '512',
|
|
"SiteName": 'wp docker test',
|
|
"finalURL": 'docker.cyberpanel.net',
|
|
"blogTitle": 'docker site',
|
|
"adminUser": 'testdbwp',
|
|
"adminPassword": 'testdbwp',
|
|
"adminEmail": 'usman@cyberpersons.com',
|
|
"htaccessPath": '/home/docker.cyberpanel.net/public_html/.htaccess',
|
|
"externalApp": 'docke8463',
|
|
"docRoot": "/home/docker.cyberpanel.net"
|
|
}
|
|
ds = Docker_Sites('', data)
|
|
ds.DeployN8NContainer()
|
|
|
|
elif args.function == 'DeleteDockerApp':
|
|
data = {
|
|
"domain": args.domain}
|
|
ds = Docker_Sites('', data)
|
|
ds.DeleteDockerApp()
|
|
|
|
|
|
except BaseException as msg:
|
|
print(str(msg))
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
Main()
|