Skip to content

Commit 3ab6158

Browse files
Merge branch 'master' of git://github.com/juju/python-libjuju into 2.9
2 parents c9540e6 + 7a853c0 commit 3ab6158

24 files changed

Lines changed: 558 additions & 31 deletions

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.8.6
1+
2.9.3

docs/changelog.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
11
Changelog
22
---------
33

4+
2.9.3
5+
^^^^^
6+
7+
Monday August 12 2021
8+
9+
* Bug fix - Fix 'Default to bundle series if the charm has no series field' #514
10+
11+
2.9.2
12+
^^^^^
13+
14+
Monday June 28 2021
15+
16+
* Bug fix - Fix 'metadata referenced before assignment' error #509
17+
18+
2.9.1
19+
^^^^^
20+
21+
Wednesday June 16 2021
22+
23+
* Bug fix - Bundle Exposed endpoints missing #502
24+
* Bug fix - Fix series requirement for local charms #504
25+
* Add local charm update support #507
26+
27+
2.9.0
28+
^^^^^
29+
30+
Thursday May 27 2021
31+
32+
* Update facade methods for Juju 2.9.0
33+
* Update facade methods for Juju 2.9.1
34+
* Bug fix - Support for Juju client proxies (LP#1926595)
35+
* Bug fix - Honor charm channel in bundles #496
36+
* Remove machine workaround for Juju 2.2.3
37+
438
2.8.6
539
^^^^^
640

examples/deploy_local_resource.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
This example:
3+
4+
1. Connects to the current model
5+
2. Deploy a local charm with a oci-image resource and waits until it reports
6+
itself active
7+
3. Destroys the unit and application
8+
9+
"""
10+
from juju import loop
11+
from juju.model import Model
12+
from pathlib import Path
13+
14+
15+
async def main():
16+
model = Model()
17+
print('Connecting to model')
18+
# connect to current model with current user, per Juju CLI
19+
await model.connect()
20+
21+
try:
22+
print('Deploying local-charm')
23+
base_dir = Path(__file__).absolute().parent.parent
24+
charm_path = '{}/tests/integration/oci-image-charm'.format(base_dir)
25+
resources = {"oci-image": "ubuntu/latest"}
26+
application = await model.deploy(
27+
charm_path,
28+
resources=resources,
29+
)
30+
31+
print('Waiting for active')
32+
await model.block_until(
33+
lambda: all(unit.workload_status == 'active'
34+
for unit in application.units),
35+
timeout=120,
36+
)
37+
38+
print('Removing Charm')
39+
await application.remove()
40+
finally:
41+
print('Disconnecting from model')
42+
await model.disconnect()
43+
44+
45+
if __name__ == '__main__':
46+
loop.run(main())

examples/local_refresh.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
This example:
3+
4+
1. Connects to the current model
5+
2. Upgrades previously deployed ubuntu charm
6+
7+
"""
8+
from juju import loop
9+
from juju.model import Model
10+
11+
12+
async def main():
13+
model = Model()
14+
print('Connecting to model')
15+
# connect to current model with current user, per Juju CLI
16+
await model.connect()
17+
18+
try:
19+
print('Get deployed application')
20+
app = model.appplications["ubuntu"]
21+
22+
print('Refresh/Upgrade Ubuntu charm with local charm')
23+
await app.refresh(path="path/to/local/ubuntu.charm")
24+
finally:
25+
print('Disconnecting from model')
26+
await model.disconnect()
27+
28+
29+
if __name__ == '__main__':
30+
loop.run(main())

juju/application.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import asyncio
1616
import json
1717
import logging
18+
import os
1819

19-
from . import model, tag
20+
from . import model, tag, utils
2021
from .status import derive_status
2122
from .annotationhelper import _get_annotations, _set_annotations
2223
from .client import client
2324
from .errors import JujuError
25+
from .bundle import get_charm_series
2426
from .placement import parse as parse_placement
2527

2628
log = logging.getLogger(__name__)
@@ -586,9 +588,10 @@ async def refresh(
586588
:param str switch: Crossgrade charm url
587589
588590
"""
589-
# TODO: Support local upgrades
590591
if path is not None:
591-
raise NotImplementedError("path option is not implemented")
592+
await self.local_refresh(channel, force, force_series, force_units,
593+
path, resources)
594+
return
592595
if resources is not None:
593596
raise NotImplementedError("resources option is not implemented")
594597

@@ -692,6 +695,61 @@ async def refresh(
692695

693696
upgrade_charm = refresh
694697

698+
async def local_refresh(
699+
self, channel=None, force=False, force_series=False, force_units=False,
700+
path=None, resources=None):
701+
"""Refresh the charm for this application with a local charm.
702+
703+
:param str channel: Channel to use when getting the charm from the
704+
charm store, e.g. 'development'
705+
:param bool force_series: Refresh even if series of deployed
706+
application is not supported by the new charm
707+
:param bool force_units: Refresh all units immediately, even if in
708+
error state
709+
:param str path: Refresh to a charm located at path
710+
:param dict resources: Dictionary of resource name/filepath pairs
711+
:param int revision: Explicit refresh revision
712+
:param str switch: Crossgrade charm url
713+
714+
"""
715+
app_facade = self._facade()
716+
717+
charm_dir = os.path.abspath(
718+
os.path.expanduser(path))
719+
model_config = await self.get_config()
720+
721+
series = get_charm_series(charm_dir)
722+
if not series:
723+
model_config = await self.get_config()
724+
default_series = model_config.get("default-series")
725+
if default_series:
726+
series = default_series.value
727+
charm_url = await self.model.add_local_charm_dir(charm_dir, series)
728+
metadata = utils.get_local_charm_metadata(path)
729+
if resources is not None:
730+
resources = await self.model.add_local_resources(self.entity_id,
731+
charm_url,
732+
metadata,
733+
resources=resources)
734+
735+
# Update application
736+
await app_facade.SetCharm(
737+
application=self.entity_id,
738+
channel=channel,
739+
charm_url=charm_url,
740+
config_settings=None,
741+
config_settings_yaml=None,
742+
force=force,
743+
force_series=force_series,
744+
force_units=force_units,
745+
resource_ids=resources,
746+
storage_constraints=None,
747+
)
748+
749+
await self.model.block_until(
750+
lambda: self.data['charm-url'] == charm_url
751+
)
752+
695753
async def get_metrics(self):
696754
"""Get metrics for this application's units.
697755

juju/bundle.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,13 @@ async def run(self, context):
456456
:param context: is used for any methods or properties required to
457457
perform a change.
458458
"""
459+
# NB: this should really be handled by the controller when generating the
460+
# bundle change plan, and this short-term workaround may be missing some
461+
# aspects of the logic which the CLI client contains to handle edge cases.
462+
if self.application in context.model.applications:
463+
log.debug('Skipping %s; already in model', self.application)
464+
return self.application
465+
459466
# resolve indirect references
460467
charm = context.resolve(self.charm)
461468
options = {}
@@ -480,6 +487,9 @@ async def run(self, context):
480487
origin = context.origins.get(str(url), {}).get(str(channel), None)
481488
if origin is None:
482489
raise JujuError("expected origin to be valid for application {} and charm {} with channel {}".format(self.application, str(url), str(channel)))
490+
if self.series is None or self.series == "":
491+
self.series = context.bundle.get("bundle",
492+
context.bundle.get("series", None))
483493

484494
await context.model._deploy(
485495
charm_url=charm,
@@ -577,9 +587,9 @@ async def run(self, context):
577587
return self.charm
578588

579589
if Schema.CHARM_STORE.matches(url.schema):
580-
entity_id = await context.charmstore.entityId(self.charm)
590+
entity_id = await context.charmstore.entityId(self.charm, channel=self.channel)
581591
log.debug('Adding %s', entity_id)
582-
await context.client_facade.AddCharm(channel=None, url=entity_id, force=False)
592+
await context.client_facade.AddCharm(channel=self.channel, url=entity_id, force=False)
583593
identifier = entity_id
584594
origin = client.CharmOrigin(source="charm-store", risk="stable")
585595

@@ -754,6 +764,14 @@ async def run(self, context):
754764
ep1 = context.resolve_relation(self.endpoint1)
755765
ep2 = context.resolve_relation(self.endpoint2)
756766

767+
# NB: this should really be handled by the controller when generating the
768+
# bundle change plan, and this short-term workaround may be missing some
769+
# aspects of the logic which the CLI client contains to handle edge cases.
770+
existing = [rel for rel in context.model.relations if rel.matches(ep1, ep2)]
771+
if existing:
772+
log.info('Skipping %s <-> %s; already related', ep1, ep2)
773+
return existing[0]
774+
757775
log.info('Relating %s <-> %s', ep1, ep2)
758776
return await context.model.add_relation(ep1, ep2)
759777

@@ -960,6 +978,7 @@ class ExposeChange(ChangeInfo):
960978
def __init__(self, change_id, requires, params=None):
961979
super(ExposeChange, self).__init__(change_id, requires)
962980

981+
self.exposed_endpoints = None
963982
if isinstance(params, list):
964983
self.application = params[0]
965984
elif isinstance(params, dict):

juju/charm.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# limitations under the License.
1414

1515
import logging
16-
1716
from . import model
1817

1918
log = logging.getLogger(__name__)

0 commit comments

Comments
 (0)