From 20c02765c30bdb7e14e3c020b2d7e956f88c8150 Mon Sep 17 00:00:00 2001
From: Alex Brett <alex.brett@cloud.com>
Date: Tue, 9 Jul 2024 16:45:16 +0000
Subject: [PATCH] Updates to Portable SR functionality

Modifies functionality to understand the use of legacy format VDIs:
- Adds an option to search for a legacy format VDI if a new one is not found
- Handles the case where multiple legacy VDIs are identified
- Passes the appropriate option to `xe-restore-metadata` when needed

This is XSA-459 / CVE-2024-31144

Also update the subprocess calls to be compatible with python3

Signed-off-by: Alex Brett <alex.brett@cloud.com>
---
 plugins-base/XSFeatureDRRestore.py | 172 ++++++++++++++++++++++-------
 1 file changed, 135 insertions(+), 37 deletions(-)

diff --git a/plugins-base/XSFeatureDRRestore.py b/plugins-base/XSFeatureDRRestore.py
index 2cadde5..ea7fd7c 100644
--- a/plugins-base/XSFeatureDRRestore.py
+++ b/plugins-base/XSFeatureDRRestore.py
@@ -19,14 +19,93 @@ if __name__ == "__main__":
 from XSConsoleStandard import *
 import subprocess
 
+def _listBackups(sr_uuid, vdi_uuid, legacy=False):
+    # list the available backups
+    Layout.Inst().TransientBanner(Lang("Found VDI, retrieving available backups..."))
+    command = ["%s/xe-restore-metadata" % (Config.Inst().HelperPath(),), "-l", "-u", sr_uuid, "-x", vdi_uuid]
+    if legacy:
+        command.append("-o")
+    cmd = subprocess.Popen(command,
+                           stdout = subprocess.PIPE,
+                           stderr = subprocess.PIPE,
+                           universal_newlines = True)
+    output, errput = cmd.communicate()
+    status = cmd.returncode
+    if status != 0:
+        raise Exception("(%s,%s)" % (output,errput))
+    Layout.Inst().PushDialogue(DRRestoreSelection(output, vdi_uuid, sr_uuid, legacy=legacy))
+
+class DRRestoreVDISelection(Dialogue):
+    def __init__(self, sr_uuid, vdi_uuids):
+        Dialogue.__init__(self)
+
+        choices = []
+
+        self.sr_uuid = sr_uuid
+        self.vdi_uuids = vdi_uuids
+        index = 0
+        for choice in self.vdi_uuids:
+            cdef = ChoiceDef(choice, lambda i=index: self.HandleVDIChoice(i))
+            index = index + 1
+            choices.append(cdef)
+
+        self.testMenu = Menu(self, None, "", choices)
+        self.ChangeState('LISTVDIS')
+
+    def BuildPane(self):
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang('Restore Virtual Machine Metadata'))
+        pane.AddBox()
+
+    def UpdateFieldsLISTVDIS(self):
+        pane = self.Pane()
+        pane.ResetFields()
+
+        pane.TitleSet("Available Metadata VDIs")
+        pane.AddTitleField(Lang("Select Metadata VDI to Restore From"))
+        pane.AddWarningField(Lang("You should only restore metadata from a trustworthy VDI; loading untrustworthy metadata may put your system at risk"))
+        pane.AddMenuField(self.testMenu)
+        pane.AddKeyHelpField( { Lang("<Enter>") : Lang("OK"), Lang("<Esc>") : Lang("Cancel") } )
+
+    def UpdateFields(self):
+        self.Pane().ResetPosition()
+        getattr(self, 'UpdateFields'+self.state)() # Despatch method named 'UpdateFields'+self.state
+
+    def ChangeState(self, inState):
+        self.state = inState
+        self.BuildPane()
+        self.UpdateFields()
+
+    def HandleVDIChoice(self, inChoice):
+        _listBackups(self.sr_uuid, self.vdi_uuids[inChoice], legacy=True)
+
+    def HandleKeyLISTVDIS(self, inKey):
+        handled = self.testMenu.HandleKey(inKey)
+        if not handled and inKey == 'KEY_LEFT':
+            Layout.Inst().PopDialogue()
+            handled = True
+        return handled
+
+    def HandleKey(self, inKey):
+        handled = False
+        if hasattr(self, 'HandleKey'+self.state):
+            handled = getattr(self, 'HandleKey'+self.state)(inKey)
+
+        if not handled and inKey == 'KEY_ESCAPE':
+            Layout.Inst().PopDialogue()
+            handled = True
+
+        return handled
+
 class DRRestoreSelection(Dialogue):
 
-    def __init__(self, date_choices, vdi_uuid, sr_uuid):
+    def __init__(self, date_choices, vdi_uuid, sr_uuid, legacy=False):
         Dialogue.__init__(self)
 
         choices = []
         self.vdi_uuid = vdi_uuid
         self.sr_uuid = sr_uuid
+        self.legacy = legacy
         self.date_choices = date_choices.splitlines()
         index = 0
         for choice in self.date_choices:
@@ -86,14 +165,19 @@ class DRRestoreSelection(Dialogue):
             Layout.Inst().PushDialogue(InfoDialogue(Lang("Internal Error, unexpected choice: " + inChoice)))
         else:
             chosen_mode = inChoice
-            if dryRun:
-              dry_flag="-n "
-            else:
-              dry_flag=""
             Layout.Inst().TransientBanner(Lang("Restoring VM Metadata.  This may take a few minutes..."))
