mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-10-26 07:46:35 +01:00
1532 lines
62 KiB
Python
1532 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 set X-Forwarded-For $ip
|
|
RequestHeader set X-Forwarded-Proto https
|
|
RequestHeader set X-Forwarded-Host "{domain}"
|
|
RequestHeader set Origin "{domain}, {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': '0.0.0.0',
|
|
'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'
|
|
}
|
|
}
|
|
|
|
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()
|