#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention App Center
# Application class
#
# Copyright 2015-2022 Univention GmbH
#
# https://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
# <https://www.gnu.org/licenses/>.
#
import os
import os.path
import re
from copy import copy
from distutils.version import LooseVersion
import platform
from inspect import getargspec
from weakref import ref
import six
from six.moves.urllib_parse import urlsplit
from six.moves.configparser import RawConfigParser, NoOptionError, NoSectionError
from univention.appcenter.log import get_base_logger
from univention.appcenter.packages import get_package_manager, packages_are_installed
from univention.appcenter.meta import UniventionMetaClass, UniventionMetaInfo
from univention.appcenter.utils import app_ports, mkdir, get_free_disk_space, get_current_ram_available, get_locale, container_mode, _
from univention.appcenter.ucr import ucr_get, ucr_includes, ucr_is_true, ucr_load, ucr_run_filter
from univention.appcenter.settings import Setting
from univention.appcenter.ini_parser import read_ini_file
from six import with_metaclass, string_types, PY3
CACHE_DIR = '/var/cache/univention-appcenter'
LOCAL_ARCHIVE = '/usr/share/univention-appcenter/archives/all.tar.gz'
LOCAL_ARCHIVE_DIR = '/usr/share/univention-appcenter/archives/'
SHARE_DIR = '/usr/share/univention-appcenter/apps'
DATA_DIR = '/var/lib/univention-appcenter/apps'
CONTAINER_SCRIPTS_PATH = '/usr/share/univention-docker-container-mode/'
app_logger = get_base_logger().getChild('apps')
if six.PY3:
# LooseVersion changed the internal order function that may now raise
# TypeError on LooseVersion("1.0.1") < LooseVersion("1.0-1")
from itertools import zip_longest
[docs] class LooseVersion(LooseVersion):
def _cmp(self, other):
for i, j in zip_longest(self.version, other.version, fillvalue=''):
if type(i) != type(j):
i = str(i)
j = str(j)
if i == j:
continue
elif i < j:
return -1
else: # i > j
return 1
return 0
[docs]class CaseSensitiveConfigParser(RawConfigParser):
[docs]class Requirement(UniventionMetaInfo):
save_as_list = '_requirements'
auto_set_name = True
pop = True
def __init__(self, actions, hard, func):
self.actions = actions
self.hard = hard
self.func = func
[docs] def test(self, app, function, package_manager):
method = getattr(app, self.name)
kwargs = {}
arguments = getargspec(method).args[1:] # remove self
if 'function' in arguments:
kwargs['function'] = function
if 'package_manager' in arguments:
kwargs['package_manager'] = package_manager
return method(**kwargs)
[docs] def contribute_to_class(self, klass, name):
super(Requirement, self).contribute_to_class(klass, name)
setattr(klass, name, self.func)
[docs]def hard_requirement(*actions):
return lambda func: Requirement(actions, True, func)
[docs]def soft_requirement(*actions):
return lambda func: Requirement(actions, False, func)
[docs]class AppAttribute(UniventionMetaInfo):
save_as_list = '_attrs'
auto_set_name = True
def __init__(self, required=False, default=None, regex=None, choices=None, localisable=False, localisable_by_file=None, strict=True):
super(AppAttribute, self).__init__()
self.regex = regex
self.default = default
self.required = required
self.choices = choices
self.localisable = localisable
self.localisable_by_file = localisable_by_file
self.strict = strict
[docs] def test_regex(self, regex, value):
if value is not None and not re.match(regex, value):
raise ValueError('Invalid format')
[docs] def test_choices(self, value):
if value is not None and value not in self.choices:
raise ValueError('Not allowed')
[docs] def test_required(self, value):
if value is None:
raise ValueError('Value required')
[docs] def test_type(self, value, instance_type):
if value is not None:
if instance_type is None:
instance_type = string_types
if not isinstance(value, instance_type):
raise ValueError('Wrong type')
[docs] def test(self, value):
try:
if self.required:
self.test_required(value)
self.test_type(value, string_types)
if self.choices:
self.test_choices(value)
if self.regex:
self.test_regex(self.regex, value)
except ValueError as e:
if self.strict:
raise
else:
app_logger.warn(str(e))
[docs] def parse(self, value):
return value
[docs] def get_value(self, component_id, ini_parser, meta_parser, locale):
ini_attr_name = self.name.replace('_', '')
priority_sections = [(meta_parser, 'Application'), (ini_parser, 'Application')]
if self.localisable and locale:
priority_sections.insert(0, (meta_parser, locale))
priority_sections.insert(2, (ini_parser, locale))
value = self.default
for parser, section in priority_sections:
try:
value = parser.get(section, ini_attr_name)
except (NoSectionError, NoOptionError):
pass
else:
break
value = self.parse(value)
self.test(value)
return value
[docs] def post_creation(self, app):
pass
# TODO: remove. deprecated
[docs] def parse_with_ini_file(self, value, ini_file):
return self.parse(value)
# TODO: remove. deprecated
[docs] def get(self, value, ini_file):
if value is None:
value = copy(self.default)
try:
value = self.parse_with_ini_file(value, ini_file)
except ValueError as exc:
raise ValueError('%s: %s (%r): %s' % (ini_file, self.name, value, exc))
else:
self.test(value)
return value
[docs]class AppComponentIDAttribute(AppAttribute):
[docs] def get_value(self, component_id, ini_parser, meta_parser, locale):
return component_id
[docs]class AppUCSVersionAttribute(AppAttribute):
[docs] def get_value(self, component_id, ini_parser, meta_parser, locale):
return ucr_get('version/version')
[docs]class AppBooleanAttribute(AppAttribute):
[docs] def test_type(self, value, instance_type):
super(AppBooleanAttribute, self).test_type(value, bool)
[docs] def parse(self, value):
if value in [True, False]:
return value
if value is not None:
if PY3:
value = RawConfigParser.BOOLEAN_STATES.get(str(value).lower())
else:
value = RawConfigParser._boolean_states.get(str(value).lower())
if value is None:
raise ValueError('Invalid value')
return value
[docs]class AppIntAttribute(AppAttribute):
[docs] def test_type(self, value, instance_type):
super(AppIntAttribute, self).test_type(value, int)
[docs] def parse(self, value):
if value is not None:
return int(value)
[docs]class AppListAttribute(AppAttribute):
[docs] def parse(self, value):
if value == '':
value = None
if isinstance(value, string_types):
value = re.split(r'\s*,\s*', value)
if value is None:
value = []
return value
[docs] def test_required(self, value):
if not value:
raise ValueError('Value required')
[docs] def test_type(self, value, instance_type):
super(AppListAttribute, self).test_type(value, list)
[docs] def test_choices(self, value):
if not value:
return
for val in value:
super(AppListAttribute, self).test_choices(val)
[docs] def test_regex(self, regex, value):
if not value:
return
for val in value:
super(AppListAttribute, self).test_regex(regex, val)
[docs]class AppFromFileAttribute(AppAttribute):
def __init__(self, klass):
self.klass = klass
[docs] def get_value(self, component_id, ini_file, meta_parser, locale):
return None
[docs] def post_creation(self, app):
values = getattr(app, 'get_%s' % self.name)()
setattr(app, self.name, [value.to_dict() for value in values])
[docs] def contribute_to_class(self, klass, name):
super(AppFromFileAttribute, self).contribute_to_class(klass, name)
def _get_objects_fn(_self):
cache_name = '_%s_cache' % name
if not hasattr(_self, cache_name):
setattr(_self, cache_name, self.klass.all_from_file(_self.get_cache_file(name), _self.get_locale()))
return getattr(_self, cache_name)
setattr(klass, 'get_%s' % name, _get_objects_fn)
[docs]class AppRatingAttribute(AppListAttribute):
[docs] def post_creation(self, app):
value = []
ratings = app.get_app_cache_obj().get_appcenter_cache_obj().get_ratings()
meta_parser = read_ini_file(app.get_cache_file('meta'))
for rating in ratings:
try:
val = int(meta_parser.get('Application', rating.name))
except (ValueError, TypeError, NoSectionError, NoOptionError):
pass
else:
rating = rating.to_dict()
rating['value'] = val
value.append(rating)
setattr(app, self.name, value)
# TODO: remove; unused
[docs]class AppLocalisedListAttribute(AppListAttribute):
_cache = {}
@classmethod
def _translate(cls, fname, locale, value, reverse=False):
if fname not in cls._cache:
cls._cache[fname] = translations = {}
cached_file = os.path.join(CACHE_DIR, '.%s' % fname)
localiser = read_ini_file(cached_file, CaseSensitiveConfigParser)
for section in localiser.sections():
translations[section] = dict(localiser.items(section))
translations = cls._cache[fname].get(locale)
if translations:
if reverse:
for k, v in translations.items():
if v == value:
value = k
break
else:
if value in translations:
value = translations[value]
return value
[docs] def get_value(self, component_id, ini_parser, meta_parser, locale):
value = super(AppLocalisedListAttribute, self).get_value(component_id, ini_parser, meta_parser, locale)
if self.localisable_by_file and locale:
for i, val in enumerate(value):
value[i] = self._translate(self.localisable_by_file, locale, val)
return value
[docs]class AppLocalisedAppCategoriesAttribute(AppListAttribute):
[docs] def post_creation(self, app):
value = getattr(app, self.name)
cache = app.get_app_cache_obj().get_appcenter_cache_obj()
value = [cache.get_app_categories().get(val.lower(), val) for val in value]
setattr(app, self.name, value)
[docs]class AppAttributeOrFalseOrNone(AppBooleanAttribute):
def __init__(self, required=False, default=None, regex=None, choices=None, localisable=False, localisable_by_file=None, strict=True):
choices = (choices or [])[:]
choices.extend([None, False])
super(AppAttributeOrFalseOrNone, self).__init__(required, default, regex, choices, localisable, localisable_by_file, strict)
[docs] def parse(self, value):
if value == 'False':
value = False
elif value == 'None':
value = None
return value
[docs] def test_type(self, value, instance_type):
if value is not False and value is not None:
super(AppBooleanAttribute, self).test_type(value, string_types)
[docs]class AppAttributeOrTrueOrNone(AppBooleanAttribute):
[docs] def parse(self, value):
if value == 'True':
value = True
elif value == 'None':
value = None
return value
[docs] def test_type(self, value, instance_type):
if value is not True and value is not None:
super(AppBooleanAttribute, self).test_type(value, string_types)
[docs]class AppFileAttribute(AppAttribute):
# TODO: UCR TOKEN
def __init__(self, required=False, default=None, regex=None, choices=None, localisable=True):
# localisable=True !
super(AppFileAttribute, self).__init__(required, default, regex, choices, localisable)
[docs] def get_value(self, component_id, ini_parser, meta_parser, locale):
return None
[docs] def post_creation(self, app):
value = None
fname = self.name.upper()
filenames = [fname, '%s_EN' % fname]
if self.localisable:
locale = app.get_locale()
if locale:
filenames.insert(0, '%s_%s' % (fname, locale.upper()))
for filename in filenames:
try:
with open(app.get_cache_file(filename), 'r') as fd:
value = ''.join(fd.readlines()).strip()
except EnvironmentError:
pass
else:
break
setattr(app, self.name, value)
# TODO: remove. deprecated - attention: install_base.py uses it
[docs] def get_filename(self, ini_file):
directory = os.path.dirname(ini_file)
component_id = os.path.splitext(os.path.basename(ini_file))[0]
fname = self.name.upper()
localised_file_exts = [fname, '%s_EN' % fname]
if self.localisable:
locale = get_locale()
if locale:
localised_file_exts.insert(0, '%s_%s' % (fname, locale.upper()))
for localised_file_ext in localised_file_exts:
filename = os.path.join(directory, '%s.%s' % (component_id, localised_file_ext))
if os.path.exists(filename):
return filename
[docs]class AppDockerScriptAttribute(AppAttribute):
[docs] def set_name(self, name):
self.default = os.path.join(CONTAINER_SCRIPTS_PATH, name[14:])
super(AppDockerScriptAttribute, self).set_name(name)
[docs]class App(with_metaclass(AppMetaClass, object)):
"""
This is the main App class. It represents *one version* of the App in
the Univention App Center. It is mainly a container for a parsed ini
file.
The attributes are described below. Technically they are added to the
class by the metaclass UniventionMetaClass. The magical parsing stuff
happens in from_ini(). In __init__ you can pass any value you want and
the App will just accept it.
Real work with the App class is done in the actions, not this class
itself.
Attributes:
id: A unique ID for the App. Different versions of the same
App have the same ID, though.
code: An internal ID like 2-char value that has no meaning
other than some internal reporting processing.
Univention handles this, not the App Provider.
component_id: The internal name of the repository on the App
Center server. Not necessarily (but often) named after
the *id*. Not part of the ini file.
ucs_version: Not part of the ini file.
name: The displayed name of the App.
version: Version of the App. Needs to be unique together with
with the *id*. Versions are compared against each other
using a very loose version comparison.
install_permissions: Whether a license needs to be bought in order
to install the App.
install_permissions_message: A message displayed to the user
when the App needs *install_permissions*, but the user
has not yet bought the App.
logo: The file name of the logo of the App. It is used in the
App Center overview when all Apps are shown in a
gallery. As the gallery items are squared, the logo
should be squared, too. Not part of the App class.
logo_detail_page: The file name of a "bigger" logo. It is shown
in the detail page of the App Center. Useful when there
is a stretched version with the logo, the name, maybe a
claim. If not given, the *logo* is used on the detail
page, too. Not part of the App class.
description: A short description of the App. Should not exceed
90 chars, otherwise it gets unreadable in the App
Center.
long_description: A more complete description of the App. HTML
allowed and required! Shown before installation, so it
should contain product highlights, use cases, etc.
thumbnails: A list of screenshots and / or YouTube video URLs.
categories: Categories this App shall be filed under.
app_categories: Categories this App is filed under in
the App catalog of univention.de.
website: Website for more information about the product (e.g.
landing page).
support_url: Website for getting support (or information about
how to buy a license).
contact: Contact email address for the customer.
vendor: Display name of the vendor. The actual creator of the
Software. See also *maintainer*.
website_vendor: Website of the vendor itself for more
information.
maintainer: Display name of the maintainer, who actually put
the App into the App Center. Often, but not necessarily
the *vendor*. If vendor and maintainer are the same,
maintainer does not need to be specified again.
website_maintainer: Website of the maintainer itself for more
information.
license: An abbreviation of a license category. See also
*license_agreement*.
license_agreement: A file containing the license text the end
user has to agree to. The file is shipped along with
the ini file. Not part of the ini file.
readme: A file containing information about first steps for
the end user. E.g., which UCS users have access to the
App. Shown in the App Center if the App is installed.
The file is shipped along with the ini file. Not part
of the ini file.
readme_install: A file containing important information for
the end user which is shown *just before* the
installation starts. The file is shipped along with
the ini file. Not part of the ini file.
readme_post_install: A file containing important information
for the end user which is shown *just after* the
installation is completed. The file is shipped along
with the ini file. Not part of the ini file.
readme_update: A file containing important information for the
end user which is shown *just before* the update
starts. Use case: Changelog. The file is shipped along
with the ini file. Not part of the ini file.
readme_post_update: A file containing important information
for the end user which is shown *just after* the update
is completed. The file is shipped along with the ini
file. Not part of the ini file.
readme_uninstall: A file containing important information for
the end user which is shown *just before* the
uninstallation starts. Use case: Warning about broken
services. The file is shipped along with the ini
file. Not part of the ini file.
readme_post_uninstall: A file containing important information
for the end user which is shown *just after* the
uninstallation is completed. Use case: Instructions how
to clean up if the App was unable to do it
automatically. The file is shipped along with the ini
file. Not part of the ini file.
notify_vendor: Whether the App provider shall be informed
about (un)installation of the App by Univention via
email.
notification_email: Email address that should be used to send
notifications. If none is provided the address from
*contact* will be used. Note: An empty email
(NotificationEmail=) is not valid! Remove the line (or
put in comments) in this case.
web_interface: The path of the App's web interface.
web_interface_name: A name for the App's web interface. If not
given, *name* is used.
web_interface_port_http: The port to the web interface (HTTP).
web_interface_port_https: The port to the web interface (HTTPS).
web_interface_proxy_scheme: Docker Apps only. Whether the web
interface in the container only supports HTTP, HTTPS
or both.
auto_mod_proxy: Docker Apps only. Whether the web interface
should be included in the host's apache configuration.
If yes, the web interface ports of the container are
used for a proxy configuration, so that the web
interface is again available on 80/443. In this case
the *web_interface* itself needs to have a distinct
path even inside the container (like "/myapp" instead
of "/" inside).
If *web_interface_proxy_scheme* is set to http, both
http and https are proxied to http in the container. If
set to https, proxy points always to https. If set to
both, http will go to http, https to https.
ucs_overview_category: Whether and if where on the start site
the *web_interface* should be registered automatically.
background_color: Which background color to use on tiles in
the App Center overview and the portal.
web_interface_link_target: Which link_target to add to a portal
entry. Currently supported:
useportaldefault: let the portal decide
embedded: in an iframe within the portal
newwindow: new browser tab (default)
samewindow: replaces portal (not recommended)
database: Which (if any) database an App wants to use. The App
Center will setup the database for the App. Useful for
Docker Apps running against the Host's database.
Supported: "mysql", "postgresql".
database_name: Name of the database to be created. Defaults to
*id*.
database_user: Name of the database user to be created.
Defaults to *id*. May not be "root" or "postgres".
database_password_file: Path to the file in which the password
will be stored. If not set, a default file will be
created.
docker_env_database_host: Environment variable name for the DB
host inside the Docker Container.
docker_env_database_port: Environment variable name for the DB
port.
docker_env_database_name: Environment variable name for the DB
name.
docker_env_database_user: Environment variable name for the DB
user.
docker_env_database_password: Environment variable name for the
DB password (of "docker_env_database_user").
docker_env_database_password_file: Environment variable name
for a file that holds the password for the DB. If set,
this file is created in the Docker Container;
*docker_env_database_password* will not be used.
plugin_of: App ID of the App the "base App" of this App. For
Docker Apps, the plugin is installed into the container
of *plugin_of*. For Non-Docker Apps this is just like
*required_apps*, but important for later migrations.
conflicted_apps: List of App IDs that may not be installed
together with this App. Works in both ways, one only
needs to specify it on one App.
required_apps: List of App IDs that need to be installed along
with this App.
required_apps_in_domain: Like *required_apps*, but the Apps may
be installed anywhere in the domain, not necessarily
on this very server.
conflicted_system_packages: List of debian package names that
cannot be installed along with the App.
required_ucs_version: The UCS version that is required for the
App to work (because a specific feature was added or
a bug was fixed after the initial release of this UCS
version). Examples: 4.1-1, 4.1-1 errata200.
supported_ucs_versions: List of UCS versions that may install
this App. Only makes sense for Docker Apps. Example:
4.1-4 errata370, 4.2-0
required_app_version_upgrade: The App version that has to be
installed before an upgrade to this version is allowed.
Does nothing when installing (not upgrading) the App.
end_of_life: If specified, this App does no longer show up in
the App Center when not installed. For old
installations, a warning is shown that the user needs
to find an alternative for the App. Should be
supported by an exhaustive *readme* file how to
migrate the App data.
without_repository: Whether this App can be installed without
adding a dedicated repository on the App Center server.
default_packages: List of debian package names that shall be
installed (probably living in the App Center server's
repository).
default_packages_master: List of package names that shall be
installed on Primary and Backup Directory Node
systems while this App is installed. Deprecated. Not
supported for Docker Apps.
additional_packages_master: List of package names that shall be
installed along with *default_packages* when installed
on a Primary Directory Node. Not supported for Docker Apps.
additional_packages_backup: List of package names that shall be
installed along with *default_packages* when installed
on a Backup Directory Node. Not supported for Docker Apps.
additional_packages_slave: List of package names that shall be
installed along with *default_packages* when installed
on a Replica Directory Node. Not supported for Docker Apps.
additional_packages_member: List of package names that shall be
installed along with *default_packages* when installed
on a Managed Node. Not supported for Docker Apps.
rating: Positive rating on specific categories regarding the
App. Controlled by Univention. Not part of the ini
file.
umc_module_name: If the App installs a UMC module, the ID can
specified so that a link may be generated by the App
Center.
umc_module_flavor: If the App installs a UMC module with
flavors, it can be specified so that a link may be
generated by the App Center.
user_activation_required: If domain users have to be somehow
modified ("activated") to use the application, the App
Center may generate a link to point the Users
module of UMC.
generic_user_activation: Automatically registers an LDAP schema
and adds a flag to the UCS user management that should
then be used to identify a user as "activated for the
App". If set to True, the name of the attribute is
*id*Activated. If set to anything else, the value is
used for the name of the attribute. If a schema file is
shipped along with the App, this file is used instead
of the auto generated one.
ports_exclusive: A list of ports the App requires to acquire
exclusively. Implicitly adds *conflicted_apps*. Docker
Apps will have these exact ports forwarded. The App
Center will also change the firewall rules.
ports_redirection: Docker Apps only. A list of ports the App
wants to get forwarded from the host to the container.
Example: 2222:22 will enable an SSH connection to the
container when the user is doing "ssh docker-host -p
2222".
ports_redirection_udp: Just like *ports_redirection*, but opens
UDP ports. Can be combined with the same
*ports_redirection* if needed.
server_role: List of UCS roles the App may be installed on.
supported_architectures: Non-Docker Apps only. List of
architectures the App supports. Docker Apps always
require amd64.
min_physical_ram: The minimal amount of memory in MB. This
value is compared with the currently available memory
(without Swap) when trying to install the application.
When the test fails, the user may still override it
and install it.
min_free_disk_space: The minimal amount of free disk space in MB.
This value is compared with the current free disk space
at the installation destination when trying to install the
application. When the test fails, the user may still override it
and install it.
shop_url: If given, a button is added to the App Center which
users can click to buy a license.
ad_member_issue_hide: When UCS is not managing the domain but
instead is only part of a Windows controlled Active
Directory domain, the environment in which the App runs
is different and certain services that this App relies
on may not not be running. Thus, the App should not be
shown at all in the App Center.
ad_member_issue_password: Like *ad_member_issue_hide* but only
shows a warning: The App needs a password service
running on the Windows domain controller, e.g. because
it needs the samba hashes to authenticate users. This
can be set up, but not automatically. A link to the
documentation how to set up that service in such
environments is shown.
app_report_object_type: In some environments, App reports are
automatically generated by a metering tool. This tool
counts a specific amount of LDAP objects.
*app_report_object_type* is the object type of these
objects. Example: users/user.
app_report_object_filter: Part of the App reporting. The
filter for *app_report_object_type*. Example:
(myAppActivated=1).
app_report_object_attribute: Part of the App reporting. If
specified, not 1 is counted per object, but the number
of values in this *app_report_object_attribute*.
Useful for *app_report_attribute_type = groups/group*
and *app_report_object_attribute = uniqueMember*.
app_report_attribute_type: Same as *app_report_object_type*
but regarding the list of DNs in
*app_report_object_attribute*.
app_report_attribute_filter: Same as
*app_report_object_filter* but regarding
*app_report_object_type*.
docker_image: Docker Image for the container. If specified the
App implicitly becomes a Docker App.
docker_inject_env_file: For Multi-Container Docker Apps, this
attribute specifies whether the (optional) environment file
shall be injected into the main, all or no services in the
docker compose file.
docker_main_service: For Multi-Container Docker Apps, this
attribute specifies the main service in the compose
file. This service's container will be used to run
scripts like *docker_script_setup*, etc.
docker_migration_works: Whether it is safe to install this
version while a non Docker version is or was installed.
docker_migration_link: A link to document where the necessary
steps to migrate the App from a Non-Docker version to a
Docker version are described. Only useful when
*docker_migration_works = False*.
docker_allowed_images: List of other Docker Images. Used for
updates. If the new version has a new *docker_image*
but the old App runs on an older image specified in
this list, the image is not exchanged.
docker_shell_command: Default command when running
"univention-app APP shell".
docker_volumes: List of volumes that shall be mounted from
the host to the container. Example:
/var/lib/host/MYAPP/:/var/lib/container/MYAPP/ mounts
the first directory in the container under the name
of the second directory.
docker_server_role: Which computer object type shall be
created in LDAP as the docker container.
docker_script_init: The CMD for the Docker App. An
empty value will use the container's entrypoint / CMD.
docker_script_setup: Path to the setup script in the container
run after the start of the container. If the App comes
with a setup script living on the App Center server,
this script is copied to this very path before being
executed.
docker_script_store_data: Like *docker_script_setup*, but for a
script that is run to backup the data just before
destroying the old container.
docker_script_restore_data_before_setup: Like
*docker_script_setup*, but for a script that is run to
restore backuped data just before running the setup
script.
docker_script_restore_data_after_setup: Like
*docker_script_setup*, but for a script that is run to
restore backuped data just after running the setup
script.
docker_script_update_available: Like *docker_script_setup*, but
for a script that is run to check whether an update is
available (packag or distribution upgrade).
docker_script_update_packages: Like *docker_script_setup*, but
for a script that is run to install package updates
(like security updates) in the container without
destroying it.
docker_script_update_release: Like *docker_script_setup*, but
for a script that is run to install distribution
updates (like new major releases of the OS) in
the container without destroying it.
docker_script_update_app_version: Like *docker_script_setup*,
but for a script that is run to specifically install
App package updates in the container without destroying
it.
docker_script_configure: Like *docker_script_setup*,
but for a script that is run after settings inside the
container were applied.
docker_ucr_style_env: Disable the passing of ucr style ("foo/bar")
environment variables into the container.
host_certificate_access: Docker Apps only. The App gets access
to the host certificate.
listener_udm_modules: List of UDM modules that a listener
integration shall watch.
"""
id = AppAttribute(regex='^[a-zA-Z0-9]+(([a-zA-Z0-9-_]+)?[a-zA-Z0-9])?$', required=True)
"""The required ID"""
code = AppAttribute(regex='^[A-Za-z0-9]{2}$', required=True)
component_id = AppComponentIDAttribute(required=True)
ucs_version = AppUCSVersionAttribute(required=True)
name = AppAttribute(required=True, localisable=True)
version = AppAttribute(required=True)
install_permissions = AppBooleanAttribute(default=False)
install_permissions_message = AppAttribute(localisable=True)
description = AppAttribute(localisable=True)
long_description = AppAttribute(localisable=True)
thumbnails = AppListAttribute(localisable=True)
categories = AppListAttribute()
app_categories = AppLocalisedAppCategoriesAttribute()
website = AppAttribute(localisable=True)
support_url = AppAttribute(localisable=True)
contact = AppAttribute()
vendor = AppAttribute()
website_vendor = AppAttribute(localisable=True)
maintainer = AppAttribute()
website_maintainer = AppAttribute(localisable=True)
license = AppAttribute(default='default')
license_agreement = AppFileAttribute()
readme = AppFileAttribute()
readme_install = AppFileAttribute()
readme_post_install = AppFileAttribute()
readme_update = AppFileAttribute()
readme_post_update = AppFileAttribute()
readme_uninstall = AppFileAttribute()
readme_post_uninstall = AppFileAttribute()
notify_vendor = AppBooleanAttribute(default=True)
notification_email = AppAttribute()
web_interface = AppAttribute()
web_interface_name = AppAttribute(localisable=True)
web_interface_port_http = AppIntAttribute(default=80)
web_interface_port_https = AppIntAttribute(default=443)
web_interface_proxy_scheme = AppAttribute(default='both', choices=['http', 'https', 'both'])
auto_mod_proxy = AppBooleanAttribute(default=True)
ucs_overview_category = AppAttributeOrFalseOrNone(default='service', choices=['admin', 'service'])
background_color = AppAttribute()
web_interface_link_target = AppAttribute(default='newwindow')
database = AppAttribute()
database_name = AppAttribute()
database_user = AppAttribute(regex='(?!^(root)$|^(postgres)$)') # anything but db superuser!
database_password_file = AppAttribute()
docker_env_database_host = AppAttribute(default='DB_HOST')
docker_env_database_port = AppAttribute(default='DB_PORT')
docker_env_database_name = AppAttribute(default='DB_NAME')
docker_env_database_user = AppAttribute(default='DB_USER')
docker_env_database_password = AppAttribute(default='DB_PASSWORD')
docker_env_database_password_file = AppAttribute()
plugin_of = AppAttribute()
conflicted_apps = AppListAttribute()
required_apps = AppListAttribute()
required_apps_in_domain = AppListAttribute()
conflicted_system_packages = AppListAttribute()
required_ucs_version = AppAttribute(regex=r'^(\d+)\.(\d+)-(\d+)(?: errata(\d+))?$')
supported_ucs_versions = AppListAttribute(regex=r'^(\d+)\.(\d+)-(\d+)(?: errata(\d+))?$')
required_app_version_upgrade = AppAttribute()
end_of_life = AppBooleanAttribute()
without_repository = AppBooleanAttribute()
default_packages = AppListAttribute()
default_packages_master = AppListAttribute()
additional_packages_master = AppListAttribute()
additional_packages_backup = AppListAttribute()
additional_packages_slave = AppListAttribute()
additional_packages_member = AppListAttribute()
settings = AppFromFileAttribute(Setting)
rating = AppRatingAttribute()
umc_module_name = AppAttribute()
umc_module_flavor = AppAttribute()
user_activation_required = AppBooleanAttribute()
generic_user_activation = AppAttributeOrTrueOrNone()
generic_user_activation_attribute = AppAttributeOrTrueOrNone()
generic_user_activation_option = AppAttributeOrTrueOrNone()
umc_options_attributes = AppListAttribute()
automatic_schema_creation = AppBooleanAttribute(default=True)
docker_env_ldap_user = AppAttribute()
ports_exclusive = AppListAttribute(regex=r'^\d+$')
ports_redirection = AppListAttribute(regex=r'^\d+:\d+$')
ports_redirection_udp = AppListAttribute(regex=r'^\d+:\d+$')
server_role = AppListAttribute(default=['domaincontroller_master', 'domaincontroller_backup', 'domaincontroller_slave', 'memberserver'], choices=['domaincontroller_master', 'domaincontroller_backup', 'domaincontroller_slave', 'memberserver'])
supported_architectures = AppListAttribute(default=['amd64', 'i386'], choices=['amd64', 'i386'])
min_physical_ram = AppIntAttribute(default=0)
min_free_disk_space = AppIntAttribute(default=None)
shop_url = AppAttribute(localisable=True)
ad_member_issue_hide = AppBooleanAttribute()
ad_member_issue_password = AppBooleanAttribute()
app_report_object_type = AppAttribute()
app_report_object_filter = AppAttribute()
app_report_object_attribute = AppAttribute()
app_report_attribute_type = AppAttribute()
app_report_attribute_filter = AppAttribute()
docker_image = AppAttribute()
docker_inject_env_file = AppAttribute()
docker_main_service = AppAttribute()
docker_migration_works = AppBooleanAttribute()
docker_migration_link = AppAttribute()
docker_allowed_images = AppListAttribute()
docker_shell_command = AppAttribute(default='/bin/bash')
docker_volumes = AppListAttribute()
docker_server_role = AppAttribute(default='memberserver', choices=['memberserver', 'domaincontroller_slave'])
docker_script_init = AppAttribute()
docker_script_setup = AppDockerScriptAttribute()
docker_script_store_data = AppDockerScriptAttribute()
docker_script_restore_data_before_setup = AppDockerScriptAttribute()
docker_script_restore_data_after_setup = AppDockerScriptAttribute()
docker_script_update_available = AppDockerScriptAttribute()
docker_script_update_packages = AppDockerScriptAttribute()
docker_script_update_release = AppDockerScriptAttribute()
docker_script_update_app_version = AppDockerScriptAttribute()
docker_script_configure = AppAttribute()
docker_ucr_style_env = AppBooleanAttribute(default=True)
docker_tmpfs = AppListAttribute(default=["/run", "/run/lock"])
host_certificate_access = AppBooleanAttribute()
listener_udm_modules = AppListAttribute()
vote_for_app = AppBooleanAttribute()
def __init__(self, _attrs, _cache, **kwargs):
if kwargs:
_attrs.update(kwargs)
self._weak_ref_app_cache = None
self._supports_ucs_version = None
self._install_permissions_exist = None
self.set_app_cache_obj(_cache)
for attr in self._attrs:
setattr(self, attr.name, _attrs.get(attr.name))
self.ucs_version = self.get_ucs_version() # compatibility
if self.docker:
if self.min_free_disk_space is None:
self.min_free_disk_space = 4000
self.supported_architectures = ['amd64']
if self.plugin_of:
for script in ['docker_script_restore_data_before_setup', 'docker_script_restore_data_after_setup']:
if getattr(self, script) == self.get_attr(script).default:
setattr(self, script, '')
else:
self.auto_mod_proxy = False
self.ports_redirection = []
[docs] def attrs_dict(self):
ret = {}
for attr in self._attrs:
ret[attr.name] = getattr(self, attr.name)
return ret
[docs] def install_permissions_exist(self):
if not self.docker:
return True
if not self.install_permissions:
return True
try:
from univention.appcenter.docker import access
except ImportError:
return True
if self._install_permissions_exist is None:
# this should be optimized
image = self.get_docker_image_name()
self._install_permissions_exist = access(image)
return self._install_permissions_exist
[docs] def get_docker_image_name(self):
if self.uses_docker_compose():
try:
import ruamel.yaml as yaml
except ImportError:
# appcenter-docker is not installed
return None
yml_file = self.get_cache_file('compose')
content = yaml.load(ucr_run_filter(open(yml_file).read()), yaml.RoundTripLoader, preserve_quotes=True)
image = content['services'][self.docker_main_service]['image']
return image
else:
image = self.get_docker_images()[0]
if self.is_installed():
image = ucr_get(self.ucr_image_key) or image
return image
[docs] def get_docker_images(self):
return [self.docker_image] + self.docker_allowed_images
[docs] def has_local_web_interface(self):
if self.web_interface:
return self.web_interface.startswith('/')
@property
def license_description(self):
return self.get_app_cache_obj().get_appcenter_cache_obj().get_license_description(self.license)
def __str__(self):
from univention.appcenter.app_cache import default_server, default_ucs_version
annotation = ''
server = default_server()
ucs_version = default_ucs_version()
if ucs_version != self.get_ucs_version():
annotation = self.get_ucs_version()
if server != self.get_server():
server = urlsplit(self.get_server()).netloc
annotation = '%s@%s' % (annotation, server)
if annotation:
annotation += '/'
return '%s%s=%s' % (annotation, self.id, self.version)
def __repr__(self):
return 'App(id="%s", version="%s", ucs_version="%s", server="%s")' % (self.id, self.version, self.get_ucs_version(), self.get_server())
@classmethod
def _get_meta_parser(cls, ini_file, ini_parser):
component_id = os.path.splitext(os.path.basename(ini_file))[0]
meta_file = os.path.join(os.path.dirname(ini_file), '%s.meta' % component_id)
return read_ini_file(meta_file)
[docs] @classmethod
def from_ini(cls, ini_file, locale=True, cache=None):
# app_logger.debug('Loading app from %s' % ini_file)
if locale is True:
locale = get_locale()
component_id = os.path.splitext(os.path.basename(ini_file))[0]
ini_parser = read_ini_file(ini_file)
meta_parser = cls._get_meta_parser(ini_file, ini_parser)
attr_values = {}
for attr in cls._attrs:
value = None
try:
value = attr.get_value(component_id, ini_parser, meta_parser, locale)
except ValueError as e:
app_logger.warning('Ignoring %s because of %s: %s' % (ini_file, attr.name, e))
return
attr_values[attr.name] = value
return cls(attr_values, cache)
@property
def docker(self):
return self.docker_image is not None or self.docker_main_service is not None
[docs] def uses_docker_compose(self):
return self.docker_main_service and not self.docker_image and os.path.exists(self.get_cache_file('compose'))
@property
def ucr_status_key(self):
return 'appcenter/apps/%s/status' % self.id
@property
def ucr_autoinstalled_key(self):
return 'appcenter/apps/%s/autoinstalled' % self.id
@property
def ucr_version_key(self):
return 'appcenter/apps/%s/version' % self.id
@property
def ucr_ucs_version_key(self):
return 'appcenter/apps/%s/ucs' % self.id
@property
def ucr_upgrade_key(self):
return 'appcenter/apps/%s/update/available' % self.id
@property
def ucr_container_key(self):
return 'appcenter/apps/%s/container' % self.id
@property
def ucr_hostdn_key(self):
return 'appcenter/apps/%s/hostdn' % self.id
@property
def ucr_image_key(self):
return 'appcenter/apps/%s/image' % self.id
@property
def ucr_docker_params_key(self):
return 'appcenter/apps/%s/docker/params' % self.id
@property
def ucr_ip_key(self):
return 'appcenter/apps/%s/ip' % self.id
@property
def ucr_ports_key(self):
return 'appcenter/apps/%s/ports/%%s' % self.id
@property
def ucr_component_key(self):
return 'repository/online/component/%s' % self.component_id
[docs] @classmethod
def get_attr(cls, attr_name):
for attr in cls._attrs:
if attr.name == attr_name:
return attr
[docs] def get_packages(self, additional=True):
packages = []
packages.extend(self.default_packages)
if additional:
role = ucr_get('server/role')
if role == 'domaincontroller_master':
packages.extend(self.additional_packages_master)
elif role == 'domaincontroller_backup':
packages.extend(self.additional_packages_backup)
elif role == 'domaincontroller_slave':
packages.extend(self.additional_packages_slave)
elif role == 'memberserver':
packages.extend(self.additional_packages_member)
return packages
[docs] def supports_ucs_version(self):
if self._supports_ucs_version is None:
self._supports_ucs_version = False
if not self.supported_ucs_versions:
self._supports_ucs_version = self.get_ucs_version() == ucr_get('version/version')
else:
for supported_version in self.supported_ucs_versions:
if supported_version.startswith('%s-' % ucr_get('version/version')):
self._supports_ucs_version = True
return self._supports_ucs_version
[docs] def is_installed(self):
if self.docker and not container_mode():
return ucr_get(self.ucr_status_key) in ['installed', 'stalled'] and ucr_get(self.ucr_version_key) == self.version and ucr_get(self.ucr_ucs_version_key, self.get_ucs_version()) == self.get_ucs_version()
else:
if not self.without_repository:
if not ucr_includes(self.ucr_component_key):
return False
return packages_are_installed(self.default_packages, strict=False)
[docs] def is_ucs_component(self):
english_cache = self.get_app_cache_obj().copy(locale='en')
app = english_cache.find_by_component_id(self.component_id)
if app is None:
# somehow the localized cache and the english cache split brains!
app_logger.warn('Could not find %r in %r' % (self, english_cache))
english_cache.clear_cache()
app = english_cache.find_by_component_id(self.component_id)
if app is None:
# giving up. not really harmful
return False
return 'UCS components' in app.categories
[docs] def get_share_dir(self):
return os.path.join(SHARE_DIR, self.id)
[docs] def get_share_file(self, ext):
return os.path.join(self.get_share_dir(), '%s.%s' % (self.id, ext))
[docs] def get_data_dir(self):
return os.path.join(DATA_DIR, self.id, 'data')
[docs] def get_conf_dir(self):
return os.path.join(DATA_DIR, self.id, 'conf')
[docs] def get_conf_file(self, fname):
if fname.startswith('/'):
fname = fname[1:]
fname = os.path.join(self.get_conf_dir(), fname)
if not os.path.exists(fname):
mkdir(os.path.dirname(fname))
return fname
[docs] def get_compose_dir(self):
return os.path.join(DATA_DIR, self.id, 'compose')
[docs] def get_compose_file(self, fname):
return os.path.join(self.get_compose_dir(), fname)
[docs] def get_ucs_version(self):
app_cache = self.get_app_cache_obj()
return app_cache.get_ucs_version()
[docs] def get_locale(self):
app_cache = self.get_app_cache_obj()
return app_cache.get_locale()
[docs] def get_server(self):
app_cache = self.get_app_cache_obj()
return app_cache.get_server()
[docs] def get_cache_dir(self):
app_cache = self.get_app_cache_obj()
return app_cache.get_cache_dir()
[docs] def get_app_cache_obj(self):
if self._weak_ref_app_cache is None:
from univention.appcenter.app_cache import AppCache
app_cache = AppCache.build()
self.set_app_cache_obj(app_cache)
return self._weak_ref_app_cache()
[docs] def set_app_cache_obj(self, app_cache_obj):
if app_cache_obj:
self._weak_ref_app_cache = ref(app_cache_obj)
else:
self._weak_ref_app_cache = None
[docs] def get_cache_file(self, ext):
return os.path.join(self.get_cache_dir(), '%s.%s' % (self.component_id, ext))
[docs] def get_ini_file(self):
return self.get_cache_file('ini')
@property
def logo_name(self):
return 'apps-%s.svg' % self.component_id
@property
def logo_detail_page_name(self):
if os.path.exists(self.get_cache_file('logodetailpage')):
return 'apps-%s-detail.svg' % self.component_id
@property
def secret_on_host(self):
return os.path.join(DATA_DIR, self.id, 'machine.secret')
[docs] def get_thumbnail_urls(self):
if not self.thumbnails:
return []
thumbnails = []
for ithumb in self.thumbnails:
if ithumb.startswith('http://') or ithumb.startswith('https://'):
# item is already a full URI
thumbnails.append(ithumb)
continue
app_path = '%s/' % self.id
ucs_version = self.get_ucs_version()
if ucs_version == '4.0' or ucs_version.startswith('3.'):
# since UCS 4.1, each app has a separate subdirectory
app_path = ''
thumbnails.append('%s/meta-inf/%s/%s%s' % (self.get_server(), ucs_version, app_path, ithumb))
return thumbnails
[docs] def get_localised(self, key, loc=None):
from univention.appcenter.actions import get_action
get = get_action('get')()
keys = [(loc, key)]
for section, name, value in get.get_values(self, keys, warn=False):
return value
[docs] def get_localised_list(self, key, loc=None):
from univention.appcenter.actions import get_action
get = get_action('get')()
ret = []
key = key.replace('_', '').lower()
keys = [(None, key), ('de', key)]
for section, name, value in get.get_values(self, keys, warn=False):
if value is None:
continue
if section is None:
section = 'en'
value = '[%s] %s' % (section, value)
ret.append(value)
return ret
[docs] @hard_requirement('install', 'upgrade')
def must_have_install_permissions(self):
'''You need to buy the App to install this version.'''
if not self.install_permissions_exist():
return {'shop_url': self.shop_url, 'version': self.version}
return True
[docs] @hard_requirement('upgrade')
def must_have_fitting_app_version(self):
'''To upgrade, at least version %(required_version)s needs to
be installed.'''
from univention.appcenter.app_cache import Apps
if self.required_app_version_upgrade:
required_version = LooseVersion(self.required_app_version_upgrade)
installed_app = Apps().find(self.id)
installed_version = LooseVersion(installed_app.version)
if required_version > installed_version:
return {'required_version': self.required_app_version_upgrade}
return True
[docs] @hard_requirement('install', 'upgrade')
def must_have_fitting_ucs_version(self):
'''The application requires UCS version %(required_version)s.'''
required_ucs_version = None
for supported_version in self.supported_ucs_versions:
if supported_version.startswith('%s-' % ucr_get('version/version')):
required_ucs_version = supported_version
break
else:
if self.get_ucs_version() == ucr_get('version/version'):
if self.required_ucs_version:
required_ucs_version = self.required_ucs_version
else:
return True
if required_ucs_version is None:
return {'required_version': self.get_ucs_version()}
major, minor = ucr_get('version/version').split('.', 1)
patchlevel = ucr_get('version/patchlevel')
errata = ucr_get('version/erratalevel')
version_bits = re.match(r'^(\d+)\.(\d+)-(\d+)(?: errata(\d+))?$', required_ucs_version).groups()
comparisons = zip(version_bits, [major, minor, patchlevel, errata])
for required, present in comparisons:
if int(required or 0) > int(present):
return {'required_version': required_ucs_version}
if int(required or 0) < int(present):
return True
return True
[docs] @hard_requirement('install', 'upgrade')
def must_have_fitting_kernel_version(self):
if self.docker:
kernel = LooseVersion(os.uname()[2])
if kernel < LooseVersion('4.9'):
return False
return True
[docs] @hard_requirement('install', 'upgrade')
def must_not_be_vote_for_app(self):
'''The application is not yet installable. Vote for this app
now and bring your favorite faster to the Univention App
Center'''
return not self.vote_for_app
[docs] @hard_requirement('install', 'upgrade')
def must_not_be_docker_if_docker_is_disabled(self):
'''The application uses a container technology while the App Center
is configured to not not support it'''
return not self.docker or ucr_is_true('appcenter/docker', True)
[docs] @hard_requirement('install', 'upgrade')
def must_not_be_docker_in_docker(self):
'''The application uses a container technology while the system
itself runs in a container. Using the application is not
supported on this host'''
return not self.docker or not container_mode()
[docs] @hard_requirement('install', 'upgrade')
def must_have_valid_license(self):
'''For the installation of this application, a UCS license key
with a key identification (Key ID) is required'''
if self.notify_vendor:
license = ucr_get('uuid/license')
if license is None:
ucr_load()
license = ucr_get('uuid/license')
return license is not None
return True
[docs] @hard_requirement('install')
def must_not_be_installed(self):
'''This application is already installed'''
return not self.is_installed()
[docs] @hard_requirement('install')
def must_not_be_end_of_life(self):
'''This application was discontinued and may not be installed
anymore'''
return not self.end_of_life
[docs] @hard_requirement('install', 'upgrade')
def must_have_supported_architecture(self):
'''This application only supports %(supported)s as
architecture. %(msg)s'''
supported_architectures = self.supported_architectures
platform_bits = platform.architecture()[0]
aliases = {'i386': '32bit', 'amd64': '64bit'}
if supported_architectures:
for architecture in supported_architectures:
if aliases[architecture] == platform_bits:
break
else:
# For now only two architectures are supported:
# 32bit and 64bit - and this will probably not change
# too soon.
# So instead of returning lists and whatnot
# just return a nice message
# Needs to be adapted when supporting different archs
supported = supported_architectures[0]
if supported == 'i386':
needs = 32
has = 64
else:
needs = 64
has = 32
msg = _('The application needs a %(needs)s-bit operating system. This server is running a %(has)s-bit operating system.') % {'needs': needs, 'has': has}
return {'supported': supported, 'msg': msg}
return True
[docs] @hard_requirement('install', 'upgrade')
def must_be_joined_if_master_packages(self):
'''This application requires an extension of the LDAP schema'''
is_joined = os.path.exists('/var/univention-join/joined')
return bool(is_joined or not self.default_packages_master)
[docs] @hard_requirement('install', 'upgrade', 'remove')
def must_not_have_concurrent_operation(self, package_manager):
'''Another package operation is in progress'''
if self.docker:
return True
else:
return package_manager.progress_state._finished # TODO: package_manager.is_finished()
[docs] @hard_requirement('install', 'upgrade')
def must_have_correct_server_role(self):
'''The application cannot be installed on the current server
role (%(current_role)s). In order to install the application,
one of the following roles is necessary: %(allowed_roles)r'''
server_role = ucr_get('server/role')
if not self._allowed_on_local_server():
return {
'current_role': server_role,
'allowed_roles': ', '.join(self.server_role),
}
return True
[docs] @hard_requirement('install', 'upgrade')
def must_have_no_conflicts_packages(self, package_manager):
'''The application conflicts with the following packages: %r'''
conflict_packages = []
for pkgname in self.conflicted_system_packages:
if package_manager.is_installed(pkgname):
conflict_packages.append(pkgname)
if conflict_packages:
return conflict_packages
return True
[docs] @hard_requirement('install', 'upgrade')
def must_have_no_conflicts_apps(self):
'''The application conflicts with the following applications:
%r'''
from univention.appcenter.app_cache import Apps
conflictedapps = set()
apps_cache = Apps()
# check ConflictedApps
for app in apps_cache.get_all_apps():
if not app._allowed_on_local_server():
# cannot be installed, continue
continue
if app.id in self.conflicted_apps or self.id in app.conflicted_apps:
if app.is_installed():
conflictedapps.add(app.id)
# check port conflicts
ports = []
for i in self.ports_exclusive:
ports.append(i)
for i in self.ports_redirection:
ports.append(i.split(':', 1)[0])
for app_id, container_port, host_port in app_ports():
if app_id != self.id and str(host_port) in ports:
conflictedapps.add(app_id)
if conflictedapps:
conflictedapps = [apps_cache.find(app_id) for app_id in conflictedapps]
return [{'id': app.id, 'name': app.name} for app in conflictedapps if app]
return True
[docs] @hard_requirement('install', 'upgrade')
def must_have_no_unmet_dependencies(self):
'''The application requires the following applications: %r'''
from univention.appcenter.app_cache import Apps
unmet_apps = []
apps_cache = Apps()
# RequiredApps
for app in apps_cache.get_all_apps():
if app.id in self.required_apps:
if not app.is_installed():
unmet_apps.append({'id': app.id, 'name': app.name, 'in_domain': False})
# Plugin
if self.plugin_of:
app = Apps.find(self.plugin_of)
if not app.is_installed():
unmet_apps.append({'id': app.id, 'name': app.name, 'in_domain': False})
# RequiredAppsInDomain
from univention.appcenter.actions import get_action
domain = get_action('domain')
apps = [apps_cache.find(app_id) for app_id in self.required_apps_in_domain]
apps_info = domain.to_dict(apps)
for app in apps_info:
if not app:
continue
if not app['is_installed_anywhere']:
local_allowed = app['id'] not in self.conflicted_apps
unmet_apps.append({'id': app['id'], 'name': app['name'], 'in_domain': True, 'local_allowed': local_allowed})
if unmet_apps:
return unmet_apps
return True
[docs] @hard_requirement('remove')
def must_not_be_depended_on(self):
'''The application is required for the following applications
to work: %r'''
from univention.appcenter.app_cache import Apps
depending_apps = []
apps_cache = Apps()
# RequiredApps
# RequiredApps
for app in apps_cache.get_all_apps():
if self.id in app.required_apps and app.is_installed():
depending_apps.append({'id': app.id, 'name': app.name})
# Plugin
if not self.docker:
for app in apps_cache.get_all_apps():
if self.id == app.plugin_of:
depending_apps.append({'id': app.id, 'name': app.name})
# RequiredAppsInDomain
apps = [app for app in apps_cache.get_all_apps() if self.id in app.required_apps_in_domain]
if apps:
from univention.appcenter.actions import get_action
domain = get_action('domain')
self_info = domain.to_dict([self])[0]
hostname = ucr_get('hostname')
if not any(inst['version'] for host, inst in self_info['installations'].items() if host != hostname):
# this is the only installation
apps_info = domain.to_dict(apps)
for app in apps_info:
if app['is_installed_anywhere']:
depending_apps.append({'id': app['id'], 'name': app['name']})
if depending_apps:
return depending_apps
return True
[docs] @hard_requirement('remove')
def must_not_remove_plugin(self):
'''It is currently impossible to remove a plugin once it is
installed. Remove %r instead.'''
from univention.appcenter.app_cache import Apps
if self.docker and self.plugin_of:
app = Apps().find(self.plugin_of)
return {'id': app.id, 'name': app.name}
return True
[docs] @soft_requirement('remove')
def shall_not_have_plugins_in_docker(self):
'''Uninstalling the App will also remove the following plugins:
%r'''
from univention.appcenter.app_cache import Apps
depending_apps = []
if self.docker:
for app in Apps().get_all_apps():
if self.id == app.plugin_of:
depending_apps.append({'id': app.id, 'name': app.name})
if depending_apps:
return depending_apps
return True
[docs] @soft_requirement('install')
def shall_have_enough_free_disk_space(self):
'''The application requires %(minimum)d MB of free disk space but only
%(current)d MB are available.'''
required_free_disk_space = self.min_free_disk_space or 0
if required_free_disk_space <= 0:
return True
current_free_disk_space = get_free_disk_space()
if current_free_disk_space and current_free_disk_space < required_free_disk_space:
return {'minimum': required_free_disk_space, 'current': current_free_disk_space}
return True
[docs] @soft_requirement('install', 'upgrade')
def shall_have_enough_ram(self, function):
'''The application requires %(minimum)d MB of free RAM but only
%(current)d MB are available.'''
from univention.appcenter.app_cache import Apps
current_ram = get_current_ram_available()
required_ram = self.min_physical_ram
if function == 'upgrade':
# is already installed, just a minor version upgrade
# RAM "used" by this installed app should count
# as free. best approach: subtract it
installed_app = Apps().find(self.id)
old_required_ram = installed_app.min_physical_ram
required_ram = required_ram - old_required_ram
if current_ram < required_ram:
return {'minimum': required_ram, 'current': current_ram}
return True
[docs] @soft_requirement('install', 'upgrade')
def shall_only_be_installed_in_ad_env_with_password_service(self):
'''The application requires the password service to be set up
on the Active Directory domain controller server.'''
return not self._has_active_ad_member_issue('password')
[docs] @hard_requirement('install', 'upgrade')
def shall_not_be_docker_if_discouraged(self):
'''The application has not been approved to migrate all
existing data. Maybe there is a migration guide:
%(migration_link)s'''
problem = self._docker_prudence_is_true() and not self.docker_migration_works
if problem:
return {'migration_link': self.docker_migration_link}
return True
def _docker_prudence_is_true(self):
if not self.docker:
return False
ret = ucr_is_true('appcenter/prudence/docker/%s' % self.id)
if not ret and self.plugin_of:
ret = ucr_is_true('appcenter/prudence/docker/%s' % self.plugin_of)
return ret
[docs] def check(self, function):
from univention.appcenter.app_cache import Apps
package_manager = get_package_manager()
hard_problems = {}
soft_problems = {}
if function == 'upgrade':
app = Apps().find(self.id)
if app > self:
# upgrade is not possible,
# special handling
hard_problems['must_have_candidate'] = False
for requirement in self._requirements:
if function not in requirement.actions:
continue
result = requirement.test(self, function, package_manager)
if result is not True:
if requirement.hard:
hard_problems[requirement.name] = result
else:
soft_problems[requirement.name] = result
return hard_problems, soft_problems
def _allowed_on_local_server(self):
server_role = ucr_get('server/role')
allowed_roles = self.server_role
return not allowed_roles or server_role in allowed_roles
def _has_active_ad_member_issue(self, issue):
return ucr_is_true('ad/member') and getattr(self, 'ad_member_issue_%s' % issue, False)
def __lt__(self, other):
"""
>>> from argparse import Namespace
>>> cache1 = Namespace(get_ucs_version=lambda: '1')
>>> cache2 = Namespace(get_ucs_version=lambda: '2')
>>> App({}, cache1, id=1, component_id=1) < App({}, cache1, id=1, component_id=1)
False
>>> App({}, cache1, id=1, component_id=1) < App({}, cache1, id=2, component_id=1)
True
>>> App({}, cache1, id=1, component_id=1) < App({}, cache2, id=1, component_id=1)
True
>>> App({}, cache1, id=1, component_id=1) < App({}, cache1, id=1, component_id=2)
True
"""
return (self.id, LooseVersion(self.get_ucs_version()), LooseVersion(self.version), self.component_id) < (other.id, LooseVersion(other.get_ucs_version()), LooseVersion(other.version), other.component_id) if isinstance(other, App) else NotImplemented
def __le__(self, other):
return (self.id, LooseVersion(self.get_ucs_version()), LooseVersion(self.version), self.component_id) <= (other.id, LooseVersion(other.get_ucs_version()), LooseVersion(other.version), other.component_id) if isinstance(other, App) else NotImplemented
def __eq__(self, other):
return (self.id, LooseVersion(self.get_ucs_version()), LooseVersion(self.version), self.component_id) == (other.id, LooseVersion(other.get_ucs_version()), LooseVersion(other.version), other.component_id) if isinstance(other, App) else NotImplemented
def __ne__(self, other):
return (self.id, LooseVersion(self.get_ucs_version()), LooseVersion(self.version), self.component_id) != (other.id, LooseVersion(other.get_ucs_version()), LooseVersion(other.version), other.component_id) if isinstance(other, App) else NotImplemented
def __ge__(self, other):
return (self.id, LooseVersion(self.get_ucs_version()), LooseVersion(self.version), self.component_id) >= (other.id, LooseVersion(other.get_ucs_version()), LooseVersion(other.version), other.component_id) if isinstance(other, App) else NotImplemented
def __gt__(self, other):
return (self.id, LooseVersion(self.get_ucs_version()), LooseVersion(self.version), self.component_id) > (other.id, LooseVersion(other.get_ucs_version()), LooseVersion(other.version), other.component_id) if isinstance(other, App) else NotImplemented