Use native Alpine package manager instead of custom one

This commit is contained in:
Disassembler 2019-02-14 21:10:08 +01:00
parent 6b306390b3
commit 49b0296967
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
16 changed files with 128 additions and 249 deletions

View File

@ -8,11 +8,5 @@
"adminpwd": "${ADMINPWD}", "adminpwd": "${ADMINPWD}",
"domain": "spotter.vm", "domain": "spotter.vm",
"port": "443" "port": "443"
},
"packages": {},
"repo": {
"pwd": "",
"url": "https://dl.dasm.cz/spotter-repo",
"user": ""
} }
} }

View File

@ -1,5 +0,0 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWJXH4Qm0kt2L86sntQH+C1zOJNQ0qMRt
0vx4krTxRs9HQTQYAy//JC92ea2aKleA8OL0JF90b1NYXcQCWdAS+vE/ng9IEAii
8C2+5nfuFeZ5YUjbQhfFblwHSM0c7hEG
-----END PUBLIC KEY-----

View File

@ -31,6 +31,7 @@ parser_unregister_container.set_defaults(action='unregister-container')
parser_register_proxy = subparsers.add_parser('register-proxy') parser_register_proxy = subparsers.add_parser('register-proxy')
parser_register_proxy.set_defaults(action='register-proxy') parser_register_proxy.set_defaults(action='register-proxy')
parser_register_proxy.add_argument('app', help='Application name') parser_register_proxy.add_argument('app', help='Application name')
parser_register_proxy.add_argument('host', help='Application subdomain')
parser_unregister_proxy = subparsers.add_parser('unregister-proxy') parser_unregister_proxy = subparsers.add_parser('unregister-proxy')
parser_unregister_proxy.set_defaults(action='unregister-proxy') parser_unregister_proxy.set_defaults(action='unregister-proxy')
@ -57,7 +58,7 @@ elif args.action == 'unregister-container':
lxcmgr.unregister_container() lxcmgr.unregister_container()
elif args.action == 'register-proxy': elif args.action == 'register-proxy':
# Used in init scripts # Used in init scripts
lxcmgr.register_proxy(args.app) lxcmgr.register_proxy(args.app, args.host)
elif args.action == 'unregister-proxy': elif args.action == 'unregister-proxy':
# Used in init scripts # Used in init scripts
lxcmgr.unregister_proxy(args.app) lxcmgr.unregister_proxy(args.app)

View File

@ -3,7 +3,6 @@
from .appmgr import AppMgr from .appmgr import AppMgr
from .config import Config from .config import Config
from .lxcmgr import LXCMgr from .lxcmgr import LXCMgr
from .pkgmgr import PkgMgr
from .vmmgr import VMMgr from .vmmgr import VMMgr
from .wsgiapp import WSGIApp from .wsgiapp import WSGIApp
@ -11,7 +10,6 @@ __all__ = [
'AppMgr', 'AppMgr',
'Config', 'Config',
'LXCMgr', 'LXCMgr',
'PkgMgr',
'VMMgr', 'VMMgr',
'WSGIApp' 'WSGIApp'
] ]

View File

