Skip to content

Commit

Permalink
Initial support for offline upgrades
Browse files Browse the repository at this point in the history
  • Loading branch information
joebonrichie committed Mar 29, 2024
1 parent 13c2898 commit 02887d4
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 1 deletion.
14 changes: 14 additions & 0 deletions eopkg-offline-update.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Update the operating system whilst offline

DefaultDependencies=no
Requires=sysinit.target dbus.socket
After=sysinit.target dbus.socket systemd-journald.socket system-update-pre.target
Before=shutdown.target system-update.target

[Service]
Type=oneshot
ExecStart=eopkg up --bypass-update-repo --yes-all
StandardOutput=journal+console

FailureAction=reboot
31 changes: 31 additions & 0 deletions pisi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,33 @@ def remove_repo(name):
raise pisi.Error(_('Repository %s does not exist. Cannot remove.')
% name)

def is_offline_upgrade_prepared():
if os.path.exists('/etc/system-update') or os.path.exists('/system-update'):
return True

return False

def clear_prepared_offline_upgrade():
try:
if os.path.exists('/etc/system-update'):
os.remove('/etc/system-update')
ctx.ui.debug(_('Deleted /etc/system-update'))
if os.path.exists('/system-update'):
os.remove('/system-update')
ctx.ui.debug(_('Deleted /system-update'))
except IOError as e:
ctx.ui.error(_('Failed to remove prepared offline upgrade, reason: %s') % e)
finally:
return True

@locked
def update_repos(repos, force=False):
if is_offline_upgrade_prepared() is True:
if force is True:
pisi.api.clear_prepared_offline_upgrade()
else:
ctx.ui.error(_('An offline upgrade is prepared for installation, blocking.'))

pisi.db.historydb.HistoryDB().create_history("repoupdate")
updated = False
try:
Expand All @@ -788,6 +813,12 @@ def update_repos(repos, force=False):

@locked
def update_repo(repo, force=False):
if is_offline_upgrade_prepared() is True:
if force is True:
pisi.api.clear_prepared_offline_upgrade()
else:
ctx.ui.error(_('An offline upgrade is prepared for installation, blocking.'))

pisi.db.historydb.HistoryDB().create_history("repoupdate")
updated = __update_repo(repo, force)
if updated:
Expand Down
8 changes: 8 additions & 0 deletions pisi/cli/updaterepo.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ def options(self):
def run(self):
self.init(database = True)

if pisi.api.is_offline_upgrade_prepared() is True:
ctx.ui.warning(_('An offline update is already prepared'))
if ctx.ui.confirm(_('Do you wish to clear the previously prepared offline update?')):
if pisi.api.clear_prepared_offline_upgrade() is False:
return
else:
return

if self.args:
repos = self.args
else:
Expand Down
20 changes: 19 additions & 1 deletion pisi/cli/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
#

import optparse
import os
import subprocess

from pisi import translate as _

import pisi.cli.command as command
import pisi.context as ctx
import pisi.util as util
import pisi.api
import pisi.db

Expand Down Expand Up @@ -62,6 +65,7 @@ def options(self):
type="string", default=None, help=_('Name of the to be upgraded packages\' repository'))
group.add_option("-f", "--fetch-only", action="store_true",
default=False, help=_("Fetch upgrades but do not install."))
group.add_option("--offline", action="store_true", default=False, help=_("Perform upgrades offline"))
group.add_option("-x", "--exclude", action="append",
default=None, help=_("When upgrading system, ignore packages and components whose basenames match pattern."))
group.add_option("--exclude-from", action="store",
Expand All @@ -74,11 +78,19 @@ def options(self):

def run(self):

if self.options.fetch_only:
if self.options.fetch_only or self.options.offline:
self.init(database=True, write=False)
else:
self.init()

if pisi.api.is_offline_upgrade_prepared() is True:
ctx.ui.warning(_('An offline update is already prepared'))
if ctx.ui.confirm(_('Do you wish to clear the previously prepared offline update?')):
if pisi.api.clear_prepared_offline_upgrade() is False:
return
else:
return

if not ctx.get_option('bypass_update_repo'):
ctx.ui.info(_('Updating repositories'))
repos = pisi.api.list_repos()
Expand All @@ -100,3 +112,9 @@ def run(self):
packages.extend(self.args)

pisi.api.upgrade(packages, repository)

if self.options.offline:
offline_file = os.path.join(ctx.config.history_dir(), 'prepared-offline-update')
if os.path.exists(offline_file):
if ctx.ui.confirm(_('The updates will be applied on next reboot. Do you wish to reboot now?')):
subprocess.Popen(["systemctl", "soft-reboot"])
48 changes: 48 additions & 0 deletions pisi/operations/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Please read the COPYING file.
#

import os
import sys

from pisi import translate as _
Expand Down Expand Up @@ -190,6 +191,12 @@ def upgrade(A=[], repo=None):
if ctx.get_option('fetch_only'):
return

# For offline upgrades return here after fetching of upgrades
# but prepare it first
if ctx.get_option('offline'):
prepare_offline_upgrade(order)
return

if conflicts:
operations.remove.remove_conflicting_packages(conflicts)

Expand All @@ -207,6 +214,47 @@ def upgrade(A=[], repo=None):
finally:
ctx.exec_usysconf()

def prepare_offline_upgrade(order):
# https://www.freedesktop.org/software/systemd/man/latest/systemd.offline-updates.html
#
# For a high-level overview
# 1. Fetch upgrades with eopkg up --fetch-only
# 2. Create symlink to /system-update to instruct systemd to perform offline upgrades
# 3. Soft reboot to initiate offline upgrades
# 3. eopkg-offline-upgrades.service will run eopkg up --bypass-update-repo --yes-all
# 4. System reboots and /system-update file is removed

# a bit of a hard coded safety hack here
if not os.path.exists('/usr/lib/systemd/system/system-update.target.wants/eopkg-offline-update.service'):
ctx.ui.error(_("eopkg-offline-update.service doesn't exist, check your installation"))
return

# If we get to this point just unconditionally clear any previously
# prepared offline upgrades to avoid issues.
if os.path.exists('/system-update'):
os.remove('/system-update')
if os.path.exists('/etc/system-update'):
os.remove('/etc/system-update')

# Nothing special about this file but we need to create some sort of symlink to /system-update
# to instruct systemd to perform an offline update
offline_file = os.path.join(ctx.config.history_dir(), 'prepared-offline-update')

packagedb = pisi.db.packagedb.PackageDB()

try:
with open(offline_file, 'w') as f:
for pkg in order:
info = packagedb.get_package(pkg)
f.write("%s\n\n" % unicode(info))
os.symlink(offline_file, '/system-update')
ctx.ui.debug(_('Created symlink from %s to /system-update') % offline_file)
except IOError:
ctx.ui.error(_('Failed to prepare offline upgrade'))
return
finally:
ctx.ui.info(util.colorize(_('Successfully prepared offline upgrade'), 'green'))

def plan_upgrade(A, force_replaced=True, replaces=None):
# FIXME: remove force_replaced
# try to construct a pisi graph of packages to
Expand Down

0 comments on commit 02887d4

Please sign in to comment.