Skip to content

Commit 8b7e7b8

Browse files
authored
Merge pull request #689 from cderici/3.0-compat
#689 #### Description This PR is the first of a bunch of PRs to enable pylibjuju to support Juju 3.0. In particular, the aim of this PR is to have all the tests in the `test_model.py` to pass, which constitutes a significant portion of libjuju's operational logic. Includes 12 commits. The summary of the changes is: - Facade versions are updated with the new schema, and the new clients are generated. - Changed incorrect uses of `ClientFacade` for a bunch of functions to use their respective correct Facades. - Update the tests/bundles that are using charmstore charms to use the charmhub instead. - There's a couple of spots where we needed `CharmHub.Info()` to get some basic charm info to deploy stuff, however, since CharmHub facade doesn't exist anymore in juju 3.0, I replaced it with a simple api call to the charmhub itself to get the necessary info. #### QA Steps All the tests in `test_model.py` should pass on a `juju 3.0` controller. So bootstrap from juju's `develop` branch (or `snap install juju --channel=latest/beta`) and run the following in libjuju: `tox -e integration -- tests/integration/test_model.py` #### Notes & Discussion - This PR targets the `juju-3.0-compatibility` branch, not the main branch. - Currently the tests in the CI don't make much sense, since the CI is being run against the latest stable juju (which is still 2.9). I expect most of them to fail until we address the problem of supporting both 2.9 and 3.0 at the same time.
2 parents fbbaf78 + 49b81d1 commit 8b7e7b8

24 files changed

Lines changed: 3974 additions & 2666 deletions

juju/bundle.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def __init__(self, model, trusted=False, forced=False):
5252
model.connection())
5353
self.ann_facade = client.AnnotationsFacade.from_connection(
5454
model.connection())
55+
self.machine_manager_facade = client.MachineManagerFacade.from_connection(
56+
model.connection())
5557

5658
# Feature detect if we have the new charms facade, otherwise fallback
5759
# to the client facade, when making calls.
@@ -617,10 +619,8 @@ async def run(self, context):
617619
self.application, charm, overrides=self.resources)
618620
elif Schema.CHARM_HUB.matches(url.schema):
619621
c_hub = charmhub.CharmHub(context.model)
620-
info = await c_hub.info(url.name, channel=self.channel)
621-
if info.errors.error_list.code:
622-
raise JujuError("unable to resolve the charm {} with channel {}".format(url.name, channel))
623-
origin.id_ = info.result.id_
622+
id_ = c_hub.get_charm_id(url.name)
623+
origin.id_ = id_
624624
resources = await context.model._add_charmhub_resources(
625625
self.application, charm, origin, overrides=self.resources)
626626
else:
@@ -726,7 +726,7 @@ async def run(self, context):
726726
if Schema.CHARM_STORE.matches(url.schema):
727727
entity_id = await context.charmstore.entityId(self.charm, channel=self.channel)
728728
log.debug('Adding %s', entity_id)
729-
await context.client_facade.AddCharm(channel=self.channel, url=entity_id, force=False)
729+
await context.charms_facade.AddCharm(channel=self.channel, url=entity_id, force=False)
730730
identifier = entity_id
731731
origin = client.CharmOrigin(source="charm-store", risk="stable")
732732

@@ -837,7 +837,7 @@ async def run(self, context):
837837

838838
# Submit the request.
839839
params = client.AddMachineParams(**params)
840-
results = await context.client_facade.AddMachines(params=[params])
840+
results = await context.machine_manager_facade.AddMachines(params=[params])
841841
error = results.machines[0].error
842842
if error:
843843
raise ValueError("Error adding machine: %s" % error.message)

juju/charmhub.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
from .client import client
22
from .errors import JujuError
33

4+
import requests
5+
import json
6+
47

58
class CharmHub:
69
def __init__(self, model):
710
self.model = model
811