-            command = "%s/xe-restore-metadata -y %s -u %s -x %s -d %s -m %s" % (Config.Inst().HelperPath(), dry_flag, self.sr_uuid, self.vdi_uuid, self.chosen_date, chosen_mode)
-            status, output = commands.getstatusoutput(command)
-            status = os.WEXITSTATUS(status)
+            command = ["%s/xe-restore-metadata" % (Config.Inst().HelperPath(),), "-y", "-u", self.sr_uuid, "-x", self.vdi_uuid, "-d", self.chosen_date, "-m", chosen_mode]
+            if dryRun:
+                command.append("-n")
+            if self.legacy:
+                command.append("-o")
+
+            cmd = subprocess.Popen(command,
+                                stdout = subprocess.PIPE,
+                                stderr = subprocess.STDOUT,
+                                universal_newlines = True)
+            output, _ = cmd.communicate()
+            status = cmd.returncode
             Layout.Inst().PopDialogue()
             if status == 0:
                 Layout.Inst().PushDialogue(InfoDialogue(Lang("Metadata Restore Succeeded: ") + output))
@@ -137,40 +221,54 @@ class DRRestoreDialogue(SRDialogue):
         }
         SRDialogue.__init__(self) # Must fill in self.custom before calling __init__
 
+    def _searchForVDI(self, sr_uuid, legacy=False):
+        # probe for the restore VDI UUID
+        command = ["%s/xe-restore-metadata" % (Config.Inst().HelperPath(),), "-p", "-u", sr_uuid]
+        if legacy:
+            command.append("-o")
+        cmd = subprocess.Popen(command,
+                               stdout = subprocess.PIPE,
+                               stderr = subprocess.PIPE,
+                               universal_newlines = True)
+        output, errput = cmd.communicate()
+        status = cmd.returncode
+        if status != 0:
+            raise Exception("(%s,%s)" % (output,errput))
+        if len(output) == 0:
+            raise Exception(errput)
+        return output
+
+    def _earlierConfirmHandler(self, inYesNo, sr_uuid):
+        if inYesNo == 'y':
+            Layout.Inst().TransientBanner(Lang("Searching for backup VDI...\n\nCtrl-C to abort"))
+            try:
+                vdi_uuids = [v.strip() for v in self._searchForVDI(sr_uuid, legacy=True).splitlines()]
+                if len(vdi_uuids) == 1:
+                    _listBackups(sr_uuid, vdi_uuids[0], legacy=True)
+                else:
+                    Layout.Inst().PushDialogue(DRRestoreVDISelection(sr_uuid, vdi_uuids))
+                    return
+            except Exception as e:
+                Layout.Inst().PushDialogue(InfoDialogue( Lang("Metadata Restore failed: ")+Lang(e)))
+        else:
+            Layout.Inst().PushDialogue(InfoDialogue( Lang("Metadata Restore failed: a backup VDI could not be found")))
+        Data.Inst().Update()
+
     def DoAction(self, inSR):
         Layout.Inst().PopDialogue()
         Layout.Inst().TransientBanner(Lang("Searching for backup VDI...\n\nCtrl-C to abort"))
         sr_uuid = inSR['uuid']
         try:
-            # probe for the restore VDI UUID
-            command = "%s/xe-restore-metadata -p -u %s" % (Config.Inst().HelperPath(), sr_uuid)
-            cmd = subprocess.Popen(command,
-                                   stdout = subprocess.PIPE,
-                                   stderr = subprocess.PIPE,
-                                   shell = True)
-            output = "".join(cmd.stdout).strip()
-            errput = "".join(cmd.stderr).strip()
-            status = cmd.wait()
-            if status != 0:
-                raise Exception("(%s,%s)" % (output,errput))
-            if len(output) == 0:
-                raise Exception(errput)
-            vdi_uuid = output
-
-            # list the available backups
-            Layout.Inst().TransientBanner(Lang("Found VDI, retrieving available backups..."))
-            command = "%s/xe-restore-metadata -l -u %s -x %s" % (Config.Inst().HelperPath(), sr_uuid, vdi_uuid)
-            cmd = subprocess.Popen(command,
-                                   stdout = subprocess.PIPE,
-                                   stderr = subprocess.PIPE,
-                                   shell = True)
-            output = "".join(cmd.stdout).strip()
-            errput = "".join(cmd.stderr).strip()
-            status = cmd.wait()
-            if status != 0:
-                raise Exception("(%s,%s)" % (output,errput))
-            Layout.Inst().PushDialogue(DRRestoreSelection(output, vdi_uuid, sr_uuid))
-        except Exception, e:
+            try:
+                vdi_uuid = self._searchForVDI(sr_uuid).strip()
+            except Exception as e:
+                # We could not uniquely identify the required VDI, ask the user if they want to check for legacy ones
+                message = Lang("A backup VDI could not be positively identified. Do you wish to scan for backup VDIs created with earlier versions (Warning: this operation should only be performed if you trust the contents of all VDIs in this storage repository)?")
+                Layout.Inst().PushDialogue(QuestionDialogue(message, lambda x: self._earlierConfirmHandler(x, sr_uuid)))
+                return
+
+            _listBackups(sr_uuid, vdi_uuid)
+        except Exception as e:
             Layout.Inst().PushDialogue(InfoDialogue( Lang("Metadata Restore failed: ")+Lang(e)))
         Data.Inst().Update()
 
-- 
2.25.1

