Skip to content

Commit e4d6109

Browse files
Kevin Allioliclaude
andcommitted
release: v1.0.2 — Aodh PUT semantics + Keystone hex ID + version sync
Found during live Aodh lifecycle test on Infomaniak (2026-04-19, post v1.0.1). All fixes validated against the real cloud. - alarm set: Aodh requires the full alarm representation on PUT, not a partial. Fetch current, merge the user's updates, strip read-only fields (alarm_id, project_id, user_id, timestamp, state_timestamp, state_reason*), then PUT. Previously returned 400 "Mandatory field missing" for any set that didn't include name+type+rule. - validate_id: accept 32-char bare hex IDs in addition to hyphenated UUIDs. Keystone projects/users/groups on many clouds (incl. Infomaniak) are exposed as hex-without-hyphens, making `alarm quota-set <project_id>` and similar commands unusable. - __version__: read from installed package metadata (importlib.metadata.version) instead of hardcoded string that drifted from pyproject.toml. `orca --version` now reflects the installed distribution. Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent e871e49 commit e4d6109

6 files changed

Lines changed: 71 additions & 21 deletions

File tree

orca_cli/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
"""orca — OpenStack Rich Command-line Alternative."""
22

3-
__version__ = "0.1.0"
3+
from __future__ import annotations
4+
5+
try:
6+
from importlib.metadata import PackageNotFoundError, version
7+
except ImportError: # pragma: no cover — py<3.8
8+
from importlib_metadata import PackageNotFoundError, version # type: ignore[no-redef]
9+
10+
try:
11+
__version__ = version("orca-openstackclient")
12+
except PackageNotFoundError: # pragma: no cover — editable source w/o install
13+
__version__ = "0.0.0+dev"

orca_cli/commands/alarm.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -194,36 +194,45 @@ def alarm_set(ctx, alarm_id, name, description, severity, enabled, repeat_action
194194
rule_json, alarm_actions, ok_actions, insufficient_data_actions):
195195
"""Update an alarm."""
196196
client = ctx.find_object(OrcaContext).ensure_client()
197-
# Fetch current alarm to know its type
198-
current = client.get(f"{_url(client)}/v2/alarms/{alarm_id}")
199-
body: dict = {}
197+
updates: dict = {}
200198
if name is not None:
201-
body["name"] = name
199+
updates["name"] = name
202200
if description is not None:
203-
body["description"] = description
201+
updates["description"] = description
204202
if severity is not None:
205-
body["severity"] = severity
203+
updates["severity"] = severity
206204
if enabled is not None:
207-
body["enabled"] = enabled
205+
updates["enabled"] = enabled
208206
if repeat_actions is not None:
209-
body["repeat_actions"] = repeat_actions
207+
updates["repeat_actions"] = repeat_actions
210208
if alarm_actions:
211-
body["alarm_actions"] = list(alarm_actions)
209+
updates["alarm_actions"] = list(alarm_actions)
212210
if ok_actions:
213-
body["ok_actions"] = list(ok_actions)
211+
updates["ok_actions"] = list(ok_actions)
214212
if insufficient_data_actions:
215-
body["insufficient_data_actions"] = list(insufficient_data_actions)
213+
updates["insufficient_data_actions"] = list(insufficient_data_actions)
214+
rule_update: dict | None = None
216215
if rule_json is not None:
217216
try:
218-
rule = json.loads(rule_json)
217+
rule_update = json.loads(rule_json)
219218
except json.JSONDecodeError as exc:
220219
raise click.BadParameter(f"Invalid JSON: {exc}", param_hint="--rule")
221-
atype = current.get("type", "")
222-
rule_key = "composite_rule" if atype == "composite" else f"{atype}_rule"
223-
body[rule_key] = rule
224-
if not body:
220+
if not updates and rule_update is None:
225221
console.print("Nothing to update.")
226222
return
223+
224+
# Aodh PUT wants the full alarm representation — fetch, merge, send.
225+
current = client.get(f"{_url(client)}/v2/alarms/{alarm_id}")
226+
body = dict(current)
227+
body.update(updates)
228+
if rule_update is not None:
229+
atype = current.get("type", "")
230+
rule_key = "composite_rule" if atype == "composite" else f"{atype}_rule"
231+
body[rule_key] = rule_update
232+
# Aodh rejects read-only fields on PUT
233+
for ro in ("alarm_id", "project_id", "user_id", "timestamp",
234+
"state_timestamp", "state_reason", "state_reason_data"):
235+
body.pop(ro, None)
227236
client.put(f"{_url(client)}/v2/alarms/{alarm_id}", json=body)
228237
console.print(f"Alarm [bold]{alarm_id}[/bold] updated.")
229238

