From 8ec3054c301ec3374ed563248076ce3e154883fe Mon Sep 17 00:00:00 2001 From: kohsuke Date: Wed, 8 Sep 2010 05:36:29 +0000 Subject: [PATCH] Added downgrade support for the core and plugins. -------- Merged revisions 32975,33588,33697,34342 via svnmerge from https://www.dev.java.net/svn/hudson/branches/downgrade ........ r32975 | kohsuke | 2010-07-16 21:27:59 +0200 (Fri, 16 Jul 2010) | 1 line committed my partially modified version ........ r33588 | dienomight | 2010-08-05 09:42:36 +0200 (Thu, 05 Aug 2010) | 13 lines Added getBackupVersion (UpdateCenter) downgrade button now shows version of backup war file Added HudsonDowngradeJob (UpdateCenter) downgrading process is works now as HudsonUpgradeJob Changed rewriteHudsonWar (Lifecycle) backup file is deleted when downgrading hudson/model/Hudson/downgrade_success.jelly is no longer necessary Changed rewriteHudsonWar(WindowsServiceLifecycle) hudson run as windows service creates backup during upgrading ........ r33697 | dienomight | 2010-08-11 17:09:48 +0200 (Wed, 11 Aug 2010) | 8 lines committed changes: "plugins downgrade functionality" added: * PluginDowngradeJob in UpdateCenter * getCanDowngrade & getBackupPath & getBackupVersion in PluginWrapper * additional "previous version" column in installed.jelly (PluginManager) with downgrade button (when it's possible to downgrade) * doDowngrade in PluginManager * deployBackup in UpdateSite ........ r34342 | dienomight | 2010-09-03 09:52:13 +0200 (Fri, 03 Sep 2010) | 12 lines Code improvements: jelly files: *I put text inside ${%} so it would be available for translation. UpdateCenter: *changed getCanDowngrade to isDowngradable *getBackupVersion use JarFile class to get version of .war backup PluginWrapper: *changed getBackupPath to getBackupFile *getBackupVersion use JarFile class to get version of plugin backup PluginManager: *changed condition in doDowngrade method ........ git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@34543 71c3de6d-444a-0410-be80-ed276b4c234a --- core/src/main/java/hudson/PluginManager.java | 1 + core/src/main/java/hudson/PluginWrapper.java | 35 +++- .../main/java/hudson/lifecycle/Lifecycle.java | 10 + .../lifecycle/WindowsServiceLifecycle.java | 11 ++ .../main/java/hudson/model/UpdateCenter.java | 172 ++++++++++++++++++ .../main/java/hudson/model/UpdateSite.java | 18 ++ .../hudson/PluginManager/installed.jelly | 8 + .../hudson/PluginManager/installed.properties | 2 +- .../hudson/model/Hudson/downgrade.jelly | 34 ++++ .../hudson/model/Hudson/downgrade.properties | 1 + .../hudson/model/Hudson/manage.jelly | 2 + 11 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 core/src/main/resources/hudson/model/Hudson/downgrade.jelly create mode 100644 core/src/main/resources/hudson/model/Hudson/downgrade.properties diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 23f19f683f..c742d5808e 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -494,6 +494,7 @@ public abstract class PluginManager extends AbstractModelObject { } rsp.sendRedirect("../updateCenter/"); } + /** * Bare-minimum configuration mechanism to change the update center. diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index b196eff2da..a1090dea80 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -37,7 +37,6 @@ import java.io.OutputStream; import java.io.Closeable; import java.net.URL; import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; import java.util.jar.Manifest; import java.util.logging.Logger; @@ -47,6 +46,9 @@ import org.apache.commons.logging.LogFactory; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; +import java.util.Enumeration; +import java.util.jar.JarFile; + /** * Represents a Hudson plug-in and associated control information * for Hudson to control {@link Plugin}. @@ -464,6 +466,37 @@ public class PluginWrapper implements Comparable { return shortName.compareToIgnoreCase(pw.shortName); } + /** + * returns true if backup of previous version of plugin exists + */ + public boolean isDowngradable() { + return getBackupFile().exists(); + } + + /** + * Where is the backup file? + */ + public File getBackupFile() { + return new File(Hudson.getInstance().getRootDir(),"plugins/"+getShortName() + ".bak"); + } + + /** + * returns the version of the backed up plugin, + * or null if there's no back up. + */ + public String getBackupVersion() { + if (getBackupFile().exists()) { + try { + JarFile backupPlugin = new JarFile(getBackupFile()); + return backupPlugin.getManifest().getMainAttributes().getValue("Plugin-Version"); + } catch (IOException e) { + LOGGER.log(WARNING, "Failed to get backup version ", e); + return null; + } + } else { + return null; + } + } // // // Action methods diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java index cd2d747aba..e138a565ff 100644 --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java @@ -139,7 +139,17 @@ public abstract class Lifecycle implements ExtensionPoint { // but let's be defensive if(dest==null) throw new IOException("hudson.war location is not known."); + // backing up the old hudson.war before it gets lost due to upgrading + // (newly downloaded hudson.war and 'backup' (hudson.war.tmp) are the same files + // unless we are trying to rewrite hudson.war by a backup itself + File bak = new File(dest.getPath() + ".bak"); + if (!by.equals(bak)) + FileUtils.copyFile(dest, bak); + FileUtils.copyFile(by, dest); + // we don't want to keep backup if we are downgrading + if (by.equals(bak)&&bak.exists()) + bak.delete(); } /** diff --git a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java index dfbd7ef688..beaf1206c5 100644 --- a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java +++ b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java @@ -87,6 +87,17 @@ public class WindowsServiceLifecycle extends Lifecycle { */ @Override public void rewriteHudsonWar(File by) throws IOException { + File dest = getHudsonWar(); + // this should be impossible given the canRewriteHudsonWar method, + // but let's be defensive + if(dest==null) throw new IOException("hudson.war location is not known."); + + // backing up the old hudson.war before its lost due to upgrading + // unless we are trying to rewrite hudson.war by a backup itself + File bak = new File(dest.getPath() + ".bak"); + if (!by.equals(bak)) + FileUtils.copyFile(dest, bak); + File rootDir = Hudson.getInstance().getRootDir(); File copyFiles = new File(rootDir,"hudson.copies"); diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 58c69d7cda..b97ef60589 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -70,10 +70,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import org.acegisecurity.context.SecurityContextHolder; + /** * Controls update center capability. * @@ -282,6 +284,45 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { rsp.sendRedirect2("."); } + /** + * Returns true if backup of hudson.war exists on the hard drive + */ + public boolean isDowngradable() { + return new File(Lifecycle.get().getHudsonWar() + ".bak").exists(); + } + + /** + * Performs hudson downgrade. + */ + public void doDowngrade(StaplerResponse rsp) throws IOException, ServletException { + requirePOST(); + Hudson.getInstance().checkPermission(Hudson.ADMINISTER); + if(!isDowngradable()) { + sendError("Hudson downgrade is not possible, probably backup does not exist"); + return; + } + + HudsonDowngradeJob job = new HudsonDowngradeJob(getCoreSource(), Hudson.getAuthentication()); + LOGGER.info("Scheduling the core downgrade"); + addJob(job); + rsp.sendRedirect2("."); + } + + /** + * Returns String with version of backup .war file, + * if the file does not exists returns null + */ + public String getBackupVersion() + { + try { + JarFile backupWar = new JarFile(new File(Lifecycle.get().getHudsonWar().getParentFile(), "hudson.war.bak")); + return backupWar.getManifest().getMainAttributes().getValue("Hudson-Version"); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to read backup version ", e); + return null;} + + } + /*package*/ synchronized Future addJob(UpdateCenterJob job) { // the first job is always the connectivity check if (sourcesUsed.add(job.site)) @@ -892,6 +933,87 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } } + /** + * Represents the state of the downgrading activity of plugin. + */ + public final class PluginDowngradeJob extends DownloadJob { + /** + * What plugin are we trying to install? + */ + public final Plugin plugin; + + private final PluginManager pm = Hudson.getInstance().getPluginManager(); + + public PluginDowngradeJob(Plugin plugin, UpdateSite site, Authentication auth) { + super(site, auth); + this.plugin = plugin; + } + + protected URL getURL() throws MalformedURLException { + return new URL(plugin.url); + } + + protected File getDestination() { + File baseDir = pm.rootDir; + return new File(baseDir, plugin.name + ".hpi"); + } + + protected File getBackup() + { + File baseDir = pm.rootDir; + return new File(baseDir, plugin.name + ".bak"); + } + + public String getName() { + return plugin.getDisplayName(); + } + + @Override + public void run() { + try { + LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName()); + + _run(); + + LOGGER.info("Downgrade successful: "+getName()); + status = new Success(); + onSuccess(); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e); + status = new Failure(e); + } + } + + @Override + protected void _run() throws IOException { + File dst = getDestination(); + File backup = getBackup(); + + config.install(this, backup, dst); + } + + /** + * Called to overwrite + * current version with backup file + */ + @Override + protected void replace(File dst, File backup) throws IOException { + dst.delete(); // any failure up to here is no big deal + if(!backup.renameTo(dst)) { + throw new IOException("Failed to rename "+backup+" to "+dst); + } + } + + protected void onSuccess() { + pm.pluginUploaded = true; + } + + @Override + public String toString() { + return super.toString()+"[plugin="+plugin.title+"]"; + } + } + /** * Represents the state of the upgrade activity of Hudson core. */ @@ -922,6 +1044,56 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } } + public final class HudsonDowngradeJob extends DownloadJob { + public HudsonDowngradeJob(UpdateSite site, Authentication auth) { + super(site, auth); + } + + protected URL getURL() throws MalformedURLException { + return new URL(site.getData().core.url); + } + + protected File getDestination() { + return Lifecycle.get().getHudsonWar(); + } + + public String getName() { + return "hudson.war"; + } + protected void onSuccess() { + status = new Success(); + } + @Override + public void run() { + try { + LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName()); + + _run(); + + LOGGER.info("Downgrading successful: "+getName()); + status = new Success(); + onSuccess(); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e); + status = new Failure(e); + } + } + + @Override + protected void _run() throws IOException { + + File backup = new File(Lifecycle.get().getHudsonWar() + ".bak"); + File dst = getDestination(); + + config.install(this, backup, dst); + } + + @Override + protected void replace(File dst, File src) throws IOException { + Lifecycle.get().rewriteHudsonWar(src); + } + } + public static final class PluginEntry implements Comparable { public Plugin plugin; public String category; diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index dd3b5135c7..9709481993 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -35,6 +35,7 @@ import hudson.util.VersionNumber; import static hudson.util.TimeUnit2.DAYS; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.jvnet.hudson.crypto.CertificateUtil; @@ -67,6 +68,7 @@ import java.security.cert.TrustAnchor; import com.trilead.ssh2.crypto.Base64; import javax.servlet.ServletContext; +import javax.servlet.ServletException; /** @@ -601,6 +603,14 @@ public class UpdateSite { return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Hudson.getAuthentication())); } + /** + * Schedules the downgrade of this plugin. + */ + public Future deployBackup() { + Hudson.getInstance().checkPermission(Hudson.ADMINISTER); + UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); + return uc.addJob(uc.new PluginDowngradeJob(this, UpdateSite.this, Hudson.getAuthentication())); + } /** * Making the installation web bound. */ @@ -608,6 +618,14 @@ public class UpdateSite { deploy(); rsp.sendRedirect2("../.."); } + + /** + * Performs the downgrade of the plugin. + */ + public void doDowngrade(StaplerResponse rsp) throws IOException { + deployBackup(); + rsp.sendRedirect("../updateCenter/"); + } } private static final long DAY = DAYS.toMillis(1); diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index 4564f188fc..5e30cedcfb 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.jelly +++ b/core/src/main/resources/hudson/PluginManager/installed.jelly @@ -42,6 +42,7 @@ THE SOFTWARE. ${%Enabled} ${%Name} ${%Version} + ${%Previously installed version} ${%Pinned} @@ -68,6 +69,13 @@ THE SOFTWARE. ${p.version} + + +
+ + +
+ diff --git a/core/src/main/resources/hudson/PluginManager/installed.properties b/core/src/main/resources/hudson/PluginManager/installed.properties index 0bb76aeaee..b901fab6c5 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.properties +++ b/core/src/main/resources/hudson/PluginManager/installed.properties @@ -20,4 +20,4 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. wiki.url=http://wiki.hudson-ci.org/display/HUDSON/Pinned+Plugins - +downgradeTo=Downgrade to {0} \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Hudson/downgrade.jelly b/core/src/main/resources/hudson/model/Hudson/downgrade.jelly new file mode 100644 index 0000000000..6a76ae2f60 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/downgrade.jelly @@ -0,0 +1,34 @@ + + + +
+ +
+
${%Restore the previous version of Hudson} + + +
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/downgrade.properties b/core/src/main/resources/hudson/model/Hudson/downgrade.properties new file mode 100644 index 0000000000..055767e4be --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/downgrade.properties @@ -0,0 +1 @@ +buttonText=Downgrade to {0} diff --git a/core/src/main/resources/hudson/model/Hudson/manage.jelly b/core/src/main/resources/hudson/model/Hudson/manage.jelly index 45d6f1dc05..b8546a16f6 100644 --- a/core/src/main/resources/hudson/model/Hudson/manage.jelly +++ b/core/src/main/resources/hudson/model/Hudson/manage.jelly @@ -63,6 +63,8 @@ THE SOFTWARE.
+ + ${%Configure global settings and paths.} -- GitLab