Commit 17583c8b authored by Mark Beierl's avatar Mark Beierl
Browse files

Merge branch 'master' into 'master'

Update squid charm

See merge request !105
parents e2197769 9c012576
Pipeline #143 passed with stage
in 1 minute and 34 seconds
/**
* markupsafe._speedups
* ~~~~~~~~~~~~~~~~~~~~
*
* C implementation of escaping for better performance. Used instead of
* the native Python implementation when compiled.
*
* :copyright: 2010 Pallets
* :license: BSD-3-Clause
*/
#include <Python.h>
#if PY_MAJOR_VERSION < 3
#define ESCAPED_CHARS_TABLE_SIZE 63
#define UNICHR(x) (PyUnicode_AS_UNICODE((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL)));
static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE];
static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE];
#endif
static PyObject* markup;
static int
init_constants(void)
{
PyObject *module;
#if PY_MAJOR_VERSION < 3
/* mapping of characters to replace */
escaped_chars_repl['"'] = UNICHR("&#34;");
escaped_chars_repl['\''] = UNICHR("&#39;");
escaped_chars_repl['&'] = UNICHR("&amp;");
escaped_chars_repl['<'] = UNICHR("&lt;");
escaped_chars_repl['>'] = UNICHR("&gt;");
/* lengths of those characters when replaced - 1 */
memset(escaped_chars_delta_len, 0, sizeof (escaped_chars_delta_len));
escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \
escaped_chars_delta_len['&'] = 4;
escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3;
#endif
/* import markup type so that we can mark the return value */
module = PyImport_ImportModule("markupsafe");
if (!module)
return 0;
markup = PyObject_GetAttrString(module, "Markup");
Py_DECREF(module);
return 1;
}
#if PY_MAJOR_VERSION < 3
static PyObject*
escape_unicode(PyUnicodeObject *in)
{
PyUnicodeObject *out;
Py_UNICODE *inp = PyUnicode_AS_UNICODE(in);
const Py_UNICODE *inp_end = PyUnicode_AS_UNICODE(in) + PyUnicode_GET_SIZE(in);
Py_UNICODE *next_escp;
Py_UNICODE *outp;
Py_ssize_t delta=0, erepl=0, delta_len=0;
/* First we need to figure out how long the escaped string will be */
while (*(inp) || inp < inp_end) {
if (*inp < ESCAPED_CHARS_TABLE_SIZE) {
delta += escaped_chars_delta_len[*inp];
erepl += !!escaped_chars_delta_len[*inp];
}
++inp;
}
/* Do we need to escape anything at all? */
if (!erepl) {
Py_INCREF(in);
return (PyObject*)in;
}
out = (PyUnicodeObject*)PyUnicode_FromUnicode(NULL, PyUnicode_GET_SIZE(in) + delta);
if (!out)
return NULL;
outp = PyUnicode_AS_UNICODE(out);
inp = PyUnicode_AS_UNICODE(in);
while (erepl-- > 0) {
/* look for the next substitution */
next_escp = inp;
while (next_escp < inp_end) {
if (*next_escp < ESCAPED_CHARS_TABLE_SIZE &&
(delta_len = escaped_chars_delta_len[*next_escp])) {
++delta_len;
break;
}
++next_escp;
}
if (next_escp > inp) {
/* copy unescaped chars between inp and next_escp */
Py_UNICODE_COPY(outp, inp, next_escp-inp);
outp += next_escp - inp;
}
/* escape 'next_escp' */
Py_UNICODE_COPY(outp, escaped_chars_repl[*next_escp], delta_len);
outp += delta_len;
inp = next_escp + 1;
}
if (inp < inp_end)
Py_UNICODE_COPY(outp, inp, PyUnicode_GET_SIZE(in) - (inp - PyUnicode_AS_UNICODE(in)));
return (PyObject*)out;
}
#else /* PY_MAJOR_VERSION < 3 */
#define GET_DELTA(inp, inp_end, delta) \
while (inp < inp_end) { \
switch (*inp++) { \
case '"': \
case '\'': \
case '&': \
delta += 4; \
break; \
case '<': \
case '>': \
delta += 3; \
break; \
} \
}
#define DO_ESCAPE(inp, inp_end, outp) \
{ \
Py_ssize_t ncopy = 0; \
while (inp < inp_end) { \
switch (*inp) { \
case '"': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = '#'; \
*outp++ = '3'; \
*outp++ = '4'; \
*outp++ = ';'; \
break; \
case '\'': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = '#'; \
*outp++ = '3'; \
*outp++ = '9'; \
*outp++ = ';'; \
break; \
case '&': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = 'a'; \
*outp++ = 'm'; \
*outp++ = 'p'; \
*outp++ = ';'; \
break; \
case '<': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = 'l'; \
*outp++ = 't'; \
*outp++ = ';'; \
break; \
case '>': \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
outp += ncopy; ncopy = 0; \
*outp++ = '&'; \
*outp++ = 'g'; \
*outp++ = 't'; \
*outp++ = ';'; \
break; \
default: \
ncopy++; \
} \
inp++; \
} \
memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
}
static PyObject*
escape_unicode_kind1(PyUnicodeObject *in)
{
Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in);
Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in);
Py_UCS1 *outp;
PyObject *out;
Py_ssize_t delta = 0;
GET_DELTA(inp, inp_end, delta);
if (!delta) {
Py_INCREF(in);
return (PyObject*)in;
}
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta,
PyUnicode_IS_ASCII(in) ? 127 : 255);
if (!out)
return NULL;
inp = PyUnicode_1BYTE_DATA(in);
outp = PyUnicode_1BYTE_DATA(out);
DO_ESCAPE(inp, inp_end, outp);
return out;
}
static PyObject*
escape_unicode_kind2(PyUnicodeObject *in)
{
Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in);
Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in);
Py_UCS2 *outp;
PyObject *out;
Py_ssize_t delta = 0;
GET_DELTA(inp, inp_end, delta);
if (!delta) {
Py_INCREF(in);
return (PyObject*)in;
}
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535);
if (!out)
return NULL;
inp = PyUnicode_2BYTE_DATA(in);
outp = PyUnicode_2BYTE_DATA(out);
DO_ESCAPE(inp, inp_end, outp);
return out;
}
static PyObject*
escape_unicode_kind4(PyUnicodeObject *in)
{
Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in);
Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in);
Py_UCS4 *outp;
PyObject *out;
Py_ssize_t delta = 0;
GET_DELTA(inp, inp_end, delta);
if (!delta) {
Py_INCREF(in);
return (PyObject*)in;
}
out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111);
if (!out)
return NULL;
inp = PyUnicode_4BYTE_DATA(in);
outp = PyUnicode_4BYTE_DATA(out);
DO_ESCAPE(inp, inp_end, outp);
return out;
}
static PyObject*
escape_unicode(PyUnicodeObject *in)
{
if (PyUnicode_READY(in))
return NULL;
switch (PyUnicode_KIND(in)) {
case PyUnicode_1BYTE_KIND:
return escape_unicode_kind1(in);
case PyUnicode_2BYTE_KIND:
return escape_unicode_kind2(in);
case PyUnicode_4BYTE_KIND:
return escape_unicode_kind4(in);
}
assert(0); /* shouldn't happen */
return NULL;
}
#endif /* PY_MAJOR_VERSION < 3 */
static PyObject*
escape(PyObject *self, PyObject *text)
{
static PyObject *id_html;
PyObject *s = NULL, *rv = NULL, *html;
if (id_html == NULL) {
#if PY_MAJOR_VERSION < 3
id_html = PyString_InternFromString("__html__");
#else
id_html = PyUnicode_InternFromString("__html__");
#endif
if (id_html == NULL) {
return NULL;
}
}
/* we don't have to escape integers, bools or floats */
if (PyLong_CheckExact(text) ||
#if PY_MAJOR_VERSION < 3
PyInt_CheckExact(text) ||
#endif
PyFloat_CheckExact(text) || PyBool_Check(text) ||
text == Py_None)
return PyObject_CallFunctionObjArgs(markup, text, NULL);
/* if the object has an __html__ method that performs the escaping */
html = PyObject_GetAttr(text ,id_html);
if (html) {
s = PyObject_CallObject(html, NULL);
Py_DECREF(html);
if (s == NULL) {
return NULL;
}
/* Convert to Markup object */
rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
Py_DECREF(s);
return rv;
}
/* otherwise make the object unicode if it isn't, then escape */
PyErr_Clear();
if (!PyUnicode_Check(text)) {
#if PY_MAJOR_VERSION < 3
PyObject *unicode = PyObject_Unicode(text);
#else
PyObject *unicode = PyObject_Str(text);
#endif
if (!unicode)
return NULL;
s = escape_unicode((PyUnicodeObject*)unicode);
Py_DECREF(unicode);
}
else
s = escape_unicode((PyUnicodeObject*)text);
/* convert the unicode string into a markup object. */
rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
Py_DECREF(s);
return rv;
}
static PyObject*
escape_silent(PyObject *self, PyObject *text)
{
if (text != Py_None)
return escape(self, text);
return PyObject_CallFunctionObjArgs(markup, NULL);
}
static PyObject*
soft_unicode(PyObject *self, PyObject *s)
{
if (!PyUnicode_Check(s))
#if PY_MAJOR_VERSION < 3
return PyObject_Unicode(s);
#else
return PyObject_Str(s);
#endif
Py_INCREF(s);
return s;
}
static PyMethodDef module_methods[] = {
{"escape", (PyCFunction)escape, METH_O,
"escape(s) -> markup\n\n"
"Convert the characters &, <, >, ', and \" in string s to HTML-safe\n"
"sequences. Use this if you need to display text that might contain\n"
"such characters in HTML. Marks return value as markup string."},
{"escape_silent", (PyCFunction)escape_silent, METH_O,
"escape_silent(s) -> markup\n\n"
"Like escape but converts None to an empty string."},
{"soft_unicode", (PyCFunction)soft_unicode, METH_O,
"soft_unicode(object) -> string\n\n"
"Make a string unicode if it isn't already. That way a markup\n"
"string is not converted back to unicode."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
#if PY_MAJOR_VERSION < 3
#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
#define PyMODINIT_FUNC void
#endif
PyMODINIT_FUNC
init_speedups(void)
{
if (!init_constants())
return;
Py_InitModule3("markupsafe._speedups", module_methods, "");
}
#else /* Python 3.x module initialization */
static struct PyModuleDef module_definition = {
PyModuleDef_HEAD_INIT,
"markupsafe._speedups",
NULL,
-1,
module_methods,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit__speedups(void)
{
if (!init_constants())
return NULL;
return PyModule_Create(&module_definition);
}
#endif
Metadata-Version: 2.1
Name: oci-image
Version: 1.0.0
Summary: Helper for dealing with OCI Image resources in the charm operator framework
Home-page: https://github.com/juju-solutions/resource-oci-image
Author: Cory Johns
Author-email: johnsca@gmail.com
License: Apache License 2.0
Platform: UNKNOWN
# OCI Image Resource helper
This is a helper for working with OCI image resources in the charm operator
framework.
## Installation
Add it to your `requirements.txt`. Since it's not in PyPI, you'll need to use
the GitHub archive URL (or `git+` URL, if you want to pin to a specific commit):
```
https://github.com/juju-solutions/resource-oci-image/archive/master.zip
```
## Usage
The `OCIImageResource` class will wrap the framework resource for the given
resource name, and calling `fetch` on it will either return the image info
or raise an `OCIImageResourceError` if it can't fetch or parse the image
info. The exception will have a `status` attribute you can use directly,
or a `status_message` attribute if you just want that.
Example usage:
```python
from ops.charm import CharmBase
from ops.main import main
from oci_image import OCIImageResource, OCIImageResourceError
class MyCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.image = OCIImageResource(self, 'resource-name')
self.framework.observe(self.on.start, self.on_start)
def on_start(self, event):
try:
image_info = self.image.fetch()
except OCIImageResourceError as e:
self.model.unit.status = e.status
event.defer()
return
self.model.pod.set_spec({'containers': [{
'name': 'my-charm',
'imageDetails': image_info,
}]})
if __name__ == "__main__":
main(MyCharm)
```
__pycache__/oci_image.cpython-38.pyc,,
oci_image-1.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
oci_image-1.0.0.dist-info/METADATA,sha256=QIpPa4JcSPa_Ci0n-DaCNp4PkKovZudFW8FnpnauJnQ,1808
oci_image-1.0.0.dist-info/RECORD,,
oci_image-1.0.0.dist-info/WHEEL,sha256=EVRjI69F5qVjm_YgqcTXPnTAv3BfSUr0WVAHuSP3Xoo,92
oci_image-1.0.0.dist-info/top_level.txt,sha256=M4dLaObLx7irI4EO-A4_VJP_b-A6dDD7hB5QyVKdHOY,10
oci_image.py,sha256=c75VR2vSmOp9pPTP2cnsxo23CqhhFbRtnIOtMjzDyXY,1794
Wheel-Version: 1.0
Generator: bdist_wheel (0.35.1)
Root-Is-Purelib: true
Tag: py3-none-any
from pathlib import Path
import yaml
from ops.framework import Object
from ops.model import BlockedStatus, ModelError
class OCIImageResource(Object):
def __init__(self, charm, resource_name):
super().__init__(charm, resource_name)
self.resource_name = resource_name
def fetch(self):
try:
resource_path = self.model.resources.fetch(self.resource_name)
except ModelError as e:
raise MissingResourceError(self.resource_name) from e
if not resource_path.exists():
raise MissingResourceError(self.resource_name)
resource_text = Path(resource_path).read_text()
if not resource_text:
raise MissingResourceError(self.resource_name)
try:
resource_data = yaml.safe_load(resource_text)
except yaml.YAMLError as e:
raise InvalidResourceError(self.resource_name) from e
else:
# Translate the data from the format used by the charm store to the
# format used by the Juju K8s pod spec, since that is how this is
# typically used.
return {
'imagePath': resource_data['registrypath'],
'username': resource_data['username'],
'password': resource_data['password'],
}
class OCIImageResourceError(ModelError):
status_type = BlockedStatus
status_message = 'Resource error'
def __init__(self, resource_name):
super().__init__(resource_name)
self.status = self.status_type(
f'{self.status_message}: {resource_name}')
class MissingResourceError(OCIImageResourceError):
status_message = 'Missing resource'
class InvalidResourceError(OCIImageResourceError):
status_message = 'Invalid resource'
Metadata-Version: 2.1
Name: ops
Version: 1.1.0
Summary: The Python library behind great charms
Home-page: https://github.com/canonical/operator
Author: The Charmcraft team at Canonical Ltd.
Author-email: charmcraft@lists.launchpad.net
License: Apache-2.0
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX :: Linux
Requires-Python: >=3.5
Description-Content-Type: text/markdown
Requires-Dist: PyYAML
# The Operator Framework
This Operator Framework simplifies [Kubernetes
operator](https://charmhub.io/about) development for
[model-driven application
management](https://juju.is/model-driven-operations).
A Kubernetes operator is a container that drives lifecycle management,
configuration, integration and daily actions for an application.
Operators simplify software management and operations. They capture
reusable app domain knowledge from experts in a software component that
can be shared.
This project extends the operator pattern to enable
[universal operators](https://juju.is/universal-operators), not just
for Kubernetes but also operators for traditional Linux or Windows
application management.
Operators use an [Operator Lifecycle Manager
(OLM)](https://juju.is/operator-lifecycle-manager) to coordinate their
work in a cluster. The system uses Golang for concurrent event
processing under the hood, but enables the operators to be written in
Python.
## Simple, composable operators
Operators should 'do one thing and do it well'. Each operator drives a
single microservice and can be [composed with other
operators](https://juju.is/integration) to deliver a complex application.
It is better to have small, reusable operators that each drive a single
microservice very well. The operator handles instantiation, scaling,
configuration, optimisation, networking, service mesh, observability,
and day-2 operations specific to that microservice.
Operator composition takes place through declarative integration in
the OLM. Operators declare integration endpoints, and discover lines of
integration between those endpoints dynamically at runtime.
## Pure Python operators
The framework provides a standard Python library and object model that
represents the application graph, and an event distribution mechanism for
distributed system coordination and communication.
The OLM is written in Golang for efficient concurrency in event handling
and distribution. Operators can be written in any language. We recommend
this Python framework for ease of design, development and collaboration.
## Better collaboration
Operator developers publish Python libraries that make it easy to integrate
your operator with their operator. The framework includes standard tools
to distribute these integration libraries and keep them up to date.
Development collaboration happens at [Charmhub.io](https://charmhub.io/) where
operators are published along with integration libraries. Design and
code review discussions are hosted in the
[Charmhub forum](https://discourse.charmhub.io/). We recommend the
[Open Operator Manifesto](https://charmhub.io/manifesto) as a guideline for
high quality operator engineering.
## Event serialization and operator services
Distributed systems can be hard! So this framework exists to make it much
simpler to reason about operator behaviour, especially in complex deployments.
The OLM provides [operator services](https://juju.is/operator-services) such
as provisioning, event delivery, leader election and model management.
Coordination between operators is provided by a cluster-wide event
distribution system. Events are serialized to avoid race conditions in any
given container or machine. This greatly simplifies the development of
operators for high availability, scale-out and integrated applications.
## Model-driven Operator Lifecycle Manager
A key goal of the project is to improve the user experience for admins
working with multiple different operators.
We embrace [model-driven operations](https://juju.is/model-driven-operations)
in the Operator Lifecycle Manager. The model encompasses capacity,
storage, networking, the application graph and administrative access.
Admins describe the application graph of integrated microservices, and
the OLM then drives instantiation. A change in the model is propagated
to all affected operators, reducing the duplication of effort and
repetition normally found in operating a complex topology of services.
Administrative actions, updates, configuration and integration are all
driven through the OLM.
# Getting started
A package of operator code is called a charm. You will use `charmcraft`
to register your operator name, and publish it when you are ready.
```
$ sudo snap install charmcraft --beta
charmcraft (beta) 0.6.0 from John Lenton (chipaca) installed
```
Charms written using the operator framework are just Python code. The goal
is to feel natural for somebody used to coding in Python, and reasonably
easy to learn for somebody who is not a pythonista.
The dependencies of the operator framework are kept as minimal as possible;
currently that's Python 3.5 or greater, and `PyYAML` (both are included by
default in Ubuntu's cloud images from 16.04 on).
# A quick introduction
Make an empty directory `my-charm` and cd into it. Then start a new charm
with:
```
$ charmcraft init
All done.
There are some notes about things we think you should do.
These are marked with ‘TODO:’, as is customary. Namely:
README.md: fill out the description
README.md: explain how to use the charm
metadata.yaml: fill out the charm's description
metadata.yaml: fill out the charm's summary
```
Charmed operators are just Python code. The entry point to your charm can
be any filename, by default this is `src/charm.py` which must be executable
(and probably have `#!/usr/bin/env python3` on the first line).
You need a `metadata.yaml` to describe your charm, and if you will support
configuration of your charm then `config.yaml` files is required too. The
`requirements.txt` specifies any Python dependencies.
```
$ tree my-charm/
my-charm/
├── actions.yaml
├── config.yaml
├── LICENSE
├── metadata.yaml
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── run_tests
├── src
│   └── charm.py
├── tests
│   ├── __init__.py
│   └── my_charm.py
```
`src/charm.py` here is the entry point to your charm code. At a minimum, it
needs to define a subclass of `CharmBase` and pass that into the framework
`main` function:
```python
from ops.charm import CharmBase
from ops.main import main
class MyCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.start, self.on_start)
def on_start(self, event):
# Handle the start event here.
if __name__ == "__main__":
main(MyCharm)
```
That should be enough for you to be able to run
```
$ charmcraft build
Done, charm left in 'my-charm.charm'
$ juju deploy ./my-charm.charm
```
> 🛈 More information on [`charmcraft`](https://pypi.org/project/charmcraft/) can
> also be found on its [github page](https://github.com/canonical/charmcraft).
Happy charming!
# Testing your charms
The operator framework provides a testing harness, so you can check your
charm does the right thing in different scenarios, without having to create
a full deployment. `pydoc3 ops.testing` has the details, including this
example:
```python
harness = Harness(MyCharm)
# Do initial setup here
relation_id = harness.add_relation('db', 'postgresql')
# Now instantiate the charm to see events as the model changes
harness.begin()
harness.add_relation_unit(relation_id, 'postgresql/0')
harness.update_relation_data(relation_id, 'postgresql/0', {'key': 'val'})
# Check that charm has properly handled the relation_joined event for postgresql/0
self.assertEqual(harness.charm. ...)
```
## Talk to us
If you need help, have ideas, or would just like to chat with us, reach out on
IRC: we're in [#smooth-operator] on freenode (or try the [webchat]).
We also pay attention to [Charmhub discourse](https://discourse.charmhub.io/)
You can also deep dive into the [API docs] if that's your thing.
[webchat]: https://webchat.freenode.net/#smooth-operator
[#smooth-operator]: irc://chat.freenode.net/%23smooth-operator
[discourse]: https://discourse.juju.is/c/charming
[API docs]: https://ops.rtfd.io/
## Operator Framework development
To work in the framework itself you will need Python >= 3.5 and the
dependencies in `requirements-dev.txt` installed in your system, or a
virtualenv:
virtualenv --python=python3 env
source env/bin/activate
pip install -r requirements-dev.txt
Then you can try `./run_tests`, it should all go green.
For improved performance on the tests, ensure that you have PyYAML
installed with the correct extensions:
apt-get install libyaml-dev
pip install --force-reinstall --no-cache-dir pyyaml
If you want to build the documentation you'll need the requirements from
`docs/requirements.txt`, or in your virtualenv
pip install -r docs/requirements.txt
and then you can run `./build_docs`.
ops-1.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
ops-1.1.0.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
ops-1.1.0.dist-info/METADATA,sha256=ffVuqPnEob6-iBYjEf3lPShSbToJL17obFFufoW2F4g,9485
ops-1.1.0.dist-info/RECORD,,
ops-1.1.0.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
ops-1.1.0.dist-info/top_level.txt,sha256=enC05wWafSg8iDKIvj3gvtAtEP2kYCyN5Gmd689q-_I,4
ops/__init__.py,sha256=WaHb0dfp1KEe6jFV8Pm_mcdJ3ModiWujnQ6xLjNzPNQ,819
ops/__pycache__/__init__.cpython-38.pyc,,
ops/__pycache__/charm.cpython-38.pyc,,
ops/__pycache__/framework.cpython-38.pyc,,
ops/__pycache__/jujuversion.cpython-38.pyc,,
ops/__pycache__/log.cpython-38.pyc,,
ops/__pycache__/main.cpython-38.pyc,,
ops/__pycache__/model.cpython-38.pyc,,
ops/__pycache__/storage.cpython-38.pyc,,
ops/__pycache__/testing.cpython-38.pyc,,
ops/__pycache__/version.cpython-38.pyc,,
ops/charm.py,sha256=7KyaNNA0t_a0h0hrzehSEWm4xU_Y5JIqGWHTg747qfU,32817
ops/framework.py,sha256=1ByOtFKRR6kRzOEbfWnGEMNevixOYf18U0oZxKq8LsA,43769
ops/jujuversion.py,sha256=9wMlUmngcAENV9RkgVVLWtZsyRQaf6XNrQQqUeY_fHA,4139
ops/lib/__init__.py,sha256=QizPpuRWXjqbH5Gv7mnH8CcPR9BX7q2YNFnxyoSsA0g,9213
ops/lib/__pycache__/__init__.cpython-38.pyc,,
ops/log.py,sha256=JVpt_Vkf_lWO2cucUcJfXjAWVTattk4xBscSs65Sn3I,2155
ops/main.py,sha256=BUJZM4soFpsY4bO6zJ1bSHQeWJcm028gq0MhJT3rC8M,15523
ops/model.py,sha256=yvM1yhidNyGpVdxkG365jPJRhQuE42EiiojBHJ7tL3c,47930
ops/storage.py,sha256=jEfszzQGYDrl5wa03I6txvea-7lI661Yq6n7sIPa0fU,14192
ops/testing.py,sha256=sH8PoNzGmfPdVWM1lBjStxHcNfQHsasFjF-WzHfDhFA,34898
ops/version.py,sha256=UuaLFU_UN-InNFu4I23Y22huxQdbsOgTQ_d_r623fx4,46
Wheel-Version: 1.0
Generator: bdist_wheel (0.34.2)
Root-Is-Purelib: true
Tag: py3-none-any
......@@ -14,5 +14,7 @@
"""The Operator Framework."""
from .version import version as __version__ # noqa: F401 (imported but unused)
# Import here the bare minimum to break the circular import between modules
from . import charm # NOQA
from . import charm # noqa: F401 (imported but unused)
......@@ -12,6 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Base objects for the Charm, events and metadata."""
import enum
import os
import pathlib
import typing
......@@ -22,31 +25,60 @@ from ops.framework import Object, EventSource, EventBase, Framework, ObjectEvent
from ops import model
def _loadYaml(source):
if yaml.__with_libyaml__:
return yaml.load(source, Loader=yaml.CSafeLoader)
return yaml.load(source, Loader=yaml.SafeLoader)
class HookEvent(EventBase):
"""A base class for events that trigger because of a Juju hook firing."""
"""Events raised by Juju to progress a charm's lifecycle.
Hooks are callback methods of a charm class (a subclass of
:class:`CharmBase`) that are invoked in response to events raised
by Juju. These callback methods are the means by which a charm
governs the lifecycle of its application.
The :class:`HookEvent` class is the base of a type hierarchy of events
related to the charm's lifecycle.
:class:`HookEvent` subtypes are grouped into the following categories
- Core lifecycle events
- Relation events
- Storage events
- Metric events
"""
class ActionEvent(EventBase):
"""A base class for events that trigger when a user asks for an Action to be run.
"""Events raised by Juju when an administrator invokes a Juju Action.
To read the parameters for the action, see the instance variable `params`.
To respond with the result of the action, call `set_results`. To add progress
messages that are visible as the action is progressing use `log`.
This class is the data type of events triggered when an administrator
invokes a Juju Action. Callbacks bound to these events may be used
for responding to the administrator's Juju Action request.
:ivar params: The parameters passed to the action (read by action-get)
To read the parameters for the action, see the instance variable :attr:`params`.
To respond with the result of the action, call :meth:`set_results`. To add
progress messages that are visible as the action is progressing use
:meth:`log`.
Attributes:
params: The parameters passed to the action.
"""
def defer(self):
"""Action events are not deferable like other events.
This is because an action runs synchronously and the user is waiting for the result.
This is because an action runs synchronously and the administrator
is waiting for the result.
"""
raise RuntimeError('cannot defer action events')
def restore(self, snapshot: dict) -> None:
"""Used by the operator framework to record the action.
Not meant to be called directly by Charm code.
Not meant to be called directly by charm code.
"""
env_action_name = os.environ.get('JUJU_ACTION_NAME')
event_action_name = self.handle.kind[:-len('_action')].replace('_', '-')
......@@ -83,92 +115,157 @@ class ActionEvent(EventBase):
class InstallEvent(HookEvent):
"""Represents the `install` hook from Juju."""
"""Event triggered when a charm is installed.
This event is triggered at the beginning of a charm's
lifecycle. Any associated callback method should be used to
perform one-time setup operations, such as installing prerequisite
software.
"""
class StartEvent(HookEvent):
"""Represents the `start` hook from Juju."""
"""Event triggered immediately after first configuation change.
This event is triggered immediately after the first
:class:`ConfigChangedEvent`. Callback methods bound to the event should be
used to ensure that the charm’s software is in a running state. Note that
the charm’s software should be configured so as to persist in this state
through reboots without further intervention on Juju’s part.
"""
class StopEvent(HookEvent):
"""Represents the `stop` hook from Juju."""
"""Event triggered when a charm is shut down.
This event is triggered when an application's removal is requested
by the client. The event fires immediately before the end of the
unit’s destruction sequence. Callback methods bound to this event
should be used to ensure that the charm’s software is not running,
and that it will not start again on reboot.
"""
class RemoveEvent(HookEvent):
"""Represents the `remove` hook from Juju. """
"""Event triggered when a unit is about to be terminated.
This event fires prior to Juju removing the charm and terminating its unit.
"""
class ConfigChangedEvent(HookEvent):
"""Represents the `config-changed` hook from Juju."""
"""Event triggered when a configuration change is requested.
This event fires in several different situations.
- immediately after the :class:`install <InstallEvent>` event.
- after a :class:`relation is created <RelationCreatedEvent>`.
- after a :class:`leader is elected <LeaderElectedEvent>`.
- after changing charm configuration using the GUI or command line
interface
- when the charm :class:`starts <StartEvent>`.
- when a new unit :class:`joins a relation <RelationJoinedEvent>`.
- when there is a :class:`change to an existing relation <RelationChangedEvent>`.
Any callback method bound to this event cannot assume that the
software has already been started; it should not start stopped
software, but should (if appropriate) restart running software to
take configuration changes into account.
"""
class UpdateStatusEvent(HookEvent):
"""Represents the `update-status` hook from Juju."""
"""Event triggered by a status update request from Juju.
This event is periodically triggered by Juju so that it can
provide constant feedback to the administrator about the status of
the application the charm is modeling. Any callback method bound
to this event should determine the "health" of the application and
set the status appropriately.
The interval between :class:`update-status <UpdateStatusEvent>` events can
be configured model-wide, e.g. ``juju model-config
update-status-hook-interval=1m``.
"""
class UpgradeCharmEvent(HookEvent):
"""Represents the `upgrade-charm` hook from Juju.
This will be triggered when a user has run `juju upgrade-charm`. It is run after Juju
has unpacked the upgraded charm code, and so this event will be handled with new code.
class UpgradeCharmEvent(HookEvent):
"""Event triggered by request to upgrade the charm.
This event will be triggered when an administrator executes ``juju
upgrade-charm``. The event fires after Juju has unpacked the upgraded charm
code, and so this event will be handled by the callback method bound to the
event in the new codebase. The associated callback method is invoked
provided there is no existing error state. The callback method should be
used to reconcile current state written by an older version of the charm
into whatever form that is needed by the current charm version.
"""
class PreSeriesUpgradeEvent(HookEvent):
"""Represents the `pre-series-upgrade` hook from Juju.
This happens when a user has run `juju upgrade-series MACHINE prepare` and
will fire for each unit that is running on the machine, telling them that
the user is preparing to upgrade the Machine's series (eg trusty->bionic).
The charm should take actions to prepare for the upgrade (a database charm
would want to write out a version-independent dump of the database, so that
when a new version of the database is available in a new series, it can be
used.)
Once all units on a machine have run `pre-series-upgrade`, the user will
initiate the steps to actually upgrade the machine (eg `do-release-upgrade`).
When the upgrade has been completed, the :class:`PostSeriesUpgradeEvent` will fire.
"""Event triggered to prepare a unit for series upgrade.
This event triggers when an administrator executes ``juju upgrade-series
MACHINE prepare``. The event will fire for each unit that is running on the
specified machine. Any callback method bound to this event must prepare the
charm for an upgrade to the series. This may include things like exporting
database content to a version neutral format, or evacuating running
instances to other machines.
It can be assumed that only after all units on a machine have executed the
callback method associated with this event, the administrator will initiate
steps to actually upgrade the series. After the upgrade has been completed,
the :class:`PostSeriesUpgradeEvent` will fire.
"""
class PostSeriesUpgradeEvent(HookEvent):
"""Represents the `post-series-upgrade` hook from Juju.
This is run after the user has done a distribution upgrade (or rolled back
and kept the same series). It is called in response to
`juju upgrade-series MACHINE complete`. Charms are expected to do whatever
steps are necessary to reconfigure their applications for the new series.
"""Event triggered after a series upgrade.
This event is triggered after the administrator has done a distribution
upgrade (or rolled back and kept the same series). It is called in response
to ``juju upgrade-series MACHINE complete``. Associated charm callback
methods are expected to do whatever steps are necessary to reconfigure their
applications for the new series. This may include things like populating the
upgraded version of a database. Note however charms are expected to check if
the series has actually changed or whether it was rolled back to the
original series.
"""
class LeaderElectedEvent(HookEvent):
"""Represents the `leader-elected` hook from Juju.
Juju will trigger this when a new lead unit is chosen for a given application.
This represents the leader of the charm information (not necessarily the primary
of a running application). The main utility is that charm authors can know
that only one unit will be a leader at any given time, so they can do
configuration, etc, that would otherwise require coordination between units.
(eg, selecting a password for a new relation)
"""Event triggered when a new leader has been elected.
Juju will trigger this event when a new leader unit is chosen for
a given application.
This event fires at least once after Juju selects a leader
unit. Callback methods bound to this event may take any action
required for the elected unit to assert leadership. Note that only
the elected leader unit will receive this event.
"""
class LeaderSettingsChangedEvent(HookEvent):
"""Represents the `leader-settings-changed` hook from Juju.
"""Event triggered when leader changes any settings.
Deprecated. This represents when a lead unit would call `leader-set` to inform
the other units of an application that they have new information to handle.
This has been deprecated in favor of using a Peer relation, and having the
leader set a value in the Application data bag for that peer relation.
(see :class:`RelationChangedEvent`).
DEPRECATED NOTICE
This event has been deprecated in favor of using a Peer relation,
and having the leader set a value in the Application data bag for
that peer relation. (see :class:`RelationChangedEvent`).
"""
class CollectMetricsEvent(HookEvent):
"""Represents the `collect-metrics` hook from Juju.
"""Event triggered by Juju to collect metrics.
Juju fires this event every five minutes for the lifetime of the
unit. Callback methods bound to this event may use the :meth:`add_metrics`
method of this class to send measurements to Juju.
Note that events firing during a CollectMetricsEvent are currently
sandboxed in how they can interact with Juju. To report metrics
use :meth:`.add_metrics`.
Note that associated callback methods are currently sandboxed in
how they can interact with Juju.
"""
def add_metrics(self, metrics: typing.Mapping, labels: typing.Mapping = None) -> None:
......@@ -186,15 +283,21 @@ class CollectMetricsEvent(HookEvent):
class RelationEvent(HookEvent):
"""A base class representing the various relation lifecycle events.
Charmers should not be creating RelationEvents directly. The events will be
generated by the framework from Juju related events. Users can observe them
from the various `CharmBase.on[relation_name].relation_*` events.
Relation lifecycle events are generated when application units
participate in relations. Units can only participate in relations
after they have been "started", and before they have been
"stopped". Within that time window, the unit may participate in
several different relations at a time, including multiple
relations with the same name.
Attributes:
relation: The Relation involved in this event
app: The remote application that has triggered this event
unit: The remote unit that has triggered this event. This may be None
if the relation event was triggered as an Application level event
relation: The :class:`~ops.model.Relation` involved in this event
app: The remote :class:`~ops.model.Application` that has triggered this
event
unit: The remote unit that has triggered this event. This may be
``None`` if the relation event was triggered as an
:class:`~ops.model.Application` level event
"""
def __init__(self, handle, relation, app=None, unit=None):
......@@ -211,7 +314,7 @@ class RelationEvent(HookEvent):
def snapshot(self) -> dict:
"""Used by the framework to serialize the event to disk.
Not meant to be called by Charm code.
Not meant to be called by charm code.
"""
snapshot = {
'relation_name': self.relation.name,
......@@ -226,7 +329,7 @@ class RelationEvent(HookEvent):
def restore(self, snapshot: dict) -> None:
"""Used by the framework to deserialize the event from disk.
Not meant to be called by Charm code.
Not meant to be called by charm code.
"""
self.relation = self.framework.model.get_relation(
snapshot['relation_name'], snapshot['relation_id'])
......@@ -245,7 +348,7 @@ class RelationEvent(HookEvent):
class RelationCreatedEvent(RelationEvent):
"""Represents the `relation-created` hook from Juju.
"""Event triggered when a new relation is created.
This is triggered when a new relation to another app is added in Juju. This
can occur before units for those applications have started. All existing
......@@ -254,62 +357,146 @@ class RelationCreatedEvent(RelationEvent):
class RelationJoinedEvent(RelationEvent):
"""Represents the `relation-joined` hook from Juju.
This is triggered whenever a new unit of a related application joins the relation.
(eg, a unit was added to an existing related app, or a new relation was established
with an application that already had units.)
"""Event triggered when a new unit joins a relation.
This event is triggered whenever a new unit of a related
application joins the relation. The event fires only when that
remote unit is first observed by the unit. Callback methods bound
to this event may set any local unit settings that can be
determined using no more than the name of the joining unit and the
remote ``private-address`` setting, which is always available when
the relation is created and is by convention not deleted.
"""
class RelationChangedEvent(RelationEvent):
"""Represents the `relation-changed` hook from Juju.
This is triggered whenever there is a change to the data bucket for a related
application or unit. Look at `event.relation.data[event.unit/app]` to see the
new information.
"""Event triggered when relation data changes.
This event is triggered whenever there is a change to the data bucket for a
related application or unit. Look at ``event.relation.data[event.unit/app]``
to see the new information, where ``event`` is the event object passed to
the callback method bound to this event.
This event always fires once, after :class:`RelationJoinedEvent`, and
will subsequently fire whenever that remote unit changes its settings for
the relation. Callback methods bound to this event should be the only ones
that rely on remote relation settings. They should not error if the settings
are incomplete, since it can be guaranteed that when the remote unit or
application changes its settings, the event will fire again.
The settings that may be queried, or set, are determined by the relation’s
interface.
"""
class RelationDepartedEvent(RelationEvent):
"""Represents the `relation-departed` hook from Juju.
This is the inverse of the RelationJoinedEvent, representing when a unit
is leaving the relation (the unit is being removed, the app is being removed,
the relation is being removed). It is fired once for each unit that is
going away.
"""Event triggered when a unit leaves a relation.
This is the inverse of the :class:`RelationJoinedEvent`, representing when a
unit is leaving the relation (the unit is being removed, the app is being
removed, the relation is being removed). It is fired once for each unit that
is going away.
When the remote unit is known to be leaving the relation, this will result
in the :class:`RelationChangedEvent` firing at least once, after which the
:class:`RelationDepartedEvent` will fire. The :class:`RelationDepartedEvent`
will fire once only. Once the :class:`RelationDepartedEvent` has fired no
further :class:`RelationChangedEvent` will fire.
Callback methods bound to this event may be used to remove all
references to the departing remote unit, because there’s no
guarantee that it’s still part of the system; it’s perfectly
probable (although not guaranteed) that the system running that
unit has already shut down.
Once all callback methods bound to this event have been run for such a
relation, the unit agent will fire the :class:`RelationBrokenEvent`.
"""
class RelationBrokenEvent(RelationEvent):
"""Represents the `relation-broken` hook from Juju.
If a relation is being removed (`juju remove-relation` or `juju remove-application`),
once all the units have been removed, RelationBrokenEvent will fire to signal
that the relationship has been fully terminated.
"""Event triggered when a relation is removed.
If a relation is being removed (``juju remove-relation`` or ``juju
remove-application``), once all the units have been removed, this event will
fire to signal that the relationship has been fully terminated.
The event indicates that the current relation is no longer valid, and that
the charm’s software must be configured as though the relation had never
existed. It will only be called after every callback method bound to
:class:`RelationDepartedEvent` has been run. If a callback method
bound to this event is being executed, it is gauranteed that no remote units
are currently known locally.
"""
class StorageEvent(HookEvent):
"""Base class representing Storage related events."""
"""Base class representing storage-related events.
Juju can provide a variety of storage types to a charms. The
charms can define several different types of storage that are
allocated from Juju. Changes in state of storage trigger sub-types
of :class:`StorageEvent`.
"""
class StorageAttachedEvent(StorageEvent):
"""Represents the `storage-attached` hook from Juju.
"""Event triggered when new storage becomes available.
This event is triggered when new storage is available for the
charm to use.
Called when new storage is available for the charm to use.
Callback methods bound to this event allow the charm to run code
when storage has been added. Such methods will be run before the
:class:`InstallEvent` fires, so that the installation routine may
use the storage. The name prefix of this hook will depend on the
storage key defined in the ``metadata.yaml`` file.
"""
class StorageDetachingEvent(StorageEvent):
"""Represents the `storage-detaching` hook from Juju.
"""Event triggered prior to removal of storage.
Called when storage a charm has been using is going away.
This event is triggered when storage a charm has been using is
going away.
Callback methods bound to this event allow the charm to run code
before storage is removed. Such methods will be run before storage
is detached, and always before the :class:`StopEvent` fires, thereby
allowing the charm to gracefully release resources before they are
removed and before the unit terminates. The name prefix of the
hook will depend on the storage key defined in the ``metadata.yaml``
file.
"""
class CharmEvents(ObjectEvents):
"""The events that are generated by Juju in response to the lifecycle of an application."""
"""Events generated by Juju pertaining to application lifecycle.
This class is used to create an event descriptor (``self.on``) attribute for
a charm class that inherits from :class:`CharmBase`. The event descriptor
may be used to set up event handlers for corresponding events.
By default the following events will be provided through
:class:`CharmBase`::
self.on.install
self.on.start
self.on.remove
self.on.update_status
self.on.config_changed
self.on.upgrade_charm
self.on.pre_series_upgrade
self.on.post_series_upgrade
self.on.leader_elected
self.on.collect_metrics
In addition to these, depending on the charm's metadata (``metadata.yaml``),
named relation and storage events may also be defined. These named events
are created by :class:`CharmBase` using charm metadata. The named events may be
accessed as ``self.on[<name>].<relation_or_storage_event>``
"""
install = EventSource(InstallEvent)
start = EventSource(StartEvent)
......@@ -326,28 +513,51 @@ class CharmEvents(ObjectEvents):
class CharmBase(Object):
"""Base class that represents the Charm overall.
"""Base class that represents the charm overall.
:class:`CharmBase` is used to create a charm. This is done by inheriting
from :class:`CharmBase` and customising the sub class as required. So to
create your own charm, say ``MyCharm``, define a charm class and set up the
required event handlers (“hooks”) in its constructor::
import logging
from ops.charm import CharmBase
from ops.main import main
logger = logging.getLogger(__name__)
def MyCharm(CharmBase):
def __init__(self, *args):
logger.debug('Initializing Charm')
super().__init__(*args)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.stop, self._on_stop)
# ...
Usually this initialization is done by ops.main.main() rather than Charm authors
directly instantiating a Charm.
if __name__ == "__main__":
main(MyCharm)
As shown in the example above, a charm class is instantiated by
:func:`~ops.main.main` rather than charm authors directly instantiating a
charm.
Args:
framework: The framework responsible for managing the Model and events for this
Charm.
key: Arbitrary key to distinguish this instance of CharmBase from another.
Generally is None when initialized by the framework. For charms instantiated by
main.main(), this is currenly None.
Attributes:
on: Defines all events that the Charm will fire.
charm.
key: Ignored; will remove after deprecation period of the signature change.
"""
# note that without the #: below, sphinx will copy the whole of CharmEvents
# docstring inline which is less than ideal.
#: Used to set up event handlers; see :class:`CharmEvents`.
on = CharmEvents()
def __init__(self, framework: Framework, key: typing.Optional[str]):
"""Initialize the Charm with its framework and application name.
"""
super().__init__(framework, key)
def __init__(self, framework: Framework, key: typing.Optional = None):
super().__init__(framework, None)
for relation_name in self.framework.meta.relations:
relation_name = relation_name.replace('-', '_')
......@@ -378,32 +588,38 @@ class CharmBase(Object):
@property
def meta(self) -> 'CharmMeta':
"""CharmMeta of this charm.
"""
"""Metadata of this charm."""
return self.framework.meta
@property
def charm_dir(self) -> pathlib.Path:
"""Root directory of the Charm as it is running.
"""
"""Root directory of the charm as it is running."""
return self.framework.charm_dir
@property
def config(self) -> model.ConfigData:
"""A mapping containing the charm's config and current values."""
return self.model.config
class CharmMeta:
"""Object containing the metadata for the charm.
This is read from metadata.yaml and/or actions.yaml. Generally charms will
define this information, rather than reading it at runtime. This class is
mostly for the framework to understand what the charm has defined.
This is read from ``metadata.yaml`` and/or ``actions.yaml``. Generally
charms will define this information, rather than reading it at runtime. This
class is mostly for the framework to understand what the charm has defined.
The maintainers, tags, terms, series, and extra_bindings attributes are all
lists of strings. The requires, provides, peers, relations, storage,
resources, and payloads attributes are all mappings of names to instances
of the respective RelationMeta, StorageMeta, ResourceMeta, or PayloadMeta.
The :attr:`maintainers`, :attr:`tags`, :attr:`terms`, :attr:`series`, and
:attr:`extra_bindings` attributes are all lists of strings. The
:attr:`requires`, :attr:`provides`, :attr:`peers`, :attr:`relations`,
:attr:`storages`, :attr:`resources`, and :attr:`payloads` attributes are all
mappings of names to instances of the respective :class:`RelationMeta`,
:class:`StorageMeta`, :class:`ResourceMeta`, or :class:`PayloadMeta`.
The relations attribute is a convenience accessor which includes all of the
requires, provides, and peers RelationMeta items. If needed, the role of
the relation definition can be obtained from its role attribute.
The :attr:`relations` attribute is a convenience accessor which includes all
of the ``requires``, ``provides``, and ``peers`` :class:`RelationMeta`
items. If needed, the role of the relation definition can be obtained from
its :attr:`role <RelationMeta.role>` attribute.
Attributes:
name: The name of this charm
......@@ -435,6 +651,7 @@ class CharmMeta:
Args:
raw: a mapping containing the contents of metadata.yaml
actions_raw: a mapping containing the contents of actions.yaml
"""
def __init__(self, raw: dict = {}, actions_raw: dict = {}):
......@@ -451,13 +668,11 @@ class CharmMeta:
self.series = raw.get('series', [])
self.subordinate = raw.get('subordinate', False)
self.min_juju_version = raw.get('min-juju-version')
self.requires = {name: RelationMeta('requires', name, rel)
self.requires = {name: RelationMeta(RelationRole.requires, name, rel)
for name, rel in raw.get('requires', {}).items()}
self.provides = {name: RelationMeta('provides', name, rel)
self.provides = {name: RelationMeta(RelationRole.provides, name, rel)
for name, rel in raw.get('provides', {}).items()}
# TODO: (jam 2020-05-11) The *role* should be 'peer' even though it comes from the
# 'peers' section.
self.peers = {name: RelationMeta('peers', name, rel)
self.peers = {name: RelationMeta(RelationRole.peer, name, rel)
for name, rel in raw.get('peers', {}).items()}
self.relations = {}
self.relations.update(self.requires)
......@@ -483,28 +698,53 @@ class CharmMeta:
This can be a simple string, or a file-like object. (passed to `yaml.safe_load`).
actions: YAML description of Actions for this charm (eg actions.yaml)
"""
meta = yaml.safe_load(metadata)
meta = _loadYaml(metadata)
raw_actions = {}
if actions is not None:
raw_actions = yaml.safe_load(actions)
raw_actions = _loadYaml(actions)
if raw_actions is None:
raw_actions = {}
return cls(meta, raw_actions)
class RelationRole(enum.Enum):
"""An annotation for a charm's role in a relation.
For each relation a charm's role may be
- A Peer
- A service consumer in the relation ('requires')
- A service provider in the relation ('provides')
"""
peer = 'peer'
requires = 'requires'
provides = 'provides'
def is_peer(self) -> bool:
"""Return whether the current role is peer.
A convenience to avoid having to import charm.
"""
return self is RelationRole.peer
class RelationMeta:
"""Object containing metadata about a relation definition.
Should not be constructed directly by Charm code. Is gotten from one of
Should not be constructed directly by charm code. Is gotten from one of
:attr:`CharmMeta.peers`, :attr:`CharmMeta.requires`, :attr:`CharmMeta.provides`,
:attr:`CharmMeta.relations`.
or :attr:`CharmMeta.relations`.
Attributes:
role: This is one of requires/provides/peers
role: This is :class:`RelationRole`; one of peer/requires/provides
relation_name: Name of this relation from metadata.yaml
interface_name: Optional definition of the interface protocol.
scope: "global" or "container" scope based on how the relation should be used.
"""
def __init__(self, role, relation_name, raw):
def __init__(self, role: RelationRole, relation_name: str, raw: dict):
if not isinstance(role, RelationRole):
raise TypeError("role should be a Role, not {!r}".format(role))
self.role = role
self.relation_name = relation_name
self.interface_name = raw['interface']
......@@ -512,7 +752,17 @@ class RelationMeta:
class StorageMeta:
"""Object containing metadata about a storage definition."""
"""Object containing metadata about a storage definition.
Attributes:
storage_name: Name of storage
type: Storage type
description: A text description of the storage
read_only: Whether or not the storage is read only
minimum_size: Minimum size of storage
location: Mount point of storage
multiple_range: Range of numeric qualifiers when multiple storage units are used
"""
def __init__(self, name, raw):
self.storage_name = name
......@@ -533,7 +783,13 @@ class StorageMeta:
class ResourceMeta:
"""Object containing metadata about a resource definition."""
"""Object containing metadata about a resource definition.
Attributes:
resource_name: Name of resource
filename: Name of file
description: A text description of resource
"""
def __init__(self, name, raw):
self.resource_name = name
......@@ -543,7 +799,12 @@ class ResourceMeta:
class PayloadMeta:
"""Object containing metadata about a payload definition."""
"""Object containing metadata about a payload definition.
Attributes:
payload_name: Name of payload
type: Payload type
"""
def __init__(self, name, raw):
self.payload_name = name
......
# Copyright 2019-2020 Canonical Ltd.
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -12,22 +12,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Operator Framework infrastructure."""
import collections
import collections.abc
import inspect
import keyword
import logging
import marshal
import os
import pathlib
import pdb
import pickle
import re
import sqlite3
import sys
import types
import weakref
from datetime import timedelta
from ops import charm
from ops.storage import (
NoSnapshotError,
SQLiteStorage,
)
logger = logging.getLogger(__name__)
class Handle:
......@@ -62,6 +69,7 @@ class Handle:
self._path = "{}".format(kind)
def nest(self, kind, key):
"""Create a new handle as child of the current one."""
return Handle(self, kind, key)
def __hash__(self):
......@@ -75,22 +83,27 @@ class Handle:
@property
def parent(self):
"""Return own parent handle."""
return self._parent
@property
def kind(self):
"""Return the handle's kind."""
return self._kind
@property
def key(self):
"""Return the handle's key."""
return self._key
@property
def path(self):
"""Return the handle's path."""
return self._path
@classmethod
def from_path(cls, path):
"""Build a handle from the indicated path."""
handle = None
for pair in path.split("/"):
pair = pair.split("[")
......@@ -110,12 +123,69 @@ class Handle:
class EventBase:
"""The base for all the different Events.
Inherit this and override 'snapshot' and 'restore' methods to build a custom event.
"""
def __init__(self, handle):
self.handle = handle
self.deferred = False
def __repr__(self):
return "<%s via %s>" % (self.__class__.__name__, self.handle)
def defer(self):
"""Defer the event to the future.
Deferring an event from a handler puts that handler into a queue, to be
called again the next time the charm is invoked. This invocation may be
the result of an action, or any event other than metric events. The
queue of events will be dispatched before the new event is processed.
From the above you may deduce, but it's important to point out:
* ``defer()`` does not interrupt the execution of the current event
handler. In almost all cases, a call to ``defer()`` should be followed
by an explicit ``return`` from the handler;
* the re-execution of the deferred event handler starts from the top of
the handler method (not where defer was called);
* only the handlers that actually called ``defer()`` are called again
(that is: despite talking about “deferring an event” it is actually
the handler/event combination that is deferred); and
* any deferred events get processed before the event (or action) that
caused the current invocation of the charm.
The general desire to call ``defer()`` happens when some precondition
isn't yet met. However, care should be exercised as to whether it is
better to defer this event so that you see it again, or whether it is
better to just wait for the event that indicates the precondition has
been met.
For example, if ``config-changed`` is fired, and you are waiting for
different config, there is no reason to defer the event because there
will be a *different* ``config-changed`` event when the config actually
changes, rather than checking to see if maybe config has changed prior
to every other event that occurs.
Similarly, if you need 2 events to occur before you are ready to
proceed (say event A and B). When you see event A, you could chose to
``defer()`` it because you haven't seen B yet. However, that leads to:
1. event A fires, calls defer()
2. event B fires, event A handler is called first, still hasn't seen B
happen, so is deferred again. Then B happens, which progresses since
it has seen A.
3. At some future time, event C happens, which also checks if A can
proceed.
"""
logger.debug("Deferring %s.", self)
self.deferred = True
def snapshot(self):
......@@ -182,6 +252,7 @@ class EventSource:
class BoundEvent:
"""Event bound to an Object."""
def __repr__(self):
return '<BoundEvent {} bound to {}.{} at {}>'.format(
......@@ -254,6 +325,7 @@ class _Metaclass(type):
class Object(metaclass=_Metaclass):
"""Base class of all the charm-related objects."""
handle_kind = HandleKind()
......@@ -274,6 +346,7 @@ class Object(metaclass=_Metaclass):
@property
def model(self):
"""Shortcut for more simple access the model."""
return self.framework.model
......@@ -327,26 +400,32 @@ class ObjectEvents(Object):
event_descriptor._set_name(cls, event_kind)
setattr(cls, event_kind, event_descriptor)
def events(self):
"""Return a mapping of event_kinds to bound_events for all available events.
"""
events_map = {}
def _event_kinds(self):
event_kinds = []
# We have to iterate over the class rather than instance to allow for properties which
# might call this method (e.g., event views), leading to infinite recursion.
for attr_name, attr_value in inspect.getmembers(type(self)):
if isinstance(attr_value, EventSource):
# We actually care about the bound_event, however, since it
# provides the most info for users of this method.
event_kind = attr_name
bound_event = getattr(self, event_kind)
events_map[event_kind] = bound_event
return events_map
event_kinds.append(attr_name)
return event_kinds
def events(self):
"""Return a mapping of event_kinds to bound_events for all available events."""
return {event_kind: getattr(self, event_kind) for event_kind in self._event_kinds()}
def __getitem__(self, key):
return PrefixedEvents(self, key)
def __repr__(self):
k = type(self)
event_kinds = ', '.join(sorted(self._event_kinds()))
return '<{}.{}: {}>'.format(k.__module__, k.__qualname__, event_kinds)
class PrefixedEvents:
"""Events to be found in all events using a specific prefix."""
def __init__(self, emitter, key):
self._emitter = emitter
......@@ -357,28 +436,21 @@ class PrefixedEvents:
class PreCommitEvent(EventBase):
pass
"""Events that will be emited first on commit."""
class CommitEvent(EventBase):
pass
"""Events that will be emited second on commit."""
class FrameworkEvents(ObjectEvents):
"""Manager of all framework events."""
pre_commit = EventSource(PreCommitEvent)
commit = EventSource(CommitEvent)
class NoSnapshotError(Exception):
def __init__(self, handle_path):
self.handle_path = handle_path
def __str__(self):
return 'no snapshot data found for {} object'.format(self.handle_path)
class NoTypeError(Exception):
"""No class to hold it was found when restoring an event."""
def __init__(self, handle_path):
self.handle_path = handle_path
......@@ -387,97 +459,6 @@ class NoTypeError(Exception):
return "cannot restore {} since no class was registered for it".format(self.handle_path)
class SQLiteStorage:
DB_LOCK_TIMEOUT = timedelta(hours=1)
def __init__(self, filename):
# The isolation_level argument is set to None such that the implicit
# transaction management behavior of the sqlite3 module is disabled.
self._db = sqlite3.connect(str(filename),
isolation_level=None,
timeout=self.DB_LOCK_TIMEOUT.total_seconds())
self._setup()
def _setup(self):
# Make sure that the database is locked until the connection is closed,
# not until the transaction ends.
self._db.execute("PRAGMA locking_mode=EXCLUSIVE")
c = self._db.execute("BEGIN")
c.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='snapshot'")
if c.fetchone()[0] == 0:
# Keep in mind what might happen if the process dies somewhere below.
# The system must not be rendered permanently broken by that.
self._db.execute("CREATE TABLE snapshot (handle TEXT PRIMARY KEY, data BLOB)")
self._db.execute('''
CREATE TABLE notice (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
event_path TEXT,
observer_path TEXT,
method_name TEXT)
''')
self._db.commit()
def close(self):
self._db.close()
def commit(self):
self._db.commit()
# There's commit but no rollback. For abort to be supported, we'll need logic that
# can rollback decisions made by third-party code in terms of the internal state
# of objects that have been snapshotted, and hooks to let them know about it and
# take the needed actions to undo their logic until the last snapshot.
# This is doable but will increase significantly the chances for mistakes.
def save_snapshot(self, handle_path, snapshot_data):
self._db.execute("REPLACE INTO snapshot VALUES (?, ?)", (handle_path, snapshot_data))
def load_snapshot(self, handle_path):
c = self._db.cursor()
c.execute("SELECT data FROM snapshot WHERE handle=?", (handle_path,))
row = c.fetchone()
if row:
return row[0]
return None
def drop_snapshot(self, handle_path):
self._db.execute("DELETE FROM snapshot WHERE handle=?", (handle_path,))
def save_notice(self, event_path, observer_path, method_name):
self._db.execute('INSERT INTO notice VALUES (NULL, ?, ?, ?)',
(event_path, observer_path, method_name))
def drop_notice(self, event_path, observer_path, method_name):
self._db.execute('''
DELETE FROM notice
WHERE event_path=?
AND observer_path=?
AND method_name=?
''', (event_path, observer_path, method_name))
def notices(self, event_path):
if event_path:
c = self._db.execute('''
SELECT event_path, observer_path, method_name
FROM notice
WHERE event_path=?
ORDER BY sequence
''', (event_path,))
else:
c = self._db.execute('''
SELECT event_path, observer_path, method_name
FROM notice
ORDER BY sequence
''')
while True:
rows = c.fetchmany()
if not rows:
break
for row in rows:
yield tuple(row)
# the message to show to the user when a pdb breakpoint goes active
_BREAKPOINT_WELCOME_MESSAGE = """
Starting pdb to debug charm operator.
......@@ -488,7 +469,11 @@ More details at https://discourse.jujucharms.com/t/debugging-charm-hooks
"""
_event_regex = r'^(|.*/)on/[a-zA-Z_]+\[\d+\]$'
class Framework(Object):
"""Main interface to from the Charm to the Operator Framework internals."""
on = FrameworkEvents()
......@@ -497,11 +482,10 @@ class Framework(Object):
meta = None
charm_dir = None
def __init__(self, data_path, charm_dir, meta, model):
def __init__(self, storage, charm_dir, meta, model):
super().__init__(self, None)
self._data_path = data_path
self.charm_dir = charm_dir
self.meta = meta
self.model = model
......@@ -511,7 +495,11 @@ class Framework(Object):
self._type_registry = {} # {(parent_path, kind): cls}
self._type_known = set() # {cls}
self._storage = SQLiteStorage(data_path)
if isinstance(storage, (str, pathlib.Path)):
logger.warning(
"deprecated: Framework now takes a Storage not a path")
storage = SQLiteStorage(storage)
self._storage = storage
# We can't use the higher-level StoredState because it relies on events.
self.register_type(StoredStateData, None, StoredStateData.handle_kind)
......@@ -522,18 +510,36 @@ class Framework(Object):
self._stored = StoredStateData(self, '_stored')
self._stored['event_count'] = 0
# Hook into builtin breakpoint, so if Python >= 3.7, devs will be able to just do
# breakpoint(); if Python < 3.7, this doesn't affect anything
sys.breakpointhook = self.breakpoint
# Flag to indicate that we already presented the welcome message in a debugger breakpoint
self._breakpoint_welcomed = False
# Parse once the env var, which may be used multiple times later
# Parse the env var once, which may be used multiple times later
debug_at = os.environ.get('JUJU_DEBUG_AT')
self._juju_debug_at = debug_at.split(',') if debug_at else ()
def set_breakpointhook(self):
"""Hook into sys.breakpointhook so the builtin breakpoint() works as expected.
This method is called by ``main``, and is not intended to be
called by users of the framework itself outside of perhaps
some testing scenarios.
It returns the old value of sys.excepthook.
The breakpoint function is a Python >= 3.7 feature.
This method was added in ops 1.0; before that, it was done as
part of the Framework's __init__.
"""
old_breakpointhook = getattr(sys, 'breakpointhook', None)
if old_breakpointhook is not None:
# Hook into builtin breakpoint, so if Python >= 3.7, devs will be able to just do
# breakpoint()
sys.breakpointhook = self.breakpoint
return old_breakpointhook
def close(self):
"""Close the underlying backends."""
self._storage.close()
def _track(self, obj):
......@@ -551,6 +557,7 @@ class Framework(Object):
self._objects.pop(obj.handle.path, None)
def commit(self):
"""Save changes to the underlying backends."""
# Give a chance for objects to persist data they want to before a commit is made.
self.on.pre_commit.emit()
# Make sure snapshots are saved by instances of StoredStateData. Any possible state
......@@ -561,6 +568,7 @@ class Framework(Object):
self._storage.commit()
def register_type(self, cls, parent, kind=None):
"""Register a type to a handle."""
if parent and not isinstance(parent, Handle):
parent = parent.handle
if parent:
......@@ -597,21 +605,17 @@ class Framework(Object):
msg = "unable to save the data for {}, it must contain only simple types: {!r}"
raise ValueError(msg.format(value.__class__.__name__, data))
# Use pickle for serialization, so the value remains portable.
raw_data = pickle.dumps(data)
self._storage.save_snapshot(value.handle.path, raw_data)
self._storage.save_snapshot(value.handle.path, data)
def load_snapshot(self, handle):
"""Load a persistent snapshot."""
parent_path = None
if handle.parent:
parent_path = handle.parent.path
cls = self._type_registry.get((parent_path, handle.kind))
if not cls:
raise NoTypeError(handle.path)
raw_data = self._storage.load_snapshot(handle.path)
if not raw_data:
raise NoSnapshotError(handle.path)
data = pickle.loads(raw_data)
data = self._storage.load_snapshot(handle.path)
obj = cls.__new__(cls)
obj.framework = self
obj.handle = handle
......@@ -620,31 +624,39 @@ class Framework(Object):
return obj
def drop_snapshot(self, handle):
"""Discard a persistent snapshot."""
self._storage.drop_snapshot(handle.path)
def observe(self, bound_event, observer):
def observe(self, bound_event: BoundEvent, observer: types.MethodType):
"""Register observer to be called when bound_event is emitted.
The bound_event is generally provided as an attribute of the object that emits
the event, and is created in this style:
the event, and is created in this style::
class SomeObject:
something_happened = Event(SomethingHappened)
That event may be observed as:
That event may be observed as::
framework.observe(someobj.something_happened, self.on_something_happened)
If the method to be called follows the name convention "on_<event name>", it
may be omitted from the observe call. That means the above is equivalent to:
framework.observe(someobj.something_happened, self)
framework.observe(someobj.something_happened, self._on_something_happened)
Raises:
RuntimeError: if bound_event or observer are the wrong type.
"""
if not isinstance(bound_event, BoundEvent):
raise RuntimeError(
'Framework.observe requires a BoundEvent as second parameter, got {}'.format(
bound_event))
if not isinstance(observer, types.MethodType):
# help users of older versions of the framework
if isinstance(observer, charm.CharmBase):
raise TypeError(
'observer methods must now be explicitly provided;'
' please replace observe(self.on.{0}, self)'
' with e.g. observe(self.on.{0}, self._on_{0})'.format(
bound_event.event_kind))
raise RuntimeError(
'Framework.observe requires a method as third parameter, got {}'.format(observer))
event_type = bound_event.event_type
event_kind = bound_event.event_kind
......@@ -658,22 +670,13 @@ class Framework(Object):
raise RuntimeError(
'event emitter {} must have a "handle" attribute'.format(type(emitter).__name__))
method_name = None
if isinstance(observer, types.MethodType):
method_name = observer.__name__
observer = observer.__self__
else:
method_name = "on_" + event_kind
if not hasattr(observer, method_name):
raise RuntimeError(
'Observer method not provided explicitly'
' and {} type has no "{}" method'.format(type(observer).__name__,
method_name))
# Validate that the method has an acceptable call signature.
sig = inspect.signature(getattr(observer, method_name))
sig = inspect.signature(observer)
# Self isn't included in the params list, so the first arg will be the event.
extra_params = list(sig.parameters.values())[1:]
method_name = observer.__name__
observer = observer.__self__
if not sig.parameters:
raise TypeError(
'{}.{} must accept event parameter'.format(type(observer).__name__, method_name))
......@@ -697,10 +700,7 @@ class Framework(Object):
def _emit(self, event):
"""See BoundEvent.emit for the public way to call this."""
# Save the event for all known observers before the first notification
# takes place, so that either everyone interested sees it, or nobody does.
self.save_snapshot(event)
saved = False
event_path = event.handle.path
event_kind = event.handle.kind
parent_path = event.handle.parent.path
......@@ -711,9 +711,15 @@ class Framework(Object):
continue
if _event_kind and _event_kind != event_kind:
continue
if not saved:
# Save the event for all known observers before the first notification
# takes place, so that either everyone interested sees it, or nobody does.
self.save_snapshot(event)
saved = True
# Again, only commit this after all notices are saved.
self._storage.save_notice(event_path, observer_path, method_name)
self._reemit(event_path)
if saved:
self._reemit(event_path)
def reemit(self):
"""Reemit previously deferred events to the observers that deferred them.
......@@ -732,7 +738,7 @@ class Framework(Object):
event_handle = Handle.from_path(event_path)
if last_event_path != event_path:
if not deferred:
if not deferred and last_event_path is not None:
self._storage.drop_snapshot(last_event_path)
last_event_path = event_path
deferred = False
......@@ -746,6 +752,8 @@ class Framework(Object):
event.deferred = False
observer = self._observer.get(observer_path)
if observer:
if single_event_path is None:
logger.debug("Re-emitting %s.", event)
custom_handler = getattr(observer, method_name, None)
if custom_handler:
event_is_from_juju = isinstance(event, charm.HookEvent)
......@@ -766,7 +774,7 @@ class Framework(Object):
# scratch in the next path.
self.framework._forget(event)
if not deferred:
if not deferred and last_event_path is not None:
self._storage.drop_snapshot(last_event_path)
def _show_debug_code_message(self):
......@@ -797,6 +805,9 @@ class Framework(Object):
raise ValueError('breakpoint names must look like "foo" or "foo-bar"')
indicated_breakpoints = self._juju_debug_at
if not indicated_breakpoints:
return
if 'all' in indicated_breakpoints or name in indicated_breakpoints:
self._show_debug_code_message()
......@@ -804,9 +815,32 @@ class Framework(Object):
# it to use our caller's frame
code_frame = inspect.currentframe().f_back
pdb.Pdb().set_trace(code_frame)
else:
logger.warning(
"Breakpoint %r skipped (not found in the requested breakpoints: %s)",
name, indicated_breakpoints)
def remove_unreferenced_events(self):
"""Remove events from storage that are not referenced.
In older versions of the framework, events that had no observers would get recorded but
never deleted. This makes a best effort to find these events and remove them from the
database.
"""
event_regex = re.compile(_event_regex)
to_remove = []
for handle_path in self._storage.list_snapshots():
if event_regex.match(handle_path):
notices = self._storage.notices(handle_path)
if next(notices, None) is None:
# There are no notices for this handle_path, it is valid to remove it
to_remove.append(handle_path)
for handle_path in to_remove:
self._storage.drop_snapshot(handle_path)
class StoredStateData(Object):
"""Manager of the stored data."""
def __init__(self, parent, attr_name):
super().__init__(parent, attr_name)
......@@ -824,19 +858,23 @@ class StoredStateData(Object):
return key in self._cache
def snapshot(self):
"""Return the current state."""
return self._cache
def restore(self, snapshot):
"""Restore current state to the given snapshot."""
self._cache = snapshot
self.dirty = False
def on_commit(self, event):
"""Save changes to the storage backend."""
if self.dirty:
self.framework.save_snapshot(self)
self.dirty = False
class BoundStoredState:
"""Stored state data bound to a specific Object."""
def __init__(self, parent, attr_name):
parent.framework.register_type(StoredStateData, parent)
......@@ -851,7 +889,7 @@ class BoundStoredState:
self.__dict__["_data"] = data
self.__dict__["_attr_name"] = attr_name
parent.framework.observe(parent.framework.on.commit, self._data)
parent.framework.observe(parent.framework.on.commit, self._data.on_commit)
def __getattr__(self, key):
# "on" is the only reserved key that can't be used in the data map.
......@@ -875,7 +913,7 @@ class BoundStoredState:
self._data[key] = _unwrap_stored(self._data, value)
def set_default(self, **kwargs):
""""Set the value of any given key if it has not already been set"""
"""Set the value of any given key if it has not already been set."""
for k, v in kwargs.items():
if k not in self._data:
self._data[k] = v
......@@ -974,7 +1012,16 @@ def _unwrap_stored(parent_data, value):
return value
def _wrapped_repr(obj):
t = type(obj)
if obj._under:
return "{}.{}({!r})".format(t.__module__, t.__name__, obj._under)
else:
return "{}.{}()".format(t.__module__, t.__name__)
class StoredDict(collections.abc.MutableMapping):
"""A dict-like object that uses the StoredState as backend."""
def __init__(self, stored_data, under):
self._stored_data = stored_data
......@@ -1005,8 +1052,11 @@ class StoredDict(collections.abc.MutableMapping):
else:
return NotImplemented
__repr__ = _wrapped_repr
class StoredList(collections.abc.MutableSequence):
"""A list-like object that uses the StoredState as backend."""
def __init__(self, stored_data, under):
self._stored_data = stored_data
......@@ -1027,10 +1077,12 @@ class StoredList(collections.abc.MutableSequence):
return len(self._under)
def insert(self, index, value):
"""Insert value before index."""
self._under.insert(index, value)
self._stored_data.dirty = True
def append(self, value):
"""Append value to the end of the list."""
self._under.append(value)
self._stored_data.dirty = True
......@@ -1074,18 +1126,29 @@ class StoredList(collections.abc.MutableSequence):
else:
return NotImplemented
__repr__ = _wrapped_repr
class StoredSet(collections.abc.MutableSet):
"""A set-like object that uses the StoredState as backend."""
def __init__(self, stored_data, under):
self._stored_data = stored_data
self._under = under
def add(self, key):
"""Add a key to a set.
This has no effect if the key is already present.
"""
self._under.add(key)
self._stored_data.dirty = True
def discard(self, key):
"""Remove a key from a set if it is a member.
If the key is not a member, do nothing.
"""
self._under.discard(key)
self._stored_data.dirty = True
......@@ -1132,3 +1195,5 @@ class StoredSet(collections.abc.MutableSet):
return self._under == other
else:
return NotImplemented
__repr__ = _wrapped_repr
......@@ -12,12 +12,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""A helper to work with the Juju version."""
import os
import re
from functools import total_ordering
@total_ordering
class JujuVersion:
"""Helper to work with the Juju version.
It knows how to parse the ``JUJU_VERSION`` environment variable, and exposes different
capabilities according to the specific version, allowing also to compare with other
versions.
"""
PATTERN = r'''^
(?P<major>\d{1,9})\.(?P<minor>\d{1,9}) # <major> and <minor> numbers are always there
......@@ -83,3 +92,23 @@ class JujuVersion:
elif self.build != other.build:
return self.build < other.build
return False
@classmethod
def from_environ(cls) -> 'JujuVersion':
"""Build a JujuVersion from JUJU_VERSION."""
v = os.environ.get('JUJU_VERSION')
if v is None:
v = '0.0.0'
return cls(v)
def has_app_data(self) -> bool:
"""Determine whether this juju version knows about app data."""
return (self.major, self.minor, self.patch) >= (2, 7, 0)
def is_dispatch_aware(self) -> bool:
"""Determine whether this juju version knows about dispatch."""
return (self.major, self.minor, self.patch) >= (2, 8, 0)
def has_controller_storage(self) -> bool:
"""Determine whether this juju version supports controller-side storage."""
return (self.major, self.minor, self.patch) >= (2, 8, 0)
# Copyright 2020 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.
"""Infrastructure for the opslib functionality."""
import logging
import os
import re
import sys
from ast import literal_eval
from importlib.util import module_from_spec
from importlib.machinery import ModuleSpec
from pkgutil import get_importer
from types import ModuleType
from typing import List
__all__ = ('use', 'autoimport')
logger = logging.getLogger(__name__)
_libraries = None
_libline_re = re.compile(r'''^LIB([A-Z]+)\s*=\s*([0-9]+|['"][a-zA-Z0-9_.\-@]+['"])''')
_libname_re = re.compile(r'''^[a-z][a-z0-9]+$''')
# Not perfect, but should do for now.
_libauthor_re = re.compile(r'''^[A-Za-z0-9_+.-]+@[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.[a-z]{2,3}$''')
def use(name: str, api: int, author: str) -> ModuleType:
"""Use a library from the ops libraries.
Args:
name: the name of the library requested.
api: the API version of the library.
author: the author of the library. If not given, requests the
one in the standard library.
Raises:
ImportError: if the library cannot be found.
TypeError: if the name, api, or author are the wrong type.
ValueError: if the name, api, or author are invalid.
"""
if not isinstance(name, str):
raise TypeError("invalid library name: {!r} (must be a str)".format(name))
if not isinstance(author, str):
raise TypeError("invalid library author: {!r} (must be a str)".format(author))
if not isinstance(api, int):
raise TypeError("invalid library API: {!r} (must be an int)".format(api))
if api < 0:
raise ValueError('invalid library api: {} (must be ≥0)'.format(api))
if not _libname_re.match(name):
raise ValueError("invalid library name: {!r} (chars and digits only)".format(name))
if not _libauthor_re.match(author):
raise ValueError("invalid library author email: {!r}".format(author))
if _libraries is None:
autoimport()
versions = _libraries.get((name, author), ())
for lib in versions:
if lib.api == api:
return lib.import_module()
others = ', '.join(str(lib.api) for lib in versions)
if others:
msg = 'cannot find "{}" from "{}" with API version {} (have {})'.format(
name, author, api, others)
else:
msg = 'cannot find library "{}" from "{}"'.format(name, author)
raise ImportError(msg, name=name)
def autoimport():
"""Find all libs in the path and enable use of them.
You only need to call this if you've installed a package or
otherwise changed sys.path in the current run, and need to see the
changes. Otherwise libraries are found on first call of `use`.
"""
global _libraries
_libraries = {}
for spec in _find_all_specs(sys.path):
lib = _parse_lib(spec)
if lib is None:
continue
versions = _libraries.setdefault((lib.name, lib.author), [])
versions.append(lib)
versions.sort(reverse=True)
def _find_all_specs(path):
for sys_dir in path:
if sys_dir == "":
sys_dir = "."
try:
top_dirs = os.listdir(sys_dir)
except (FileNotFoundError, NotADirectoryError):
continue
except OSError as e:
logger.debug("Tried to look for ops.lib packages under '%s': %s", sys_dir, e)
continue
logger.debug("Looking for ops.lib packages under '%s'", sys_dir)
for top_dir in top_dirs:
opslib = os.path.join(sys_dir, top_dir, 'opslib')
try:
lib_dirs = os.listdir(opslib)
except (FileNotFoundError, NotADirectoryError):
continue
except OSError as e:
logger.debug(" Tried '%s': %s", opslib, e) # *lots* of things checked here
continue
else:
logger.debug(" Trying '%s'", opslib)
finder = get_importer(opslib)
if finder is None:
logger.debug(" Finder for '%s' is None", opslib)
continue
if not hasattr(finder, 'find_spec'):
logger.debug(" Finder for '%s' has no find_spec", opslib)
continue
for lib_dir in lib_dirs:
spec_name = "{}.opslib.{}".format(top_dir, lib_dir)
spec = finder.find_spec(spec_name)
if spec is None:
logger.debug(" No spec for %r", spec_name)
continue
if spec.loader is None:
# a namespace package; not supported
logger.debug(" No loader for %r (probably a namespace package)", spec_name)
continue
logger.debug(" Found %r", spec_name)
yield spec
# only the first this many lines of a file are looked at for the LIB* constants
_MAX_LIB_LINES = 99
# these keys, with these types, are needed to have an opslib
_NEEDED_KEYS = {'NAME': str, 'AUTHOR': str, 'API': int, 'PATCH': int}
def _join_and(keys: List[str]) -> str:
if len(keys) == 0:
return ""
if len(keys) == 1:
return keys[0]
return ", ".join(keys[:-1]) + ", and " + keys[-1]
class _Missing:
"""Helper to get the difference between what was found and what was needed when logging."""
def __init__(self, found):
self._found = found
def __str__(self):
exp = set(_NEEDED_KEYS)
got = set(self._found)
if len(got) == 0:
return "missing {}".format(_join_and(sorted(exp)))
return "got {}, but missing {}".format(
_join_and(sorted(got)),
_join_and(sorted(exp - got)))
def _parse_lib(spec):
if spec.origin is None:
# "can't happen"
logger.warning("No origin for %r (no idea why; please report)", spec.name)
return None
logger.debug(" Parsing %r", spec.name)
try:
with open(spec.origin, 'rt', encoding='utf-8') as f:
libinfo = {}
for n, line in enumerate(f):
if len(libinfo) == len(_NEEDED_KEYS):
break
if n > _MAX_LIB_LINES:
logger.debug(
" Missing opslib metadata after reading to line %d: %s",
_MAX_LIB_LINES, _Missing(libinfo))
return None
m = _libline_re.match(line)
if m is None:
continue
key, value = m.groups()
if key in _NEEDED_KEYS:
value = literal_eval(value)
if not isinstance(value, _NEEDED_KEYS[key]):
logger.debug(
" Bad type for %s: expected %s, got %s",
key, _NEEDED_KEYS[key].__name__, type(value).__name__)
return None
libinfo[key] = value
else:
if len(libinfo) != len(_NEEDED_KEYS):
logger.debug(
" Missing opslib metadata after reading to end of file: %s",
_Missing(libinfo))
return None
except Exception as e:
logger.debug(" Failed: %s", e)
return None
lib = _Lib(spec, libinfo['NAME'], libinfo['AUTHOR'], libinfo['API'], libinfo['PATCH'])
logger.debug(" Success: found library %s", lib)
return lib
class _Lib:
def __init__(self, spec: ModuleSpec, name: str, author: str, api: int, patch: int):
self.spec = spec
self.name = name
self.author = author
self.api = api
self.patch = patch
self._module = None
def __repr__(self):
return "<_Lib {}>".format(self)
def __str__(self):
return "{0.name} by {0.author}, API {0.api}, patch {0.patch}".format(self)
def import_module(self) -> ModuleType:
if self._module is None:
module = module_from_spec(self.spec)
self.spec.loader.exec_module(module)
self._module = module
return self._module
def __eq__(self, other):
if not isinstance(other, _Lib):
return NotImplemented
a = (self.name, self.author, self.api, self.patch)
b = (other.name, other.author, other.api, other.patch)
return a == b
def __lt__(self, other):
if not isinstance(other, _Lib):
return NotImplemented
a = (self.name, self.author, self.api, self.patch)
b = (other.name, other.author, other.api, other.patch)
return a < b
......@@ -12,6 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Interface to emit messages to the Juju logging system."""
import sys
import logging
......@@ -23,6 +26,11 @@ class JujuLogHandler(logging.Handler):
self.model_backend = model_backend
def emit(self, record):
"""Send the specified logging record to the Juju backend.
This method is not used directly by the Operator Framework code, but by
:class:`logging.Handler` itself as part of the logging machinery.
"""
self.model_backend.juju_log(record.levelname, self.format(record))
......@@ -45,3 +53,6 @@ def setup_root_logging(model_backend, debug=False):
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
sys.excepthook = lambda etype, value, tb: logger.error(
"Uncaught exception while in charm code:", exc_info=(etype, value, tb))
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment