From 9f049ca14403f3696d54d186e6b1b15181f055df Mon Sep 17 00:00:00 2001 From: Martin Basti Date: May 04 2015 11:16:26 +0000 Subject: Server Upgrade: Verify version and platform Verify version and platform before upgrade or ipactl start|restart Upgrade: * do not allow upgrade on different platforms * do not allow upgrade data with higher version than build has Start: * do not start services if platform mismatch * do not start services if upgrade is needed * do not start services if data with higher version than build has New ipactl options: --skip-version-check: do not validate IPA version --ignore-service-failures (was --force): ignore if a service start fail and continue with starting other services --force: combine --skip-version-check and --ignore-service-failures https://fedorahosted.org/freeipa/ticket/4904 Reviewed-By: Jan Cholasta Reviewed-By: David Kupka --- diff --git a/Makefile b/Makefile index 3225a61..abf5838 100644 --- a/Makefile +++ b/Makefile @@ -157,6 +157,8 @@ version-update: release-update > ipa-client/version.m4 if [ "$(SUPPORTED_PLATFORM)" != "" ]; then \ + sed -e s/__PLATFORM__/$(SUPPORTED_PLATFORM)/ \ + ipaplatform/__init__.py.in > ipaplatform/__init__.py; \ rm -f ipaplatform/paths.py ipaplatform/services.py ipaplatform/tasks.py; \ ln -s $(SUPPORTED_PLATFORM)/paths.py ipaplatform/paths.py; \ ln -s $(SUPPORTED_PLATFORM)/services.py ipaplatform/services.py; \ diff --git a/install/tools/ipactl b/install/tools/ipactl index b1b0b6e..b37f555 100755 --- a/install/tools/ipactl +++ b/install/tools/ipactl @@ -90,17 +90,41 @@ def parse_options(): parser.add_option("-d", "--debug", action="store_true", dest="debug", help="Display debugging information") parser.add_option("-f", "--force", action="store_true", dest="force", - help="If any service start fails, do not rollback the" - + " services, continue with the operation") + help="Force IPA to start. Combine options " + "--skip-version-check and --ignore-service-failures") + parser.add_option("--ignore-service-failures", action="store_true", + dest="ignore_service_failures", + help="If any service start fails, do not rollback the " + "services, continue with the operation") + parser.add_option("--skip-version-check", action="store_true", + dest="skip_version_check", default=False, + help="skip version check") options, args = parser.parse_args() safe_options = parser.get_safe_opts(options) + if options.force: + options.ignore_service_failures = True + options.skip_version_check = True + return safe_options, options, args def emit_err(err): sys.stderr.write(err + '\n') + +def version_check(): + try: + installutils.check_version() + except (installutils.UpgradeMissingVersionError, + installutils.UpgradeDataOlderVersionError): + emit_err("Upgrade required: please run ipa-server-upgrade command") + raise IpactlError("Aborting ipactl") + except installutils.UpgradeVersionError as e: + emit_err("IPA version error: %s" % e) + raise IpactlError("Aborting ipactl") + + def get_config(dirsrv): base = DN(('cn', api.env.host), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) srcfilter = '(ipaConfigString=enabledService)' @@ -217,6 +241,11 @@ def stop_dirsrv(dirsrv): def ipa_start(options): + if not options.skip_version_check: + version_check() + else: + print "Skipping version check" + if os.path.isfile(tasks.get_svc_list_file()): emit_err("Existing service file detected!") emit_err("Assuming stale, cleaning and proceeding") @@ -241,7 +270,7 @@ def ipa_start(options): emit_err("Failed to read data from service file: " + str(e)) emit_err("Shutting down") - if not options.force: + if not options.ignore_service_failures: stop_dirsrv(dirsrv) if isinstance(e, IpactlError): @@ -261,8 +290,9 @@ def ipa_start(options): svchandle.start(capture_output=get_capture_output(svc, options.debug)) except Exception: emit_err("Failed to start %s Service" % svc) - #if force start specified, skip rollback and continue with the next service - if options.force: + # if ignore_service_failures is specified, skip rollback and + # continue with the next service + if options.ignore_service_failures: emit_err("Forced start, ignoring %s Service, continuing normal operation" % svc) continue @@ -313,6 +343,11 @@ def ipa_stop(options): def ipa_restart(options): + if not options.skip_version_check: + version_check() + else: + print "Skipping version check" + dirsrv = services.knownservices.dirsrv new_svc_list = [] dirsrv_restart = True @@ -379,7 +414,7 @@ def ipa_restart(options): emit_err("Failed to restart Directory Service: " + str(e)) emit_err("Shutting down") - if not options.force: + if not options.ignore_service_failures: stop_services(reversed(svc_list)) stop_dirsrv(dirsrv) @@ -395,8 +430,9 @@ def ipa_restart(options): svchandle.restart(capture_output=get_capture_output(svc, options.debug)) except Exception: emit_err("Failed to restart %s Service" % svc) - #if force start specified, skip rollback and continue with the next service - if options.force: + # if ignore_service_failures is specified, + # skip rollback and continue with the next service + if options.ignore_service_failures: emit_err("Forced restart, ignoring %s Service, continuing normal operation" % svc) continue @@ -415,8 +451,9 @@ def ipa_restart(options): svchandle.start(capture_output=get_capture_output(svc, options.debug)) except Exception: emit_err("Failed to start %s Service" % svc) - #if force start specified, skip rollback and continue with the next service - if options.force: + # if ignore_service_failures is specified, skip rollback and + # continue with the next service + if options.ignore_service_failures: emit_err("Forced start, ignoring %s Service, continuing normal operation" % svc) continue diff --git a/install/tools/man/ipa-server-upgrade.1 b/install/tools/man/ipa-server-upgrade.1 index 02f252e..b3d89bd 100644 --- a/install/tools/man/ipa-server-upgrade.1 +++ b/install/tools/man/ipa-server-upgrade.1 @@ -18,6 +18,12 @@ ipa\-server\-upgrade will: .SH "OPTIONS" .TP +\fB\-\-skip\-version-\check\fR +Skip version check. WARNING: this option may break your system +.TP +\fB\-\-force\fR +Force upgrade (alias for --skip-version-check) +.TP \fB\-\-version\fR Show IPA version .TP diff --git a/install/tools/man/ipactl.8 b/install/tools/man/ipactl.8 index 5a1fd27..136fe9a 100644 --- a/install/tools/man/ipactl.8 +++ b/install/tools/man/ipactl.8 @@ -41,5 +41,11 @@ Stop then start all of the services that make up IPA \fB\-d\fR, \fB\-\-debug\fR Display debugging information .TP -\fB\-f\fR, \fB\-\-force\fR +\fB\-\-skip\-version\-check\fR +Skip version check +.TP +\fB\-\-ignore\-service\-failures\fR If any service start fails, do not rollback the services, continue with the operation +.TP +\fB\-f\fR, \fB\-\-force\fR +Force IPA to start. Combine options --skip-version-check and --ignore-service-failures diff --git a/ipaplatform/__init__.py b/ipaplatform/__init__.py deleted file mode 100644 index cf342aa..0000000 --- a/ipaplatform/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Authors: -# Tomas Babej -# -# Copyright (C) 2014 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -''' -Module containing platform-specific functionality for every platform. -''' diff --git a/ipaplatform/__init__.py.in b/ipaplatform/__init__.py.in new file mode 100644 index 0000000..61f6f3c --- /dev/null +++ b/ipaplatform/__init__.py.in @@ -0,0 +1,12 @@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +''' +Module containing platform-specific functionality for every platform. +''' + +NAME = "__PLATFORM__" + +# FIXME: too much cyclic dependencies +# from __PLATFORM__ import paths, tasks, services diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py index ff71c2b..10c5e83 100644 --- a/ipaplatform/base/tasks.py +++ b/ipaplatform/base/tasks.py @@ -24,6 +24,9 @@ This module contains default platform-specific implementations of system tasks. import pwd import grp + +from pkg_resources import parse_version + from ipaplatform.paths import paths from ipapython.ipa_log_manager import log_mgr from ipapython import ipautil @@ -208,5 +211,12 @@ class BaseTaskNamespace(object): else: log.debug('user %s exists', name) + def parse_ipa_version(self, version): + """ + :param version: textual version + :return: object implementing proper __cmp__ method for version compare + """ + return parse_version(version) + task_namespace = BaseTaskNamespace() diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index 8a76e77..da00bcf 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -511,6 +511,8 @@ class DsInstance(service.Service): sub_dict=self.sub_dict) files = ld.get_all_files(ldapupdate.UPDATES_DIR) ld.update(files) + installutils.store_version() + def __add_referint_module(self): self._ldap_mod("referint-conf.ldif") diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index 787a120..8a4f2ca 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -35,6 +35,8 @@ from dns.exception import DNSException import ldap from nss.error import NSPRError +import ipaplatform + from ipapython import ipautil, sysrestore, admintool, dogtag, version from ipapython.admintool import ScriptError from ipapython.ipa_log_manager import root_logger, log_mgr @@ -42,9 +44,10 @@ from ipalib.util import validate_hostname from ipapython import config from ipalib import errors, x509 from ipapython.dn import DN -from ipaserver.install import certs, service +from ipaserver.install import certs, service, sysupgrade from ipaplatform import services from ipaplatform.paths import paths +from ipaplatform.tasks import tasks # Used to determine install status IPA_MODULES = [ @@ -67,6 +70,27 @@ class HostReverseLookupError(HostLookupError): class HostnameLocalhost(HostLookupError): pass + +class UpgradeVersionError(Exception): + pass + + +class UpgradePlatformError(UpgradeVersionError): + pass + + +class UpgradeDataOlderVersionError(UpgradeVersionError): + pass + + +class UpgradeDataNewerVersionError(UpgradeVersionError): + pass + + +class UpgradeMissingVersionError(UpgradeVersionError): + pass + + class ReplicaConfig: def __init__(self, top_dir=None): self.realm_name = "" @@ -1037,3 +1061,47 @@ def load_external_cert(files, subject_base): ca_file.flush() return cert_file, ca_file + + +def store_version(): + """Store current data version and platform. This is required for check if + upgrade is required. + """ + sysupgrade.set_upgrade_state('ipa', 'data_version', + version.VENDOR_VERSION) + sysupgrade.set_upgrade_state('ipa', 'platform', ipaplatform.NAME) + + +def check_version(): + """ + :raise UpgradePlatformError: if platform is not the same + :raise UpgradeDataOlderVersionError: if data needs to be upgraded + :raise UpgradeDataNewerVersionError: older version of IPA was detected than data + :raise UpgradeMissingVersionError: if platform or version is missing + """ + platform = sysupgrade.get_upgrade_state('ipa', 'platform') + if platform is not None: + if platform != ipaplatform.NAME: + raise UpgradePlatformError( + "platform mismatch (expected '%s', current '%s')" % ( + platform, ipaplatform.NAME) + ) + else: + raise UpgradeMissingVersionError("no platform stored") + + data_version = sysupgrade.get_upgrade_state('ipa', 'data_version') + if data_version is not None: + parsed_data_ver = tasks.parse_ipa_version(data_version) + parsed_ipa_ver = tasks.parse_ipa_version(version.VENDOR_VERSION) + if parsed_data_ver < parsed_ipa_ver: + raise UpgradeDataOlderVersionError( + "data needs to be upgraded (expected version '%s', current " + "version '%s')" % (version.VENDOR_VERSION, data_version) + ) + elif parsed_data_ver > parsed_ipa_ver: + raise UpgradeDataNewerVersionError( + "data are in newer version than IPA (data version '%s', IPA " + "version '%s')" % (data_version, version.VENDOR_VERSION) + ) + else: + raise UpgradeMissingVersionError("no data_version stored") diff --git a/ipaserver/install/ipa_server_upgrade.py b/ipaserver/install/ipa_server_upgrade.py index 6d77fdd..148d1fe 100644 --- a/ipaserver/install/ipa_server_upgrade.py +++ b/ipaserver/install/ipa_server_upgrade.py @@ -21,11 +21,21 @@ class ServerUpgrade(admintool.AdminTool): @classmethod def add_options(cls, parser): - super(ServerUpgrade, cls).add_options(parser, debug_option=True) + super(ServerUpgrade, cls).add_options(parser) + parser.add_option("--force", action="store_true", + dest="force", default=False, + help="force upgrade (alias for --skip-version-check)") + parser.add_option("--skip-version-check", action="store_true", + dest="skip_version_check", default=False, + help="skip version check. WARNING: this may break " + "your system") def validate_options(self): super(ServerUpgrade, self).validate_options(needs_root=True) + if self.options.force: + self.options.skip_version_check = True + try: installutils.check_server_configuration() except RuntimeError as e: @@ -43,6 +53,24 @@ class ServerUpgrade(admintool.AdminTool): options = self.options + if not options.skip_version_check: + # check IPA version and data version + try: + installutils.check_version() + except (installutils.UpgradePlatformError, + installutils.UpgradeDataNewerVersionError) as e: + raise admintool.ScriptError( + 'Unable to execute IPA upgrade: %s' % e, 1) + except installutils.UpgradeMissingVersionError as e: + self.log.info("Missing version: %s", e) + except installutils.UpgradeVersionError: + # Ignore other errors + pass + else: + self.log.info("Skipping version check") + self.log.warning("Upgrade without version check may break your " + "system") + realm = krbV.default_context().default_realm data_upgrade = IPAUpgrade(realm) data_upgrade.create_instance() @@ -57,6 +85,9 @@ class ServerUpgrade(admintool.AdminTool): else: self.log.info('Data update complete, no data were modified') + # store new data version after upgrade + installutils.store_version() + # FIXME: remove this when new installer will be ready # execute upgrade of configuration cmd = ['ipa-upgradeconfig', ]