@ -1,16 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json
import os import os
import requests
import subprocess import subprocess
import time
from .pkgmgr import InstallItem, PkgMgr class InstallItem:
def __init__(self):
self.bytes_total = 1
self.bytes_downloaded = 0
LXC_ROOT = '/var/lib/lxc' @property
def percent_downloaded(self):
return round(self.bytes_downloaded / self.bytes_total * 100)
class AppMgr: class AppMgr:
def __init__(self, conf): def __init__(self, conf):
self.conf = conf self.conf = conf
self.pkgmgr = PkgMgr(conf) self.online_packages = {}
def start_app(self, item): def start_app(self, item):
# Start the actual app service # Start the actual app service
@ -55,19 +63,43 @@ class AppMgr:
return os.path.exists(os.path.join('/etc/runlevels/default', app)) return os.path.exists(os.path.join('/etc/runlevels/default', app))
def install_app(self, item): def install_app(self, item):
# Main installation function. Wrapper for download, registration and install script # Main installation function. Wrapper for installation via native package manager
item.data = InstallItem() item.data = InstallItem()
self.pkgmgr.install_app(item.key, item.data) # Alpine apk provides machine-readable progress in bytes_downloaded/bytes_total format output to file descriptor of choice
pipe_rfd, pipe_wfd = os.pipe()
with subprocess.Popen(['apk', '--progress-fd', str(pipe_wfd), '--no-cache', 'add', 'vm-{}@vm'.format(item.key)], pass_fds=[pipe_wfd]) as p:
while p.poll() == None:
time.sleep(0.1)
data = b''
while True:
chunk = os.read(pipe_rfd, 8192)
data += chunk
if len(chunk) < 8192:
break
progress = data.decode().splitlines()[-1].split('/')
item.data.bytes_downloaded = progress[0]
item.data.bytes_total = progress[1]
os.close(pipe_rfd)
os.close(pipe_wfd)
def uninstall_app(self, item): def uninstall_app(self, item):
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration # Main uninstallation function. Wrapper for uninstallation via native package manager
app = item.key app = item.key
self.stop_app(item) self.stop_app(item)
if self.is_service_autostarted(app): if self.is_service_autostarted(app):
self.update_app_autostart(app, False) self.update_app_autostart(app, False)
if name in self.conf['apps']: if name in self.conf['apps']:
del self.conf['apps'][name] del self.conf['apps'][name]
self.pkgmgr.uninstall_app(app) subprocess.run(['apk', '--no-cache', 'del', 'vm-{}@vm'.format(app)])
def fetch_online_packages(self):
# Fetches list of online packages
repo = vmmgr.get_repo_settings()
packages = requests.get('{}/packages'.format(repo['url']), auth=(repo['user'], repo['pwd']), timeout=5)
if packages.status_code != 200:
return packages.status_code
self.online_packages = json.loads(packages.content)
return 200
def get_services_deps(self): def get_services_deps(self):
# Fisrt, build a dictionary of {app: [needs]} # Fisrt, build a dictionary of {app: [needs]}
@ -96,13 +128,6 @@ class AppMgr:
self.conf['common']['gmaps-api-key'] = gmaps_api_key self.conf['common']['gmaps-api-key'] = gmaps_api_key
self.conf.save() self.conf.save()
def update_repo_settings(self, url, user, pwd):
# Update lxc repository configuration
self.conf['repo']['url'] = url
self.conf['repo']['user'] = user
self.conf['repo']['pwd'] = pwd
self.conf.save()
def shutdown_vm(self): def shutdown_vm(self):
subprocess.run(['/sbin/poweroff']) subprocess.run(['/sbin/poweroff'])

View File

@ -3,8 +3,7 @@
import fcntl import fcntl
import json import json
CONF_FILE = '/etc/vmmgr/config.json' from .paths import CONF_FILE, CONF_LOCK
LOCK_FILE = '/var/lock/vmmgr-config.lock'
class Config: class Config:
def __init__(self): def __init__(self):
@ -12,14 +11,14 @@ class Config:
def load(self): def load(self):
# Load configuration from file. Uses file lock as interprocess mutex # Load configuration from file. Uses file lock as interprocess mutex
with open(LOCK_FILE, 'w') as lock: with open(CONF_LOCK, 'w') as lock:
fcntl.lockf(lock, fcntl.LOCK_EX) fcntl.lockf(lock, fcntl.LOCK_EX)
with open(CONF_FILE, 'r') as f: with open(CONF_FILE, 'r') as f:
self.data = json.load(f) self.data = json.load(f)
def save(self): def save(self):
# Save configuration to a file. Uses file lock as interprocess mutex # Save configuration to a file. Uses file lock as interprocess mutex
with open(LOCK_FILE, 'w') as lock: with open(CONF_LOCK, 'w') as lock:
fcntl.lockf(lock, fcntl.LOCK_EX) fcntl.lockf(lock, fcntl.LOCK_EX)
with open(CONF_FILE, 'w') as f: with open(CONF_FILE, 'w') as f:
json.dump(self.data, f, sort_keys=True, indent=4) json.dump(self.data, f, sort_keys=True, indent=4)

View File

