# Copyright (c) 2014-2019, iocage # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted providing that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """iocage upgrade module""" import datetime import fileinput import hashlib import os import pathlib import subprocess as su import tempfile import urllib.request import iocage_lib.ioc_common import iocage_lib.ioc_json import iocage_lib.ioc_list class IOCUpgrade: """Will upgrade a jail to the specified RELEASE.""" def __init__(self, new_release, path, interactive=True, silent=False, callback=None, ): super().__init__() self.pool = iocage_lib.ioc_json.IOCJson().json_get_value("pool") self.iocroot = iocage_lib.ioc_json.IOCJson( self.pool).json_get_value("iocroot") self.freebsd_version = iocage_lib.ioc_common.checkoutput( ["freebsd-version"]) self.conf = iocage_lib.ioc_json.IOCJson(path.rsplit( '/root', 1)[0]).json_get_value('all') self.uuid = self.conf["host_hostuuid"] self.host_release = os.uname()[2] _release = self.conf["release"].rsplit("-", 1)[0] self.jail_release = _release if "-RELEASE" in _release else \ self.conf["release"] self.new_release = new_release self.path = path self.status, self.jid = iocage_lib.ioc_list.IOCList.list_get_jid( self.uuid) self._freebsd_version = f"{self.iocroot}/jails/" \ f"{self.uuid}/root/bin/freebsd-version" self.date = datetime.datetime.utcnow().strftime("%F") self.interactive = interactive self.silent = silent path = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:'\ '/usr/local/bin:/root/bin' self.upgrade_env = { 'PAGER': '/bin/cat', 'PATH': path, 'PWD': '/', 'HOME': '/', 'TERM': 'xterm-256color' } self.callback = callback # symbolic link created on fetch by freebsd-update bd_hash = hashlib.sha256((self.path + '\n').encode('utf-8')).hexdigest() self.freebsd_install_link = os.path.join(self.path, 'var/db/freebsd-update', bd_hash + '-install') def upgrade_jail(self): iocage_lib.ioc_common.tmp_dataset_checks(self.callback, self.silent) if "HBSD" in self.freebsd_version: su.Popen(["hbsd-upgrade", "-j", self.jid]).communicate() return if not os.path.isfile(f"{self.path}/etc/freebsd-update.conf"): return self.__upgrade_check_conf__() f_rel = f'{self.new_release.rsplit("-RELEASE")[0]}' f = 'https://raw.githubusercontent.com/freebsd/freebsd-src' \ f'/releng/{f_rel}/usr.sbin/freebsd-update/freebsd-update.sh' tmp = None try: tmp = tempfile.NamedTemporaryFile(delete=False) with urllib.request.urlopen(f) as fbsd_update: tmp.write(fbsd_update.read()) tmp.close() os.chmod(tmp.name, 0o755) fetch_cmd = [ tmp.name, "-b", self.path, "-d", f"{self.path}/var/db/freebsd-update/", "-f", f"{self.path}/etc/freebsd-update.conf", "--not-running-from-cron", "--currently-running " f"{self.jail_release}", "-r", self.new_release, "upgrade" ] # FreeNAS MW/Others, this is a best effort as things may require # stdin input, in which case dropping to a tty is the best solution if not self.interactive: with iocage_lib.ioc_exec.IOCExec( fetch_cmd, self.path.replace('/root', ''), uuid=self.uuid, unjailed=True, stdin_bytestring=b'y\n', callback=self.callback, ) as _exec: iocage_lib.ioc_common.consume_and_log( _exec, callback=self.callback ) else: iocage_lib.ioc_exec.InteractiveExec( fetch_cmd, self.path.replace('/root', ''), uuid=self.uuid, unjailed=True ) if not os.path.islink(self.freebsd_install_link): msg = 'Upgrade failed, nothing to install after fetch!' iocage_lib.ioc_common.logit( { 'level': 'EXCEPTION', 'message': msg }, _callback=self.callback, silent=self.silent ) for _ in range(50): # up to 50 invocations to prevent runaway if os.path.islink(self.freebsd_install_link): self.__upgrade_install__(tmp.name) else: break if os.path.islink(self.freebsd_install_link): msg = f'Upgrade failed, freebsd-update won\'t finish!' iocage_lib.ioc_common.logit( { 'level': 'EXCEPTION', 'message': msg }, _callback=self.callback, silent=self.silent ) new_release = iocage_lib.ioc_common.get_jail_freebsd_version( self.path, self.new_release ) if f_rel.startswith('12'): # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=239498 cp = su.Popen( ['pkg-static', '-j', self.jid, 'install', '-q', '-f', '-y', 'pkg'], stdout=su.PIPE, stderr=su.PIPE ) _, stderr = cp.communicate() if cp.returncode: # Let's make this non-fatal as this is only being done as a convenience to user iocage_lib.ioc_common.logit( { 'level': 'ERROR', 'message': 'Unable to install pkg after upgrade' }, _callback=self.callback, silent=self.silent, ) finally: if tmp: if not tmp.closed: tmp.close() os.remove(tmp.name) iocage_lib.ioc_json.IOCJson( self.path.replace('/root', ''), silent=True).json_set_value(f"release={new_release}") return new_release def upgrade_basejail(self, snapshot=True, snap_name=None): if "HBSD" in self.freebsd_version: # TODO: Not supported yet msg = "Upgrading basejails on HardenedBSD is not supported yet." iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": msg }, _callback=self.callback, silent=self.silent) release_p = pathlib.Path(f"{self.iocroot}/releases/{self.new_release}") self._freebsd_version = f"{self.iocroot}/releases/"\ f"{self.new_release}/root/bin/freebsd-version" if not release_p.exists(): msg = f"{self.new_release} is missing, please fetch it!" iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": msg }, _callback=self.callback, silent=self.silent) if snapshot: self.__snapshot_jail__() p = pathlib.Path( f"{self.iocroot}/releases/{self.new_release}/root/usr/src") p_files = [] if p.exists(): for f in p.iterdir(): # We want to make sure files actually exist as well p_files.append(f) if not p_files: msg = f"{self.new_release} is missing 'src.txz', please refetch!" iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": msg }, _callback=self.callback, silent=self.silent) self.__upgrade_replace_basejail_paths__() ioc_up_dir = pathlib.Path(f"{self.path}/iocage_upgrade") if not ioc_up_dir.exists(): ioc_up_dir.mkdir(exist_ok=True, parents=True) mount_cmd = [ "mount_nullfs", "-o", "ro", f"{self.iocroot}/releases/{self.new_release}/root/usr/src", f"{self.path}/iocage_upgrade" ] try: iocage_lib.ioc_exec.SilentExec( mount_cmd, self.path.replace('/root', ''), uuid=self.uuid, unjailed=True ) except iocage_lib.ioc_exceptions.CommandFailed: msg = "Mounting src into jail failed! Rolling back snapshot." self.__rollback_jail__(name=snap_name) iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": msg }, _callback=self.callback, silent=self.silent) etcupdate_cmd = [ "/usr/sbin/jexec", f"ioc-{self.uuid.replace('.', '_')}", "/usr/sbin/etcupdate", "-F", "-s", "/iocage_upgrade" ] try: iocage_lib.ioc_exec.SilentExec( etcupdate_cmd, self.path.replace('/root', ''), uuid=self.uuid, unjailed=True ) except iocage_lib.ioc_exceptions.CommandFailed: # These are now the result of a failed merge, nuking and putting # the backup back msg = "etcupdate failed! Rolling back snapshot." self.__rollback_jail__(name=snap_name) su.Popen([ "umount", "-f", f"{self.path}/iocage_upgrade" ]).communicate() iocage_lib.ioc_common.logit( { "level": "EXCEPTION", "message": msg }, _callback=self.callback, silent=self.silent) new_release = iocage_lib.ioc_common.get_jail_freebsd_version( f'{self.iocroot}/releases/{self.new_release}/root', self.new_release ) iocage_lib.ioc_json.IOCJson( f"{self.path.replace('/root', '')}", silent=True).json_set_value(f"release={new_release}") mq = pathlib.Path(f"{self.path}/var/spool/mqueue") if not mq.exists(): mq.mkdir(exist_ok=True, parents=True) iocage_lib.ioc_exec.SilentExec( ['newaliases'], self.path.replace('/root', ''), uuid=self.uuid ) umount_command = [ "umount", "-f", f"{self.path}/iocage_upgrade" ] iocage_lib.ioc_exec.SilentExec( umount_command, self.path.replace('/root', ''), uuid=self.uuid, unjailed=True ) return new_release def __upgrade_install__(self, name): """Installs the upgrade.""" install_cmd = [ name, "-b", self.path, "-d", f"{self.path}/var/db/freebsd-update/", "-f", f"{self.path}/etc/freebsd-update.conf", "-r", self.new_release, "install" ] if not self.interactive: with iocage_lib.ioc_exec.IOCExec( install_cmd, self.path.replace('/root', ''), uuid=self.uuid, unjailed=True, callback=self.callback, ) as _exec: iocage_lib.ioc_common.consume_and_log( _exec, callback=self.callback ) else: iocage_lib.ioc_exec.InteractiveExec( install_cmd, self.path.replace('/root', ''), uuid=self.uuid, unjailed=True ) def __upgrade_check_conf__(self): """ Replaces freebsd-update.conf's default Components configuration to not update kernel """ f = f"{self.path}/etc/freebsd-update.conf" text = "Components src world kernel" replace = "Components src world" self.__upgrade_replace_text__(f, text, replace) def __upgrade_replace_basejail_paths__(self): f = f"{self.iocroot}/jails/{self.uuid}/fstab" self.__upgrade_replace_text__(f, self.jail_release, self.new_release) @staticmethod def __upgrade_replace_text__(path, text, replace): with fileinput.FileInput(path, inplace=True, backup=".bak") as _file: for line in _file: print(line.replace(text, replace), end='') os.remove(f"{path}.bak") def __snapshot_jail__(self): import iocage_lib.iocage as ioc # Avoids dep issues name = f"ioc_upgrade_{self.date}" ioc.IOCage(jail=self.uuid, skip_jails=True, silent=True).snapshot(name) def __rollback_jail__(self, name=None): import iocage_lib.iocage as ioc # Avoids dep issues name = name if name else f'ioc_upgrade_{self.date}' iocage = ioc.IOCage(jail=self.uuid, skip_jails=True, silent=True) iocage.stop() iocage.rollback(name)