diff --git a/package-updates/CHANGELOG b/package-updates/CHANGELOG
index c6dd08cd9..806fb24bd 100644
--- a/package-updates/CHANGELOG
+++ b/package-updates/CHANGELOG
@@ -1,3 +1,5 @@
+---- Changes since 2.641 ----
+Added a Held updates view with controls to hold, unhold or explicitly update APT-held packages.
---- Changes since 1.490 ----
First version of this module.
---- Changes since 1.500 ----
diff --git a/package-updates/index.cgi b/package-updates/index.cgi
index 8df9e0eb5..e0f820d28 100755
--- a/package-updates/index.cgi
+++ b/package-updates/index.cgi
@@ -9,6 +9,7 @@ if ($in{'clear'}) {
$in{'search'} = '';
}
$has_repos = defined(&software::list_package_repos);
+$has_holds = &supports_package_holds();
# Start of mode tabs
print &ui_tabs_start([ [ 'pkgs', $text{'index_tabpkgs'} ],
@@ -23,7 +24,10 @@ $in{'mode'} ||= 'updates';
# Show mode selector (all, updates only, updates and new)
@grid = ( );
-foreach $m ('current', 'updates', 'security', 'new') {
+@modes = ('current', 'updates');
+push(@modes, 'held') if ($has_holds);
+push(@modes, 'security', 'new');
+foreach $m (@modes) {
$mmsg = $text{'index_mode_'.$m};
if ($in{'mode'} eq $m) {
push(@mlinks, "$mmsg");
@@ -68,15 +72,21 @@ foreach $p (sort { $a->{'name'} cmp $b->{'name'} } (@current, @avail)) {
$c = $current{$p->{'name'}."/".$p->{'system'}};
$a = $avail{$p->{'name'}."/".$p->{'system'}};
- if ($a && $c && (&compare_versions($a, $c) > 0 || $upmode)) {
+ if ($a && $c && (&compare_versions($a, $c) > 0 || $upmode ||
+ $in{'mode'} eq 'held')) {
# An update is available
- $msg = "".
- &text('index_new', $a->{'version'})."";
- $need = 1;
+ $msg = $a->{'held'} ?
+ "".
+ &text('index_held', $c->{'version'}, $a->{'version'}).
+ "" :
+ "".
+ &text('index_new', $a->{'version'})."";
+ $need = $a->{'held'} ? 0 : 1;
next if ($in{'mode'} eq 'security' && !$a->{'security'});
next if ($in{'mode'} ne 'updates' &&
$in{'mode'} ne 'current' &&
- $in{'mode'} ne 'security');
+ $in{'mode'} ne 'security' &&
+ $in{'mode'} ne 'held');
}
elsif ($a && !$c) {
# Could be installed, but isn't currently
@@ -143,18 +153,20 @@ if ($in{'mode'} eq 'new' && !$in{'search'}) {
}
else {
# Show the packages, if any
+ $update_label = $in{'mode'} eq 'new' ? $text{'index_install'} :
+ $in{'mode'} eq 'held' ? $text{'index_updateheld'} :
+ $text{'index_update'};
if (@rows) {
print &text('index_count', scalar(@rows)),"
\n";
print &ui_form_start("update.cgi", "post");
- print &ui_submit($in{'mode'} eq 'new' ? $text{'index_install'}
- : $text{'index_update'}, "ok_top" );
+ print &ui_submit($update_label, "ok_top" );
print &ui_submit($text{'index_refresh'}, "refresh_top"), "
";
}
+ @buttons = ( [ "ok", $update_label ] );
+ push(@buttons, [ "refresh", $text{'index_refresh'} ]);
print &ui_form_columns_table(
"",
- [ [ "ok", $in{'mode'} eq 'new' ? $text{'index_install'}
- : $text{'index_update'} ],
- [ "refresh", $text{'index_refresh'} ] ],
+ \@buttons,
1,
undef,
[ [ "mode", $in{'mode'} ],
@@ -296,4 +308,3 @@ if ($has_repos) {
print &ui_tabs_end(1);
&ui_print_footer("/", $text{'index'});
-
diff --git a/package-updates/lang/en b/package-updates/lang/en
index df86114cf..c865d450a 100644
--- a/package-updates/lang/en
+++ b/package-updates/lang/en
@@ -6,8 +6,10 @@ index_source=Source
index_bad2=Update to version $1 not yet available : $1
index_bad=Update to version $1 advised : $2
index_new=New version $1
+index_held=Held at version $1; version $2 is available
index_ok=Running latest $1
index_update=Update Selected Packages
+index_updateheld=Update Selected Held Packages
index_install=Install Selected Packages
index_return=package list
index_header=Scheduled checking options
@@ -39,6 +41,7 @@ index_webmintheme=Webmin theme $1
index_mode=States to display:
index_mode_current=Installed
index_mode_updates=Only updates
+index_mode_held=Held updates
index_mode_new=Only new
index_mode_security=Only security updates
index_allsel=Packages to show:
@@ -46,6 +49,7 @@ index_all_0=Only Virtualmin related
index_all_1=All packages
index_none_all=No packages managed by an update system were found on your system!
index_none_updates=No packages available to be updated were found.
+index_none_held=No held packages with available updates were found.
index_none_both=No new packages or packages available to be updated were found.
index_none_new=No new packages for installation were found.
index_none_security=No packages available for security updates were found.
@@ -100,6 +104,10 @@ update_rusure=Are you sure you wish to install the $1 packages listed below? Thi
update_oldver=Current version
update_newver=New version
update_confirm=Install Now
+update_confirmheld=Update Held Packages
+update_heldnote=These packages are held. This action explicitly updates them once, and leaves them held for future updates.
+update_enotheld=Package $1 is not currently held by APT
+update_enoheldops=No update operation was found for the selected held packages. Refresh the package list and try again.
update_none=None
update_ops=Building complete list of packages ..
update_rebootdesc=One of the installed packages requires a reboot to be fully applied.
@@ -114,6 +122,8 @@ log_schedup=Background installed $1 updated packages
log_sched=Enabled scheduled updates
log_unsched=Disabled scheduled updates
log_refresh=Refreshed available packages
+log_hold=Held updates for $1 packages
+log_unhold=Unheld updates for $1 packages
log_enable_repos=Enabled $1 package repositories
log_disable_repos=Disabled $1 package repositories
log_delete_repos=Deleted $1 package repositories
@@ -140,6 +150,10 @@ view_source=Installation source
view_changelog=Changelog for available version
view_software=Manage Package
view_update=Update Package
+view_updateheld=Update Held Package
+view_hold=Hold Package
+view_unhold=Unhold Package
+view_held=Held at version $1
view_install=Install Package
system_yum=YUM
@@ -159,4 +173,12 @@ repos_title=Delete Repositories
repos_rusure=Are you sure you want to delete the $1 selected package repositories? Packages installed from them will still be available, but may not be updatable.
repos_ok=Delete Now
+hold_enotsupported=The active package update system does not support package holds
+hold_enone=No packages were selected
+hold_enotinstalled=Package $1 is not installed
+hold_enotheld=Package $1 is not currently held
+hold_esystem=Package $1 is not managed by the active package update system
+hold_efailed=Failed to hold packages: $1
+unhold_efailed=Failed to unhold packages: $1
+
__norefs=1
diff --git a/package-updates/log_parser.pl b/package-updates/log_parser.pl
index fed1dbbd4..c5f7618ff 100644
--- a/package-updates/log_parser.pl
+++ b/package-updates/log_parser.pl
@@ -21,6 +21,9 @@ elsif ($action eq 'update') {
elsif ($action eq 'schedup') {
return &text('log_schedup', $object);
}
+elsif ($action eq 'hold' || $action eq 'unhold') {
+ return &text('log_'.$action, $object);
+ }
elsif ($action eq 'sched') {
return $text{$object ? 'log_sched' : 'log_unsched'};
}
diff --git a/package-updates/package-updates-lib.pl b/package-updates/package-updates-lib.pl
index 54eed7eb2..9612d7397 100644
--- a/package-updates/package-updates-lib.pl
+++ b/package-updates/package-updates-lib.pl
@@ -21,6 +21,7 @@ eval "use WebminCore;";
$available_cache_file = &cache_file_path("available.cache");
$current_cache_file = &cache_file_path("current.cache");
$updates_cache_file = &cache_file_path("updates.cache");
+$held_updates_cache_file = &cache_file_path("held-updates.cache");
$cron_cmd = "$module_config_directory/update.pl";
$yum_cache_file = &cache_file_path("yumcache");
@@ -308,30 +309,74 @@ sub supports_updates_available
return defined(&software::update_system_updates);
}
-# updates_available(no-cache)
+# supports_package_holds()
+# Returns true if the current update system can list and change package holds.
+sub supports_package_holds
+{
+return defined(&software::list_update_system_holds) &&
+ defined(&software::update_system_hold);
+}
+
+# list_package_holds()
+# Returns the package names currently held by the update system.
+sub list_package_holds
+{
+return ( ) if (!&supports_package_holds());
+return &software::list_update_system_holds();
+}
+
+# package_is_held(package, [holds])
+# Returns true if a package is in a supplied or freshly-read list of holds.
+sub package_is_held
+{
+my ($name, $holds) = @_;
+my @holds = $holds ? @$holds : &list_package_holds();
+return 1 if (grep { $_ eq $name } @holds);
+if ($software::update_system eq 'apt' &&
+ defined(&software::strip_apt_package_arch)) {
+ my $base = &software::strip_apt_package_arch($name);
+ return 1 if (grep {
+ &software::strip_apt_package_arch($_) eq $base
+ } @holds);
+ }
+return 0;
+}
+
+# update_package_holds(&packages, hold)
+# Holds or unholds packages. Returns undef on success, or an error message.
+sub update_package_holds
+{
+my ($packages, $hold) = @_;
+return $text{'hold_enotsupported'} if (!&supports_package_holds());
+return &software::update_system_hold($packages, $hold);
+}
+
+# updates_available(no-cache, [include-held])
# Returns an array of hash refs of package updates available, according to
# the update system, with caching.
sub updates_available
{
-my ($nocache) = @_;
-if (!scalar(@updates_available_cache)) {
- if ($nocache || &cache_expired($updates_cache_file)) {
+my ($nocache, $include_held) = @_;
+my $cache_file = $include_held ? $held_updates_cache_file :
+ $updates_cache_file;
+my $cache = $include_held ? \@held_updates_available_cache :
+ \@updates_available_cache;
+if (!scalar(@$cache)) {
+ if ($nocache || &cache_expired($cache_file)) {
# Get from original source
- @updates_available_cache = &software::update_system_updates();
- foreach my $a (@updates_available_cache) {
+ @$cache = &software::update_system_updates($include_held);
+ foreach my $a (@$cache) {
$a->{'update'} = $a->{'name'};
$a->{'system'} = $software::update_system;
}
- &write_cache_file($updates_cache_file,
- \@updates_available_cache);
+ &write_cache_file($cache_file, $cache);
}
else {
# Use on-disk cache
- @updates_available_cache =
- &read_cache_file($updates_cache_file);
+ @$cache = &read_cache_file($cache_file);
}
}
-return @updates_available_cache;
+return @$cache;
}
# package_install(package-name, [system], [new-install], [flags])
@@ -343,12 +388,14 @@ my ($name, $system, $install, $flags) = @_;
$system ||= $software::update_system;
my @rv;
my $pkg;
+my $include_held = $system eq 'apt' && defined($flags) &&
+ $flags eq '--allow-change-held-packages';
# First get from list of updates
($pkg) = grep { $_->{'update'} eq $name &&
($_->{'system'} eq $system || !$system) }
sort { &compare_versions($b, $a) }
- &list_possible_updates(0);
+ &list_possible_updates(0, 0, $include_held);
if (!$pkg) {
# Then try list of all available packages
($pkg) = grep { $_->{'update'} eq $name &&
@@ -458,14 +505,14 @@ if (defined(&software::update_system_operations)) {
return ( );
}
-# list_possible_updates([nocache], [nocache-no-data])
+# list_possible_updates([nocache], [nocache-no-data], [include-held])
# Returns a list of updates that are available. Each element in the array
# is a hash ref containing a name, version, description and severity flag.
# Intended for calling from themes. Nocache 0=cache everything, 1=flush all
# caches, 2=flush only current. Nocache-no-data prohibits collecting data
sub list_possible_updates
{
-my ($nocache, $nocache_no_data) = @_;
+my ($nocache, $nocache_no_data, $include_held) = @_;
my @rv;
return @rv if ($nocache_no_data);
my @current = &list_current($nocache);
@@ -476,9 +523,10 @@ if (&supports_updates_available()) {
foreach my $c (@current) {
$currentmap{$c->{'name'},$c->{'system'}} ||= $c;
}
- foreach my $a (&updates_available($nocache == 1)) {
+ foreach my $a (&updates_available($nocache == 1, $include_held)) {
my $c = $currentmap{$a->{'name'},$a->{'system'}};
next if (!$c);
+ next if ($a->{'held'} && !$include_held);
next if ($a->{'version'} eq $c->{'version'} &&
$a->{'epoch'} eq $c->{'epoch'});
push(@rv, { 'name' => $a->{'name'},
@@ -489,6 +537,7 @@ if (&supports_updates_available()) {
'epoch' => $a->{'epoch'},
'oldepoch' => $c->{'epoch'},
'security' => $a->{'security'},
+ 'held' => $a->{'held'},
'source' => $a->{'source'},
'desc' => $c->{'desc'} || $a->{'desc'} });
}
@@ -700,10 +749,13 @@ sub flush_package_caches
{
unlink($current_cache_file);
unlink($updates_cache_file);
+unlink($held_updates_cache_file);
unlink($available_cache_file);
unlink($available_cache_file.'0');
unlink($available_cache_file.'1');
@packages_available_cache = ( );
+@updates_available_cache = ( );
+@held_updates_available_cache = ( );
%read_cache_file_cache = ( );
}
@@ -713,6 +765,8 @@ unlink($available_cache_file.'1');
sub list_for_mode
{
my ($mode, $nocache) = @_;
+return grep { $_->{'held'} }
+ &list_possible_updates($nocache, 0, 1) if ($mode eq 'held');
return $mode eq 'updates' || $mode eq 'security' ?
&list_possible_updates($nocache) : &list_available($nocache);
}
diff --git a/package-updates/save_view.cgi b/package-updates/save_view.cgi
index 2fd5f2866..fbff27020 100755
--- a/package-updates/save_view.cgi
+++ b/package-updates/save_view.cgi
@@ -8,8 +8,14 @@ if ($in{'software'}) {
&redirect("../software/edit_pack.cgi?package=".&urlize($in{'name'}).
"&version=".&urlize($in{'version'}));
}
-else {
+elsif ($in{'hold'} || $in{'unhold'}) {
+ $action = $in{'hold'} ? "hold" : "unhold";
&redirect("update.cgi?u=".&urlize($in{'name'}."/".$in{'system'}).
- "&all=$in{'all'}&mode=$in{'mode'}");
+ "&$action=1&mode=".&urlize($in{'mode'}));
+ }
+else {
+ $mode = $in{'held'} ? "held" : $in{'mode'};
+ &redirect("update.cgi?u=".&urlize($in{'name'}."/".$in{'system'}).
+ "&all=$in{'all'}&mode=".&urlize($mode));
}
diff --git a/package-updates/update.cgi b/package-updates/update.cgi
index af66b8d30..4a2b88602 100755
--- a/package-updates/update.cgi
+++ b/package-updates/update.cgi
@@ -19,7 +19,38 @@ else {
$redir =~ /\?/ ? "$redir&tab=pkgs" : "$redir?tab=pkgs";
}
-if ($in{'refresh'} || $in{'refresh_top'}) {
+$hold_action = $in{'hold'} ? 1 : $in{'unhold'} ? 0 : undef;
+if (defined($hold_action)) {
+ # Hold or unhold selected packages
+ &supports_package_holds() || &error($text{'hold_enotsupported'});
+ @holdpkgs = split(/\0/, $in{'u'});
+ @holdpkgs || &error($text{'hold_enone'});
+ @current = &list_current(1);
+ %current = map { $_->{'name'}."/".$_->{'system'}, 1 } @current;
+ @held = &list_package_holds();
+ @holdnames = ( );
+ foreach $ps (@holdpkgs) {
+ ($p, $s) = split(/\//, $ps, 2);
+ $current{$p."/".$s} || &error(&text('hold_enotinstalled', $p));
+ $s eq $software::update_system ||
+ &error(&text('hold_esystem', $p));
+ if (!$hold_action && !&package_is_held($p, \@held)) {
+ &error(&text('hold_enotheld', $p));
+ }
+ push(@holdnames, $p);
+ }
+ @holdnames = &unique(@holdnames);
+ $err = &update_package_holds(\@holdnames, $hold_action);
+ &error(&text($hold_action ? 'hold_efailed' : 'unhold_efailed', $err))
+ if ($err);
+ &flush_package_caches();
+ $logaction = $hold_action ? 'hold' : 'unhold';
+ &webmin_log($logaction, "packages", scalar(@holdnames),
+ { 'packages' => \@holdnames });
+ &redirect("index.cgi?mode=".&urlize($in{'mode'}).
+ "&search=".&urlize($in{'search'}));
+ }
+elsif ($in{'refresh'} || $in{'refresh_top'}) {
&ui_print_unbuffered_header(undef, $text{'refresh_title'}, "");
# Clear all caches
@@ -40,6 +71,21 @@ else {
# Upgrade some packages
my @pkgs = split(/\0/, $in{'u'});
@pkgs || &error($text{'update_enone'});
+ $allow_held = 0;
+ if ($in{'mode'} eq 'held') {
+ # The held-updates page is the only UI that can explicitly
+ # override an APT hold for a single update transaction.
+ &supports_package_holds() || &error($text{'hold_enotsupported'});
+ @held = &list_package_holds();
+ foreach $ps (@pkgs) {
+ ($p, $s) = split(/\//, $ps, 2);
+ $s eq 'apt' && &package_is_held($p, \@held) ||
+ &error(&text('update_enotheld', $p));
+ }
+ $allow_held = 1;
+ }
+ $install_flags = $allow_held ? '--allow-change-held-packages' :
+ $in{'flags'};
&ui_print_unbuffered_header(undef,
$in{'mode'} eq 'new' ? $text{'update_title2'} : $text{'update_title'}, "");
@@ -57,6 +103,7 @@ else {
push(@pkgnames, $p);
}
@ops = &list_package_operations(join(" ", @pkgnames), $s);
+ &error($text{'update_enoheldops'}) if (!@ops && $allow_held);
}
if (@ops) {
@@ -74,9 +121,14 @@ else {
foreach $ps (@pkgs) {
$confform .= &ui_hidden("u", $ps);
}
+ $confform .= &ui_alert_box($text{'update_heldnote'},
+ 'warn', undef, undef, '')
+ if ($allow_held && !$bottom);
$confform .= &text('update_rusure', scalar(@ops)),"
\n"
if (!$bottom);
- $confform .= &ui_form_end([ [ "confirm", $text{'update_confirm'} ] ]);
+ $confform .= &ui_form_end([ [ "confirm",
+ $allow_held ? $text{'update_confirmheld'} :
+ $text{'update_confirm'} ] ]);
};
print &$getconfform();
@@ -127,7 +179,7 @@ else {
"
\n";
print "
";
&additional_log('exec', undef, $cmd);
@@ -68,14 +81,27 @@ while() {
print &html_escape("$_");
}
close(CMD);
+local $status = $?;
+
+# Restore holds after --allow-change-held-packages, which applies to the whole
+# transaction and can clear holds on selected packages or dependencies.
+if (@rehold) {
+ local $rehold_error = &update_system_hold(\@rehold, 1);
+ if ($rehold_error) {
+ print &text('apt_reholdfailed',
+ "".&html_escape(join(" ", @rehold))."",
+ &html_escape($rehold_error)),"\n";
+ }
+ }
&reset_environment();
-if (!@rv && $config{'package_system'} ne 'debian' && !$?) {
+if (!@rv && $config{'package_system'} ne 'debian' && !$status) {
# Other systems don't list the packages installed!
@rv = @newpacks;
}
print "
\n";
-if ($?) { print "$text{'apt_failed'}\n"; } +if ($status) { print "$text{'apt_failed'}
\n"; } else { print "$text{'apt_ok'}
\n"; }
+$? = $status;
return @rv;
}
@@ -239,21 +265,13 @@ return @rv;
# Returns a list of available package updates
sub update_system_updates
{
+my ($include_holds) = @_;
&execute_command("$apt_get_command update");
-# Find held packages by dpkg
-local %holds;
-if ($config{'package_system'} eq 'debian') {
- &clean_language();
- &open_execute_command(HOLDS, "dpkg --get-selections", 1, 1);
- while(