fix(hermes): ha-updates --rollback must really roll back (review 1771)
- install --rollback without --version is refused before any HA call, and --rollback to the entity's latest_version is refused as a forward install, so --rollback cannot bypass the 60-minute HACS limit. - SKILL.md command list shows --rollback. - Tests for both refusals; 16 pass locally and in the pinned CI image.
This commit is contained in:
@@ -30,7 +30,7 @@ python3 /opt/hermes-skills/ha-updates/scripts/ha.py window # in_
|
||||
python3 /opt/hermes-skills/ha-updates/scripts/ha.py updates # pending update entities
|
||||
python3 /opt/hermes-skills/ha-updates/scripts/ha.py snapshot <name> # health -> /opt/data/ha-updates/<name>.json
|
||||
python3 /opt/hermes-skills/ha-updates/scripts/ha.py compare <before> <after> # exit 1 + problems on regression
|
||||
python3 /opt/hermes-skills/ha-updates/scripts/ha.py install <entity_id> [--version V] # waits until settled
|
||||
python3 /opt/hermes-skills/ha-updates/scripts/ha.py install <entity_id> [--version V] [--rollback] # waits until settled; --rollback needs --version <old>
|
||||
python3 /opt/hermes-skills/ha-updates/scripts/ha.py restart # restart, wait for RUNNING
|
||||
```
|
||||
|
||||
|
||||
@@ -216,6 +216,8 @@ def entity(entity_id):
|
||||
|
||||
|
||||
def cmd_install(a):
|
||||
if a.rollback and not a.version:
|
||||
sys.exit("ha: refused: --rollback needs --version <previous version>")
|
||||
w = require_window("install") # before any HA call
|
||||
if not a.rollback and domains().get(a.entity_id) == "hacs":
|
||||
w = require_window("hacs")
|
||||
@@ -229,6 +231,8 @@ def cmd_install(a):
|
||||
body = {"entity_id": a.entity_id}
|
||||
if a.version:
|
||||
body["version"] = a.version
|
||||
if a.rollback and a.version == attrs.get("latest_version"):
|
||||
sys.exit("ha: refused: --rollback to the latest version is a forward install")
|
||||
target = a.version or attrs.get("latest_version")
|
||||
started = time.time()
|
||||
try:
|
||||
|
||||
@@ -53,12 +53,15 @@ class Window(unittest.TestCase):
|
||||
with mock.patch.object(ha, "window_state", return_value=ha.window_state(berlin(4, 40))):
|
||||
self.assertEqual(ha.require_window("restart")["minutes_left"], 20)
|
||||
|
||||
def install_at(self, now, domain, rollback=False):
|
||||
args = mock.Mock(entity_id="update.x", version=None, timeout=3600, rollback=rollback)
|
||||
state = {"attributes": {"supported_features": 1, "installed_version": "1", "latest_version": "2"}}
|
||||
def install_at(self, now, domain, rollback=False, version=None):
|
||||
args = mock.Mock(entity_id="update.x", version=version or ("1" if rollback else None),
|
||||
timeout=3600, rollback=rollback)
|
||||
state = {"attributes": {"supported_features": 1 | 2, "installed_version": "2" if rollback else "1",
|
||||
"latest_version": "2"}}
|
||||
done_state = {"attributes": {"installed_version": args.version or "2"}}
|
||||
with mock.patch.object(ha, "window_state", return_value=ha.window_state(now)), \
|
||||
mock.patch.object(ha, "domains", return_value={"update.x": domain}), \
|
||||
mock.patch.object(ha, "entity", side_effect=[state, {"attributes": {"installed_version": "2"}}]), \
|
||||
mock.patch.object(ha, "entity", side_effect=[state, done_state]), \
|
||||
mock.patch.object(ha, "call") as call, mock.patch("builtins.print"):
|
||||
with self.assertRaises(SystemExit) as done:
|
||||
ha.cmd_install(args)
|
||||
@@ -76,6 +79,14 @@ class Window(unittest.TestCase):
|
||||
self.assertEqual(self.install_at(berlin(4, 0), "hacs"), 55 * 60)
|
||||
self.assertEqual(self.install_at(berlin(4, 30), "hacs", rollback=True), 25 * 60)
|
||||
|
||||
def test_rollback_must_be_a_rollback(self):
|
||||
with mock.patch.object(ha, "call") as call:
|
||||
with self.assertRaisesRegex(SystemExit, "--rollback needs --version"):
|
||||
ha.cmd_install(mock.Mock(entity_id="update.x", version=None, timeout=3600, rollback=True))
|
||||
call.assert_not_called()
|
||||
with self.assertRaisesRegex(AssertionError, "forward install"):
|
||||
self.install_at(berlin(4, 30), "hacs", rollback=True, version="2")
|
||||
|
||||
def test_install_and_restart_check_window_before_any_call(self):
|
||||
with mock.patch.object(ha, "window_state", return_value=ha.window_state(berlin(12, 0))), \
|
||||
mock.patch.object(ha, "call") as call:
|
||||
|
||||
Reference in New Issue
Block a user