139 lines
5.1 KiB
Python

# -*- coding: utf-8 -*-
import json
import os
import requests
import subprocess
import time
class InstallItem:
def __init__(self):
self.bytes_total = 1
self.bytes_downloaded = 0
@property
def percent_downloaded(self):
return round(self.bytes_downloaded / self.bytes_total * 100)
class AppMgr:
def __init__(self, conf):
self.conf = conf
self.online_packages = {}
def start_app(self, item):
# Start the actual app service
app = item.key
if app in self.conf['apps'] and not self.is_service_started(app):
self.start_service(app)
def start_service(self, service):
subprocess.run(['/sbin/service', service, 'start'], check=True)
def stop_app(self, item):
# Stop the actual app service
app = item.key
if app in self.conf['apps'] and self.is_service_started(app):
self.stop_service(app)
# Stop the app service's dependencies if they are not used by another running app
deps = self.get_services_deps()
for dep in self.get_service_deps(app):
if not any([self.is_service_started(d) for d in deps[dep]]):
self.stop_service(dep)
def stop_service(self, service):
subprocess.run(['/sbin/service', service, 'stop'], check=True)
def update_app_visibility(self, app, visible):
# Update visibility for the app in the configuration
if app in self.conf['apps']:
self.conf['apps'][app]['visible'] = visible
self.conf.save()
def update_app_autostart(self, app, enabled):
# Add/remove the app to OpenRC default runlevel
if app in self.conf['apps']:
subprocess.run(['/sbin/rc-update', 'add' if enabled else 'del', app])
def is_service_started(self, app):
# Check OpenRC service status without calling any binary
return os.path.exists(os.path.join('/run/openrc/started', app))
def is_service_autostarted(self, app):
# Check OpenRC service enablement
return os.path.exists(os.path.join('/etc/runlevels/default', app))
def install_app(self, item):
# Main installation function. Wrapper for installation via native package manager
item.data = InstallItem()
# 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)
# Read pipe
data = b''
while True:
chunk = os.read(pipe_rfd, 8192)
data += chunk
if len(chunk) < 8192:
break
# Parse last apk progress line
progress = data.decode().splitlines()[-1].split('/')
item.data.bytes_downloaded = progress[0]
item.data.bytes_total = progress[1]
# Close pipe
os.close(pipe_rfd)
os.close(pipe_wfd)
def uninstall_app(self, item):
# Main uninstallation function. Wrapper for uninstallation via native package manager
app = item.key
self.stop_app(item)
if self.is_service_autostarted(app):
self.update_app_autostart(app, False)
if name in self.conf['apps']:
del self.conf['apps'][name]
subprocess.run(['apk', '--no-cache', 'del', 'vm-{}@vm'.format(app)])
def fetch_online_packages(self, repo_conf):
# Fetches list of online packages
auth = (repo_conf['user'], repo_conf['pwd']) if repo_conf['user'] else None
packages = requests.get('{}/packages.json'.format(repo_conf['url']), auth=auth, 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):
# Fisrt, build a dictionary of {app: [needs]}
needs = {}
for app in self.conf['apps'].copy():
needs[app] = self.get_service_deps(app)
# Then reverse it to {need: [apps]}
deps = {}
for app, need in needs.items():
for n in need:
deps.setdefault(n, []).append(app)
return deps
def get_service_deps(self, app):
# Get "need" line from init script and split it to list
try:
with open(os.path.join('/etc/init.d', app), 'r') as f:
return [l for l in f.readlines() if l.strip().startswith('need')][0].split()[1:]
except:
pass
return []
def update_common_settings(self, email, gmaps_api_key):
# Update common configuration values
self.conf['common']['email'] = email
self.conf['common']['gmaps-api-key'] = gmaps_api_key
self.conf.save()
def shutdown_vm(self):
subprocess.run(['/sbin/poweroff'])
def reboot_vm(self):
subprocess.run(['/sbin/reboot'])