@ -10,10 +10,9 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
CERT_PUB_FILE = '/etc/ssl/services.pem' from .paths import CERT_PUB_FILE, CERT_KEY_FILE, ACME_CRON
CERT_KEY_FILE = '/etc/ssl/services.key'
SIG_PUB_FILE = '/etc/vmmgr/packages.pub' # TODO: Use old method without cryptography module?
ACME_CRON = '/etc/periodic/daily/acme-sh'
def create_cert(domain): def create_cert(domain):
# Create selfsigned certificate with wildcard alternative subject name # Create selfsigned certificate with wildcard alternative subject name
@ -58,11 +57,6 @@ def get_cert_info():
data['method'] = 'selfsigned' data['method'] = 'selfsigned'
return data return data
def verify_signature(file, signature):
with open(SIG_PUB_FILE, 'rb') as f:
pub_key = serialization.load_pem_public_key(f.read(), default_backend())
pub_key.verify(signature, file, ec.ECDSA(hashes.SHA512()))
def adminpwd_hash(password): def adminpwd_hash(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

View File

@ -6,8 +6,7 @@ import shutil
import subprocess import subprocess
from . import templates from . import templates
from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_ROOT, NGINX_DIR
NGINX_DIR = '/etc/nginx/conf.d'
class LXCMgr: class LXCMgr:
def __init__(self, conf): def __init__(self, conf):
@ -25,7 +24,7 @@ class LXCMgr:
def clean_ephemeral_layer(self, app): def clean_ephemeral_layer(self, app):
# Cleans containers ephemeral layer. # Cleans containers ephemeral layer.
# This is done early in the container start process, so the inode of the delta0 directory must remain unchanged # This is done early in the container start process, so the inode of the delta0 directory must remain unchanged
layer = os.path.join('/var/lib/lxc', app, 'delta0') layer = os.path.join(LXC_ROOT, app, 'delta0')
if os.path.exists(layer): if os.path.exists(layer):
for item in os.scandir(layer): for item in os.scandir(layer):
shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path) shutil.rmtree(item.path) if item.is_dir() else os.unlink(item.path)
@ -53,10 +52,10 @@ class LXCMgr:
# Leases the first unused IP from range 172.17.0.0/16 # Leases the first unused IP from range 172.17.0.0/16
# Uses file lock as interprocess mutex # Uses file lock as interprocess mutex
ip = None ip = None
with open('/var/lock/vmmgr-hosts.lock', 'w') as lock: with open(HOSTS_LOCK, 'w') as lock:
fcntl.lockf(lock, fcntl.LOCK_EX) fcntl.lockf(lock, fcntl.LOCK_EX)
# Load all existing records # Load all existing records
with open('/etc/hosts', 'r') as f: with open(HOSTS_FILE, 'r') as f:
leases = [l.strip().split(' ', 1) for l in f] leases = [l.strip().split(' ', 1) for l in f]
# If this call is a request for lease, find the first unassigned IP # If this call is a request for lease, find the first unassigned IP
if is_request: if is_request:
@ -70,7 +69,7 @@ class LXCMgr:
else: else:
leases = [l for l in leases if l[1] != app] leases = [l for l in leases if l[1] != app]
# Write the contents back to the file # Write the contents back to the file
with open('/etc/hosts', 'w') as f: with open(HOSTS_FILE, 'w') as f:
for lease in leases: for lease in leases:
f.write('{} {}\n'.format(lease[0], lease[1])) f.write('{} {}\n'.format(lease[0], lease[1]))
return ip return ip
@ -86,10 +85,10 @@ class LXCMgr:
setup_env['GMAPS_API_KEY'] = self.conf['common']['gmaps-api-key'] setup_env['GMAPS_API_KEY'] = self.conf['common']['gmaps-api-key']
subprocess.run([script], env=setup_env, check=True) subprocess.run([script], env=setup_env, check=True)
def register_proxy(self, app): def register_proxy(self, app, host):
# Setup proxy configuration and reload nginx # Setup proxy configuration and reload nginx
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f: with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
f.write(templates.NGINX.format(app=app, host=self.conf['packages'][app]['host'], domain=self.conf['host']['domain'], port=self.conf['host']['port'])) f.write(templates.NGINX.format(app=app, host=host, domain=self.conf['host']['domain'], port=self.conf['host']['port']))
self.reload_nginx() self.reload_nginx()
def unregister_proxy(self, app): def unregister_proxy(self, app):

View File

