Skip to content

Commit c1b997f

Browse files
committed
Fix the unit.run function to conform to the juju 3.0 way of interaction
... regardless of the version of the client and/or facade being used Fixes #707
1 parent f7ac5c4 commit c1b997f

5 files changed

Lines changed: 81 additions & 4 deletions

File tree

examples/run_action.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
from juju import jasyncio
3+
from juju.model import Model
4+
5+
# logging.basicConfig(level='DEBUG')
6+
7+
# Get a k8s controller
8+
# e.g. juju bootstrap microk8s test-k8s
9+
10+
# Add a model
11+
# e.g. juju add-model test-actions
12+
13+
# Then run this module
14+
# e.g. python examples/run_action.py
15+
16+
17+
async def _get_password():
18+
model = Model()
19+
await model.connect()
20+
await model.deploy('zinc-k8s')
21+
await model.wait_for_idle(status="active")
22+
23+
unit = model.applications['zinc-k8s'].units[0]
24+
action1 = await unit.run_action("get-admin-password")
25+
assert action1.status == 'pending'
26+
27+
action2 = await action1.wait()
28+
assert action2.status == 'completed'
29+
30+
print(action2.results["admin-password"])
31+
32+
# They are the same object
33+
assert action1 is action2
34+
35+
await model.disconnect()
36+
37+
38+
if __name__ == "__main__":
39+
jasyncio.run(_get_password())

juju/action.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ def __init__(self, entity_id, model, history_index=-1, connected=True):
1111
def status(self):
1212
return self.data['status']
1313

14-
async def wait(self):
14+
async def fetch_output(self):
1515
self.results = await self.model.get_action_output(self.id)
16+
17+
async def wait(self):
18+
self.results or await self.fetch_output()
1619
return self

juju/client/connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
log = logging.getLogger('juju.client.connection')
1818

1919
client_facades = {
20-
'Action': {'versions': [2, 7]},
20+
'Action': {'versions': [2, 6, 7]},
2121
'ActionPruner': {'versions': [1]},
2222
'Agent': {'versions': [2, 3]},
2323
'AgentTools': {'versions': [1]},

juju/unit.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ async def run(self, command, timeout=None):
194194
considered failed
195195
:returns: A :class:`juju.action.Action` instance.
196196
197+
Note that this is very similarly to unit.run_action only enqueues the action.
198+
You will need to call ``action.wait()`` on the resulting `Action` instance
199+
if you wish to block until the action is complete.
200+
197201
"""
198202
action = client.ActionFacade.from_connection(self.connection)
199203

@@ -204,15 +208,32 @@ async def run(self, command, timeout=None):
204208
# Convert seconds to nanoseconds
205209
timeout = int(timeout * 1000000000)
206210

211+
# It's not enough to only check for the old_client on the connection here
212+
# The old client's ActionFacade is updated to version 7, so
213+
# 2.9 track client may be using either ActionFacade v6 or v7 too
214+
old_facade = client.ActionFacade.best_facade_version(self.connection) <= 6
215+
207216
res = await action.Run(
208217
applications=[],
209218
commands=command,
210219
machines=[],
211220
timeout=timeout,
212221
units=[self.name],
213222
)
214-
the_action = res.results[0] if self.connection.is_using_old_client else res.actions[0]
215-
return await self.model.wait_for_action(the_action.action.tag)
223+
224+
action_result = res.results[0] if old_facade else res.actions[0]
225+
action = action_result.action
226+
227+
action_id = action.tag
228+
if action_id.startswith("action-"):
229+
# strip the action- part of "action-<num>" tag
230+
action_id = action_id[7:]
231+
232+
error = action_result.error
233+
if error:
234+
raise JujuError("Action error - {} : {}".format(error.code, error.message))
235+
236+
return await self.model._wait_for_new('action', action_id)
216237

217238
async def run_action(self, action_name, **params):
218239
"""Run an action on this unit.

tests/integration/test_unit.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,38 @@ async def test_run(event_loop):
7878
channel='stable',
7979
)
8080

81+
await model.wait_for_idle(status="active")
82+
8183
for unit in app.units:
8284
action = await unit.run('unit-get public-address')
8385
assert isinstance(action, Action)
86+
assert action.status == 'pending'
87+
await action.wait()
8488
assert action.status == 'completed'
8589
break
8690

8791
for unit in app.units:
8892
action = await unit.run('sleep 1', timeout=0.5)
8993
assert isinstance(action, Action)
94+
await action.wait()
9095
assert action.status == 'failed'
9196
break
9297

9398
for unit in app.units:
9499
action = await unit.run('sleep 0.5', timeout=2)
95100
assert isinstance(action, Action)
101+
await action.wait()
96102
assert action.status == 'completed'
97103
break
98104

105+
unit = app.units[0]
106+
action = await unit.run("df -h", timeout=None)
107+
assert action.status == 'pending'
108+
action = await action.wait()
109+
assert action.status == 'completed'
110+
assert action.results
111+
assert action.results['return-code'] == 0
112+
99113

100114
@base.bootstrapped
101115
@pytest.mark.asyncio

0 commit comments

Comments
 (0)