v8.1.2: Delete letsencrypt.sh script and add LetsEncrypt API endpoints

Remove the letsencrypt.sh script and integrate LetsEncrypt functionality directly into the web application via new API endpoints. This includes creating, updating, retrieving, and deleting LetsEncrypt configurations, improving maintainability and user interaction with the LetsEncrypt feature.
pull/401/head
Aidaho 2024-11-03 10:12:08 +03:00
parent d7f699d376
commit 8ebf934f06
40 changed files with 1221 additions and 309 deletions

View File

@ -12,6 +12,7 @@ from app.views.service.views import (ServiceView, ServiceActionView, ServiceBack
ServiceConfigVersionsView, ServiceConfigList)
from app.views.service.haproxy_section_views import ListenSectionView, UserListSectionView, PeersSectionView, \
GlobalSectionView, DefaultsSectionView
from app.views.service.lets_encrypt_views import LetsEncryptsView, LetsEncryptView
from app.views.ha.views import HAView, HAVIPView, HAVIPsView
from app.views.user.views import UserView, UserGroupView, UserRoles
from app.views.udp.views import UDPListener, UDPListeners, UDPListenerActionView
@ -87,6 +88,8 @@ register_api(GitBackupView, 'backup_git', '/server/backup/git', 'backup_id')
bp.add_url_rule('/server/<server_id>/ip', view_func=ServerIPView.as_view('server_ip_ip'), methods=['GET'])
bp.add_url_rule('/server/<int:server_id>/ip', view_func=ServerIPView.as_view('server_ip'), methods=['GET'])
register_api_for_not_api(CredView, 'cred', '/server/cred', 'cred_id')
register_api(LetsEncryptView, 'le_api', '/service/letsencrypt', 'le_id')
bp.add_url_rule('service/letsencrypts', view_func=LetsEncryptsView.as_view('les_api'), methods=['GET'])
bp.add_url_rule('/server/creds', view_func=CredsView.as_view('creds'), methods=['GET'])
bp.add_url_rule('/server/portscanner/<server_id>', view_func=PortScannerView.as_view('port_scanner_ip'), methods=['GET', 'POST'])
bp.add_url_rule('/server/portscanner/<int:server_id>', view_func=PortScannerView.as_view('port_scanner'), methods=['GET', 'POST'])

View File

@ -135,9 +135,10 @@ def default_values():
print(str(e))
data_source = [
{'name': 'superAdmin', 'description': 'Has the highest level of administrative permissions and controls the actions of all other users'},
{'name': 'admin', 'description': 'Has access everywhere except the Admin area'},
{'name': 'user', 'description': 'Has the same rights as the admin but has no access to the Servers page'},
{'name': 'superAdmin',
'description': 'Has the highest level of administrative permissions and controls the actions of all other users'},
{'name': 'admin', 'description': 'Has admin access to its groups'},
{'name': 'user', 'description': 'Has the same rights as the admin but has no access to the Admin area'},
{'name': 'guest', 'description': 'Read-only access'}
]
@ -680,7 +681,7 @@ def update_db_v_8_1_0_3():
def update_ver():
try:
Version.update(version='8.1.0.1').execute()
Version.update(version='8.1.2').execute()
except Exception:
print('Cannot update version')

View File

@ -5,7 +5,6 @@ from flask import render_template
import app.modules.db.sql as sql
import app.modules.db.add as add_sql
import app.modules.db.server as server_sql
import app.modules.server.ssh as ssh_mod
import app.modules.common.common as common
import app.modules.config.config as config_mod
import app.modules.config.common as config_common
@ -326,45 +325,6 @@ def get_saved_servers(group: str, term: str) -> dict:
return a
def get_le_cert(server_ip: str, lets_domain: str, lets_email: str) -> str:
proxy = sql.get_setting('proxy')
ssl_path = common.return_nice_path(sql.get_setting('cert_path'), is_service=0)
haproxy_dir = sql.get_setting('haproxy_dir')
script = "letsencrypt.sh"
proxy_serv = ''
ssh_settings = ssh_mod.return_ssh_keys_path(server_ip)
full_path = '/var/www/haproxy-wi/app'
os.system(f"cp {full_path}/scripts/{script} {full_path}/{script}")
if proxy is not None and proxy != '' and proxy != 'None':
proxy_serv = proxy
commands = [
f"chmod +x {full_path}/{script} && {full_path}/{script} PROXY={proxy_serv} haproxy_dir={haproxy_dir} DOMAIN={lets_domain} "
f"EMAIL={lets_email} SSH_PORT={ssh_settings['port']} SSL_PATH={ssl_path} HOST={server_ip} USER={ssh_settings['user']} "
f"PASS='{ssh_settings['password']}' KEY={ssh_settings['key']}"
]
output, error = server_mod.subprocess_execute(commands[0])
if error:
roxywi_common.logging('Roxy-WI server', error, roxywi=1)
return error
else:
for line in output:
if any(s in line for s in ("msg", "FAILED")):
try:
line = line.split(':')[1]
line = line.split('"')[1]
return line + "<br>"
except Exception:
return output
else:
os.remove(f'{full_path}/{script}')
return 'success: Certificate has been created'
def get_ssl_cert(server_ip: str, cert_id: int) -> str:
cert_path = sql.get_setting('cert_path')
command = f"openssl x509 -in {cert_path}/{cert_id} -text"

View File

@ -73,9 +73,9 @@ def update_ssh_passphrase(cred_id: int, passphrase: str):
out_error(e)
def get_ssh_by_id_and_group(creds_id: int, group_id: int) -> Cred:
def get_ssh_by_id_and_group(cred_id: int, group_id: int) -> Cred:
try:
return Cred.select().where((Cred.group_id == group_id) & (Cred.id == creds_id)).execute()
return Cred.select().where((Cred.group_id == group_id) & (Cred.id == cred_id)).execute()
except Cred.DoesNotExist:
raise RoxywiResourceNotFound
except Exception as e:

View File

@ -786,6 +786,20 @@ class HaproxySection(BaseModel):
constraints = [SQL('UNIQUE (server_id, type, name)')]
class LetsEncrypt(BaseModel):
id = AutoField
server_id = ForeignKeyField(Server, null=True, on_delete='SET NULL')
domains = CharField()
email = CharField()
api_key = CharField()
api_token = CharField()
type = CharField()
description = CharField()
class Meta:
table_name = 'lets_encrypt'
def create_tables():
conn = connect()
with conn:
@ -796,5 +810,5 @@ def create_tables():
NginxMetrics, SystemInfo, Services, UserName, GitSetting, CheckerSetting, ApacheMetrics, WafNginx, ServiceStatus,
KeepaliveRestart, PD, SmonHistory, SmonAgent, SmonTcpCheck, SmonHttpCheck, SmonPingCheck, SmonDnsCheck, S3Backup,
SmonStatusPage, SmonStatusPageCheck, HaCluster, HaClusterSlave, HaClusterVip, HaClusterVirt, HaClusterService,
HaClusterRouter, MM, UDPBalancer, HaproxySection]
HaClusterRouter, MM, UDPBalancer, HaproxySection, LetsEncrypt]
)

View File

@ -263,12 +263,14 @@ def delete_ha_virt(vip_id: int) -> None:
pass
def check_ha_virt(vip_id: int) -> bool:
def check_ha_virt(vip_id: int) -> int:
try:
HaClusterVirt.get(HaClusterVirt.vip_id == vip_id).virt_id
except Exception:
return False
return True
_ = HaClusterVirt.get(HaClusterVirt.vip_id == vip_id).virt_id
return 1
except HaClusterVirt.DoesNotExist:
return 0
except Exception as e:
out_error(e)
def select_ha_virts(cluster_id: int) -> HaClusterVirt:

54
app/modules/db/le.py Normal file
View File

