Samuel Allan 539f36817f
[tempest-k8s] Fix error on remove-application
Previously on application remove,
tempest-k8s charm would error at the relation-broken hook,
causing the removal to fail:

```
tempest/0*                   error        idle       10.1.169.40         hook failed: "identity-ops-relation-broken"
```

with traceback:

```
  File "/var/lib/juju/agents/unit-tempest-0/charm/src/handlers.py", line 609, in _on_provider_goneaway
    self.charm.set_tempest_ready(False)
  File "/var/lib/juju/agents/unit-tempest-0/charm/./src/charm.py", line 283, in set_tempest_ready
    self.peers.set_unit_data({TEMPEST_READY_KEY: "true" if ready else ""})
  File "/var/lib/juju/agents/unit-tempest-0/charm/lib/ops_sunbeam/relation_handlers.py", line 625, in set_unit_data
    self.interface.set_unit_data(settings)
  File "/var/lib/juju/agents/unit-tempest-0/charm/lib/ops_sunbeam/interfaces.py", line 150, in set_unit_data
    self.peers_rel.data[self.model.unit][k] = v
AttributeError: 'NoneType' object has no attribute 'data'
```

This fixes the issue for tempest (and potentially if similar issues
are present on other sunbeam charms),
by checking if peels_rel exists because trying to interact with it.

Change-Id: I5e01c1dba719a63160f0452282d57b9eae14b41b
2024-05-15 14:32:46 +02:00

168 lines
5.2 KiB
Python

# Copyright 2021 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.
"""Common interfaces not charm specific."""
import logging
from typing import (
Dict,
List,
Optional,
)
import ops.model
from ops.framework import (
EventBase,
EventSource,
Object,
ObjectEvents,
StoredState,
)
class PeersRelationCreatedEvent(EventBase):
"""The PeersRelationCreatedEvent indicates that peer relation now exists.
It does not indicate that any peers are available or have joined, simply
that the relation exists. This is useful to to indicate that the
application databag is available for storing information shared across
units.
"""
pass
class PeersDataChangedEvent(EventBase):
"""The PeersDataChangedEvent indicates peer data hjas changed."""
pass
class PeersRelationJoinedEvent(EventBase):
"""The PeersRelationJoinedEvent indicates a new unit has joined."""
pass
class PeersEvents(ObjectEvents):
"""Peer Events."""
peers_relation_created = EventSource(PeersRelationCreatedEvent)
peers_relation_joined = EventSource(PeersRelationJoinedEvent)
peers_data_changed = EventSource(PeersDataChangedEvent)
class OperatorPeers(Object):
"""Interface for the peers relation."""
on = PeersEvents()
state = StoredState()
def __init__(self, charm: ops.charm.CharmBase, relation_name: str) -> None:
"""Run constructor."""
super().__init__(charm, relation_name)
self.relation_name = relation_name
self.framework.observe(
charm.on[relation_name].relation_created, self.on_created
)
self.framework.observe(
charm.on[relation_name].relation_joined, self.on_joined
)
self.framework.observe(
charm.on[relation_name].relation_changed, self.on_changed
)
@property
def peers_rel(self) -> ops.model.Relation | None:
"""Peer relation."""
return self.framework.model.get_relation(self.relation_name)
@property
def _app_data_bag(self) -> Dict[str, str]:
"""Return all app data on peer relation."""
if not self.peers_rel:
return {}
return self.peers_rel.data[self.peers_rel.app]
def on_joined(self, event: ops.framework.EventBase) -> None:
"""Handle relation joined event."""
logging.info("Peer joined")
self.on.peers_relation_joined.emit()
def on_created(self, event: ops.framework.EventBase) -> None:
"""Handle relation created event."""
logging.info("Peers on_created")
self.on.peers_relation_created.emit()
def on_changed(self, event: ops.framework.EventBase) -> None:
"""Handle relation changed event."""
logging.info("Peers on_changed")
self.on.peers_data_changed.emit()
def set_app_data(self, settings: Dict[str, str]) -> None:
"""Publish settings on the peer app data bag."""
for k, v in settings.items():
self._app_data_bag[k] = v
def get_app_data(self, key: str) -> Optional[str]:
"""Get the value corresponding to key from the app data bag."""
if not self.peers_rel:
return None
return self._app_data_bag.get(key)
def get_all_app_data(self) -> Dict[str, str]:
"""Return all the app data from the relation."""
return self._app_data_bag
def get_all_unit_values(
self, key: str, include_local_unit: bool = False
) -> List[str]:
"""Retrieve value for key from all related units.
:param include_local_unit: Include value set by local unit
"""
values = []
if not self.peers_rel:
return values
for unit in self.peers_rel.units:
value = self.peers_rel.data[unit].get(key)
if value is not None:
values.append(value)
local_unit_value = self.peers_rel.data[self.model.unit].get(key)
if include_local_unit and local_unit_value:
values.append(local_unit_value)
return values
def set_unit_data(self, settings: Dict[str, str]) -> None:
"""Publish settings on the peer unit data bag."""
if not self.peers_rel:
return
for k, v in settings.items():
self.peers_rel.data[self.model.unit][k] = v
def all_joined_units(self) -> List[ops.model.Unit]:
"""All remote units joined to the peer relation."""
if not self.peers_rel:
return []
return set(self.peers_rel.units)
def expected_peer_units(self) -> int:
"""Return the Number of units expected on relation.
NOTE: This count includes this unit
"""
if not self.peers_rel:
return 0
return self.peers_rel.app.planned_units()