How to access a resource from your charm

See also: Model (ops.model.Model)

In order to make use of file resources in a charm, the Charmed Operator Framework provides a helper method to fetch the path of a file resource. Consider this simple resource definition:

resources:
  my-resource:
    type: file
    filename: somefile.txt
    description: test resource

In the charm code, we can now use Model.resources.fetch(<resource_name>) to get the path to the resource, then manipulate it as we need:

# ...
import logging
from ops.model import ModelError, BlockedStatus, ErrorStatus
# ...
logger = logging.getLogger(__name__)

def _on_config_changed(self, event):
    # Get the path to the file resource named 'my-resource'
    try:
        resource_path = self.model.resources.fetch("my-resource")
    except ModelError as e:
        self.unit.status = BlockedStatus(
            "Something went wrong when claiming resource 'my-resource; "
            "run `juju debug-log` for more info'"
        ) 
       # might actually be worth it to just reraise this exception and let the charm error out;
       # depends on whether we can recover from this.
        logger.error(e)
        return
    except NameError as e:
        self.unit.status = BlockedStatus(
            "Resource 'my-resource' not found; "
            "did you forget to declare it in metadata.yaml?"
        )
        logger.error(e)
        return

    # Open the file and read it
    with open(resource_path, "r") as f:
        content = f.read()
    # do something

The fetch() method will raise a ModelError if the resource does not exist, and returns a Python Path object to the resource if it does.

Note: During development, it may be useful to specify the resource at deploy time to facilitate faster testing without the need to publish a new charm/resource in between minor fixes. In the below snippet, we create a simple file with some text content, and pass it to the Juju controller to use in place of any published my-resource resource:

echo "TEST" > /tmp/somefile.txt
charmcraft pack
juju deploy ./my-charm.charm --resource my-resource=/tmp/somefile.txt

Hi,

Just to confirm, isn’t the exception NameError that raises instead of ModelError as it says in the documentation?

Thanks.

1 Like

It raises NameError if the resource is not known (you didn’t declare it in metadata.yaml), it raises ModelError if the resource can’t be fetched/opened by the backend for whatever reason. So I think it’d be a good idea to catch both in this example code.

1 Like

@ppasotti Would you have time to modify the example code to reflect both?

Done.

I do however have some doubts about whether it’s a good idea to catch that ModelError there, let me ask @pengale to confirm :slight_smile:

1 Like

Thanks, @ppasotti! PS I’m glad to see this is sparking a conversation.

@tmihoc @ppasotti I think that it is appropriate to catch the ModelError here. Something has gone wrong, and it is probably going to need human intervention to fix. Which is what the “blocked” status is for :slight_smile:

By the time that you get to the config changed hook, I believe that you have resources, if you’re going to have them.

@jameinel am I correct with that last statement?

I’ve talked myself both in and out of whether these should be Error or Blocked. I’m guessing talking through my decision tree is probably useful.

If you are getting NameError, that really is a charm author programming error. They are talking about something that they didn’t inform juju about in metadata.yaml. Similarly ModelError indicates some ‘unknown error’ that nobody is really able to predict. (Apparently a download/transfer/something else failed.) Is it useful that if install got a ModelError we would then progress the lifecycle and give you config-changed, or is it better to actually acknowledge that install could not be completed and we should hang the lifecycle for this unit.

That said, any time a charm goes into error it really is very hard to service anything as an Admin, as it generally implies you need an updated version of the Charm. It is plausible that there are 3 things your application does,and while 1 of them is fully broken, the other 2 are just fine.

The problem with Blocked also is that while install became blocked, since getting resource errors is not a common pattern, it is entirely likely that config-changed will override to say that “everything is happy” because the Charm author wouldn’t have thought to check that all of their resources are happy. Which means there would be something in the log at some point about going to blocked, but there is no ongoing communication to the user about its current failure mode.

(Which makes me think we need named blocks that need to be individually cleared rather than a global ‘Blocked’ y/n state. That also handles the “I have 3 relations and one of them is blocked but the other 2 are good”.)

I think @rwcarlsen might find your last point interesting, as he thinks about relations, handshakes, and the possibility of allowing charms to mark individual relations as broken/allright.

My overarching question is: is it EVER a good idea to catch (and not resurface) ModelErrors? I feel like a ModelError is not something a charm can recover from without a new charm version (or code changes somewhere, either way).

However as @jameinel says, if your charm can still do some other things in production (work at 80%) and something is better than nothing, then it should catch that error, log it, and do its best to keep the rest of it working. But it may be hard to let the user know that something went wrong because we only have 1 status per charm. Or we start abusing active to express some compound status:

statuses = {
  'storage_1': self.storage_1.get_status(), # 'Active' | 'Blocked' | 'Error'
  'storage_2': self.storage_2.get_status(),
  'relation-1': self.relation1_requirer.get_status(),
  'relation-2': self.relation2_provider.get_status()
}
unit.status = ActiveStatus(str(statuses))