@ -6,6 +6,8 @@ import requests
import socket import socket
import subprocess import subprocess
from .paths import MYIP_URL, PING_URL
def compile_url(domain, port, proto='https'): def compile_url(domain, port, proto='https'):
port = '' if (proto == 'https' and port == '443') or (proto == 'http' and port == '80') else ':{}'.format(port) port = '' if (proto == 'https' and port == '443') or (proto == 'http' and port == '80') else ':{}'.format(port)
return '{}://{}{}'.format(proto, domain, port) return '{}://{}{}'.format(proto, domain, port)
@ -28,7 +30,7 @@ def get_external_ip(version):
allowed_gai_family = requests.packages.urllib3.util.connection.allowed_gai_family allowed_gai_family = requests.packages.urllib3.util.connection.allowed_gai_family
try: try:
requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family
return requests.get('https://tools.dasm.cz/myip.php', timeout=5).text return requests.get(MYIP_URL, timeout=5).text
except: except:
return None return None
finally: finally:
@ -50,7 +52,7 @@ def resolve_ip(domain, qtype):
def ping_url(url): def ping_url(url):
try: try:
return requests.get('https://tools.dasm.cz/vm-ping.php', params={'url': url}, timeout=5).text == 'vm-pong' return requests.get(PING_URL, params={'url': url}, timeout=5).text == 'vm-pong'
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
raise raise
except: except:

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Config
CONF_FILE = '/etc/vmmgr/config.json'
CONF_LOCK = '/var/lock/vmmgr-config.lock'
# Crypto
ACME_CRON = '/etc/periodic/daily/acme-sh'
ACME_DIR = '/etc/acme.sh.d'
CERT_KEY_FILE = '/etc/ssl/services.key'
CERT_PUB_FILE = '/etc/ssl/services.pem'
# LXC
HOSTS_FILE = '/etc/hosts'
HOSTS_LOCK = '/var/lock/vmmgr-hosts.lock'
LXC_ROOT = '/var/lib/lxc'
# OS
ISSUE_FILE = '/etc/issue'
NGINX_DIR = '/etc/nginx/conf.d'
REPO_FILE = '/etc/apk/repositories'
# URLs
MYUP_URL = 'https://tools.dasm.cz/myip.php'
PING_URL = 'https://tools.dasm.cz/vm-ping.php'

View File

