Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shutdown: add 'wait' mode #1467

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion autoinstall-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,8 @@
"type": "string",
"enum": [
"reboot",
"poweroff"
"poweroff",
"wait"
]
}
},
Expand Down
4 changes: 3 additions & 1 deletion documentation/autoinstall-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<a name="late-commands"></a>

Expand Down
27 changes: 27 additions & 0 deletions subiquity/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ApplicationState,
ErrorReportKind,
ErrorReportRef,
ShutdownMode,
)
from subiquity.journald import journald_listen
from subiquity.ui.frame import SubiquityUI
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions subiquity/common/apidef.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions subiquity/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 14 additions & 6 deletions subiquity/server/controllers/shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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()
Expand Down
44 changes: 44 additions & 0 deletions subiquity/server/controllers/tests/test_shutdown.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)
7 changes: 7 additions & 0 deletions system_setup/server/controllers/shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down