@ -0,0 +1,54 @@
from app.modules.db.db_model import LetsEncrypt, Server
from app.modules.db.common import out_error
from app.modules.roxywi.exception import RoxywiResourceNotFound
def get_le(le_id: int) -> LetsEncrypt:
try:
return LetsEncrypt.get(LetsEncrypt.id == le_id)
except LetsEncrypt.DoesNotExist:
raise RoxywiResourceNotFound
except Exception as e:
out_error(e)
def get_le_with_group(le_id: int, group_id: int) -> LetsEncrypt:
try:
return LetsEncrypt.select().join(Server).where(
(LetsEncrypt.id == le_id) &
(Server.group_id == group_id)
).get()
except LetsEncrypt.DoesNotExist:
raise RoxywiResourceNotFound
except Exception as e:
out_error(e)
def select_le_with_group(group_id: int) -> LetsEncrypt:
try:
return LetsEncrypt.select().join(Server).where(Server.group_id == group_id).execute()
except Exception as e:
out_error(e)
def insert_le(**kwargs) -> int:
try:
return LetsEncrypt.insert(**kwargs).execute()
except Exception as e:
out_error(e)
def update_le(le_id: int, **kwargs) -> int:
try:
return LetsEncrypt.update(**kwargs).where(LetsEncrypt.id == le_id).execute()
except LetsEncrypt.DoesNotExist:
raise RoxywiResourceNotFound
except Exception as e:
out_error(e)
def delete_le(le_id: int) -> None:
try:
LetsEncrypt.delete().where(LetsEncrypt.id == le_id).execute()
except Exception as e:
out_error(e)

View File