@ -1,180 +0,0 @@
# -*- coding: utf-8 -*-
import hashlib
import json
import os
import requests
import shutil
import subprocess
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from . import crypto
LXC_ROOT = '/var/lib/lxc'
STAGE_DOWNLOAD = 0
STAGE_INSTALL_DEPS = 1
STAGE_INSTALL_APP = 2
class InstallItem:
def __init__(self):
self.stage = STAGE_DOWNLOAD
self.bytes_total = 1
self.bytes_downloaded = 0
@property
def percent_downloaded(self):
# Limit the displayed percentage to 0 - 99
return min(99, round(self.bytes_downloaded / self.bytes_total * 100))
class PkgMgr:
def __init__(self, conf):
self.repo_url = repo_url
self.conf = conf
self.online_packages = {}
def get_repo_resource(self, resource_url, stream=False):
return requests.get('{}/{}'.format(self.repo_url, resource_url), auth=self.repo_auth, timeout=5, stream=stream)
def fetch_online_packages(self):
# Fetches and verifies online packages. Can raise InvalidSignature
packages = self.get_repo_resource('packages')
if packages.status_code != 200:
return packages.status_code
packages = packages.content
packages_sig = self.get_repo_resource('packages.sig').content
crypto.verify_signature(packages, packages_sig)
self.online_packages = json.loads(packages)
return 200
def install_app(self, app, item):
# Main installation function. Wrapper for download, registration and install script
self.fetch_online_packages()
# Clean packages which previously failed to install
self.clean_pending_packages()
# Get all packages on which the app depends and which have not been installed yet
deps = [d for d in self.get_install_deps(app) if d not in self.conf['packages'] or 'pending' in self.conf['packages'][d]]
item.bytes_total = sum(self.online_packages[d]['size'] for d in deps)
for dep in deps:
self.download_package(dep, item)
for dep in deps:
# Set stage to INSTALLING_DEPS or INSTALLING based on which backage in sequence is being installed
item.stage = STAGE_INSTALL_APP if dep == deps[-1] else STAGE_INSTALL_DEPS
# Purge old data before unpacking to clean previous failed installation
self.purge_package(dep)
self.unpack_package(dep)
# Run uninstall script before installation to clean previous failed installation
self.run_uninstall_script(dep)
self.register_package(dep)
self.run_install_script(dep)
def uninstall_app(self, app):
# Main uninstallation function. Wrapper for uninstall script, filesystem purge and unregistration
deps = self.get_install_deps(app, False)[::-1]
for dep in deps:
if dep not in self.get_uninstall_deps():
self.run_uninstall_script(dep)
self.purge_package(dep)
self.unregister_package(dep)
def download_package(self, name, item):
tmp_archive = '/tmp/{}.tar.xz'.format(name)
r = self.get_repo_resource('{}.tar.xz'.format(name), True)
with open(tmp_archive, 'wb') as f:
for chunk in r.iter_content(chunk_size=65536):
if chunk:
item.bytes_downloaded += f.write(chunk)
# Verify hash
if self.online_packages[name]['sha512'] != self.hash_file(tmp_archive):
raise InvalidSignature(name)
def hash_file(self, file_path):
sha512 = hashlib.sha512()
with open(file_path, 'rb') as f:
while True:
data = f.read(65536)
if not data:
break
sha512.update(data)
return sha512.hexdigest()
def unpack_package(self, name):
# Unpack archive
tmp_archive = '/tmp/{}.tar.xz'.format(name)
subprocess.run(['tar', 'xJf', tmp_archive], cwd='/', check=True)
os.unlink(tmp_archive)
def purge_package(self, name):
# Removes package and shared data from filesystem
lxcpath = self.conf['packages'][name]['lxcpath'] if name in self.conf['packages'] else self.online_packages[name]['lxcpath']
lxc_dir = os.path.join(LXC_ROOT, lxcpath)
if os.path.exists(lxc_dir):
shutil.rmtree(lxc_dir)
srv_dir = os.path.join('/srv/', name)
if os.path.exists(srv_dir):
shutil.rmtree(srv_dir)
lxc_log = '/var/log/lxc/{}.log'.format(name)
if os.path.exists(lxc_log):
os.unlink(lxc_log)
def register_package(self, name):
# Registers a package in installed packages
metadata = self.online_packages[name].copy()
del metadata['sha512']
del metadata['size']
metadata['pending'] = True
self.conf['packages'][name] = metadata
self.conf.save()
def unregister_package(self, name):
# Removes a package from installed packages
del self.conf['packages'][name]
self.conf.save()
def clean_pending_packages(self):
# Remove registered packages with pending flag set from previously failed installation
for name in self.conf['packages'].copy():
if 'pending' in self.conf['packages'][name]:
self.unregister_package(name)
self.conf.save()
def run_install_script(self, name):
# Runs install.sh for a package, if the script is present
install_dir = os.path.join('/srv/', name, 'install')
install_script = os.path.join('/srv/', name, 'install.sh')
if os.path.exists(install_script):
subprocess.run(install_script, check=True)
os.unlink(install_script)
if os.path.exists(install_dir):
shutil.rmtree(install_dir)
# Reload config to reflect whatever vmmgr register-app from the install script has written in it
self.conf.load()
del self.conf['packages'][name]['pending']
self.conf.save()
def run_uninstall_script(self, name):
# Runs uninstall.sh for a package, if the script is present
uninstall_script = os.path.join('/srv/', name, 'uninstall.sh')
if os.path.exists(uninstall_script):
subprocess.run(uninstall_script, check=True)
def get_install_deps(self, name, online=True):
# Flatten dependency tree for a package while preserving the dependency order
packages = self.online_packages if online else self.conf['packages']
deps = packages[name]['deps'].copy()
for dep in deps[::-1]:
deps[:0] = [d for d in self.get_install_deps(dep, online)]
deps = list(dict.fromkeys(deps + [name]))
return deps
def get_uninstall_deps(self):
# Create reverse dependency tree for all installed packages
deps = {}
for name in self.conf['packages'].copy():
for d in self.conf['packages'][name]['deps']:
deps.setdefault(d, []).append(name)
return deps

View File

