Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Surpress KeyError when applications are missing #367

Merged
merged 4 commits into from
Nov 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions examples/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
This example shows how to reconnect to a model if you encounter an error

1. Connects to current model.
2. Attempts to get an application that doesn't exist.
3. Disconnect then reconnect.

"""
from juju import loop
from juju.model import Model
from juju.errors import JujuEntityNotFoundError


async def main():
model = Model()

retryCount = 3
for i in range(0, retryCount):
await model.connect_current()
try:
model.applications['foo'].relations
except JujuEntityNotFoundError as e:
print(e.entity_name)
finally:
await model.disconnect()
# Everything worked out, continue on wards.


if __name__ == '__main__':
loop.run(main())
23 changes: 23 additions & 0 deletions juju/charm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2019 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from . import model

log = logging.getLogger(__name__)


class Charm(model.ModelEntity):
pass
22 changes: 22 additions & 0 deletions juju/delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ def get_entity_class(self):
return RemoteApplication


class CharmDelta(EntityDelta):
def get_id(self):
return self.data['charm-url']

@classmethod
def get_entity_class(self):
from .charm import Charm
return Charm


class ApplicationOfferDelta(EntityDelta):
def get_id(self):
return self.data['application-name']

@classmethod
def get_entity_class(self):
from .remoteapplication import ApplicationOffer
return ApplicationOffer


_delta_types = {
'action': ActionDelta,
'application': ApplicationDelta,
Expand All @@ -87,4 +107,6 @@ def get_entity_class(self):
'unit': UnitDelta,
'relation': RelationDelta,
'remoteApplication': RemoteApplicationDelta,
'charm': CharmDelta,
'applicationOffer': ApplicationOfferDelta,
}
12 changes: 12 additions & 0 deletions juju/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,15 @@ def endpoints(self):
for servers in self.redirect_info['servers']
for s in servers if s['scope'] == 'public' or not self.follow_redirect
]


class JujuEntityNotFoundError(JujuError):
"""Exception indicating that an entity was not found in the state. It was
expected that the entity was found in state and this is a terminal
condition.
To fix this condition, you should disconnect and reconnect to ensure that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline from here

any missing entities are correctly picked up."""
def __init__(self, entity_name, entity_types=None):
self.entity_name = entity_name
self.entity_types = entity_types
super().__init__("Entity not found: {}".format(entity_name))
21 changes: 20 additions & 1 deletion juju/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ def remote_applications(self):
"""
return self._live_entity_map('remoteApplication')

@property
def application_offers(self):
"""Return a map of application-name:Application for all applications
offers currently in the model.
"""
return self._live_entity_map('applicationOffer')

@property
def machines(self):
"""Return a map of machine-id:Machine for all machines currently in
Expand Down Expand Up @@ -732,6 +739,13 @@ def remote_applications(self):
"""
return self.state.remote_applications

@property
def application_offers(self):
"""Return a map of application-name:Application for all applications
offers currently in the model.
"""
return self.state.application_offers