12+
def get_charm_id(self, charm_name):
13+
conn, headers, path_prefix = self.model.connection().https_connection()
14+
15+
url = "http://api.snapcraft.io/v2/charms/info/{}".format(charm_name)
16+
_response = requests.get(url)
17+
if not _response.status_code == 200:
18+
raise JujuError("Got {} from {}".format(_response.status_code, url))
19+
response = json.loads(_response.text)
20+
return response['id']
21+
922
async def info(self, name, channel=None):
1023
"""info displays detailed information about a CharmHub charm. The charm
1124
can be specified by the exact name.

juju/client/_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ class EntityWatcherFacade(TypeFactory):
259259
pass
260260

261261

262+
class EnvironUpgraderFacade(TypeFactory):
263+
pass
264+
265+
262266
class ExternalControllerUpdaterFacade(TypeFactory):
263267
pass
264268

juju/client/_client1.py

Lines changed: 533 additions & 309 deletions
Large diffs are not rendered by default.

juju/client/_client11.py

Lines changed: 107 additions & 93 deletions
Large diffs are not rendered by default.

juju/client/_client13.py

Lines changed: 16 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -205,24 +205,17 @@ class ApplicationFacade(Type):
205205
'exposed': {'type': 'boolean'},
206206
'exposed-endpoints': {'patternProperties': {'.*': {'$ref': '#/definitions/ExposedEndpoint'}},
207207
'type': 'object'},
208+
'life': {'type': 'string'},
208209
'principal': {'type': 'boolean'},
209210
'remote': {'type': 'boolean'},
210211
'series': {'type': 'string'},
211212
'tag': {'type': 'string'}},
212213
'required': ['tag',
213214
'principal',
214215
'exposed',
215-
'remote'],
216+
'remote',
217+
'life'],
216218
'type': 'object'},
217-
'ApplicationSet': {'additionalProperties': False,
218-
'properties': {'application': {'type': 'string'},
219-
'branch': {'type': 'string'},
220-
'options': {'patternProperties': {'.*': {'type': 'string'}},
221-
'type': 'object'}},
222-
'required': ['application',
223-
'branch',
224-
'options'],
225-
'type': 'object'},
226219
'ApplicationSetCharm': {'additionalProperties': False,
227220
'properties': {'application': {'type': 'string'},
228221
'channel': {'type': 'string'},
@@ -449,9 +442,11 @@ class ApplicationFacade(Type):
449442
'cross-model': {'type': 'boolean'},
450443
'endpoint': {'type': 'string'},
451444
'related-endpoint': {'type': 'string'},
445+
'relation-id': {'type': 'integer'},
452446
'unit-relation-data': {'patternProperties': {'.*': {'$ref': '#/definitions/RelationData'}},
453447
'type': 'object'}},
454-
'required': ['endpoint',
448+
'required': ['relation-id',
449+
'endpoint',
455450
'cross-model',
456451
'related-endpoint',
457452
'ApplicationData',
@@ -594,11 +589,6 @@ class ApplicationFacade(Type):
594589
'pool': {'type': 'string'},
595590
'size': {'type': 'integer'}},
596591
'type': 'object'},
597-
'StringResult': {'additionalProperties': False,
598-
'properties': {'error': {'$ref': '#/definitions/Error'},
599-
'result': {'type': 'string'}},
600-
'required': ['result'],
601-
'type': 'object'},
602592
'Subnet': {'additionalProperties': False,
603593
'properties': {'cidr': {'type': 'string'},
604594
'life': {'type': 'string'},
@@ -629,6 +619,7 @@ class ApplicationFacade(Type):
629619
'properties': {'address': {'type': 'string'},
630620
'charm': {'type': 'string'},
631621
'leader': {'type': 'boolean'},
622+
'life': {'type': 'string'},
632623
'machine': {'type': 'string'},
633624
'opened-ports': {'items': {'type': 'string'},
634625
'type': 'array'},
@@ -794,12 +785,6 @@ class ApplicationFacade(Type):
794785
'properties': {'Params': {'$ref': '#/definitions/ApplicationGet'},
795786
'Result': {'$ref': '#/definitions/ApplicationGetResults'}},
796787
'type': 'object'},
797-
'GetCharmURL': {'description': 'GetCharmURL returns the charm '
798-
'URL the given application is\n'
799-
'running at present.',
800-
'properties': {'Params': {'$ref': '#/definitions/ApplicationGet'},
801-
'Result': {'$ref': '#/definitions/StringResult'}},
802-
'type': 'object'},
803788
'GetCharmURLOrigin': {'description': 'GetCharmURLOrigin '
804789
'returns the charm URL '
805790
'and charm origin the '
@@ -843,20 +828,13 @@ class ApplicationFacade(Type):
843828
'properties': {'Params': {'$ref': '#/definitions/ScaleApplicationsParams'},
844829
'Result': {'$ref': '#/definitions/ScaleApplicationResults'}},
845830
'type': 'object'},
846-
'Set': {'description': 'Set implements the server side of '
847-
'Application.Set.\n'
848-
'It does not unset values that are set '
849-
'to an empty string.\n'
850-
'Unset should be used for that.',
851-
'properties': {'Params': {'$ref': '#/definitions/ApplicationSet'}},
852-
'type': 'object'},
853831
'SetCharm': {'description': 'SetCharm sets the charm for a '
854832
'given for the application.',
855833
'properties': {'Params': {'$ref': '#/definitions/ApplicationSetCharm'}},
856834
'type': 'object'},
857-
'SetConfigs': {'description': 'SetConfig implements the server '
858-
'side of Application.SetConfig. '
859-
'Both\n'
835+
'SetConfigs': {'description': 'SetConfigs implements the '
836+
'server side of '
837+
'Application.SetConfig. Both\n'
860838
'application and charm config '
861839
'are set. It does not unset '
862840
'values in\n'
@@ -892,14 +870,12 @@ class ApplicationFacade(Type):
892870
'properties': {'Params': {'$ref': '#/definitions/ApplicationUnexpose'}},
893871
'type': 'object'},
894872
'UnitsInfo': {'description': 'UnitsInfo returns unit '
895-
'information.',
873+
'information for the given '
874+
'entities (units or\n'
875+
'applications).',
896876
'properties': {'Params': {'$ref': '#/definitions/Entities'},
897877
'Result': {'$ref': '#/definitions/UnitInfoResults'}},
898878
'type': 'object'},
899-
'Unset': {'description': 'Unset implements the server side of '
900-
'Client.Unset.',
901-
'properties': {'Params': {'$ref': '#/definitions/ApplicationUnset'}},
902-
'type': 'object'},
903879
'UnsetApplicationsConfig': {'description': 'UnsetApplicationsConfig '
904880
'implements the '
905881
'server side of '
@@ -1338,35 +1314,6 @@ async def Get(self, application=None, branch=None):
13381314

13391315

13401316

1341-
@ReturnMapping(StringResult)
1342-
async def GetCharmURL(self, application=None, branch=None):
1343-
'''
1344-
GetCharmURL returns the charm URL the given application is
1345-
running at present.
1346-
1347-
application : str
1348-
branch : str
1349-
Returns -> StringResult
1350-
'''
1351-
if application is not None and not isinstance(application, (bytes, str)):
1352-
raise Exception("Expected application to be a str, received: {}".format(type(application)))
1353-
1354-
if branch is not None and not isinstance(branch, (bytes, str)):
1355-
raise Exception("Expected branch to be a str, received: {}".format(type(branch)))
1356-
1357-
# map input types to rpc msg
1358-
_params = dict()
1359-
msg = dict(type='Application',
1360-
request='GetCharmURL',
1361-
version=13,
1362-
params=_params)
1363-
_params['application'] = application
1364-
_params['branch'] = branch
1365-
reply = await self.rpc(msg)
1366-
return reply
1367-
1368-
1369-
13701317
@ReturnMapping(CharmURLOriginResult)
13711318
async def GetCharmURLOrigin(self, application=None, branch=None):
13721319
'''
@@ -1522,41 +1469,6 @@ async def ScaleApplications(self, applications=None):
15221469

15231470

15241471

1525-
@ReturnMapping(None)
1526-
async def Set(self, application=None, branch=None, options=None):
1527-
'''
1528-
Set implements the server side of Application.Set.
1529-
It does not unset values that are set to an empty string.
1530-
Unset should be used for that.
1531-
1532-
application : str
1533-
branch : str
1534-
options : typing.Mapping[str, str]
1535-
Returns -> None
1536-
'''
1537-
if application is not None and not isinstance(application, (bytes, str)):
1538-
raise Exception("Expected application to be a str, received: {}".format(type(application)))
1539-
1540-
if branch is not None and not isinstance(branch, (bytes, str)):
1541-
raise Exception("Expected branch to be a str, received: {}".format(type(branch)))
1542-
1543-
if options is not None and not isinstance(options, dict):
1544-
raise Exception("Expected options to be a Mapping, received: {}".format(type(options)))
1545-
1546-
# map input types to rpc msg
1547-
_params = dict()
1548-
msg = dict(type='Application',
1549-
request='Set',
1550-
version=13,
1551-
params=_params)
1552-
_params['application'] = application
1553-
_params['branch'] = branch
1554-
_params['options'] = options
1555-
reply = await self.rpc(msg)
1556-
return reply
1557-
1558-
1559-
15601472
@ReturnMapping(None)
15611473
async def SetCharm(self, application=None, channel=None, charm_origin=None, charm_url=None, config_settings=None, config_settings_yaml=None, endpoint_bindings=None, force=None, force_series=None, force_units=None, generation=None, resource_ids=None, storage_constraints=None):
15621474
'''
@@ -1643,7 +1555,7 @@ async def SetCharm(self, application=None, channel=None, charm_origin=None, char
16431555
@ReturnMapping(ErrorResults)
16441556
async def SetConfigs(self, args=None):
16451557
'''
1646-
SetConfig implements the server side of Application.SetConfig. Both
1558+
SetConfigs implements the server side of Application.SetConfig. Both
16471559
application and charm config are set. It does not unset values in
16481560
Config map that are set to an empty string. Unset should be used for that.
16491561
@@ -1771,7 +1683,8 @@ async def Unexpose(self, application=None, exposed_endpoints=None):
17711683
@ReturnMapping(UnitInfoResults)
17721684
async def UnitsInfo(self, entities=None):
17731685
'''
1774-
UnitsInfo returns unit information.
1686+
UnitsInfo returns unit information for the given entities (units or
1687+
applications).
17751688
17761689
entities : typing.Sequence[~Entity]
17771690
Returns -> UnitInfoResults
@@ -1791,39 +1704,6 @@ async def UnitsInfo(self, entities=None):
17911704

17921705

17931706

1794-
@ReturnMapping(None)
1795-
async def Unset(self, application=None, branch=None, options=None):
1796-
'''
1797-
Unset implements the server side of Client.Unset.
1798-
1799-
application : str
1800-
branch : str
1801-
options : typing.Sequence[str]
1802-
Returns -> None
1803-
'''
1804-
if application is not None and not isinstance(application, (bytes, str)):
1805-
raise Exception("Expected application to be a str, received: {}".format(type(application)))
1806-
1807-
if branch is not None and not isinstance(branch, (bytes, str)):
1808-
raise Exception("Expected branch to be a str, received: {}".format(type(branch)))
1809-
1810-
if options is not None and not isinstance(options, (bytes, str, list)):
1811-
raise Exception("Expected options to be a Sequence, received: {}".format(type(options)))
1812-
1813-
# map input types to rpc msg
1814-
_params = dict()
1815-
msg = dict(type='Application',
1816-
request='Unset',
1817-
version=13,
1818-
params=_params)
1819-
_params['application'] = application
1820-
_params['branch'] = branch
1821-
_params['options'] = options
1822-
reply = await self.rpc(msg)
1823-
return reply
1824-
1825-
1826-
18271707
@ReturnMapping(ErrorResults)
18281708
async def UnsetApplicationsConfig(self, args=None):
18291709
'''

0 commit comments

Comments
 (0)