@ -4,9 +4,10 @@ from typing import Optional, Annotated, Union, Literal, Any, Dict, List
from shlex import quote
from pydantic_core import CoreSchema, core_schema
from pydantic import BaseModel, Base64Str, StringConstraints, IPvAnyAddress, GetCoreSchemaHandler, AnyUrl
from pydantic import BaseModel, Base64Str, StringConstraints, IPvAnyAddress, GetCoreSchemaHandler, AnyUrl, root_validator, EmailStr
DomainName = Annotated[str, StringConstraints(pattern=r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$")]
WildcardDomainName = Annotated[str, StringConstraints(pattern=r"^(?:[a-z0-9\*](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$")]
class EscapedString(str):
@ -134,6 +135,7 @@ class ServerRequest(BaseModel):
class GroupQuery(BaseModel):
group_id: Optional[int] = None
recurse: Optional[bool] = False
class GroupRequest(BaseModel):
@ -301,6 +303,52 @@ class SavedServerRequest(BaseModel):
description: Optional[EscapedString] = None
class LetsEncryptRequest(BaseModel):
server_id: int
domains: List[WildcardDomainName]
email: Optional[EmailStr] = None
type: Literal['standalone', 'route53', 'cloudflare', 'digitalocean', 'linode']
api_key: Optional[EscapedString] = None
api_token: EscapedString
description: Optional[EscapedString] = None
@root_validator(pre=True)
@classmethod
def is_email_when_standalone(cls, values):
cert_type = ''
email = ''
if 'type' in values:
cert_type = values['type']
if 'email' in values:
email = values['email']
if cert_type == 'standalone' and email == '':
raise ValueError('Email must be when type is standalone')
return values
@root_validator(pre=True)
@classmethod
def is_api_key_when_route53(cls, values):
cert_type = ''
api_key = ''
if 'type' in values:
cert_type = values['type']
if 'api_key' in values:
api_key = values['api_key']
if cert_type == 'route53' and api_key == '':
raise ValueError('api_key(secret key) must be when type is route53')
return values
class LetsEncryptDeleteRequest(BaseModel):
server_id: int
domains: List[WildcardDomainName]
email: Optional[str] = None
type: Literal['standalone', 'route53', 'cloudflare', 'digitalocean', 'linode']
api_key: Optional[EscapedString] = None
api_token: EscapedString
description: Optional[EscapedString] = None
class HaproxyBinds(BaseModel):
ip: Optional[str] = None
port: Annotated[int, Gt(1), Le(65535)]

View File

@ -12,7 +12,6 @@ import app.modules.db.history as history_sql
import app.modules.db.portscanner as ps_sql
import app.modules.server.ssh as mod_ssh
import app.modules.common.common as common
import app.modules.roxywi.auth as roxywi_auth
import app.modules.roxywi.common as roxywi_common

View File

@ -51,8 +51,8 @@ def return_ssh_keys_path(server_ip: str, cred_id: int = None) -> dict:
ssh_settings.setdefault('passphrase', passphrase)
try:
ssh_port = [str(server[10]) for server in server_sql.select_servers(server=server_ip)]
ssh_settings.setdefault('port', ssh_port[0])
server = server_sql.get_server_by_ip(server_ip)
ssh_settings.setdefault('port', server.port)
except Exception as e:
raise Exception(f'error: Cannot get SSH port: {e}')
@ -242,8 +242,6 @@ def get_creds(group_id: int = None, cred_id: int = None, not_shared: bool = Fals
def _return_correct_ssh_file(cred: CredRequest) -> str:
lib_path = get_config.get_config_var('main', 'lib_path')
group_name = group_sql.get_group_name_by_id(cred.group_id)
# if not cred.key_enabled:
# return ''
if group_name not in cred.name:
return f'{lib_path}/keys/{cred.name}_{group_name}.pem'
else:

View File

@ -193,7 +193,7 @@ def insert_vip(cluster_id: int, cluster: HAClusterVIP, group_id: int) -> int:
raise Exception(f'Cannot get servers: {e}')
try:
vip_id = HaClusterVip.insert(cluster_id=cluster_id, router_id=router_id, vip=vip, return_master=cluster.return_master).execute()
vip_id = HaClusterVip.insert(cluster_id=cluster_id, router_id=router_id, vip=vip, use_src=cluster.use_src, return_master=cluster.return_master).execute()
except Exception as e:
raise Exception(f'error: Cannot save VIP {vip}: {e}')

View File

@ -230,7 +230,7 @@ def generate_service_inv(json_data: ServiceInstall, installed_service: str) -> o
def run_ansible(inv: dict, server_ips: list, ansible_role: str) -> dict:
inventory_path = '/var/www/haproxy-wi/app/scripts/ansible/inventory'
inventory = f'{inventory_path}/{ansible_role}.json'
inventory = f'{inventory_path}/{ansible_role}-{random.randint(0, 35)}.json'
proxy = sql.get_setting('proxy')
proxy_serv = ''
tags = ''
@ -330,8 +330,13 @@ def run_ansible(inv: dict, server_ips: list, ansible_role: str) -> dict:
def run_ansible_locally(inv: dict, ansible_role: str) -> dict:
inventory_path = '/var/www/haproxy-wi/app/scripts/ansible/inventory'
inventory = f'{inventory_path}/{ansible_role}-{random.randint(0, 35)}.json'
# proxy = sql.get_setting('proxy')
# proxy_serv = ''
proxy_serv = ''
proxy = sql.get_setting('proxy')
if proxy is not None and proxy != '' and proxy != 'None':
proxy_serv = proxy
inv['server']['hosts']['localhost']['PROXY'] = proxy_serv
envvars = {
'ANSIBLE_DISPLAY_OK_HOSTS': 'no',

View File

@ -284,15 +284,6 @@ def create_map():
return add_mod.edit_map(map_name, group)
@bp.post('lets')
def lets():
server_ip = common.checkAjaxInput(request.form.get('serv'))
lets_domain = common.checkAjaxInput(request.form.get('lets_domain'))
lets_email = common.checkAjaxInput(request.form.get('lets_email'))
return add_mod.get_le_cert(server_ip, lets_domain, lets_email)
@bp.post('/nginx/upstream')
@get_user_params()
def add_nginx_upstream():

View File

@ -17,7 +17,7 @@ import app.modules.service.common as service_common
import app.modules.roxywi.common as roxywi_common
import app.modules.roxywi.overview as roxy_overview
from app.views.service.views import ServiceActionView, ServiceBackendView, ServiceView
from app.views.service.lets_encrypt_views import LetsEncryptView, LetsEncryptsView
bp.add_url_rule('/<service>/<server_id>/<any(start, stop, reload, restart):action>', view_func=ServiceActionView.as_view('service_action_ip'), methods=['GET'])
bp.add_url_rule('/<service>/<int:server_id>/<any(start, stop, reload, restart):action>', view_func=ServiceActionView.as_view('service_action'), methods=['GET'])
@ -25,6 +25,9 @@ bp.add_url_rule('/<service>/<server_id>/backend', view_func=ServiceBackendView.a
bp.add_url_rule('/<service>/<int:server_id>/backend', view_func=ServiceBackendView.as_view('service_backend'), methods=['GET'])
bp.add_url_rule('/<service>/<server_id>/status', view_func=ServiceView.as_view('service_ip'), methods=['GET'])
bp.add_url_rule('/<service>/<int:server_id>/status', view_func=ServiceView.as_view('service'), methods=['GET'])
bp.add_url_rule('/letsencrypt', view_func=LetsEncryptView.as_view('le_web'), methods=['POST'])
bp.add_url_rule('/letsencrypt/<int:le_id>', view_func=LetsEncryptView.as_view('le_web_id'), methods=['GET', 'PUT', 'DELETE'])
bp.add_url_rule('/letsencrypts', view_func=LetsEncryptsView.as_view('le_webs'), methods=['GET'])
@bp.before_request

View File

@ -14,7 +14,7 @@
cron:
name: "Roxy-WI Backup configs for server {{ SERVER }} {{ item }}"
special_time: "{{ TIME }}"
job: "rsync -arv {{ TYPE }} /var/lib/roxy-wi/configs/{{ item }}/{{ SERVER }}* {{ USER }}@{{ HOST }}:{{ RPATH }}/roxy-wi-configs-backup/configs/{{ item }} -e 'ssh -i {{ KEY }} -o StrictHostKeyChecking=no' --log-file=/var/www/haproxy-wi/log/backup.log"
job: "rsync -arv {{ TYPE }} /var/lib/roxy-wi/configs/{{ item }}/{{ SERVER }}* {{ USER }}@{{ HOST }}:{{ RPATH }}/roxy-wi-configs-backup/configs/{{ item }} -e 'ssh -i {{ KEY }} -o StrictHostKeyChecking=no' --log-file=/var/www/roxy-wi/log/backup.log"
when: not DELJOB
delegate_to: localhost
with_items:

View File

@ -1,56 +1,9 @@
- hosts: "{{ variable_host }}"
---
- name: Obtain Lets Encrypt certificate
hosts: localhost
connection: local
become: yes
become_method: sudo
tasks:
- name: install EPEL Repository
yum:
name: epel-release
state: latest
when: (ansible_facts['os_family'] == "RedHat" or ansible_facts['os_family'] == 'CentOS')
ignore_errors: yes
failed_when: false
no_log: True
environment:
http_proxy: "{{PROXY}}"
https_proxy: "{{PROXY}}"
- name: Install certbot
package:
name: certbot
state: present
environment:
http_proxy: "{{PROXY}}"
https_proxy: "{{PROXY}}"
- name: Kill cerbot standalone
shell: ps ax |grep 'certbot certonly --standalone' |grep -v grep |awk '{print $1}' |xargs kill
ignore_errors: yes
failed_when: false
no_log: True
- name: Get cert
command: certbot certonly --standalone -d "{{DOMAIN}}" --non-interactive --agree-tos --email "{{EMAIL}}" --http-01-port=8888
- name: Combine into pem file
shell: cat /etc/letsencrypt/live/{{DOMAIN}}/fullchain.pem /etc/letsencrypt/live/{{DOMAIN}}/privkey.pem > "{{SSL_PATH}}"/"{{DOMAIN}}".pem
- name: Creates directory
file:
path: "{{haproxy_dir}}/scripts"
state: directory
- name: Copy renew script
template:
src: /var/www/haproxy-wi/app/scripts/ansible/roles/renew_letsencrypt.j2
dest: "{{haproxy_dir}}/scripts/renew_letsencrypt.sh"
mode: '0755'
ignore_errors: yes
failed_when: false
no_log: True
- name: Creates cron jobs
cron:
name: "Let's encrypt renew script"
special_time: "monthly"
job: '{{haproxy_dir}}/scripts/renew_letsencrypt.sh'
gather_facts: yes
roles:
- role: letsencrypt

View File

@ -0,0 +1,13 @@
---
- name: Delete RSYNC job
cron:
name: "Roxy-WI le certificate {{ main_domain }} {{ item.key }}"
special_time: monthly
state: absent
job: "rsync -arv /etc/letsencrypt/live/{{main_domain}}/* {{ item.value.split('@')[0] }}@{{ item.key }}:{{ ssl_path }} -e 'ssh -i {{ item.value.split('@')[1] }} -o StrictHostKeyChecking=no' --log-file=/var/www/roxy-wi/log/letsencrypt.log"
loop: "{{ servers | dict2items }}"
- name: Delete DNS secret file
file:
path: "~/.secrets/certbot/{{ cert_type }}-{{ main_domain }}.ini"
state: absent

View File

@ -0,0 +1,125 @@
---
- name: Kill cerbot standalone
shell: ps ax |grep 'certbot certonly --standalone' |grep -v grep |awk '{print $1}' |xargs kill
ignore_errors: yes
failed_when: false
no_log: True
- name: Creates certbot directory
file:
path: ~/.secrets/certbot/
state: directory
- name: Install Standalone
when: cert_type == "standalone"
block:
- name: install EPEL Repository
yum:
name: epel-release
state: latest
when: (ansible_facts['os_family'] == "RedHat" or ansible_facts['os_family'] == 'CentOS')
ignore_errors: yes
failed_when: false
no_log: True
environment:
http_proxy: "{{PROXY}}"
https_proxy: "{{PROXY}}"
- name: Install certbot
package:
name: certbot
state: present
environment:
http_proxy: "{{PROXY}}"
https_proxy: "{{PROXY}}"
- name: Get cert
command: certbot certonly --standalone "{{domains_command}}" --non-interactive --agree-tos --email "{{email}}" --http-01-port=8888
- name: Combine into pem file
shell: "cat /etc/letsencrypt/live/{{main_domain}}/fullchain.pem /etc/letsencrypt/live/{{main_domain}}/privkey.pem > {{ssl_path}}/{{main_domain}}.pem"
- name: Creates directory
file:
path: "{{haproxy_dir}}/scripts"
state: directory
- name: Copy renew script
template:
src: renew_letsencrypt.j2
dest: "{{haproxy_dir}}/scripts/renew_letsencrypt.sh"
mode: '0755'
ignore_errors: yes
failed_when: false
no_log: True
- name: Creates cron jobs
cron:
name: "Let's encrypt renew script"
special_time: "monthly"
job: '{{haproxy_dir}}/scripts/renew_letsencrypt.sh'
- name: Install DNS cert
when: cert_type != "standalone"
block:
- name: install EPEL Repository
yum:
name: epel-release
state: latest
when: (ansible_facts['os_family'] == "RedHat" or ansible_facts['os_family'] == 'CentOS')
ignore_errors: yes
failed_when: false
no_log: True
environment:
http_proxy: "{{PROXY}}"
https_proxy: "{{PROXY}}"
- name: Install certbot
package:
name: certbot
state: present
environment:
http_proxy: "{{PROXY}}"
https_proxy: "{{PROXY}}"
- name: Install cert bot plugin
pip:
name: "certbot-dns-{{ cert_type }}"
executable: /usr/local/bin/pip3
state: latest
- name: Copy DNS secret file
template:
src: "{{ cert_type }}.j2"
dest: "~/.secrets/certbot/{{ cert_type }}-{{ main_domain }}.ini"
- name: Obtain certificate
shell: "certbot certonly --dns-{{ cert_type }} {{domains_command}} --dns-{{ cert_type }}-credentials ~/.secrets/certbot/{{ cert_type }}-{{ main_domain }}.ini --dns-{{ cert_type }}-propagation-seconds 60"
environment:
AWS_CONFIG_FILE: "~/.secrets/certbot/{{ cert_type }}.ini"
# - name: Obtain certificate
# shell: "touch /etc/letsencrypt/live/{{main_domain}}/fullchain.pem & touch /etc/letsencrypt/live/{{main_domain}}/privkey.pem"
# environment:
# AWS_CONFIG_FILE: "~/.secrets/certbot/{{ cert_type }}.ini"
- name: Combine into pem file
shell: cat /etc/letsencrypt/live/{{main_domain}}/fullchain.pem /etc/letsencrypt/live/{{main_domain}}/privkey.pem > /etc/letsencrypt/live/{{main_domain}}/{{main_domain}}.pem
- name: Copy certificate
shell: "scp -o StrictHostKeyChecking=no -i {{ item.value.split('@')[1] }} /etc/letsencrypt/live/{{main_domain}}/* {{ item.value.split('@')[0] }}@{{ item.key }}:{{ ssl_path }}"
loop: "{{ servers | dict2items }}"
- name: Create certbot certificate renew job
cron:
name: "Roxy-WI certbot certificate renew"
minute: "0"
hour: "0,12"
job: "root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' && sudo certbot renew -q"
- name: Create RSYNC job
cron:
name: "Roxy-WI le certificate {{ main_domain }} {{ item.key }}"
special_time: monthly
job: "rsync -arv /etc/letsencrypt/live/{{main_domain}}/* {{ item.value.split('@')[0] }}@{{ item.key }}:{{ ssl_path }} -e 'ssh -i {{ item.value.split('@')[1] }} -o StrictHostKeyChecking=no' --log-file=/var/www/roxy-wi/log/letsencrypt.log"
loop: "{{ servers | dict2items }}"

View File

@ -0,0 +1 @@
- include_tasks: "{{ action }}.yml"

View File

@ -0,0 +1,2 @@
# Cloudflare API token used by Certbot
dns_cloudflare_api_token = {{ token }}

View File

@ -0,0 +1,2 @@
# DigitalOcean API credentials used by Certbot
dns_digitalocean_token = {{ token }}

View File

@ -0,0 +1,3 @@
# Linode API credentials used by Certbot
dns_linode_key = {{ token }}
dns_linode_version = 4

View File

@ -20,4 +20,4 @@ for i in $(ls -d */ |awk -F"/" '{print $1}'); do
done
# Reload HAProxy
sudo systemctl reload haproxy
sudo systemctl reload haproxy

View File

@ -0,0 +1,3 @@
[default]
aws_access_key_id={{ secret_key }}
aws_secret_access_key={{ token }}

View File

@ -0,0 +1,12 @@
---
- name: Obtain Lets Encrypt certificate
hosts: all
connection: local
become: yes
become_method: sudo
gather_facts: yes
roles:
- role: letsencrypt
environment:
http_proxy: "{{PROXY}}"
https_proxy: "{{PROXY}}"

View File

@ -1,44 +0,0 @@
#!/bin/bash
for ARGUMENT in "$@"
do
KEY=$(echo $ARGUMENT | cut -f1 -d=)
VALUE=$(echo $ARGUMENT | cut -f2 -d=)
case "$KEY" in
PROXY) PROXY=${VALUE} ;;
HOST) HOST=${VALUE} ;;
USER) USER=${VALUE} ;;
PASS) PASS=${VALUE} ;;
KEY) KEY=${VALUE} ;;
SSH_PORT) SSH_PORT=${VALUE} ;;
DOMAIN) DOMAIN=${VALUE} ;;
EMAIL) EMAIL=${VALUE} ;;
SSL_PATH) SSL_PATH=${VALUE} ;;
haproxy_dir) haproxy_dir=${VALUE} ;;
*)
esac
done
export ANSIBLE_HOST_KEY_CHECKING=False
export ANSIBLE_DISPLAY_SKIPPED_HOSTS=False
export ACTION_WARNINGS=False
export LOCALHOST_WARNING=False
export COMMAND_WARNINGS=False
PWD=/var/www/haproxy-wi/app/scripts/ansible/
echo "$HOST ansible_port=$SSH_PORT" > $PWD/$HOST
if [[ $KEY == "" ]]; then
ansible-playbook $PWD/roles/letsencrypt.yml -e "ansible_user=$USER ansible_ssh_pass=$PASS ansible_port=$SSH_PORT variable_host=$HOST PROXY=$PROXY DOMAIN=$DOMAIN EMAIL=$EMAIL haproxy_dir=$haproxy_dir SSL_PATH=$SSL_PATH" -i $PWD/$HOST
else
ansible-playbook $PWD/roles/letsencrypt.yml --key-file $KEY -e "ansible_user=$USER ansible_port=$SSH_PORT variable_host=$HOST PROXY=$PROXY DOMAIN=$DOMAIN EMAIL=$EMAIL haproxy_dir=$haproxy_dir SSL_PATH=$SSL_PATH" -i $PWD/$HOST
fi
if [ $? -gt 0 ]
then
echo "error: Can't create SSL certificate"
exit 1
else
echo "ok"
fi
rm -f $PWD/$HOST

