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

feat: add Unit.reboot for machine charms #1041

Merged
merged 31 commits into from Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8aec383
Expose juju-reboot [--now].
tonyandrewmeyer Oct 10, 2023
9aa26d8
Fix name.
tonyandrewmeyer Oct 10, 2023
a8576c3
Improve exception docs.
tonyandrewmeyer Oct 10, 2023
74b2e1f
Merge branch 'main' into juju-reboot-929
tonyandrewmeyer Oct 10, 2023
3516f37
Remove whitespace.
tonyandrewmeyer Oct 10, 2023
4607904
Update ops/model.py
tonyandrewmeyer Oct 10, 2023
300fd84
Update ops/testing.py
tonyandrewmeyer Oct 10, 2023
cbd71d2
Rename exception & fix doc link.
tonyandrewmeyer Oct 11, 2023
9253398
Fix type confusion.
tonyandrewmeyer Oct 11, 2023
28063d5
Allow RebootNow as an exception class name.
tonyandrewmeyer Oct 11, 2023
d21dc3c
Update test/test_testing.py
tonyandrewmeyer Oct 11, 2023
41ca79f
Merge branch 'main' into juju-reboot-929
tonyandrewmeyer Oct 11, 2023
7fba4f1
Fix accidental change.
tonyandrewmeyer Oct 11, 2023
5799f9e
Adjust docs as per code review.
tonyandrewmeyer Oct 11, 2023
43acb42
Note Unit.reboot addition.
tonyandrewmeyer Oct 11, 2023
f46c128
Simplify documentation as per review comment.
tonyandrewmeyer Oct 11, 2023
09b6033
Add a property that tells you when reboot() was last called.
tonyandrewmeyer Oct 11, 2023
8f95030
Also note the new harness property.
tonyandrewmeyer Oct 11, 2023
0aaee0f
Style fix.
tonyandrewmeyer Oct 11, 2023
42232d9
Typo
tonyandrewmeyer Oct 11, 2023
f341653
Typo
tonyandrewmeyer Oct 11, 2023
20a391c
Drive-by: update with other changes.
tonyandrewmeyer Oct 11, 2023
50eec52
Add 'new in' version specifiers.
tonyandrewmeyer Oct 12, 2023
2e56068
Merge branch 'main' into juju-reboot-929
tonyandrewmeyer Oct 17, 2023
15c7517
Merge branch 'main' into juju-reboot-929
tonyandrewmeyer Oct 17, 2023
7cc5bdc
Provide a counter of the number of reboots, rather than a datetime, t…
tonyandrewmeyer Oct 17, 2023
fdddc08
Have ops guarantee that nothing after reboot(now=True) will run, rath…
tonyandrewmeyer Oct 17, 2023
cbfe4f8
Use the same exception as the non-testing code.
tonyandrewmeyer Oct 17, 2023
17e8be0
Merge branch 'main' into juju-reboot-929
tonyandrewmeyer Oct 17, 2023
2cdb33f
Update ops/testing.py
tonyandrewmeyer Oct 17, 2023
9b544d0
Merge branch 'main' into juju-reboot-929
benhoyt Oct 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions ops/model.py
Expand Up @@ -691,6 +691,32 @@ def set_ports(self, *ports: Union[int, 'Port']) -> None:
for protocol, port in desired - existing:
self._backend.open_port(protocol, port)

def reboot(self, now: bool = False) -> None:
"""Reboot the host machine, after stopping all containers hosted on the machine.
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved

Normally, the reboot will only take place after the current hook successfully
completes. Use the ``now`` argument when multiple reboots are required, to
reboot immediately without waiting for the hook to complete, and to restart the
hook after rebooting.
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved

This will silently fail for Kubernetes charms.
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved

This can only be called for the current unit, and cannot be used in an action
hook.

Args:
now: terminate immediately without waiting for the current hook to complete,
restarting the hook after reboot.

Raises:
RuntimeError: if called on a remote unit.
:class:`ModelError`: if used in an action hook.

"""
if not self._is_our_unit:
raise RuntimeError(f'cannot reboot a remote unit {self}')
self._backend.reboot(now)


@dataclasses.dataclass(frozen=True)
class Port:
Expand Down Expand Up @@ -3357,6 +3383,12 @@ def _parse_opened_port(cls, port_str: str) -> Optional[Port]:
protocol_lit = typing.cast(typing.Literal['tcp', 'udp'], protocol)
return Port(protocol_lit, int(port))

