Skip to content

Commit 7a70f97

Browse files
authored
Merge pull request #879 from cderici/utilities-for-subordinate-units
#879 #### Description This is in response to an [LP bug](https://bugs.launchpad.net/juju/+bug/1987332), which is about getting the subordinate units using libjuju. [The way that `zaza` was doing it](https://github.com/openstack-charmers/zaza/blob/b2f2b46e7903cbd601917c7e4e40d2e00ba4d03d/zaza/utilities/juju.py#LL449C57-L449C57) is incorrect, in that it uses the `Charm` field in the subordinate units to get the charm_url. This is an internal field and shouldn't be used externally. Furthermore the naming of this field is somewhat misleading, as [it's really the `upgrading-from` field](https://github.com/juju/juju/blob/63900364b3f56c273ed3842f77b40b710b2c87a4/cmd/juju/status/formatted.go#LL230C101-L230C101) that's being set only during an upgrade -to indicate the previous charm url prior to the upgrade- and it is to be deleted/unset right after the upgrade is done. This is why getting the charm-url from this field is not correct. This change adds some useful utilities (some fields and some methods) to interact with the subordinate units, in particular, we're adding: - `ModelState.subordinate_units` and `model.subordinate_units` - `Application.subordinate_units` - `Unit.is_subordinate`, `Unit.principal_unit` and `Unit.get_subordinates()` #### QA Steps This also adds the test `tests/integration/test_unit.py::test_subordinate_units`, so the following should be working: ``` tox -e integration -- tests/integration/test_unit.py::test_subordinate_units ``` #### Notes & Discussion Marked to be forward-ported into the main branch.
2 parents ead1b48 + a75065f commit 7a70f97

4 files changed

Lines changed: 70 additions & 0 deletions

File tree

juju/application.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ def units(self):
6464
if unit.application == self.name
6565
]
6666

67+
@property
68+
def subordinate_units(self):
69+
"""Returns the subordinate units of this application"""
70+
return [u for u in self.units if u.is_subordinate]
71+
6772
@property
6873
def relations(self):
6974
return [rel for rel in self.model.relations if rel.matches(self.name)]

juju/model.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ def units(self):
171171
"""
172172
return self._live_entity_map('unit')
173173

174+
@property
175+
def subordinate_units(self):
176+
"""Return a map of unit-id:Unit for all subordinate units"""
177+
return {u_name: u for u_name, u in self.units.items() if u.is_subordinate}
178+
174179
@property
175180
def relations(self):
176181
"""Return a map of relation-id:Relation for all relations currently in
@@ -969,6 +974,14 @@ def units(self):
969974
"""
970975
return self.state.units
971976

977+
@property
978+
def subordinate_units(self):
979+
"""Return a map of unit-id:Unit for all subordiante units currently in
980+
the model.
981+
982+
"""
983+
return self.state.subordinate_units
984+
972985
@property
973986
def relations(self):
974987
"""Return a list of all Relations currently in the model.

juju/unit.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ def agent_status_since(self):
2626
"""
2727
return pyrfc3339.parse(self.safe_data['agent-status']['since'])
2828

29+
@property
30+
def is_subordinate(self):
31+
"""True if the unit is subordinate of another unit
32+
33+
"""
34+
return self.safe_data['subordinate']
35+
36+
@property
37+
def principal_unit(self):
38+
"""Returns the name of the unit of which this unit is a subordinate to.
39+
Returns '' for principal units themselves.
40+
"""
41+
return self.safe_data['principal']
42+
2943
@property
3044
def agent_status_message(self):
3145
"""Get the agent status message.
@@ -77,6 +91,14 @@ def public_address(self):
7791
def tag(self):
7892
return tag.unit(self.name)
7993

94+
def get_subordinates(self):
95+
"""Returns the unit objects that are subordinates to this unit
96+
97+
:return [Unit]
98+
"""
99+
return [u for u_name, u in self.model.units.items() if u.is_subordinate and
100+
u.principal_unit == self.name]
101+
80102
async def destroy(self):
81103
"""Destroy this unit.
82104

tests/integration/test_unit.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,33 @@ async def test_resolve_local(event_loop):
214214
# Errored units won't get cleaned up unless we force them.
215215
await asyncio.gather(*(machine.destroy(force=True)
216216
for machine in model.machines.values()))
217+
218+
219+
@base.bootstrapped
220+
@pytest.mark.asyncio
221+
async def test_subordinate_units(event_loop):
222+
async with base.CleanModel() as model:
223+
u_app = await model.deploy('ubuntu')
224+
n_app = await model.deploy('ntp')
225+
await model.relate('ubuntu', 'ntp')
226+
await model.wait_for_idle()
227+
228+
# model subordinates
229+
model_subs = model.subordinate_units
230+
assert len(model_subs) == 1
231+
assert 'ntp/0' in model_subs
232+
assert 'ubuntu/0' not in model_subs
233+
234+
n_unit = model_subs['ntp/0']
235+
u_unit = u_app.units[0]
236+
237+
# application subordinates
238+
app_sub_names = [u.name for u in n_app.subordinate_units]
239+
assert n_unit.name in app_sub_names
240+
assert u_unit.name not in app_sub_names
241+
242+
assert n_unit.is_subordinate
243+
assert not u_unit.is_subordinate
244+
assert n_unit.principal_unit == 'ubuntu/0'
245+
assert u_unit.principal_unit == ''
246+
assert [u.name for u in u_unit.get_subordinates()] == [n_unit.name]

0 commit comments

Comments
 (0)