@property
def machines(self):
"""Return a map of machine-id:Machine for all machines currently in
Expand Down Expand Up @@ -883,7 +897,12 @@ async def _all_watcher():
old_obj, new_obj = self.state.apply_delta(delta)
await self._notify_observers(delta, old_obj, new_obj)
except KeyError as e:
log.debug("unknown delta type: %s", e.args[0])
# TODO (stickupkid): we should raise the unknown delta
# type, so we handle correctly all the types comming from
# the all watcher. Currently they're ignored, causing
# issue.
# raise JujuError("unknown delta type {}".format(e.args))
log.warn("unknown delta type: %s", e.args[0])
self._watch_received.set()
except CancelledError:
pass
Expand Down
9 changes: 8 additions & 1 deletion juju/relation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from . import model
from .errors import JujuEntityNotFoundError

log = logging.getLogger(__name__)

Expand All @@ -20,7 +21,9 @@ def application(self):
return self.model.applications[app_name]
if app_name in self.model.remote_applications:
return self.model.remote_applications[app_name]
raise KeyError(app_name)
if app_name in self.model.application_offers:
return self.model.application_offers[app_name]
raise JujuEntityNotFoundError(app_name, ["application", "remoteApplication"])

@property
def name(self):
Expand Down Expand Up @@ -107,6 +110,10 @@ def matches(self, *specs):
else:
app_name, endpoint_name = spec, None
for endpoint in self.endpoints:
# The all watcher hasn't updated the internal state, so it can
# appear that the remote application doesn't exist.
if endpoint.application is None:
continue
if app_name == endpoint.application.name and \
endpoint_name in (endpoint.name, None):
# found a match for this spec, so move to next one
Expand Down
6 changes: 6 additions & 0 deletions juju/remoteapplication.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ def status_message(self):
@property
def tag(self):
return tag.application(self.name)


class ApplicationOffer(model.ModelEntity):
@property
def tag(self):
return tag.application(self.name)
63 changes: 63 additions & 0 deletions tests/unit/test_relation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import unittest

import mock

from juju.model import Model
from juju.relation import Relation
from juju.errors import JujuEntityNotFoundError


def _make_delta(entity, type_, data=None):
from juju.client.client import Delta
from juju.delta import get_entity_delta

delta = Delta([entity, type_, data])
return get_entity_delta(delta)


class TestRelation(unittest.TestCase):
def test_relation_does_not_match(self):
model = Model()
model._connector = mock.MagicMock()

delta = _make_delta('application', 'add', dict(name='foo'))
model.state.apply_delta(delta)
delta = _make_delta('relation', 'bar', dict(id="uuid-1234", name='foo', endpoints=[{"application-name": "foo"}]))
model.state.apply_delta(delta)

rel = Relation("uuid-1234", model)
self.assertFalse(rel.matches(["endpoint"]))

def test_relation_does_match(self):
model = Model()
model._connector = mock.MagicMock()

delta = _make_delta('application', 'add', dict(name='foo'))
model.state.apply_delta(delta)
delta = _make_delta('relation', 'bar', dict(id="uuid-1234", name='foo', endpoints=[{"application-name": "foo"}]))
model.state.apply_delta(delta)

rel = Relation("uuid-1234", model)
self.assertFalse(rel.matches(["foo"]))

def test_relation_does_match_remote_app(self):
model = Model()
model._connector = mock.MagicMock()

delta = _make_delta('remoteApplication', 'add', dict(name='foo'))
model.state.apply_delta(delta)
delta = _make_delta('relation', 'bar', dict(id="uuid-1234", name='foo', endpoints=[{"application-name": "foo"}]))
model.state.apply_delta(delta)

rel = Relation("uuid-1234", model)
self.assertFalse(rel.matches(["foo"]))

def test_relation_does_not_match_anything(self):
model = Model()
model._connector = mock.MagicMock()

delta = _make_delta('relation', 'bar', dict(id="uuid-1234", name='foo', endpoints=[{"application-name": "foo"}]))
model.state.apply_delta(delta)

rel = Relation("uuid-1234", model)
self.assertRaises(JujuEntityNotFoundError, rel.matches, ["xxx"])
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ deps =
# default tox env excludes integration and serial tests
commands =
# These need to be installed in a specific order
pip install urllib3==1.22
pip install urllib3==1.25.7
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to surpress the 3.8 warnings:

DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
class AuthContext(collections.Mapping):

pip install pylxd
py.test --tb native -ra -v -s -n auto -k 'not integration' -m 'not serial' {posargs}

Expand All @@ -50,7 +50,7 @@ deps =
envdir = {toxworkdir}/py3
commands =
# These need to be installed in a specific order
pip install urllib3==1.22
pip install urllib3==1.25.7
pip install pylxd
py.test --tb native -ra -v -n auto -k 'integration' -m 'not serial' {posargs}

Expand Down