orca_cli/core/validators.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
66

77

88
def validate_id(ctx: click.Context, param: click.Parameter, value: str) -> str:
9-
"""Validate that the given value looks like a valid resource ID (UUID or numeric)."""
9+
"""Validate that the given value looks like a valid resource ID.
10+
11+
Accepts:
12+
- Hyphenated UUID: ``8-4-4-4-12`` hex (e.g. Nova/Neutron resource IDs).
13+
- Bare hex UUID: 32 hex chars (e.g. Keystone project/user IDs).
14+
- Numeric ID (e.g. flavor IDs on older clouds, quota resources).
15+
"""
1016
uuid_pattern = re.compile(
1117
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
1218
re.IGNORECASE,
1319
)
20+
hex_pattern = re.compile(r"^[0-9a-f]{32}$", re.IGNORECASE)
1421
numeric_pattern = re.compile(r"^\d+$")
15-
if not (uuid_pattern.match(value) or numeric_pattern.match(value)):
22+
if not (uuid_pattern.match(value) or hex_pattern.match(value) or numeric_pattern.match(value)):
1623
raise click.BadParameter(f"'{value}' is not a valid resource ID (expected UUID or numeric).")
1724
return value
1825

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "orca-openstackclient"
7-
version = "1.0.1"
7+
version = "1.0.2"
88
description = "orca — OpenStack Rich Command-line Alternative"
99
authors = ["Kevin Allioli"]
1010
license = "Apache-2.0"

tests/test_alarm.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,22 @@ def test_set_name(self, invoke, mock_client):
208208
result = invoke(["alarm", "set", ALARM_ID, "--name", "renamed"])
209209
assert result.exit_code == 0
210210
body = mock_client.put.call_args[1]["json"]
211+
# Aodh needs the full representation: the update carries the name
212+
# override *and* the merged existing fields (type, rule, severity…).
211213
assert body["name"] == "renamed"
214+
assert body["type"] == "gnocchi_resources_threshold"
215+
assert body["severity"] == "low"
216+
assert "gnocchi_resources_threshold_rule" in body
217+
218+
def test_set_strips_readonly_fields(self, invoke, mock_client):
219+
_aodh(mock_client)
220+
mock_client.get.return_value = _alarm()
221+
result = invoke(["alarm", "set", ALARM_ID, "--name", "renamed"])
222+
assert result.exit_code == 0
223+
body = mock_client.put.call_args[1]["json"]
224+
for ro in ("alarm_id", "project_id", "user_id",
225+
"timestamp", "state_timestamp"):
226+
assert ro not in body
212227

213228
def test_set_rule(self, invoke, mock_client):
214229
_aodh(mock_client)
@@ -220,7 +235,7 @@ def test_set_rule(self, invoke, mock_client):
220235
result = invoke(["alarm", "set", ALARM_ID, "--rule", new_rule])
221236
assert result.exit_code == 0
222237
body = mock_client.put.call_args[1]["json"]
223-
assert "gnocchi_resources_threshold_rule" in body
238+
assert body["gnocchi_resources_threshold_rule"]["threshold"] == 90.0
224239

225240
def test_set_nothing(self, invoke, mock_client):
226241
_aodh(mock_client)

tests/test_validators.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ def test_accepts_uppercase_uuid(self):
2121
def test_accepts_numeric(self):
2222
assert validate_id(None, None, "42") == "42"
2323

24+
def test_accepts_bare_hex_uuid(self):
25+
# Keystone project/user IDs are commonly hex-without-hyphens
26+
uid = "120b4dbd14934ba6ba1975792ae6b789"
27+
assert validate_id(None, None, uid) == uid
28+
29+
def test_rejects_hex_wrong_length(self):
30+
with pytest.raises(click.BadParameter):
31+
validate_id(None, None, "120b4dbd14934ba6")
32+
2433
def test_rejects_arbitrary_string(self):
2534
with pytest.raises(click.BadParameter, match="not a valid resource ID"):
2635
validate_id(None, None, "not-a-uuid")

0 commit comments

Comments
 (0)