Skip to content

Commit 6320078

Browse files
authored
Merge pull request #526 from SimonRichardson/merge-master-into-2.9
#526 Merge master into 2.9, this should bring all the new changes in master into 2.9 From there we should be able to cut a new release with charmhub support.
2 parents c9540e6 + 52ae745 commit 6320078

27 files changed

Lines changed: 619 additions & 76 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__)

juju/client/connection.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ class Monitor:
163163

164164
def __init__(self, connection):
165165
self.connection = weakref.ref(connection)
166-
self.reconnecting = asyncio.Lock(loop=connection.loop)
167-
self.close_called = asyncio.Event(loop=connection.loop)
166+
self.reconnecting = asyncio.Lock()
167+
self.close_called = asyncio.Event()
168168

169169
@property
170170
def status(self):
@@ -434,7 +434,7 @@ async def _pinger(self):
434434
async def _do_ping():
435435
try:
436436
await pinger_facade.Ping()
437-
await asyncio.sleep(10, loop=self.loop)
437+
await asyncio.sleep(10)
438438
except CancelledError:
439439
pass
440440

@@ -636,7 +636,7 @@ async def _try_endpoint(endpoint, cacert, delay):
636636
0.1 * i))
637637
for i, (endpoint, cacert) in enumerate(endpoints)]
638638
for attempt in range(self._retries + 1):
639-
for task in asyncio.as_completed(tasks, loop=self.loop):
639+
for task in asyncio.as_completed(tasks):
640640
try:
641641
result = await task
642642
break
@@ -805,7 +805,7 @@ async def login(self):
805805

806806
class _Task:
807807
def __init__(self, task, loop):
808-
self.stopped = asyncio.Event(loop=loop)
808+
self.stopped = asyncio.Event()
809809
self.stopped.set()
810810
self.task = task
811811
self.loop = loop

juju/machine.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ async def _scp(self, source, destination, scp_opts):
140140
raise JujuError("command failed: %s" % cmd)
141141

142142
async def ssh(
143-
self, command, user=None, proxy=False, ssh_opts=None):
143+
self, command, user='ubuntu', proxy=False, ssh_opts=None):
144144
"""Execute a command over SSH on this machine.
145145
146146
:param str command: Command to execute
@@ -164,10 +164,13 @@ async def ssh(
164164
cmd.extend(ssh_opts.split() if isinstance(ssh_opts, str) else ssh_opts)
165165
cmd.extend([command])
166166
loop = self.model.loop
167-
process = await asyncio.create_subprocess_exec(*cmd, loop=loop)
168-
await process.wait()
167+
process = await asyncio.create_subprocess_exec(
168+
*cmd, loop=loop, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
169+
stdout, stderr = await process.communicate()
169170
if process.returncode != 0:
170-
raise JujuError("command failed: %s" % cmd)
171+
raise JujuError("command failed: %s with %s" % (cmd, stderr.decode()))
172+
# stdout is a bytes-like object, returning a string might be more useful
173+
return stdout.decode()
171174

172175
def status_history(self, num=20, utc=False):
173176
"""Get status history for this machine.
@@ -232,12 +235,11 @@ def dns_name(self):
232235
233236
May return None if no suitable address is found.
234237
"""
235-
for scope in ['public', 'local-cloud']:
236-
addresses = self.safe_data['addresses'] or []
237-
addresses = [address for address in addresses
238-
if address['scope'] == scope]
239-
if addresses:
240-
return addresses[0]['value']
238+
addresses = self.safe_data['addresses'] or []
239+
for address in addresses:
240+
scope = address['scope']
241+
if scope == 'public' or scope == 'local-cloud':
242+
return address['value']
241243
return None
242244

243245
@property

0 commit comments

Comments
 (0)