From 57d7a631a9513d949d812b0f07537303cff145d7 Mon Sep 17 00:00:00 2001 From: Alex Brett 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 --- 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 @@ 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("") : Lang("OK"), Lang("") : 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 @@ def HandleMethodChoice(self, inChoice, dryRun): 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 @@ def __init__(self): } 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()