diff --git a/autoinstall-schema.json b/autoinstall-schema.json
index 05b211201..f21b3be82 100644
--- a/autoinstall-schema.json
+++ b/autoinstall-schema.json
@@ -454,7 +454,8 @@
"type": "string",
"enum": [
"reboot",
- "poweroff"
+ "poweroff",
+ "wait"
]
}
},
diff --git a/documentation/autoinstall-reference.md b/documentation/autoinstall-reference.md
index 453cb5017..cb6ac87ef 100644
--- a/documentation/autoinstall-reference.md
+++ b/documentation/autoinstall-reference.md
@@ -507,11 +507,13 @@ Supported values are:
**default:** `reboot`
**can be interactive:** no
-Request the system to shutdown or reboot automatically after the installation has finished.
+Request the system to shutdown or reboot automatically after the installation has finished. Wait means that after install is complete, no powerstate change occurs and instead it waits indefinitely.
+
Supported values are:
* `reboot`
* `shutdown`
+ * `wait`
diff --git a/subiquity/client/client.py b/subiquity/client/client.py
index 784d80fd4..721d0770b 100644
--- a/subiquity/client/client.py
+++ b/subiquity/client/client.py
@@ -49,6 +49,7 @@
ApplicationState,
ErrorReportKind,
ErrorReportRef,
+ ShutdownMode,
)
from subiquity.journald import journald_listen
from subiquity.ui.frame import SubiquityUI
@@ -219,6 +220,28 @@ def resp_hook(self, response):
raise Abort(report.ref())
return response
+ async def noninteractive_confirmation_shutdown(self):
+ await asyncio.sleep(1)
+ reboot = _('reboot')
+ poweroff = _('poweroff')
+ answer = None
+ print(_("The install is complete."))
+ print()
+ prompt = "\n\n{} ({}|{})".format(
+ _("Shutdown the system?"), reboot, poweroff)
+ while answer not in (reboot, poweroff):
+ print(prompt)
+ answer = await run_in_thread(input)
+ if answer == reboot:
+ mode = ShutdownMode('reboot')
+ elif answer == poweroff:
+ mode = ShutdownMode('poweroff')
+ try:
+ await self.client.shutdown.POST(mode=mode, immediate=True)
+ except aiohttp.client_exceptions.ServerDisconnectedError:
+ # In this case, this is a good thing
+ pass
+
async def noninteractive_confirmation(self):
await asyncio.sleep(1)
yes = _('yes')
@@ -255,6 +278,10 @@ async def noninteractive_watch_app_state(self, initial_status):
confirm_task = None
while True:
app_state = app_status.state
+ if app_state == ApplicationState.DONE:
+ mode = await self.client.shutdown.GET()
+ if ShutdownMode(mode) == ShutdownMode.WAIT:
+ await self.noninteractive_confirmation_shutdown()
if app_state == ApplicationState.NEEDS_CONFIRMATION:
if confirm_task is None:
confirm_task = self.aio_loop.create_task(
diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py
index 0ce0808d7..6ac4939a9 100644
--- a/subiquity/common/apidef.py
+++ b/subiquity/common/apidef.py
@@ -332,6 +332,7 @@ def GET() -> TimeZoneInfo: ...
def POST(tz: str): ...
class shutdown:
+ def GET() -> ShutdownMode: ...
def POST(mode: ShutdownMode, immediate: bool = False): ...
class mirror:
diff --git a/subiquity/common/types.py b/subiquity/common/types.py
index 4e89dae30..5b78a1add 100644
--- a/subiquity/common/types.py
+++ b/subiquity/common/types.py
@@ -595,8 +595,9 @@ class UbuntuProCheckTokenAnswer:
class ShutdownMode(enum.Enum):
- REBOOT = enum.auto()
- POWEROFF = enum.auto()
+ REBOOT = 'reboot'
+ POWEROFF = 'poweroff'
+ WAIT = 'wait'
@attr.s(auto_attribs=True)
diff --git a/subiquity/server/controllers/shutdown.py b/subiquity/server/controllers/shutdown.py
index b203a08a4..d7857213f 100644
--- a/subiquity/server/controllers/shutdown.py
+++ b/subiquity/server/controllers/shutdown.py
@@ -40,7 +40,7 @@ class ShutdownController(SubiquityController):
autoinstall_key = 'shutdown'
autoinstall_schema = {
'type': 'string',
- 'enum': ['reboot', 'poweroff']
+ 'enum': [m.value for m in ShutdownMode],
}
def __init__(self, app):
@@ -56,10 +56,11 @@ def __init__(self, app):
self.mode = ShutdownMode.REBOOT
def load_autoinstall_data(self, data):
- if data == 'reboot':
- self.mode = ShutdownMode.REBOOT
- elif data == 'poweroff':
- self.mode = ShutdownMode.POWEROFF
+ if data is not None:
+ self.mode = ShutdownMode(data)
+
+ async def GET(self) -> ShutdownMode:
+ return self.mode
async def POST(self, mode: ShutdownMode, immediate: bool = False):
self.mode = mode
@@ -70,7 +71,9 @@ async def POST(self, mode: ShutdownMode, immediate: bool = False):
await self.shuttingdown_event.wait()
def interactive(self):
- return self.app.interactive
+ # Under normal conditions there is no matching client controller.
+ # When we're DONE, the client may be inquiring about shutdown state.
+ return True
def start(self):
self.app.aio_loop.create_task(self._wait_install())
@@ -115,6 +118,11 @@ async def copy_logs_to_target(self, context):
@with_context(description='mode={self.mode.name}')
def shutdown(self, context):
+ if self.mode == ShutdownMode.WAIT:
+ log.debug('Not shutting down yet due to shutdown mode "wait"')
+ self.server_reboot_event.clear()
+ self.app.aio_loop.create_task(self._run())
+ return
self.shuttingdown_event.set()
if self.opts.dry_run:
self.app.exit()
diff --git a/subiquity/server/controllers/tests/test_shutdown.py b/subiquity/server/controllers/tests/test_shutdown.py
new file mode 100644
index 000000000..2f2b132b6
--- /dev/null
+++ b/subiquity/server/controllers/tests/test_shutdown.py
@@ -0,0 +1,44 @@
+# Copyright 2022 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from parameterized import parameterized
+
+from subiquitycore.tests import SubiTestCase
+from subiquitycore.tests.mocks import make_app
+
+from subiquity.server.controllers.shutdown import ShutdownController
+from subiquity.common.types import ShutdownMode
+
+
+class TestSubiquityModel(SubiTestCase):
+ def setUp(self):
+ self.app = make_app()
+ self.controller = ShutdownController(self.app)
+
+ @parameterized.expand([
+ [{'shutdown': 'reboot'}],
+ [{'shutdown': 'poweroff'}],
+ [{'shutdown': 'wait'}],
+ ])
+ async def test_load_ai(self, ai_data):
+ expected = ai_data.get('shutdown')
+ self.controller.load_autoinstall_data(expected)
+ self.assertEqual(self.controller.mode.value, expected)
+ self.assertEqual(self.controller.mode, ShutdownMode(expected))
+
+ async def test_supported_values(self):
+ actual = ShutdownController.autoinstall_schema['enum']
+ expected = ['reboot', 'poweroff', 'wait']
+ self.assertEqual(expected, actual)
diff --git a/system_setup/server/controllers/shutdown.py b/system_setup/server/controllers/shutdown.py
index c72dbaddd..b8626011f 100644
--- a/system_setup/server/controllers/shutdown.py
+++ b/system_setup/server/controllers/shutdown.py
@@ -30,6 +30,11 @@ class WSLShutdownMode(enum.Enum):
class SetupShutdownController(ShutdownController):
+ autoinstall_schema = {
+ 'type': 'string',
+ 'enum': ['reboot', 'poweroff'],
+ }
+
def __init__(self, app):
# This isn't the most beautiful way, but the shutdown controller
# depends on Install, override with our configure one.
@@ -60,6 +65,8 @@ def shutdown(self, context):
elif self.mode == ShutdownMode.POWEROFF:
log.debug("Setting launcher for shut down")
launcher_status += ["action=shutdown"]
+ else:
+ raise Exception(f"Unknown Shutdown Mode {self.mode}")
default_uid = self.app.controllers.Install.get_default_uid()
if default_uid is not None and default_uid != 0: