Skip to content

Commit

Permalink
Fix the unit.run function to conform to the juju 3.0 way of interaction
Browse files Browse the repository at this point in the history
... regardless of the version of the client and/or facade being used

Fixes juju#707
  • Loading branch information
cderici committed Aug 11, 2022
1 parent ac33c29 commit 0d04d6e
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 4 deletions.
39 changes: 39 additions & 0 deletions examples/run_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

from juju import jasyncio
from juju.model import Model

# logging.basicConfig(level='DEBUG')

# Get a k8s controller
# e.g. juju bootstrap microk8s test-k8s

# Add a model
# e.g. juju add-model test-actions

# Then run this module
# e.g. python examples/run_action.py


async def _get_password():
model = Model()
await model.connect()
await model.deploy('zinc-k8s')
await model.wait_for_idle(status="active")

unit = model.applications['zinc-k8s'].units[0]
action1 = await unit.run_action("get-admin-password")
assert action1.status == 'pending'

action2 = await action1.wait()
assert action2.status == 'completed'

print(action2.results["admin-password"])

# They are the same object
assert action1 is action2

await model.disconnect()


if __name__ == "__main__":
jasyncio.run(_get_password())
5 changes: 4 additions & 1 deletion juju/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ def __init__(self, entity_id, model, history_index=-1, connected=True):
def status(self):
return self.data['status']

async def wait(self):
async def fetch_output(self):
self.results = await self.model.get_action_output(self.id)

async def wait(self):
self.results or await self.fetch_output()
return self
2 changes: 1 addition & 1 deletion juju/client/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
log = logging.getLogger('juju.client.connection')

client_facades = {
'Action': {'versions': [2, 7]},
'Action': {'versions': [2, 6, 7]},
'ActionPruner': {'versions': [1]},
'Agent': {'versions': [2, 3]},
'AgentTools': {'versions': [1]},
Expand Down
25 changes: 23 additions & 2 deletions juju/unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ async def run(self, command, timeout=None):
considered failed
:returns: A :class:`juju.action.Action` instance.
Note that this is very similarly to unit.run_action only enqueues the action.
You will need to call ``action.wait()`` on the resulting `Action` instance
if you wish to block until the action is complete.
"""
action = client.ActionFacade.from_connection(self.connection)

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

# It's not enough to only check for the old_client on the connection here
# The old client's ActionFacade is updated to version 7, so
# 2.9 track client may be using either ActionFacade v6 or v7 too
old_facade = client.ActionFacade.best_facade_version(self.connection) <= 6

res = await action.Run(
applications=[],
commands=command,
machines=[],
timeout=timeout,
units=[self.name],
)
the_action = res.results[0] if self.connection.is_using_old_client else res.actions[0]
return await self.model.wait_for_action(the_action.action.tag)

action_result = res.results[0] if old_facade else res.actions[0]
action = action_result.action

action_id = action.tag
if action_id.startswith("action-"):
# strip the action- part of "action-<num>" tag
action_id = action_id[7:]

error = action_result.error
if error:
raise JujuError("Action error - {} : {}".format(error.code, error.message))

return await self.model._wait_for_new('action', action_id)

async def run_action(self, action_name, **params):
"""Run an action on this unit.
Expand Down
14 changes: 14 additions & 0 deletions tests/integration/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,38 @@ async def test_run(event_loop):
channel='stable',
)

await model.wait_for_idle(status="active")

for unit in app.units:
action = await unit.run('unit-get public-address')
assert isinstance(action, Action)
assert action.status == 'pending'
await action.wait()
assert action.status == 'completed'
break

for unit in app.units:
action = await unit.run('sleep 1', timeout=0.5)
assert isinstance(action, Action)
await action.wait()
assert action.status == 'failed'
break

for unit in app.units:
action = await unit.run('sleep 0.5', timeout=2)
assert isinstance(action, Action)
await action.wait()
assert action.status == 'completed'
break

unit = app.units[0]
action = await unit.run("df -h", timeout=None)
assert action.status == 'pending'
action = await action.wait()
assert action.status == 'completed'
assert action.results
assert action.results['return-code'] == 0


@base.bootstrapped
@pytest.mark.asyncio
Expand Down

0 comments on commit 0d04d6e

Please sign in to comment.