def reboot(self, now: bool = False):
if now:
self._run("juju-reboot", "--now")
else:
self._run("juju-reboot")


class _ModelBackendValidator:
"""Provides facilities for validating inputs and formatting them for model backends."""
Expand Down
26 changes: 26 additions & 0 deletions ops/testing.py
Expand Up @@ -1882,6 +1882,24 @@ class _Secret:
grants: Dict[int, Set[str]] = dataclasses.field(default_factory=dict)


class RebootingMachineError(Exception):
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
"""Raised when the machine would reboot.

When :meth:`Unit.reboot` is called with ``now=True`` in a machine charm, the
unit's machine is rebooted, interrupting the execution of the event hook. To
simulate that when using the testing harness, write a test that expects a
``RebootingMachineError`` to be raised, for example::
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved

def test_installation(self):
self.harness.begin()
with self.assertRaises(ops.testing.RebootMachineError):
self.harness.charm.on.install.emit()
# More asserts here that the first part of installation was done.
self.harness.charm.on.install.emit()
# More asserts here that the installation continued appropriately.
"""


@_copy_docstrings(model._ModelBackend)
@_record_calls
class _TestingModelBackend:
Expand Down Expand Up @@ -2503,6 +2521,14 @@ def _check_protocol_and_port(self, protocol: str, port: Optional[int]):
else:
raise model.ModelError(f'ERROR invalid protocol "{protocol}", expected "tcp", "udp", or "icmp"\n') # NOQA: test_quote_backslashes

def reboot(self, now: bool = False):
if not now:
# We can't simulate the reboot, so just do nothing.
benhoyt marked this conversation as resolved.
Show resolved Hide resolved
return
# This should exit, reboot, and re-emit the event, but we'll need the caller
# to handle that. We raise an exception so that they can simulate the exit.
raise RebootingMachineError()


@_copy_docstrings(pebble.ExecProcess)
class _TestingExecProcess:
Expand Down
22 changes: 22 additions & 0 deletions test/test_model.py
Expand Up @@ -3596,5 +3596,27 @@ def test_set_ports_noop(self):
])


class TestUnit(unittest.TestCase):
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
def setUp(self):
self.model = ops.model.Model(ops.charm.CharmMeta(), ops.model._ModelBackend('myapp/0'))
self.unit = self.model.unit

def test_reboot(self):
fake_script(self, 'juju-reboot', 'exit 0')
self.unit.reboot()
self.assertEqual(fake_script_calls(self, clear=True), [
['juju-reboot', ''],
])
self.unit.reboot(now=True)
self.assertEqual(fake_script_calls(self, clear=True), [
['juju-reboot', '--now'],
])

with self.assertRaises(RuntimeError):
self.model.get_unit('other').reboot()
with self.assertRaises(RuntimeError):
self.model.get_unit('other').reboot(now=True)


if __name__ == "__main__":
unittest.main()
26 changes: 26 additions & 0 deletions test/test_testing.py
Expand Up @@ -3337,6 +3337,32 @@ def test_get_pebble_methods(self):
client = backend.get_pebble('/custom/socket/path')
self.assertIsInstance(client, _TestingPebbleClient)

def test_reboot(self):
class RebootingCharm(ops.CharmBase):
def __init__(self, *args, **kwargs): # type: ignore
super().__init__(*args, **kwargs) # type: ignore
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
self.framework.observe(self.on.install, self._reboot)
self.framework.observe(self.on.remove, self._reboot_now)

def _reboot(self, event: ops.RemoveEvent):
tonyandrewmeyer marked this conversation as resolved.
Show resolved Hide resolved
self.unit.reboot()

def _reboot_now(self, event: ops.InstallEvent):
self.unit.reboot(now=True)

harness = ops.testing.Harness(RebootingCharm, meta='''
name: test-app
''')
self.addCleanup(harness.cleanup)
backend = harness._backend
backend.reboot()
with self.assertRaises(ops.testing.RebootingMachineError):
backend.reboot(now=True)
harness.begin()
harness.charm.on.install.emit()
with self.assertRaises(ops.testing.RebootingMachineError):
harness.charm.on.remove.emit()


class _TestingPebbleClientMixin:
def get_testing_client(self):
Expand Down