@ -112,3 +112,9 @@ ISSUE = '''
- \x1b[1m{url}\x1b[0m - \x1b[1m{url}\x1b[0m
- \x1b[1m{ip}\x1b[0m\x1b[?1c - \x1b[1m{ip}\x1b[0m\x1b[?1c
''' '''
REPOSITORIES = '''
http://dl-cdn.alpinelinux.org/alpine/v3.9/main
http://dl-cdn.alpinelinux.org/alpine/v3.9/community
@vm {url}
'''

View File

@ -21,10 +21,11 @@ def is_valid_email(email):
parts = email.split('@') parts = email.split('@')
return len(parts) == 2 and bool(box_re.match(parts[0])) and bool(domain_re.match(parts[1])) return len(parts) == 2 and bool(box_re.match(parts[0])) and bool(domain_re.match(parts[1]))
def is_valid_url(url): def is_valid_repo_url(url):
# Check if URL is valid http(s) and doesn't contain extra parts
try: try:
parsed = urlparse(url) parsed = urlparse(url)
return parsed.scheme in ('http', 'https') return parsed.scheme in ('http', 'https') and not parsed.parameters and not parsed.query and not parsed.fragment
except: except:
pass pass
return False return False

View File

@ -3,14 +3,12 @@
import os import os
import shutil import shutil
import subprocess import subprocess
import urllib
from . import crypto from . import crypto
from . import templates from . import templates
from . import net from . import net
from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, NGINX_DIR, REPO_FILE
ISSUE_FILE = '/etc/issue'
NGINX_DIR = '/etc/nginx/conf.d'
ACME_CRON = '/etc/periodic/daily/acme-sh'
class VMMgr: class VMMgr:
def __init__(self, conf): def __init__(self, conf):
@ -59,6 +57,30 @@ class VMMgr:
# Save config to file # Save config to file
self.conf.save() self.conf.save()
def get_repo_settings(self):
# Read, parse and return current @vm repository configuration
with open(REPO_FILE) as f:
url = [l for l in f.read().splitlines() if l.startswith('@vm')][0].split(' ', 2)[1]
url = urllib.parse.urlparse(url)
return {'url': '{}://{}{}'.format(url.scheme, url.netloc, url.path),
'user': url.username,
'pwd': url.password}
def set_repo_settings(self, url, user, pwd):
# Update @vm repository configuration
url = urllib.parse.urlparse(url)
# Create URL with username and password
repo_url = [url.scheme, '://']
if user:
repo_url.append(urllib.quote(user, safe=''))
if pwd:
repo_url.extend((':', urllib.quote(pwd, safe='')))
repo_url.append('@')
repo_url.extend((url.netloc, url.path))
# Update URL in repositories file
with open(REPO_FILE, 'w') as f:
f.write(templates.REPOSITORIES.format(url=''.join(repo_url)))
def create_selfsigned_cert(self): def create_selfsigned_cert(self):
# Disable acme.sh cronjob # Disable acme.sh cronjob
os.chmod(ACME_CRON, 0o640) os.chmod(ACME_CRON, 0o640)
@ -69,16 +91,16 @@ class VMMgr:
def request_acme_cert(self): def request_acme_cert(self):
# Remove all possible conflicting certificates requested in the past # Remove all possible conflicting certificates requested in the past
certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')] certs = [i for i in os.listdir(ACME_DIR) if i not in ('account.conf', 'ca', 'http.header')]
for cert in certs: for cert in certs:
if cert != self.domain: if cert != self.domain:
subprocess.run(['/usr/bin/acme.sh', '--remove', '-d', cert]) subprocess.run(['/usr/bin/acme.sh', '--remove', '-d', cert])
# Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before # Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before
if not os.path.exists(os.path.join('/etc/acme.sh.d', self.domain)): if not os.path.exists(os.path.join(ACME_DIR, self.domain)):
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain] cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
for app in self.conf['apps'].copy(): for app in self.conf['apps'].copy():
cmd += ['-d', '{}.{}'.format(self.conf['packages'][app]['host'], self.domain)] cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
cmd += ['-w', '/etc/acme.sh.d'] cmd += ['-w', ACME_DIR]
# Request the certificate # Request the certificate
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
# Otherwise just try to renew # Otherwise just try to renew

View File

@ -29,8 +29,6 @@ class WSGIApp:
self.vmmgr = VMMgr(self.conf) self.vmmgr = VMMgr(self.conf)
self.appmgr = AppMgr(self.conf) self.appmgr = AppMgr(self.conf)
self.queue = ActionQueue() self.queue = ActionQueue()
# Clean broken and interrupted installations in case of unclean previous shutdown
self.appmgr.clean_pending_packages()
self.jinja_env = Environment(loader=FileSystemLoader('/usr/share/vmmgr/templates'), autoescape=True, lstrip_blocks=True, trim_blocks=True) self.jinja_env = Environment(loader=FileSystemLoader('/usr/share/vmmgr/templates'), autoescape=True, lstrip_blocks=True, trim_blocks=True)
self.jinja_env.globals.update(is_app_visible=self.is_app_visible) self.jinja_env.globals.update(is_app_visible=self.is_app_visible)
self.url_map = Map(( self.url_map = Map((
@ -176,7 +174,7 @@ class WSGIApp:
app_data = {} app_data = {}
for app in actionable_apps: for app in actionable_apps:
installed = app in self.conf['apps'] installed = app in self.conf['apps']
title = self.conf['packages'][app]['title'] if installed else self.appmgr.online_packages[app]['title'] title = self.conf['apps'][app]['title'] if installed else self.appmgr.online_packages[app]['title']
visible = self.conf['apps'][app]['visible'] if installed else False visible = self.conf['apps'][app]['visible'] if installed else False
autostarted = self.appmgr.is_service_autostarted(app) if installed else False autostarted = self.appmgr.is_service_autostarted(app) if installed else False
if app in pending_actions: if app in pending_actions:
@ -248,7 +246,7 @@ class WSGIApp:
def verify_dns_action(self, request): def verify_dns_action(self, request):
# Check if all FQDNs for all applications are resolvable and point to current external IP # Check if all FQDNs for all applications are resolvable and point to current external IP
domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['packages'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']] domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']]
ipv4 = net.get_external_ip(4) ipv4 = net.get_external_ip(4)
ipv6 = net.get_external_ip(6) ipv6 = net.get_external_ip(6)
for domain in domains: for domain in domains:
@ -269,7 +267,7 @@ class WSGIApp:
# Check if all applications are accessible from the internet using 3rd party ping service # Check if all applications are accessible from the internet using 3rd party ping service
proto = kwargs['proto'] proto = kwargs['proto']
port = self.vmmgr.port if proto == 'https' else '80' port = self.vmmgr.port if proto == 'https' else '80'
domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['packages'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']] domains = [self.vmmgr.domain]+['{}.{}'.format(self.conf['apps'][app]['host'], self.vmmgr.domain) for app in self.conf['apps']]
for domain in domains: for domain in domains:
url = net.compile_url(domain, port, proto) url = net.compile_url(domain, port, proto)
try: try:
@ -313,10 +311,10 @@ class WSGIApp:
def update_repo_action(self, request): def update_repo_action(self, request):
# Update repository URL and credentials # Update repository URL and credentials
url = request.form['repourl'] url = request.form['repourl']
if not validator.is_valid_url(url): if not validator.is_valid_repo_url(url):
request.session['msg'] = 'repo:error:{}'.format(request.session.lang.invalid_url(request.form['repourl'])) request.session['msg'] = 'repo:error:{}'.format(request.session.lang.invalid_url(request.form['repourl']))
else: else:
self.appmgr.update_repo_settings(url, request.form['repousername'], request.form['repopassword']) self.vmmgr.update_repo_settings(url, request.form['repousername'], request.form['repopassword'])
request.session['msg'] = 'repo:info:{}'.format(request.session.lang.repo_updated()) request.session['msg'] = 'repo:info:{}'.format(request.session.lang.repo_updated())
return redirect('/setup-apps') return redirect('/setup-apps')

View File

@ -39,7 +39,7 @@
<ul style="column-count:3"> <ul style="column-count:3">
<li>{{ conf['host']['domain'] }}</li> <li>{{ conf['host']['domain'] }}</li>
{% for app in conf['apps']|sort %} {% for app in conf['apps']|sort %}
<li>{{ conf['packages'][app]['host'] }}.{{ conf['host']['domain'] }}</li> <li>{{ conf['apps'][app]['host'] }}.{{ conf['host']['domain'] }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<input type="button" id="verify-dns" value="Ověřit nastavení DNS"> <input type="button" id="verify-dns" value="Ověřit nastavení DNS">