View File

@ -1,6 +1,25 @@
window.onload = function() {
var cur_url = window.location.href.split('/').pop();
let activeTabIdx = $('#tabs').tabs('option','active');
if (cur_url.split('#')[1] === 'ssl') {
if (activeTabIdx === 4) {
getLes();
}
}
}
$( function() {
$("#tabs ul li").click(function () {
let activeTab = $(this).find("a").attr("href");
let activeTabClass = activeTab.replace('#', '');
$('.menu li ul li').each(function () {
activeSubMenu($(this), activeTabClass)
});
if (activeTab === '#ssl') {
getLes();
}
});
$("#listen-mode-select").on('selectmenuchange', function () {
if ($("#listen-mode-select option:selected").val() == "tcp") {
if ($("#listen-mode-select option:selected").val() === "tcp") {
$("#https-listen-span").hide("fast");
$("#https-hide-listen").hide("fast");
$("#compression").checkboxradio("disable");
@ -19,7 +38,7 @@ $( function() {
}
});
$("#frontend-mode-select").on('selectmenuchange', function () {
if ($("#frontend-mode-select option:selected").val() == "tcp") {
if ($("#frontend-mode-select option:selected").val() === "tcp") {
$("#https-frontend-span").hide("fast");
$("#https-hide-frontend").hide("fast");
$("#compression2").checkboxradio("disable");
@ -442,6 +461,7 @@ $( function() {
$(this).children("#add3").css('border-left', '4px solid #5D9CEB');
$(this).children("#add3").css('background-color', 'var(--right-menu-blue-rolor)');
});
getLes();
$("#tabs").tabs("option", "active", 4);
});
$("#add4").on("click", function () {
@ -530,40 +550,6 @@ $( function() {
}
});
});
$('#lets_button').click(function () {
let lets_domain = $('#lets_domain').val();
let lets_email = $('#lets_email').val();
if (lets_email == '' || lets_domain == '') {
toastr.error('Fields cannot be empty');
} else if (validateEmail(lets_email)) {
$("#ajax-ssl").html(wait_mess);
$.ajax({
url: "/add/lets",
data: {
serv: $('#serv_for_lets').val(),
lets_domain: lets_domain,
lets_email: lets_email
},
type: "POST",
success: function (data) {
if (data.indexOf('error:') != '-1' || data.indexOf('ERROR') != '-1' || data.indexOf('FAILED') != '-1') {
toastr.clear();
toastr.error(data);
} else if (data.indexOf('WARNING') != '-1') {
toastr.clear();
toastr.warning(data);
} else {
toastr.clear();
toastr.success(data);
}
$("#ajax-ssl").html('');
}
});
} else {
toastr.clear();
toastr.error('Wrong e-mail format');
}
});
$('[name=add-server-input]').click(function () {
$("[name=add_servers]").append(add_server_var);
changePortCheckFromServerPort();

222
app/static/js/le.js Normal file
View File

@ -0,0 +1,222 @@
let provides = {'standalone': "Stand alone", 'route53': 'Route53', 'linode': 'Linode', 'cloudflare': 'Cloudflare', 'digitalocean': 'Digitalocean'};
$( function() {
let typeSelect = $( "#new-le-type" );
typeSelect.on('selectmenuchange',function() {
if (typeSelect.val() === 'standalone') {
$('.le-standalone').show();
$('.le-dns').hide();
$('.le-aws').hide();
} else if (typeSelect.val() === 'cloudflare' || typeSelect.val() === 'digitalocean' || typeSelect.val() === 'linode') {
$('.le-standalone').hide();
$('.le-dns').show();
$('.le-aws').hide();
} else if (typeSelect.val() === 'route53' ) {
$('.le-standalone').hide();
$('.le-dns').hide();
$('.le-aws').show();
}
});
});
function addLe(dialogId) {
let domain = $('#new-le-domain').val();
let email = $('#new-le-email').val();
let type = $('#new-le-type').val();
let api_key = '';
let api_token = $('#new-le-token').val();
let valid = true;
let allFields = '';
if (type === 'standalone') {
allFields = $([]).add($('#new-le-domain')).add($('#new-le-email'));
allFields.removeClass("ui-state-error");
valid = valid && checkLength($('#new-le-email'), "Email", 1);
}
if (type === 'cloudflare' || type === 'digitalocean' || type === 'linode') {
allFields = $([]).add($('#new-le-domain')).add($('#new-le-token'));
allFields.removeClass("ui-state-error");
valid = valid && checkLength($('#new-le-token'), "Token", 1);
}
if (type === 'route53') {
allFields = $([]).add($('#new-le-domain')).add($('#new-le-access_key_id')).add($('#new-le-secret_access_key'));
allFields.removeClass("ui-state-error");
valid = valid && checkLength($('#new-le-access_key_id'), "Access key ID", 1);
valid = valid && checkLength($('#new-le-secret_access_key'), "Access key", 1);
}
valid = valid && checkLength($('#new-le-domain'), "Domains", 1);
if ($('#new-le-server_id').val() === '------' || $('#new-le-server_id').val() === null) {
toastr.warning('Select server firts')
return false;
}
if (!valid) {
return false;
}
if (type === 'standalone') {
if (!validateEmail(email)) {
toastr.warning('Invalid email format');
return false;
}
}
if (type === 'route53') {
api_key = $('#new-le-access_key_id').val();
api_token = $('#new-le-secret_access_key').val();
}
let domains = [];
if (domain.includes(',')) {
domains = domain.split(',').filter(function (item) {
return item.trim() !== '';
});
} else if (domain.includes(' ')) {
domains = domain.split(' ').filter(function (item) {
return item.trim() !== '';
});
} else {
domains.push(domain);
}
let jsonData = {
'server_id': $('#new-le-server_id').val(),
'domains': domains,
'email': email,
'type': type,
'api_key': api_key,
'api_token': api_token,
'description': $('#new-le-description').val(),
}
$.ajax({
url: '/service/letsencrypt',
method: 'POST',
data: JSON.stringify(jsonData),
contentType: "application/json; charset=utf-8",
success: function (data) {
if (data.status === 'failed') {
toastr.error(data);
} else {
getLe(data['id'], dialogId);
}
},
});
}
function removeLe(leId) {
$("#lets-" + leId).css("background-color", "#f2dede");
$.ajax({
url: '/service/letsencrypt/' + leId,
method: 'DELETE',
contentType: "application/json; charset=utf-8",
statusCode: {
204: function (xhr) {
$("#lets-" + leId).remove();
},
404: function (xhr) {
$("#lets-" + leId).remove();
}
},
success: function (data) {
if (data) {
if (data.status === "failed") {
toastr.error(data);
}
}
},
});
}
function confirmDeleteLe(id) {
$( "#dialog-confirm" ).dialog({
resizable: false,
height: "auto",
width: 400,
modal: true,
title: delete_word + " Let's encrypt?",
buttons: [{
text: delete_word,
click: function () {
$(this).dialog("close");
removeLe(id);
}
},{
text: cancel_word,
click: function () {
$(this).dialog("close");
}
}]
});
}
function openLeDialog() {
$("#le-add-table").dialog({
autoOpen: true,
resizable: false,
height: "auto",
width: 500,
modal: true,
title: $('#translate').attr('data-create') + " Let's encrypt",
show: {
effect: "fade",
duration: 200
},
hide: {
effect: "fade",
duration: 200
},
buttons: [
{
text: $('#translate').attr('data-create'),
click: function () {
addLe($(this));
}
}, {
text: cancel_word,
click: function () {
$(this).dialog("close");
}
}
]
});
}
function getLe(leId, dialogId) {
$.ajax({
url: '/service/letsencrypt/' + leId + "?recurse=True",
contentType: "application/json; charset=utf-8",
success: function (data) {
if (data.status === 'failed') {
toastr.error(data);
} else {
showLe(data);
$.getScript(awesome);
$(dialogId).dialog("close");
}
},
});
}
function getLes() {
$.ajax({
url: '/service/letsencrypts?recurse=True',
contentType: "application/json; charset=utf-8",
success: function (data) {
if (data.status === 'failed') {
toastr.error(data);
} else {
$('#le_table_body').empty();
for (let k in data) {
showLe(data[k]);
}
$.getScript(awesome);
}
},
});
}
function showLe(data) {
let list_domains = '';
for (let d of eval(data['domains'])) {
list_domains += d + ' ';
if (d < data['domains'].length - 1) {
list_domains += ', ';
}
}
let le_tag = elem("tr", {"id":"lets-" + data['id']}, [
elem("td", {"class":"padding10 first-collumn"}, data['server_id']['hostname']),
elem("td", {"style": "width: 10%;"}, provides[data['type']]),
elem("td", {"style": "width: 30%;"}, list_domains),
elem("td", {"style": "width: 38%;"}, data['description']),
elem("td", null, [
elem("a", {"class":"delete","onclick":"confirmDeleteLe("+data['id']+")","title":"Delete","style":"cursor: pointer; width: 5%;"}),
])
])
$('#le_table_body').append(le_tag);
}

View File

@ -353,7 +353,10 @@ function renderServiceChart(data, labels, server, service) {
config.options.plugins.title.text = data[1] + ' ' + additional_title;
let myChart = new Chart(ctx, config);
myChart.update();
stream_chart(myChart, service, server);
charts.push(myChart)
if (service !== 'waf') {
stream_chart(myChart, service, server);
}
}
function getNginxChartData(server) {
$.ajax({

View File

@ -41,40 +41,13 @@ function show_current_page(id) {
$( function() {
$('.menu li ul li').each(function () {
let link = $(this).find('a').attr('href');
let link2 = link.split('#')[1];
let link3 = link.split('/')[2];
let link4 = link.split('/')[3];
// if (cur_url[1] == null) {
// cur_url[1] = 'haproxy';
// }
let full_uri = window.location.pathname
let full_uri1 = window.location.hash
let full_uri2 = cur_url[0] + '/' + cur_url[1] + '/' + cur_url[2]
let full_uri3 = link2 + '/' + link3 + '/' + link4
// console.log(link)
// console.log(window.location.hash)
let params = new URL(document.location.toString()).searchParams;
// console.log(params.get("service"))
// console.log(link)
// console.log(full_uri)
// console.log(full_uri1)
// console.log(full_uri + "/" + full_uri1)
// if (full_uri1 === '/service/haproxy') {
// console.log(full_uri)
// console.log(full_uri1)
// }
if (full_uri === link) {
show_current_page($(this))
// } else if (window.location.pathname.indexOf('add/haproxy#') != '-1' && link.indexOf('add/haproxy#proxy') != '-1') {
// show_current_page($(this))
} else if (link === full_uri + full_uri1) {
show_current_page($(this))
// } else if (link === '/admin#servers' && full_uri1 === '#servers') {
// show_current_page($(this))
// } else if (link === '/admin#ssh' && full_uri1 === '#ssh') {
// show_current_page($(this))
// } else if (link === '/add/haproxy#proxy' && full_uri1 === '#proxy') {
// show_current_page($(this))
} else if (link === '/add/haproxy#ssl' && full_uri1 === '#ssl' && params.get("service") != 'nginx') {
show_current_page($(this))
} else if (link === '/add/haproxy#ssl' && full_uri1 === '#ssl' && params.get("service") === 'nginx') {
@ -1007,15 +980,20 @@ function changePassword() {
effect: "fade",
duration: 200
},
buttons: {
"Change": function () {
changeUserPasswordItOwn($(this));
},
Cancel: function () {
$(this).dialog("close");
$('#missmatchpass').hide();
buttons: [
{
text: $('#translate').attr('data-change'),
click: function () {
changeUserPasswordItOwn($(this));
}
}, {
text: cancel_word,
click: function () {
$(this).dialog("close");
$('#missmatchpass').hide();
}
}
}
]
});
}
function changeUserPasswordItOwn(d) {

View File

@ -19,6 +19,7 @@
<script src="/static/js/add.js"></script>
<script src="/static/js/edit_config.js"></script>
<script src="/static/js/le.js"></script>
<div id="tabs">
<ul>
<li><a href="#create" title="{{lang.words.add|title()}} {{lang.words.proxy}}: {{lang.words.create|title()}} {{lang.words.proxy}} - Roxy-WI">{{lang.words.create|title()}} {{lang.words.proxy}}</a></li>
@ -92,29 +93,19 @@
</td>
</tr>
</table>
<table>
<table id="le_table">
<caption><h3>Let's Encrypt</h3></caption>
<tr class="overviewHead">
<td class="padding10 first-collumn">{{lang.words.server|title()}}</td>
<td>{{lang.words.domain|title()}}</td>
<td>{{lang.words.email|title()}}</td>
<td>{{lang.words.type|title()}}</td>
<td>{{lang.words.domains|title()}}</td>
<td>{{lang.words.desc|title()}}</td>
<td></td>
<td></td>
</tr>
<tr>
<td class="padding10 first-collumn">
{{ select('serv_for_lets', values=g.user_params['servers'], is_servers='true') }}
</td>
<td>
{{ input('lets_domain', placeholder="example.com") }}
</td>
<td>
{{ input('lets_email') }}
</td>
<td>
<button id="lets_button">{{lang.words.w_get|title()}} {{lang.words.w_a}} {{lang.words.cert}}</button>
</td>
</tr>
<tbody id="le_table_body"></tbody>
</table>
<br /><span class="add-button" title="{{lang.words.create|title()}}" onclick="openLeDialog()">+ {{lang.words.create|title()}}</span>
<div id="ajax-ssl"></div>
</div>
<div id="option">
@ -366,4 +357,80 @@ for (var i = 0; i <= serv_ports.length; i++) {
'This can be done easily in HAProxy by adding the keyword backup on the server line. If multiple backup servers are configured, only the first active one is used.">backup</label><input type="checkbox" name="backup" value="1" id="' + uniqId + '">');
}
</script>
<div id="le-add-table" style="display: none;">
<table class="overview" id="group-add-table-overview" title="{{lang.words.add|title()}} {{lang.words.w_a}} {{lang.words.new3}} {{lang.words.group2}}">
{% include 'include/tr_validate_tips.html' %}
<tr>
<td class="padding20 first-collumn">
{{ lang.words.server|title() }}
</td>
<td>
{{ select('new-le-server_id', values=g.user_params['servers'], is_servers='true', by_id=1) }}
</td>
</tr>
<tr>
<td class="padding20 first-collumn">
{{ lang.words.type|title() }}
</td>
<td>
<select id="new-le-type">
<option value="standalone">Stand alone</option>
<option value="route53">Route 53</option>
<option value="cloudflare">CloudFlare</option>
<option value="digitalocean">DigitalOcean</option>
<option value="linode">Linode</option>
</select>
</td>
</tr>
<tr>
<td class="padding20 first-collumn">
{{ lang.words.domains|title() }}
</td>
<td>
{{ input('new-le-domain') }}
<div class="tooltip tooltipTop">{{ lang.add_page.desc.comma_separated }}</div>
</td>
</tr>
<tr class="le-standalone">
<td class="padding20 first-collumn">
{{ lang.words.email|title() }}
</td>
<td>
{{ input('new-le-email') }}
</td>
</tr>
<tr class="le-dns" style="display: none;">
<td class="padding20 first-collumn">
{{ lang.words.token|title() }}
</td>
<td>
{{ input('new-le-token') }}
</td>
</tr>
<tr class="le-aws" style="display: none;">
<td class="padding20 first-collumn">
Access key ID
</td>
<td>
{{ input('new-le-access_key_id') }}
</td>
</tr>
<tr class="le-aws" style="display: none;">
<td class="padding20 first-collumn">
Secret access key
</td>
<td>
{{ input('new-le-secret_access_key') }}
</td>
</tr>
<tr>
<td class="padding20 first-collumn">
{{ lang.words.desc|title() }}
</td>
<td>
{{ input('new-le-description') }}
</td>
</tr>
</table>
</div>
{% endblock %}

View File

@ -4,16 +4,16 @@
<div id="cluster-{{cluster.id}}" class="div-server-hapwi">
<div class="server-name">
<a href="/ha/cluster/{{cluster.id}}" title="{{lang.words.open|title()}} {{lang.words.cluster|replace("'", "")}}">
<span id="cluster-name-{{cluster.id}}">{{cluster.name}}</span>
<span id="cluster-name-{{cluster.id}}">{{cluster.name|replace("'", "")}}</span>
<span id="cluster-desc-{{cluster.id}}">{% if cluster.desc != '' %} ({{cluster.description|replace("'", "")}}) {% endif %}</span>
</a>
<span class="server-action">
{% if g.user_params['role'] <= 3 %}
<a class="plus" onclick="add_vip_ha_cluster('{{cluster.id}}', '{{cluster.name}}')"></a>
<a class="plus" onclick="add_vip_ha_cluster('{{cluster.id}}', '{{cluster.name|replace("'", "")}}')"></a>
<a class="edit" onclick="createHaClusterStep1(true, '{{cluster.id}}')"></a>
<a class="delete" onclick="confirmDeleteCluster('{{cluster.id}}')"></a>
{% endif %}
<a href="{{ url_for('main.service_history', service='cluster', server_ip=cluster.id) }}" title="{{lang.words.view|title()}} {{lang.words.history3}} {{cluster.name}}" class="history" style="margin: 0 5px 0 10px;"></a>
<a href="{{ url_for('main.service_history', service='cluster', server_ip=cluster.id) }}" title="{{lang.words.view|title()}} {{lang.words.history3}} {{cluster.name|replace("'", "")}}" class="history" style="margin: 0 5px 0 10px;"></a>
</span>
</div>
<div class="server-desc">
@ -47,7 +47,7 @@
<span id="cluster-vip">
{%- for vip in vips %}
{% if g.user_params['role'] <= 2 %}
<a style="cursor: pointer;" onclick="add_vip_ha_cluster('{{vip.cluster_id}}', '{{cluster.name}}', '{{vip.id}}', '{{vip.vip}}', 1)" title="{{lang.words.edit|title()}} VIP">{{vip.vip}}</a>
<a style="cursor: pointer;" onclick="add_vip_ha_cluster('{{vip.cluster_id}}', '{{cluster.name|replace("'", "")}}', '{{vip.id}}', '{{vip.vip}}', 1)" title="{{lang.words.edit|title()}} VIP">{{vip.vip}}</a>
{% else %}
{{vip.vip}}
{%- endif -%}

View File

@ -20,7 +20,7 @@
data-installing="{{lang.words.installing|title()}}" data-creating="{{lang.words.creating|title()}}" data-roxywi_timeout="{{lang.ha_page.roxywi_timeout}}"
data-check_apache_log="{{lang.ha_page.check_apache_log}}" data-was_installed="{{lang.ha_page.was_installed}}" data-start_enter="{{lang.ha_page.start_enter}}"
data-apply="{{lang.words.apply|title()}}" data-reconfigure="{{lang.words.reconfigure|title()}}" data-server="{{lang.words.server|title()}}" data-port="{{lang.words.port}}"
data-weight="{{lang.words.weight}}" data-uptime="{{lang.words.uptime}}" data-downtime="{{lang.words.downtime}}" />
data-weight="{{lang.words.weight}}" data-uptime="{{lang.words.uptime}}" data-downtime="{{lang.words.downtime}}" data-create="{{lang.words.create|title()}}" />
{% include 'include/main_head.html' %}
</head>
<body>

View File

@ -404,6 +404,7 @@
"option_temp": "Create, edit and delete options with given parameters. And after use them as autocomplete in the 'Add' sections",
"server_temp": "Create, edit and delete servers. And after use them as autocomplete in the 'Add' sections",
"use_add": "And use it in the 'Add' sections",
"comma_separated": "You can specify several, separated by a comma or a space",
},
"buttons": {
"disable_ssl_check": "Disable SSL check",
@ -870,6 +871,7 @@
"rule": "rule",
"existing": "existing",
"domain": "domain",
"domains": "domains",
"all": "all",
"just": "just",
"without": "without",

View File

@ -404,6 +404,7 @@
"option_temp": "Créer, modifier et supprimer des options avec des paramètres donnés. Et ensuite les utiliser comme autocomplétion dans les sections 'Ajouter'",
"server_temp": "Créer, modifier et supprimer des serveurs. Et par la suite, utilisez-les en tant que liste déroulante dans les sections 'Ajouter'",
"use_add": "Et utilisez-le dans les sections 'Ajouter'.",
"comma_separated": "Vous pouvez en spécifier plusieurs, séparés par une virgule ou un espace",
},
"buttons": {
"disable_ssl_check": "Désactiver la vérification SSL",
@ -870,6 +871,7 @@
"rule": "règle",
"existing": "existant",
"domain": "domaine",
"domains": "domaines",
"all": "tout",
"just": "juste",
"without": "sans",

View File

@ -404,6 +404,7 @@
"option_temp": "Crie, edite e exclua opções com determinados parâmetros. Usá-los como preenchimento automático nas seções 'Adicionar'",
"server_temp": "Crie, edite e exclua servidores com determinados parâmetros. Usá-los como preenchimento automático nas seções 'Adicionar'",
"use_add": "Usa isso as seções 'Adicionar'",
"comma_separated": "Você pode especificar vários, separados por uma vírgula ou um espaço",
},
"buttons": {
"disable_ssl_check": "Ativar a verificação de SSL",
@ -868,8 +869,9 @@
"display": "exibir",
"default_backend": "Default backend",
"rule": "regra",
"existing": "existing",
"domain": "domain",
"existing": "existente",
"domain": "domínio",
"domains": "domínios",
"all": "todos",
"just": "somente",
"without": "sem",

View File

@ -404,6 +404,7 @@
"option_temp": "Создавать, редактировать и удалять опции с заданными параметрами. И после использовать их как автозаполнение в разделах «Добавить прокси»",
"server_temp": "Создание, редактирование и удаление серверов. И после использовать их как автозаполнение в разделах «Добавить прокси»",
"use_add": "И использовать их в разделах «Добавить прокси»",
"comma_separated": "Можно указать несколько, разделенных запятой, либо пробелом",
},
"buttons": {
"disable_ssl_check": "Отключить проверку SSL",
@ -870,6 +871,7 @@
"rule": "правило",
"existing": "существующие",
"domain": "домен",
"domains": "домены",
"all": "все",
"just": "только",
"without": "без",

View File

@ -5,6 +5,7 @@ from flask import render_template, request, jsonify, g
from playhouse.shortcuts import model_to_dict
import app.modules.db.ha_cluster as ha_sql
import app.modules.db.service as service_sql
import app.modules.roxywi.common as roxywi_common
import app.modules.common.common as common
import app.modules.service.ha_cluster as ha_cluster
@ -39,36 +40,87 @@ class HAView(MethodView):
type: 'integer'
responses:
200:
description: Successful operation
description: HA details retrieved successfully
schema:
type: 'object'
type: object
properties:
description:
type: 'string'
type: string
description: Description of the HA
eth:
type: 'string'
type: string
description: Ethernet interface
group_id:
type: 'integer'
haproxy:
type: 'integer'
type: integer
description: Group ID
id:
type: 'integer'
type: integer
description: ID of the HA
name:
type: 'string'
nginx:
type: 'integer'
type: string
description: Name of the listener
pos:
type: 'integer'
type: integer
description: Position
return_master:
type: integer
description: Return master flag
servers:
type: array
items:
type: object
properties:
eth:
type: string
description: Ethernet interface
id:
type: integer
description: Server ID
master:
type: integer
description: Master flag
services:
type: object
properties:
apache:
type: object
properties:
docker:
type: integer
description: Docker flag for Apache
enabled:
type: integer
description: Enabled flag for Apache
haproxy:
type: object
properties:
docker:
type: integer
description: Docker flag for HAProxy
enabled:
type: integer
description: Enabled flag for HAProxy
nginx:
type: object
properties:
docker:
type: integer
description: Docker flag for NGINX
enabled:
type: integer
description: Enabled flag for NGINX
syn_flood:
type: 'integer'
type: integer
description: SYN Flood protection flag
use_src:
type: 'integer'
type: integer
description: Use source flag
vip:
type: 'string'
type: string
description: Virtual IP address
virt_server:
type: 'boolean'
default:
description: Unexpected error
type: integer
description: Virtual server flag
"""
if not cluster_id:
if request.method == 'GET':
@ -88,25 +140,48 @@ class HAView(MethodView):
cluster_services = ha_sql.select_cluster_services(cluster_id)
vip = ha_sql.select_cluster_vip(cluster_id, router_id)
is_virt = ha_sql.check_ha_virt(vip.id)
if vip.use_src:
use_src = 1
else:
use_src = 0
for cluster in clusters:
settings = model_to_dict(cluster)
settings.setdefault('vip', vip.vip)
settings.setdefault('virt_server', is_virt)
settings.setdefault('use_src', vip.use_src)
settings.setdefault('use_src', use_src)
settings.setdefault('return_master', vip.return_master)
settings['servers'] = []
settings['services'] = {
'haproxy': {
'enabled': 0,
'docker': 0
},
'nginx': {
'enabled': 0,
'docker': 0
},
'apache': {
'enabled': 0,
'docker': 0
}
}
for slave in slaves:
server_id = slave[0]
if slave[31]:
settings.setdefault('eth', slave[32])
server_settings = {'id': server_id, 'eth': slave[32], 'master': slave[31]}
settings['servers'].append(server_settings)
for c_s in cluster_services:
is_dockerized = int(service_sql.select_service_setting(server_id, c_s.service_id, 'dockerized'))
if int(c_s.service_id) == 1:
settings.setdefault('haproxy', 1)
elif int(c_s.service_id) == 2:
settings.setdefault('nginx', 1)
elif int(c_s.service_id) == 4:
settings.setdefault('apache', 1)
settings['services']['haproxy'] = {'enabled': 1, 'docker': is_dockerized}
if int(c_s.service_id) == 2:
settings['services']['nginx'] = {'enabled': 1, 'docker': is_dockerized}
if int(c_s.service_id) == 4:
settings['services']['apache'] = {'enabled': 1, 'docker': is_dockerized}
return jsonify(settings)
@ -353,42 +428,74 @@ class HAVIPView(MethodView):
description: 'Can be only "cluster"'
required: true
type: 'string'
- in: 'path'
name: 'cluster_id'
description: 'ID of the HA cluster to retrieve'
- name: cluster_id
in: path
type: integer
required: true
type: 'integer'
- in: 'path'
name: 'vip_id'
description: 'ID of the VIP to retrieve'
description: ID of the cluster
- name: vip_id
in: path
type: integer
required: true
type: 'integer'
description: ID of the VIP
responses:
200:
description: Successful operation
description: HAVIP details retrieved successfully
schema:
type: object
properties:
cluster_id:
type: 'integer'
type: integer
description: ID of the cluster
eth:
type: string
description: Ethernet interface
id:
type: 'integer'
type: integer
description: ID of the HAVIP
return_master:
type: 'integer'
type: integer
description: Return master flag
router_id:
type: 'integer'
type: integer
description: ID of the router
servers:
type: array
items:
type: object
properties:
eth:
type: string
description: Ethernet interface
id:
type: integer
description: Server ID
master:
type: integer
description: Master flag
use_src:
type: 'integer'
type: integer
description: Use source flag
vip:
type: 'string'
default:
description: Unexpected error
type: string
description: Virtual IP address
virt_server:
type: integer
description: Virtual server flag
"""
try:
vip = ha_sql.select_cluster_vip_by_vip_id(cluster_id, vip_id)
slaves = ha_sql.select_cluster_slaves(cluster_id, vip.router_id)
settings = model_to_dict(vip, recurse=False)
is_virt = ha_sql.check_ha_virt(vip.id)
settings.setdefault('virt_server', is_virt)
settings['servers'] = []
for slave in slaves:
server_id = slave[0]
if slave[31]:
settings.setdefault('eth', slave[32])
server_settings = {'id': server_id, 'eth': slave[32], 'master': slave[31]}
settings['servers'].append(server_settings)
return jsonify(settings)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get VIP')

View File

@ -0,0 +1,392 @@
import ast
from typing import Union
from flask.views import MethodView
from flask_pydantic import validate
from flask import jsonify
from flask_jwt_extended import jwt_required
from playhouse.shortcuts import model_to_dict
import app.modules.db.sql as sql
import app.modules.db.le as le_sql
import app.modules.db.server as server_sql
import app.modules.common.common as common
import app.modules.roxywi.common as roxywi_common
import app.modules.service.installation as service_mod
from app.modules.db.db_model import LetsEncrypt
from app.modules.server.ssh import return_ssh_keys_path
from app.middleware import get_user_params, page_for_admin, check_group
from app.modules.roxywi.class_models import LetsEncryptRequest, LetsEncryptDeleteRequest, IdResponse, GroupQuery, BaseResponse
from app.modules.common.common_classes import SupportClass
class LetsEncryptView(MethodView):
methods = ['GET', 'POST', 'PUT', 'DELETE']
decorators = [jwt_required(), get_user_params(), page_for_admin(level=3), check_group()]
@validate(query=GroupQuery)
def get(self, le_id: int, query: GroupQuery):
"""
Get Let's Encrypt details.
---
tags:
- Let's Encrypt
parameters:
- name: le_id
in: path
type: integer
required: true
description: ID of the Let's Encrypt configuration
- name: group_id
in: query
type: integer
required: false
description: ID of the group (only for role superAdmin)
responses:
200:
description: Let's Encrypt details retrieved successfully
schema:
type: object
properties:
api_key:
type: string
description: API key
api_token:
type: string
description: API token
description:
type: string
description: Description of the Let's Encrypt configuration
domains:
type: array
description: List of domains associated with the Let's Encrypt configuration
email:
type: string
description: Email associated with the Let's Encrypt account
id:
type: integer
description: ID of the Let's Encrypt configuration
server_id:
type: integer
description: ID of the server
type:
type: string
description: Type of the Let's Encrypt configuration
enum: ['standalone', 'route53', 'cloudflare', 'digitalocean', 'linode']
"""
group_id = SupportClass.return_group_id(query)
try:
le = le_sql.get_le_with_group(le_id, group_id)
le_dict = model_to_dict(le, recurse=False)
le_dict['domains'] = ast.literal_eval(le_dict['domains'])
return jsonify(le_dict)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get Let\'s Encrypt')
@validate(body=LetsEncryptRequest)
def post(self, body: LetsEncryptRequest):
"""
Create a Let's Encrypt configuration.
---
tags:
- Let's Encrypt
parameters:
- name: group_id
in: query
type: integer
required: false
description: ID of the group (only for role superAdmin)
- name: body
in: body
required: true
schema:
type: object
properties:
api_key:
type: string
description: API key
api_token:
type: string
description: API token
description:
type: string
description: Description of the Let's Encrypt configuration
domains:
type: array
description: List of domains associated with the Let's Encrypt configuration
email:
type: string
description: Email associated with the Let's Encrypt account
id:
type: integer
description: ID of the Let's Encrypt configuration
server_id:
type: integer
description: ID of the server
type:
type: string
description: Type of the Let's Encrypt configuration
enum: ['standalone', 'route53', 'cloudflare', 'digitalocean', 'linode']
responses:
201:
description: Let's Encrypt configuration created successfully
"""
try:
self._create_env(body)
last_id = le_sql.insert_le(**body.model_dump(mode='json'))
return IdResponse(id=last_id).model_dump(), 201
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot create Let\'s Encrypt')
@validate(body=LetsEncryptRequest, query=GroupQuery)
def put(self, le_id: int, body: LetsEncryptRequest, query: GroupQuery):
"""
Update a Let's Encrypt configuration.
---
tags:
- Let's Encrypt
parameters:
- name: le_id
in: path
type: integer
required: true
description: ID of the Let's Encrypt configuration
- name: group_id
in: query
type: integer
required: false
description: ID of the group (only for role superAdmin)
- name: body
in: body
required: true
schema:
type: object
properties:
api_key:
type: string
description: API key
api_token:
type: string
description: API token
description:
type: string
description: Description of the Let's Encrypt configuration
domains:
type: array
description: List of domains associated with the Let's Encrypt configuration
email:
type: string
description: Email associated with the Let's Encrypt account
id:
type: integer
description: ID of the Let's Encrypt configuration
server_id:
type: integer
description: ID of the server
type:
type: string
description: Type of the Let's Encrypt configuration
enum: ['standalone', 'route53', 'cloudflare', 'digitalocean', 'linode']
responses:
201:
description: Let's Encrypt configuration updated successfully
"""
group_id = SupportClass.return_group_id(query)
try:
le = le_sql.get_le_with_group(le_id, group_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot find Let\'s Encrypt')
try:
le_dict = _return_domains_list(le)
data = LetsEncryptRequest(**le_dict)
self._create_env(data, 'delete')
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot update Let\'s Encrypt on server')
try:
le_sql.update_le(le_id, **body.model_dump(mode='json'))
self._create_env(body, 'install')
return IdResponse(id=le_id).model_dump(), 201
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot update Let\'s Encrypt')
@validate(query=GroupQuery)
def delete(self, le_id: int, query: GroupQuery):
"""
Delete Let's Encrypt details.
---
tags:
- Let's Encrypt
parameters:
- name: le_id
in: path
type: integer
required: true
description: ID of the Let's Encrypt configuration
- name: group_id
in: query
type: integer
required: false
description: ID of the group (only for role superAdmin)
responses:
204:
description: Let's Encrypt deleted successfully
"""
group_id = SupportClass.return_group_id(query)
try:
le = le_sql.get_le_with_group(le_id, group_id)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot find Let\'s Encrypt')
try:
le_dict = _return_domains_list(le)
le_dict['emails'] = None
data = LetsEncryptDeleteRequest(**le_dict)
self._create_env(data, action='delete')
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete Let\'s Encrypt from server')
try:
le_sql.delete_le(le_id)
return BaseResponse().model_dump(mode='json'), 204
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot delete Let\'s Encrypt')
def _create_env(self, data: Union[LetsEncryptRequest, LetsEncryptDeleteRequest], action: str = 'install' ):
server_ips = []
server_ip = 'localhost'
domains_command = ''
servers = {}
main_domain = data.domains[0]
inv = {"server": {"hosts": {}}}
masters = server_sql.is_master(server_ip)
ssl_path = common.return_nice_path(sql.get_setting('cert_path'), is_service=0)
if data.type == 'standalone':
server_ip = server_sql.get_server_by_id(data.server_id).ip
ssh_settings = return_ssh_keys_path(server_ip)
servers[server_ip] = f"{ssh_settings['user']}@{ssh_settings['key']}"
ansible_role = 'letsencrypt_standalone'
else:
master_ip = server_sql.get_server_by_id(data.server_id).ip
ssh_settings = return_ssh_keys_path(master_ip)
servers[master_ip] = f"{ssh_settings['user']}@{ssh_settings['key']}"
ansible_role = 'letsencrypt'
for domain in data.domains:
domains_command += f' -d {domain}'
for master in masters:
if master[0] is not None:
ssh_settings = return_ssh_keys_path(master[0])
servers[master[0]] = f"{ssh_settings['user']}@{ssh_settings['key']}"
inv['server']['hosts'][master[0]] = {
'token': data.api_token,
'secret_key': data.api_key,
'email': data.email,
'ssl_path': ssl_path,
'domains_command': domains_command,
'main_domain': main_domain,
'servers': servers,
'action': action,
'cert_type': data.type
}
server_ips.append(master[0])
inv['server']['hosts'][server_ip] = {
'token': data.api_token,
'secret_key': data.api_key,
'email': data.email,
'ssl_path': ssl_path,
'domains_command': domains_command,
'main_domain': main_domain,
'servers': servers,
'action': action,
'cert_type': data.type
}
server_ips.append(server_ip)
if data.type != 'standalone':
try:
output = service_mod.run_ansible_locally(inv, ansible_role)
except Exception as e:
raise e
else:
try:
output = service_mod.run_ansible(inv, server_ips, 'letsencrypt')
except Exception as e:
raise e
if len(output['failures']) > 0 or len(output['dark']) > 0:
raise Exception('Cannot create certificate. Check Apache error log')
class LetsEncryptsView(MethodView):
methods = ['GET']
decorators = [jwt_required(), get_user_params(), page_for_admin(level=3), check_group()]
@validate(query=GroupQuery)
def get(self, query: GroupQuery):
"""
Get all Let's Encrypt configurations.
---
tags:
- Let's Encrypt
parameters:
- name: group_id
in: query
type: integer
required: false
description: ID of the group (only for role superAdmin)
responses:
200:
description: List of Let's Encrypt configurations retrieved successfully
schema:
type: array
items:
type: object
properties:
api_key:
type: string
description: API key
api_token:
type: string
description: API token
description:
type: string
description: Description of the Let's Encrypt configuration
domains:
type: array
items:
type: string
description: Domains associated with the Let's Encrypt configuration
email:
type: string
description: Email associated with the Let's Encrypt account
id:
type: integer
description: ID of the Let's Encrypt configuration
server_id:
type: integer
description: ID of the server
type:
type: string
description: Type of the Let's Encrypt configuration
"""
group_id = SupportClass.return_group_id(query)
le_list = []
try:
les = le_sql.select_le_with_group(group_id)
for le in les:
le_list.append(_return_domains_list(le, query.recurse))
return jsonify(le_list)
except Exception as e:
return roxywi_common.handler_exceptions_for_json_data(e, 'Cannot get Let\'s Encrypts')
def _return_domains_list(le: LetsEncrypt, recurse: bool = False) -> dict:
le_dict = model_to_dict(le, recurse=recurse)
le_dict['domains'] = ast.literal_eval(le_dict['domains'])
return le_dict

View File

@ -21,6 +21,7 @@ Flask-APScheduler==1.13.1
Flask-Caching==2.3.0
Flask-JWT-Extended==4.6.0
Flask-Pydantic==0.12.0
pydantic[email]
flask-swagger==0.2.14
ansible-core==2.16.3
ansible-runner==2.3.1