Repository: xapi-project/xsconsole
Pull request #59: CP-54331 Enable XSConsole to Configure Auto mode
Author: login: LunfanZhang
Created: 2025-05-19T02:26:47Z | Merged: 2025-05-29T02:33:45Z

This PR aims to allows users to configure SSH Auto Mode. This functionality enables the system to automatically enable or disable SSH service based on XAPI conditions.


![auto-mode-3-2](https://github.com/user-attachments/assets/622f3c94-dc1d-431a-a2d7-ea427e67f7f3)
![auto-mode-3-3](https://github.com/user-attachments/assets/bf3a2fff-d34e-45ad-a3a3-845d3a1a1d3b)




Diff:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d238968..b026c19 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -15,14 +15,14 @@ env:
 jobs:
   python-checks:
     name: minimaltest (pre-commit)
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     steps:
       - name: Checkout code
         uses: actions/checkout@v3
 
       - name: Install dependencies
         run: |
-          #: Install Python 2.7 from Ubuntu 20.04 using apt-get install
+          #: Install Python 2.7 from Ubuntu 22.04 using apt-get install
           sudo apt-get update && sudo apt-get install -y python2
           curl -sSL https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py
           python2 get-pip.py
diff --git a/XSConsoleData.py b/XSConsoleData.py
index 9677ca2..960e727 100644
--- a/XSConsoleData.py
+++ b/XSConsoleData.py
@@ -1099,6 +1099,11 @@ def DisableSSH(self):
         self.RequireSession()
         self.session.xenapi.host.disable_ssh(self.host.opaqueref())
 
+    def SetSSHAutoMode(self, inMode):
+        Auth.Inst().AssertAuthenticatedOrPasswordUnset()
+        self.RequireSession()
+        self.session.xenapi.host.set_ssh_auto_mode(self.host.opaqueref(), inMode)
+
     def Ping(self,  inDest):
         # Must be careful that no unsanitised data is passed to the command
         if not re.match(r'[0-9a-zA-Z][-0-9a-zA-Z.]*$',  inDest):
diff --git a/plugins-base/XSFeatureRemoteShell.py b/plugins-base/XSFeatureRemoteShell.py
index 17d8fe2..a4e4d3e 100644
--- a/plugins-base/XSFeatureRemoteShell.py
+++ b/plugins-base/XSFeatureRemoteShell.py
@@ -18,6 +18,68 @@
 
 from XSConsoleStandard import *
 
+DISABLE = 0
+ENABLE = 1
+AUTO = 2
+
+class DisableOptionsDialogue(Dialogue):
+    def __init__(self):
+        Dialogue.__init__(self)
+
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang("Auto-mode Options"))
+        pane.AddBox()
+
+        self.disableMenu = Menu(self, None, Lang("Disable Options"), [
+            ChoiceDef(Lang("Standard Disable"), lambda: self.HandleChoice(DISABLE)),
+            ChoiceDef(Lang("Disable and Turn on Auto-mode"), lambda: self.HandleChoice(AUTO))
+            ])
+
+        self.UpdateFields()
+
+    def UpdateFields(self):
+        pane = self.Pane()
+        pane.ResetFields()
+
+        pane.AddTitleField(Lang("When auto-mode is enabled: SSH is disabled when XAPI is running properly, "
+                        "and enabled when XAPI is down for emergency troubleshooting."))
+        pane.AddMenuField(self.disableMenu)
+        pane.AddKeyHelpField( { Lang("<Enter>") : Lang("OK"), Lang("<Esc>") : Lang("Cancel") } )
+
+    def HandleKey(self, inKey):
+        handled = self.disableMenu.HandleKey(inKey)
+
+        if not handled and inKey == 'KEY_ESCAPE':
+            Layout.Inst().PopDialogue()
+            handled = True
+
+        return handled
+
+    def HandleChoice(self, inChoice):
+        data = Data.Inst()
+        Layout.Inst().PopDialogue()
+
+        try:
+            message = Lang("Configuration Successful")
+            data.DisableSSH()
+
+            if ShellPipe(['/sbin/pidof', 'sshd-session']).CallRC() == 0: # If PIDs are available
+                message = Lang("New connections via the remote shell are now disabled, but there are "
+                    "ssh connections still ongoing.  If necessary, use 'killall sshd-session' from the Local "
+                    "Command Shell to terminate them.")
+            if inChoice == AUTO:
+                data.SetSSHAutoMode(True)
+                message = Lang("Auto configuration for ssh xapi console has been set. New connections via "
+                        "the remote shell will be enabled/disabled based on the state of the xapi")
+
+            Layout.Inst().PushDialogue(InfoDialogue(message))
+
+        except Exception as e:
+            Layout.Inst().PushDialogue(InfoDialogue(Lang("Failed: ")+Lang(e)))
+
+        data.Update()
+
+
 class RemoteShellDialogue(Dialogue):
     def __init__(self):
         Dialogue.__init__(self)
@@ -27,8 +89,8 @@ def __init__(self):
         pane.AddBox()
 
         self.remoteShellMenu = Menu(self, None, Lang("Configure Remote Shell"), [
-            ChoiceDef(Lang("Enable"), lambda: self.HandleChoice(True) ),
-            ChoiceDef(Lang("Disable"), lambda: self.HandleChoice(False) )
+            ChoiceDef(Lang("Enable"), lambda: self.HandleChoice(ENABLE)),
+            ChoiceDef(Lang("Disable"), lambda: self.HandleDisable())
             ])
 
         self.UpdateFields()
@@ -50,29 +112,25 @@ def HandleKey(self, inKey):
 
         return handled
 
-    def HandleChoice(self,  inChoice):
+    def HandleChoice(self, inChoice):
         data = Data.Inst()
         Layout.Inst().PopDialogue()
 
         try:
             message = Lang("Configuration Successful")
-            if inChoice:
+            if inChoice == ENABLE:
                 data.EnableSSH()
-            else:
-                data.DisableSSH()
-
-                if ShellPipe(['/sbin/pidof', 'sshd']).CallRC() == 0: # If PIDs are available
-                    message = Lang("New connections via the remote shell are now disabled, but there are "
-                        "ssh connections still ongoing.  If necessary, use 'killall sshd' from the Local "
-                        "Command Shell to terminate them.")
-
-            Layout.Inst().PushDialogue(InfoDialogue(message))
+                Layout.Inst().PushDialogue(InfoDialogue(message))
 
         except Exception as e:
-            Layout.Inst().PushDialogue(InfoDialogue( Lang("Failed: ")+Lang(e)))
+            Layout.Inst().PushDialogue(InfoDialogue(Lang("Failed: ")+Lang(e)))
 
         data.Update()
 
+    def HandleDisable(self):
+        Layout.Inst().PopDialogue()
+        Layout.Inst().PushDialogue(DisableOptionsDialogue())
+
 
 class XSFeatureRemoteShell:
     @classmethod
@@ -106,7 +164,7 @@ def Register(self):
             {
                 'menuname' : 'MENU_REMOTE',
                 'menupriority' : 100,
-                'menutext' : Lang('Enable/Disable Remote Shell') ,
+                'menutext' : Lang('Enable/Disable/Auto Remote Shell') ,
                 'statusupdatehandler' : self.StatusUpdateHandler,
                 'activatehandler' : self.ActivateHandler
             }


Reviews:
user: login: stephenchengCloud
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T06:09:17Z
user: login: LunfanZhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T06:13:18Z
user: login: stephenchengCloud
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T06:56:06Z
user: login: stephenchengCloud
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T07:16:00Z
user: login: DeliZhangX
state: APPROVED
author_association: NONE
submitted_at: 2025-05-20T08:53:01Z
user: login: BengangY
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:19:36Z
user: login: BengangY
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:20:29Z
user: login: BengangY
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:21:24Z
user: login: BengangY
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:22:32Z
user: login: BengangY
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:25:26Z
user: login: LunfanZhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:31:13Z
user: login: BengangY
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:43:46Z

Review comments:
diff_hunk: @@ -15,14 +15,14 @@ env:
 jobs:
   python-checks:
     name: minimaltest (pre-commit)
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
path: .github/workflows/main.yml
user: login: stephenchengCloud
body: This change is already in https://github.com/xapi-project/xsconsole/pull/57/files.

Need to rebase.
created_at: 2025-05-19T06:09:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,14 +15,14 @@ env:
 jobs:
   python-checks:
     name: minimaltest (pre-commit)
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
path: .github/workflows/main.yml
user: login: LunfanZhang
body: But it`s on the feature branch currently, so the check will fail without this change.
created_at: 2025-05-19T06:13:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,14 +15,14 @@ env:
 jobs:
   python-checks:
     name: minimaltest (pre-commit)
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
path: .github/workflows/main.yml
user: login: stephenchengCloud
body: Sorry, I didn't see it was a feature branch.
created_at: 2025-05-19T06:56:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,68 @@
 
 from XSConsoleStandard import *
 
+DISABLE = 0
+ENABLE = 1
+AUTO = 2
+
+class DisableOptionsDialogue(Dialogue):
+    def __init__(self):
+        Dialogue.__init__(self)
+
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang("Auto mode Options"))
+        pane.AddBox()
+
+        self.disableMenu = Menu(self, None, Lang("Disable Options"), [
+            ChoiceDef(Lang("Standard Disable"), lambda: self.HandleChoice(DISABLE)),
+            ChoiceDef(Lang("Disable and turn on Auto-mode"), lambda: self.HandleChoice(AUTO))
+            ])
+
+        self.UpdateFields()
+
+    def UpdateFields(self):
+        pane = self.Pane()
+        pane.ResetFields()
+
+        pane.AddTitleField(Lang("When auto-mode is enabled: SSH is disabled when XAPI is running properly, "
+                        "and enabled when XAPI is down for emergency troubleshooting."))
+        pane.AddMenuField(self.disableMenu)
+        pane.AddKeyHelpField( { Lang("<Enter>") : Lang("OK"), Lang("<Esc>") : Lang("Cancel") } )
+
+    def HandleKey(self, inKey):
+        handled = self.disableMenu.HandleKey(inKey)
+
+        if not handled and inKey == 'KEY_ESCAPE':
+            Layout.Inst().PopDialogue()
+            handled = True
+
+        return handled
+
+    def HandleChoice(self, inChoice):
+        data = Data.Inst()
+        Layout.Inst().PopDialogue()
+
+        try:
+            message = Lang("Configuration Successful")
+            data.DisableSSH()
+
+            if ShellPipe(['/sbin/pidof', 'sshd']).CallRC() == 0: # If PIDs are available
path: plugins-base/XSFeatureRemoteShell.py
user: login: BengangY
body: I think here should be `'/sbin/pidof', 'sshd-session'`. After ssh is disabled, the process `sshd` doesn't exist. But if some ssh connections still exists, there should be some processes named `sshd-session`.
created_at: 2025-05-23T02:19:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,68 @@
 
 from XSConsoleStandard import *
 
+DISABLE = 0
+ENABLE = 1
+AUTO = 2
+
+class DisableOptionsDialogue(Dialogue):
+    def __init__(self):
+        Dialogue.__init__(self)
+
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang("Auto mode Options"))
+        pane.AddBox()
+
+        self.disableMenu = Menu(self, None, Lang("Disable Options"), [
+            ChoiceDef(Lang("Standard Disable"), lambda: self.HandleChoice(DISABLE)),
+            ChoiceDef(Lang("Disable and turn on Auto-mode"), lambda: self.HandleChoice(AUTO))
+            ])
+
+        self.UpdateFields()
+
+    def UpdateFields(self):
+        pane = self.Pane()
+        pane.ResetFields()
+
+        pane.AddTitleField(Lang("When auto-mode is enabled: SSH is disabled when XAPI is running properly, "
+                        "and enabled when XAPI is down for emergency troubleshooting."))
+        pane.AddMenuField(self.disableMenu)
+        pane.AddKeyHelpField( { Lang("<Enter>") : Lang("OK"), Lang("<Esc>") : Lang("Cancel") } )
+
+    def HandleKey(self, inKey):
+        handled = self.disableMenu.HandleKey(inKey)
+
+        if not handled and inKey == 'KEY_ESCAPE':
+            Layout.Inst().PopDialogue()
+            handled = True
+
+        return handled
+
+    def HandleChoice(self, inChoice):
+        data = Data.Inst()
+        Layout.Inst().PopDialogue()
+
+        try:
+            message = Lang("Configuration Successful")
+            data.DisableSSH()
+
+            if ShellPipe(['/sbin/pidof', 'sshd']).CallRC() == 0: # If PIDs are available
+                message = Lang("New connections via the remote shell are now disabled, but there are "
+                    "ssh connections still ongoing.  If necessary, use 'killall sshd' from the Local "
path: plugins-base/XSFeatureRemoteShell.py
user: login: BengangY
body: `killall sshd` may not work. Would it be `killall sshd-session`?
created_at: 2025-05-23T02:20:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,68 @@
 
 from XSConsoleStandard import *
 
+DISABLE = 0
+ENABLE = 1
+AUTO = 2
+
+class DisableOptionsDialogue(Dialogue):
+    def __init__(self):
+        Dialogue.__init__(self)
+
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang("Auto mode Options"))
+        pane.AddBox()
+
+        self.disableMenu = Menu(self, None, Lang("Disable Options"), [
+            ChoiceDef(Lang("Standard Disable"), lambda: self.HandleChoice(DISABLE)),
+            ChoiceDef(Lang("Disable and turn on Auto-mode"), lambda: self.HandleChoice(AUTO))
+            ])
+
+        self.UpdateFields()
+
+    def UpdateFields(self):
+        pane = self.Pane()
+        pane.ResetFields()
+
+        pane.AddTitleField(Lang("When auto-mode is enabled: SSH is disabled when XAPI is running properly, "
+                        "and enabled when XAPI is down for emergency troubleshooting."))
+        pane.AddMenuField(self.disableMenu)
+        pane.AddKeyHelpField( { Lang("<Enter>") : Lang("OK"), Lang("<Esc>") : Lang("Cancel") } )
+
+    def HandleKey(self, inKey):
+        handled = self.disableMenu.HandleKey(inKey)
+
+        if not handled and inKey == 'KEY_ESCAPE':
+            Layout.Inst().PopDialogue()
+            handled = True
+
+        return handled
+
+    def HandleChoice(self, inChoice):
+        data = Data.Inst()
+        Layout.Inst().PopDialogue()
+
+        try:
+            message = Lang("Configuration Successful")
+            data.DisableSSH()
+
+            if ShellPipe(['/sbin/pidof', 'sshd']).CallRC() == 0: # If PIDs are available
+                message = Lang("New connections via the remote shell are now disabled, but there are "
+                    "ssh connections still ongoing.  If necessary, use 'killall sshd' from the Local "
path: plugins-base/XSFeatureRemoteShell.py
user: login: BengangY
body: Or `pkill sshd`.
created_at: 2025-05-23T02:21:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,68 @@
 
 from XSConsoleStandard import *
 
+DISABLE = 0
+ENABLE = 1
+AUTO = 2
+
+class DisableOptionsDialogue(Dialogue):
+    def __init__(self):
+        Dialogue.__init__(self)
+
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang("Auto mode Options"))
path: plugins-base/XSFeatureRemoteShell.py
user: login: BengangY
body: `Auto mode` -> `Auto-mode`.
created_at: 2025-05-23T02:22:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,68 @@
 
 from XSConsoleStandard import *
 
+DISABLE = 0
+ENABLE = 1
+AUTO = 2
+
+class DisableOptionsDialogue(Dialogue):
+    def __init__(self):
+        Dialogue.__init__(self)
+
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang("Auto mode Options"))
+        pane.AddBox()
+
+        self.disableMenu = Menu(self, None, Lang("Disable Options"), [
+            ChoiceDef(Lang("Standard Disable"), lambda: self.HandleChoice(DISABLE)),
+            ChoiceDef(Lang("Disable and turn on Auto-mode"), lambda: self.HandleChoice(AUTO))
path: plugins-base/XSFeatureRemoteShell.py
user: login: BengangY
body: `turn on` -> `Turn on`.
created_at: 2025-05-23T02:25:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,68 @@
 
 from XSConsoleStandard import *
 
+DISABLE = 0
+ENABLE = 1
+AUTO = 2
+
+class DisableOptionsDialogue(Dialogue):
+    def __init__(self):
+        Dialogue.__init__(self)
+
+        pane = self.NewPane(DialoguePane(self.parent))
+        pane.TitleSet(Lang("Auto mode Options"))
+        pane.AddBox()
+
+        self.disableMenu = Menu(self, None, Lang("Disable Options"), [
+            ChoiceDef(Lang("Standard Disable"), lambda: self.HandleChoice(DISABLE)),
+            ChoiceDef(Lang("Disable and turn on Auto-mode"), lambda: self.HandleChoice(AUTO))
+            ])
+
+        self.UpdateFields()
+
+    def UpdateFields(self):
+        pane = self.Pane()
+        pane.ResetFields()
+
+        pane.AddTitleField(Lang("When auto-mode is enabled: SSH is disabled when XAPI is running properly, "
+                        "and enabled when XAPI is down for emergency troubleshooting."))
+        pane.AddMenuField(self.disableMenu)
+        pane.AddKeyHelpField( { Lang("<Enter>") : Lang("OK"), Lang("<Esc>") : Lang("Cancel") } )
+
+    def HandleKey(self, inKey):
+        handled = self.disableMenu.HandleKey(inKey)
+
+        if not handled and inKey == 'KEY_ESCAPE':
+            Layout.Inst().PopDialogue()
+            handled = True
+
+        return handled
+
+    def HandleChoice(self, inChoice):
+        data = Data.Inst()
+        Layout.Inst().PopDialogue()
+
+        try:
+            message = Lang("Configuration Successful")
+            data.DisableSSH()
+
+            if ShellPipe(['/sbin/pidof', 'sshd']).CallRC() == 0: # If PIDs are available
path: plugins-base/XSFeatureRemoteShell.py
user: login: LunfanZhang
body: Good point, this prompt is moved from other place, so the last authors seems like do not aware of this.
created_at: 2025-05-23T07:31:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: BengangY
created_at: 2025-05-23T01:34:05Z
body: @LunfanZhang Please update the screenshot based on the newest design.
author_association: CONTRIBUTOR
user: login: LunfanZhang
created_at: 2025-05-23T01:37:04Z
body: > @LunfanZhang Please update the screenshot based on the newest design.

Hi @BengangY I have updated the screenshot in PR description.
author_association: CONTRIBUTOR
user: login: LunfanZhang
created_at: 2025-05-23T07:32:56Z
body: @rosslagerwall @minglumlu may also be interesting in this change was well
author_association: CONTRIBUTOR