#!/usr/bin/python3
#
# Univention Management Console
# This installation wizard guides the installation of UCS@school in the domain
#
# Copyright 2013-2025 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.
import errno
import fcntl
import filecmp
import glob
import json
import os
import os.path
import re
import select
import socket
import subprocess
import tempfile
import threading
import dns.exception
import dns.resolver
import ldap
from six.moves.urllib_request import urlretrieve
from ucsschool.lib.models.computer import SchoolDCSlave
from ucsschool.lib.models.school import School
from univention.admin.uexceptions import noObject
from univention.config_registry.frontend import ucr_update
from univention.lib.i18n import Translation
from univention.lib.package_manager import PackageManager
from univention.lib.umc import Client, ConnectionError, Forbidden, HTTPError
from univention.management.console.base import Base, UMC_Error
from univention.management.console.config import ucr
from univention.management.console.ldap import get_machine_connection
from univention.management.console.log import MODULE
from univention.management.console.modules.decorators import SimpleThread, sanitize, simple_response
from univention.management.console.modules.sanitizers import (
BooleanSanitizer,
ChoicesSanitizer,
StringSanitizer,
)
_ = Translation("ucs-school-umc-installer").translate
os.umask(0o022) # switch back to default umask
RE_FQDN = re.compile(
r"^[a-z]([a-z0-9-]*[a-z0-9])*\.([a-z0-9]([a-z0-9-]*[a-z0-9])*[.])*[a-z0-9]([a-z0-9-]*[a-z0-9])*$"
)
RE_HOSTNAME = re.compile(
r"^[a-z]([a-z0-9-]*[a-z0-9])*(\.([a-z0-9]([a-z0-9-]*[a-z0-9])*[.])*[a-z0-9]([a-z0-9-]*[a-z0-9])*)?$"
) # keep in sync with schoolinstaller.js widgets.master.regExp
RE_HOSTNAME_OR_EMPTY = re.compile(
r"^([a-z]([a-z0-9-]*[a-z0-9])*(\.([a-z0-9]([a-z0-9-]*[a-z0-9])*[.])*"
r"[a-z0-9]([a-z0-9-]*[a-z0-9])*)?)?$"
)
RE_OU = re.compile(r"^[a-zA-Z0-9](([a-zA-Z0-9_]*)([a-zA-Z0-9]$))?$")
RE_OU_OR_EMPTY = re.compile(r"^([a-zA-Z0-9](([a-zA-Z0-9_]*)([a-zA-Z0-9]$))?)?$")
CMD_ENABLE_EXEC = ["/usr/share/univention-updater/enable-apache2-umc", "--no-restart"]
CMD_DISABLE_EXEC = "/usr/share/univention-updater/disable-apache2-umc"
CERTIFICATE_PATH = "/etc/univention/ssl/ucsCA/CAcert.pem"
# class SetupSanitizer(StringSanitizer):
#
# def _sanitize(self, value, name, further_args):
# ucr.load()
# server_role = ucr.get('server/role')
# if value == 'singlemaster':
# if server_role == 'domaincontroller_master' or server_role == 'domaincontroller_backup':
# return 'ucs-school-singleserver'
# self.raise_validation_error(
# _('Single server environment not allowed on server role "%s"') % server_role)
# if value == 'multiserver':
# if server_role == 'domaincontroller_master' or server_role == 'domaincontroller_backup':
# return 'ucs-school-multiserver'
# elif server_role == 'domaincontroller_slave':
# return 'ucs-school-replica'
# self.raise_validation_error(
# _('Multi server environment not allowed on server role "%s"') % server_role)
# self.raise_validation_error(_('Value "%s" not allowed') % value)
[docs]
class HostSanitizer(StringSanitizer):
def _sanitize(self, value, name, further_args):
value = super(HostSanitizer, self)._sanitize(value, name, further_args)
if not value:
return ""
try:
return socket.getfqdn(value)
except socket.gaierror:
# invalid FQDN
self.raise_validation_error(_("The entered FQDN is not a valid value"))
[docs]
class SchoolInstallerError(UMC_Error):
pass
[docs]
def get_master_dns_lookup():
# DNS lookup for the Primary Directory Node entry
try:
query = "_domaincontroller_master._tcp.%s." % ucr.get("domainname")
resolver = dns.resolver.Resolver()
resolver.lifetime = 6.0
result = resolver.query(query, "SRV")
if result:
return result[0].target.canonicalize().split(1)[0].to_text()
except dns.resolver.NXDOMAIN:
MODULE.error("Error to perform a DNS query for service record: %s" % (query,))
except dns.resolver.NoAnswer:
MODULE.error("Got non authoritative answer in DNS query for service record: %s" % (query,))
except dns.resolver.Timeout:
MODULE.error("DNS query for service record of %s timed out." % (query,))
except dns.exception.DNSException as exc:
MODULE.error("DNSException during query for %s: %s" % (query, exc))
return ""
[docs]
def umc(username, password, master, path, options=None, flavor=None):
MODULE.info(
"Executing on %r as %r: %r flavor=%r options=%r" % (master, username, path, flavor, options)
)
client = Client(master, username, password)
return client.umc_command(path, options, flavor)
[docs]
def create_ou_local(ou, display_name):
"""Calls create_ou locally as user root (only on Primary Directory Node)."""
if not display_name:
MODULE.warn("create_ou_local(): display name is undefined - using OU name as display name")
display_name = ou
# call create_ou
cmd = ["/usr/share/ucs-school-import/scripts/create_ou", "--displayName", display_name, ou]
MODULE.info("Executing: %s" % " ".join(cmd))
process = subprocess.Popen( # nosec
cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True
)
stdout, stderr = process.communicate()
# check for errors
if process.returncode != 0:
stdout, stderr = stdout.decode("UTF-8", "ignore"), stderr.decode("UTF-8", "ignore")
raise SchoolInstallerError("Failed to execute create_ou: %s\n%s%s" % (cmd, stderr, stdout))
[docs]
def create_ou_remote(
master, username, password, ou, display_name, educational_slave, administrative_slave=None
):
"""Create a school OU via the UMC interface."""
options = {"name": ou, "display_name": display_name, "dc_name": educational_slave}
if administrative_slave:
options["dc_name_administrative"] = administrative_slave
try:
umc(
username,
password,
master,
"schoolwizards/schools/create",
[{"object": options}],
"schoolwizards/schools",
)
except (ConnectionError, HTTPError) as exc:
raise SchoolInstallerError("Failed creating OU: %s" % (exc,))
[docs]
def system_join(username, password, info_handler, error_handler, step_handler):
# make sure we got the correct server role
server_role = ucr.get("server/role")
assert server_role in (
"domaincontroller_slave",
"domaincontroller_backup",
"domaincontroller_master",
)
# get the number of join scripts
n_joinscripts = len(glob.glob("/usr/lib/univention-install/*.inst"))
steps_per_script = 100.0 / n_joinscripts
# disable UMC/apache restart
MODULE.info("disabling UMC and apache server restart")
subprocess.call(CMD_DISABLE_EXEC) # nosec
try:
with tempfile.NamedTemporaryFile("w+") as password_file:
password_file.write("%s" % password)
password_file.flush()
# regular expressions for output parsing
error_pattern = re.compile(r"^\* Message:\s*(?P<message>.*)\s*$")
joinscript_pattern = re.compile(r"(Configure|Running)\s+(?P<script>.*)\.inst.*$")
info_pattern = re.compile(r"^(?P<message>.*?)\s*:?\s*\x1b.*$")
# call to univention-join
process = None
if server_role == "domaincontroller_slave":
# Replica Directory Node -> complete re-join
MODULE.process("Performing system join...")
process = subprocess.Popen( # nosec
["/usr/sbin/univention-join", "-dcaccount", username, "-dcpwd", password_file.name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True,
)
else:
# Backup Directory Node/Primary Directory Node -> only run join scripts
MODULE.process("Executing join scripts ...")
process = subprocess.Popen( # nosec
[
"/usr/sbin/univention-run-join-scripts",
"-dcaccount",
username,
"-dcpwd",
password_file.name,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True,
)
failed_join_scripts = []
executed_join_scripts = set()
def parse(line):
if not line.strip():
return
MODULE.process(repr(line.strip()).strip("\"'"))
# parse output... first check for errors
m = error_pattern.match(line)
if m:
error_handler(
_(
"Software packages have been installed, however, the system join could not "
"be completed: %s. More details can be found in the log file "
"/var/log/univention/join.log. Please retry the join process via the UMC "
'module "Domain join" after resolving any conflicting issues.'
)
% m.groupdict().get("message")
)
return
# check for currently called join script
m = joinscript_pattern.match(line)
if m:
current_script = m.groupdict().get("script")
info_handler(_("Executing join script %s") % (current_script,))
if current_script not in executed_join_scripts:
executed_join_scripts.add(current_script)
step_handler(steps_per_script)
if "failed" in line:
failed_join_scripts.append(current_script)
return
# check for other information
m = info_pattern.match(line)
if m:
info_handler(m.groupdict().get("message"))
return
# make stdout file descriptor of the process non-blocking
fd = process.stdout.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
unfinished_line = ""
while True:
try:
fd = select.select([process.stdout], [], [])[0][0]
except IndexError:
continue # not ready / no further data
except select.error as exc:
if exc.args[0] == errno.EINTR:
continue
raise
# get the next line
line = fd.read().decode("UTF-8", "replace")
if not line:
break # no more text from stdout
unfinished_line = (
"" if line.endswith("\n") else "%s%s" % (unfinished_line, line.rsplit("\n", 1)[-1])
)
for _line in line.splitlines():
parse(_line)
if unfinished_line:
parse(unfinished_line)
# get all remaining output
stdout, stderr = process.communicate()
stdout, stderr = stdout.decode("UTF-8"), stderr.decode("UTF-8")
if stderr:
# write stderr into the log file
MODULE.warn("stderr from univention-join: %s" % (stderr,))
# check for errors
if process.returncode != 0:
# error case
MODULE.warn("Could not perform system join: %s%s" % (stdout, stderr))
error_handler(
_(
"Software packages have been installed successfully, however, the join process "
"could not be executed. More details can be found in the log file "
"/var/log/univention/join.log. Please retry to join the system via the UMC "
'module "Domain join" after resolving any conflicting issues.'
)
)
elif failed_join_scripts:
MODULE.warn("The following join scripts could not be executed: %s" % failed_join_scripts)
error_handler(
_(
"Software packages have been installed successfully, however, some join scripts"
" could not be executed. More details can be found in the log file "
"/var/log/univention/join.log. Please retry to execute the join scripts via the"
' UMC module "Domain join" after resolving any conflicting issues.'
)
)
finally:
# make sure that UMC servers and apache can be restarted again
MODULE.info("enabling UMC and apache server restart")
subprocess.call(CMD_ENABLE_EXEC, close_fds=True) # nosec
[docs]
class Progress(object):
def __init__(self, max_steps=100):
self.reset(max_steps)
[docs]
def reset(self, max_steps=100):
self.max_steps = max_steps
self.finished = False
self.steps = 0
self.component = _("Initializing")
self.info = ""
self.errors = []
[docs]
def poll(self):
return {
"finished": self.finished,
"steps": 100 * float(self.steps) / self.max_steps,
"component": self.component,
"info": self.info,
"errors": self.errors,
}
[docs]
def finish(self):
self.finished = True
[docs]
def info_handler(self, info):
MODULE.process(info)
self.info = info
[docs]
def error_handler(self, err):
MODULE.warn(err)
self.errors.append(err)
[docs]
def step_handler(self, steps):
self.steps = steps
[docs]
def add_steps(self, steps=1):
self.steps += steps
[docs]
class Instance(Base):
[docs]
def init(self):
self._finishedLock = threading.Lock()
self._errors = []
self.progress_state = Progress()
self._installation_started = False
self.package_manager = PackageManager(
info_handler=self.progress_state.info_handler,
step_handler=self.progress_state.step_handler,
error_handler=self.progress_state.error_handler,
lock=False,
always_noninteractive=True,
)
self.package_manager.set_max_steps(100.0)
self.original_certificate_file = None
[docs]
def error_handling(self, etype, exc, etraceback):
if isinstance(exc, SchoolInstallerError):
# restore the original certificate... this is done at any error before the system join
self.restore_original_certificate()
[docs]
def get_samba_version(self):
"""Returns 3 or 4 for Samba4 or Samba3 installation, respectively, and returns None otherwise."""
if self.package_manager.is_installed("univention-samba4"):
return 4
elif self.package_manager.is_installed("univention-samba"):
return 3
return None
[docs]
def get_school_environment(self):
"""Returns 'singlemaster', 'multiserver', or None"""
if self.package_manager.is_installed("ucs-school-singleserver"):
return "singlemaster"
elif any(
self.package_manager.is_installed(package)
for package in [
"ucs-school-replica",
"ucs-school-nonedu-replica",
"ucs-school-central-replica",
"ucs-school-multiserver",
]
):
return "multiserver"
return None
[docs]
def get_school_version(self):
return ucr.get("appcenter/apps/ucsschool/version")
[docs]
@simple_response
def query(self, **kwargs):
"""Returns a dictionary of initial values for the form."""
ucr.load()
return {
"server_role": ucr.get("server/role"),
"joined": os.path.exists("/var/univention-join/joined"),
"samba": self.get_samba_version(),
"school_environment": self.get_school_environment(),
"guessed_master": get_master_dns_lookup(),
"hostname": ucr.get("hostname"),
"create_demo": ucr.is_true("ucsschool/join/create_demo", True),
}
[docs]
@simple_response
def progress(self):
if not self._installation_started:
if self._installation_started is False:
self.progress_state.finish()
self.progress_state.error_handler(
"Critical: There is no current installation running. Maybe the previous process "
"died?"
)
self._installation_started = False
return self.progress_state.poll()
[docs]
@sanitize(
master=HostSanitizer(
required=True, regex_pattern=RE_HOSTNAME
), # TODO: add regex error message; Bug #42955
username=StringSanitizer(required=True, minimum=1),
password=StringSanitizer(required=True, minimum=1),
school=StringSanitizer(required=True, regex_pattern=RE_OU), # TODO: add regex error message
)
@simple_response
def get_schoolinfo(self, username, password, master, school):
"""Queries the specified Primary Directory Node for information about the specified school"""
return self._umc_master(
username, password, master, "schoolinstaller/get/schoolinfo/master", {"school": school}
)
[docs]
@sanitize(
school=StringSanitizer(required=True, regex_pattern=RE_OU), # TODO: add regex error message
)
@simple_response
def get_schoolinfo_master(self, school):
"""
Fetches LDAP information from Primary Directory Node about specified OU.
This function assumes that the given arguments have already been validated!
"""
school_name = school
try:
lo, po = get_machine_connection(write=True)
school = School.from_dn(School(name=school_name).dn, None, lo)
except noObject:
exists = False
class_share_server = None
home_share_server = None
educational_slaves = []
administrative_slaves = []
except ldap.SERVER_DOWN:
raise # handled via UMC
except ldap.LDAPError as exc:
MODULE.warn("LDAP error during receiving school info: %s" % (exc,))
raise UMC_Error(_("The LDAP connection to the Primary Directory Node failed."))
else:
exists = True
class_share_server = school.class_share_file_server
home_share_server = school.home_share_file_server
educational_slaves = [
SchoolDCSlave.from_dn(dn, None, lo).name for dn in school.educational_servers
]
administrative_slaves = [
SchoolDCSlave.from_dn(dn, None, lo).name for dn in school.administrative_servers
]
return {
"exists": exists,
"school": school_name,
"classShareServer": class_share_server,
"homeShareServer": home_share_server,
"educational_slaves": educational_slaves,
"administrative_slaves": administrative_slaves,
}
def _umc_master(self, username, password, master, uri, data=None):
try:
return umc(username, password, master, uri, data).result
except Forbidden:
raise SchoolInstallerError(
_(
"Make sure ucs-school-umc-installer is installed on the Primary Directory Node and "
"all join scripts are executed."
)
)
except (ConnectionError, HTTPError) as exc:
# TODO: set status, message, result:
raise SchoolInstallerError(
_("Could not connect to the Primary Directory Node %s: %s") % (master, exc)
)
[docs]
@sanitize(
username=StringSanitizer(required=True),
password=StringSanitizer(required=True),
master=HostSanitizer(required=True, regex_pattern=RE_HOSTNAME_OR_EMPTY),
schoolOU=StringSanitizer(required=True, regex_pattern=RE_OU_OR_EMPTY),
setup=ChoicesSanitizer(["multiserver", "singlemaster"]),
server_type=ChoicesSanitizer(["educational", "administrative"]),
nameEduServer=StringSanitizer(
regex_pattern=RE_HOSTNAME_OR_EMPTY
), # javascript wizard page always passes value to backend, even if empty
createDemo=BooleanSanitizer(),
)
def install(self, request):
# get all arguments
username = request.options.get("username")
password = request.options.get("password")
master = request.options.get("master")
school_ou = request.options.get("schoolOU")
educational_slave = request.options.get("nameEduServer")
ou_display_name = request.options.get("OUdisplayname", school_ou) # use OU name as fallback
server_type = request.options.get("server_type")
setup = request.options.get("setup")
server_role = ucr.get("server/role")
joined = os.path.exists("/var/univention-join/joined")
create_demo = request.options.get("createDemo")
if self._installation_started:
raise ValueError("The installation was started twice. This should not have happened.")
if server_role != "domaincontroller_slave":
# use the credentials of the currently authenticated user on a Primary Directory Node/Backup
# Directory Node
self.require_password()
username = request.username
password = request.password
master = "%s.%s" % (ucr.get("hostname"), ucr.get("domainname"))
if server_role == "domaincontroller_backup":
master = ucr.get("ldap/master")
self.original_certificate_file = None
# check for valid school OU
if (
(setup == "singlemaster" and server_role == "domaincontroller_master")
or server_role == "domaincontroller_slave"
) and not RE_OU.match(school_ou):
raise SchoolInstallerError(_("The specified school OU is not valid."))
# check for valid server role
if server_role not in (
"domaincontroller_master",
"domaincontroller_backup",
"domaincontroller_slave",
):
raise SchoolInstallerError(
_(
"Invalid server role! UCS@school can only be installed on the system roles Primary "
"Directory Node, Backup Directory Node, or Replica Directory Node."
)
)
if server_role == "domaincontroller_slave" and not server_type:
raise SchoolInstallerError(_("Server type has to be set for Replica Directory Node"))
if (
server_role == "domaincontroller_slave"
and server_type == "administrative"
and not educational_slave
):
raise SchoolInstallerError(
_(
"The name of an educational server has to be specified if the system shall be "
"configured as administrative server."
)
)
if (
server_role == "domaincontroller_slave"
and server_type == "administrative"
and educational_slave.lower() == ucr.get("hostname").lower()
):
raise SchoolInstallerError(
_(
"The name of the educational server may not be equal to the name of the "
"administrative Replica Directory Node."
)
)
if server_role == "domaincontroller_slave":
# on Replica Directory Nodes, download the certificate from the Primary Directory Node in
# order to be able to build up secure connections
self.original_certificate_file = self.retrieve_root_certificate(master)
if server_role != "domaincontroller_master":
# check for a compatible environment on the Primary Directory Node
masterinfo = self._umc_master(username, password, master, "schoolinstaller/get/metainfo")
school_environment = masterinfo["school_environment"]
master_samba_version = masterinfo["samba"]
if not school_environment:
raise SchoolInstallerError(
_(
"Please install UCS@school on the Primary Directory Node. Cannot "
"proceed installation on this system."
)
)
if master_samba_version == 3:
raise SchoolInstallerError(
_(
"This UCS domain uses Samba 3 which is no longer supported by UCS@school. "
"Please update all domain systems to samba 4 to be able to continue."
)
)
if server_role == "domaincontroller_slave" and school_environment != "multiserver":
raise SchoolInstallerError(
_(
"The Primary Directory Node is not configured for a UCS@school multi server "
"environment. Cannot proceed installation on this system."
)
)
if server_role == "domaincontroller_backup" and school_environment != setup:
raise SchoolInstallerError(
_(
"The UCS@school Primary Directory Node needs to be configured similarly to this "
"Backup Directory Node. Please choose the correct environment type for this "
"system."
)
)
if server_role == "domaincontroller_backup" and not joined:
raise SchoolInstallerError(
_(
"In order to install UCS@school on a Backup Directory Node, the system needs"
" to be joined first."
)
)
if create_demo is not None:
ucr_update(ucr, {"ucsschool/join/create_demo": str(create_demo).lower()})
# everything ok, try to acquire the lock for the package installation
lock_aquired = self.package_manager.lock(raise_on_fail=False)
if not lock_aquired:
MODULE.warn("Could not aquire lock for package manager")
raise SchoolInstallerError(
_(
"Cannot get lock for installation process. Another package manager seems to block "
"the operation."
)
)
# see which packages we need to install
MODULE.process("performing UCS@school installation")
packages_to_install = []
installed_samba_version = self.get_samba_version()
if installed_samba_version == 3:
raise SchoolInstallerError(
_(
"This UCS domain uses Samba 3 which is no longer supported by UCS@school. Please "
"update all domain systems to samba 4 to be able to continue."
)
)
if server_role == "domaincontroller_slave":
# Replica Directory Node
packages_to_install.extend(["univention-samba4", "univention-s4-connector"])
if server_type == "educational":
packages_to_install.append("ucs-school-replica")
else:
packages_to_install.append("ucs-school-nonedu-replica")
else: # Primary Directory Node or Backup Directory Node
if setup == "singlemaster":
if installed_samba_version:
pass # do not install samba a second time
else: # otherwise install samba4
packages_to_install.extend(["univention-samba4", "univention-s4-connector"])
packages_to_install.append("ucs-school-singleserver")
elif setup == "multiserver":
packages_to_install.append("ucs-school-multiserver")
else:
raise SchoolInstallerError(_("Invalid UCS@school configuration."))
MODULE.info("Packages to be installed: %s" % ", ".join(packages_to_install))
# reset the current installation progress
steps = 100 # installation -> 100
if server_role != "domaincontroller_backup" and not (
server_role == "domaincontroller_master" and setup == "multiserver"
):
steps += 10 # create_ou -> 10
if server_role == "domaincontroller_slave":
steps += 10 # move_slave_into_ou -> 10
steps += 100 # system_join -> 100 steps
self._installation_started = True
progress_state = self.progress_state
progress_state.reset(steps)
progress_state.component = _("Installation of UCS@school packages")
self.package_manager.reset_status()
def _thread(_self, packages):
MODULE.process("Start Veyon proxy app installation")
app_info = json.loads(
subprocess.check_output( # nosec
["/usr/bin/univention-app", "info", "--as-json"]
).decode("UTF-8")
)
veyon_installed = any(
(
app_string.split("=")[0] == "ucsschool-veyon-proxy"
for app_string in app_info.get("installed", [])
)
)
if veyon_installed:
MODULE.process("Veyon proxy app already installed. Skip installation.")
elif setup == "multiserver":
MODULE.process(
"Veyon proxy app is not installed by the installer on primary nodes in multiserver "
"mode. Skip installation."
)
else:
MODULE.process(
"The output for the installation of the Veyon proxy app can be found in "
"/var/log/univention/appcenter.log"
)
with tempfile.NamedTemporaryFile("w+") as pw_file:
pw_file.write(request.password)
pw_file.flush()
cmd = [
"univention-app",
"install",
"ucsschool-veyon-proxy",
"--username",
request.username,
"--pwdfile",
pw_file.name,
"--noninteractive",
]
return_code = subprocess.call(cmd, close_fds=True) # nosec
if return_code != 0:
MODULE.warn(
"The Veyon proxy app could not be installed. Please install manually to ensure "
"a working computerroom module."
)
MODULE.process("Starting package installation")
with _self.package_manager.locked(reset_status=True, set_finished=True):
with _self.package_manager.no_umc_restart(exclude_apache=True):
_self.package_manager.progress_state.info("Updating package cache")
proc = subprocess.Popen( # nosec
["/usr/bin/apt-get", "update"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True,
)
stdout, stderr = proc.communicate()
stdout, stderr = stdout.decode("UTF-8"), stderr.decode("UTF-8")
MODULE.info(
"Output of apt-get update:\nSTDOUT:\n%s\n\nSTDERR:\n%s\n" % (stdout, stderr)
)
_self.package_manager.reopen_cache()
_self.package_manager.progress_state.info("Package cache update completed")
if not _self.package_manager.install(*packages):
raise SchoolInstallerError(_("Failed to install packages."))
if server_role != "domaincontroller_backup" and not (
server_role == "domaincontroller_master" and setup == "multiserver"
):
# create the school OU (not on Backup Directory Node and not on Primary Directory Node
# w/multi server environment)
MODULE.info("Starting creation of LDAP school OU structure...")
progress_state.component = _("Creation of LDAP school structure")
progress_state.info = ""
try:
if server_role == "domaincontroller_slave":
_educational_slave = (
ucr.get("hostname") if server_type == "educational" else educational_slave
)
administrative_slave = (
None if server_type == "educational" else ucr.get("hostname")
)
create_ou_remote(
master,
username,
password,
school_ou,
ou_display_name,
_educational_slave,
administrative_slave,
)
elif server_role == "domaincontroller_master":
create_ou_local(school_ou, ou_display_name)
except SchoolInstallerError as exc:
MODULE.error(str(exc))
raise SchoolInstallerError(
_(
"The UCS@school software packages have been installed, however, a school "
"OU could not be created and consequently a re-join of the system has not "
"been performed. Please create a new school OU structure using the UMC "
'module "Add schools" on the Primary Directory Node and perform a domain '
'join on this machine via the UMC module "Domain join".'
)
)
progress_state.add_steps(10)
if server_role == "domaincontroller_slave":
# make sure that the Replica Directory Node is correctly moved below its OU
MODULE.info(
"Trying to move the Replica Directory Node entry in the right OU structure..."
)
result = umc(
username,
password,
master,
"schoolwizards/schools/move_dc",
{"schooldc": ucr.get("hostname"), "schoolou": school_ou},
"schoolwizards/schools",
).result
if not result.get("success"):
MODULE.warn(
"Could not successfully move the Replica Directory Node into its correct OU "
"structure:\n%s" % result.get("message")
)
raise SchoolInstallerError(
_(
"Validating the LDAP school OU structure failed. It seems that the current "
"Replica Directory Node has already been assigned to a different school or "
"that the specified school OU name is already in use."
)
)
# system join on a Replica Directory Node
progress_state.component = _("Domain join")
if server_role == "domaincontroller_slave":
progress_state.info = _("Preparing domain join...")
MODULE.process("Starting system join...")
else: # run join scripts on Backup Directory Node/Primary Directory Node
progress_state.info = _("Executing join scripts...")
MODULE.process("Running join scripts...")
system_join(
username,
password,
info_handler=self.progress_state.info_handler,
step_handler=self.progress_state.add_steps,
error_handler=self.progress_state.error_handler,
)
def _finished(thread, result):
MODULE.info("Finished installation")
progress_state.finish()
progress_state.info = _("finished...")
self._installation_started = None
if isinstance(result, SchoolInstallerError):
MODULE.warn("Error during installation: %s" % (result,))
self.restore_original_certificate()
progress_state.error_handler(str(result))
elif isinstance(result, BaseException):
self.restore_original_certificate()
msg = "Traceback (most recent call last):\n" + "".join(thread.trace)
MODULE.error("Exception during installation: %s" % msg)
progress_state.error_handler(
_("An unexpected error occurred during installation: %s") % result
)
thread = SimpleThread(
"ucsschool-install",
_thread,
_finished,
)
thread.run(self, packages_to_install)
self.finished(request.id, None)
[docs]
def retrieve_root_certificate(self, master):
"""
On a Replica Directory Node, download the root certificate from the specified Primary
Directory Node and install it on the system. In this way it can be ensured that secure
connections can be performed even though the system has not been joined yet.
Returns the renamed original file if it has been renamed. Otherwise None is returned.
"""
if ucr.get("server/role") != "domaincontroller_slave":
# only do this on a Replica Directory Node
return
# make sure the directory exists
if not os.path.exists(os.path.dirname(CERTIFICATE_PATH)):
os.makedirs(os.path.dirname(CERTIFICATE_PATH))
original_certificate_file = None
# download the certificate from the Primary Directory Node
certificate_uri = "http://%s/ucs-root-ca.crt" % (master,)
MODULE.info("Downloading root certificate from: %s" % (master,))
try:
certificate_file, headers = urlretrieve(certificate_uri) # nosec
if not filecmp.cmp(CERTIFICATE_PATH, certificate_file):
# we need to update the certificate file...
# save the original file first and make sure we do not override any existing file
count = 1
original_certificate_file = CERTIFICATE_PATH + ".orig"
while os.path.exists(original_certificate_file):
count += 1
original_certificate_file = CERTIFICATE_PATH + ".orig%s" % count
os.rename(CERTIFICATE_PATH, original_certificate_file)
MODULE.info("Backing up old root certificate as: %s" % (original_certificate_file,))
# place the downloaded certificate at the original position
os.rename(certificate_file, CERTIFICATE_PATH)
os.chmod(CERTIFICATE_PATH, 0o644)
except EnvironmentError as exc:
# print warning and ignore error
MODULE.warn(
"Could not download root certificate [%s], error ignored: %s" % (certificate_uri, exc)
)
self.original_certificate_file = original_certificate_file
self.restore_original_certificate()
return original_certificate_file
[docs]
def restore_original_certificate(self):
# try to restore the original certificate file
if self.original_certificate_file and os.path.exists(self.original_certificate_file):
try:
MODULE.info("Restoring original root certificate.")
os.rename(self.original_certificate_file, CERTIFICATE_PATH)
except EnvironmentError as exc:
MODULE.warn("Could not restore original root certificate: %s" % (exc,))
self.original_certificate_file = None